Vault 4632 auth remount oss (#14141)
* Update plugin-portal.mdx (#13229) Add a Vault plugin to allow authentication via SSH certificates and public keys * oss changes Co-authored-by: Wim <wim@42.be>
This commit is contained in:
parent
f0dc3a553f
commit
475b55b460
|
@ -2,8 +2,10 @@ package http
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
|
@ -433,3 +435,130 @@ func TestSysTuneAuth_showUIMount(t *testing.T) {
|
|||
t.Fatalf("bad:\nExpected: %#v\nActual:%#v", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysRemountAuth(t *testing.T) {
|
||||
core, _, token := vault.TestCoreUnsealed(t)
|
||||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
TestServerAuth(t, addr, token)
|
||||
|
||||
resp := testHttpPost(t, token, addr+"/v1/sys/auth/foo", map[string]interface{}{
|
||||
"type": "noop",
|
||||
"description": "foo",
|
||||
})
|
||||
testResponseStatus(t, resp, 204)
|
||||
|
||||
resp = testHttpPost(t, token, addr+"/v1/sys/remount", map[string]interface{}{
|
||||
"from": "auth/foo",
|
||||
"to": "auth/bar",
|
||||
})
|
||||
testResponseStatus(t, resp, 200)
|
||||
|
||||
// Poll until the remount succeeds
|
||||
var remountResp map[string]interface{}
|
||||
testResponseBody(t, resp, &remountResp)
|
||||
vault.RetryUntil(t, 5*time.Second, func() error {
|
||||
resp = testHttpGet(t, token, addr+"/v1/sys/remount/status/"+remountResp["migration_id"].(string))
|
||||
testResponseStatus(t, resp, 200)
|
||||
|
||||
var remountStatusResp map[string]interface{}
|
||||
testResponseBody(t, resp, &remountStatusResp)
|
||||
|
||||
status := remountStatusResp["data"].(map[string]interface{})["migration_info"].(map[string]interface{})["status"]
|
||||
if status != "success" {
|
||||
return fmt.Errorf("Expected migration status to be successful, got %q", status)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
resp = testHttpGet(t, token, addr+"/v1/sys/auth")
|
||||
|
||||
var actual map[string]interface{}
|
||||
expected := map[string]interface{}{
|
||||
"lease_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": json.Number("0"),
|
||||
"wrap_info": nil,
|
||||
"warnings": nil,
|
||||
"auth": nil,
|
||||
"data": map[string]interface{}{
|
||||
"bar/": map[string]interface{}{
|
||||
"description": "foo",
|
||||
"type": "noop",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"token_type": "default-service",
|
||||
"force_no_cache": false,
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
"options": map[string]interface{}{},
|
||||
},
|
||||
"token/": map[string]interface{}{
|
||||
"description": "token based credentials",
|
||||
"type": "token",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"force_no_cache": false,
|
||||
"token_type": "default-service",
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
"options": interface{}(nil),
|
||||
},
|
||||
},
|
||||
"bar/": map[string]interface{}{
|
||||
"description": "foo",
|
||||
"type": "noop",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"token_type": "default-service",
|
||||
"force_no_cache": false,
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
"options": map[string]interface{}{},
|
||||
},
|
||||
"token/": map[string]interface{}{
|
||||
"description": "token based credentials",
|
||||
"type": "token",
|
||||
"external_entropy_access": false,
|
||||
"config": map[string]interface{}{
|
||||
"default_lease_ttl": json.Number("0"),
|
||||
"max_lease_ttl": json.Number("0"),
|
||||
"token_type": "default-service",
|
||||
"force_no_cache": false,
|
||||
},
|
||||
"local": false,
|
||||
"seal_wrap": false,
|
||||
"options": interface{}(nil),
|
||||
},
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
|
||||
expected["request_id"] = actual["request_id"]
|
||||
for k, v := range actual["data"].(map[string]interface{}) {
|
||||
if v.(map[string]interface{})["accessor"] == "" {
|
||||
t.Fatalf("no accessor from %s", k)
|
||||
}
|
||||
if v.(map[string]interface{})["uuid"] == "" {
|
||||
t.Fatalf("no uuid from %s", k)
|
||||
}
|
||||
|
||||
expected[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"]
|
||||
expected[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"]
|
||||
expected["data"].(map[string]interface{})[k].(map[string]interface{})["accessor"] = v.(map[string]interface{})["accessor"]
|
||||
expected["data"].(map[string]interface{})[k].(map[string]interface{})["uuid"] = v.(map[string]interface{})["uuid"]
|
||||
}
|
||||
|
||||
if diff := deep.Equal(actual, expected); diff != nil {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
}
|
||||
|
|
115
vault/auth.go
115
vault/auth.go
|
@ -44,6 +44,11 @@ var (
|
|||
// credentialAliases maps old backend names to new backend names, allowing us
|
||||
// to move/rename backends but maintain backwards compatibility
|
||||
credentialAliases = map[string]string{"aws-ec2": "aws"}
|
||||
|
||||
// protectedAuths marks auth mounts that are protected and cannot be remounted
|
||||
protectedAuths = []string{
|
||||
"auth/token",
|
||||
}
|
||||
)
|
||||
|
||||
// enableCredential is used to enable a new credential backend
|
||||
|
@ -274,7 +279,7 @@ func (c *Core) disableCredentialInternal(ctx context.Context, path string, updat
|
|||
entry := c.router.MatchingMountEntry(ctx, path)
|
||||
|
||||
// Mark the entry as tainted
|
||||
if err := c.taintCredEntry(ctx, path, updateStorage); err != nil {
|
||||
if err := c.taintCredEntry(ctx, ns.ID, path, updateStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -387,6 +392,103 @@ func (c *Core) removeCredEntry(ctx context.Context, path string, updateStorage b
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Core) remountCredential(ctx context.Context, src, dst namespace.MountPathDetails, updateStorage bool) error {
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(src.MountPath, credentialRoutePrefix) {
|
||||
return fmt.Errorf("cannot remount non-auth mount %q", src.MountPath)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(dst.MountPath, credentialRoutePrefix) {
|
||||
return fmt.Errorf("cannot remount auth mount to non-auth mount %q", dst.MountPath)
|
||||
}
|
||||
|
||||
for _, auth := range protectedAuths {
|
||||
if strings.HasPrefix(src.MountPath, auth) {
|
||||
return fmt.Errorf("cannot remount %q", src.MountPath)
|
||||
}
|
||||
}
|
||||
|
||||
for _, auth := range protectedAuths {
|
||||
if strings.HasPrefix(dst.MountPath, auth) {
|
||||
return fmt.Errorf("cannot remount to %q", dst.MountPath)
|
||||
}
|
||||
}
|
||||
|
||||
srcRelativePath := src.GetRelativePath(ns)
|
||||
dstRelativePath := dst.GetRelativePath(ns)
|
||||
|
||||
// Verify exact match of the route
|
||||
srcMatch := c.router.MatchingMountEntry(ctx, srcRelativePath)
|
||||
if srcMatch == nil {
|
||||
return fmt.Errorf("no matching mount at %q", src.Namespace.Path+src.MountPath)
|
||||
}
|
||||
|
||||
if match := c.router.MountConflict(ctx, dstRelativePath); match != "" {
|
||||
return fmt.Errorf("path in use at %q", match)
|
||||
}
|
||||
|
||||
// Mark the entry as tainted
|
||||
if err := c.taintCredEntry(ctx, src.Namespace.ID, src.MountPath, updateStorage); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Taint the router path to prevent routing
|
||||
if err := c.router.Taint(ctx, srcRelativePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.expiration != nil {
|
||||
revokeCtx := namespace.ContextWithNamespace(ctx, src.Namespace)
|
||||
// Revoke all the dynamic keys
|
||||
if err := c.expiration.RevokePrefix(revokeCtx, src.MountPath, true); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.authLock.Lock()
|
||||
if match := c.router.MountConflict(ctx, dstRelativePath); match != "" {
|
||||
c.authLock.Unlock()
|
||||
return fmt.Errorf("path in use at %q", match)
|
||||
}
|
||||
|
||||
srcMatch.Tainted = false
|
||||
srcMatch.NamespaceID = dst.Namespace.ID
|
||||
srcMatch.namespace = dst.Namespace
|
||||
srcPath := srcMatch.Path
|
||||
srcMatch.Path = strings.TrimPrefix(dst.MountPath, credentialRoutePrefix)
|
||||
|
||||
// Update the mount table
|
||||
if err := c.persistAuth(ctx, c.auth, &srcMatch.Local); err != nil {
|
||||
srcMatch.Path = srcPath
|
||||
srcMatch.Tainted = true
|
||||
c.authLock.Unlock()
|
||||
if err == logical.ErrReadOnly && c.perfStandby {
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to update auth table with error %+v", err)
|
||||
}
|
||||
|
||||
// Remount the backend, setting the existing route entry
|
||||
// against the new path
|
||||
if err := c.router.Remount(ctx, srcRelativePath, dstRelativePath); err != nil {
|
||||
c.authLock.Unlock()
|
||||
return err
|
||||
}
|
||||
c.authLock.Unlock()
|
||||
|
||||
// Un-taint the new path in the router
|
||||
if err := c.router.Untaint(ctx, dstRelativePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// remountCredEntryForceInternal takes a copy of the mount entry for the path and fully
|
||||
// unmounts and remounts the backend to pick up any changes, such as filtered
|
||||
// paths. This should be only used internal.
|
||||
|
@ -420,26 +522,21 @@ func (c *Core) remountCredEntryForceInternal(ctx context.Context, path string, u
|
|||
}
|
||||
|
||||
// taintCredEntry is used to mark an entry in the auth table as tainted
|
||||
func (c *Core) taintCredEntry(ctx context.Context, path string, updateStorage bool) error {
|
||||
func (c *Core) taintCredEntry(ctx context.Context, nsID, path string, updateStorage bool) error {
|
||||
c.authLock.Lock()
|
||||
defer c.authLock.Unlock()
|
||||
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Taint the entry from the auth table
|
||||
// We do this on the original since setting the taint operates
|
||||
// on the entries which a shallow clone shares anyways
|
||||
entry, err := c.auth.setTaint(ns.ID, strings.TrimPrefix(path, credentialRoutePrefix), true, mountStateUnmounting)
|
||||
entry, err := c.auth.setTaint(nsID, strings.TrimPrefix(path, credentialRoutePrefix), true, mountStateUnmounting)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure there was a match
|
||||
if entry == nil {
|
||||
return fmt.Errorf("no matching backend")
|
||||
return fmt.Errorf("no matching backend for path %q namespaceID %q", path, nsID)
|
||||
}
|
||||
|
||||
if updateStorage {
|
||||
|
|
|
@ -580,3 +580,166 @@ func TestCore_CredentialInitialize(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func remountCredentialFromRoot(c *Core, src, dst string, updateStorage bool) error {
|
||||
srcPathDetails := c.splitNamespaceAndMountFromPath("", src)
|
||||
dstPathDetails := c.splitNamespaceAndMountFromPath("", dst)
|
||||
return c.remountCredential(namespace.RootContext(nil), srcPathDetails, dstPathDetails, updateStorage)
|
||||
}
|
||||
|
||||
func TestCore_RemountCredential(t *testing.T) {
|
||||
c, keys, _ := TestCoreUnsealed(t)
|
||||
c.credentialBackends["noop"] = func(context.Context, *logical.BackendConfig) (logical.Backend, error) {
|
||||
return &NoopBackend{
|
||||
BackendType: logical.TypeCredential,
|
||||
}, nil
|
||||
}
|
||||
|
||||
me := &MountEntry{
|
||||
Table: credentialTableType,
|
||||
Path: "foo",
|
||||
Type: "noop",
|
||||
}
|
||||
err := c.enableCredential(namespace.RootContext(nil), me)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
match := c.router.MatchingMount(namespace.RootContext(nil), "auth/foo/bar")
|
||||
if match != "auth/foo/" {
|
||||
t.Fatalf("missing mount, match: %q", match)
|
||||
}
|
||||
|
||||
err = remountCredentialFromRoot(c, "auth/foo", "auth/bar", true)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
match = c.router.MatchingMount(namespace.RootContext(nil), "auth/bar/baz")
|
||||
if match != "auth/bar/" {
|
||||
t.Fatalf("auth method not at new location, match: %q", match)
|
||||
}
|
||||
|
||||
c.sealInternal()
|
||||
for i, key := range keys {
|
||||
unseal, err := TestCoreUnseal(c, key)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if i+1 == len(keys) && !unseal {
|
||||
t.Fatalf("should be unsealed")
|
||||
}
|
||||
}
|
||||
|
||||
match = c.router.MatchingMount(namespace.RootContext(nil), "auth/bar/baz")
|
||||
if match != "auth/bar/" {
|
||||
t.Fatalf("auth method not at new location after unseal, match: %q", match)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCore_RemountCredential_Cleanup(t *testing.T) {
|
||||
noop := &NoopBackend{
|
||||
Login: []string{"login"},
|
||||
BackendType: logical.TypeCredential,
|
||||
}
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
c.credentialBackends["noop"] = func(context.Context, *logical.BackendConfig) (logical.Backend, error) {
|
||||
return noop, nil
|
||||
}
|
||||
|
||||
me := &MountEntry{
|
||||
Table: credentialTableType,
|
||||
Path: "foo",
|
||||
Type: "noop",
|
||||
}
|
||||
err := c.enableCredential(namespace.RootContext(nil), me)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Store the view
|
||||
view := c.router.MatchingStorageByAPIPath(namespace.RootContext(nil), "auth/foo/")
|
||||
|
||||
// Inject data
|
||||
se := &logical.StorageEntry{
|
||||
Key: "plstodelete",
|
||||
Value: []byte("test"),
|
||||
}
|
||||
if err := view.Put(context.Background(), se); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Generate a new token auth
|
||||
noop.Response = &logical.Response{
|
||||
Auth: &logical.Auth{
|
||||
Policies: []string{"foo"},
|
||||
},
|
||||
}
|
||||
r := &logical.Request{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "auth/foo/login",
|
||||
}
|
||||
resp, err := c.HandleRequest(namespace.RootContext(nil), r)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if resp.Auth.ClientToken == "" {
|
||||
t.Fatalf("bad: %#v", resp)
|
||||
}
|
||||
|
||||
// Disable should cleanup
|
||||
err = remountCredentialFromRoot(c, "auth/foo", "auth/bar", true)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
// Token should be revoked
|
||||
te, err := c.tokenStore.Lookup(namespace.RootContext(nil), resp.Auth.ClientToken)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if te != nil {
|
||||
t.Fatalf("bad: %#v", te)
|
||||
}
|
||||
|
||||
// View should be empty
|
||||
out, err := logical.CollectKeys(context.Background(), view)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if len(out) != 1 && out[0] != "plstokeep" {
|
||||
t.Fatalf("bad: %#v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCore_RemountCredential_InvalidSource(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
err := remountCredentialFromRoot(c, "foo", "auth/bar", true)
|
||||
if err.Error() != `cannot remount non-auth mount "foo/"` {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCore_RemountCredential_InvalidDestination(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
err := remountCredentialFromRoot(c, "auth/foo", "bar", true)
|
||||
if err.Error() != `cannot remount auth mount to non-auth mount "bar/"` {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCore_RemountCredential_ProtectedSource(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
err := remountCredentialFromRoot(c, "auth/token", "auth/bar", true)
|
||||
if err.Error() != `cannot remount "auth/token/"` {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCore_RemountCredential_ProtectedDestination(t *testing.T) {
|
||||
c, _, _ := TestCoreUnsealed(t)
|
||||
err := remountCredentialFromRoot(c, "auth/foo", "auth/token", true)
|
||||
if err.Error() != `cannot remount to "auth/token/"` {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1208,14 +1208,33 @@ func (b *SystemBackend) handleRemount(ctx context.Context, req *logical.Request,
|
|||
return handleError(fmt.Errorf("invalid destination mount: %v", err))
|
||||
}
|
||||
|
||||
// Prevent target and source mounts from being in a protected path
|
||||
for _, p := range protectedMounts {
|
||||
if strings.HasPrefix(fromPathDetails.MountPath, p) {
|
||||
return handleError(fmt.Errorf("cannot remount %q", fromPathDetails.MountPath))
|
||||
// Check that target is a valid auth mount, if source is an auth mount
|
||||
if strings.HasPrefix(fromPathDetails.MountPath, credentialRoutePrefix) {
|
||||
if !strings.HasPrefix(toPathDetails.MountPath, credentialRoutePrefix) {
|
||||
return handleError(fmt.Errorf("cannot remount auth mount to non-auth mount %q", toPathDetails.MountPath))
|
||||
}
|
||||
// Prevent target and source auth mounts from being in a protected path
|
||||
for _, auth := range protectedAuths {
|
||||
if strings.HasPrefix(fromPathDetails.MountPath, auth) {
|
||||
return handleError(fmt.Errorf("cannot remount %q", fromPathDetails.MountPath))
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(toPathDetails.MountPath, p) {
|
||||
return handleError(fmt.Errorf("cannot remount to destination %+v", toPathDetails.MountPath))
|
||||
for _, auth := range protectedAuths {
|
||||
if strings.HasPrefix(toPathDetails.MountPath, auth) {
|
||||
return handleError(fmt.Errorf("cannot remount to destination %q", toPathDetails.MountPath))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Prevent target and source non-auth mounts from being in a protected path
|
||||
for _, p := range protectedMounts {
|
||||
if strings.HasPrefix(fromPathDetails.MountPath, p) {
|
||||
return handleError(fmt.Errorf("cannot remount %q", fromPathDetails.MountPath))
|
||||
}
|
||||
|
||||
if strings.HasPrefix(toPathDetails.MountPath, p) {
|
||||
return handleError(fmt.Errorf("cannot remount to destination %+v", toPathDetails.MountPath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1225,9 +1244,10 @@ func (b *SystemBackend) handleRemount(ctx context.Context, req *logical.Request,
|
|||
return handleError(fmt.Errorf("no matching mount at %q", sanitizePath(fromPath)))
|
||||
}
|
||||
|
||||
if match := b.Core.router.MatchingMount(ctx, toPath); match != "" {
|
||||
return handleError(fmt.Errorf("existing mount at %q", match))
|
||||
if match := b.Core.router.MountConflict(ctx, sanitizePath(toPath)); match != "" {
|
||||
return handleError(fmt.Errorf("path already in use at %q", match))
|
||||
}
|
||||
|
||||
// If we are a performance secondary cluster we should forward the request
|
||||
// to the primary. We fail early here since the view in use isn't marked as
|
||||
// readonly
|
||||
|
@ -1246,12 +1266,7 @@ func (b *SystemBackend) handleRemount(ctx context.Context, req *logical.Request,
|
|||
|
||||
logger := b.Core.Logger().Named("mounts.migration").With("migration_id", migrationID, "namespace", ns.Path, "to_path", toPath, "from_path", fromPath)
|
||||
|
||||
var err error
|
||||
if !strings.Contains(fromPath, "auth") {
|
||||
err = b.moveSecretsEngine(ns, logger, migrationID, entry.ViewPath(), fromPathDetails, toPathDetails)
|
||||
} else {
|
||||
logger.Error("Remount is unsupported for the source mount", "err", err)
|
||||
}
|
||||
err := b.moveMount(ns, logger, migrationID, entry, fromPathDetails, toPathDetails)
|
||||
if err != nil {
|
||||
logger.Error("remount failed", "error", err)
|
||||
if err := b.Core.setMigrationStatus(migrationID, MigrationFailureStatus); err != nil {
|
||||
|
@ -1269,14 +1284,25 @@ func (b *SystemBackend) handleRemount(ctx context.Context, req *logical.Request,
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// moveSecretsEngine carries out a remount operation on the secrets engine, updating the migration status as required
|
||||
// moveMount carries out a remount operation on the secrets engine or auth method, updating the migration status as required
|
||||
// It is expected to be called asynchronously outside of a request context, hence it creates a context derived from the active one
|
||||
// and intermittently checks to see if it is still open.
|
||||
func (b *SystemBackend) moveSecretsEngine(ns *namespace.Namespace, logger log.Logger, migrationID, viewPath string, fromPathDetails, toPathDetails namespace.MountPathDetails) error {
|
||||
func (b *SystemBackend) moveMount(ns *namespace.Namespace, logger log.Logger, migrationID string, entry *MountEntry, fromPathDetails, toPathDetails namespace.MountPathDetails) error {
|
||||
logger.Info("Starting to update the mount table and revoke leases")
|
||||
revokeCtx := namespace.ContextWithNamespace(b.Core.activeContext, ns)
|
||||
|
||||
var err error
|
||||
// Attempt remount
|
||||
if err := b.Core.remountSecretsEngine(revokeCtx, fromPathDetails, toPathDetails, !b.Core.perfStandby); err != nil {
|
||||
switch entry.Table {
|
||||
case credentialTableType:
|
||||
err = b.Core.remountCredential(revokeCtx, fromPathDetails, toPathDetails, !b.Core.perfStandby)
|
||||
case mountTableType:
|
||||
err = b.Core.remountSecretsEngine(revokeCtx, fromPathDetails, toPathDetails, !b.Core.perfStandby)
|
||||
default:
|
||||
return fmt.Errorf("cannot remount mount of table %q", entry.Table)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1286,7 +1312,7 @@ func (b *SystemBackend) moveSecretsEngine(ns *namespace.Namespace, logger log.Lo
|
|||
|
||||
logger.Info("Removing the source mount from filtered paths on secondaries")
|
||||
// Remove from filtered mounts and restart evaluation process
|
||||
if err := b.Core.removePathFromFilteredPaths(revokeCtx, fromPathDetails.GetFullPath(), viewPath); err != nil {
|
||||
if err := b.Core.removePathFromFilteredPaths(revokeCtx, fromPathDetails.GetFullPath(), entry.ViewPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/go-test/deep"
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||
"github.com/hashicorp/vault/helper/builtinplugins"
|
||||
"github.com/hashicorp/vault/helper/identity"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
|
@ -683,6 +684,175 @@ func TestSystemBackend_CapabilitiesAccessor_BC(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSystemBackend_remount_auth(t *testing.T) {
|
||||
err := AddTestCredentialBackend("userpass", credUserpass.Factory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c, b, _ := testCoreSystemBackend(t)
|
||||
|
||||
userpassMe := &MountEntry{
|
||||
Table: credentialTableType,
|
||||
Path: "userpass1/",
|
||||
Type: "userpass",
|
||||
Description: "userpass",
|
||||
}
|
||||
err = c.enableCredential(namespace.RootContext(nil), userpassMe)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "remount")
|
||||
req.Data["from"] = "auth/userpass1"
|
||||
req.Data["to"] = "auth/userpass2"
|
||||
req.Data["config"] = structs.Map(MountConfig{})
|
||||
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
RetryUntil(t, 5*time.Second, func() error {
|
||||
req = logical.TestRequest(t, logical.ReadOperation, fmt.Sprintf("remount/status/%s", resp.Data["migration_id"]))
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
migrationInfo := resp.Data["migration_info"].(*MountMigrationInfo)
|
||||
if migrationInfo.MigrationStatus != MigrationSuccessStatus.String() {
|
||||
return fmt.Errorf("Expected migration status to be successful, got %q", migrationInfo.MigrationStatus)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestSystemBackend_remount_auth_invalid(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "remount")
|
||||
req.Data["from"] = "auth/unknown"
|
||||
req.Data["to"] = "auth/foo"
|
||||
req.Data["config"] = structs.Map(MountConfig{})
|
||||
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Data["error"].(string), "no matching mount at \"auth/unknown/\"") {
|
||||
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
req.Data["to"] = "foo"
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Data["error"].(string), "cannot remount auth mount to non-auth mount \"foo/\"") {
|
||||
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemBackend_remount_auth_protected(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "remount")
|
||||
req.Data["from"] = "auth/token"
|
||||
req.Data["to"] = "auth/foo"
|
||||
req.Data["config"] = structs.Map(MountConfig{})
|
||||
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Data["error"].(string), "cannot remount \"auth/token/\"") {
|
||||
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
req.Data["from"] = "auth/foo"
|
||||
req.Data["to"] = "auth/token"
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Data["error"].(string), "cannot remount to destination \"auth/token/\"") {
|
||||
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemBackend_remount_auth_destinationInUse(t *testing.T) {
|
||||
err := AddTestCredentialBackend("userpass", credUserpass.Factory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c, b, _ := testCoreSystemBackend(t)
|
||||
|
||||
userpassMe := &MountEntry{
|
||||
Table: credentialTableType,
|
||||
Path: "userpass1/",
|
||||
Type: "userpass",
|
||||
Description: "userpass",
|
||||
}
|
||||
err = c.enableCredential(namespace.RootContext(nil), userpassMe)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
userpassMe2 := &MountEntry{
|
||||
Table: credentialTableType,
|
||||
Path: "userpass2/",
|
||||
Type: "userpass",
|
||||
Description: "userpass",
|
||||
}
|
||||
err = c.enableCredential(namespace.RootContext(nil), userpassMe2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := logical.TestRequest(t, logical.UpdateOperation, "remount")
|
||||
req.Data["from"] = "auth/userpass1"
|
||||
req.Data["to"] = "auth/userpass2"
|
||||
req.Data["config"] = structs.Map(MountConfig{})
|
||||
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Data["error"].(string), "path already in use at \"auth/userpass2/\"") {
|
||||
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
req.Data["to"] = "auth/userpass2/mypass"
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Data["error"].(string), "path already in use at \"auth/userpass2/\"") {
|
||||
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||
}
|
||||
|
||||
userpassMe3 := &MountEntry{
|
||||
Table: credentialTableType,
|
||||
Path: "userpass3/mypass/",
|
||||
Type: "userpass",
|
||||
Description: "userpass",
|
||||
}
|
||||
err = c.enableCredential(namespace.RootContext(nil), userpassMe3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req.Data["to"] = "auth/userpass3/"
|
||||
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Data["error"].(string), "path already in use at \"auth/userpass3/mypass/\"") {
|
||||
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemBackend_remount(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
|
||||
|
|
|
@ -903,11 +903,11 @@ func (c *Core) remountSecretsEngine(ctx context.Context, src, dst namespace.Moun
|
|||
// Verify exact match of the route
|
||||
srcMatch := c.router.MatchingMountEntry(ctx, srcRelativePath)
|
||||
if srcMatch == nil {
|
||||
return fmt.Errorf("no matching mount at %+v", src)
|
||||
return fmt.Errorf("no matching mount at %q", src.Namespace.Path+src.MountPath)
|
||||
}
|
||||
|
||||
if match := c.router.MatchingMount(ctx, dstRelativePath); match != "" {
|
||||
return fmt.Errorf("existing mount at %q", match)
|
||||
if match := c.router.MountConflict(ctx, dstRelativePath); match != "" {
|
||||
return fmt.Errorf("path in use at %q", match)
|
||||
}
|
||||
|
||||
// Mark the entry as tainted
|
||||
|
@ -937,9 +937,9 @@ func (c *Core) remountSecretsEngine(ctx context.Context, src, dst namespace.Moun
|
|||
}
|
||||
|
||||
c.mountsLock.Lock()
|
||||
if match := c.router.MatchingMount(ctx, dstRelativePath); match != "" {
|
||||
if match := c.router.MountConflict(ctx, dstRelativePath); match != "" {
|
||||
c.mountsLock.Unlock()
|
||||
return fmt.Errorf("existing mount at %q", match)
|
||||
return fmt.Errorf("path in use at %q", match)
|
||||
}
|
||||
|
||||
srcMatch.Tainted = false
|
||||
|
@ -957,8 +957,7 @@ func (c *Core) remountSecretsEngine(ctx context.Context, src, dst namespace.Moun
|
|||
return err
|
||||
}
|
||||
|
||||
c.logger.Error("failed to update mounts table", "error", err)
|
||||
return logical.CodedError(500, "failed to update mounts table")
|
||||
return fmt.Errorf("failed to update mount table with error %+v", err)
|
||||
}
|
||||
|
||||
// Remount the backend
|
||||
|
@ -973,7 +972,6 @@ func (c *Core) remountSecretsEngine(ctx context.Context, src, dst namespace.Moun
|
|||
return err
|
||||
}
|
||||
|
||||
c.logger.Info("successful remount", "old_path", src, "new_path", dst)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -445,63 +444,6 @@ func testCore_Unmount_Cleanup(t *testing.T, causeFailure bool) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCore_RemountConcurrent(t *testing.T) {
|
||||
c2, _, _ := TestCoreUnsealed(t)
|
||||
noop := &NoopBackend{}
|
||||
c2.logicalBackends["noop"] = func(context.Context, *logical.BackendConfig) (logical.Backend, error) {
|
||||
return noop, nil
|
||||
}
|
||||
|
||||
// Mount the noop backend
|
||||
mount1 := &MountEntry{
|
||||
Table: mountTableType,
|
||||
Path: "test1/",
|
||||
Type: "noop",
|
||||
}
|
||||
|
||||
if err := c2.mount(namespace.RootContext(nil), mount1); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
mount2 := &MountEntry{
|
||||
Table: mountTableType,
|
||||
Path: "test2/",
|
||||
Type: "noop",
|
||||
}
|
||||
if err := c2.mount(namespace.RootContext(nil), mount2); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := c2.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "test1", "foo", true)
|
||||
if err != nil {
|
||||
t.Logf("err: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := c2.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "test2", "foo", true)
|
||||
if err != nil {
|
||||
t.Logf("err: %v", err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
c2MountMap := map[string]interface{}{}
|
||||
for _, v := range c2.mounts.Entries {
|
||||
|
||||
if _, ok := c2MountMap[v.Path]; ok {
|
||||
t.Fatalf("duplicated mount path found at %s", v.Path)
|
||||
}
|
||||
c2MountMap[v.Path] = v
|
||||
}
|
||||
}
|
||||
|
||||
func TestCore_Remount(t *testing.T) {
|
||||
c, keys, _ := TestCoreUnsealed(t)
|
||||
err := c.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "secret", "foo", true)
|
||||
|
@ -514,21 +456,9 @@ func TestCore_Remount(t *testing.T) {
|
|||
t.Fatalf("failed remount")
|
||||
}
|
||||
|
||||
inmemSink := metrics.NewInmemSink(1000000*time.Hour, 2000000*time.Hour)
|
||||
conf := &CoreConfig{
|
||||
Physical: c.physical,
|
||||
DisableMlock: true,
|
||||
BuiltinRegistry: NewMockBuiltinRegistry(),
|
||||
MetricSink: metricsutil.NewClusterMetricSink("test-cluster", inmemSink),
|
||||
MetricsHelper: metricsutil.NewMetricsHelper(inmemSink, false),
|
||||
}
|
||||
c2, err := NewCore(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
defer c2.Shutdown()
|
||||
c.sealInternal()
|
||||
for i, key := range keys {
|
||||
unseal, err := TestCoreUnseal(c2, key)
|
||||
unseal, err := TestCoreUnseal(c, key)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
@ -537,20 +467,9 @@ func TestCore_Remount(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// Verify matching mount tables
|
||||
if c.mounts.Type != c2.mounts.Type {
|
||||
t.Fatal("types don't match")
|
||||
}
|
||||
cMountMap := map[string]interface{}{}
|
||||
for _, v := range c.mounts.Entries {
|
||||
cMountMap[v.Path] = v
|
||||
}
|
||||
c2MountMap := map[string]interface{}{}
|
||||
for _, v := range c2.mounts.Entries {
|
||||
c2MountMap[v.Path] = v
|
||||
}
|
||||
if diff := deep.Equal(cMountMap, c2MountMap); diff != nil {
|
||||
t.Fatal(diff)
|
||||
match = c.router.MatchingMount(namespace.RootContext(nil), "foo/bar")
|
||||
if match != "foo/" {
|
||||
t.Fatalf("failed remount")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue