From e8663801458a57538f9687c7e2c1ded25b7d3e32 Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Tue, 23 Oct 2018 04:26:01 -0700 Subject: [PATCH 01/15] Adding support for Consul 1.4 ACL system --- builtin/logical/consul/path_roles.go | 53 ++- builtin/logical/consul/path_token.go | 63 ++- builtin/logical/consul/secret_token.go | 16 +- "builtin/logical/consul/\302\261" | 182 +++++++++ vendor/github.com/hashicorp/consul/api/acl.go | 364 +++++++++++++++++- .../github.com/hashicorp/consul/api/agent.go | 18 + .../github.com/hashicorp/consul/api/debug.go | 106 +++++ vendor/vendor.json | 6 +- .../source/api/secret/consul/index.html.md | 28 +- .../source/docs/secrets/consul/index.html.md | 42 +- 10 files changed, 826 insertions(+), 52 deletions(-) create mode 100644 "builtin/logical/consul/\302\261" create mode 100644 vendor/github.com/hashicorp/consul/api/debug.go diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index db9c2fe9dade..f07e9c7e643f 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -32,7 +32,13 @@ func pathRoles() *framework.Path { "policy": &framework.FieldSchema{ Type: framework.TypeString, Description: `Policy document, base64 encoded. Required -for 'client' tokens.`, +for 'client' tokens. Required for Consul pre-1.4`, + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `List of policies attached to the token. Required +for Consul 1.4 or above`, }, "token_type": &framework.FieldSchema{ @@ -97,35 +103,48 @@ func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.Field if result.Policy != "" { resp.Data["policy"] = base64.StdEncoding.EncodeToString([]byte(result.Policy)) } + if len(result.Policies) > 0 { + resp.Data["policies"] = result.Policies + } return resp, nil } func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { tokenType := d.Get("token_type").(string) + policy := d.Get("policy").(string) + policies := d.Get("policies").([]string) + if len(policies) == 0 { + switch tokenType { + case "client": + case "management": + default: + return logical.ErrorResponse( + "token_type must be \"client\" or \"management\""), nil + } + } - switch tokenType { - case "client": - case "management": - default: + if policy != "" && len(policies) > 0 { return logical.ErrorResponse( - "token_type must be \"client\" or \"management\""), nil + "Use either a policy document, or a list of policies, depending on your Consul version"), nil } name := d.Get("name").(string) - policy := d.Get("policy").(string) + var policyRaw []byte var err error - if tokenType != "management" { - if policy == "" { - return logical.ErrorResponse( - "policy cannot be empty when not using management tokens"), nil - } - policyRaw, err = base64.StdEncoding.DecodeString(d.Get("policy").(string)) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf( - "Error decoding policy base64: %s", err)), nil + if len(policies) == 0 { + if tokenType != "management" { + if policy == "" { + return logical.ErrorResponse( + "policy cannot be empty when not using management tokens"), nil + } } } + policyRaw, err = base64.StdEncoding.DecodeString(d.Get("policy").(string)) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Error decoding policy base64: %s", err)), nil + } var lease time.Duration leaseParamRaw, ok := d.GetOk("lease") @@ -135,6 +154,7 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{ Policy: string(policyRaw), + Policies: []string(policies), Lease: lease, TokenType: tokenType, }) @@ -159,6 +179,7 @@ func pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.Fie type roleConfig struct { Policy string `json:"policy"` + Policies []string `json:"policies"` Lease time.Duration `json:"lease"` TokenType string `json:"token_type"` } diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go index d79651e80d15..844d660ea035 100644 --- a/builtin/logical/consul/path_token.go +++ b/builtin/logical/consul/path_token.go @@ -61,25 +61,54 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr writeOpts := &api.WriteOptions{} writeOpts = writeOpts.WithContext(ctx) - - // Create it - token, _, err := c.ACL().Create(&api.ACLEntry{ - Name: tokenName, - Type: result.TokenType, - Rules: result.Policy, - }, writeOpts) - if err != nil { - return logical.ErrorResponse(err.Error()), nil + var s *logical.Response + // Create an ACLEntry for Consul pre 1.4 + if result.Policy != "" { + token, _, err := c.ACL().Create(&api.ACLEntry{ + Name: tokenName, + Type: result.TokenType, + Rules: result.Policy, + }, writeOpts) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + // Use the helper to create the secret + s = b.Secret(SecretTokenType).Response(map[string]interface{}{ + "token": token, + }, map[string]interface{}{ + "token": token, + "role": role, + "version": "1.3", + }) + s.Secret.TTL = result.Lease } - // Use the helper to create the secret - s := b.Secret(SecretTokenType).Response(map[string]interface{}{ - "token": token, - }, map[string]interface{}{ - "token": token, - "role": role, - }) - s.Secret.TTL = result.Lease + //Create an ACLToken for Consul 1.4 and above + if len(result.Policies) > 0 { + var policyLink = []*api.ACLTokenPolicyLink{} + for _, policyName := range result.Policies { + policyLink = append(policyLink, &api.ACLTokenPolicyLink{ + Name: policyName, + }) + } + token, _, err := c.ACL().TokenCreate(&api.ACLToken{ + Description: tokenName, + Policies: policyLink, + }, writeOpts) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + // Use the helper to create the secret + s = b.Secret(SecretTokenType).Response(map[string]interface{}{ + "token": token.SecretID, + "accessor": token.AccessorID, + }, map[string]interface{}{ + "token": token.AccessorID, + "role": role, + "version": "1.4", + }) + s.Secret.TTL = result.Lease + } return s, nil } diff --git a/builtin/logical/consul/secret_token.go b/builtin/logical/consul/secret_token.go index 45bf7ff4d1a4..4a2cfb624a26 100644 --- a/builtin/logical/consul/secret_token.go +++ b/builtin/logical/consul/secret_token.go @@ -67,6 +67,7 @@ func secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.F } tokenRaw, ok := req.Secret.InternalData["token"] + version, ok := req.Secret.InternalData["version"] if !ok { // We return nil here because this is a pre-0.5.3 problem and there is // nothing we can do about it. We already can't revoke the lease @@ -75,9 +76,18 @@ func secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.F return nil, nil } - _, err := c.ACL().Destroy(tokenRaw.(string), nil) - if err != nil { - return nil, err + if version == "1.3" || version == nil { + _, err := c.ACL().Destroy(tokenRaw.(string), nil) + if err != nil { + return nil, err + } + } + + if version == "1.4" { + _, err := c.ACL().TokenDelete(tokenRaw.(string), nil) + if err != nil { + return nil, err + } } return nil, nil diff --git "a/builtin/logical/consul/\302\261" "b/builtin/logical/consul/\302\261" new file mode 100644 index 000000000000..a8740e1461d2 --- /dev/null +++ "b/builtin/logical/consul/\302\261" @@ -0,0 +1,182 @@ +package consul + +import ( + "context" + "encoding/base64" + "fmt" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathListRoles(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "roles/?$", + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ListOperation: b.pathRoleList, + }, + } +} + +func pathRoles() *framework.Path { + return &framework.Path{ + Pattern: "roles/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role", + }, + + "policy": &framework.FieldSchema{ + Type: framework.TypeString, + Description: `Policy document, base64 encoded. Required +for 'client' tokens. Required for Consul pre-1.4`, + }, + + "policies": &framework.FieldSchema{ + Type: framework.TypeCommaStringSlice, + Description: `List of policies attached to the token. Required +for Consul 1.4 or above`, + }, + + "token_type": &framework.FieldSchema{ + Type: framework.TypeString, + Default: "client", + Description: `Which type of token to create: 'client' +or 'management'. If a 'management' token, +the "policy" parameter is not required. +Defaults to 'client'.`, + }, + + "lease": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: "Lease time of the role.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: pathRolesRead, + logical.UpdateOperation: pathRolesWrite, + logical.DeleteOperation: pathRolesDelete, + }, + } +} + +func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + entries, err := req.Storage.List(ctx, "policy/") + if err != nil { + return nil, err + } + + return logical.ListResponse(entries), nil +} + +func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + entry, err := req.Storage.Get(ctx, "policy/"+name) + if err != nil { + return nil, err + } + if entry == nil { + return nil, nil + } + + var result roleConfig + if err := entry.DecodeJSON(&result); err != nil { + return nil, err + } + + if result.TokenType == "" { + result.TokenType = "client" + } + + // Generate the response + resp := &logical.Response{ + Data: map[string]interface{}{ + "lease": int64(result.Lease.Seconds()), + "token_type": result.TokenType, + }, + } + if result.Policy != "" { + resp.Data["policy"] = base64.StdEncoding.EncodeToString([]byte(result.Policy)) + } + if len(result.Policies) > 0 { + resp.Data["policies"] = result.Policies + } + return resp, nil +} + +func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + tokenType := d.Get("token_type").(string) + policy := d.Get("policy").(string) + policies := d.Get("policies").([]string) + if policy != "" { + switch tokenType { + case "client": + case "management": + default: + return logical.ErrorResponse( + "token_type must be \"client\" or \"management\""), nil + } + + if policy != "" && len(policies) > 0 { + return logical.ErrorResponse( + "Use either a policy document, or a list of policies, depending on your Consul version"), nil + } + + name := d.Get("name").(string) + + var policyRaw []byte + var err error + if tokenType != "management" { + if policy == "" { + return logical.ErrorResponse( + "policy cannot be empty when not using management tokens"), nil + } + policyRaw, err = base64.StdEncoding.DecodeString(d.Get("policy").(string)) + if err != nil { + return logical.ErrorResponse(fmt.Sprintf( + "Error decoding policy base64: %s", err)), nil + } + } + + var lease time.Duration + leaseParamRaw, ok := d.GetOk("lease") + if ok { + lease = time.Second * time.Duration(leaseParamRaw.(int)) + } + + entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{ + Policy: string(policyRaw), + Policies: []string(policies), + Lease: lease, + TokenType: tokenType, + }) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(ctx, entry); err != nil { + return nil, err + } + + return nil, nil +} + +func pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + if err := req.Storage.Delete(ctx, "policy/"+name); err != nil { + return nil, err + } + return nil, nil +} + +type roleConfig struct { + Policy string `json:"policy"` + Policies []string `json:"policies"` + Lease time.Duration `json:"lease"` + TokenType string `json:"token_type"` +} diff --git a/vendor/github.com/hashicorp/consul/api/acl.go b/vendor/github.com/hashicorp/consul/api/acl.go index 8ec9aa58557c..ca2e4e0cc138 100644 --- a/vendor/github.com/hashicorp/consul/api/acl.go +++ b/vendor/github.com/hashicorp/consul/api/acl.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "io/ioutil" "time" ) @@ -12,7 +14,42 @@ const ( ACLManagementType = "management" ) -// ACLEntry is used to represent an ACL entry +type ACLTokenPolicyLink struct { + ID string + Name string +} + +// ACLToken represents an ACL Token +type ACLToken struct { + CreateIndex uint64 + ModifyIndex uint64 + AccessorID string + SecretID string + Description string + Policies []*ACLTokenPolicyLink + Local bool + CreateTime time.Time `json:",omitempty"` + Hash []byte `json:",omitempty"` + + // DEPRECATED (ACL-Legacy-Compat) + // Rules will only be present for legacy tokens returned via the new APIs + Rules string `json:",omitempty"` +} + +type ACLTokenListEntry struct { + CreateIndex uint64 + ModifyIndex uint64 + AccessorID string + Description string + Policies []*ACLTokenPolicyLink + Local bool + CreateTime time.Time + Hash []byte + Legacy bool +} + +// ACLEntry is used to represent a legacy ACL token +// The legacy tokens are deprecated. type ACLEntry struct { CreateIndex uint64 ModifyIndex uint64 @@ -32,6 +69,28 @@ type ACLReplicationStatus struct { LastError time.Time } +// ACLPolicy represents an ACL Policy. +type ACLPolicy struct { + ID string + Name string + Description string + Rules string + Datacenters []string + Hash []byte + CreateIndex uint64 + ModifyIndex uint64 +} + +type ACLPolicyListEntry struct { + ID string + Name string + Description string + Datacenters []string + Hash []byte + CreateIndex uint64 + ModifyIndex uint64 +} + // ACL can be used to query the ACL endpoints type ACL struct { c *Client @@ -44,20 +103,20 @@ func (c *Client) ACL() *ACL { // Bootstrap is used to perform a one-time ACL bootstrap operation on a cluster // to get the first management token. -func (a *ACL) Bootstrap() (string, *WriteMeta, error) { +func (a *ACL) Bootstrap() (*ACLToken, *WriteMeta, error) { r := a.c.newRequest("PUT", "/v1/acl/bootstrap") rtt, resp, err := requireOK(a.c.doRequest(r)) if err != nil { - return "", nil, err + return nil, nil, err } defer resp.Body.Close() wm := &WriteMeta{RequestTime: rtt} - var out struct{ ID string } + var out ACLToken if err := decodeBody(resp, &out); err != nil { - return "", nil, err + return nil, nil, err } - return out.ID, wm, nil + return &out, wm, nil } // Create is used to generate a new token with the given parameters @@ -191,3 +250,296 @@ func (a *ACL) Replication(q *QueryOptions) (*ACLReplicationStatus, *QueryMeta, e } return entries, qm, nil } + +func (a *ACL) TokenCreate(token *ACLToken, q *WriteOptions) (*ACLToken, *WriteMeta, error) { + if token.AccessorID != "" { + return nil, nil, fmt.Errorf("Cannot specify an AccessorID in Token Creation") + } + + if token.SecretID != "" { + return nil, nil, fmt.Errorf("Cannot specify a SecretID in Token Creation") + } + + r := a.c.newRequest("PUT", "/v1/acl/token") + r.setWriteOptions(q) + r.obj = token + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + var out ACLToken + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, wm, nil +} + +func (a *ACL) TokenUpdate(token *ACLToken, q *WriteOptions) (*ACLToken, *WriteMeta, error) { + if token.AccessorID == "" { + return nil, nil, fmt.Errorf("Must specify an AccessorID for Token Updating") + } + r := a.c.newRequest("PUT", "/v1/acl/token/"+token.AccessorID) + r.setWriteOptions(q) + r.obj = token + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + var out ACLToken + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, wm, nil +} + +func (a *ACL) TokenClone(tokenID string, description string, q *WriteOptions) (*ACLToken, *WriteMeta, error) { + if tokenID == "" { + return nil, nil, fmt.Errorf("Must specify a tokenID for Token Cloning") + } + + r := a.c.newRequest("PUT", "/v1/acl/token/clone/"+tokenID) + r.setWriteOptions(q) + r.obj = struct{ Description string }{description} + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + var out ACLToken + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, wm, nil +} + +func (a *ACL) TokenDelete(tokenID string, q *WriteOptions) (*WriteMeta, error) { + r := a.c.newRequest("DELETE", "/v1/acl/token/"+tokenID) + r.setWriteOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + return wm, nil +} + +func (a *ACL) TokenRead(tokenID string, q *QueryOptions) (*ACLToken, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/acl/token/"+tokenID) + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out ACLToken + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, qm, nil +} + +func (a *ACL) TokenReadSelf(q *QueryOptions) (*ACLToken, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/acl/token/self") + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out ACLToken + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, qm, nil +} + +func (a *ACL) TokenList(q *QueryOptions) ([]*ACLTokenListEntry, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/acl/tokens") + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var entries []*ACLTokenListEntry + if err := decodeBody(resp, &entries); err != nil { + return nil, nil, err + } + return entries, qm, nil +} + +// TokenUpgrade performs an almost identical operation as TokenUpdate. The only difference is +// that not all parts of the token must be specified here and the server will patch the token +// with the existing secret id, description etc. +func (a *ACL) TokenUpgrade(token *ACLToken, q *WriteOptions) (*ACLToken, *WriteMeta, error) { + if token.AccessorID == "" { + return nil, nil, fmt.Errorf("Must specify an AccessorID for Token Updating") + } + r := a.c.newRequest("PUT", "/v1/acl/token/upgrade"+token.AccessorID) + r.setWriteOptions(q) + r.obj = token + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + var out ACLToken + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, wm, nil +} + +func (a *ACL) PolicyCreate(policy *ACLPolicy, q *WriteOptions) (*ACLPolicy, *WriteMeta, error) { + if policy.ID != "" { + return nil, nil, fmt.Errorf("Cannot specify an ID in Policy Creation") + } + + r := a.c.newRequest("PUT", "/v1/acl/policy") + r.setWriteOptions(q) + r.obj = policy + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + var out ACLPolicy + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, wm, nil +} + +func (a *ACL) PolicyUpdate(policy *ACLPolicy, q *WriteOptions) (*ACLPolicy, *WriteMeta, error) { + if policy.ID == "" { + return nil, nil, fmt.Errorf("Must specify an ID in Policy Creation") + } + + r := a.c.newRequest("PUT", "/v1/acl/policy/"+policy.ID) + r.setWriteOptions(q) + r.obj = policy + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + var out ACLPolicy + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, wm, nil +} + +func (a *ACL) PolicyDelete(policyID string, q *WriteOptions) (*WriteMeta, error) { + r := a.c.newRequest("DELETE", "/v1/acl/policy/"+policyID) + r.setWriteOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + return wm, nil +} + +func (a *ACL) PolicyRead(policyID string, q *QueryOptions) (*ACLPolicy, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/acl/policy/"+policyID) + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out ACLPolicy + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + + return &out, qm, nil +} + +func (a *ACL) PolicyList(q *QueryOptions) ([]*ACLPolicyListEntry, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/acl/policies") + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var entries []*ACLPolicyListEntry + if err := decodeBody(resp, &entries); err != nil { + return nil, nil, err + } + return entries, qm, nil +} + +func (a *ACL) PolicyTranslate(rules string) (string, error) { + r := a.c.newRequest("POST", "/v1/acl/policy/translate") + r.obj = rules + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return "", err + } + defer resp.Body.Close() + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + ruleBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("Failed to read translated rule body: %v", err) + } + + return string(ruleBytes), nil + +} diff --git a/vendor/github.com/hashicorp/consul/api/agent.go b/vendor/github.com/hashicorp/consul/api/agent.go index d6002f5834e4..8e5ffde30be1 100644 --- a/vendor/github.com/hashicorp/consul/api/agent.go +++ b/vendor/github.com/hashicorp/consul/api/agent.go @@ -314,6 +314,24 @@ func (a *Agent) Self() (map[string]map[string]interface{}, error) { return out, nil } +// Host is used to retrieve information about the host the +// agent is running on such as CPU, memory, and disk. Requires +// a operator:read ACL token. +func (a *Agent) Host() (map[string]interface{}, error) { + r := a.c.newRequest("GET", "/v1/agent/host") + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var out map[string]interface{} + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return out, nil +} + // Metrics is used to query the agent we are speaking to for // its current internal metric data func (a *Agent) Metrics() (*MetricsInfo, error) { diff --git a/vendor/github.com/hashicorp/consul/api/debug.go b/vendor/github.com/hashicorp/consul/api/debug.go new file mode 100644 index 000000000000..238046853a01 --- /dev/null +++ b/vendor/github.com/hashicorp/consul/api/debug.go @@ -0,0 +1,106 @@ +package api + +import ( + "fmt" + "io/ioutil" + "strconv" +) + +// Debug can be used to query the /debug/pprof endpoints to gather +// profiling information about the target agent.Debug +// +// The agent must have enable_debug set to true for profiling to be enabled +// and for these endpoints to function. +type Debug struct { + c *Client +} + +// Debug returns a handle that exposes the internal debug endpoints. +func (c *Client) Debug() *Debug { + return &Debug{c} +} + +// Heap returns a pprof heap dump +func (d *Debug) Heap() ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/heap") + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} + +// Profile returns a pprof CPU profile for the specified number of seconds +func (d *Debug) Profile(seconds int) ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/profile") + + // Capture a profile for the specified number of seconds + r.params.Set("seconds", strconv.Itoa(seconds)) + + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} + +// Trace returns an execution trace +func (d *Debug) Trace(seconds int) ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/trace") + + // Capture a trace for the specified number of seconds + r.params.Set("seconds", strconv.Itoa(seconds)) + + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} + +// Goroutine returns a pprof goroutine profile +func (d *Debug) Goroutine() ([]byte, error) { + r := d.c.newRequest("GET", "/debug/pprof/goroutine") + + _, resp, err := d.c.doRequest(r) + if err != nil { + return nil, fmt.Errorf("error making request: %s", err) + } + defer resp.Body.Close() + + // We return a raw response because we're just passing through a response + // from the pprof handlers + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error decoding body: %s", err) + } + + return body, nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index ec1143ef0e84..dff2cb7c97e7 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1095,10 +1095,10 @@ "revisionTime": "2016-01-25T11:53:50Z" }, { - "checksumSHA1": "PJDKLws64a9Lb00kp9qgyhAD81s=", + "checksumSHA1": "pMikh4ir9TYnYGY5C68RetWhAnY=", "path": "github.com/hashicorp/consul/api", - "revision": "34e95314827d336e4893aa23ee651b67d320c3ee", - "revisionTime": "2018-10-11T20:31:27Z" + "revision": "87a2b2020d802cd08e0dbdf53fa7db40ea47d399", + "revisionTime": "2018-10-19T20:37:47Z" }, { "checksumSHA1": "L/RlKwIgOVgm516yOHXfT4HcGAk=", diff --git a/website/source/api/secret/consul/index.html.md b/website/source/api/secret/consul/index.html.md index 2817d2889836..f9bc575cbe33 100644 --- a/website/source/api/secret/consul/index.html.md +++ b/website/source/api/secret/consul/index.html.md @@ -67,7 +67,7 @@ updated attributes. | :------- | :--------------------------- | :--------------------- | | `POST` | `/consul/roles/:name` | `204 (empty body)` | -### Parameters +### Parameters for Consul version below 1.4 - `name` `(string: )` – Specifies the name of an existing role against which to create this Consul credential. This is part of the request URL. @@ -112,6 +112,32 @@ $ curl \ http://127.0.0.1:8200/v1/consul/roles/example-role ``` +### Parameters for Consul versions 1.4 and above + +- `lease` `(string: "")` – Specifies the lease for this role. This is provided + as a string duration with a time suffix like `"30s"` or `"1h"`. If not + provided, the default Vault lease is used. + +- `policies` `(string: )` – Comma separated list of policies to be applied + to the tokens. + +### Sample payload +```json +{ + "policies": "global-management" +} +``` + +### Sample request + +```sh +curl \ +→ --request POST \ +→ --header "X-Vault-Token: ..."\ +→ --data @payload.json \ +→ http://127.0.0.1:8200/v1/consul/roles/example-role +``` + ## Read Role This endpoint queries for information about a Consul role with the given name. diff --git a/website/source/docs/secrets/consul/index.html.md b/website/source/docs/secrets/consul/index.html.md index f775f0cbd727..65b758e21167 100644 --- a/website/source/docs/secrets/consul/index.html.md +++ b/website/source/docs/secrets/consul/index.html.md @@ -28,7 +28,7 @@ management tool. By default, the secrets engine will mount at the name of the engine. To enable the secrets engine at a different path, use the `-path` argument. -1. Acquire a [management token][consul-mgmt-token] from Consul, using the +2. In Consul versions below 1.4, acquire a [management token][consul-mgmt-token] from Consul, using the `acl_master_token` from your Consul configuration file or another management token: @@ -48,8 +48,20 @@ token: "ID": "7652ba4c-0f6e-8e75-5724-5e083d72cfe4" } ``` +For Consul 1.4 and above, use the command line to generate a token with the appropiate policy: -1. Configure Vault to connect and authenticate to Consul: + ```sh + $ CONSUL_HTTP_TOKEN=d54fe46a-1f57-a589-3583-6b78e334b03b consul acl token create -policy-name=global-management + AccessorID: 865dc5e9-e585-3180-7b49-4ddc0fc45135 + SecretID: ef35f0f1-885b-0cab-573c-7c91b65a7a7e + Description: + Local: false + Create Time: 2018-10-22 17:40:24.128188 -0700 PDT + Policies: + 00000000-0000-0000-0000-000000000001 - global-management + ``` + +3. Configure Vault to connect and authenticate to Consul: ```text $ vault write consul/config/access \ @@ -58,16 +70,22 @@ token: Success! Data written to: consul/config/access ``` -1. Configure a role that maps a name in Vault to a Consul ACL policy. -When users generate credentials, they are generated against this role: +4. Configure a role that maps a name in Vault to a Consul ACL policy. Depending on your Consul version, +you will either provide a policy document and a token_type, or a set of policies. +When users generate credentials, they are generated against this role. For Consul versions below 1.4: ```text $ vault write consul/roles/my-role policy=$(base64 <<< 'key "" { policy = "read" }') Success! Data written to: consul/roles/my-role ``` +The policy must be base64-encoded. The policy language is [documented by Consul](https://www.consul.io/docs/internals/acl.html). - The policy must be base64-encoded. The policy language is [documented by - Consul](https://www.consul.io/docs/internals/acl.html). +For Consul versions 1.4 and above, [generate a policy in Consul](https://www.consul.io/docs/guides/acl.html), and procede to link it +to the role: + ```text + $ vault write consul/roles/my-role policies=readonly + Success! Data written to: consul/roles/my-role + ``` ## Usage @@ -87,6 +105,18 @@ lease_renewable true token 642783bf-1540-526f-d4de-fe1ac1aed6f0 ``` +When using Consul 1.4, the response will include the accessor for the token + +```text +$ vault read consul/creds/my-role +Key Value +--- ----- +lease_id consul/creds/my-role/7miMPnYaBCaVWDS9clNE0Nv3 +lease_duration 768h +lease_renewable true +accessor 6d5a0348-dffe-e87b-4266-2bec03800abb +token bc7a42c0-9c59-23b4-8a09-7173c474dc42 +``` ## API The Consul secrets engine has a full HTTP API. Please see the From a393cea1cf33d7fbe82a8b5cc6f1f47edf75b493 Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Tue, 23 Oct 2018 16:14:18 -0700 Subject: [PATCH 02/15] Working tests --- builtin/logical/consul/backend_old_test.go | 470 +++++++++++++++++++++ builtin/logical/consul/backend_test.go | 408 ++++-------------- builtin/logical/consul/path_roles.go | 10 +- builtin/logical/consul/path_token.go | 4 +- 4 files changed, 568 insertions(+), 324 deletions(-) create mode 100644 builtin/logical/consul/backend_old_test.go diff --git a/builtin/logical/consul/backend_old_test.go b/builtin/logical/consul/backend_old_test.go new file mode 100644 index 000000000000..ce14056019c4 --- /dev/null +++ b/builtin/logical/consul/backend_old_test.go @@ -0,0 +1,470 @@ +package consul + +import ( + "context" + "encoding/base64" + "fmt" + "log" + "os" + "reflect" + "sync" + "testing" + "time" + + consulapi "github.com/hashicorp/consul/api" + "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" + "github.com/mitchellh/mapstructure" + dockertest "gopkg.in/ory-am/dockertest.v2" +) + +var ( + testImagePull sync.Once +) + +func prepareTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (cid dockertest.ContainerID, retAddress string) { + if os.Getenv("CONSUL_ADDR") != "" { + return "", os.Getenv("CONSUL_ADDR") + } + + // Without this the checks for whether the container has started seem to + // never actually pass. There's really no reason to expose the test + // containers, so don't. + dockertest.BindDockerToLocalhost = "yep" + + testImagePull.Do(func() { + dockertest.Pull(dockertest.ConsulImageName) + }) + + try := 0 + cid, connErr := dockertest.ConnectToConsul(60, 500*time.Millisecond, func(connAddress string) bool { + try += 1 + // Build a client and verify that the credentials work + config := consulapi.DefaultConfig() + config.Address = connAddress + config.Token = dockertest.ConsulACLMasterToken + client, err := consulapi.NewClient(config) + if err != nil { + if try > 50 { + panic(err) + } + return false + } + + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "setuptest", + Value: []byte("setuptest"), + }, nil) + if err != nil { + if try > 50 { + panic(err) + } + return false + } + + retAddress = connAddress + return true + }) + + if connErr != nil { + t.Fatalf("could not connect to consul: %v", connErr) + } + + return +} + +func cleanupTestContainer(t *testing.T, cid dockertest.ContainerID) { + err := cid.KillRemove() + if err != nil { + t.Fatal(err) + } +} + +func TestBackend_config_access(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } + + confReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/access", + Storage: config.StorageView, + Data: connData, + } + + resp, err := b.HandleRequest(context.Background(), confReq) + if err != nil || (resp != nil && resp.IsError()) || resp != nil { + t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) + } + + confReq.Operation = logical.ReadOperation + resp, err = b.HandleRequest(context.Background(), confReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) + } + + expected := map[string]interface{}{ + "address": connData["address"].(string), + "scheme": "http", + } + if !reflect.DeepEqual(expected, resp.Data) { + t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) + } + if resp.Data["token"] != nil { + t.Fatalf("token should not be set in the response") + } +} + +func TestBackend_basic(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfig(t, connData), + testAccStepWritePolicy(t, "test", testPolicy, ""), + testAccStepReadToken(t, "test", connData), + }, + }) +} + +func TestBackend_renew_revoke(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } + + req := &logical.Request{ + Storage: config.StorageView, + Operation: logical.UpdateOperation, + Path: "config/access", + Data: connData, + } + resp, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Path = "roles/test" + req.Data = map[string]interface{}{ + "policy": base64.StdEncoding.EncodeToString([]byte(testPolicy)), + "lease": "6h", + } + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.ReadOperation + req.Path = "creds/test" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("resp nil") + } + if resp.IsError() { + t.Fatalf("resp is error: %v", resp.Error()) + } + + generatedSecret := resp.Secret + generatedSecret.TTL = 6 * time.Hour + + var d struct { + Token string `mapstructure:"token"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + t.Fatal(err) + } + log.Printf("[WARN] Generated token: %s", d.Token) + + // Build a client and verify that the credentials work + consulapiConfig := consulapi.DefaultConfig() + consulapiConfig.Address = connData["address"].(string) + consulapiConfig.Token = d.Token + client, err := consulapi.NewClient(consulapiConfig) + if err != nil { + t.Fatal(err) + } + + log.Printf("[WARN] Verifying that the generated token works...") + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "foo", + Value: []byte("bar"), + }, nil) + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.RenewOperation + req.Secret = generatedSecret + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response from renew") + } + + req.Operation = logical.RevokeOperation + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + log.Printf("[WARN] Verifying that the generated token does not work...") + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "foo", + Value: []byte("bar"), + }, nil) + if err == nil { + t.Fatal("expected error") + } +} + +func TestBackend_management(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cid, connURL := prepareTestContainer(t, config.StorageView, b) + if cid != "" { + defer cleanupTestContainer(t, cid) + } + connData := map[string]interface{}{ + "address": connURL, + "token": dockertest.ConsulACLMasterToken, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfig(t, connData), + testAccStepWriteManagementPolicy(t, "test", ""), + testAccStepReadManagementToken(t, "test", connData), + }, + }) +} + +func TestBackend_crud(t *testing.T) { + b, _ := Factory(context.Background(), logical.TestBackendConfig()) + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepWritePolicy(t, "test", testPolicy, ""), + testAccStepWritePolicy(t, "test2", testPolicy, ""), + testAccStepWritePolicy(t, "test3", testPolicy, ""), + testAccStepReadPolicy(t, "test", testPolicy, 0), + testAccStepListPolicy(t, []string{"test", "test2", "test3"}), + testAccStepDeletePolicy(t, "test"), + }, + }) +} + +func TestBackend_role_lease(t *testing.T) { + b, _ := Factory(context.Background(), logical.TestBackendConfig()) + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepWritePolicy(t, "test", testPolicy, "6h"), + testAccStepReadPolicy(t, "test", testPolicy, 6*time.Hour), + testAccStepDeletePolicy(t, "test"), + }, + }) +} + +func testAccStepConfig( + t *testing.T, config map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/access", + Data: config, + } +} + +func testAccStepReadToken( + t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "creds/" + name, + Check: func(resp *logical.Response) error { + var d struct { + Token string `mapstructure:"token"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + log.Printf("[WARN] Generated token: %s", d.Token) + + // Build a client and verify that the credentials work + config := consulapi.DefaultConfig() + config.Address = conf["address"].(string) + config.Token = d.Token + client, err := consulapi.NewClient(config) + if err != nil { + return err + } + + log.Printf("[WARN] Verifying that the generated token works...") + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "foo", + Value: []byte("bar"), + }, nil) + if err != nil { + return err + } + + return nil + }, + } +} + +func testAccStepReadManagementToken( + t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "creds/" + name, + Check: func(resp *logical.Response) error { + var d struct { + Token string `mapstructure:"token"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + log.Printf("[WARN] Generated token: %s", d.Token) + + // Build a client and verify that the credentials work + config := consulapi.DefaultConfig() + config.Address = conf["address"].(string) + config.Token = d.Token + client, err := consulapi.NewClient(config) + if err != nil { + return err + } + + log.Printf("[WARN] Verifying that the generated token works...") + _, _, err = client.ACL().Create(&consulapi.ACLEntry{ + Type: "management", + Name: "test2", + }, nil) + if err != nil { + return err + } + + return nil + }, + } +} + +func testAccStepWritePolicy(t *testing.T, name string, policy string, lease string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "roles/" + name, + Data: map[string]interface{}{ + "policy": base64.StdEncoding.EncodeToString([]byte(policy)), + "lease": lease, + }, + } +} + +func testAccStepWriteManagementPolicy(t *testing.T, name string, lease string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "roles/" + name, + Data: map[string]interface{}{ + "token_type": "management", + "lease": lease, + }, + } +} + +func testAccStepReadPolicy(t *testing.T, name string, policy string, lease time.Duration) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "roles/" + name, + Check: func(resp *logical.Response) error { + policyRaw := resp.Data["policy"].(string) + out, err := base64.StdEncoding.DecodeString(policyRaw) + if err != nil { + return err + } + if string(out) != policy { + return fmt.Errorf("mismatch: %s %s", out, policy) + } + + l := resp.Data["lease"].(int64) + if lease != time.Second*time.Duration(l) { + return fmt.Errorf("mismatch: %v %v", l, lease) + } + return nil + }, + } +} + +func testAccStepListPolicy(t *testing.T, names []string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ListOperation, + Path: "roles/", + Check: func(resp *logical.Response) error { + respKeys := resp.Data["keys"].([]string) + if !reflect.DeepEqual(respKeys, names) { + return fmt.Errorf("mismatch: %#v %#v", respKeys, names) + } + return nil + }, + } +} + +func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.DeleteOperation, + Path: "roles/" + name, + } +} + +const testPolicy = ` +key "" { + policy = "write" +} +` diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index ce14056019c4..37091e4e87ba 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -2,85 +2,95 @@ package consul import ( "context" - "encoding/base64" "fmt" - "log" "os" "reflect" - "sync" "testing" "time" consulapi "github.com/hashicorp/consul/api" "github.com/hashicorp/vault/logical" - logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" - dockertest "gopkg.in/ory-am/dockertest.v2" + "github.com/ory/dockertest" ) -var ( - testImagePull sync.Once -) +func prepareTestContainerNewACL(t *testing.T) (cleanup func(), retAddress string, consulToken string) { + consulToken = os.Getenv("CONSUL_HTTP_TOKEN") -func prepareTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (cid dockertest.ContainerID, retAddress string) { - if os.Getenv("CONSUL_ADDR") != "" { - return "", os.Getenv("CONSUL_ADDR") - } + retAddress = os.Getenv("CONSUL_HTTP_ADDR") - // Without this the checks for whether the container has started seem to - // never actually pass. There's really no reason to expose the test - // containers, so don't. - dockertest.BindDockerToLocalhost = "yep" + if retAddress != "" { + return func() {}, retAddress, consulToken + } - testImagePull.Do(func() { - dockertest.Pull(dockertest.ConsulImageName) - }) + pool, err := dockertest.NewPool("") + if err != nil { + t.Fatalf("Failed to connect to docker: %s", err) + } - try := 0 - cid, connErr := dockertest.ConnectToConsul(60, 500*time.Millisecond, func(connAddress string) bool { - try += 1 - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = connAddress - config.Token = dockertest.ConsulACLMasterToken - client, err := consulapi.NewClient(config) - if err != nil { - if try > 50 { - panic(err) - } - return false - } + dockerOptions := &dockertest.RunOptions{ + Repository: "ncorrare/consul", + Tag: "v1.4.0-rc1", + Cmd: []string{"agent", "-dev", "-client", "0.0.0.0", "-hcl", `acl { enabled = true default_policy = "deny" }`}, + Env: []string{"CONSUL_BIND_INTERFACE=eth0"}, + } + resource, err := pool.RunWithOptions(dockerOptions) + if err != nil { + t.Fatalf("Could not start local Consul docker container: %s", err) + } - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "setuptest", - Value: []byte("setuptest"), - }, nil) + cleanup = func() { + err := pool.Purge(resource) if err != nil { - if try > 50 { - panic(err) - } - return false + t.Fatalf("Failed to cleanup local container: %s", err) } - - retAddress = connAddress - return true - }) - - if connErr != nil { - t.Fatalf("could not connect to consul: %v", connErr) } - return -} + retAddress = fmt.Sprintf("localhost:%s", resource.GetPort("8500/tcp")) -func cleanupTestContainer(t *testing.T, cid dockertest.ContainerID) { - err := cid.KillRemove() - if err != nil { - t.Fatal(err) + // exponential backoff-retry + if err = pool.Retry(func() error { + var err error + consulConfig := consulapi.DefaultNonPooledConfig() + consulConfig.Address = retAddress + consul, err := consulapi.NewClient(consulConfig) + if err != nil { + return err + } + aclbootstrap, _, err := consul.ACL().Bootstrap() + if err != nil { + return err + } + consulToken = aclbootstrap.SecretID + t.Logf("[WARN] Generated Master token: %s", consulToken) + policy := &consulapi.ACLPolicy{ + Name: "test", + Description: "test", + Rules: `node_prefix "" { + policy = "write" + } + + service_prefix "" { + policy = "read" + } + `, + } + q := &consulapi.WriteOptions{ + Token: consulToken, + } + _, _, err = consul.ACL().PolicyCreate(policy, q) + if err != nil { + return err + } + return nil + }); err != nil { + cleanup() + t.Fatalf("Could not connect to docker: %s", err) } + return cleanup, retAddress, consulToken } -func TestBackend_config_access(t *testing.T) { +func TestBackend_old_config_access(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) @@ -88,13 +98,12 @@ func TestBackend_config_access(t *testing.T) { t.Fatal(err) } - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } + cleanup, connURL, connToken := prepareTestContainerNewACL(t) + defer cleanup() + connData := map[string]interface{}{ "address": connURL, - "token": dockertest.ConsulACLMasterToken, + "token": connToken, } confReq := &logical.Request{ @@ -127,34 +136,7 @@ func TestBackend_config_access(t *testing.T) { } } -func TestBackend_basic(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } - connData := map[string]interface{}{ - "address": connURL, - "token": dockertest.ConsulACLMasterToken, - } - - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData), - testAccStepWritePolicy(t, "test", testPolicy, ""), - testAccStepReadToken(t, "test", connData), - }, - }) -} - -func TestBackend_renew_revoke(t *testing.T) { +func TestBackend_old_renew_revoke(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) @@ -162,13 +144,11 @@ func TestBackend_renew_revoke(t *testing.T) { t.Fatal(err) } - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } + cleanup, connURL, connToken := prepareTestContainerNewACL(t) + defer cleanup() connData := map[string]interface{}{ "address": connURL, - "token": dockertest.ConsulACLMasterToken, + "token": connToken, } req := &logical.Request{ @@ -184,8 +164,8 @@ func TestBackend_renew_revoke(t *testing.T) { req.Path = "roles/test" req.Data = map[string]interface{}{ - "policy": base64.StdEncoding.EncodeToString([]byte(testPolicy)), - "lease": "6h", + "policies": []string{"test"}, + "lease": "6h", } resp, err = b.HandleRequest(context.Background(), req) if err != nil { @@ -209,15 +189,16 @@ func TestBackend_renew_revoke(t *testing.T) { generatedSecret.TTL = 6 * time.Hour var d struct { - Token string `mapstructure:"token"` + Token string `mapstructure:"token"` + Accessor string `mapstructure:"accessor"` } if err := mapstructure.Decode(resp.Data, &d); err != nil { t.Fatal(err) } - log.Printf("[WARN] Generated token: %s", d.Token) + t.Logf("[WARN] Generated token: %s with accessor %s", d.Token, d.Accessor) // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultConfig() + consulapiConfig := consulapi.DefaultNonPooledConfig() consulapiConfig.Address = connData["address"].(string) consulapiConfig.Token = d.Token client, err := consulapi.NewClient(consulapiConfig) @@ -225,11 +206,8 @@ func TestBackend_renew_revoke(t *testing.T) { t.Fatal(err) } - log.Printf("[WARN] Verifying that the generated token works...") - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) + t.Log("[WARN] Verifying that the generated token works...") + _, err = client.Catalog(), nil if err != nil { t.Fatal(err) } @@ -250,221 +228,19 @@ func TestBackend_renew_revoke(t *testing.T) { t.Fatal(err) } - log.Printf("[WARN] Verifying that the generated token does not work...") - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err == nil { - t.Fatal("expected error") - } -} + // Build a management client and verify that the token does not exist anymore + consulmgmtConfig := consulapi.DefaultNonPooledConfig() + consulmgmtConfig.Address = connData["address"].(string) + consulmgmtConfig.Token = connData["token"].(string) + mgmtclient, err := consulapi.NewClient(consulmgmtConfig) -func TestBackend_management(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } - connData := map[string]interface{}{ - "address": connURL, - "token": dockertest.ConsulACLMasterToken, - } - - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData), - testAccStepWriteManagementPolicy(t, "test", ""), - testAccStepReadManagementToken(t, "test", connData), - }, - }) -} - -func TestBackend_crud(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepWritePolicy(t, "test", testPolicy, ""), - testAccStepWritePolicy(t, "test2", testPolicy, ""), - testAccStepWritePolicy(t, "test3", testPolicy, ""), - testAccStepReadPolicy(t, "test", testPolicy, 0), - testAccStepListPolicy(t, []string{"test", "test2", "test3"}), - testAccStepDeletePolicy(t, "test"), - }, - }) -} - -func TestBackend_role_lease(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepWritePolicy(t, "test", testPolicy, "6h"), - testAccStepReadPolicy(t, "test", testPolicy, 6*time.Hour), - testAccStepDeletePolicy(t, "test"), - }, - }) -} - -func testAccStepConfig( - t *testing.T, config map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config/access", - Data: config, + q := &consulapi.QueryOptions{ + Datacenter: "DC1", } -} -func testAccStepReadToken( - t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "creds/" + name, - Check: func(resp *logical.Response) error { - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - log.Printf("[WARN] Generated token: %s", d.Token) - - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = conf["address"].(string) - config.Token = d.Token - client, err := consulapi.NewClient(config) - if err != nil { - return err - } - - log.Printf("[WARN] Verifying that the generated token works...") - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err != nil { - return err - } - - return nil - }, - } -} - -func testAccStepReadManagementToken( - t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "creds/" + name, - Check: func(resp *logical.Response) error { - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - log.Printf("[WARN] Generated token: %s", d.Token) - - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = conf["address"].(string) - config.Token = d.Token - client, err := consulapi.NewClient(config) - if err != nil { - return err - } - - log.Printf("[WARN] Verifying that the generated token works...") - _, _, err = client.ACL().Create(&consulapi.ACLEntry{ - Type: "management", - Name: "test2", - }, nil) - if err != nil { - return err - } - - return nil - }, - } -} - -func testAccStepWritePolicy(t *testing.T, name string, policy string, lease string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/" + name, - Data: map[string]interface{}{ - "policy": base64.StdEncoding.EncodeToString([]byte(policy)), - "lease": lease, - }, - } -} - -func testAccStepWriteManagementPolicy(t *testing.T, name string, lease string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/" + name, - Data: map[string]interface{}{ - "token_type": "management", - "lease": lease, - }, - } -} - -func testAccStepReadPolicy(t *testing.T, name string, policy string, lease time.Duration) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "roles/" + name, - Check: func(resp *logical.Response) error { - policyRaw := resp.Data["policy"].(string) - out, err := base64.StdEncoding.DecodeString(policyRaw) - if err != nil { - return err - } - if string(out) != policy { - return fmt.Errorf("mismatch: %s %s", out, policy) - } - - l := resp.Data["lease"].(int64) - if lease != time.Second*time.Duration(l) { - return fmt.Errorf("mismatch: %v %v", l, lease) - } - return nil - }, - } -} - -func testAccStepListPolicy(t *testing.T, names []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "roles/", - Check: func(resp *logical.Response) error { - respKeys := resp.Data["keys"].([]string) - if !reflect.DeepEqual(respKeys, names) { - return fmt.Errorf("mismatch: %#v %#v", respKeys, names) - } - return nil - }, - } -} - -func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "roles/" + name, + t.Log("[WARN] Verifying that the generated token does not exist...") + _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) + if err == nil { + t.Fatal("err: expected error") } } - -const testPolicy = ` -key "" { - policy = "write" -} -` diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index f07e9c7e643f..fb4d4fb4c12e 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -132,12 +132,10 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel var policyRaw []byte var err error - if len(policies) == 0 { - if tokenType != "management" { - if policy == "" { - return logical.ErrorResponse( - "policy cannot be empty when not using management tokens"), nil - } + if len(policies) == 0 || policies == nil { + if tokenType != "management" && policy == "" { + return logical.ErrorResponse( + "policy cannot be empty when not using management tokens"), nil } } policyRaw, err = base64.StdEncoding.DecodeString(d.Get("policy").(string)) diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go index 844d660ea035..17adc63dcabb 100644 --- a/builtin/logical/consul/path_token.go +++ b/builtin/logical/consul/path_token.go @@ -63,7 +63,7 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr writeOpts = writeOpts.WithContext(ctx) var s *logical.Response // Create an ACLEntry for Consul pre 1.4 - if result.Policy != "" { + if (result.Policy != "" && result.TokenType == "client") || (result.Policy == "" && result.TokenType == "management") { token, _, err := c.ACL().Create(&api.ACLEntry{ Name: tokenName, Type: result.TokenType, @@ -84,7 +84,7 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr } //Create an ACLToken for Consul 1.4 and above - if len(result.Policies) > 0 { + if len(result.Policies) > 0 && result.Policy == "" && result.TokenType != "management" { var policyLink = []*api.ACLTokenPolicyLink{} for _, policyName := range result.Policies { policyLink = append(policyLink, &api.ACLTokenPolicyLink{ From 1daf02317f03551c097685a47e8f56f8609833ef Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Tue, 23 Oct 2018 16:17:32 -0700 Subject: [PATCH 03/15] Fixed logic gate --- builtin/logical/consul/path_roles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index fb4d4fb4c12e..f386ad2b57fd 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -123,7 +123,7 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel } } - if policy != "" && len(policies) > 0 { + if policy != "" && len(policies) == 0 { return logical.ErrorResponse( "Use either a policy document, or a list of policies, depending on your Consul version"), nil } From 462c4c84098425fc78d8e3a8c9fae76053c51e89 Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Tue, 23 Oct 2018 16:50:18 -0700 Subject: [PATCH 04/15] Fixed logical gate that evaluate empty policy or empty list of policy names --- builtin/logical/consul/path_roles.go | 2 +- "builtin/logical/consul/\302\261" | 182 --------------------------- 2 files changed, 1 insertion(+), 183 deletions(-) delete mode 100644 "builtin/logical/consul/\302\261" diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index f386ad2b57fd..ae7ee8e51ce7 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -123,7 +123,7 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel } } - if policy != "" && len(policies) == 0 { + if (policy == "" && tokenType != "management") && len(policies) == 0 { return logical.ErrorResponse( "Use either a policy document, or a list of policies, depending on your Consul version"), nil } diff --git "a/builtin/logical/consul/\302\261" "b/builtin/logical/consul/\302\261" deleted file mode 100644 index a8740e1461d2..000000000000 --- "a/builtin/logical/consul/\302\261" +++ /dev/null @@ -1,182 +0,0 @@ -package consul - -import ( - "context" - "encoding/base64" - "fmt" - "time" - - "github.com/hashicorp/vault/logical" - "github.com/hashicorp/vault/logical/framework" -) - -func pathListRoles(b *backend) *framework.Path { - return &framework.Path{ - Pattern: "roles/?$", - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ListOperation: b.pathRoleList, - }, - } -} - -func pathRoles() *framework.Path { - return &framework.Path{ - Pattern: "roles/" + framework.GenericNameRegex("name"), - Fields: map[string]*framework.FieldSchema{ - "name": &framework.FieldSchema{ - Type: framework.TypeString, - Description: "Name of the role", - }, - - "policy": &framework.FieldSchema{ - Type: framework.TypeString, - Description: `Policy document, base64 encoded. Required -for 'client' tokens. Required for Consul pre-1.4`, - }, - - "policies": &framework.FieldSchema{ - Type: framework.TypeCommaStringSlice, - Description: `List of policies attached to the token. Required -for Consul 1.4 or above`, - }, - - "token_type": &framework.FieldSchema{ - Type: framework.TypeString, - Default: "client", - Description: `Which type of token to create: 'client' -or 'management'. If a 'management' token, -the "policy" parameter is not required. -Defaults to 'client'.`, - }, - - "lease": &framework.FieldSchema{ - Type: framework.TypeDurationSecond, - Description: "Lease time of the role.", - }, - }, - - Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: pathRolesRead, - logical.UpdateOperation: pathRolesWrite, - logical.DeleteOperation: pathRolesDelete, - }, - } -} - -func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - entries, err := req.Storage.List(ctx, "policy/") - if err != nil { - return nil, err - } - - return logical.ListResponse(entries), nil -} - -func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - - entry, err := req.Storage.Get(ctx, "policy/"+name) - if err != nil { - return nil, err - } - if entry == nil { - return nil, nil - } - - var result roleConfig - if err := entry.DecodeJSON(&result); err != nil { - return nil, err - } - - if result.TokenType == "" { - result.TokenType = "client" - } - - // Generate the response - resp := &logical.Response{ - Data: map[string]interface{}{ - "lease": int64(result.Lease.Seconds()), - "token_type": result.TokenType, - }, - } - if result.Policy != "" { - resp.Data["policy"] = base64.StdEncoding.EncodeToString([]byte(result.Policy)) - } - if len(result.Policies) > 0 { - resp.Data["policies"] = result.Policies - } - return resp, nil -} - -func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - tokenType := d.Get("token_type").(string) - policy := d.Get("policy").(string) - policies := d.Get("policies").([]string) - if policy != "" { - switch tokenType { - case "client": - case "management": - default: - return logical.ErrorResponse( - "token_type must be \"client\" or \"management\""), nil - } - - if policy != "" && len(policies) > 0 { - return logical.ErrorResponse( - "Use either a policy document, or a list of policies, depending on your Consul version"), nil - } - - name := d.Get("name").(string) - - var policyRaw []byte - var err error - if tokenType != "management" { - if policy == "" { - return logical.ErrorResponse( - "policy cannot be empty when not using management tokens"), nil - } - policyRaw, err = base64.StdEncoding.DecodeString(d.Get("policy").(string)) - if err != nil { - return logical.ErrorResponse(fmt.Sprintf( - "Error decoding policy base64: %s", err)), nil - } - } - - var lease time.Duration - leaseParamRaw, ok := d.GetOk("lease") - if ok { - lease = time.Second * time.Duration(leaseParamRaw.(int)) - } - - entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{ - Policy: string(policyRaw), - Policies: []string(policies), - Lease: lease, - TokenType: tokenType, - }) - if err != nil { - return nil, err - } - - if err := req.Storage.Put(ctx, entry); err != nil { - return nil, err - } - - return nil, nil -} - -func pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - name := d.Get("name").(string) - if err := req.Storage.Delete(ctx, "policy/"+name); err != nil { - return nil, err - } - return nil, nil -} - -type roleConfig struct { - Policy string `json:"policy"` - Policies []string `json:"policies"` - Lease time.Duration `json:"lease"` - TokenType string `json:"token_type"` -} From 497033da02e2e391aa6cda8555bd27135a3ab6fb Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Tue, 23 Oct 2018 16:55:44 -0700 Subject: [PATCH 05/15] Ensure tests are run against appropiate Consul versions --- builtin/logical/consul/backend_old_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/builtin/logical/consul/backend_old_test.go b/builtin/logical/consul/backend_old_test.go index ce14056019c4..5e97d512d4f9 100644 --- a/builtin/logical/consul/backend_old_test.go +++ b/builtin/logical/consul/backend_old_test.go @@ -33,7 +33,8 @@ func prepareTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (c dockertest.BindDockerToLocalhost = "yep" testImagePull.Do(func() { - dockertest.Pull(dockertest.ConsulImageName) + //dockertest.Pull(dockertest.ConsulImageName) + dockertest.Pull("consul:1.3.0") }) try := 0 From 8948641074de3178db28143f62d14828490af81e Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Thu, 25 Oct 2018 06:31:44 -0700 Subject: [PATCH 06/15] Running tests against official container with a 1.4.0-rc1 tag --- builtin/logical/consul/backend_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index 37091e4e87ba..3f61a0c2f5d6 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -29,8 +29,8 @@ func prepareTestContainerNewACL(t *testing.T) (cleanup func(), retAddress string } dockerOptions := &dockertest.RunOptions{ - Repository: "ncorrare/consul", - Tag: "v1.4.0-rc1", + Repository: "consul", + Tag: "1.4.0-rc1", Cmd: []string{"agent", "-dev", "-client", "0.0.0.0", "-hcl", `acl { enabled = true default_policy = "deny" }`}, Env: []string{"CONSUL_BIND_INTERFACE=eth0"}, } From 78bc7cf44e203d5a1fda903844ae94ee1fbe204c Mon Sep 17 00:00:00 2001 From: Nicolas Corrarello Date: Sat, 27 Oct 2018 23:05:29 +0100 Subject: [PATCH 07/15] policies can never be nil (as even if it is empty will be an empty array) --- builtin/logical/consul/path_roles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index ae7ee8e51ce7..b88a8fec67ac 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -132,7 +132,7 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel var policyRaw []byte var err error - if len(policies) == 0 || policies == nil { + if len(policies) == 0 { if tokenType != "management" && policy == "" { return logical.ErrorResponse( "policy cannot be empty when not using management tokens"), nil From 6d01aa4361fe4dff2ca2dd8399deb101f410b731 Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Wed, 31 Oct 2018 16:17:29 -0400 Subject: [PATCH 08/15] addressing feedback, refactoring tests --- builtin/logical/consul/backend.go | 2 +- builtin/logical/consul/backend_old_test.go | 471 --------------------- builtin/logical/consul/backend_test.go | 412 +++++++++++++++++- builtin/logical/consul/client.go | 4 +- builtin/logical/consul/path_config.go | 14 +- builtin/logical/consul/path_roles.go | 20 +- builtin/logical/consul/path_token.go | 59 ++- builtin/logical/consul/secret_token.go | 25 +- 8 files changed, 458 insertions(+), 549 deletions(-) delete mode 100644 builtin/logical/consul/backend_old_test.go diff --git a/builtin/logical/consul/backend.go b/builtin/logical/consul/backend.go index 55b77bbd0c30..ccd98ab4fdec 100644 --- a/builtin/logical/consul/backend.go +++ b/builtin/logical/consul/backend.go @@ -25,7 +25,7 @@ func Backend() *backend { }, Paths: []*framework.Path{ - pathConfigAccess(), + pathConfigAccess(&b), pathListRoles(&b), pathRoles(), pathToken(&b), diff --git a/builtin/logical/consul/backend_old_test.go b/builtin/logical/consul/backend_old_test.go deleted file mode 100644 index 5e97d512d4f9..000000000000 --- a/builtin/logical/consul/backend_old_test.go +++ /dev/null @@ -1,471 +0,0 @@ -package consul - -import ( - "context" - "encoding/base64" - "fmt" - "log" - "os" - "reflect" - "sync" - "testing" - "time" - - consulapi "github.com/hashicorp/consul/api" - "github.com/hashicorp/vault/logical" - logicaltest "github.com/hashicorp/vault/logical/testing" - "github.com/mitchellh/mapstructure" - dockertest "gopkg.in/ory-am/dockertest.v2" -) - -var ( - testImagePull sync.Once -) - -func prepareTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (cid dockertest.ContainerID, retAddress string) { - if os.Getenv("CONSUL_ADDR") != "" { - return "", os.Getenv("CONSUL_ADDR") - } - - // Without this the checks for whether the container has started seem to - // never actually pass. There's really no reason to expose the test - // containers, so don't. - dockertest.BindDockerToLocalhost = "yep" - - testImagePull.Do(func() { - //dockertest.Pull(dockertest.ConsulImageName) - dockertest.Pull("consul:1.3.0") - }) - - try := 0 - cid, connErr := dockertest.ConnectToConsul(60, 500*time.Millisecond, func(connAddress string) bool { - try += 1 - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = connAddress - config.Token = dockertest.ConsulACLMasterToken - client, err := consulapi.NewClient(config) - if err != nil { - if try > 50 { - panic(err) - } - return false - } - - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "setuptest", - Value: []byte("setuptest"), - }, nil) - if err != nil { - if try > 50 { - panic(err) - } - return false - } - - retAddress = connAddress - return true - }) - - if connErr != nil { - t.Fatalf("could not connect to consul: %v", connErr) - } - - return -} - -func cleanupTestContainer(t *testing.T, cid dockertest.ContainerID) { - err := cid.KillRemove() - if err != nil { - t.Fatal(err) - } -} - -func TestBackend_config_access(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } - connData := map[string]interface{}{ - "address": connURL, - "token": dockertest.ConsulACLMasterToken, - } - - confReq := &logical.Request{ - Operation: logical.UpdateOperation, - Path: "config/access", - Storage: config.StorageView, - Data: connData, - } - - resp, err := b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) || resp != nil { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - confReq.Operation = logical.ReadOperation - resp, err = b.HandleRequest(context.Background(), confReq) - if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) - } - - expected := map[string]interface{}{ - "address": connData["address"].(string), - "scheme": "http", - } - if !reflect.DeepEqual(expected, resp.Data) { - t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) - } - if resp.Data["token"] != nil { - t.Fatalf("token should not be set in the response") - } -} - -func TestBackend_basic(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } - connData := map[string]interface{}{ - "address": connURL, - "token": dockertest.ConsulACLMasterToken, - } - - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData), - testAccStepWritePolicy(t, "test", testPolicy, ""), - testAccStepReadToken(t, "test", connData), - }, - }) -} - -func TestBackend_renew_revoke(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } - connData := map[string]interface{}{ - "address": connURL, - "token": dockertest.ConsulACLMasterToken, - } - - req := &logical.Request{ - Storage: config.StorageView, - Operation: logical.UpdateOperation, - Path: "config/access", - Data: connData, - } - resp, err := b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Path = "roles/test" - req.Data = map[string]interface{}{ - "policy": base64.StdEncoding.EncodeToString([]byte(testPolicy)), - "lease": "6h", - } - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.ReadOperation - req.Path = "creds/test" - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("resp nil") - } - if resp.IsError() { - t.Fatalf("resp is error: %v", resp.Error()) - } - - generatedSecret := resp.Secret - generatedSecret.TTL = 6 * time.Hour - - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - t.Fatal(err) - } - log.Printf("[WARN] Generated token: %s", d.Token) - - // Build a client and verify that the credentials work - consulapiConfig := consulapi.DefaultConfig() - consulapiConfig.Address = connData["address"].(string) - consulapiConfig.Token = d.Token - client, err := consulapi.NewClient(consulapiConfig) - if err != nil { - t.Fatal(err) - } - - log.Printf("[WARN] Verifying that the generated token works...") - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err != nil { - t.Fatal(err) - } - - req.Operation = logical.RenewOperation - req.Secret = generatedSecret - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("got nil response from renew") - } - - req.Operation = logical.RevokeOperation - resp, err = b.HandleRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - - log.Printf("[WARN] Verifying that the generated token does not work...") - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err == nil { - t.Fatal("expected error") - } -} - -func TestBackend_management(t *testing.T) { - config := logical.TestBackendConfig() - config.StorageView = &logical.InmemStorage{} - b, err := Factory(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - cid, connURL := prepareTestContainer(t, config.StorageView, b) - if cid != "" { - defer cleanupTestContainer(t, cid) - } - connData := map[string]interface{}{ - "address": connURL, - "token": dockertest.ConsulACLMasterToken, - } - - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepConfig(t, connData), - testAccStepWriteManagementPolicy(t, "test", ""), - testAccStepReadManagementToken(t, "test", connData), - }, - }) -} - -func TestBackend_crud(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepWritePolicy(t, "test", testPolicy, ""), - testAccStepWritePolicy(t, "test2", testPolicy, ""), - testAccStepWritePolicy(t, "test3", testPolicy, ""), - testAccStepReadPolicy(t, "test", testPolicy, 0), - testAccStepListPolicy(t, []string{"test", "test2", "test3"}), - testAccStepDeletePolicy(t, "test"), - }, - }) -} - -func TestBackend_role_lease(t *testing.T) { - b, _ := Factory(context.Background(), logical.TestBackendConfig()) - logicaltest.Test(t, logicaltest.TestCase{ - Backend: b, - Steps: []logicaltest.TestStep{ - testAccStepWritePolicy(t, "test", testPolicy, "6h"), - testAccStepReadPolicy(t, "test", testPolicy, 6*time.Hour), - testAccStepDeletePolicy(t, "test"), - }, - }) -} - -func testAccStepConfig( - t *testing.T, config map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "config/access", - Data: config, - } -} - -func testAccStepReadToken( - t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "creds/" + name, - Check: func(resp *logical.Response) error { - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - log.Printf("[WARN] Generated token: %s", d.Token) - - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = conf["address"].(string) - config.Token = d.Token - client, err := consulapi.NewClient(config) - if err != nil { - return err - } - - log.Printf("[WARN] Verifying that the generated token works...") - _, err = client.KV().Put(&consulapi.KVPair{ - Key: "foo", - Value: []byte("bar"), - }, nil) - if err != nil { - return err - } - - return nil - }, - } -} - -func testAccStepReadManagementToken( - t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "creds/" + name, - Check: func(resp *logical.Response) error { - var d struct { - Token string `mapstructure:"token"` - } - if err := mapstructure.Decode(resp.Data, &d); err != nil { - return err - } - log.Printf("[WARN] Generated token: %s", d.Token) - - // Build a client and verify that the credentials work - config := consulapi.DefaultConfig() - config.Address = conf["address"].(string) - config.Token = d.Token - client, err := consulapi.NewClient(config) - if err != nil { - return err - } - - log.Printf("[WARN] Verifying that the generated token works...") - _, _, err = client.ACL().Create(&consulapi.ACLEntry{ - Type: "management", - Name: "test2", - }, nil) - if err != nil { - return err - } - - return nil - }, - } -} - -func testAccStepWritePolicy(t *testing.T, name string, policy string, lease string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/" + name, - Data: map[string]interface{}{ - "policy": base64.StdEncoding.EncodeToString([]byte(policy)), - "lease": lease, - }, - } -} - -func testAccStepWriteManagementPolicy(t *testing.T, name string, lease string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.UpdateOperation, - Path: "roles/" + name, - Data: map[string]interface{}{ - "token_type": "management", - "lease": lease, - }, - } -} - -func testAccStepReadPolicy(t *testing.T, name string, policy string, lease time.Duration) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ReadOperation, - Path: "roles/" + name, - Check: func(resp *logical.Response) error { - policyRaw := resp.Data["policy"].(string) - out, err := base64.StdEncoding.DecodeString(policyRaw) - if err != nil { - return err - } - if string(out) != policy { - return fmt.Errorf("mismatch: %s %s", out, policy) - } - - l := resp.Data["lease"].(int64) - if lease != time.Second*time.Duration(l) { - return fmt.Errorf("mismatch: %v %v", l, lease) - } - return nil - }, - } -} - -func testAccStepListPolicy(t *testing.T, names []string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.ListOperation, - Path: "roles/", - Check: func(resp *logical.Response) error { - respKeys := resp.Data["keys"].([]string) - if !reflect.DeepEqual(respKeys, names) { - return fmt.Errorf("mismatch: %#v %#v", respKeys, names) - } - return nil - }, - } -} - -func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { - return logicaltest.TestStep{ - Operation: logical.DeleteOperation, - Path: "roles/" + name, - } -} - -const testPolicy = ` -key "" { - policy = "write" -} -` diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index 3f61a0c2f5d6..690f04c2c80d 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -2,23 +2,25 @@ package consul import ( "context" + "encoding/base64" "fmt" + "log" "os" "reflect" + "strings" "testing" "time" consulapi "github.com/hashicorp/consul/api" "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" "github.com/mitchellh/mapstructure" "github.com/ory/dockertest" ) -func prepareTestContainerNewACL(t *testing.T) (cleanup func(), retAddress string, consulToken string) { +func prepareTestContainer(t *testing.T, version string) (cleanup func(), retAddress string, consulToken string) { consulToken = os.Getenv("CONSUL_HTTP_TOKEN") - retAddress = os.Getenv("CONSUL_HTTP_ADDR") - if retAddress != "" { return func() {}, retAddress, consulToken } @@ -28,15 +30,19 @@ func prepareTestContainerNewACL(t *testing.T) (cleanup func(), retAddress string t.Fatalf("Failed to connect to docker: %s", err) } + config := `acl { enabled = true default_policy = "deny" }` + if strings.HasPrefix(version, "1.3") { + config = `datacenter = "test" acl_default_policy = "deny" acl_datacenter = "test" acl_master_token = "test"` + } + dockerOptions := &dockertest.RunOptions{ Repository: "consul", - Tag: "1.4.0-rc1", - Cmd: []string{"agent", "-dev", "-client", "0.0.0.0", "-hcl", `acl { enabled = true default_policy = "deny" }`}, - Env: []string{"CONSUL_BIND_INTERFACE=eth0"}, + Tag: version, + Cmd: []string{"agent", "-dev", "-client", "0.0.0.0", "-hcl", config}, } resource, err := pool.RunWithOptions(dockerOptions) if err != nil { - t.Fatalf("Could not start local Consul docker container: %s", err) + t.Fatalf("Could not start local Consul %s docker container: %s", version, err) } cleanup = func() { @@ -57,12 +63,29 @@ func prepareTestContainerNewACL(t *testing.T) (cleanup func(), retAddress string if err != nil { return err } + + // For version of Consul < 1.4 + if strings.HasPrefix(version, "1.3") { + consulToken = "test" + _, err = consul.KV().Put(&consulapi.KVPair{ + Key: "setuptest", + Value: []byte("setuptest"), + }, &consulapi.WriteOptions{ + Token: consulToken, + }) + if err != nil { + return err + } + return nil + } + + // New default behavior aclbootstrap, _, err := consul.ACL().Bootstrap() if err != nil { return err } consulToken = aclbootstrap.SecretID - t.Logf("[WARN] Generated Master token: %s", consulToken) + t.Logf("Generated Master token: %s", consulToken) policy := &consulapi.ACLPolicy{ Name: "test", Description: "test", @@ -90,7 +113,21 @@ func prepareTestContainerNewACL(t *testing.T) (cleanup func(), retAddress string return cleanup, retAddress, consulToken } -func TestBackend_old_config_access(t *testing.T) { +func TestBackend_Config_Access(t *testing.T) { + t.Run("config_access", func(t *testing.T) { + t.Parallel() + t.Run("pre-1.4.0", func(t *testing.T) { + t.Parallel() + testBackendConfigAccess(t, "1.3.0") + }) + t.Run("1.4.0-rc", func(t *testing.T) { + t.Parallel() + testBackendConfigAccess(t, "1.4.0-rc1") + }) + }) +} + +func testBackendConfigAccess(t *testing.T, version string) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) @@ -98,7 +135,7 @@ func TestBackend_old_config_access(t *testing.T) { t.Fatal(err) } - cleanup, connURL, connToken := prepareTestContainerNewACL(t) + cleanup, connURL, connToken := prepareTestContainer(t, version) defer cleanup() connData := map[string]interface{}{ @@ -136,7 +173,26 @@ func TestBackend_old_config_access(t *testing.T) { } } -func TestBackend_old_renew_revoke(t *testing.T) { +func TestBackend_Renew_Revoke(t *testing.T) { + t.Run("renew_revoke", func(t *testing.T) { + t.Parallel() + t.Run("pre-1.4.0", func(t *testing.T) { + t.Parallel() + testBackendRenewRevoke(t, "1.3.0") + }) + t.Run("1.4.0-rc", func(t *testing.T) { + t.Parallel() + t.Run("legacy", func(t *testing.T) { + t.Parallel() + testBackendRenewRevoke(t, "1.3.0") + }) + + testBackendRenewRevoke14(t, "1.4.0-rc1") + }) + }) +} + +func testBackendRenewRevoke(t *testing.T, version string) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} b, err := Factory(context.Background(), config) @@ -144,7 +200,112 @@ func TestBackend_old_renew_revoke(t *testing.T) { t.Fatal(err) } - cleanup, connURL, connToken := prepareTestContainerNewACL(t) + cleanup, connURL, connToken := prepareTestContainer(t, version) + defer cleanup() + connData := map[string]interface{}{ + "address": connURL, + "token": connToken, + } + + req := &logical.Request{ + Storage: config.StorageView, + Operation: logical.UpdateOperation, + Path: "config/access", + Data: connData, + } + resp, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Path = "roles/test" + req.Data = map[string]interface{}{ + "policy": base64.StdEncoding.EncodeToString([]byte(testPolicy)), + "lease": "6h", + } + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.ReadOperation + req.Path = "creds/test" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("resp nil") + } + if resp.IsError() { + t.Fatalf("resp is error: %v", resp.Error()) + } + + generatedSecret := resp.Secret + generatedSecret.TTL = 6 * time.Hour + + var d struct { + Token string `mapstructure:"token"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + t.Fatal(err) + } + t.Logf("Generated token: %s", d.Token) + + // Build a client and verify that the credentials work + consulapiConfig := consulapi.DefaultConfig() + consulapiConfig.Address = connData["address"].(string) + consulapiConfig.Token = d.Token + client, err := consulapi.NewClient(consulapiConfig) + if err != nil { + t.Fatal(err) + } + + t.Logf("Verifying that the generated token works...") + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "foo", + Value: []byte("bar"), + }, nil) + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.RenewOperation + req.Secret = generatedSecret + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("got nil response from renew") + } + + req.Operation = logical.RevokeOperation + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + t.Logf("Verifying that the generated token does not work...") + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "foo", + Value: []byte("bar"), + }, nil) + if err == nil { + t.Fatal("expected error") + } + +} + +func testBackendRenewRevoke14(t *testing.T, version string) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cleanup, connURL, connToken := prepareTestContainer(t, version) defer cleanup() connData := map[string]interface{}{ "address": connURL, @@ -195,7 +356,7 @@ func TestBackend_old_renew_revoke(t *testing.T) { if err := mapstructure.Decode(resp.Data, &d); err != nil { t.Fatal(err) } - t.Logf("[WARN] Generated token: %s with accessor %s", d.Token, d.Accessor) + t.Logf("Generated token: %s with accessor %s", d.Token, d.Accessor) // Build a client and verify that the credentials work consulapiConfig := consulapi.DefaultNonPooledConfig() @@ -206,7 +367,7 @@ func TestBackend_old_renew_revoke(t *testing.T) { t.Fatal(err) } - t.Log("[WARN] Verifying that the generated token works...") + t.Log("Verifying that the generated token works...") _, err = client.Catalog(), nil if err != nil { t.Fatal(err) @@ -238,9 +399,230 @@ func TestBackend_old_renew_revoke(t *testing.T) { Datacenter: "DC1", } - t.Log("[WARN] Verifying that the generated token does not exist...") + t.Log("Verifying that the generated token does not exist...") _, _, err = mgmtclient.ACL().TokenRead(d.Accessor, q) if err == nil { t.Fatal("err: expected error") } } + +func TestBackend_Management(t *testing.T) { + t.Run("management", func(t *testing.T) { + t.Parallel() + t.Run("pre-1.4.0", func(t *testing.T) { + t.Parallel() + testBackendManagement(t, "1.3.0") + }) + t.Run("1.4.0-rc", func(t *testing.T) { + t.Parallel() + testBackendManagement(t, "1.4.0-rc1") + }) + }) +} + +func testBackendManagement(t *testing.T, version string) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cleanup, connURL, connToken := prepareTestContainer(t, version) + defer cleanup() + connData := map[string]interface{}{ + "address": connURL, + "token": connToken, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfig(t, connData), + testAccStepWriteManagementPolicy(t, "test", ""), + testAccStepReadManagementToken(t, "test", connData), + }, + }) +} + +func TestBackend_crud(t *testing.T) { + b, _ := Factory(context.Background(), logical.TestBackendConfig()) + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepWritePolicy(t, "test", testPolicy, ""), + testAccStepWritePolicy(t, "test2", testPolicy, ""), + testAccStepWritePolicy(t, "test3", testPolicy, ""), + testAccStepReadPolicy(t, "test", testPolicy, 0), + testAccStepListPolicy(t, []string{"test", "test2", "test3"}), + testAccStepDeletePolicy(t, "test"), + }, + }) +} + +func TestBackend_role_lease(t *testing.T) { + b, _ := Factory(context.Background(), logical.TestBackendConfig()) + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepWritePolicy(t, "test", testPolicy, "6h"), + testAccStepReadPolicy(t, "test", testPolicy, 6*time.Hour), + testAccStepDeletePolicy(t, "test"), + }, + }) +} + +func testAccStepConfig( + t *testing.T, config map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "config/access", + Data: config, + } +} + +func testAccStepReadToken( + t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "creds/" + name, + Check: func(resp *logical.Response) error { + var d struct { + Token string `mapstructure:"token"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + log.Printf("[WARN] Generated token: %s", d.Token) + + // Build a client and verify that the credentials work + config := consulapi.DefaultConfig() + config.Address = conf["address"].(string) + config.Token = d.Token + client, err := consulapi.NewClient(config) + if err != nil { + return err + } + + log.Printf("[WARN] Verifying that the generated token works...") + _, err = client.KV().Put(&consulapi.KVPair{ + Key: "foo", + Value: []byte("bar"), + }, nil) + if err != nil { + return err + } + + return nil + }, + } +} + +func testAccStepReadManagementToken( + t *testing.T, name string, conf map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "creds/" + name, + Check: func(resp *logical.Response) error { + var d struct { + Token string `mapstructure:"token"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + log.Printf("[WARN] Generated token: %s", d.Token) + + // Build a client and verify that the credentials work + config := consulapi.DefaultConfig() + config.Address = conf["address"].(string) + config.Token = d.Token + client, err := consulapi.NewClient(config) + if err != nil { + return err + } + + log.Printf("[WARN] Verifying that the generated token works...") + _, _, err = client.ACL().Create(&consulapi.ACLEntry{ + Type: "management", + Name: "test2", + }, nil) + if err != nil { + return err + } + + return nil + }, + } +} + +func testAccStepWritePolicy(t *testing.T, name string, policy string, lease string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "roles/" + name, + Data: map[string]interface{}{ + "policy": base64.StdEncoding.EncodeToString([]byte(policy)), + "lease": lease, + }, + } +} + +func testAccStepWriteManagementPolicy(t *testing.T, name string, lease string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.UpdateOperation, + Path: "roles/" + name, + Data: map[string]interface{}{ + "token_type": "management", + "lease": lease, + }, + } +} + +func testAccStepReadPolicy(t *testing.T, name string, policy string, lease time.Duration) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: "roles/" + name, + Check: func(resp *logical.Response) error { + policyRaw := resp.Data["policy"].(string) + out, err := base64.StdEncoding.DecodeString(policyRaw) + if err != nil { + return err + } + if string(out) != policy { + return fmt.Errorf("mismatch: %s %s", out, policy) + } + + l := resp.Data["lease"].(int64) + if lease != time.Second*time.Duration(l) { + return fmt.Errorf("mismatch: %v %v", l, lease) + } + return nil + }, + } +} + +func testAccStepListPolicy(t *testing.T, names []string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ListOperation, + Path: "roles/", + Check: func(resp *logical.Response) error { + respKeys := resp.Data["keys"].([]string) + if !reflect.DeepEqual(respKeys, names) { + return fmt.Errorf("mismatch: %#v %#v", respKeys, names) + } + return nil + }, + } +} + +func testAccStepDeletePolicy(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.DeleteOperation, + Path: "roles/" + name, + } +} + +const testPolicy = ` +key "" { + policy = "write" +} +` diff --git a/builtin/logical/consul/client.go b/builtin/logical/consul/client.go index 228529deb124..38cf8a890013 100644 --- a/builtin/logical/consul/client.go +++ b/builtin/logical/consul/client.go @@ -8,8 +8,8 @@ import ( "github.com/hashicorp/vault/logical" ) -func client(ctx context.Context, s logical.Storage) (*api.Client, error, error) { - conf, userErr, intErr := readConfigAccess(ctx, s) +func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, error, error) { + conf, userErr, intErr := b.readConfigAccess(ctx, s) if intErr != nil { return nil, nil, intErr } diff --git a/builtin/logical/consul/path_config.go b/builtin/logical/consul/path_config.go index 61087ff5b26e..2454b1b7cace 100644 --- a/builtin/logical/consul/path_config.go +++ b/builtin/logical/consul/path_config.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/vault/logical/framework" ) -func pathConfigAccess() *framework.Path { +func pathConfigAccess(b *backend) *framework.Path { return &framework.Path{ Pattern: "config/access", Fields: map[string]*framework.FieldSchema{ @@ -35,13 +35,13 @@ func pathConfigAccess() *framework.Path { }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: pathConfigAccessRead, - logical.UpdateOperation: pathConfigAccessWrite, + logical.ReadOperation: b.pathConfigAccessRead, + logical.UpdateOperation: b.pathConfigAccessWrite, }, } } -func readConfigAccess(ctx context.Context, storage logical.Storage) (*accessConfig, error, error) { +func (b *backend) readConfigAccess(ctx context.Context, storage logical.Storage) (*accessConfig, error, error) { entry, err := storage.Get(ctx, "config/access") if err != nil { return nil, nil, err @@ -58,8 +58,8 @@ func readConfigAccess(ctx context.Context, storage logical.Storage) (*accessConf return conf, nil, nil } -func pathConfigAccessRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { - conf, userErr, intErr := readConfigAccess(ctx, req.Storage) +func (b *backend) pathConfigAccessRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + conf, userErr, intErr := b.readConfigAccess(ctx, req.Storage) if intErr != nil { return nil, intErr } @@ -78,7 +78,7 @@ func pathConfigAccessRead(ctx context.Context, req *logical.Request, data *frame }, nil } -func pathConfigAccessWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { entry, err := logical.StorageEntryJSON("config/access", accessConfig{ Address: data.Get("address").(string), Scheme: data.Get("scheme").(string), diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index b88a8fec67ac..3893a46f354a 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -112,7 +112,9 @@ func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.Field func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { tokenType := d.Get("token_type").(string) policy := d.Get("policy").(string) + name := d.Get("name").(string) policies := d.Get("policies").([]string) + if len(policies) == 0 { switch tokenType { case "client": @@ -121,24 +123,14 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel return logical.ErrorResponse( "token_type must be \"client\" or \"management\""), nil } - } - - if (policy == "" && tokenType != "management") && len(policies) == 0 { - return logical.ErrorResponse( - "Use either a policy document, or a list of policies, depending on your Consul version"), nil - } - name := d.Get("name").(string) - - var policyRaw []byte - var err error - if len(policies) == 0 { - if tokenType != "management" && policy == "" { + if policy == "" && tokenType != "management" { return logical.ErrorResponse( - "policy cannot be empty when not using management tokens"), nil + "Use either a policy document, or a list of policies, depending on your Consul version"), nil } } - policyRaw, err = base64.StdEncoding.DecodeString(d.Get("policy").(string)) + + policyRaw, err := base64.StdEncoding.DecodeString(d.Get("policy").(string)) if err != nil { return logical.ErrorResponse(fmt.Sprintf( "Error decoding policy base64: %s", err)), nil diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go index 17adc63dcabb..7a260a0ec893 100644 --- a/builtin/logical/consul/path_token.go +++ b/builtin/logical/consul/path_token.go @@ -48,7 +48,7 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr } // Get the consul client - c, userErr, intErr := client(ctx, req.Storage) + c, userErr, intErr := b.client(ctx, req.Storage) if intErr != nil { return nil, intErr } @@ -61,9 +61,10 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr writeOpts := &api.WriteOptions{} writeOpts = writeOpts.WithContext(ctx) - var s *logical.Response + // Create an ACLEntry for Consul pre 1.4 - if (result.Policy != "" && result.TokenType == "client") || (result.Policy == "" && result.TokenType == "management") { + if (result.Policy != "" && result.TokenType == "client") || + (result.Policy == "" && result.TokenType == "management") { token, _, err := c.ACL().Create(&api.ACLEntry{ Name: tokenName, Type: result.TokenType, @@ -73,42 +74,40 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr return logical.ErrorResponse(err.Error()), nil } // Use the helper to create the secret - s = b.Secret(SecretTokenType).Response(map[string]interface{}{ + s := b.Secret(SecretTokenType).Response(map[string]interface{}{ "token": token, }, map[string]interface{}{ - "token": token, - "role": role, - "version": "1.3", + "token": token, + "role": role, }) s.Secret.TTL = result.Lease + return s, nil } //Create an ACLToken for Consul 1.4 and above - if len(result.Policies) > 0 && result.Policy == "" && result.TokenType != "management" { - var policyLink = []*api.ACLTokenPolicyLink{} - for _, policyName := range result.Policies { - policyLink = append(policyLink, &api.ACLTokenPolicyLink{ - Name: policyName, - }) - } - token, _, err := c.ACL().TokenCreate(&api.ACLToken{ - Description: tokenName, - Policies: policyLink, - }, writeOpts) - if err != nil { - return logical.ErrorResponse(err.Error()), nil - } - // Use the helper to create the secret - s = b.Secret(SecretTokenType).Response(map[string]interface{}{ - "token": token.SecretID, - "accessor": token.AccessorID, - }, map[string]interface{}{ - "token": token.AccessorID, - "role": role, - "version": "1.4", + var policyLink = []*api.ACLTokenPolicyLink{} + for _, policyName := range result.Policies { + policyLink = append(policyLink, &api.ACLTokenPolicyLink{ + Name: policyName, }) - s.Secret.TTL = result.Lease } + token, _, err := c.ACL().TokenCreate(&api.ACLToken{ + Description: tokenName, + Policies: policyLink, + }, writeOpts) + if err != nil { + return logical.ErrorResponse(err.Error()), nil + } + // Use the helper to create the secret + s := b.Secret(SecretTokenType).Response(map[string]interface{}{ + "token": token.SecretID, + "accessor": token.AccessorID, + }, map[string]interface{}{ + "token": token.AccessorID, + "role": role, + "version": "1.4", + }) + s.Secret.TTL = result.Lease return s, nil } diff --git a/builtin/logical/consul/secret_token.go b/builtin/logical/consul/secret_token.go index 4a2cfb624a26..9e2e674d35f8 100644 --- a/builtin/logical/consul/secret_token.go +++ b/builtin/logical/consul/secret_token.go @@ -24,14 +24,14 @@ func secretToken(b *backend) *framework.Secret { }, Renew: b.secretTokenRenew, - Revoke: secretTokenRevoke, + Revoke: b.secretTokenRevoke, } } func (b *backend) secretTokenRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { resp := &logical.Response{Secret: req.Secret} roleRaw, ok := req.Secret.InternalData["role"] - if !ok || roleRaw == nil { + if !ok { return resp, nil } @@ -56,8 +56,8 @@ func (b *backend) secretTokenRenew(ctx context.Context, req *logical.Request, d return resp, nil } -func secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { - c, userErr, intErr := client(ctx, req.Storage) +func (b *backend) secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + c, userErr, intErr := b.client(ctx, req.Storage) if intErr != nil { return nil, intErr } @@ -67,7 +67,6 @@ func secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.F } tokenRaw, ok := req.Secret.InternalData["token"] - version, ok := req.Secret.InternalData["version"] if !ok { // We return nil here because this is a pre-0.5.3 problem and there is // nothing we can do about it. We already can't revoke the lease @@ -76,18 +75,26 @@ func secretTokenRevoke(ctx context.Context, req *logical.Request, d *framework.F return nil, nil } - if version == "1.3" || version == nil { + var version string + versionRaw, ok := req.Secret.InternalData["version"] + if ok { + version = versionRaw.(string) + } + + switch version { + case "": + // Pre 1.4 tokens _, err := c.ACL().Destroy(tokenRaw.(string), nil) if err != nil { return nil, err } - } - - if version == "1.4" { + case "1.4": _, err := c.ACL().TokenDelete(tokenRaw.(string), nil) if err != nil { return nil, err } + default: + return nil, fmt.Errorf("Invalid version string in data: %s", version) } return nil, nil From a93b43bf2e3ae6e4a18795b9c0cf723e3dfd6fdf Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Wed, 31 Oct 2018 16:20:55 -0400 Subject: [PATCH 09/15] removing cast --- builtin/logical/consul/path_roles.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index 3893a46f354a..25aabec884b1 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -144,7 +144,7 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{ Policy: string(policyRaw), - Policies: []string(policies), + Policies: policies, Lease: lease, TokenType: tokenType, }) From 8ec0c5343a36d144f2645414eb081fcb459beb8c Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Wed, 31 Oct 2018 16:53:35 -0400 Subject: [PATCH 10/15] converting old lease field to ttl, adding max ttl --- builtin/logical/consul/path_roles.go | 47 ++++++++++++++----- builtin/logical/consul/path_token.go | 6 ++- builtin/logical/consul/secret_token.go | 3 +- .../source/api/secret/consul/index.html.md | 12 +++-- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index 25aabec884b1..7d85949ef4c8 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -50,9 +50,19 @@ the "policy" parameter is not required. Defaults to 'client'.`, }, + "ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: "TTL for the Consul token created from the role.", + }, + + "max_ttl": &framework.FieldSchema{ + Type: framework.TypeDurationSecond, + Description: "Max TTL for the Consul token created from the role.", + }, + "lease": &framework.FieldSchema{ Type: framework.TypeDurationSecond, - Description: "Lease time of the role.", + Description: "Deprecated, use ttl.", }, }, @@ -97,6 +107,7 @@ func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.Field resp := &logical.Response{ Data: map[string]interface{}{ "lease": int64(result.Lease.Seconds()), + "ttl": int64(result.Lease.Seconds()), "token_type": result.TokenType, }, } @@ -118,35 +129,46 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel if len(policies) == 0 { switch tokenType { case "client": + if policy == "" { + return logical.ErrorResponse( + "Use either a policy document, or a list of policies, depending on your Consul version"), nil + } case "management": default: return logical.ErrorResponse( "token_type must be \"client\" or \"management\""), nil } - - if policy == "" && tokenType != "management" { - return logical.ErrorResponse( - "Use either a policy document, or a list of policies, depending on your Consul version"), nil - } } - policyRaw, err := base64.StdEncoding.DecodeString(d.Get("policy").(string)) + policyRaw, err := base64.StdEncoding.DecodeString(policy) if err != nil { return logical.ErrorResponse(fmt.Sprintf( "Error decoding policy base64: %s", err)), nil } - var lease time.Duration - leaseParamRaw, ok := d.GetOk("lease") + var ttl time.Duration + ttlRaw, ok := d.GetOk("ttl") + if ok { + ttl = time.Second * time.Duration(ttlRaw.(int)) + } else { + leaseParamRaw, ok := d.GetOk("lease") + if ok { + ttl = time.Second * time.Duration(leaseParamRaw.(int)) + } + } + + var maxTTL time.Duration + maxTTLRaw, ok := d.GetOk("max_ttl") if ok { - lease = time.Second * time.Duration(leaseParamRaw.(int)) + maxTTL = time.Second * time.Duration(maxTTLRaw.(int)) } entry, err := logical.StorageEntryJSON("policy/"+name, roleConfig{ Policy: string(policyRaw), Policies: policies, - Lease: lease, TokenType: tokenType, + TTL: ttl, + MaxTTL: maxTTL, }) if err != nil { return nil, err @@ -170,6 +192,7 @@ func pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.Fie type roleConfig struct { Policy string `json:"policy"` Policies []string `json:"policies"` - Lease time.Duration `json:"lease"` + TTL time.Duration `json:"lease"` + MaxTTL time.Duration `json:"max_ttl"` TokenType string `json:"token_type"` } diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go index 7a260a0ec893..f29df7d1ad6e 100644 --- a/builtin/logical/consul/path_token.go +++ b/builtin/logical/consul/path_token.go @@ -80,7 +80,8 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr "token": token, "role": role, }) - s.Secret.TTL = result.Lease + s.Secret.TTL = result.TTL + s.Secret.MaxTTL = result.MaxTTL return s, nil } @@ -107,7 +108,8 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr "role": role, "version": "1.4", }) - s.Secret.TTL = result.Lease + s.Secret.TTL = result.TTL + s.Secret.MaxTTL = result.MaxTTL return s, nil } diff --git a/builtin/logical/consul/secret_token.go b/builtin/logical/consul/secret_token.go index 9e2e674d35f8..4a2460f929a3 100644 --- a/builtin/logical/consul/secret_token.go +++ b/builtin/logical/consul/secret_token.go @@ -52,7 +52,8 @@ func (b *backend) secretTokenRenew(ctx context.Context, req *logical.Request, d if err := entry.DecodeJSON(&result); err != nil { return nil, err } - resp.Secret.TTL = result.Lease + resp.Secret.TTL = result.TTL + resp.Secret.MaxTTL = result.MaxTTL return resp, nil } diff --git a/website/source/api/secret/consul/index.html.md b/website/source/api/secret/consul/index.html.md index f9bc575cbe33..35d514466676 100644 --- a/website/source/api/secret/consul/index.html.md +++ b/website/source/api/secret/consul/index.html.md @@ -72,10 +72,6 @@ updated attributes. - `name` `(string: )` – Specifies the name of an existing role against which to create this Consul credential. This is part of the request URL. -- `lease` `(string: "")` – Specifies the lease for this role. This is provided - as a string duration with a time suffix like `"30s"` or `"1h"`. If not - provided, the default Vault lease is used. - - `policy` `(string: )` – Specifies the base64 encoded ACL policy. The ACL format can be found in the [Consul ACL documentation](https://www.consul.io/docs/internals/acl.html). This is @@ -84,6 +80,14 @@ updated attributes. - `token_type` `(string: "client")` - Specifies the type of token to create when using this role. Valid values are `"client"` or `"management"`. +- `ttl` `(duration: "")` – Specifies the TTL for this role. This is provided + as a string duration with a time suffix like `"30s"` or `"1h"` or as seconds. If not + provided, the default Vault TTL is used. + +- `max_ttl` `(duration: "")` – Specifies the max TTL for this role. This is provided + as a string duration with a time suffix like `"30s"` or `"1h"` or as seconds. If not + provided, the default Vault Max TTL is used. + ### Sample Payload To create management tokens: From 84fac7705b2b704c6e055b070622309a25396e6a Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Thu, 1 Nov 2018 08:40:26 -0400 Subject: [PATCH 11/15] cleanup --- builtin/logical/consul/backend.go | 2 +- builtin/logical/consul/path_roles.go | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/builtin/logical/consul/backend.go b/builtin/logical/consul/backend.go index ccd98ab4fdec..3cd74c4cf319 100644 --- a/builtin/logical/consul/backend.go +++ b/builtin/logical/consul/backend.go @@ -27,7 +27,7 @@ func Backend() *backend { Paths: []*framework.Path{ pathConfigAccess(&b), pathListRoles(&b), - pathRoles(), + pathRoles(&b), pathToken(&b), }, diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index 7d85949ef4c8..7870fc27e84a 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -20,7 +20,7 @@ func pathListRoles(b *backend) *framework.Path { } } -func pathRoles() *framework.Path { +func (b *backend) pathRoles() *framework.Path { return &framework.Path{ Pattern: "roles/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ @@ -62,14 +62,14 @@ Defaults to 'client'.`, "lease": &framework.FieldSchema{ Type: framework.TypeDurationSecond, - Description: "Deprecated, use ttl.", + Description: "DEPRECATED: Use ttl.", }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: pathRolesRead, - logical.UpdateOperation: pathRolesWrite, - logical.DeleteOperation: pathRolesDelete, + logical.ReadOperation: b.pathRolesRead, + logical.UpdateOperation: b.pathRolesWrite, + logical.DeleteOperation: b.pathRolesDelete, }, } } @@ -83,7 +83,7 @@ func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, d *fra return logical.ListResponse(entries), nil } -func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathRolesRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) entry, err := req.Storage.Get(ctx, "policy/"+name) @@ -106,8 +106,9 @@ func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.Field // Generate the response resp := &logical.Response{ Data: map[string]interface{}{ - "lease": int64(result.Lease.Seconds()), - "ttl": int64(result.Lease.Seconds()), + "lease": int64(result.TTL.Seconds()), + "ttl": int64(result.TTL.Seconds()), + "max_ttl": int64(result.MaxTTL.Seconds()), "token_type": result.TokenType, }, } @@ -120,7 +121,7 @@ func pathRolesRead(ctx context.Context, req *logical.Request, d *framework.Field return resp, nil } -func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { tokenType := d.Get("token_type").(string) policy := d.Get("policy").(string) name := d.Get("name").(string) @@ -181,7 +182,7 @@ func pathRolesWrite(ctx context.Context, req *logical.Request, d *framework.Fiel return nil, nil } -func pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { +func (b *backend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { name := d.Get("name").(string) if err := req.Storage.Delete(ctx, "policy/"+name); err != nil { return nil, err From 920a3e158dd06ae511135690d6ebd678cb0a9e4d Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Thu, 1 Nov 2018 08:47:40 -0400 Subject: [PATCH 12/15] adding missing test --- builtin/logical/consul/backend_test.go | 44 ++++++++++++++++++++++++++ builtin/logical/consul/path_roles.go | 2 +- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index 690f04c2c80d..d3e53b922362 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -445,6 +445,50 @@ func testBackendManagement(t *testing.T, version string) { }) } +func TestBackend_Basic(t *testing.T) { + t.Run("basic", func(t *testing.T) { + t.Parallel() + t.Run("pre-1.4.0", func(t *testing.T) { + t.Parallel() + testBackendBasic(t, "1.3.0") + }) + t.Run("1.4.0-rc", func(t *testing.T) { + t.Parallel() + t.Run("legacy", func(t *testing.T) { + t.Parallel() + testBackendRenewRevoke(t, "1.3.0") + }) + + testBackendBasic(t, "1.4.0-rc1") + }) + }) +} + +func testBackendBasic(t *testing.T, version string) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cleanup, connURL, connToken := prepareTestContainer(t, version) + defer cleanup() + connData := map[string]interface{}{ + "address": connURL, + "token": connToken, + } + + logicaltest.Test(t, logicaltest.TestCase{ + Backend: b, + Steps: []logicaltest.TestStep{ + testAccStepConfig(t, connData), + testAccStepWritePolicy(t, "test", testPolicy, ""), + testAccStepReadToken(t, "test", connData), + }, + }) +} + func TestBackend_crud(t *testing.T) { b, _ := Factory(context.Background(), logical.TestBackendConfig()) logicaltest.Test(t, logicaltest.TestCase{ diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index 7870fc27e84a..93f87628318f 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -20,7 +20,7 @@ func pathListRoles(b *backend) *framework.Path { } } -func (b *backend) pathRoles() *framework.Path { +func pathRoles(b *backend) *framework.Path { return &framework.Path{ Pattern: "roles/" + framework.GenericNameRegex("name"), Fields: map[string]*framework.FieldSchema{ From f059535dee7fff00caef51c21260bacbbce4a00a Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Thu, 1 Nov 2018 08:50:48 -0400 Subject: [PATCH 13/15] testing wrong version --- builtin/logical/consul/backend_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index d3e53b922362..5c3d2238bb8c 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -184,7 +184,7 @@ func TestBackend_Renew_Revoke(t *testing.T) { t.Parallel() t.Run("legacy", func(t *testing.T) { t.Parallel() - testBackendRenewRevoke(t, "1.3.0") + testBackendRenewRevoke(t, "1.4.0-rc1") }) testBackendRenewRevoke14(t, "1.4.0-rc1") @@ -456,7 +456,7 @@ func TestBackend_Basic(t *testing.T) { t.Parallel() t.Run("legacy", func(t *testing.T) { t.Parallel() - testBackendRenewRevoke(t, "1.3.0") + testBackendRenewRevoke(t, "1.4.0-rc1") }) testBackendBasic(t, "1.4.0-rc1") From 38c57e100c75effd82096d589b3c56ee4deef5ba Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Thu, 1 Nov 2018 10:41:45 -0400 Subject: [PATCH 14/15] adding support for local tokens --- builtin/logical/consul/backend_test.go | 128 ++++++++++++++++++ builtin/logical/consul/path_roles.go | 10 ++ builtin/logical/consul/path_token.go | 4 + .../source/api/secret/consul/index.html.md | 13 +- 4 files changed, 152 insertions(+), 3 deletions(-) diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go index 5c3d2238bb8c..dba4be62d2c1 100644 --- a/builtin/logical/consul/backend_test.go +++ b/builtin/logical/consul/backend_test.go @@ -406,6 +406,134 @@ func testBackendRenewRevoke14(t *testing.T, version string) { } } +func TestBackend_LocalToken(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cleanup, connURL, connToken := prepareTestContainer(t, "1.4.0-rc1") + defer cleanup() + connData := map[string]interface{}{ + "address": connURL, + "token": connToken, + } + + req := &logical.Request{ + Storage: config.StorageView, + Operation: logical.UpdateOperation, + Path: "config/access", + Data: connData, + } + resp, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Path = "roles/test" + req.Data = map[string]interface{}{ + "policies": []string{"test"}, + "ttl": "6h", + "local": false, + } + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Path = "roles/test_local" + req.Data = map[string]interface{}{ + "policies": []string{"test"}, + "ttl": "6h", + "local": true, + } + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.ReadOperation + req.Path = "creds/test" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("resp nil") + } + if resp.IsError() { + t.Fatalf("resp is error: %v", resp.Error()) + } + + var d struct { + Token string `mapstructure:"token"` + Accessor string `mapstructure:"accessor"` + Local bool `mapstructure:"local"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + t.Fatal(err) + } + t.Logf("Generated token: %s with accessor %s", d.Token, d.Accessor) + + if d.Local { + t.Fatalf("requested global token, got local one") + } + + // Build a client and verify that the credentials work + consulapiConfig := consulapi.DefaultNonPooledConfig() + consulapiConfig.Address = connData["address"].(string) + consulapiConfig.Token = d.Token + client, err := consulapi.NewClient(consulapiConfig) + if err != nil { + t.Fatal(err) + } + + t.Log("Verifying that the generated token works...") + _, err = client.Catalog(), nil + if err != nil { + t.Fatal(err) + } + + req.Operation = logical.ReadOperation + req.Path = "creds/test_local" + resp, err = b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatal(err) + } + if resp == nil { + t.Fatal("resp nil") + } + if resp.IsError() { + t.Fatalf("resp is error: %v", resp.Error()) + } + + if err := mapstructure.Decode(resp.Data, &d); err != nil { + t.Fatal(err) + } + t.Logf("Generated token: %s with accessor %s", d.Token, d.Accessor) + + if !d.Local { + t.Fatalf("requested local token, got global one") + } + + // Build a client and verify that the credentials work + consulapiConfig = consulapi.DefaultNonPooledConfig() + consulapiConfig.Address = connData["address"].(string) + consulapiConfig.Token = d.Token + client, err = consulapi.NewClient(consulapiConfig) + if err != nil { + t.Fatal(err) + } + + t.Log("Verifying that the generated token works...") + _, err = client.Catalog(), nil + if err != nil { + t.Fatal(err) + } +} + func TestBackend_Management(t *testing.T) { t.Run("management", func(t *testing.T) { t.Parallel() diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index 93f87628318f..5f8bd34a1b29 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -41,6 +41,12 @@ for 'client' tokens. Required for Consul pre-1.4`, for Consul 1.4 or above`, }, + "local": &framework.FieldSchema{ + Type: framework.TypeBool, + Description: `Indicates that the token should not be replicated globally +and instead be local to the current datacenter. Available in Consul 1.4 and above.`, + }, + "token_type": &framework.FieldSchema{ Type: framework.TypeString, Default: "client", @@ -110,6 +116,7 @@ func (b *backend) pathRolesRead(ctx context.Context, req *logical.Request, d *fr "ttl": int64(result.TTL.Seconds()), "max_ttl": int64(result.MaxTTL.Seconds()), "token_type": result.TokenType, + "local": result.Local, }, } if result.Policy != "" { @@ -126,6 +133,7 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f policy := d.Get("policy").(string) name := d.Get("name").(string) policies := d.Get("policies").([]string) + local := d.Get("local").(bool) if len(policies) == 0 { switch tokenType { @@ -170,6 +178,7 @@ func (b *backend) pathRolesWrite(ctx context.Context, req *logical.Request, d *f TokenType: tokenType, TTL: ttl, MaxTTL: maxTTL, + Local: local, }) if err != nil { return nil, err @@ -196,4 +205,5 @@ type roleConfig struct { TTL time.Duration `json:"lease"` MaxTTL time.Duration `json:"max_ttl"` TokenType string `json:"token_type"` + Local bool `json:"local"` } diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go index f29df7d1ad6e..1aa727fd7e9b 100644 --- a/builtin/logical/consul/path_token.go +++ b/builtin/logical/consul/path_token.go @@ -73,6 +73,7 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr if err != nil { return logical.ErrorResponse(err.Error()), nil } + // Use the helper to create the secret s := b.Secret(SecretTokenType).Response(map[string]interface{}{ "token": token, @@ -95,14 +96,17 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr token, _, err := c.ACL().TokenCreate(&api.ACLToken{ Description: tokenName, Policies: policyLink, + Local: result.Local, }, writeOpts) if err != nil { return logical.ErrorResponse(err.Error()), nil } + // Use the helper to create the secret s := b.Secret(SecretTokenType).Response(map[string]interface{}{ "token": token.SecretID, "accessor": token.AccessorID, + "local": token.Local, }, map[string]interface{}{ "token": token.AccessorID, "role": role, diff --git a/website/source/api/secret/consul/index.html.md b/website/source/api/secret/consul/index.html.md index 35d514466676..9c16bfba3f11 100644 --- a/website/source/api/secret/consul/index.html.md +++ b/website/source/api/secret/consul/index.html.md @@ -72,13 +72,20 @@ updated attributes. - `name` `(string: )` – Specifies the name of an existing role against which to create this Consul credential. This is part of the request URL. -- `policy` `(string: )` – Specifies the base64 encoded ACL policy. The +- `token_type` `(string: "client")` - Specifies the type of token to create when + using this role. Valid values are `"client"` or `"management"`. + +- `policy` `(string: )` – Specifies the base64 encoded ACL policy. The ACL format can be found in the [Consul ACL documentation](https://www.consul.io/docs/internals/acl.html). This is required unless the `token_type` is `management`. -- `token_type` `(string: "client")` - Specifies the type of token to create when - using this role. Valid values are `"client"` or `"management"`. +- `policies` `(list: )` – The list of policies to assign to the generated + token. This is only available in Consul 1.4 and greater. + +- `local` `(bool: false)` - Indicates that the token should not be replicated + globally and instead be local to the current datacenter. Only available in Consul + 1.4 and greater. - `ttl` `(duration: "")` – Specifies the TTL for this role. This is provided as a string duration with a time suffix like `"30s"` or `"1h"` or as seconds. If not From 79bbd8684e3a0e3a42e535e1a1f1c70982aa04ff Mon Sep 17 00:00:00 2001 From: Chris Hoffman Date: Fri, 2 Nov 2018 10:42:46 -0400 Subject: [PATCH 15/15] addressing feedback --- builtin/logical/consul/path_roles.go | 6 +++--- builtin/logical/consul/path_token.go | 6 +++++- builtin/logical/consul/secret_token.go | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/builtin/logical/consul/path_roles.go b/builtin/logical/consul/path_roles.go index 5f8bd34a1b29..cb1df25ebc5c 100644 --- a/builtin/logical/consul/path_roles.go +++ b/builtin/logical/consul/path_roles.go @@ -32,13 +32,13 @@ func pathRoles(b *backend) *framework.Path { "policy": &framework.FieldSchema{ Type: framework.TypeString, Description: `Policy document, base64 encoded. Required -for 'client' tokens. Required for Consul pre-1.4`, +for 'client' tokens. Required for Consul pre-1.4.`, }, "policies": &framework.FieldSchema{ Type: framework.TypeCommaStringSlice, - Description: `List of policies attached to the token. Required -for Consul 1.4 or above`, + Description: `List of policies to attach to the token. Required +for Consul 1.4 or above.`, }, "local": &framework.FieldSchema{ diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go index 1aa727fd7e9b..ee6686f1ffd0 100644 --- a/builtin/logical/consul/path_token.go +++ b/builtin/logical/consul/path_token.go @@ -11,6 +11,10 @@ import ( "github.com/hashicorp/vault/logical/framework" ) +const ( + tokenPolicyType = "token" +) + func pathToken(b *backend) *framework.Path { return &framework.Path{ Pattern: "creds/" + framework.GenericNameRegex("role"), @@ -110,7 +114,7 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr }, map[string]interface{}{ "token": token.AccessorID, "role": role, - "version": "1.4", + "version": tokenPolicyType, }) s.Secret.TTL = result.TTL s.Secret.MaxTTL = result.MaxTTL diff --git a/builtin/logical/consul/secret_token.go b/builtin/logical/consul/secret_token.go index 4a2460f929a3..6197bff59cdb 100644 --- a/builtin/logical/consul/secret_token.go +++ b/builtin/logical/consul/secret_token.go @@ -89,7 +89,7 @@ func (b *backend) secretTokenRevoke(ctx context.Context, req *logical.Request, d if err != nil { return nil, err } - case "1.4": + case tokenPolicyType: _, err := c.ACL().TokenDelete(tokenRaw.(string), nil) if err != nil { return nil, err