From 5674fbb0909db376b9578374a993cba38829b0d8 Mon Sep 17 00:00:00 2001 From: "R.B. Boyer" Date: Mon, 8 Apr 2019 13:19:09 -0500 Subject: [PATCH] acl: ACL Tokens can now be assigned an optional set of service identities (#5390) These act like a special cased version of a Policy Template for granting a token the privileges necessary to register a service and its connect proxy, and read upstreams from the catalog. --- agent/consul/acl.go | 104 ++++++++++- agent/consul/acl_endpoint.go | 44 ++++- agent/consul/acl_endpoint_test.go | 118 +++++++++++++ agent/consul/acl_test.go | 89 ++++++++++ agent/consul/leader.go | 4 +- agent/consul/state/acl.go | 6 + agent/consul/state/acl_test.go | 51 ++++++ agent/structs/acl.go | 161 +++++++++++++++--- agent/structs/acl_legacy.go | 17 +- agent/structs/acl_test.go | 63 +++++++ api/acl.go | 52 +++--- command/acl/acl_helpers.go | 37 ++++ command/acl/token/create/token_create.go | 19 ++- command/acl/token/update/token_update.go | 47 ++++- vendor/github.com/hashicorp/consul/api/acl.go | 52 +++--- 15 files changed, 761 insertions(+), 103 deletions(-) diff --git a/agent/consul/acl.go b/agent/consul/acl.go index 4f1061e5d173..2e89eba2f284 100644 --- a/agent/consul/acl.go +++ b/agent/consul/acl.go @@ -4,10 +4,11 @@ import ( "fmt" "log" "os" + "sort" "sync" "time" - "github.com/armon/go-metrics" + metrics "github.com/armon/go-metrics" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/api" @@ -133,7 +134,7 @@ type ACLResolverConfig struct { // - Resolving policies remotely via an ACL.PolicyResolve RPC // // Remote Resolution: -// Remote resolution can be done syncrhonously or asynchronously depending +// Remote resolution can be done synchronously or asynchronously depending // on the ACLDownPolicy in the Config passed to the resolver. // // When the down policy is set to async-cache and we have already cached values @@ -503,7 +504,9 @@ func (r *ACLResolver) filterPoliciesByScope(policies structs.ACLPolicies) struct func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (structs.ACLPolicies, error) { policyIDs := identity.PolicyIDs() - if len(policyIDs) == 0 { + serviceIdentities := identity.ServiceIdentityList() + + if len(policyIDs) == 0 && len(serviceIdentities) == 0 { policy := identity.EmbeddedPolicy() if policy != nil { return []*structs.ACLPolicy{policy}, nil @@ -513,9 +516,96 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) ( return nil, nil } + syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities) + // For the new ACLs policy replication is mandatory for correct operation on servers. Therefore // we only attempt to resolve policies locally - policies := make([]*structs.ACLPolicy, 0, len(policyIDs)) + policies, err := r.collectPoliciesForIdentity(identity, policyIDs, len(syntheticPolicies)) + if err != nil { + return nil, err + } + + policies = append(policies, syntheticPolicies...) + filtered := r.filterPoliciesByScope(policies) + return filtered, nil +} + +func (r *ACLResolver) synthesizePoliciesForServiceIdentities(serviceIdentities []*structs.ACLServiceIdentity) []*structs.ACLPolicy { + if len(serviceIdentities) == 0 { + return nil + } + + // Collect and dedupe service identities. Prefer increasing datacenter scope. + serviceIdentities = dedupeServiceIdentities(serviceIdentities) + + syntheticPolicies := make([]*structs.ACLPolicy, 0, len(serviceIdentities)) + for _, s := range serviceIdentities { + syntheticPolicies = append(syntheticPolicies, s.SyntheticPolicy()) + } + + return syntheticPolicies +} + +func dedupeServiceIdentities(in []*structs.ACLServiceIdentity) []*structs.ACLServiceIdentity { + // From: https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable + + if len(in) <= 1 { + return in + } + + sort.Slice(in, func(i, j int) bool { + return in[i].ServiceName < in[j].ServiceName + }) + + j := 0 + for i := 1; i < len(in); i++ { + if in[j].ServiceName == in[i].ServiceName { + // Prefer increasing scope. + if len(in[j].Datacenters) == 0 || len(in[i].Datacenters) == 0 { + in[j].Datacenters = nil + } else { + in[j].Datacenters = mergeStringSlice(in[j].Datacenters, in[i].Datacenters) + } + continue + } + j++ + in[j] = in[i] + } + + // Discard the skipped items. + for i := j + 1; i < len(in); i++ { + in[i] = nil + } + + return in[:j+1] +} + +func mergeStringSlice(a, b []string) []string { + out := make([]string, 0, len(a)+len(b)) + out = append(out, a...) + out = append(out, b...) + return dedupeStringSlice(out) +} + +func dedupeStringSlice(in []string) []string { + // From: https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable + + sort.Strings(in) + + j := 0 + for i := 1; i < len(in); i++ { + if in[j] == in[i] { + continue + } + j++ + in[j] = in[i] + } + + return in[:j+1] +} + +func (r *ACLResolver) collectPoliciesForIdentity(identity structs.ACLIdentity, policyIDs []string, extraCap int) ([]*structs.ACLPolicy, error) { + policies := make([]*structs.ACLPolicy, 0, len(policyIDs)+extraCap) // Get all associated policies var missing []string @@ -559,7 +649,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) ( // Hot-path if we have no missing or expired policies if len(missing)+len(expired) == 0 { - return r.filterPoliciesByScope(policies), nil + return policies, nil } hasMissing := len(missing) > 0 @@ -579,7 +669,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) ( if !waitForResult { // waitForResult being false requires that all the policies were cached already policies = append(policies, expired...) - return r.filterPoliciesByScope(policies), nil + return policies, nil } res := <-waitChan @@ -596,7 +686,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) ( } } - return r.filterPoliciesByScope(policies), nil + return policies, nil } func (r *ACLResolver) resolveTokenToPolicies(token string) (structs.ACLPolicies, error) { diff --git a/agent/consul/acl_endpoint.go b/agent/consul/acl_endpoint.go index c19ff297645a..65be9b9e8aa9 100644 --- a/agent/consul/acl_endpoint.go +++ b/agent/consul/acl_endpoint.go @@ -8,13 +8,13 @@ import ( "regexp" "time" - "github.com/armon/go-metrics" + metrics "github.com/armon/go-metrics" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/lib" - "github.com/hashicorp/go-memdb" - "github.com/hashicorp/go-uuid" + memdb "github.com/hashicorp/go-memdb" + uuid "github.com/hashicorp/go-uuid" ) const ( @@ -24,7 +24,11 @@ const ( ) // Regex for matching -var validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`) +var ( + validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`) + validServiceIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`) + serviceIdentityNameMaxLength = 256 +) // ACL endpoint is used to manipulate ACLs type ACL struct { @@ -275,10 +279,11 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok cloneReq := structs.ACLTokenSetRequest{ Datacenter: args.Datacenter, ACLToken: structs.ACLToken{ - Policies: token.Policies, - Local: token.Local, - Description: token.Description, - ExpirationTime: token.ExpirationTime, + Policies: token.Policies, + ServiceIdentities: token.ServiceIdentities, + Local: token.Local, + Description: token.Description, + ExpirationTime: token.ExpirationTime, }, WriteRequest: args.WriteRequest, } @@ -450,6 +455,18 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs. } token.Policies = policies + for _, svcid := range token.ServiceIdentities { + if svcid.ServiceName == "" { + return fmt.Errorf("Service identity is missing the service name field on this token") + } + if token.Local && len(svcid.Datacenters) > 0 { + return fmt.Errorf("Service identity %q cannot specify a list of datacenters on a local token", svcid.ServiceName) + } + if !isValidServiceIdentityName(svcid.ServiceName) { + return fmt.Errorf("Service identity %q has an invalid name. Only alphanumeric characters, '-' and '_' are allowed", svcid.ServiceName) + } + } + if token.Rules != "" { return fmt.Errorf("Rules cannot be specified for this token") } @@ -487,6 +504,17 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs. return nil } +// isValidServiceIdentityName returns true if the provided name can be used as +// an ACLServiceIdentity ServiceName. This is more restrictive than standard +// catalog registration, which basically takes the view that "everything is +// valid". +func isValidServiceIdentityName(name string) bool { + if len(name) < 1 || len(name) > serviceIdentityNameMaxLength { + return false + } + return validServiceIdentityName.MatchString(name) +} + func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) error { if err := a.aclPreCheck(); err != nil { return err diff --git a/agent/consul/acl_endpoint_test.go b/agent/consul/acl_endpoint_test.go index f06f62b0c07e..b68ffd9de70f 100644 --- a/agent/consul/acl_endpoint_test.go +++ b/agent/consul/acl_endpoint_test.go @@ -919,6 +919,124 @@ func TestACLEndpoint_TokenSet(t *testing.T) { require.Len(t, token.Policies, 0) }) + t.Run("Create it with invalid service identity (empty)", func(t *testing.T) { + req := structs.ACLTokenSetRequest{ + Datacenter: "dc1", + ACLToken: structs.ACLToken{ + Description: "foobar", + Policies: nil, + Local: false, + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ServiceName: ""}, + }, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + + resp := structs.ACLToken{} + + err := acl.TokenSet(&req, &resp) + requireErrorContains(t, err, "Service identity is missing the service name field") + }) + + t.Run("Create it with invalid service identity (too large)", func(t *testing.T) { + long := strings.Repeat("x", serviceIdentityNameMaxLength+1) + req := structs.ACLTokenSetRequest{ + Datacenter: "dc1", + ACLToken: structs.ACLToken{ + Description: "foobar", + Policies: nil, + Local: false, + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ServiceName: long}, + }, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + + resp := structs.ACLToken{} + + err := acl.TokenSet(&req, &resp) + require.NotNil(t, err) + }) + + for _, test := range []struct { + name string + ok bool + }{ + {"-abc", false}, + {"abc-", false}, + {"a-bc", true}, + {"_abc", false}, + {"abc_", false}, + {"a_bc", true}, + {":abc", false}, + {"abc:", false}, + {"a:bc", false}, + {"Abc", false}, + {"aBc", false}, + {"abC", false}, + {"0abc", true}, + {"abc0", true}, + {"a0bc", true}, + } { + var testName string + if test.ok { + testName = "Create it with valid service identity (by regex): " + test.name + } else { + testName = "Create it with invalid service identity (by regex): " + test.name + } + t.Run(testName, func(t *testing.T) { + req := structs.ACLTokenSetRequest{ + Datacenter: "dc1", + ACLToken: structs.ACLToken{ + Description: "foobar", + Policies: nil, + Local: false, + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ServiceName: test.name}, + }, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + + resp := structs.ACLToken{} + + err := acl.TokenSet(&req, &resp) + if test.ok { + require.NoError(t, err) + + // Get the token directly to validate that it exists + tokenResp, err := retrieveTestToken(codec, "root", "dc1", resp.AccessorID) + require.NoError(t, err) + token := tokenResp.Token + require.ElementsMatch(t, req.ACLToken.ServiceIdentities, token.ServiceIdentities) + } else { + require.NotNil(t, err) + } + }) + } + + t.Run("Create it with invalid service identity (datacenters set on local token)", func(t *testing.T) { + req := structs.ACLTokenSetRequest{ + Datacenter: "dc1", + ACLToken: structs.ACLToken{ + Description: "foobar", + Policies: nil, + Local: true, + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ServiceName: "foo", Datacenters: []string{"dc2"}}, + }, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + + resp := structs.ACLToken{} + + err := acl.TokenSet(&req, &resp) + requireErrorContains(t, err, "cannot specify a list of datacenters on a local token") + }) + for _, test := range []struct { name string offset time.Duration diff --git a/agent/consul/acl_test.go b/agent/consul/acl_test.go index 38b723cbdbd1..9ff773ca41b5 100644 --- a/agent/consul/acl_test.go +++ b/agent/consul/acl_test.go @@ -2861,3 +2861,92 @@ service "service" { t.Fatalf("err: %v", err) } } + +func TestDedupeServiceIdentities(t *testing.T) { + srvid := func(name string, datacenters ...string) *structs.ACLServiceIdentity { + return &structs.ACLServiceIdentity{ + ServiceName: name, + Datacenters: datacenters, + } + } + + tests := []struct { + name string + in []*structs.ACLServiceIdentity + expect []*structs.ACLServiceIdentity + }{ + { + name: "empty", + in: nil, + expect: nil, + }, + { + name: "one", + in: []*structs.ACLServiceIdentity{ + srvid("foo"), + }, + expect: []*structs.ACLServiceIdentity{ + srvid("foo"), + }, + }, + { + name: "just names", + in: []*structs.ACLServiceIdentity{ + srvid("fooZ"), + srvid("fooA"), + srvid("fooY"), + srvid("fooB"), + }, + expect: []*structs.ACLServiceIdentity{ + srvid("fooA"), + srvid("fooB"), + srvid("fooY"), + srvid("fooZ"), + }, + }, + { + name: "just names with dupes", + in: []*structs.ACLServiceIdentity{ + srvid("fooZ"), + srvid("fooA"), + srvid("fooY"), + srvid("fooB"), + srvid("fooA"), + srvid("fooB"), + srvid("fooY"), + srvid("fooZ"), + }, + expect: []*structs.ACLServiceIdentity{ + srvid("fooA"), + srvid("fooB"), + srvid("fooY"), + srvid("fooZ"), + }, + }, + { + name: "names with dupes and datacenters", + in: []*structs.ACLServiceIdentity{ + srvid("fooZ", "dc2", "dc4"), + srvid("fooA"), + srvid("fooY", "dc1"), + srvid("fooB"), + srvid("fooA", "dc9", "dc8"), + srvid("fooB"), + srvid("fooY", "dc1"), + srvid("fooZ", "dc3", "dc4"), + }, + expect: []*structs.ACLServiceIdentity{ + srvid("fooA"), + srvid("fooB"), + srvid("fooY", "dc1"), + srvid("fooZ", "dc2", "dc3", "dc4"), + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := dedupeServiceIdentities(test.in) + require.ElementsMatch(t, test.expect, got) + }) + } +} diff --git a/agent/consul/leader.go b/agent/consul/leader.go index 5b17d589ec20..c3d36aee11b7 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -645,7 +645,9 @@ func (s *Server) startACLUpgrade() { } // Assign the global-management policy to legacy management tokens - if len(newToken.Policies) == 0 && newToken.Type == structs.ACLTokenTypeManagement { + if len(newToken.Policies) == 0 && + len(newToken.ServiceIdentities) == 0 && + newToken.Type == structs.ACLTokenTypeManagement { newToken.Policies = append(newToken.Policies, structs.ACLTokenPolicyLink{ID: structs.ACLPolicyGlobalManagementID}) } diff --git a/agent/consul/state/acl.go b/agent/consul/state/acl.go index 36c0a03e4692..abeb1f7e226f 100644 --- a/agent/consul/state/acl.go +++ b/agent/consul/state/acl.go @@ -478,6 +478,12 @@ func (s *Store) aclTokenSetTxn(tx *memdb.Txn, idx uint64, token *structs.ACLToke return err } + for _, svcid := range token.ServiceIdentities { + if svcid.ServiceName == "" { + return fmt.Errorf("Encountered a Token with an empty service identity name in the state store") + } + } + // Set the indexes if original != nil { if original.AccessorID != "" && token.AccessorID != original.AccessorID { diff --git a/agent/consul/state/acl_test.go b/agent/consul/state/acl_test.go index 0e01fbcfe3df..9ec57674bc7a 100644 --- a/agent/consul/state/acl_test.go +++ b/agent/consul/state/acl_test.go @@ -277,6 +277,38 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) { require.Equal(t, ErrMissingACLTokenAccessor, err) }) + t.Run("Missing Service Identity Fields", func(t *testing.T) { + t.Parallel() + s := testACLTokensStateStore(t) + token := &structs.ACLToken{ + AccessorID: "daf37c07-d04d-4fd5-9678-a8206a57d61a", + SecretID: "39171632-6f34-4411-827f-9416403687f4", + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{}, + }, + } + + err := s.ACLTokenSet(2, token, false) + require.Error(t, err) + }) + + t.Run("Missing Service Identity Name", func(t *testing.T) { + t.Parallel() + s := testACLTokensStateStore(t) + token := &structs.ACLToken{ + AccessorID: "daf37c07-d04d-4fd5-9678-a8206a57d61a", + SecretID: "39171632-6f34-4411-827f-9416403687f4", + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ + Datacenters: []string{"dc1"}, + }, + }, + } + + err := s.ACLTokenSet(2, token, false) + require.Error(t, err) + }) + t.Run("Missing Policy ID", func(t *testing.T) { t.Parallel() s := testACLTokensStateStore(t) @@ -322,6 +354,11 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) { ID: "a0625e95-9b3e-42de-a8d6-ceef5b6f3286", }, }, + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ + ServiceName: "web", + }, + }, } require.NoError(t, s.ACLTokenSet(2, token.Clone(), false)) @@ -334,6 +371,8 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) { require.Equal(t, uint64(2), rtoken.ModifyIndex) require.Len(t, rtoken.Policies, 1) require.Equal(t, "node-read", rtoken.Policies[0].Name) + require.Len(t, rtoken.ServiceIdentities, 1) + require.Equal(t, "web", rtoken.ServiceIdentities[0].ServiceName) }) t.Run("Update", func(t *testing.T) { @@ -347,6 +386,11 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) { ID: "a0625e95-9b3e-42de-a8d6-ceef5b6f3286", }, }, + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ + ServiceName: "web", + }, + }, } require.NoError(t, s.ACLTokenSet(2, token.Clone(), false)) @@ -359,6 +403,11 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) { ID: structs.ACLPolicyGlobalManagementID, }, }, + ServiceIdentities: []*structs.ACLServiceIdentity{ + &structs.ACLServiceIdentity{ + ServiceName: "db", + }, + }, } require.NoError(t, s.ACLTokenSet(3, updated.Clone(), false)) @@ -372,6 +421,8 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) { require.Len(t, rtoken.Policies, 1) require.Equal(t, structs.ACLPolicyGlobalManagementID, rtoken.Policies[0].ID) require.Equal(t, "global-management", rtoken.Policies[0].Name) + require.Len(t, rtoken.ServiceIdentities, 1) + require.Equal(t, "db", rtoken.ServiceIdentities[0].ServiceName) }) } diff --git a/agent/structs/acl.go b/agent/structs/acl.go index 84e179366104..fee2cd85ea3f 100644 --- a/agent/structs/acl.go +++ b/agent/structs/acl.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "errors" "fmt" + "hash" "hash/fnv" "sort" "strings" @@ -84,6 +85,22 @@ session_prefix "" { // This is the policy ID for anonymous access. This is configurable by the // user. ACLTokenAnonymousID = "00000000-0000-0000-0000-000000000002" + + // aclPolicyTemplateServiceIdentity is the template used for synthesizing + // policies for service identities. + aclPolicyTemplateServiceIdentity = ` +service "%s" { + policy = "write" +} +service "%s-sidecar-proxy" { + policy = "write" +} +service_prefix "" { + policy = "read" +} +node_prefix "" { + policy = "read" +}` ) func ACLIDReserved(id string) bool { @@ -113,6 +130,7 @@ type ACLIdentity interface { SecretToken() string PolicyIDs() []string EmbeddedPolicy() *ACLPolicy + ServiceIdentityList() []*ACLServiceIdentity IsExpired(asOf time.Time) bool } @@ -121,6 +139,49 @@ type ACLTokenPolicyLink struct { Name string `hash:"ignore"` } +// ACLServiceIdentity represents a high-level grant of all necessary privileges +// to assume the identity of the named Service in the Catalog and within +// Connect. +type ACLServiceIdentity struct { + ServiceName string + + // Datacenters that the synthetic policy will be valid within. + // - No wildcards allowed + // - If empty then the synthetic policy is valid within all datacenters + // + // Only valid for global tokens. It is an error to specify this for local tokens. + Datacenters []string `json:",omitempty"` +} + +func (s *ACLServiceIdentity) Clone() *ACLServiceIdentity { + s2 := *s + s2.Datacenters = cloneStringSlice(s.Datacenters) + return &s2 +} + +func (s *ACLServiceIdentity) AddToHash(h hash.Hash) { + h.Write([]byte(s.ServiceName)) + for _, dc := range s.Datacenters { + h.Write([]byte(dc)) + } +} + +func (s *ACLServiceIdentity) SyntheticPolicy() *ACLPolicy { + // Given that we validate this string name before persisting, we do not + // have to escape it before doing the following interpolation. + rules := fmt.Sprintf(aclPolicyTemplateServiceIdentity, s.ServiceName, s.ServiceName) + + hasher := fnv.New128a() + policy := &ACLPolicy{} + policy.ID = fmt.Sprintf("%x", hasher.Sum([]byte(rules))) + policy.Name = fmt.Sprintf("synthetic-policy-%s", policy.ID) + policy.Rules = rules + policy.Syntax = acl.SyntaxCurrent + policy.Datacenters = s.Datacenters + policy.SetHash(true) + return policy +} + type ACLToken struct { // This is the UUID used for tracking and management purposes AccessorID string @@ -131,10 +192,13 @@ type ACLToken struct { // Human readable string to display for the token (Optional) Description string - // List of policy links - nil/empty for legacy tokens + // List of policy links - nil/empty for legacy tokens or if service identities are in use. // Note this is the list of IDs and not the names. Prior to token creation // the list of policy names gets validated and the policy IDs get stored herein - Policies []ACLTokenPolicyLink + Policies []ACLTokenPolicyLink `json:",omitempty"` + + // List of services to generate synthetic policies for. + ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` // Type is the V1 Token Type // DEPRECATED (ACL-Legacy-Compat) - remove once we no longer support v1 ACL compat @@ -181,11 +245,18 @@ type ACLToken struct { func (t *ACLToken) Clone() *ACLToken { t2 := *t t2.Policies = nil + t2.ServiceIdentities = nil if len(t.Policies) > 0 { t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies)) copy(t2.Policies, t.Policies) } + if len(t.ServiceIdentities) > 0 { + t2.ServiceIdentities = make([]*ACLServiceIdentity, len(t.ServiceIdentities)) + for i, s := range t.ServiceIdentities { + t2.ServiceIdentities[i] = s.Clone() + } + } return &t2 } @@ -198,13 +269,29 @@ func (t *ACLToken) SecretToken() string { } func (t *ACLToken) PolicyIDs() []string { - var ids []string + if len(t.Policies) == 0 { + return nil + } + + ids := make([]string, 0, len(t.Policies)) for _, link := range t.Policies { ids = append(ids, link.ID) } return ids } +func (t *ACLToken) ServiceIdentityList() []*ACLServiceIdentity { + if len(t.ServiceIdentities) == 0 { + return nil + } + + out := make([]*ACLServiceIdentity, 0, len(t.ServiceIdentities)) + for _, s := range t.ServiceIdentities { + out = append(out, s.Clone()) + } + return out +} + func (t *ACLToken) IsExpired(asOf time.Time) bool { if asOf.IsZero() || t.ExpirationTime.IsZero() { return false @@ -214,6 +301,7 @@ func (t *ACLToken) IsExpired(asOf time.Time) bool { func (t *ACLToken) UsesNonLegacyFields() bool { return len(t.Policies) > 0 || + len(t.ServiceIdentities) > 0 || t.Type == "" || !t.ExpirationTime.IsZero() || t.ExpirationTTL != 0 @@ -280,6 +368,10 @@ func (t *ACLToken) SetHash(force bool) []byte { hash.Write([]byte(link.ID)) } + for _, srvid := range t.ServiceIdentities { + srvid.AddToHash(hash) + } + // Finalize the hash hashVal := hash.Sum(nil) @@ -295,6 +387,12 @@ func (t *ACLToken) EstimateSize() int { for _, link := range t.Policies { size += len(link.ID) + len(link.Name) } + for _, srvid := range t.ServiceIdentities { + size += len(srvid.ServiceName) + for _, dc := range srvid.Datacenters { + size += len(dc) + } + } return size } @@ -302,32 +400,34 @@ func (t *ACLToken) EstimateSize() int { type ACLTokens []*ACLToken type ACLTokenListStub struct { - AccessorID string - Description string - Policies []ACLTokenPolicyLink - Local bool - ExpirationTime time.Time `json:",omitempty"` - CreateTime time.Time `json:",omitempty"` - Hash []byte - CreateIndex uint64 - ModifyIndex uint64 - Legacy bool `json:",omitempty"` + AccessorID string + Description string + Policies []ACLTokenPolicyLink `json:",omitempty"` + ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` + Local bool + ExpirationTime time.Time `json:",omitempty"` + CreateTime time.Time `json:",omitempty"` + Hash []byte + CreateIndex uint64 + ModifyIndex uint64 + Legacy bool `json:",omitempty"` } type ACLTokenListStubs []*ACLTokenListStub func (token *ACLToken) Stub() *ACLTokenListStub { return &ACLTokenListStub{ - AccessorID: token.AccessorID, - Description: token.Description, - Policies: token.Policies, - Local: token.Local, - ExpirationTime: token.ExpirationTime, - CreateTime: token.CreateTime, - Hash: token.Hash, - CreateIndex: token.CreateIndex, - ModifyIndex: token.ModifyIndex, - Legacy: token.Rules != "", + AccessorID: token.AccessorID, + Description: token.Description, + Policies: token.Policies, + ServiceIdentities: token.ServiceIdentities, + Local: token.Local, + ExpirationTime: token.ExpirationTime, + CreateTime: token.CreateTime, + Hash: token.Hash, + CreateIndex: token.CreateIndex, + ModifyIndex: token.ModifyIndex, + Legacy: token.Rules != "", } } @@ -381,11 +481,7 @@ type ACLPolicy struct { func (p *ACLPolicy) Clone() *ACLPolicy { p2 := *p - p2.Datacenters = nil - if len(p.Datacenters) > 0 { - p2.Datacenters = make([]string, len(p.Datacenters)) - copy(p2.Datacenters, p.Datacenters) - } + p2.Datacenters = cloneStringSlice(p.Datacenters) return &p2 } @@ -765,3 +861,12 @@ type ACLPolicyBatchSetRequest struct { type ACLPolicyBatchDeleteRequest struct { PolicyIDs []string } + +func cloneStringSlice(s []string) []string { + if len(s) == 0 { + return nil + } + out := make([]string, len(s)) + copy(out, s) + return out +} diff --git a/agent/structs/acl_legacy.go b/agent/structs/acl_legacy.go index ebd8ece82b5b..fa182d888423 100644 --- a/agent/structs/acl_legacy.go +++ b/agent/structs/acl_legacy.go @@ -73,14 +73,15 @@ func (a *ACL) Convert() *ACLToken { } return &ACLToken{ - AccessorID: "", - SecretID: a.ID, - Description: a.Name, - Policies: nil, - Type: a.Type, - Rules: a.Rules, - Local: false, - RaftIndex: a.RaftIndex, + AccessorID: "", + SecretID: a.ID, + Description: a.Name, + Policies: nil, + ServiceIdentities: nil, + Type: a.Type, + Rules: a.Rules, + Local: false, + RaftIndex: a.RaftIndex, } } diff --git a/agent/structs/acl_test.go b/agent/structs/acl_test.go index 2e7e9edcdba4..bfc585a55d92 100644 --- a/agent/structs/acl_test.go +++ b/agent/structs/acl_test.go @@ -140,6 +140,69 @@ func TestStructs_ACLToken_EmbeddedPolicy(t *testing.T) { }) } +func TestStructs_ACLServiceIdentity_SyntheticPolicy(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + serviceName string + datacenters []string + expectRules string + }{ + {"web", nil, ` +service "web" { + policy = "write" +} +service "web-sidecar-proxy" { + policy = "write" +} +service_prefix "" { + policy = "read" +} +node_prefix "" { + policy = "read" +}`}, + {"companion-cube-99", []string{"dc1", "dc2"}, ` +service "companion-cube-99" { + policy = "write" +} +service "companion-cube-99-sidecar-proxy" { + policy = "write" +} +service_prefix "" { + policy = "read" +} +node_prefix "" { + policy = "read" +}`}, + } { + name := test.serviceName + if len(test.datacenters) > 0 { + name += " [" + strings.Join(test.datacenters, ", ") + "]" + } + t.Run(name, func(t *testing.T) { + svcid := &ACLServiceIdentity{ + ServiceName: test.serviceName, + Datacenters: test.datacenters, + } + + expect := &ACLPolicy{ + Syntax: acl.SyntaxCurrent, + Datacenters: test.datacenters, + Rules: test.expectRules, + } + + got := svcid.SyntheticPolicy() + require.NotEmpty(t, got.ID) + require.Equal(t, got.Name, "synthetic-policy-"+got.ID) + // strip irrelevant fields before equality + got.ID = "" + got.Name = "" + got.Hash = nil + require.Equal(t, expect, got) + }) + } +} + func TestStructs_ACLToken_SetHash(t *testing.T) { t.Parallel() diff --git a/api/acl.go b/api/acl.go index 667d21548227..3cf02276212d 100644 --- a/api/acl.go +++ b/api/acl.go @@ -22,17 +22,18 @@ type ACLTokenPolicyLink struct { // ACLToken represents an ACL Token type ACLToken struct { - CreateIndex uint64 - ModifyIndex uint64 - AccessorID string - SecretID string - Description string - Policies []*ACLTokenPolicyLink - Local bool - ExpirationTTL time.Duration `json:",omitempty"` - ExpirationTime time.Time `json:",omitempty"` - CreateTime time.Time `json:",omitempty"` - Hash []byte `json:",omitempty"` + CreateIndex uint64 + ModifyIndex uint64 + AccessorID string + SecretID string + Description string + Policies []*ACLTokenPolicyLink `json:",omitempty"` + ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` + Local bool + ExpirationTTL time.Duration `json:",omitempty"` + ExpirationTime time.Time `json:",omitempty"` + 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 @@ -40,16 +41,17 @@ type ACLToken struct { } type ACLTokenListEntry struct { - CreateIndex uint64 - ModifyIndex uint64 - AccessorID string - Description string - Policies []*ACLTokenPolicyLink - Local bool - ExpirationTime time.Time `json:",omitempty"` - CreateTime time.Time - Hash []byte - Legacy bool + CreateIndex uint64 + ModifyIndex uint64 + AccessorID string + Description string + Policies []*ACLTokenPolicyLink `json:",omitempty"` + ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` + Local bool + ExpirationTime time.Time `json:",omitempty"` + CreateTime time.Time + Hash []byte + Legacy bool } // ACLEntry is used to represent a legacy ACL token @@ -75,6 +77,14 @@ type ACLReplicationStatus struct { LastError time.Time } +// ACLServiceIdentity represents a high-level grant of all necessary privileges +// to assume the identity of the named Service in the Catalog and within +// Connect. +type ACLServiceIdentity struct { + ServiceName string + Datacenters []string `json:",omitempty"` +} + // ACLPolicy represents an ACL Policy. type ACLPolicy struct { ID string diff --git a/command/acl/acl_helpers.go b/command/acl/acl_helpers.go index 1b9f9cee74b8..d1e364acd680 100644 --- a/command/acl/acl_helpers.go +++ b/command/acl/acl_helpers.go @@ -27,6 +27,14 @@ func PrintToken(token *api.ACLToken, ui cli.Ui, showMeta bool) { for _, policy := range token.Policies { ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) } + ui.Info(fmt.Sprintf("Service Identities:")) + for _, svcid := range token.ServiceIdentities { + if len(svcid.Datacenters) > 0 { + ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) + } else { + ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName)) + } + } if token.Rules != "" { ui.Info(fmt.Sprintf("Rules:")) ui.Info(token.Rules) @@ -51,6 +59,14 @@ func PrintTokenListEntry(token *api.ACLTokenListEntry, ui cli.Ui, showMeta bool) for _, policy := range token.Policies { ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) } + ui.Info(fmt.Sprintf("Service Identities:")) + for _, svcid := range token.ServiceIdentities { + if len(svcid.Datacenters) > 0 { + ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", "))) + } else { + ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName)) + } + } } func PrintPolicy(policy *api.ACLPolicy, ui cli.Ui, showMeta bool) { @@ -191,3 +207,24 @@ func GetRulesFromLegacyToken(client *api.Client, tokenID string, isSecret bool) return token.Rules, nil } + +func ExtractServiceIdentities(serviceIdents []string) ([]*api.ACLServiceIdentity, error) { + var out []*api.ACLServiceIdentity + for _, svcidRaw := range serviceIdents { + parts := strings.Split(svcidRaw, ":") + switch len(parts) { + case 2: + out = append(out, &api.ACLServiceIdentity{ + ServiceName: parts[0], + Datacenters: strings.Split(parts[1], ","), + }) + case 1: + out = append(out, &api.ACLServiceIdentity{ + ServiceName: parts[0], + }) + default: + return nil, fmt.Errorf("Malformed -service-identity argument: %q", svcidRaw) + } + } + return out, nil +} diff --git a/command/acl/token/create/token_create.go b/command/acl/token/create/token_create.go index 624ec8e6477d..0760c18f073c 100644 --- a/command/acl/token/create/token_create.go +++ b/command/acl/token/create/token_create.go @@ -25,6 +25,7 @@ type cmd struct { policyIDs []string policyNames []string + serviceIdents []string expirationTTL time.Duration description string local bool @@ -41,6 +42,9 @@ func (c *cmd) init() { "policy to use for this token. May be specified multiple times") c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+ "policy to use for this token. May be specified multiple times") + c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+ + "service identity to use for this token. May be specified multiple times. Format is "+ + "the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...") c.flags.DurationVar(&c.expirationTTL, "expires-ttl", 0, "Duration of time this "+ "token should be valid for") c.http = &flags.HTTPFlags{} @@ -54,8 +58,9 @@ func (c *cmd) Run(args []string) int { return 1 } - if len(c.policyNames) == 0 && len(c.policyIDs) == 0 { - c.UI.Error(fmt.Sprintf("Cannot create a token without specifying -policy-name or -policy-id at least once")) + if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && + len(c.serviceIdents) == 0 { + c.UI.Error(fmt.Sprintf("Cannot create a token without specifying -policy-name, -policy-id, or -service-identity at least once")) return 1 } @@ -73,6 +78,13 @@ func (c *cmd) Run(args []string) int { newToken.ExpirationTTL = c.expirationTTL } + parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + newToken.ServiceIdentities = parsedServiceIdents + for _, policyName := range c.policyNames { // We could resolve names to IDs here but there isn't any reason why its would be better // than allowing the agent to do it. @@ -119,4 +131,7 @@ Usage: consul acl token create [options] $ consul acl token create -description "Replication token" \ -policy-id b52fc3de-5 \ -policy-name "acl-replication" + -policy-name "acl-replication" \ + -service-identity "web" \ + -service-identity "db:east,west" ` diff --git a/command/acl/token/update/token_update.go b/command/acl/token/update/token_update.go index 08498087967d..c2ad08468810 100644 --- a/command/acl/token/update/token_update.go +++ b/command/acl/token/update/token_update.go @@ -22,13 +22,15 @@ type cmd struct { http *flags.HTTPFlags help string - tokenID string - policyIDs []string - policyNames []string - description string - mergePolicies bool - showMeta bool - upgradeLegacy bool + tokenID string + policyIDs []string + policyNames []string + serviceIdents []string + description string + mergePolicies bool + mergeServiceIdents bool + showMeta bool + upgradeLegacy bool } func (c *cmd) init() { @@ -37,6 +39,8 @@ func (c *cmd) init() { "as the content hash and raft indices should be shown for each entry") c.flags.BoolVar(&c.mergePolicies, "merge-policies", false, "Merge the new policies "+ "with the existing policies") + c.flags.BoolVar(&c.mergeServiceIdents, "merge-service-identities", false, "Merge the new service identities "+ + "with the existing service identities") c.flags.StringVar(&c.tokenID, "id", "", "The Accessor ID of the token to read. "+ "It may be specified as a unique ID prefix but will error if the prefix "+ "matches multiple token Accessor IDs") @@ -45,6 +49,9 @@ func (c *cmd) init() { "policy to use for this token. May be specified multiple times") c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+ "policy to use for this token. May be specified multiple times") + c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+ + "service identity to use for this token. May be specified multiple times. Format is "+ + "the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...") c.flags.BoolVar(&c.upgradeLegacy, "upgrade-legacy", false, "Add new polices "+ "to a legacy token replacing all existing rules. This will cause the legacy "+ "token to behave exactly like a new token but keep the same Secret.\n"+ @@ -107,6 +114,12 @@ func (c *cmd) Run(args []string) int { token.Description = c.description } + parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents) + if err != nil { + c.UI.Error(err.Error()) + return 1 + } + if c.mergePolicies { for _, policyName := range c.policyNames { found := false @@ -162,6 +175,26 @@ func (c *cmd) Run(args []string) int { } } + if c.mergeServiceIdents { + for _, svcid := range parsedServiceIdents { + found := -1 + for i, link := range token.ServiceIdentities { + if link.ServiceName == svcid.ServiceName { + found = i + break + } + } + + if found != -1 { + token.ServiceIdentities[found] = svcid + } else { + token.ServiceIdentities = append(token.ServiceIdentities, svcid) + } + } + } else { + token.ServiceIdentities = parsedServiceIdents + } + token, _, err = client.ACL().TokenUpdate(token, nil) if err != nil { c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tokenID, err)) diff --git a/vendor/github.com/hashicorp/consul/api/acl.go b/vendor/github.com/hashicorp/consul/api/acl.go index 667d21548227..3cf02276212d 100644 --- a/vendor/github.com/hashicorp/consul/api/acl.go +++ b/vendor/github.com/hashicorp/consul/api/acl.go @@ -22,17 +22,18 @@ type ACLTokenPolicyLink struct { // ACLToken represents an ACL Token type ACLToken struct { - CreateIndex uint64 - ModifyIndex uint64 - AccessorID string - SecretID string - Description string - Policies []*ACLTokenPolicyLink - Local bool - ExpirationTTL time.Duration `json:",omitempty"` - ExpirationTime time.Time `json:",omitempty"` - CreateTime time.Time `json:",omitempty"` - Hash []byte `json:",omitempty"` + CreateIndex uint64 + ModifyIndex uint64 + AccessorID string + SecretID string + Description string + Policies []*ACLTokenPolicyLink `json:",omitempty"` + ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` + Local bool + ExpirationTTL time.Duration `json:",omitempty"` + ExpirationTime time.Time `json:",omitempty"` + 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 @@ -40,16 +41,17 @@ type ACLToken struct { } type ACLTokenListEntry struct { - CreateIndex uint64 - ModifyIndex uint64 - AccessorID string - Description string - Policies []*ACLTokenPolicyLink - Local bool - ExpirationTime time.Time `json:",omitempty"` - CreateTime time.Time - Hash []byte - Legacy bool + CreateIndex uint64 + ModifyIndex uint64 + AccessorID string + Description string + Policies []*ACLTokenPolicyLink `json:",omitempty"` + ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` + Local bool + ExpirationTime time.Time `json:",omitempty"` + CreateTime time.Time + Hash []byte + Legacy bool } // ACLEntry is used to represent a legacy ACL token @@ -75,6 +77,14 @@ type ACLReplicationStatus struct { LastError time.Time } +// ACLServiceIdentity represents a high-level grant of all necessary privileges +// to assume the identity of the named Service in the Catalog and within +// Connect. +type ACLServiceIdentity struct { + ServiceName string + Datacenters []string `json:",omitempty"` +} + // ACLPolicy represents an ACL Policy. type ACLPolicy struct { ID string