diff --git a/http/handler.go b/http/handler.go index b0f9864da0..241ab3fbc4 100644 --- a/http/handler.go +++ b/http/handler.go @@ -21,7 +21,7 @@ const ( // WrapHeaderName is the name of the header containing a directive to wrap the // response. - WrapDurationHeaderName = "X-Vault-Wrap-Duration" + WrapTTLHeaderName = "X-Vault-Wrap-TTL" ) // Handler returns an http.Handler for the API. This can be used on @@ -161,29 +161,29 @@ func requestAuth(r *http.Request, req *logical.Request) *logical.Request { return req } -// requestWrapDuration adds the WrapDuration value to the logical.Request if it +// requestWrapTTL adds the WrapTTL value to the logical.Request if it // exists. -func requestWrapDuration(r *http.Request, req *logical.Request) (*logical.Request, error) { +func requestWrapTTL(r *http.Request, req *logical.Request) (*logical.Request, error) { // First try for the header value - wrapDuration := r.Header.Get(WrapDurationHeaderName) - if wrapDuration == "" { + wrapTTL := r.Header.Get(WrapTTLHeaderName) + if wrapTTL == "" { return req, nil } // If it has an allowed suffix parse as a duration string - if strings.HasSuffix(wrapDuration, "s") || strings.HasSuffix(wrapDuration, "m") || strings.HasSuffix(wrapDuration, "h") { - dur, err := time.ParseDuration(wrapDuration) + if strings.HasSuffix(wrapTTL, "s") || strings.HasSuffix(wrapTTL, "m") || strings.HasSuffix(wrapTTL, "h") { + dur, err := time.ParseDuration(wrapTTL) if err != nil { return req, err } - req.WrapDuration = dur + req.WrapTTL = dur } else { // Parse as a straight number of seconds - seconds, err := strconv.ParseInt(wrapDuration, 10, 64) + seconds, err := strconv.ParseInt(wrapTTL, 10, 64) if err != nil { return req, err } - req.WrapDuration = time.Duration(time.Duration(seconds) * time.Second) + req.WrapTTL = time.Duration(time.Duration(seconds) * time.Second) } return req, nil diff --git a/http/logical.go b/http/logical.go index 4552930c5d..541aded895 100644 --- a/http/logical.go +++ b/http/logical.go @@ -76,9 +76,9 @@ func handleLogical(core *vault.Core, dataOnly bool, prepareRequestCallback Prepa Data: data, Connection: getConnection(r), }) - req, err = requestWrapDuration(r, req) + req, err = requestWrapTTL(r, req) if err != nil { - respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing X-Vault-Wrap-Duration header: {{err}}", err)) + respondError(w, http.StatusBadRequest, errwrap.Wrapf("error parsing X-Vault-Wrap-TTL header: {{err}}", err)) return } @@ -130,7 +130,16 @@ func respondLogical(w http.ResponseWriter, r *http.Request, path string, dataOnl return } - httpResp = logical.SanitizeResponse(resp) + if resp.WrapInfo.Token != "" { + httpResp = logical.HTTPResponse{ + WrapInfo: &logical.HTTPWrapInfo{ + Token: resp.WrapInfo.Token, + TTL: int(resp.WrapInfo.TTL.Seconds()), + }, + } + } else { + httpResp = logical.SanitizeResponse(resp) + } } // Respond diff --git a/http/logical_test.go b/http/logical_test.go index c99f0461da..f89aa0bd18 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -40,8 +40,9 @@ func TestLogical(t *testing.T) { "data": map[string]interface{}{ "data": "bar", }, - "auth": nil, - "warnings": nilWarnings, + "auth": nil, + "wrap_info": nil, + "warnings": nilWarnings, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) @@ -139,8 +140,9 @@ func TestLogical_StandbyRedirect(t *testing.T) { "creation_ttl": float64(0), "role": "", }, - "warnings": nilWarnings, - "auth": nil, + "warnings": nilWarnings, + "wrap_info": nil, + "auth": nil, } testResponseStatus(t, resp, 200) @@ -177,6 +179,7 @@ func TestLogical_CreateToken(t *testing.T) { "renewable": false, "lease_duration": float64(0), "data": nil, + "wrap_info": nil, "auth": map[string]interface{}{ "policies": []interface{}{"root"}, "metadata": nil, diff --git a/http/sys_policy_test.go b/http/sys_policy_test.go index 27495ae496..084fa58d80 100644 --- a/http/sys_policy_test.go +++ b/http/sys_policy_test.go @@ -17,8 +17,8 @@ func TestSysPolicies(t *testing.T) { var actual map[string]interface{} expected := map[string]interface{}{ - "policies": []interface{}{"default", "root"}, - "keys": []interface{}{"default", "root"}, + "policies": []interface{}{"cubbyhole-response-wrapping", "default", "root"}, + "keys": []interface{}{"cubbyhole-response-wrapping", "default", "root"}, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) @@ -62,14 +62,19 @@ func TestSysWritePolicy(t *testing.T) { var actual map[string]interface{} expected := map[string]interface{}{ - "policies": []interface{}{"default", "foo", "root"}, - "keys": []interface{}{"default", "foo", "root"}, + "policies": []interface{}{"cubbyhole-response-wrapping", "default", "foo", "root"}, + "keys": []interface{}{"cubbyhole-response-wrapping", "default", "foo", "root"}, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) if !reflect.DeepEqual(actual, expected) { t.Fatalf("bad: got\n%#v\nexpected\n%#v\n", actual, expected) } + + resp = testHttpPost(t, token, addr+"/v1/sys/policy/cubbyhole-response-wrapping", map[string]interface{}{ + "rules": ``, + }) + testResponseStatus(t, resp, 400) } func TestSysDeletePolicy(t *testing.T) { @@ -86,12 +91,17 @@ func TestSysDeletePolicy(t *testing.T) { resp = testHttpDelete(t, token, addr+"/v1/sys/policy/foo") testResponseStatus(t, resp, 204) + // Also attempt to delete these since they should not be allowed (ignore + // responses, if they exist later that's sufficient) + resp = testHttpDelete(t, token, addr+"/v1/sys/policy/default") + resp = testHttpDelete(t, token, addr+"/v1/sys/policy/cubbyhole-response-wrapping") + resp = testHttpGet(t, token, addr+"/v1/sys/policy") var actual map[string]interface{} expected := map[string]interface{}{ - "policies": []interface{}{"default", "root"}, - "keys": []interface{}{"default", "root"}, + "policies": []interface{}{"cubbyhole-response-wrapping", "default", "root"}, + "keys": []interface{}{"cubbyhole-response-wrapping", "default", "root"}, } testResponseStatus(t, resp, 200) testResponseBody(t, resp, &actual) diff --git a/logical/request.go b/logical/request.go index 2a5f5ab137..1dedb6ac49 100644 --- a/logical/request.go +++ b/logical/request.go @@ -54,9 +54,9 @@ type Request struct { // request path with the MountPoint trimmed off. MountPoint string - // WrapDuration contains the requested TTL of the token used to wrap the + // WrapTTL contains the requested TTL of the token used to wrap the // response in a cubbyhole. - WrapDuration time.Duration + WrapTTL time.Duration } // Get returns a data field and guards for nil Data diff --git a/logical/response.go b/logical/response.go index c5adc49dc8..9ad49325bf 100644 --- a/logical/response.go +++ b/logical/response.go @@ -30,7 +30,7 @@ const ( type WrapInfo struct { // Setting to non-zero specifies that the response should be wrapped. // Specifies the desired TTL of the wrapping token. - Duration time.Duration + TTL time.Duration // The token containing the wrapped response Token string @@ -132,6 +132,11 @@ func (r *Response) ClearWarnings() { r.warnings = make([]string, 0, 1) } +// Copies the warnings from the other response to this one +func (r *Response) CloneWarnings(other *Response) { + r.warnings = other.warnings +} + // IsError returns true if this response seems to indicate an error. func (r *Response) IsError() bool { return r != nil && len(r.Data) == 1 && r.Data["error"] != nil diff --git a/logical/sanitize.go b/logical/sanitize.go index ca471e6973..8731a3f40f 100644 --- a/logical/sanitize.go +++ b/logical/sanitize.go @@ -5,6 +5,7 @@ func SanitizeResponse(input *Response) *HTTPResponse { Data: input.Data, Warnings: input.Warnings(), } + if input.Secret != nil { logicalResp.LeaseID = input.Secret.LeaseID logicalResp.Renewable = input.Secret.Renewable @@ -32,6 +33,7 @@ type HTTPResponse struct { Renewable bool `json:"renewable"` LeaseDuration int `json:"lease_duration"` Data map[string]interface{} `json:"data"` + WrapInfo *HTTPWrapInfo `json:"wrap_info"` Warnings []string `json:"warnings"` Auth *HTTPAuth `json:"auth"` } @@ -44,3 +46,8 @@ type HTTPAuth struct { LeaseDuration int `json:"lease_duration"` Renewable bool `json:"renewable"` } + +type HTTPWrapInfo struct { + Token string `json:"token"` + TTL int `json:"ttl"` +} diff --git a/vault/core.go b/vault/core.go index 4c209e0665..1936ea9ed7 100644 --- a/vault/core.go +++ b/vault/core.go @@ -7,8 +7,6 @@ import ( "log" "net/url" "os" - "sort" - "strings" "sync" "time" @@ -18,7 +16,6 @@ import ( "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/helper/mlock" - "github.com/hashicorp/vault/helper/strutil" "github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/physical" "github.com/hashicorp/vault/shamir" @@ -374,303 +371,6 @@ func (c *Core) Shutdown() error { return c.sealInternal() } -// HandleRequest is used to handle a new incoming request -func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err error) { - c.stateLock.RLock() - defer c.stateLock.RUnlock() - if c.sealed { - return nil, ErrSealed - } - if c.standby { - return nil, ErrStandby - } - - // Allowing writing to a path ending in / makes it extremely difficult to - // understand user intent for the filesystem-like backends (generic, - // cubbyhole) -- did they want a key named foo/ or did they want to write - // to a directory foo/ with no (or forgotten) key, or...? It also affects - // lookup, because paths ending in / are considered prefixes by some - // backends. Basically, it's all just terrible, so don't allow it. - if strings.HasSuffix(req.Path, "/") && - (req.Operation == logical.UpdateOperation || - req.Operation == logical.CreateOperation) { - return logical.ErrorResponse("cannot write to a path ending in '/'"), nil - } - - var auth *logical.Auth - if c.router.LoginPath(req.Path) { - resp, auth, err = c.handleLoginRequest(req) - } else { - resp, auth, err = c.handleRequest(req) - } - - // Ensure we don't leak internal data - if resp != nil { - if resp.Secret != nil { - resp.Secret.InternalData = nil - } - if resp.Auth != nil { - resp.Auth.InternalData = nil - } - } - - // Create an audit trail of the response - if err := c.auditBroker.LogResponse(auth, req, resp, err); err != nil { - c.logger.Printf("[ERR] core: failed to audit response (request path: %s): %v", - req.Path, err) - return nil, ErrInternalError - } - - return -} - -func (c *Core) handleRequest(req *logical.Request) (retResp *logical.Response, retAuth *logical.Auth, retErr error) { - defer metrics.MeasureSince([]string{"core", "handle_request"}, time.Now()) - - // Validate the token - auth, te, err := c.checkToken(req) - if te != nil { - defer func() { - // Attempt to use the token (decrement num_uses) - // If a secret was generated and num_uses is currently 1, it will be - // immediately revoked; in that case, don't return the leased - // credentials as they are now invalid. - if retResp != nil && - te != nil && te.NumUses == 1 && - retResp.Secret != nil && - // Some backends return a TTL even without a Lease ID - retResp.Secret.LeaseID != "" { - retResp = logical.ErrorResponse("Secret cannot be returned; token had one use left, so leased credentials were immediately revoked.") - } - if err := c.tokenStore.UseToken(te); err != nil { - c.logger.Printf("[ERR] core: failed to use token: %v", err) - retResp = nil - retAuth = nil - retErr = ErrInternalError - } - }() - } - if err != nil { - // If it is an internal error we return that, otherwise we - // return invalid request so that the status codes can be correct - var errType error - switch err { - case ErrInternalError, logical.ErrPermissionDenied: - errType = err - default: - errType = logical.ErrInvalidRequest - } - - if err := c.auditBroker.LogRequest(auth, req, err); err != nil { - c.logger.Printf("[ERR] core: failed to audit request with path (%s): %v", - req.Path, err) - } - - return logical.ErrorResponse(err.Error()), nil, errType - } - - // Attach the display name - req.DisplayName = auth.DisplayName - - // Create an audit trail of the request - if err := c.auditBroker.LogRequest(auth, req, nil); err != nil { - c.logger.Printf("[ERR] core: failed to audit request with path (%s): %v", - req.Path, err) - return nil, auth, ErrInternalError - } - - // Route the request - resp, err := c.router.Route(req) - - // If there is a secret, we must register it with the expiration manager. - // We exclude renewal of a lease, since it does not need to be re-registered - if resp != nil && resp.Secret != nil && !strings.HasPrefix(req.Path, "sys/renew/") { - // Get the SystemView for the mount - sysView := c.router.MatchingSystemView(req.Path) - if sysView == nil { - c.logger.Println("[ERR] core: unable to retrieve system view from router") - return nil, auth, ErrInternalError - } - - // Apply the default lease if none given - if resp.Secret.TTL == 0 { - resp.Secret.TTL = sysView.DefaultLeaseTTL() - } - - // Limit the lease duration - maxTTL := sysView.MaxLeaseTTL() - if resp.Secret.TTL > maxTTL { - resp.Secret.TTL = maxTTL - } - - // Generic mounts should return the TTL but not register - // for a lease as this provides a massive slowdown - registerLease := true - matchingBackend := c.router.MatchingBackend(req.Path) - if matchingBackend == nil { - c.logger.Println("[ERR] core: unable to retrieve generic backend from router") - return nil, auth, ErrInternalError - } - if ptbe, ok := matchingBackend.(*PassthroughBackend); ok { - if !ptbe.GeneratesLeases() { - registerLease = false - resp.Secret.Renewable = false - } - } - - if registerLease { - leaseID, err := c.expiration.Register(req, resp) - if err != nil { - c.logger.Printf( - "[ERR] core: failed to register lease "+ - "(request path: %s): %v", req.Path, err) - return nil, auth, ErrInternalError - } - resp.Secret.LeaseID = leaseID - } - } - - // Only the token store is allowed to return an auth block, for any - // other request this is an internal error. We exclude renewal of a token, - // since it does not need to be re-registered - if resp != nil && resp.Auth != nil && !strings.HasPrefix(req.Path, "auth/token/renew") { - if !strings.HasPrefix(req.Path, "auth/token/") { - c.logger.Printf( - "[ERR] core: unexpected Auth response for non-token backend "+ - "(request path: %s)", req.Path) - return nil, auth, ErrInternalError - } - - // Register with the expiration manager. We use the token's actual path - // here because roles allow suffixes. - te, err := c.tokenStore.Lookup(resp.Auth.ClientToken) - if err != nil { - c.logger.Printf("[ERR] core: failed to lookup token: %v", err) - return nil, nil, ErrInternalError - } - - if err := c.expiration.RegisterAuth(te.Path, resp.Auth); err != nil { - c.logger.Printf("[ERR] core: failed to register token lease "+ - "(request path: %s): %v", req.Path, err) - return nil, auth, ErrInternalError - } - } - - // Return the response and error - return resp, auth, err -} - -// handleLoginRequest is used to handle a login request, which is an -// unauthenticated request to the backend. -func (c *Core) handleLoginRequest(req *logical.Request) (*logical.Response, *logical.Auth, error) { - defer metrics.MeasureSince([]string{"core", "handle_login_request"}, time.Now()) - - // Create an audit trail of the request, auth is not available on login requests - if err := c.auditBroker.LogRequest(nil, req, nil); err != nil { - c.logger.Printf("[ERR] core: failed to audit request with path %s: %v", - req.Path, err) - return nil, nil, ErrInternalError - } - - // Route the request - resp, err := c.router.Route(req) - - // A login request should never return a secret! - if resp != nil && resp.Secret != nil { - c.logger.Printf("[ERR] core: unexpected Secret response for login path"+ - "(request path: %s)", req.Path) - return nil, nil, ErrInternalError - } - - // If the response generated an authentication, then generate the token - var auth *logical.Auth - if resp != nil && resp.Auth != nil { - auth = resp.Auth - - // Determine the source of the login - source := c.router.MatchingMount(req.Path) - source = strings.TrimPrefix(source, credentialRoutePrefix) - source = strings.Replace(source, "/", "-", -1) - - // Prepend the source to the display name - auth.DisplayName = strings.TrimSuffix(source+auth.DisplayName, "-") - - sysView := c.router.MatchingSystemView(req.Path) - if sysView == nil { - c.logger.Printf("[ERR] core: unable to look up sys view for login path"+ - "(request path: %s)", req.Path) - return nil, nil, ErrInternalError - } - - // Set the default lease if non-provided, root tokens are exempt - if auth.TTL == 0 && !strutil.StrListContains(auth.Policies, "root") { - auth.TTL = sysView.DefaultLeaseTTL() - } - - // Limit the lease duration - if auth.TTL > sysView.MaxLeaseTTL() { - auth.TTL = sysView.MaxLeaseTTL() - } - - // Generate a token - te := TokenEntry{ - Path: req.Path, - Policies: auth.Policies, - Meta: auth.Metadata, - DisplayName: auth.DisplayName, - CreationTime: time.Now().Unix(), - TTL: auth.TTL, - } - - if strutil.StrListSubset(te.Policies, []string{"root"}) { - te.Policies = []string{"root"} - } else { - // Use a map to filter out/prevent duplicates - policyMap := map[string]bool{} - for _, policy := range te.Policies { - if policy == "" { - // Don't allow a policy with no name, even though it is a valid - // slice member - continue - } - policyMap[policy] = true - } - - // Add the default policy - policyMap["default"] = true - - te.Policies = []string{} - for k, _ := range policyMap { - te.Policies = append(te.Policies, k) - } - - sort.Strings(te.Policies) - } - - if err := c.tokenStore.create(&te); err != nil { - c.logger.Printf("[ERR] core: failed to create token: %v", err) - return nil, auth, ErrInternalError - } - - // Populate the client token and accessor - auth.ClientToken = te.ID - auth.Accessor = te.Accessor - auth.Policies = te.Policies - - // Register with the expiration manager - if err := c.expiration.RegisterAuth(te.Path, auth); err != nil { - c.logger.Printf("[ERR] core: failed to register token lease "+ - "(request path: %s): %v", req.Path, err) - return nil, auth, ErrInternalError - } - - // Attach the display name, might be used by audit backends - req.DisplayName = auth.DisplayName - } - - return resp, auth, err -} - func (c *Core) fetchACLandTokenEntry(req *logical.Request) (*ACL, *TokenEntry, error) { defer metrics.MeasureSince([]string{"core", "fetch_acl_and_token"}, time.Now()) diff --git a/vault/logical_system.go b/vault/logical_system.go index 52f1c7a0b3..2af8f277ea 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -1086,6 +1086,7 @@ func (b *SystemBackend) handlePolicySet( func (b *SystemBackend) handlePolicyDelete( req *logical.Request, data *framework.FieldData) (*logical.Response, error) { name := data.Get("name").(string) + if err := b.Core.policyStore.DeletePolicy(name); err != nil { return handleError(err) } diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index 97b0a6a4d8..f38f448325 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -610,8 +610,8 @@ func TestSystemBackend_policyList(t *testing.T) { } exp := map[string]interface{}{ - "keys": []string{"default", "root"}, - "policies": []string{"default", "root"}, + "keys": []string{"cubbyhole-response-wrapping", "default", "root"}, + "policies": []string{"cubbyhole-response-wrapping", "default", "root"}, } if !reflect.DeepEqual(resp.Data, exp) { t.Fatalf("got: %#v expect: %#v", resp.Data, exp) @@ -663,8 +663,8 @@ func TestSystemBackend_policyCRUD(t *testing.T) { } exp = map[string]interface{}{ - "keys": []string{"default", "foo", "root"}, - "policies": []string{"default", "foo", "root"}, + "keys": []string{"cubbyhole-response-wrapping", "default", "foo", "root"}, + "policies": []string{"cubbyhole-response-wrapping", "default", "foo", "root"}, } if !reflect.DeepEqual(resp.Data, exp) { t.Fatalf("got: %#v expect: %#v", resp.Data, exp) @@ -698,8 +698,8 @@ func TestSystemBackend_policyCRUD(t *testing.T) { } exp = map[string]interface{}{ - "keys": []string{"default", "root"}, - "policies": []string{"default", "root"}, + "keys": []string{"cubbyhole-response-wrapping", "default", "root"}, + "policies": []string{"cubbyhole-response-wrapping", "default", "root"}, } if !reflect.DeepEqual(resp.Data, exp) { t.Fatalf("got: %#v expect: %#v", resp.Data, exp) diff --git a/vault/policy_store.go b/vault/policy_store.go index 8bbb79a8de..6d3d32ad29 100644 --- a/vault/policy_store.go +++ b/vault/policy_store.go @@ -17,6 +17,14 @@ const ( // policyCacheSize is the number of policies that are kept cached policyCacheSize = 1024 + + // cubbyholeResponseWrappingPolicy is the policy that ensures cubbyhole + // response wrapping can always succeed + cubbyholeResponseWrappingPolicy = ` +path "cubbyhole/response" { + capabilities = ["create", "read"] +} +` ) // PolicyStore is used to provide durable storage of policy, and to @@ -63,6 +71,19 @@ func (c *Core) setupPolicyStore() error { return err } } + + // Ensure that the cubbyhole response wrapping policy exists + policy, err = c.policyStore.GetPolicy("cubbyhole-response-wrapping") + if err != nil { + return errwrap.Wrapf("error fetching default policy from store: {{err}}", err) + } + if policy == nil || policy.Raw != cubbyholeResponseWrappingPolicy { + err := c.policyStore.createCubbyholeResponseWrappingPolicy() + if err != nil { + return err + } + } + return nil } @@ -79,10 +100,17 @@ func (ps *PolicyStore) SetPolicy(p *Policy) error { if p.Name == "root" { return fmt.Errorf("cannot update root policy") } + if p.Name == "cubbyhole-response-wrapping" { + return fmt.Errorf("cannot update cubbyhole-response-wrapping policy") + } if p.Name == "" { return fmt.Errorf("policy name missing") } + return ps.setPolicyInternal(p) +} + +func (ps *PolicyStore) setPolicyInternal(p *Policy) error { // Create the entry entry, err := logical.StorageEntryJSON(p.Name, &PolicyEntry{ Version: 2, @@ -174,6 +202,9 @@ func (ps *PolicyStore) DeletePolicy(name string) error { if name == "default" { return fmt.Errorf("cannot delete default policy") } + if name == "cubbyhole-response-wrapping" { + return fmt.Errorf("cannot delete cubbyhole-response-wrapping policy") + } if err := ps.view.Delete(name); err != nil { return fmt.Errorf("failed to delete policy: %v", err) } @@ -235,5 +266,19 @@ path "cubbyhole" { } policy.Name = "default" - return ps.SetPolicy(policy) + return ps.setPolicyInternal(policy) +} + +func (ps *PolicyStore) createCubbyholeResponseWrappingPolicy() error { + policy, err := Parse(cubbyholeResponseWrappingPolicy) + if err != nil { + return errwrap.Wrapf("error parsing cubbyhole-response-wrapping policy: {{err}}", err) + } + + if policy == nil { + return fmt.Errorf("parsing cubbyhole-response-wrapping policy resulted in nil policy") + } + + policy.Name = "cubbyhole-response-wrapping" + return ps.setPolicyInternal(policy) } diff --git a/vault/policy_store_test.go b/vault/policy_store_test.go index 4e4b8fe0f9..4a071d32ef 100644 --- a/vault/policy_store_test.go +++ b/vault/policy_store_test.go @@ -110,6 +110,32 @@ func TestPolicyStore_CRUD(t *testing.T) { } } +// Test predefined policy handling +func TestPolicyStore_Predefined(t *testing.T) { + core, _, _ := TestCoreUnsealed(t) + // Ensure both default policies are created + err := core.setupPolicyStore() + if err != nil { + t.Fatalf("err: %v", err) + } + // List should be two elements + out, err := core.policyStore.ListPolicies() + if err != nil { + t.Fatalf("err: %v", err) + } + if len(out) != 2 || out[0] != "cubbyhole-response-wrapping" || out[1] != "default" { + t.Fatalf("bad: %v", out) + } + + p, err := core.policyStore.GetPolicy("cubbyhole-response-wrapping") + if err != nil { + t.Fatalf("err: %v", err) + } + if p.Raw != cubbyholeResponseWrappingPolicy { + t.Fatalf("bad: expected\n%s\ngot\n%s\n", cubbyholeResponseWrappingPolicy, p.Raw) + } +} + func TestPolicyStore_ACL(t *testing.T) { ps := mockPolicyStore(t) diff --git a/vault/request_handling.go b/vault/request_handling.go new file mode 100644 index 0000000000..6974479029 --- /dev/null +++ b/vault/request_handling.go @@ -0,0 +1,407 @@ +package vault + +import ( + "sort" + "strings" + "sync" + "time" + + "github.com/armon/go-metrics" + "github.com/hashicorp/vault/helper/strutil" + "github.com/hashicorp/vault/logical" +) + +var ( + // Value for memoizing whether cubbyhole is mounted, e.g. if we are in normal operation and not test mode + cubbyholeMounted *bool + + // mutex to ensure the same + cubbyholeMountedMutex sync.Mutex +) + +// HandleRequest is used to handle a new incoming request +func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err error) { + c.stateLock.RLock() + defer c.stateLock.RUnlock() + if c.sealed { + return nil, ErrSealed + } + if c.standby { + return nil, ErrStandby + } + + // Allowing writing to a path ending in / makes it extremely difficult to + // understand user intent for the filesystem-like backends (generic, + // cubbyhole) -- did they want a key named foo/ or did they want to write + // to a directory foo/ with no (or forgotten) key, or...? It also affects + // lookup, because paths ending in / are considered prefixes by some + // backends. Basically, it's all just terrible, so don't allow it. + if strings.HasSuffix(req.Path, "/") && + (req.Operation == logical.UpdateOperation || + req.Operation == logical.CreateOperation) { + return logical.ErrorResponse("cannot write to a path ending in '/'"), nil + } + + var auth *logical.Auth + if c.router.LoginPath(req.Path) { + resp, auth, err = c.handleLoginRequest(req) + } else { + resp, auth, err = c.handleRequest(req) + } + + // Ensure we don't leak internal data + if resp != nil { + if resp.Secret != nil { + resp.Secret.InternalData = nil + } + if resp.Auth != nil { + resp.Auth.InternalData = nil + } + } + + // In order to wrap, we need cubbyhole to be mounted, so we ensure that + // cubbyhole is actually mounted, as it may not be during tests. We memoize + // this response, since cubbyhole cannot be mounted or unmounted during + // normal operation. + if cubbyholeMounted == nil { + cubbyholeMountedMutex.Lock() + cubbyholeMounted = new(bool) + // Ensure it wasn't changed by another goroutine + if cubbyholeMounted == nil { + if c.router.MatchingMount("cubbyhole") != "" { + *cubbyholeMounted = true + } else { + *cubbyholeMounted = false + } + } + cubbyholeMountedMutex.Unlock() + } + + // We are wrapping if there is anything to wrap (not a nil response) and a + // TTL was specified for the token, plus if cubbyhole is mounted (which + // will be the case normally) + wrapping := *cubbyholeMounted && resp != nil && resp.WrapInfo.TTL != 0 + + // If we are wrapping, the first part happens before auditing so that + // resp.WrapInfo.Token can contain the HMAC'd wrapping token ID in the + // audit logs, so that it can be determined from the audit logs whether the + // token was ever actually used. + if wrapping { + // Create the wrapping token + te := TokenEntry{ + Path: req.Path, + Policies: []string{"cubbyhole-response-wrapping"}, + CreationTime: time.Now().Unix(), + TTL: resp.WrapInfo.TTL, + NumUses: 1, + } + + if err := c.tokenStore.create(&te); err != nil { + c.logger.Printf("[ERR] core: failed to create wrapping token: %v", err) + return nil, ErrInternalError + } + + resp.WrapInfo.Token = te.ID + + httpResponse := logical.SanitizeResponse(resp) + + cubbyReq := &logical.Request{ + Operation: logical.CreateOperation, + Path: "cubbyhole/response", + ClientToken: te.ID, + Data: map[string]interface{}{ + "response": httpResponse, + }, + } + + _, err = c.router.Route(cubbyReq) + if err != nil { + c.logger.Printf("[ERR] core: failed to store wrapped response information: %v", err) + return nil, ErrInternalError + } + + auth := &logical.Auth{ + ClientToken: te.ID, + Policies: []string{"cubbyhole-response-wrapping"}, + LeaseOptions: logical.LeaseOptions{ + TTL: te.TTL, + Renewable: false, + }, + } + + // Register the wrapped token with the expiration manager + if err := c.expiration.RegisterAuth(te.Path, auth); err != nil { + c.logger.Printf("[ERR] core: failed to register cubbyhole wrapping token lease "+ + "(request path: %s): %v", req.Path, err) + return nil, ErrInternalError + } + } + + // Create an audit trail of the response + if err := c.auditBroker.LogResponse(auth, req, resp, err); err != nil { + c.logger.Printf("[ERR] core: failed to audit response (request path: %s): %v", + req.Path, err) + return nil, ErrInternalError + } + + // If we are wrapping, now is when we create a new response object with the + // wrapped information, since the original response has been audit logged + if wrapping { + wrappingResp := &logical.Response{ + WrapInfo: logical.WrapInfo{ + Token: resp.WrapInfo.Token, + }, + } + wrappingResp.CloneWarnings(resp) + resp = wrappingResp + } + + return +} + +func (c *Core) handleRequest(req *logical.Request) (retResp *logical.Response, retAuth *logical.Auth, retErr error) { + defer metrics.MeasureSince([]string{"core", "handle_request"}, time.Now()) + + // Validate the token + auth, te, err := c.checkToken(req) + if te != nil { + defer func() { + // Attempt to use the token (decrement num_uses) + // If a secret was generated and num_uses is currently 1, it will be + // immediately revoked; in that case, don't return the leased + // credentials as they are now invalid. + if retResp != nil && + te != nil && te.NumUses == 1 && + retResp.Secret != nil && + // Some backends return a TTL even without a Lease ID + retResp.Secret.LeaseID != "" { + retResp = logical.ErrorResponse("Secret cannot be returned; token had one use left, so leased credentials were immediately revoked.") + } + if err := c.tokenStore.UseToken(te); err != nil { + c.logger.Printf("[ERR] core: failed to use token: %v", err) + retResp = nil + retAuth = nil + retErr = ErrInternalError + } + }() + } + if err != nil { + // If it is an internal error we return that, otherwise we + // return invalid request so that the status codes can be correct + var errType error + switch err { + case ErrInternalError, logical.ErrPermissionDenied: + errType = err + default: + errType = logical.ErrInvalidRequest + } + + if err := c.auditBroker.LogRequest(auth, req, err); err != nil { + c.logger.Printf("[ERR] core: failed to audit request with path (%s): %v", + req.Path, err) + } + + return logical.ErrorResponse(err.Error()), nil, errType + } + + // Attach the display name + req.DisplayName = auth.DisplayName + + // Create an audit trail of the request + if err := c.auditBroker.LogRequest(auth, req, nil); err != nil { + c.logger.Printf("[ERR] core: failed to audit request with path (%s): %v", + req.Path, err) + return nil, auth, ErrInternalError + } + + // Route the request + resp, err := c.router.Route(req) + + // If there is a secret, we must register it with the expiration manager. + // We exclude renewal of a lease, since it does not need to be re-registered + if resp != nil && resp.Secret != nil && !strings.HasPrefix(req.Path, "sys/renew/") { + // Get the SystemView for the mount + sysView := c.router.MatchingSystemView(req.Path) + if sysView == nil { + c.logger.Println("[ERR] core: unable to retrieve system view from router") + return nil, auth, ErrInternalError + } + + // Apply the default lease if none given + if resp.Secret.TTL == 0 { + resp.Secret.TTL = sysView.DefaultLeaseTTL() + } + + // Limit the lease duration + maxTTL := sysView.MaxLeaseTTL() + if resp.Secret.TTL > maxTTL { + resp.Secret.TTL = maxTTL + } + + // Generic mounts should return the TTL but not register + // for a lease as this provides a massive slowdown + registerLease := true + matchingBackend := c.router.MatchingBackend(req.Path) + if matchingBackend == nil { + c.logger.Println("[ERR] core: unable to retrieve generic backend from router") + return nil, auth, ErrInternalError + } + if ptbe, ok := matchingBackend.(*PassthroughBackend); ok { + if !ptbe.GeneratesLeases() { + registerLease = false + resp.Secret.Renewable = false + } + } + + if registerLease { + leaseID, err := c.expiration.Register(req, resp) + if err != nil { + c.logger.Printf( + "[ERR] core: failed to register lease "+ + "(request path: %s): %v", req.Path, err) + return nil, auth, ErrInternalError + } + resp.Secret.LeaseID = leaseID + } + } + + // Only the token store is allowed to return an auth block, for any + // other request this is an internal error. We exclude renewal of a token, + // since it does not need to be re-registered + if resp != nil && resp.Auth != nil && !strings.HasPrefix(req.Path, "auth/token/renew") { + if !strings.HasPrefix(req.Path, "auth/token/") { + c.logger.Printf( + "[ERR] core: unexpected Auth response for non-token backend "+ + "(request path: %s)", req.Path) + return nil, auth, ErrInternalError + } + + // Register with the expiration manager. We use the token's actual path + // here because roles allow suffixes. + te, err := c.tokenStore.Lookup(resp.Auth.ClientToken) + if err != nil { + c.logger.Printf("[ERR] core: failed to lookup token: %v", err) + return nil, nil, ErrInternalError + } + + if err := c.expiration.RegisterAuth(te.Path, resp.Auth); err != nil { + c.logger.Printf("[ERR] core: failed to register token lease "+ + "(request path: %s): %v", req.Path, err) + return nil, auth, ErrInternalError + } + } + + // Return the response and error + return resp, auth, err +} + +// handleLoginRequest is used to handle a login request, which is an +// unauthenticated request to the backend. +func (c *Core) handleLoginRequest(req *logical.Request) (*logical.Response, *logical.Auth, error) { + defer metrics.MeasureSince([]string{"core", "handle_login_request"}, time.Now()) + + // Create an audit trail of the request, auth is not available on login requests + if err := c.auditBroker.LogRequest(nil, req, nil); err != nil { + c.logger.Printf("[ERR] core: failed to audit request with path %s: %v", + req.Path, err) + return nil, nil, ErrInternalError + } + + // Route the request + resp, err := c.router.Route(req) + + // A login request should never return a secret! + if resp != nil && resp.Secret != nil { + c.logger.Printf("[ERR] core: unexpected Secret response for login path"+ + "(request path: %s)", req.Path) + return nil, nil, ErrInternalError + } + + // If the response generated an authentication, then generate the token + var auth *logical.Auth + if resp != nil && resp.Auth != nil { + auth = resp.Auth + + // Determine the source of the login + source := c.router.MatchingMount(req.Path) + source = strings.TrimPrefix(source, credentialRoutePrefix) + source = strings.Replace(source, "/", "-", -1) + + // Prepend the source to the display name + auth.DisplayName = strings.TrimSuffix(source+auth.DisplayName, "-") + + sysView := c.router.MatchingSystemView(req.Path) + if sysView == nil { + c.logger.Printf("[ERR] core: unable to look up sys view for login path"+ + "(request path: %s)", req.Path) + return nil, nil, ErrInternalError + } + + // Set the default lease if non-provided, root tokens are exempt + if auth.TTL == 0 && !strutil.StrListContains(auth.Policies, "root") { + auth.TTL = sysView.DefaultLeaseTTL() + } + + // Limit the lease duration + if auth.TTL > sysView.MaxLeaseTTL() { + auth.TTL = sysView.MaxLeaseTTL() + } + + // Generate a token + te := TokenEntry{ + Path: req.Path, + Policies: auth.Policies, + Meta: auth.Metadata, + DisplayName: auth.DisplayName, + CreationTime: time.Now().Unix(), + TTL: auth.TTL, + } + + if strutil.StrListSubset(te.Policies, []string{"root"}) { + te.Policies = []string{"root"} + } else { + // Use a map to filter out/prevent duplicates + policyMap := map[string]bool{} + for _, policy := range te.Policies { + if policy == "" { + // Don't allow a policy with no name, even though it is a valid + // slice member + continue + } + policyMap[policy] = true + } + + // Add the default policy + policyMap["default"] = true + + te.Policies = []string{} + for k, _ := range policyMap { + te.Policies = append(te.Policies, k) + } + + sort.Strings(te.Policies) + } + + if err := c.tokenStore.create(&te); err != nil { + c.logger.Printf("[ERR] core: failed to create token: %v", err) + return nil, auth, ErrInternalError + } + + // Populate the client token and accessor + auth.ClientToken = te.ID + auth.Accessor = te.Accessor + auth.Policies = te.Policies + + // Register with the expiration manager + if err := c.expiration.RegisterAuth(te.Path, auth); err != nil { + c.logger.Printf("[ERR] core: failed to register token lease "+ + "(request path: %s): %v", req.Path, err) + return nil, auth, ErrInternalError + } + + // Attach the display name, might be used by audit backends + req.DisplayName = auth.DisplayName + } + + return resp, auth, err +} diff --git a/vault/router.go b/vault/router.go index 803b2757e8..377afa32ae 100644 --- a/vault/router.go +++ b/vault/router.go @@ -261,19 +261,19 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (resp *l // If either of the request or response requested wrapping, ensure that // the lowest value is what ends up in the response. switch { - case req.WrapDuration == 0 && resp.WrapInfo.Duration == 0: - case req.WrapDuration != 0 && resp.WrapInfo.Duration != 0: - if req.WrapDuration < resp.WrapInfo.Duration { - resp.WrapInfo.Duration = req.WrapDuration + case req.WrapTTL == 0 && resp.WrapInfo.TTL == 0: + case req.WrapTTL != 0 && resp.WrapInfo.TTL != 0: + if req.WrapTTL < resp.WrapInfo.TTL { + resp.WrapInfo.TTL = req.WrapTTL } - case req.WrapDuration != 0: - resp.WrapInfo.Duration = req.WrapDuration + case req.WrapTTL != 0: + resp.WrapInfo.TTL = req.WrapTTL // Only case left is that only resp defines it, which doesn't need to // be explicitly handled } // Now set the mount point if we are wrapping - if resp.WrapInfo.Duration != 0 { + if resp.WrapInfo.TTL != 0 { resp.WrapInfo.MountPoint = mount } } diff --git a/vault/router_test.go b/vault/router_test.go index ec1611b90b..5530d85227 100644 --- a/vault/router_test.go +++ b/vault/router_test.go @@ -20,7 +20,7 @@ type NoopBackend struct { Requests []*logical.Request Response *logical.Response - WrapDuration time.Duration + WrapTTL time.Duration } func (n *NoopBackend) HandleRequest(req *logical.Request) (*logical.Response, error) { @@ -34,12 +34,12 @@ func (n *NoopBackend) HandleRequest(req *logical.Request) (*logical.Response, er return nil, fmt.Errorf("missing view") } - if n.Response == nil && (req.WrapDuration != 0 || n.WrapDuration != 0) { + if n.Response == nil && (req.WrapTTL != 0 || n.WrapTTL != 0) { n.Response = &logical.Response{} } - if n.WrapDuration != 0 { - n.Response.WrapInfo.Duration = n.WrapDuration + if n.WrapTTL != 0 { + n.Response.WrapInfo.TTL = n.WrapTTL } return n.Response, nil @@ -420,10 +420,10 @@ func TestRouter_Wrapping(t *testing.T) { // Just in the request req = &logical.Request{ - Path: "wraptest/foo", - ClientToken: root, - Operation: logical.UpdateOperation, - WrapDuration: time.Duration(15 * time.Second), + Path: "wraptest/foo", + ClientToken: root, + Operation: logical.UpdateOperation, + WrapTTL: time.Duration(15 * time.Second), } resp, err = core.HandleRequest(req) if err != nil { @@ -432,13 +432,13 @@ func TestRouter_Wrapping(t *testing.T) { if resp == nil { t.Fatalf("bad: %v", resp) } - if resp.WrapInfo.Duration != time.Duration(15*time.Second) || + if resp.WrapInfo.TTL != time.Duration(15*time.Second) || resp.WrapInfo.MountPoint != "wraptest/" { t.Fatalf("bad: %#v", resp) } // Just in the response - n.WrapDuration = time.Duration(15 * time.Second) + n.WrapTTL = time.Duration(15 * time.Second) req = &logical.Request{ Path: "wraptest/foo", ClientToken: root, @@ -451,18 +451,18 @@ func TestRouter_Wrapping(t *testing.T) { if resp == nil { t.Fatalf("bad: %v", resp) } - if resp.WrapInfo.Duration != time.Duration(15*time.Second) || + if resp.WrapInfo.TTL != time.Duration(15*time.Second) || resp.WrapInfo.MountPoint != "wraptest/" { t.Fatalf("bad: %#v", resp) } // In both, with request less - n.WrapDuration = time.Duration(15 * time.Second) + n.WrapTTL = time.Duration(15 * time.Second) req = &logical.Request{ - Path: "wraptest/foo", - ClientToken: root, - Operation: logical.UpdateOperation, - WrapDuration: time.Duration(10 * time.Second), + Path: "wraptest/foo", + ClientToken: root, + Operation: logical.UpdateOperation, + WrapTTL: time.Duration(10 * time.Second), } resp, err = core.HandleRequest(req) if err != nil { @@ -471,18 +471,18 @@ func TestRouter_Wrapping(t *testing.T) { if resp == nil { t.Fatalf("bad: %v", resp) } - if resp.WrapInfo.Duration != time.Duration(10*time.Second) || + if resp.WrapInfo.TTL != time.Duration(10*time.Second) || resp.WrapInfo.MountPoint != "wraptest/" { t.Fatalf("bad: %#v", resp) } // In both, with response less - n.WrapDuration = time.Duration(10 * time.Second) + n.WrapTTL = time.Duration(10 * time.Second) req = &logical.Request{ - Path: "wraptest/foo", - ClientToken: root, - Operation: logical.UpdateOperation, - WrapDuration: time.Duration(15 * time.Second), + Path: "wraptest/foo", + ClientToken: root, + Operation: logical.UpdateOperation, + WrapTTL: time.Duration(15 * time.Second), } resp, err = core.HandleRequest(req) if err != nil { @@ -491,7 +491,7 @@ func TestRouter_Wrapping(t *testing.T) { if resp == nil { t.Fatalf("bad: %v", resp) } - if resp.WrapInfo.Duration != time.Duration(10*time.Second) || + if resp.WrapInfo.TTL != time.Duration(10*time.Second) || resp.WrapInfo.MountPoint != "wraptest/" { t.Fatalf("bad: %#v", resp) }