diff --git a/http/handler.go b/http/handler.go index 785a1acea..01a8fdcec 100644 --- a/http/handler.go +++ b/http/handler.go @@ -449,25 +449,6 @@ func WrapForwardedForHandler(h http.Handler, l *configutil.Listener) http.Handle }) } -// A lookup on a token that is about to expire returns nil, which means by the -// time we can validate a wrapping token lookup will return nil since it will -// be revoked after the call. So we have to do the validation here. -func wrappingVerificationFunc(ctx context.Context, core *vault.Core, req *logical.Request) error { - if req == nil { - return fmt.Errorf("invalid request") - } - - valid, err := core.ValidateWrappingToken(ctx, req) - if err != nil { - return fmt.Errorf("error validating wrapping token: %w", err) - } - if !valid { - return consts.ErrInvalidWrappingToken - } - - return nil -} - // stripPrefix is a helper to strip a prefix from the path. It will // return false from the second return value if it the prefix doesn't exist. func stripPrefix(prefix, path string) (string, bool) { @@ -731,7 +712,7 @@ func forwardBasedOnHeaders(core *vault.Core, r *http.Request) (bool, error) { return false, nil } - return core.MissingRequiredState(r.Header.Values(VaultIndexHeaderName)), nil + return core.MissingRequiredState(r.Header.Values(VaultIndexHeaderName), core.PerfStandby()), nil } // handleRequestForwarding determines whether to forward a request or not, @@ -977,7 +958,7 @@ func getTokenFromReq(r *http.Request) (string, bool) { } // requestAuth adds the token to the logical.Request if it exists. -func requestAuth(core *vault.Core, r *http.Request, req *logical.Request) (*logical.Request, error) { +func requestAuth(r *http.Request, req *logical.Request) { // Attach the header value if we have it token, fromAuthzHeader := getTokenFromReq(r) if token != "" { @@ -987,29 +968,7 @@ func requestAuth(core *vault.Core, r *http.Request, req *logical.Request) (*logi req.ClientTokenSource = logical.ClientTokenFromAuthzHeader } - // Also attach the accessor if we have it. This doesn't fail if it - // 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. - te, err := core.LookupToken(r.Context(), token) - if err != nil { - dotCount := strings.Count(token, ".") - // 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 dotCount != 2 || - dotCount == 2 && token[1] == '.' { - return req, err - } - } - if err == nil && te != nil { - req.ClientTokenAccessor = te.Accessor - req.ClientTokenRemainingUses = te.NumUses - req.SetTokenEntry(te) - } } - - return req, nil } func requestPolicyOverride(r *http.Request, req *logical.Request) error { diff --git a/http/handler_test.go b/http/handler_test.go index 5373fa56c..c228629ea 100644 --- a/http/handler_test.go +++ b/http/handler_test.go @@ -650,7 +650,8 @@ func TestHandler_requestAuth(t *testing.T) { for _, r := range []*http.Request{rWithVault, rWithAuthorization} { req := logical.TestRequest(t, logical.ReadOperation, "test/path") r = r.WithContext(rootCtx) - req, err = requestAuth(core, r, req) + requestAuth(r, req) + err = core.PopulateTokenEntry(rootCtx, req) if err != nil { t.Fatalf("err: %s", err) } @@ -675,25 +676,14 @@ func TestHandler_requestAuth(t *testing.T) { } req := logical.TestRequest(t, logical.ReadOperation, "test/path") - req, err = requestAuth(core, rNothing, req) + requestAuth(rNothing, req) + err = core.PopulateTokenEntry(rootCtx, req) if err != nil { t.Fatalf("expected no error, got %s", err) } if req.ClientToken != "" { t.Fatalf("client token should not be filled, got %s", req.ClientToken) } - - rFragmentedHeader, err := http.NewRequest("GET", "v1/test/path", nil) - if err != nil { - t.Fatalf("err: %s", err) - } - rFragmentedHeader.Header.Set("Authorization", "Bearer something somewhat") - req = logical.TestRequest(t, logical.ReadOperation, "test/path") - - _, err = requestAuth(core, rFragmentedHeader, req) - if err == nil { - t.Fatalf("expected an error, got none") - } } func TestHandler_getTokenFromReq(t *testing.T) { diff --git a/http/help.go b/http/help.go index a31ef1e57..45099bd7b 100644 --- a/http/help.go +++ b/http/help.go @@ -1,10 +1,8 @@ package http import ( - "fmt" "net/http" - "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" @@ -35,19 +33,12 @@ func handleHelp(core *vault.Core, w http.ResponseWriter, r *http.Request) { } path := ns.TrimmedPath(r.URL.Path[len("/v1/"):]) - req, err := requestAuth(core, r, &logical.Request{ + req := &logical.Request{ Operation: logical.HelpOperation, Path: path, Connection: getConnection(r), - }) - if err != nil { - if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) { - respondError(w, http.StatusForbidden, nil) - return - } - respondError(w, http.StatusBadRequest, fmt.Errorf("error performing token check: %w", err)) - return } + requestAuth(r, req) resp, err := core.HandleRequest(r.Context(), req) if err != nil { diff --git a/http/help_test.go b/http/help_test.go index cc3879b36..4d4bc2c54 100644 --- a/http/help_test.go +++ b/http/help_test.go @@ -1,6 +1,7 @@ package http import ( + "net/http" "testing" "github.com/hashicorp/vault/vault" @@ -12,7 +13,12 @@ func TestHelp(t *testing.T) { defer ln.Close() TestServerAuth(t, addr, token) - resp := testHttpGet(t, token, addr+"/v1/sys/mounts?help=1") + resp := testHttpGet(t, "", addr+"/v1/sys/mounts?help=1") + if resp.StatusCode != http.StatusBadRequest { + t.Fatal("expected bad request with no token") + } + + resp = testHttpGet(t, token, addr+"/v1/sys/mounts?help=1") var actual map[string]interface{} testResponseStatus(t, resp, 200) diff --git a/http/logical.go b/http/logical.go index 9f5df35a6..dd9abce34 100644 --- a/http/logical.go +++ b/http/logical.go @@ -12,7 +12,6 @@ import ( "strings" "time" - "github.com/hashicorp/errwrap" uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/consts" @@ -220,18 +219,8 @@ func buildLogicalRequest(core *vault.Core, w http.ResponseWriter, r *http.Reques return nil, nil, status, err } - rawRequired := r.Header.Values(VaultIndexHeaderName) - if len(rawRequired) != 0 && core.MissingRequiredState(rawRequired) { - return nil, nil, http.StatusPreconditionFailed, fmt.Errorf("required index state not present") - } - - req, err = requestAuth(core, r, req) - if err != nil { - if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) { - return nil, nil, http.StatusForbidden, nil - } - return nil, nil, http.StatusBadRequest, fmt.Errorf("error performing token check: %w", err) - } + req.SetRequiredState(r.Header.Values(VaultIndexHeaderName)) + requestAuth(r, req) req, err = requestWrapInfo(r, req) if err != nil { @@ -313,129 +302,6 @@ func handleLogicalInternal(core *vault.Core, injectDataIntoTopLevel bool, noForw return } - // Always forward requests that are using a limited use count token. - if core.PerfStandby() && req.ClientTokenRemainingUses > 0 { - // Prevent forwarding on local-only requests. - if noForward { - respondError(w, http.StatusBadRequest, vault.ErrCannotForwardLocalOnly) - return - } - if origBody != nil { - r.Body = origBody - } - forwardRequest(core, w, r) - return - } - - // req.Path will be relative by this point. The prefix check is first - // to fail faster if we're not in this situation since it's a hot path - switch { - case strings.HasPrefix(req.Path, "sys/wrapping/"), strings.HasPrefix(req.Path, "auth/token/"): - // Get the token ns info; if we match the paths below we want to - // swap in the token context (but keep the relative path) - te := req.TokenEntry() - newCtx := r.Context() - if te != nil { - ns, err := vault.NamespaceByID(newCtx, te.NamespaceID, core) - if err != nil { - core.Logger().Warn("error looking up namespace from the token's namespace ID", "error", err) - respondError(w, http.StatusInternalServerError, err) - return - } - if ns != nil { - newCtx = namespace.ContextWithNamespace(newCtx, ns) - } - } - switch req.Path { - // Route the token wrapping request to its respective sys NS - case "sys/wrapping/lookup", "sys/wrapping/rewrap", "sys/wrapping/unwrap": - r = r.WithContext(newCtx) - if err := wrappingVerificationFunc(r.Context(), core, req); err != nil { - if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) { - respondError(w, http.StatusForbidden, err) - } else { - respondError(w, http.StatusBadRequest, err) - } - return - } - - // The -self paths have no meaning outside of the token NS, so - // requests for these paths always go to the token NS - case "auth/token/lookup-self", "auth/token/renew-self", "auth/token/revoke-self": - r = r.WithContext(newCtx) - - // For the following operations, we can set the proper namespace context - // using the token's embedded nsID if a relative path was provided. Since - // this is done at the HTTP layer, the operation will still be gated by - // ACLs. - case "auth/token/lookup", "auth/token/renew", "auth/token/revoke", "auth/token/revoke-orphan": - token, ok := req.Data["token"] - // If the token is not present (e.g. a bad request), break out and let the backend - // handle the error - if !ok { - // If this is a token lookup request and if the token is not - // explicitly provided, it will use the client token so we simply set - // the context to the client token's context. - if req.Path == "auth/token/lookup" { - r = r.WithContext(newCtx) - } - break - } - _, nsID := namespace.SplitIDFromString(token.(string)) - if nsID != "" { - ns, err := vault.NamespaceByID(newCtx, nsID, core) - if err != nil { - core.Logger().Warn("error looking up namespace from the token's namespace ID", "error", err) - respondError(w, http.StatusInternalServerError, err) - return - } - if ns != nil { - newCtx = namespace.ContextWithNamespace(newCtx, ns) - r = r.WithContext(newCtx) - } - } - } - - // The following relative sys/leases/ paths handles re-routing requests - // to the proper namespace using the lease ID on applicable paths. - case strings.HasPrefix(req.Path, "sys/leases/"): - switch req.Path { - // For the following operations, we can set the proper namespace context - // using the lease's embedded nsID if a relative path was provided. Since - // this is done at the HTTP layer, the operation will still be gated by - // ACLs. - case "sys/leases/lookup", "sys/leases/renew", "sys/leases/revoke", "sys/leases/revoke-force": - leaseID, ok := req.Data["lease_id"] - // If lease ID is not present, break out and let the backend handle the error - if !ok { - break - } - _, nsID := namespace.SplitIDFromString(leaseID.(string)) - if nsID != "" { - newCtx := r.Context() - ns, err := vault.NamespaceByID(newCtx, nsID, core) - if err != nil { - core.Logger().Warn("error looking up namespace from the lease's namespace ID", "error", err) - respondError(w, http.StatusInternalServerError, err) - return - } - if ns != nil { - newCtx = namespace.ContextWithNamespace(newCtx, ns) - r = r.WithContext(newCtx) - } - } - } - - // Prevent any metrics requests to be forwarded from a standby node. - // Instead, we return an error since we cannot be sure if we have an - // active token store to validate the provided token. - case strings.HasPrefix(req.Path, "sys/metrics"): - if isStandby, _ := core.Standby(); isStandby { - respondError(w, http.StatusBadRequest, vault.ErrCannotForwardLocalOnly) - return - } - } - // Make the internal request. We attach the connection info // as well in case this is an authentication request that requires // it. Vault core handles stripping this if we need to. This also diff --git a/sdk/logical/error.go b/sdk/logical/error.go index f6269a2e4..d2eb3a31e 100644 --- a/sdk/logical/error.go +++ b/sdk/logical/error.go @@ -42,6 +42,11 @@ var ( // unrecoverable error. // e.g.: misconfigured or disconnected storage backend. ErrUnrecoverable = errors.New("unrecoverable error") + + // ErrMissingRequiredState is returned when a request can't be satisfied + // with the data in the local node's storage, based on the provided + // X-Vault-Index request header. + ErrMissingRequiredState = errors.New("required index state not present") ) type HTTPCodedError interface { diff --git a/sdk/logical/request.go b/sdk/logical/request.go index 1a103a57a..b88aabce2 100644 --- a/sdk/logical/request.go +++ b/sdk/logical/request.go @@ -205,6 +205,11 @@ type Request struct { // request that generated this logical.Request object. ResponseWriter *HTTPResponseWriter `json:"-" sentinel:""` + // requiredState is used internally to propagate the X-Vault-Index request + // header to later levels of request processing that operate only on + // logical.Request. + requiredState []string + // responseState is used internally to propagate the state that should appear // in response headers; it's attached to the request rather than the response // because not all requests yields non-nil responses. @@ -273,6 +278,14 @@ func (r *Request) SetLastRemoteWAL(last uint64) { r.lastRemoteWAL = last } +func (r *Request) RequiredState() []string { + return r.requiredState +} + +func (r *Request) SetRequiredState(state []string) { + r.requiredState = state +} + func (r *Request) ResponseState() *WALState { return r.responseState } diff --git a/sdk/logical/response_util.go b/sdk/logical/response_util.go index ce743507f..a05f74684 100644 --- a/sdk/logical/response_util.go +++ b/sdk/logical/response_util.go @@ -102,6 +102,8 @@ func RespondErrorCommon(req *Request, resp *Response, err error) (int, error) { statusCode = http.StatusBadRequest case errwrap.Contains(err, ErrPermissionDenied.Error()): statusCode = http.StatusForbidden + case errwrap.Contains(err, consts.ErrInvalidWrappingToken.Error()): + statusCode = http.StatusBadRequest case errwrap.Contains(err, ErrUnsupportedOperation.Error()): statusCode = http.StatusMethodNotAllowed case errwrap.Contains(err, ErrUnsupportedPath.Error()): @@ -114,6 +116,8 @@ func RespondErrorCommon(req *Request, resp *Response, err error) (int, error) { statusCode = http.StatusTooManyRequests case errwrap.Contains(err, ErrLeaseCountQuotaExceeded.Error()): statusCode = http.StatusTooManyRequests + case errwrap.Contains(err, ErrMissingRequiredState.Error()): + statusCode = http.StatusPreconditionFailed } } diff --git a/vault/core.go b/vault/core.go index 3f60d2366..55dbbfc6b 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1673,8 +1673,7 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr }() if req == nil { - retErr = multierror.Append(retErr, errors.New("nil request to seal")) - return retErr + return errors.New("nil request to seal") } // Since there is no token store in standby nodes, sealing cannot be done. @@ -1684,14 +1683,19 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr // same thing. if c.standby { c.logger.Error("vault cannot seal when in standby mode; please restart instead") - retErr = multierror.Append(retErr, errors.New("vault cannot seal when in standby mode; please restart instead")) - return retErr + return errors.New("vault cannot seal when in standby mode; please restart instead") } + err := c.PopulateTokenEntry(ctx, req) + if err != nil { + if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) { + return logical.ErrPermissionDenied + } + return logical.ErrInvalidRequest + } acl, te, entity, identityPolicies, err := c.fetchACLTokenEntryAndEntity(ctx, req) if err != nil { - retErr = multierror.Append(retErr, err) - return retErr + return err } // Audit-log the request before going any further @@ -1717,19 +1721,16 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr } if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil { c.logger.Error("failed to audit request", "request_path", req.Path, "error", err) - retErr = multierror.Append(retErr, errors.New("failed to audit request, cannot continue")) - return retErr + return errors.New("failed to audit request, cannot continue") } if entity != nil && entity.Disabled { c.logger.Warn("permission denied as the entity on the token is disabled") - retErr = multierror.Append(retErr, logical.ErrPermissionDenied) - return retErr + return logical.ErrPermissionDenied } if te != nil && te.EntityID != "" && entity == nil { c.logger.Warn("permission denied as the entity on the token is invalid") - retErr = multierror.Append(retErr, logical.ErrPermissionDenied) - return retErr + return logical.ErrPermissionDenied } // Attempt to use the token (decrement num_uses) @@ -1738,13 +1739,11 @@ func (c *Core) sealInitCommon(ctx context.Context, req *logical.Request) (retErr te, err = c.tokenStore.UseToken(ctx, te) if err != nil { c.logger.Error("failed to use token", "error", err) - retErr = multierror.Append(retErr, ErrInternalError) - return retErr + return ErrInternalError } if te == nil { // Token is no longer valid - retErr = multierror.Append(retErr, logical.ErrPermissionDenied) - return retErr + return logical.ErrPermissionDenied } } diff --git a/vault/core_util.go b/vault/core_util.go index d9f43a285..86fba7e3f 100644 --- a/vault/core_util.go +++ b/vault/core_util.go @@ -177,6 +177,6 @@ func (c *Core) AllowForwardingViaHeader() bool { return false } -func (c *Core) MissingRequiredState(raw []string) bool { +func (c *Core) MissingRequiredState(raw []string, perfStandby bool) bool { return false } diff --git a/vault/ha.go b/vault/ha.go index 158a6e604..54d518991 100644 --- a/vault/ha.go +++ b/vault/ha.go @@ -11,17 +11,17 @@ import ( "sync/atomic" "time" + "github.com/armon/go-metrics" + "github.com/hashicorp/errwrap" + aeadwrapper "github.com/hashicorp/go-kms-wrapping/wrappers/aead" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/physical" - - "github.com/armon/go-metrics" - aeadwrapper "github.com/hashicorp/go-kms-wrapping/wrappers/aead" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/vault/seal" "github.com/oklog/run" ) @@ -218,8 +218,7 @@ func (c *Core) StepDown(httpCtx context.Context, req *logical.Request) (retErr e defer metrics.MeasureSince([]string{"core", "step_down"}, time.Now()) if req == nil { - retErr = multierror.Append(retErr, errors.New("nil request to step-down")) - return retErr + return errors.New("nil request to step-down") } c.stateLock.RLock() @@ -243,10 +242,16 @@ func (c *Core) StepDown(httpCtx context.Context, req *logical.Request) (retErr e } }() + err := c.PopulateTokenEntry(ctx, req) + if err != nil { + if errwrap.Contains(err, logical.ErrPermissionDenied.Error()) { + return logical.ErrPermissionDenied + } + return logical.ErrInvalidRequest + } acl, te, entity, identityPolicies, err := c.fetchACLTokenEntryAndEntity(ctx, req) if err != nil { - retErr = multierror.Append(retErr, err) - return retErr + return err } // Audit-log the request before going any further @@ -272,20 +277,17 @@ func (c *Core) StepDown(httpCtx context.Context, req *logical.Request) (retErr e } if err := c.auditBroker.LogRequest(ctx, logInput, c.auditedHeaders); err != nil { c.logger.Error("failed to audit request", "request_path", req.Path, "error", err) - retErr = multierror.Append(retErr, errors.New("failed to audit request, cannot continue")) - return retErr + return errors.New("failed to audit request, cannot continue") } if entity != nil && entity.Disabled { c.logger.Warn("permission denied as the entity on the token is disabled") - retErr = multierror.Append(retErr, logical.ErrPermissionDenied) - return retErr + return logical.ErrPermissionDenied } if te != nil && te.EntityID != "" && entity == nil { c.logger.Warn("permission denied as the entity on the token is invalid") - retErr = multierror.Append(retErr, logical.ErrPermissionDenied) - return retErr + return logical.ErrPermissionDenied } // Attempt to use the token (decrement num_uses) @@ -293,13 +295,11 @@ func (c *Core) StepDown(httpCtx context.Context, req *logical.Request) (retErr e te, err = c.tokenStore.UseToken(ctx, te) if err != nil { c.logger.Error("failed to use token", "error", err) - retErr = multierror.Append(retErr, ErrInternalError) - return retErr + return ErrInternalError } if te == nil { // Token has been revoked - retErr = multierror.Append(retErr, logical.ErrPermissionDenied) - return retErr + return logical.ErrPermissionDenied } } diff --git a/vault/request_handling.go b/vault/request_handling.go index 2dfdf6b73..fc624764e 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -421,16 +421,15 @@ func (c *Core) switchedLockHandleRequest(httpCtx context.Context, req *logical.R cancel() return nil, fmt.Errorf("could not parse namespace from http context: %w", err) } - ctx = namespace.ContextWithNamespace(ctx, ns) - resp, err = c.handleCancelableRequest(ctx, ns, req) + resp, err = c.handleCancelableRequest(namespace.ContextWithNamespace(ctx, ns), req) req.SetTokenEntry(nil) cancel() return resp, err } -func (c *Core) handleCancelableRequest(ctx context.Context, ns *namespace.Namespace, req *logical.Request) (resp *logical.Response, err error) { +func (c *Core) handleCancelableRequest(ctx context.Context, req *logical.Request) (resp *logical.Response, err error) { // Allowing writing to a path ending in / makes it extremely difficult to // understand user intent for the filesystem-like backends (kv, // cubbyhole) -- did they want a key named foo/ or did they want to write @@ -453,6 +452,133 @@ func (c *Core) handleCancelableRequest(ctx context.Context, ns *namespace.Namesp defer waitGroup.Done() } + if c.MissingRequiredState(req.RequiredState(), c.perfStandby) { + return nil, logical.ErrMissingRequiredState + } + + err = c.PopulateTokenEntry(ctx, req) + if err != nil { + return nil, err + } + + // Always forward requests that are using a limited use count token. + if c.perfStandby && req.ClientTokenRemainingUses > 0 { + // Prevent forwarding on local-only requests. + return nil, logical.ErrPerfStandbyPleaseForward + } + + ns, err := namespace.FromContext(ctx) + if err != nil { + return nil, fmt.Errorf("could not parse namespace from http context: %w", err) + } + + // req.Path will be relative by this point. The prefix check is first + // to fail faster if we're not in this situation since it's a hot path + switch { + case strings.HasPrefix(req.Path, "sys/wrapping/"), strings.HasPrefix(req.Path, "auth/token/"): + // Get the token ns info; if we match the paths below we want to + // swap in the token context (but keep the relative path) + te := req.TokenEntry() + newCtx := ctx + if te != nil { + ns, err := NamespaceByID(ctx, te.NamespaceID, c) + if err != nil { + c.Logger().Warn("error looking up namespace from the token's namespace ID", "error", err) + return nil, err + } + if ns != nil { + newCtx = namespace.ContextWithNamespace(ctx, ns) + } + } + switch req.Path { + // Route the token wrapping request to its respective sys NS + case "sys/wrapping/lookup", "sys/wrapping/rewrap", "sys/wrapping/unwrap": + ctx = newCtx + // A lookup on a token that is about to expire returns nil, which means by the + // time we can validate a wrapping token lookup will return nil since it will + // be revoked after the call. So we have to do the validation here. + valid, err := c.validateWrappingToken(ctx, req) + if err != nil { + return nil, fmt.Errorf("error validating wrapping token: %w", err) + } + if !valid { + return nil, consts.ErrInvalidWrappingToken + } + + // The -self paths have no meaning outside of the token NS, so + // requests for these paths always go to the token NS + case "auth/token/lookup-self", "auth/token/renew-self", "auth/token/revoke-self": + ctx = newCtx + + // For the following operations, we can set the proper namespace context + // using the token's embedded nsID if a relative path was provided. + // The operation will still be gated by ACLs, which are checked later. + case "auth/token/lookup", "auth/token/renew", "auth/token/revoke", "auth/token/revoke-orphan": + token, ok := req.Data["token"] + // If the token is not present (e.g. a bad request), break out and let the backend + // handle the error + if !ok { + // If this is a token lookup request and if the token is not + // explicitly provided, it will use the client token so we simply set + // the context to the client token's context. + if req.Path == "auth/token/lookup" { + ctx = newCtx + } + break + } + _, nsID := namespace.SplitIDFromString(token.(string)) + if nsID != "" { + ns, err := NamespaceByID(ctx, nsID, c) + if err != nil { + c.Logger().Warn("error looking up namespace from the token's namespace ID", "error", err) + return nil, err + } + if ns != nil { + ctx = namespace.ContextWithNamespace(ctx, ns) + } + } + } + + // The following relative sys/leases/ paths handles re-routing requests + // to the proper namespace using the lease ID on applicable paths. + case strings.HasPrefix(req.Path, "sys/leases/"): + switch req.Path { + // For the following operations, we can set the proper namespace context + // using the lease's embedded nsID if a relative path was provided. + // The operation will still be gated by ACLs, which are checked later. + case "sys/leases/lookup", "sys/leases/renew", "sys/leases/revoke", "sys/leases/revoke-force": + leaseID, ok := req.Data["lease_id"] + // If lease ID is not present, break out and let the backend handle the error + if !ok { + break + } + _, nsID := namespace.SplitIDFromString(leaseID.(string)) + if nsID != "" { + ns, err := NamespaceByID(ctx, nsID, c) + if err != nil { + c.Logger().Warn("error looking up namespace from the lease's namespace ID", "error", err) + return nil, err + } + if ns != nil { + ctx = namespace.ContextWithNamespace(ctx, ns) + } + } + } + + // Prevent any metrics requests to be forwarded from a standby node. + // Instead, we return an error since we cannot be sure if we have an + // active token store to validate the provided token. + case strings.HasPrefix(req.Path, "sys/metrics"): + if c.standby && !c.perfStandby { + return nil, ErrCannotForwardLocalOnly + } + } + + ns, err = namespace.FromContext(ctx) + if err != nil { + return nil, errwrap.Wrapf("could not parse namespace from http context: {{err}}", err) + } + if !hasNamespaces(c) && ns.Path != "" { return nil, logical.CodedError(403, "namespaces feature not enabled") } @@ -1368,3 +1494,35 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st return nil } + +// 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. +// Should be called with read stateLock held. +func (c *Core) PopulateTokenEntry(ctx context.Context, req *logical.Request) error { + if req.ClientToken == "" { + return nil + } + + // Also attach the accessor if we have it. This doesn't fail if it + // 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. + token := req.ClientToken + te, err := c.LookupToken(ctx, token) + if err != nil { + dotCount := strings.Count(token, ".") + // 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 dotCount != 2 || (dotCount == 2 && token[1] == '.') { + return fmt.Errorf("error performing token check: %w", err) + } + } + if err == nil && te != nil { + req.ClientTokenAccessor = te.Accessor + req.ClientTokenRemainingUses = te.NumUses + req.SetTokenEntry(te) + } + return nil +} diff --git a/vault/token_store.go b/vault/token_store.go index e2f7c4edb..a3c547c0b 100644 --- a/vault/token_store.go +++ b/vault/token_store.go @@ -452,14 +452,13 @@ func (ts *TokenStore) paths() []*framework.Path { // is particularly useful to fetch the accessor of the client token and get it // populated in the logical request along with the client token. The accessor // of the client token can get audit logged. +// +// Should be called with read stateLock held. func (c *Core) LookupToken(ctx context.Context, token string) (*logical.TokenEntry, error) { if c.Sealed() { return nil, consts.ErrSealed } - c.stateLock.RLock() - defer c.stateLock.RUnlock() - if c.standby && !c.perfStandby { return nil, consts.ErrStandby } diff --git a/vault/wrapping.go b/vault/wrapping.go index 848458321..4ec02fcfe 100644 --- a/vault/wrapping.go +++ b/vault/wrapping.go @@ -334,7 +334,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. -func (c *Core) ValidateWrappingToken(ctx context.Context, req *logical.Request) (valid bool, err error) { +func (c *Core) validateWrappingToken(ctx context.Context, req *logical.Request) (valid bool, err error) { if req == nil { return false, fmt.Errorf("invalid request") } @@ -343,9 +343,6 @@ func (c *Core) ValidateWrappingToken(ctx context.Context, req *logical.Request) return false, consts.ErrSealed } - c.stateLock.RLock() - defer c.stateLock.RUnlock() - if c.standby && !c.perfStandby { return false, consts.ErrStandby }