diff --git a/vault/identity_store.go b/vault/identity_store.go index c297549fce..c4c43c7d65 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -67,22 +67,23 @@ func (i *IdentityStore) resetDB() error { func NewIdentityStore(ctx context.Context, core *Core, config *logical.BackendConfig, logger log.Logger) (*IdentityStore, error) { iStore := &IdentityStore{ - view: config.StorageView, - logger: logger, - router: core.router, - redirectAddr: core.redirectAddr, - localNode: core, - namespacer: core, - metrics: core.MetricSink(), - totpPersister: core, - groupUpdater: core, - tokenStorer: core, - entityCreator: core, - mountLister: core, - mfaBackend: core.loginMFABackend, - aliasLocks: locksutil.CreateLocks(), - activationManager: core.FeatureActivationFlags, - activationErrorHandler: core, + view: config.StorageView, + logger: logger, + router: core.router, + redirectAddr: core.redirectAddr, + localNode: core, + namespacer: core, + metrics: core.MetricSink(), + totpPersister: core, + groupUpdater: core, + tokenStorer: core, + entityCreator: core, + mountLister: core, + syntheticAliasAccessorValidator: core, + mfaBackend: core.loginMFABackend, + aliasLocks: locksutil.CreateLocks(), + activationManager: core.FeatureActivationFlags, + activationErrorHandler: core, } // Create a memdb instance, which by default, operates on lower cased diff --git a/vault/identity_store_aliases.go b/vault/identity_store_aliases.go index e2b7db272a..065bbe6132 100644 --- a/vault/identity_store_aliases.go +++ b/vault/identity_store_aliases.go @@ -136,6 +136,42 @@ This field is deprecated, use canonical_id.`, } } +// validateAliasMountAccessor validates mount_accessor values for entity aliases. +// +// It accepts either a real mounted backend accessor or a supported synthetic +// accessor validated by the synthetic alias accessor validator extension point. +// +// For mounted backend accessors, this returns the matched mount entry. For +// synthetic accessors, this returns a minimal entry carrying namespace/local +// semantics used by alias create/update checks. +func (i *IdentityStore) validateAliasMountAccessor(ctx context.Context, mountAccessor string) (*MountEntry, error) { + if mountAccessor == "" { + return nil, fmt.Errorf("invalid mount accessor %q", mountAccessor) + } + if mountEntry := i.router.MatchingMountByAccessor(mountAccessor); mountEntry != nil { + return mountEntry, nil + } + + if i.syntheticAliasAccessorValidator == nil { + i.logger.Error("synthetic alias accessor validator is not configured", "mount_accessor", mountAccessor) + return nil, fmt.Errorf("failed to validate mount accessor %q due to internal configuration error", mountAccessor) + } + + valid, err := i.syntheticAliasAccessorValidator.validateSyntheticAliasAccessor(ctx, mountAccessor) + if err != nil { + return nil, err + } + if !valid { + return nil, fmt.Errorf("invalid mount accessor %q", mountAccessor) + } + + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, err + } + return &MountEntry{NamespaceID: ns.ID}, nil +} + func aliasFieldSchema() map[string]*framework.FieldSchema { return map[string]*framework.FieldSchema{ "id": { @@ -284,15 +320,15 @@ func (i *IdentityStore) handleAliasCreateUpdate() framework.OperationFunc { return logical.ErrorResponse("'id' or 'mount_accessor' and 'name' must be provided"), nil } - mountEntry := i.router.MatchingMountByAccessor(mountAccessor) - if mountEntry == nil { - return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil + mountEntry, err := i.validateAliasMountAccessor(ctx, mountAccessor) + if err != nil { + return logical.ErrorResponse(err.Error()), nil } - if mountEntry.NamespaceID != ns.ID { + if mountEntry != nil && mountEntry.NamespaceID != ns.ID { return logical.ErrorResponse("matching mount is in a different namespace than request"), logical.ErrPermissionDenied } - localMount := mountEntry.Local + localMount := mountEntry != nil && mountEntry.Local // Look up the alias by factors; if it's found it's an update return i.handleAliasCreateUpdateCommon(ctx, ns, mountAccessor, name, canonicalID, externalID, issuer, customMetadata, localMount, "") @@ -497,11 +533,11 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name !strutil.EqualStringMaps(customMetadata, alias.CustomMetadata) || issuer != alias.Issuer || externalID != alias.ExternalID { // Check here to see if such an alias already exists, if so bail - mountEntry := i.router.MatchingMountByAccessor(mountAccessor) - if mountEntry == nil { - return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil + mountEntry, err := i.validateAliasMountAccessor(ctx, mountAccessor) + if err != nil { + return logical.ErrorResponse(err.Error()), nil } - if mountEntry.NamespaceID != alias.NamespaceID { + if mountEntry != nil && mountEntry.NamespaceID != alias.NamespaceID { return logical.ErrorResponse("given mount accessor is not in the same namespace as the existing alias"), logical.ErrPermissionDenied } @@ -536,15 +572,16 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name alias.CustomMetadata = customMetadata } - mountValidationResp := i.router.ValidateMountByAccessor(alias.MountAccessor) - if mountValidationResp == nil { - return nil, fmt.Errorf("invalid mount accessor %q", alias.MountAccessor) + mountEntry, err := i.validateAliasMountAccessor(ctx, alias.MountAccessor) + if err != nil { + return nil, err } + mountIsLocal := mountEntry != nil && mountEntry.Local newEntity := currentEntity if canonicalID != "" && canonicalID != alias.CanonicalID { // Don't allow moving local aliases between entities. - if mountValidationResp.MountLocal { + if mountIsLocal { return logical.ErrorResponse("local aliases can't be moved between entities"), nil } @@ -590,11 +627,11 @@ func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name currentEntity = nil } - if mountValidationResp.MountLocal { + if mountIsLocal { alias, err = i.processLocalAlias(ctx, &logical.Alias{ MountAccessor: mountAccessor, Name: name, - Local: mountValidationResp.MountLocal, + Local: mountIsLocal, CustomMetadata: customMetadata, Issuer: issuer, ExternalID: externalID, diff --git a/vault/identity_store_aliases_stubs_oss.go b/vault/identity_store_aliases_stubs_oss.go new file mode 100644 index 0000000000..396f910aa1 --- /dev/null +++ b/vault/identity_store_aliases_stubs_oss.go @@ -0,0 +1,12 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import "context" + +func (c *Core) validateSyntheticAliasAccessor(context.Context, string) (bool, error) { + return false, nil +} diff --git a/vault/identity_store_structs.go b/vault/identity_store_structs.go index 55ea947594..0e405082c9 100644 --- a/vault/identity_store_structs.go +++ b/vault/identity_store_structs.go @@ -104,17 +104,18 @@ type IdentityStore struct { // operated case insensitively disableLowerCasedNames bool - router *Router - redirectAddr string - localNode LocalNode - namespacer Namespacer - metrics metricsutil.Metrics - totpPersister TOTPPersister - groupUpdater GroupUpdater - tokenStorer TokenStorer - entityCreator EntityCreator - mountLister MountLister - mfaBackend *LoginMFABackend + router *Router + redirectAddr string + localNode LocalNode + namespacer Namespacer + metrics metricsutil.Metrics + totpPersister TOTPPersister + groupUpdater GroupUpdater + tokenStorer TokenStorer + entityCreator EntityCreator + mountLister MountLister + syntheticAliasAccessorValidator SyntheticAliasAccessorValidator + mfaBackend *LoginMFABackend // aliasLocks is used to protect modifications to alias entries based on the uniqueness factor // which is name + accessor @@ -196,6 +197,12 @@ type MountLister interface { var _ MountLister = &Core{} +type SyntheticAliasAccessorValidator interface { + validateSyntheticAliasAccessor(context.Context, string) (bool, error) +} + +var _ SyntheticAliasAccessorValidator = &Core{} + type Sealer interface { Shutdown() error }