From a70bc7c3cf11575d605637105f89453c34ee5144 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 10 Sep 2025 10:55:10 -0600 Subject: [PATCH] Backport Fix cert auth role quotas into ce/main (#9246) --- builtin/credential/cert/path_login.go | 7 +++++ changelog/_9201.txt | 3 ++ http/util.go | 2 +- vault/core.go | 45 +++++++++++++++------------ vault/request_handling.go | 2 +- vault/router.go | 27 +++++++--------- 6 files changed, 48 insertions(+), 38 deletions(-) create mode 100644 changelog/_9201.txt diff --git a/builtin/credential/cert/path_login.go b/builtin/credential/cert/path_login.go index 6826d3c7f4..6050c9ea89 100644 --- a/builtin/credential/cert/path_login.go +++ b/builtin/credential/cert/path_login.go @@ -69,6 +69,13 @@ func (b *backend) loginPathWrapper(wrappedOp func(ctx context.Context, req *logi } func (b *backend) pathLoginResolveRole(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // Quota role rule creates send a probe to test if the backend returns + // ErrUnsupportedOperation for ResolveRole, and there's no req.Storage populated + // for these. So just return a non-ErrUnsupportedOperation error. + if req.Storage == nil { + return logical.ErrorResponse("no storage"), logical.ErrMissingRequiredState + } + config, err := b.Config(ctx, req.Storage) if err != nil { return nil, err diff --git a/changelog/_9201.txt b/changelog/_9201.txt new file mode 100644 index 0000000000..76968474f5 --- /dev/null +++ b/changelog/_9201.txt @@ -0,0 +1,3 @@ +```release-note:bug +core: Role based quotas now work for cert auth +``` diff --git a/http/util.go b/http/util.go index 79cfd48102..e196d81d12 100644 --- a/http/util.go +++ b/http/util.go @@ -153,7 +153,7 @@ func rateLimitQuotaWrapping(handler http.Handler, core *vault.Core) http.Handler if requiresResolveRole { buf := bytes.Buffer{} teeReader := io.TeeReader(r.Body, &buf) - role := core.DetermineRoleFromLoginRequestFromReader(r.Context(), mountPath, teeReader) + role := core.DetermineRoleFromLoginRequestFromReader(r.Context(), mountPath, teeReader, getConnection(r), r.Header) // Reset the body if it was read if buf.Len() > 0 { diff --git a/vault/core.go b/vault/core.go index 9bee997744..b906174763 100644 --- a/vault/core.go +++ b/vault/core.go @@ -4422,48 +4422,53 @@ func (c *Core) LoadNodeID() (string, error) { // DetermineRoleFromLoginRequest will determine the role that should be applied to a quota for a given // login request -func (c *Core) DetermineRoleFromLoginRequest(ctx context.Context, mountPoint string, data map[string]interface{}) string { +func (c *Core) DetermineRoleFromLoginRequest(ctx context.Context, mountPoint string, data map[string]interface{}, conn *logical.Connection, headers map[string][]string) string { c.authLock.RLock() defer c.authLock.RUnlock() - matchingBackend := c.router.MatchingBackend(ctx, mountPoint) - if matchingBackend == nil || matchingBackend.Type() != logical.TypeCredential { - // Role based quotas do not apply to this request - return "" - } - return c.doResolveRoleLocked(ctx, mountPoint, matchingBackend, data) + return c.doResolveRoleLocked(ctx, mountPoint, data, conn, headers) } // DetermineRoleFromLoginRequestFromReader will determine the role that should // be applied to a quota for a given login request. The reader will only be // consumed if the matching backend for the mount point exists and is a secret // backend -func (c *Core) DetermineRoleFromLoginRequestFromReader(ctx context.Context, mountPoint string, reader io.Reader) string { +func (c *Core) DetermineRoleFromLoginRequestFromReader(ctx context.Context, mountPoint string, reader io.Reader, conn *logical.Connection, header http.Header) string { c.authLock.RLock() defer c.authLock.RUnlock() - matchingBackend := c.router.MatchingBackend(ctx, mountPoint) - if matchingBackend == nil || matchingBackend.Type() != logical.TypeCredential { - // Role based quotas do not apply to this request - return "" - } - data := make(map[string]interface{}) err := jsonutil.DecodeJSONFromReader(reader, &data) if err != nil { return "" } - return c.doResolveRoleLocked(ctx, mountPoint, matchingBackend, data) + return c.doResolveRoleLocked(ctx, mountPoint, data, conn, header) } -// doResolveRoleLocked does a login and resolve role request on the matching -// backend. Callers should have a read lock on c.authLock -func (c *Core) doResolveRoleLocked(ctx context.Context, mountPoint string, matchingBackend logical.Backend, data map[string]interface{}) string { - resp, err := matchingBackend.HandleRequest(ctx, &logical.Request{ +// doResolveRoleLocked does a resolve role request on the matching backend. +// Callers should have a read lock on c.authLock. +func (c *Core) doResolveRoleLocked(ctx context.Context, mountPoint string, data map[string]interface{}, conn *logical.Connection, headers http.Header) string { + be, me := c.router.MatchingBackendAndMountEntry(ctx, mountPoint) + if be == nil || be.Type() != logical.TypeCredential { + // Role based quotas do not apply to this request + return "" + } + + var passthroughRequestHeaders []string + if rawVal, ok := me.synthesizedConfigCache.Load("passthrough_request_headers"); ok { + passthroughRequestHeaders = rawVal.([]string) + } + + req := &logical.Request{ MountPoint: mountPoint, Path: "login", Operation: logical.ResolveRoleOperation, Data: data, Storage: c.router.MatchingStorageByAPIPath(ctx, mountPoint+"login"), - }) + Connection: conn, + } + if len(passthroughRequestHeaders) > 0 { + req.Headers = filteredHeaders(headers, passthroughRequestHeaders, deniedPassthroughRequestHeaders) + } + resp, err := be.HandleRequest(ctx, req) if err != nil || resp.Data["role"] == nil { return "" } diff --git a/vault/request_handling.go b/vault/request_handling.go index b1827b4844..2f47f3b3bb 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -1928,7 +1928,7 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re // for new role-based quotas upon creation, rather than counting old leases toward // the total. if reqRole == nil && requiresLease && !c.impreciseLeaseRoleTracking { - role = c.DetermineRoleFromLoginRequest(ctx, req.MountPoint, req.Data) + role = c.DetermineRoleFromLoginRequest(ctx, req.MountPoint, req.Data, req.Connection, req.Headers) } leaseGen, respTokenCreate, errCreateToken := c.LoginCreateToken(ctx, ns, req.Path, source, role, resp) diff --git a/vault/router.go b/vault/router.go index 870ed3d9d1..1f3cbcb29d 100644 --- a/vault/router.go +++ b/vault/router.go @@ -474,26 +474,20 @@ func (r *Router) matchingStorage(ctx context.Context, path string, apiPath bool) // MatchingMountEntry returns the MountEntry used for a path func (r *Router) MatchingMountEntry(ctx context.Context, path string) *MountEntry { - ns, err := namespace.FromContext(ctx) - if err != nil { - return nil - } - path = ns.Path + path - - r.l.RLock() - _, raw, ok := r.root.LongestPrefix(path) - r.l.RUnlock() - if !ok { - return nil - } - return raw.(*routeEntry).mountEntry + _, mountEntry := r.MatchingBackendAndMountEntry(ctx, path) + return mountEntry } // MatchingBackend returns the backend used for a path func (r *Router) MatchingBackend(ctx context.Context, path string) logical.Backend { + be, _ := r.MatchingBackendAndMountEntry(ctx, path) + return be +} + +func (r *Router) MatchingBackendAndMountEntry(ctx context.Context, path string) (logical.Backend, *MountEntry) { ns, err := namespace.FromContext(ctx) if err != nil { - return nil + return nil, nil } path = ns.Path + path @@ -501,14 +495,15 @@ func (r *Router) MatchingBackend(ctx context.Context, path string) logical.Backe _, raw, ok := r.root.LongestPrefix(path) r.l.RUnlock() if !ok { - return nil + return nil, nil } re := raw.(*routeEntry) + re.l.RLock() defer re.l.RUnlock() - return re.backend + return re.backend, re.mountEntry } // MatchingSystemView returns the SystemView used for a path