diff --git a/api/sudo_paths.go b/api/sudo_paths.go index 5aa7239fad..ae725c4528 100644 --- a/api/sudo_paths.go +++ b/api/sudo_paths.go @@ -57,6 +57,14 @@ var sudoPaths = map[string]*regexp.Regexp{ "/sys/storage/raft/snapshot-auto/config": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/?$`), "/sys/storage/raft/snapshot-auto/config/{name}": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/[^/]+$`), "/sys/reporting/scan": regexp.MustCompile(`^/sys/reporting/scan$`), + + // activation-flags paths requiring sudo + "/sys/activation-flags/oauth-resource-server/activate": regexp.MustCompile(`^/sys/activation-flags/oauth-resource-server/activate$`), + "/sys/activation-flags/oauth-resource-server/deactivate": regexp.MustCompile(`^/sys/activation-flags/oauth-resource-server/deactivate$`), + + // OAuth resource server profile paths requiring sudo + "/sys/config/oauth-resource-server/{name}": regexp.MustCompile(`^/sys/config/oauth-resource-server/[^/]+$`), + "/sys/config/oauth-resource-server": regexp.MustCompile(`^/sys/config/oauth-resource-server$`), } func SudoPaths() map[string]*regexp.Regexp { diff --git a/go.mod b/go.mod index 4559e3ab3b..f497edb880 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,8 @@ require ( github.com/Azure/azure-storage-blob-go v0.15.0 github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest/adal v0.9.24 + github.com/MicahParks/jwkset v0.11.0 + github.com/MicahParks/keyfunc/v3 v3.7.0 github.com/ProtonMail/go-crypto v1.3.0 github.com/ProtonMail/gopenpgp/v3 v3.2.1 github.com/SAP/go-hdb v1.10.1 diff --git a/go.sum b/go.sum index 329cbde221..e2b6b7ea2b 100644 --- a/go.sum +++ b/go.sum @@ -147,6 +147,10 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ= +github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0= +github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM= +github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= diff --git a/vault/expiration.go b/vault/expiration.go index 9843962b66..92efd1009f 100644 --- a/vault/expiration.go +++ b/vault/expiration.go @@ -1094,7 +1094,7 @@ func (m *ExpirationManager) revokeCommon(ctx context.Context, leaseID string, fo // Delete the secondary index, but only if it's a leased secret (not auth) if le.Secret != nil { var indexToken string - // Maintain secondary index by token, except for orphan batch tokens + // Maintain secondary index by token, except for orphan batch tokens and ent tokens switch le.ClientTokenType { case logical.TokenTypeBatch: te, err := m.tokenStore.lookupBatchTokenInternal(ctx, le.ClientToken) @@ -1108,6 +1108,9 @@ func (m *ExpirationManager) revokeCommon(ctx context.Context, leaseID string, fo // parent indexToken = te.Parent } + case logical.TokenTypeEnt: + // ent tokens don't maintain parent relationships; just use the token itself + indexToken = le.ClientToken default: indexToken = le.ClientToken } @@ -1656,6 +1659,15 @@ func (m *ExpirationManager) Register(ctx context.Context, req *logical.Request, } } + // If the token is an ent token, derive TTL from the ent token + if te.Type == logical.TokenTypeEnt { + entTokenExpireTime := deriveExpireTimeFromEntToken(te) + if !entTokenExpireTime.IsZero() && le.ExpireTime.After(entTokenExpireTime) { + // Use the ent token's expiration time for the lease + le.ExpireTime = entTokenExpireTime + } + } + // Acquire the lock here so persistEntry and updatePending are atomic, // although it is *very unlikely* that anybody could grab the lease ID // before this function returns. (They could find it in an index, or @@ -1714,6 +1726,14 @@ func (m *ExpirationManager) RegisterAuth(ctx context.Context, te *logical.TokenE authExpirationTime := auth.ExpirationTime() + // For ent tokens, derive expiration from ent token + if te.Type == logical.TokenTypeEnt { + entTokenExpireTime := deriveExpireTimeFromEntToken(te) + if !entTokenExpireTime.IsZero() { + authExpirationTime = entTokenExpireTime + } + } + if te.TTL == 0 && authExpirationTime.IsZero() && (len(te.Policies) != 1 || te.Policies[0] != "root") { return errors.New("refusing to register a lease for a non-root token with no TTL") } @@ -1815,6 +1835,19 @@ func (m *ExpirationManager) FetchLeaseTimesByToken(ctx context.Context, te *logi }, nil } + if te.Type == logical.TokenTypeEnt { + entTokenExpireTime := deriveExpireTimeFromEntToken(te) + if !entTokenExpireTime.IsZero() { + issueTime := time.Unix(te.CreationTime, 0) + return &leaseEntry{ + IssueTime: issueTime, + ExpireTime: entTokenExpireTime, + ClientTokenType: logical.TokenTypeEnt, + }, nil + } + return nil, errors.New("enterprise token has no valid expiration time") + } + tokenNS, err := NamespaceByID(ctx, te.NamespaceID, m.core) if err != nil { return nil, err @@ -2109,6 +2142,11 @@ func (m *ExpirationManager) revokeEntry(ctx context.Context, le *leaseEntry) err return errors.New("batch tokens cannot be revoked") } + // ent tokens are managed by external IdPs and should not be revoked through Vault backends + if le.ClientTokenType == logical.TokenTypeEnt { + return errors.New("enterprise tokens are managed by external IdPs and cannot be revoked by Vault") + } + if err := m.tokenStore.revokeTree(ctx, le); err != nil { return fmt.Errorf("failed to revoke token: %w", err) } @@ -2159,6 +2197,10 @@ func (m *ExpirationManager) renewAuthEntry(ctx context.Context, req *logical.Req return logical.ErrorResponse("batch tokens cannot be renewed"), nil } + if le.ClientTokenType == logical.TokenTypeEnt { + return logical.ErrorResponse("enterprise tokens cannot be renewed"), nil + } + auth := *le.Auth auth.IssueTime = le.IssueTime auth.Increment = increment @@ -2297,28 +2339,31 @@ func (m *ExpirationManager) deleteEntry(ctx context.Context, le *leaseEntry) err // createIndexByToken creates a secondary index from the token to a lease entry func (m *ExpirationManager) createIndexByToken(ctx context.Context, le *leaseEntry, token string) error { - tokenNS := namespace.RootNamespace - saltCtx := namespace.ContextWithNamespace(ctx, namespace.RootNamespace) - _, nsID := namespace.SplitIDFromString(token) - if nsID != "" && !IsEnterpriseToken(token) { - var err error - tokenNS, err = NamespaceByID(ctx, nsID, m.core) - if err != nil { - return err - } - if tokenNS != nil { - saltCtx = namespace.ContextWithNamespace(ctx, tokenNS) - } - } + var tokenNS *namespace.Namespace + var saltCtx context.Context + var err error - // If it's an enterprise token, we cannot get the ID from the token, - // so let's get it from the lease. if IsEnterpriseToken(token) { - ns, err := m.getNamespaceFromLeaseID(ctx, le.LeaseID) + // fetch the namespace from the lease rather than the req context to allow for cross namespace access + tokenNS, err = m.getNamespaceFromLeaseID(ctx, le.LeaseID) if err != nil { return err } - tokenNS = ns + saltCtx = namespace.ContextWithNamespace(ctx, tokenNS) + } else { + tokenNS = namespace.RootNamespace + saltCtx = namespace.ContextWithNamespace(ctx, namespace.RootNamespace) + _, nsID := namespace.SplitIDFromString(token) + if nsID != "" { + var err error + tokenNS, err = NamespaceByID(ctx, nsID, m.core) + if err != nil { + return err + } + if tokenNS != nil { + saltCtx = namespace.ContextWithNamespace(ctx, tokenNS) + } + } } saltedID, err := m.tokenStore.SaltID(saltCtx, token) @@ -2379,26 +2424,39 @@ func (m *ExpirationManager) indexByToken(ctx context.Context, le *leaseEntry) (* // removeIndexByToken removes the secondary index from the token to a lease entry func (m *ExpirationManager) removeIndexByToken(ctx context.Context, le *leaseEntry, token string) error { - tokenNS := namespace.RootNamespace - saltCtx := namespace.ContextWithNamespace(ctx, namespace.RootNamespace) - _, nsID := namespace.SplitIDFromString(token) - if nsID != "" { - var err error - tokenNS, err = NamespaceByID(ctx, nsID, m.core) + var tokenNS *namespace.Namespace + var saltCtx context.Context + var err error + + if IsEnterpriseToken(token) { + tokenNS, err = namespace.FromContext(ctx) if err != nil { return err } - if tokenNS != nil { - saltCtx = namespace.ContextWithNamespace(ctx, tokenNS) - } - // Downgrade logic for old-style (V0) namespace leases that had its - // secondary index live in the root namespace. This reverts to the old - // behavior of looking for the secondary index on these leases in the - // root namespace to be cleaned up properly. We set it here because the - // old behavior used the namespace's token store salt for its saltCtx. - if le.Version < 1 { - tokenNS = namespace.RootNamespace + saltCtx = namespace.ContextWithNamespace(ctx, tokenNS) + } else { + tokenNS = namespace.RootNamespace + saltCtx = namespace.ContextWithNamespace(ctx, namespace.RootNamespace) + _, nsID := namespace.SplitIDFromString(token) + if nsID != "" { + var err error + tokenNS, err = NamespaceByID(ctx, nsID, m.core) + if err != nil { + return err + } + if tokenNS != nil { + saltCtx = namespace.ContextWithNamespace(ctx, tokenNS) + } + + // Downgrade logic for old-style (V0) namespace leases that had its + // secondary index live in the root namespace. This reverts to the old + // behavior of looking for the secondary index on these leases in the + // root namespace to be cleaned up properly. We set it here because the + // old behavior used the namespace's token store salt for its saltCtx. + if le.Version < 1 { + tokenNS = namespace.RootNamespace + } } } @@ -2984,6 +3042,9 @@ func (le *leaseEntry) renewable() (bool, error) { case le.ClientTokenType == logical.TokenTypeBatch: return false, nil + case le.ClientTokenType == logical.TokenTypeEnt: + return false, fmt.Errorf("enterprise tokens cannot be renewed") + // Determine if the lease is expired case le.ExpireTime.Before(time.Now()): return false, fmt.Errorf("lease expired") diff --git a/vault/expiration_util_ce.go b/vault/expiration_util_ce.go index 18d5b4f66c..38221ba1a2 100644 --- a/vault/expiration_util_ce.go +++ b/vault/expiration_util_ce.go @@ -7,6 +7,7 @@ package vault import ( "fmt" + "time" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/logical" @@ -35,3 +36,7 @@ func (m *ExpirationManager) collectLeases() (map[*namespace.Namespace][]string, func (m *ExpirationManager) removeIrrevocableLeasesEnabled(c *Core) bool { return false } + +func deriveExpireTimeFromEntToken(_ *logical.TokenEntry) time.Time { + return time.Time{} +} diff --git a/vault/logical_system.go b/vault/logical_system.go index e830a62d78..b165dba6fb 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -199,6 +199,9 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf // to declare them here so that the generated OpenAPI spec gets their sudo status correct. "seal", "step-down", + "activation-flags/oauth-resource-server/activate", + "activation-flags/oauth-resource-server/deactivate", + "config/oauth-resource-server/*", }, Unauthenticated: unauthenticatedPaths, diff --git a/vault/logical_system_helpers.go b/vault/logical_system_helpers.go index 4ce29be3cb..e0fb40fcda 100644 --- a/vault/logical_system_helpers.go +++ b/vault/logical_system_helpers.go @@ -134,6 +134,13 @@ var ( "config/group-policy-application$": {operations: []logical.Operation{logical.ReadOperation, logical.UpdateOperation}}, })...) + paths = append(paths, buildEnterpriseOnlyPaths(map[string]enterprisePathStub{ + "activation-flags/oauth-resource-server/activate": {operations: []logical.Operation{logical.UpdateOperation}}, + "activation-flags/oauth-resource-server/deactivate": {operations: []logical.Operation{logical.UpdateOperation}}, + "config/oauth-resource-server/": {operations: []logical.Operation{logical.ListOperation}}, + "config/oauth-resource-server/" + framework.GenericNameRegex("name"): {parameters: []string{"name"}, operations: []logical.Operation{logical.DeleteOperation, logical.ReadOperation, logical.UpdateOperation}}, + })...) + // reporting paths paths = append(paths, buildEnterpriseOnlyPaths(map[string]enterprisePathStub{ "reporting/scan$": {operations: []logical.Operation{logical.UpdateOperation}}, diff --git a/vault/request_handling.go b/vault/request_handling.go index 23f5946c26..e6643037a0 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -2707,7 +2707,11 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st return err } } + case logical.TokenTypeEnt: + // Ensure it's not marked renewable since enterprise tokens are not renewable + auth.Renewable = false } + return nil } diff --git a/vault/request_handling_ce.go b/vault/request_handling_ce.go index 7cb5b254ab..44f772b771 100644 --- a/vault/request_handling_ce.go +++ b/vault/request_handling_ce.go @@ -21,6 +21,10 @@ func (c *Core) createAndStoreEnterpriseTokenEntry(ctx context.Context, req *logi return nil } +func isActivationFlagEnabledForEnterpriseToken(c *Core) bool { + return false +} + func getEnterpriseTokenMetadata(_ map[string]interface{}) string { return "" }