From f5dbe55f55433876e4cee1aa66cba2d02617f433 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 5 Mar 2026 10:25:59 -0500 Subject: [PATCH] VAULT-42657: Merge feature branch for OAuth and Agent Registry into main (#12587) (#12754) * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * [POC] Accept and Validate External OAuth Token for Agent Identity (#10991) * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * Add JWT token tidy cleanup functionality * Refactor JWT tidy tests to use retryUntil instead of time.Sleep * feat(identity)(Accept OAuth JWT tokens): fix leaky lease * feat(identity)(Accept OAuth JWT tokens): fix leaky lease II and add claims to TE * Add TestLogicalWithJwtAndSCIM e2e test * feat(identity)(Accept OAuth JWT tokens): add more tests * feat(identity)(Accept OAuth JWT tokens): change email * feat(identity)(Accept OAuth JWT tokens): e2e test for scim * Accept and validate RAR (#11005) * fix(identity)(Accept OAuth JWT tokens): add missing headers --------- * wip * wip * wip * feat(identity): auth with delegation jwts * feat(identity): optimize jwks fetching * wip * add lookup by entity-id * feat(identity): optimize jwks fetching * feat(identity): optimize jwks fetching and refactoring * feat(identity): fix breaking tests * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * [POC] Accept and Validate External OAuth Token for Agent Identity (#10991) * feat(identity): accept oauth tokens type: draft * feat(identity): make basic token lookup op work type: draft * fix(identity): failing tests type: draft * feat(identity): add new tests + handle renew & revoke type: draft * feat(identity): clean up and tests type: draft * feat(identity)(Accept OAuth JWT tokens): Add more tests * feat(identity)(Accept OAuth JWT tokens): Test updates * feat(identity)(Accept OAuth JWT tokens): Revert go version changes * feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests * feat(identity)(Accept OAuth JWT tokens): fix tests * feat(identity)(Accept OAuth JWT tokens): fix more CI issues * Add JWT token tidy cleanup functionality * Refactor JWT tidy tests to use retryUntil instead of time.Sleep * feat(identity)(Accept OAuth JWT tokens): fix leaky lease * feat(identity)(Accept OAuth JWT tokens): fix leaky lease II and add claims to TE * Add TestLogicalWithJwtAndSCIM e2e test * feat(identity)(Accept OAuth JWT tokens): add more tests * feat(identity)(Accept OAuth JWT tokens): change email * feat(identity)(Accept OAuth JWT tokens): e2e test for scim * Accept and validate RAR (#11005) * fix(identity)(Accept OAuth JWT tokens): add missing headers --------- * feat(identity): auth with delegation jwts * feat(identity): optimize jwks fetching * wip * wip * wip * wip * add lookup by entity-id * feat(identity): optimize jwks fetching * feat(identity): optimize jwks fetching and refactoring * feat(identity): fix breaking tests * VAULT-42642 Fix feature branch tests, some CE -> Ent moving (#12417) * VAULT-42642 Fix feathre branch tests, some CE -> Ent moving * VAULT-42642 fix last test? * skip test * proto * more test fixes * buf format * Fix createVaultEntityForUser signature (#12439) * VAULT-42533 Agent Registry: enforce entity invariants, miscellaneous improvements (#12399) * VAULT-42533 enforce entity invariants, miscellaneous improvements * typos * Improve path handling for rar + add tests (#11934) * Moving stuff around for CE * fmt * backend * more CE changes * more fixes * further fixes * rework clone * VAULT-42621 Move JWT tests (and some standalone functionality) to enterprise files (#12460) * VAULT-42621 Move JWT tests (and some standalone functionality) to enterprise files * logical * skip failing tests * more skips * missed tests * one more * VAULT-42619 Refactor token_ent and surrounding files to move logic into enterprise (#12473) * VAULT-42619 Refactor token_ent and surrounding files to move logic into enterprise * test fixes * feedback * VAULT-42631 Rename ISJWT to IsEnterpriseToken, some refactoring (#12509) * VAULT-42631 Rename ISJWT to IsEnterpriseToken, some refactoring * godocs * VAULT-42779 refactor JWT parts of acl.go to enterprise files (#12512) * VAULT-42630 move token tidy for JWT to ent files (#12534) * VAULT-42818 rename jwtjti in request struct (#12539) * VAULT-42633 CE-ify request handling and flow around validateJwtAndFetchEntity (#12541) * VAULT-42633 CE-ify request handling * Copyright headers * VAULT-42633 Move IsJWT back to CE code, add explanations (#12576) * VAULT-42633 CE-ify request handling * Copyright headers * VAULT-42633 Move IsJWT back to CE code, add explanations * VAULT-42870 Move jwtAuthManager to enterprise (#12586) * VAULT-42796 Move config to enterprise files (#12604) * VAULT-42796 Move config to enterprise files * more build failures * fix test util * two more compilatin things * VAULT42796 - fix missing return (#12617) * VAULT-42796 Move config to enterprise files * more build failures * fix test util * two more compilatin things * VAUlt-42796 fix test * Fix linting for rar_ent (#12618) * VAULT-42796 Move config to enterprise files * more build failures * fix test util * two more compilatin things * VAUlt-42796 fix test * Fix linter for rar code * fix authresults * typo * return error * fix method signature --------- Signed-off-by: Arnab Chatterjee Co-authored-by: Violet Hynes Co-authored-by: Arnab Chatterjee Co-authored-by: Arnab Chatterjee Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: davidadeleon Co-authored-by: Bianca Moreira --- sdk/helper/consts/token_consts_ce.go | 10 +++++ sdk/logical/request.go | 3 ++ sdk/logical/token.go | 8 +++- sdk/logical/token_ce.go | 10 +++++ sdk/logical/tokentype_enumer.go | 7 +-- vault/acl.go | 8 ++++ vault/acl_ce.go | 21 +++++++++ vault/request_handling.go | 65 +++++++++++++++++++++++----- vault/request_handling_ce.go | 30 +++++++++++++ vault/router.go | 4 +- vault/token_store.go | 41 +++++++++++++++--- vault/token_store_ce.go | 21 +++++++++ vault/version_store.go | 3 ++ vault/version_store_ce.go | 10 +++++ vault/wrapping.go | 10 +---- 15 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 sdk/helper/consts/token_consts_ce.go create mode 100644 sdk/logical/token_ce.go create mode 100644 vault/acl_ce.go create mode 100644 vault/request_handling_ce.go create mode 100644 vault/token_store_ce.go create mode 100644 vault/version_store_ce.go diff --git a/sdk/helper/consts/token_consts_ce.go b/sdk/helper/consts/token_consts_ce.go new file mode 100644 index 0000000000..ff9902fe0c --- /dev/null +++ b/sdk/helper/consts/token_consts_ce.go @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: MPL-2.0 + +//go:build !enterprise + +package consts + +func GetEnterpriseTokenPrefix() string { + return "unimplemented" +} diff --git a/sdk/logical/request.go b/sdk/logical/request.go index 4ba4bd4043..596d97c5db 100644 --- a/sdk/logical/request.go +++ b/sdk/logical/request.go @@ -137,6 +137,9 @@ type Request struct { // hashed. ClientToken string `json:"client_token" structs:"client_token" mapstructure:"client_token" sentinel:""` + // EnterpriseTokenMetadata is used to store metadata related to enterprise token based requests. + EnterpriseTokenMetadata string `json:"enterprise_token_metadata" structs:"enterprise_token_metadata" mapstructure:"enterprise_token_metadata" sentinel:""` + // ClientTokenAccessor is provided to the core so that the it can get // logged as part of request audit logging. ClientTokenAccessor string `json:"client_token_accessor" structs:"client_token_accessor" mapstructure:"client_token_accessor" sentinel:""` diff --git a/sdk/logical/token.go b/sdk/logical/token.go index f2caca9c9d..f1aad686f4 100644 --- a/sdk/logical/token.go +++ b/sdk/logical/token.go @@ -36,6 +36,8 @@ const ( // TokenTypeDefault is sent back by the mount, create Batch tokens TokenTypeDefaultBatch + TokenTypeEnt + // ClientIDTWEDelimiter Delimiter between the string fields used to generate a client // ID for tokens without entities. This is the 0 character, which // is a non-printable string. Please see unicode.IsPrint for details. @@ -67,6 +69,8 @@ func (t *TokenType) UnmarshalJSON(b []byte) error { *t = TokenTypeDefaultService case `"default-batch"`: *t = TokenTypeDefaultBatch + case `"ent"`: + *t = TokenTypeEnt default: return fmt.Errorf("unknown token type %q", s) } @@ -75,6 +79,8 @@ func (t *TokenType) UnmarshalJSON(b []byte) error { // TokenEntry is used to represent a given token type TokenEntry struct { + EntToken + Type TokenType `json:"type" mapstructure:"type" structs:"type" sentinel:""` // ID of this entry, generally a random UUID @@ -253,7 +259,7 @@ func (te *TokenEntry) SentinelGet(key string) (interface{}, error) { case "type": teType := te.Type switch teType { - case TokenTypeBatch, TokenTypeService: + case TokenTypeBatch, TokenTypeService, TokenTypeEnt: case TokenTypeDefault: teType = TokenTypeService default: diff --git a/sdk/logical/token_ce.go b/sdk/logical/token_ce.go new file mode 100644 index 0000000000..b5fff2a7dd --- /dev/null +++ b/sdk/logical/token_ce.go @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: MPL-2.0 + +//go:build !enterprise + +package logical + +type ( + EntToken struct{} +) diff --git a/sdk/logical/tokentype_enumer.go b/sdk/logical/tokentype_enumer.go index 9b350a74d3..39156d7cb3 100644 --- a/sdk/logical/tokentype_enumer.go +++ b/sdk/logical/tokentype_enumer.go @@ -6,9 +6,9 @@ import ( "fmt" ) -const _TokenTypeName = "defaultservicebatchdefault-servicedefault-batch" +const _TokenTypeName = "defaultservicebatchdefault-servicedefault-batchent" -var _TokenTypeIndex = [...]uint8{0, 7, 14, 19, 34, 47} +var _TokenTypeIndex = [...]uint8{0, 7, 14, 19, 34, 47, 50} func (i TokenType) String() string { if i >= TokenType(len(_TokenTypeIndex)-1) { @@ -17,7 +17,7 @@ func (i TokenType) String() string { return _TokenTypeName[_TokenTypeIndex[i]:_TokenTypeIndex[i+1]] } -var _TokenTypeValues = []TokenType{0, 1, 2, 3, 4} +var _TokenTypeValues = []TokenType{0, 1, 2, 3, 4, 5} var _TokenTypeNameToValueMap = map[string]TokenType{ _TokenTypeName[0:7]: 0, @@ -25,6 +25,7 @@ var _TokenTypeNameToValueMap = map[string]TokenType{ _TokenTypeName[14:19]: 2, _TokenTypeName[19:34]: 3, _TokenTypeName[34:47]: 4, + _TokenTypeName[47:50]: 5, } // TokenTypeString retrieves an enum value from the enum constants string name. diff --git a/vault/acl.go b/vault/acl.go index 84c6de6649..561abcd735 100644 --- a/vault/acl.go +++ b/vault/acl.go @@ -26,6 +26,8 @@ import ( // ACL is used to wrap a set of policies to provide // an efficient interface for access control. type ACL struct { + entAcl + // exactRules contains the path policies that are exact exactRules *radix.Tree @@ -49,6 +51,7 @@ type PolicyCheckOpts struct { } type AuthResults struct { + entAuthResults ACLResults *ACLResults SentinelResults *SentinelResults Allowed bool @@ -358,6 +361,11 @@ func (a *ACL) Capabilities(ctx context.Context, path string) []string { // AllowOperation is used to check if the given operation is permitted. func (a *ACL) AllowOperation(ctx context.Context, req *logical.Request, capCheckOnly bool) (ret *ACLResults) { + ret = a.performEnterpriseAclChecks(ctx, req, capCheckOnly) + if ret != nil { + return ret + } + ret = new(ACLResults) // Fast-path root diff --git a/vault/acl_ce.go b/vault/acl_ce.go new file mode 100644 index 0000000000..c352b1288a --- /dev/null +++ b/vault/acl_ce.go @@ -0,0 +1,21 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import ( + "context" + + "github.com/hashicorp/vault/sdk/logical" +) + +type ( + entAcl struct{} + entAuthResults struct{} +) + +func (a *ACL) performEnterpriseAclChecks(_ context.Context, _ *logical.Request, _ bool) (ret *ACLResults) { + return nil +} diff --git a/vault/request_handling.go b/vault/request_handling.go index 0ec4cc8a33..1195119f5c 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -101,8 +101,6 @@ func (c *Core) fetchEntityAndDerivedPolicies(ctx context.Context, tokenNS *names policies := make(map[string][]string) if !skipDeriveEntityPolicies { - // c.logger.Debug("entity successfully fetched; adding entity policies to token's policies to create ACL") - // Attach the policies on the entity if len(entity.Policies) != 0 { policies[entity.NamespaceID] = append(policies[entity.NamespaceID], entity.Policies...) @@ -239,12 +237,33 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req return nil, nil, nil, nil, ErrInternalError } + var secondEntity *identity.Entity + if IsEnterpriseToken(req.ClientToken) { + isValidEnterpriseToken, tokenMetadataContainer, entity, entity2, err := c.validateEnterpriseTokenAndFetchEntity(ctx, req.ClientToken) + if err != nil { + c.logger.Error("failed to validate enterprise token", "error", err) + } + if !isValidEnterpriseToken { + return nil, nil, nil, nil, logical.ErrPermissionDenied + } + req.EnterpriseTokenMetadata = getEnterpriseTokenMetadata(tokenMetadataContainer) + secondEntity = entity2 + err = c.createAndStoreEnterpriseTokenEntry(ctx, req, tokenMetadataContainer, entity) + if err != nil { + return nil, nil, nil, nil, multierror.Append(err, errors.New("failed in processing enterprise token")) + } + } + // Resolve the token policy var te *logical.TokenEntry switch req.TokenEntry() { case nil: var err error - te, err = c.tokenStore.Lookup(ctx, req.ClientToken) + if IsEnterpriseToken(req.ClientToken) { + te, err = c.tokenStore.Lookup(ctx, getEnterpriseTokenId(req.EnterpriseTokenMetadata)) + } else { + te, err = c.tokenStore.Lookup(ctx, req.ClientToken) + } if err != nil { c.logger.Error("failed to lookup acl token", "error", err) return nil, nil, nil, nil, ErrInternalError @@ -304,6 +323,21 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req policyNames[nsID] = policyutil.SanitizePolicies(append(policyNames[nsID], nsPolicies...), false) } + var secondEntityPolicyNames map[string][]string + if secondEntity != nil { + c.logger.Debug("building separate ACL for second entity", "entity_id", secondEntity.ID) + secondEntityPolicyNames = make(map[string][]string) + _, secondEntityIdentityPolicies, err := c.fetchEntityAndDerivedPolicies(ctx, tokenNS, secondEntity.ID, false) + if err != nil { + c.logger.Error("failed to fetch second entity policies", "error", err) + return nil, nil, nil, nil, ErrInternalError + } + // Store second entity policies separately - do NOT merge with primary entity's policies + for nsID, nsPolicies := range secondEntityIdentityPolicies { + secondEntityPolicyNames[nsID] = policyutil.SanitizePolicies(nsPolicies, false) + } + } + // Attach token's namespace information to the context. Wrapping tokens by // should be able to be used anywhere, so we also special case behavior. var tokenCtx context.Context @@ -344,6 +378,14 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req return nil, nil, nil, nil, ErrInternalError } + if secondEntity != nil && len(secondEntityPolicyNames) > 0 { + newAcl, err := c.performSecondaryEntityTokenChecks(tokenCtx, acl, secondEntity, secondEntityPolicyNames) + if err != nil { + return nil, nil, nil, nil, err + } + acl = newAcl + } + return acl, te, entity, identityPolicies, nil } @@ -804,7 +846,7 @@ func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request // We don't care if the token is a server side consistent token or not. Either way, we're going // to be returning it for these paths instead of the short token stored in vault. requestBodyToken = token.(string) - if IsSSCToken(token.(string)) { + if IsSSCToken(token.(string)) && !IsEnterpriseToken(token.(string)) { token, err = c.CheckSSCToken(ctx, token.(string), c.isLoginRequest(ctx, req), c.perfStandby) // If we receive an error from CheckSSCToken, we can assume the token is bad somehow, and the client // should receive a 403 bad token error like they do for all other invalid tokens, unless the error @@ -1096,12 +1138,15 @@ func (c *Core) handleRequest(ctx context.Context, req *logical.Request) (retResp return } + var auth *logical.Auth + var te *logical.TokenEntry + var ctErr error // Validate the token - auth, te, ctErr := c.CheckToken(ctx, req, false) - if ctErr == logical.ErrRelativePath { + auth, te, ctErr = c.CheckToken(ctx, req, false) + if errors.Is(ctErr, logical.ErrRelativePath) { return logical.ErrorResponse(ctErr.Error()), nil, ctErr } - if ctErr == logical.ErrPerfStandbyPleaseForward { + if errors.Is(ctErr, logical.ErrPerfStandbyPleaseForward) { return nil, nil, ctErr } @@ -2693,7 +2738,7 @@ func (c *Core) LocalUpdateUserFailedLoginInfo(ctx context.Context, userKey Faile // PopulateTokenEntry looks up req.ClientToken in the token store and uses // it to set other fields in req. Does nothing if ClientToken is empty -// or a JWT token, or for service tokens that don't exist in the token store. +// or an Enterprise token, or for service tokens that don't exist in the token store. // Should be called with read stateLock held. func (c *Core) PopulateTokenEntry(ctx context.Context, req *logical.Request) error { if req.ClientToken == "" { @@ -2704,7 +2749,7 @@ func (c *Core) PopulateTokenEntry(ctx context.Context, req *logical.Request) err // doesn't exist because the request may be to an unauthenticated // endpoint/login endpoint where a bad current token doesn't matter, or // a token from a Vault version pre-accessors. We ignore errors for - // JWTs. + // Enterprise tokens. token := req.ClientToken var err error req.InboundSSCToken = token @@ -2754,8 +2799,6 @@ func (c *Core) PopulateTokenEntry(ctx context.Context, req *logical.Request) err if errors.Is(err, logical.ErrPerfStandbyPleaseForward) || errors.Is(err, logical.ErrMissingRequiredState) { return err } - // If we have two dots but the second char is a dot it's a vault - // token of the form s.SOMETHING.nsid, not a JWT if !IsJWT(token) { return fmt.Errorf("error performing token check: %w", err) } diff --git a/vault/request_handling_ce.go b/vault/request_handling_ce.go new file mode 100644 index 0000000000..cbf26bae73 --- /dev/null +++ b/vault/request_handling_ce.go @@ -0,0 +1,30 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import ( + "context" + "errors" + + "github.com/hashicorp/vault/helper/identity" + "github.com/hashicorp/vault/sdk/logical" +) + +func (c *Core) validateEnterpriseTokenAndFetchEntity(ctx context.Context, tokenString string) (bool, map[string]interface{}, *identity.Entity, *identity.Entity, error) { + return false, nil, nil, nil, errors.New("not implemented") +} + +func (c *Core) createAndStoreEnterpriseTokenEntry(ctx context.Context, req *logical.Request, allClaims map[string]interface{}, entity *identity.Entity) error { + return nil +} + +func getEnterpriseTokenMetadata(_ map[string]interface{}) string { + return "" +} + +func (c *Core) performSecondaryEntityTokenChecks(_ context.Context, _ *ACL, _ *identity.Entity, _ map[string][]string) (*ACL, error) { + return nil, errors.New("not implemented") +} diff --git a/vault/router.go b/vault/router.go index d017cdbd8a..3db7fb6caa 100644 --- a/vault/router.go +++ b/vault/router.go @@ -677,8 +677,8 @@ func (r *Router) routeCommon(ctx context.Context, req *logical.Request, existenc return nil, false, false, fmt.Errorf("nil token entry") } - if te.Type != logical.TokenTypeService { - return logical.ErrorResponse(`cubbyhole operations are only supported by "service" type tokens`), false, false, nil + if te.Type != logical.TokenTypeService && te.Type != logical.TokenTypeEnt { + return logical.ErrorResponse(`cubbyhole operations are only supported by "service" or enterprise type tokens`), false, false, nil } switch { diff --git a/vault/token_store.go b/vault/token_store.go index c70f585003..eb0aa81852 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -1107,9 +1107,11 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err } switch entry.Type { - case logical.TokenTypeDefault, logical.TokenTypeService: + case logical.TokenTypeDefault, logical.TokenTypeService, logical.TokenTypeEnt: // In case it was default, force to service - entry.Type = logical.TokenTypeService + if entry.Type == logical.TokenTypeDefault { + entry.Type = logical.TokenTypeService + } // Generate an ID if necessary userSelectedID := true @@ -1126,7 +1128,7 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err } } - if userSelectedID { + if userSelectedID && entry.Type != logical.TokenTypeEnt { switch { case strings.HasPrefix(entry.ID, consts.ServiceTokenPrefix): return fmt.Errorf("custom token ID cannot have the 'hvs.' prefix") @@ -1151,7 +1153,10 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err entry.ID = fmt.Sprintf("%s.%s", entry.ID, tokenNS.ID) } - if tokenNS.ID != namespace.RootNamespaceID || strings.HasPrefix(entry.ID, consts.ServiceTokenPrefix) || strings.HasPrefix(entry.ID, consts.LegacyServiceTokenPrefix) { + if tokenNS.ID != namespace.RootNamespaceID || + strings.HasPrefix(entry.ID, consts.ServiceTokenPrefix) || + strings.HasPrefix(entry.ID, consts.LegacyServiceTokenPrefix) || + strings.HasPrefix(entry.ID, consts.GetEnterpriseTokenPrefix()) { if entry.CubbyholeID == "" { cubbyholeID, err := base62.Random(TokenLength) if err != nil { @@ -1163,7 +1168,7 @@ func (ts *TokenStore) create(ctx context.Context, entry *logical.TokenEntry) err // If the user didn't specifically pick the ID, e.g. because they were // sudo/root, check for collision; otherwise trust the process - if userSelectedID { + if userSelectedID || entry.Type == logical.TokenTypeEnt { exist, _ := ts.lookupInternal(ctx, entry.ID, false, true) if exist != nil { return fmt.Errorf("cannot create a token with a duplicate ID") @@ -1636,7 +1641,7 @@ func (ts *TokenStore) lookupInternal(ctx context.Context, id string, salted, tai // If possible, always use the token's namespace. If it doesn't match // the request namespace, ensure the request namespace is a child _, nsID := namespace.SplitIDFromString(id) - if nsID != "" { + if nsID != "" || strings.HasPrefix(id, consts.GetEnterpriseTokenPrefix()) { tokenNS, err := NamespaceByID(ctx, nsID, ts.core) if err != nil { return nil, fmt.Errorf("failed to look up namespace from the token: %w", err) @@ -1753,6 +1758,11 @@ func (ts *TokenStore) lookupInternal(ctx context.Context, id string, salted, tai } } + // don't check for lease + if entry.Type == logical.TokenTypeEnt { + return entry, nil + } + le, err := ts.expiration.FetchLeaseTimesByToken(ctx, entry) if err != nil { return nil, fmt.Errorf("failed to fetch lease times: %w", err) @@ -2266,6 +2276,11 @@ func (ts *TokenStore) handleTidy(ctx context.Context, req *logical.Request, data return fmt.Errorf("failed to fetch cubbyhole storage keys: %w", err) } + err = ts.handleTidyEnterpriseTokens(quitCtx, ns, tidyErrors) + if err != nil { + return err + } + var countParentEntries, deletedCountParentEntries, countParentList, deletedCountParentList int64 // Scan through the secondary index entries; if there is an entry @@ -3321,6 +3336,9 @@ func (ts *TokenStore) handleRevokeTree(ctx context.Context, req *logical.Request } func (ts *TokenStore) revokeCommon(ctx context.Context, req *logical.Request, data *framework.FieldData, id string) (*logical.Response, error) { + if IsEnterpriseToken(id) { + return logical.ErrorResponse("cannot revoke ent token"), nil + } te, err := ts.Lookup(ctx, id) if err != nil { return nil, err @@ -3365,6 +3383,10 @@ func (ts *TokenStore) handleRevokeOrphan(ctx context.Context, req *logical.Reque return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest } + if IsEnterpriseToken(id) { + return logical.ErrorResponse("enterprise token cannot be revoked"), nil + } + // Do a lookup. Among other things, that will ensure that this is either // running in the same namespace or a parent. te, err := ts.Lookup(ctx, id) @@ -3402,7 +3424,9 @@ func (ts *TokenStore) handleLookup(ctx context.Context, req *logical.Request, da if id == "" { return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest } - + if IsEnterpriseToken(id) { + id = getEnterpriseTokenId(req.EnterpriseTokenMetadata) + } lock := locksutil.LockForKey(ts.tokenLocks, id) lock.RLock() defer lock.RUnlock() @@ -3514,6 +3538,9 @@ func (ts *TokenStore) handleRenew(ctx context.Context, req *logical.Request, dat if id == "" { return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest } + if IsEnterpriseToken(id) { + return logical.ErrorResponse("enterprise tokens cannot be renewed"), nil + } incrementRaw := data.Get("increment").(int) // Convert the increment diff --git a/vault/token_store_ce.go b/vault/token_store_ce.go new file mode 100644 index 0000000000..8a60254e70 --- /dev/null +++ b/vault/token_store_ce.go @@ -0,0 +1,21 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +import ( + "context" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/helper/namespace" +) + +func getEnterpriseTokenId(_ string) string { + return "" +} + +func (ts *TokenStore) handleTidyEnterpriseTokens(_ context.Context, _ *namespace.Namespace, _ *multierror.Error) error { + return nil +} diff --git a/vault/version_store.go b/vault/version_store.go index 46fbac98ad..4121abaa1b 100644 --- a/vault/version_store.go +++ b/vault/version_store.go @@ -199,6 +199,9 @@ func (c *Core) IsNewInstall(ctx context.Context) bool { return oldestVersion == "" && newestVersion == "" } +// IsJWT validates if a token is of JWT format, which was historically +// a possibility for wrapping tokens, though not something we +// encourage today. func IsJWT(token string) bool { return len(token) > 3 && strings.Count(token, ".") == 2 && (token[3] != '.' && token[1] != '.') diff --git a/vault/version_store_ce.go b/vault/version_store_ce.go new file mode 100644 index 0000000000..04baeb05d8 --- /dev/null +++ b/vault/version_store_ce.go @@ -0,0 +1,10 @@ +// Copyright IBM Corp. 2016, 2025 +// SPDX-License-Identifier: BUSL-1.1 + +//go:build !enterprise + +package vault + +func IsEnterpriseToken(token string) bool { + return false +} diff --git a/vault/wrapping.go b/vault/wrapping.go index cedba608f1..36b8abfaf3 100644 --- a/vault/wrapping.go +++ b/vault/wrapping.go @@ -348,8 +348,7 @@ DONELISTHANDLING: } // validateWrappingToken checks whether a token is a wrapping token. The passed -// in logical request will be updated if the wrapping token was provided within -// a JWT token. +// in logical request may be updated. func (c *Core) validateWrappingToken(ctx context.Context, req *logical.Request) (valid bool, err error) { if req == nil { return false, fmt.Errorf("invalid request") @@ -409,13 +408,8 @@ func (c *Core) validateWrappingToken(ctx context.Context, req *logical.Request) } // Check for it being a JWT. If it is, and it is valid, we extract the - // internal client token from it and use that during lookup. The second - // check is a quick check to verify that we don't consider a namespaced - // token to be a JWT -- namespaced tokens have two dots too, but Vault - // token types (for now at least) begin with a letter representing a type - // and then a dot. + // internal client token from it and use that during lookup. if IsJWT(token) { - // Implement the jose library way parsedJWT, err := jwt.ParseSigned(token) if err != nil { return false, fmt.Errorf("wrapping token could not be parsed: %w", err)