mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
* identity: allow oauth profile alias accessors Allow identity/entity-alias mount_accessor to use sys/config/oauth-resource-server/<profile> when the profile exists in the request namespace, while preserving existing mount accessor and namespace checks for real mounts. Add focused identity alias tests for valid profile accessor acceptance and unknown profile rejection. * identity: document alias accessor validation cases Add GoDoc for validateAliasMountAccessor to clarify supported mount_accessor validation for auth-method aliases and OAuth/External JWT profile-style aliases. * identity: use namespace+configid oauth alias accessor Implement synthetic OAuth alias mount_accessor format as oauth_resource_server_<namespace_id>_<config_id> and validate by namespace and config ID for identity/entity-alias. Add stable config_id to OAuth resource-server profiles, expose it on profile read responses, and add compatibility hydration for older stored profiles missing config_id. Update identity alias tests for new accessor encoding and add cross-namespace rejection coverage. * oauth: persist legacy profile config ids on read Backfill missing OAuth Resource Server profile config_id under profile lock and persist it so config_id remains stable for synthetic identity alias accessors. Update config-id lookup to resolve profiles through the read path so legacy entries are migrated before matching. Add regression test covering legacy no-config_id profile migration and successful alias creation with migrated accessor. * identity: clarify oauth profile existence check Document that getOAuthResourceServerConfigProfileByConfigID is used only to verify the referenced OAuth profile exists during synthetic mount_accessor validation. * oauth: add config-id index for O(1) lookup Add profiles-by-config-id storage index and switch getOAuthResourceServerConfigProfileByConfigID to index-based resolution to avoid O(N) profile scans during alias accessor validation. Persist index entries on profile upsert, clean them up on delete, and keep legacy config_id backfill path consistent with indexed storage. Add regression tests for indexed lookup, missing-index behavior, and index cleanup on delete. * vault: isolate oauth alias validation by build tag * vault: move oauth accessor constants to enterprise file * vault: tighten alias accessor validation returns * vault: require oauth profile config_id on read * vault: redact oauth profile identifiers in logs * vault: remove oauth profile identifiers from logs * vault: harden oauth log redaction paths * vault: fix oauth invalidation replicated-path test fixture * vault: remove sensitive error payloads from oauth logs * Address PR review feedback for logging and tests - restore operational error logging in OAuth invalidation/read/delete paths - improve nil synthetic alias validator diagnostics with explicit log + internal error - move config_id index tests from core-based vault tests to external NewTestCluster tests - export GetOAuthResourceServerConfigProfileByConfigID for external coverage * Apply review feedback for alias validator nil case - include mount_accessor context in operational log when synthetic validator is nil - return accessor-specific internal configuration error for easier troubleshooting * Consolidate OAuth config_id tests into existing storage test file - move config_id index coverage into oauth_resource_storage_ent_test.go - remove standalone oauth_resource_config_id_index_ent_test.go * Apply review nit for accessor prefix constant - trim oauthResourceServerAliasAccessorPrefix to remove trailing underscore - build synthetic accessor using explicit separator concatenation * tests: migrate oauth alias accessor coverage to external * identity: switch oauth synthetic accessor prefix to hyphenated --------- Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
28a1f595c5
commit
b98004d1dc
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
12
vault/identity_store_aliases_stubs_oss.go
Normal file
12
vault/identity_store_aliases_stubs_oss.go
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user