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.
This commit is contained in:
R.B. Boyer 2019-04-08 13:19:09 -05:00 committed by R.B. Boyer
parent 2144bd7fbd
commit db43fc3a20
14 changed files with 730 additions and 82 deletions

View File

@ -4,10 +4,11 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"sort"
"sync" "sync"
"time" "time"
"github.com/armon/go-metrics" metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
@ -133,7 +134,7 @@ type ACLResolverConfig struct {
// - Resolving policies remotely via an ACL.PolicyResolve RPC // - Resolving policies remotely via an ACL.PolicyResolve RPC
// //
// Remote Resolution: // 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. // 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 // 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) { func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (structs.ACLPolicies, error) {
policyIDs := identity.PolicyIDs() policyIDs := identity.PolicyIDs()
if len(policyIDs) == 0 { serviceIdentities := identity.ServiceIdentityList()
if len(policyIDs) == 0 && len(serviceIdentities) == 0 {
policy := identity.EmbeddedPolicy() policy := identity.EmbeddedPolicy()
if policy != nil { if policy != nil {
return []*structs.ACLPolicy{policy}, nil return []*structs.ACLPolicy{policy}, nil
@ -513,9 +516,96 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
return nil, nil return nil, nil
} }
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities)
// For the new ACLs policy replication is mandatory for correct operation on servers. Therefore // For the new ACLs policy replication is mandatory for correct operation on servers. Therefore
// we only attempt to resolve policies locally // 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 // Get all associated policies
var missing []string var missing []string
@ -559,7 +649,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
// Hot-path if we have no missing or expired policies // Hot-path if we have no missing or expired policies
if len(missing)+len(expired) == 0 { if len(missing)+len(expired) == 0 {
return r.filterPoliciesByScope(policies), nil return policies, nil
} }
hasMissing := len(missing) > 0 hasMissing := len(missing) > 0
@ -579,7 +669,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
if !waitForResult { if !waitForResult {
// waitForResult being false requires that all the policies were cached already // waitForResult being false requires that all the policies were cached already
policies = append(policies, expired...) policies = append(policies, expired...)
return r.filterPoliciesByScope(policies), nil return policies, nil
} }
res := <-waitChan 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) { func (r *ACLResolver) resolveTokenToPolicies(token string) (structs.ACLPolicies, error) {

View File

@ -8,13 +8,13 @@ import (
"regexp" "regexp"
"time" "time"
"github.com/armon/go-metrics" metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-memdb" memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-uuid" uuid "github.com/hashicorp/go-uuid"
) )
const ( const (
@ -24,7 +24,11 @@ const (
) )
// Regex for matching // 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 // ACL endpoint is used to manipulate ACLs
type ACL struct { type ACL struct {
@ -275,10 +279,11 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
cloneReq := structs.ACLTokenSetRequest{ cloneReq := structs.ACLTokenSetRequest{
Datacenter: args.Datacenter, Datacenter: args.Datacenter,
ACLToken: structs.ACLToken{ ACLToken: structs.ACLToken{
Policies: token.Policies, Policies: token.Policies,
Local: token.Local, ServiceIdentities: token.ServiceIdentities,
Description: token.Description, Local: token.Local,
ExpirationTime: token.ExpirationTime, Description: token.Description,
ExpirationTime: token.ExpirationTime,
}, },
WriteRequest: args.WriteRequest, WriteRequest: args.WriteRequest,
} }
@ -450,6 +455,18 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
} }
token.Policies = policies 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 != "" { if token.Rules != "" {
return fmt.Errorf("Rules cannot be specified for this token") 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 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 { func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) error {
if err := a.aclPreCheck(); err != nil { if err := a.aclPreCheck(); err != nil {
return err return err

View File

@ -919,6 +919,124 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
require.Len(t, token.Policies, 0) 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 { for _, test := range []struct {
name string name string
offset time.Duration offset time.Duration

View File

@ -2861,3 +2861,92 @@ service "service" {
t.Fatalf("err: %v", err) 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)
})
}
}

View File

@ -658,7 +658,9 @@ func (s *Server) startACLUpgrade() {
} }
// Assign the global-management policy to legacy management tokens // 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}) newToken.Policies = append(newToken.Policies, structs.ACLTokenPolicyLink{ID: structs.ACLPolicyGlobalManagementID})
} }

View File

@ -478,6 +478,12 @@ func (s *Store) aclTokenSetTxn(tx *memdb.Txn, idx uint64, token *structs.ACLToke
return err 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 // Set the indexes
if original != nil { if original != nil {
if original.AccessorID != "" && token.AccessorID != original.AccessorID { if original.AccessorID != "" && token.AccessorID != original.AccessorID {

View File

@ -277,6 +277,38 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) {
require.Equal(t, ErrMissingACLTokenAccessor, err) 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.Run("Missing Policy ID", func(t *testing.T) {
t.Parallel() t.Parallel()
s := testACLTokensStateStore(t) s := testACLTokensStateStore(t)
@ -322,6 +354,11 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) {
ID: "a0625e95-9b3e-42de-a8d6-ceef5b6f3286", ID: "a0625e95-9b3e-42de-a8d6-ceef5b6f3286",
}, },
}, },
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "web",
},
},
} }
require.NoError(t, s.ACLTokenSet(2, token.Clone(), false)) 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.Equal(t, uint64(2), rtoken.ModifyIndex)
require.Len(t, rtoken.Policies, 1) require.Len(t, rtoken.Policies, 1)
require.Equal(t, "node-read", rtoken.Policies[0].Name) 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) { t.Run("Update", func(t *testing.T) {
@ -347,6 +386,11 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) {
ID: "a0625e95-9b3e-42de-a8d6-ceef5b6f3286", ID: "a0625e95-9b3e-42de-a8d6-ceef5b6f3286",
}, },
}, },
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "web",
},
},
} }
require.NoError(t, s.ACLTokenSet(2, token.Clone(), false)) require.NoError(t, s.ACLTokenSet(2, token.Clone(), false))
@ -359,6 +403,11 @@ func TestStateStore_ACLToken_SetGet(t *testing.T) {
ID: structs.ACLPolicyGlobalManagementID, ID: structs.ACLPolicyGlobalManagementID,
}, },
}, },
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "db",
},
},
} }
require.NoError(t, s.ACLTokenSet(3, updated.Clone(), false)) 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.Len(t, rtoken.Policies, 1)
require.Equal(t, structs.ACLPolicyGlobalManagementID, rtoken.Policies[0].ID) require.Equal(t, structs.ACLPolicyGlobalManagementID, rtoken.Policies[0].ID)
require.Equal(t, "global-management", rtoken.Policies[0].Name) require.Equal(t, "global-management", rtoken.Policies[0].Name)
require.Len(t, rtoken.ServiceIdentities, 1)
require.Equal(t, "db", rtoken.ServiceIdentities[0].ServiceName)
}) })
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
"hash"
"hash/fnv" "hash/fnv"
"sort" "sort"
"strings" "strings"
@ -84,6 +85,22 @@ session_prefix "" {
// This is the policy ID for anonymous access. This is configurable by the // This is the policy ID for anonymous access. This is configurable by the
// user. // user.
ACLTokenAnonymousID = "00000000-0000-0000-0000-000000000002" 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 { func ACLIDReserved(id string) bool {
@ -113,6 +130,7 @@ type ACLIdentity interface {
SecretToken() string SecretToken() string
PolicyIDs() []string PolicyIDs() []string
EmbeddedPolicy() *ACLPolicy EmbeddedPolicy() *ACLPolicy
ServiceIdentityList() []*ACLServiceIdentity
IsExpired(asOf time.Time) bool IsExpired(asOf time.Time) bool
} }
@ -121,6 +139,49 @@ type ACLTokenPolicyLink struct {
Name string `hash:"ignore"` 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 { type ACLToken struct {
// This is the UUID used for tracking and management purposes // This is the UUID used for tracking and management purposes
AccessorID string AccessorID string
@ -131,10 +192,13 @@ type ACLToken struct {
// Human readable string to display for the token (Optional) // Human readable string to display for the token (Optional)
Description string 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 // 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 // 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 // Type is the V1 Token Type
// DEPRECATED (ACL-Legacy-Compat) - remove once we no longer support v1 ACL compat // 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 { func (t *ACLToken) Clone() *ACLToken {
t2 := *t t2 := *t
t2.Policies = nil t2.Policies = nil
t2.ServiceIdentities = nil
if len(t.Policies) > 0 { if len(t.Policies) > 0 {
t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies)) t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies))
copy(t2.Policies, 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 return &t2
} }
@ -198,13 +269,29 @@ func (t *ACLToken) SecretToken() string {
} }
func (t *ACLToken) PolicyIDs() []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 { for _, link := range t.Policies {
ids = append(ids, link.ID) ids = append(ids, link.ID)
} }
return ids 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 { func (t *ACLToken) IsExpired(asOf time.Time) bool {
if asOf.IsZero() || t.ExpirationTime.IsZero() { if asOf.IsZero() || t.ExpirationTime.IsZero() {
return false return false
@ -214,6 +301,7 @@ func (t *ACLToken) IsExpired(asOf time.Time) bool {
func (t *ACLToken) UsesNonLegacyFields() bool { func (t *ACLToken) UsesNonLegacyFields() bool {
return len(t.Policies) > 0 || return len(t.Policies) > 0 ||
len(t.ServiceIdentities) > 0 ||
t.Type == "" || t.Type == "" ||
!t.ExpirationTime.IsZero() || !t.ExpirationTime.IsZero() ||
t.ExpirationTTL != 0 t.ExpirationTTL != 0
@ -280,6 +368,10 @@ func (t *ACLToken) SetHash(force bool) []byte {
hash.Write([]byte(link.ID)) hash.Write([]byte(link.ID))
} }
for _, srvid := range t.ServiceIdentities {
srvid.AddToHash(hash)
}
// Finalize the hash // Finalize the hash
hashVal := hash.Sum(nil) hashVal := hash.Sum(nil)
@ -295,6 +387,12 @@ func (t *ACLToken) EstimateSize() int {
for _, link := range t.Policies { for _, link := range t.Policies {
size += len(link.ID) + len(link.Name) 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 return size
} }
@ -302,32 +400,34 @@ func (t *ACLToken) EstimateSize() int {
type ACLTokens []*ACLToken type ACLTokens []*ACLToken
type ACLTokenListStub struct { type ACLTokenListStub struct {
AccessorID string AccessorID string
Description string Description string
Policies []ACLTokenPolicyLink Policies []ACLTokenPolicyLink `json:",omitempty"`
Local bool ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
ExpirationTime time.Time `json:",omitempty"` Local bool
CreateTime time.Time `json:",omitempty"` ExpirationTime time.Time `json:",omitempty"`
Hash []byte CreateTime time.Time `json:",omitempty"`
CreateIndex uint64 Hash []byte
ModifyIndex uint64 CreateIndex uint64
Legacy bool `json:",omitempty"` ModifyIndex uint64
Legacy bool `json:",omitempty"`
} }
type ACLTokenListStubs []*ACLTokenListStub type ACLTokenListStubs []*ACLTokenListStub
func (token *ACLToken) Stub() *ACLTokenListStub { func (token *ACLToken) Stub() *ACLTokenListStub {
return &ACLTokenListStub{ return &ACLTokenListStub{
AccessorID: token.AccessorID, AccessorID: token.AccessorID,
Description: token.Description, Description: token.Description,
Policies: token.Policies, Policies: token.Policies,
Local: token.Local, ServiceIdentities: token.ServiceIdentities,
ExpirationTime: token.ExpirationTime, Local: token.Local,
CreateTime: token.CreateTime, ExpirationTime: token.ExpirationTime,
Hash: token.Hash, CreateTime: token.CreateTime,
CreateIndex: token.CreateIndex, Hash: token.Hash,
ModifyIndex: token.ModifyIndex, CreateIndex: token.CreateIndex,
Legacy: token.Rules != "", ModifyIndex: token.ModifyIndex,
Legacy: token.Rules != "",
} }
} }
@ -381,11 +481,7 @@ type ACLPolicy struct {
func (p *ACLPolicy) Clone() *ACLPolicy { func (p *ACLPolicy) Clone() *ACLPolicy {
p2 := *p p2 := *p
p2.Datacenters = nil p2.Datacenters = cloneStringSlice(p.Datacenters)
if len(p.Datacenters) > 0 {
p2.Datacenters = make([]string, len(p.Datacenters))
copy(p2.Datacenters, p.Datacenters)
}
return &p2 return &p2
} }
@ -765,3 +861,12 @@ type ACLPolicyBatchSetRequest struct {
type ACLPolicyBatchDeleteRequest struct { type ACLPolicyBatchDeleteRequest struct {
PolicyIDs []string PolicyIDs []string
} }
func cloneStringSlice(s []string) []string {
if len(s) == 0 {
return nil
}
out := make([]string, len(s))
copy(out, s)
return out
}

View File

@ -73,14 +73,15 @@ func (a *ACL) Convert() *ACLToken {
} }
return &ACLToken{ return &ACLToken{
AccessorID: "", AccessorID: "",
SecretID: a.ID, SecretID: a.ID,
Description: a.Name, Description: a.Name,
Policies: nil, Policies: nil,
Type: a.Type, ServiceIdentities: nil,
Rules: a.Rules, Type: a.Type,
Local: false, Rules: a.Rules,
RaftIndex: a.RaftIndex, Local: false,
RaftIndex: a.RaftIndex,
} }
} }

View File

@ -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) { func TestStructs_ACLToken_SetHash(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -22,17 +22,18 @@ type ACLTokenPolicyLink struct {
// ACLToken represents an ACL Token // ACLToken represents an ACL Token
type ACLToken struct { type ACLToken struct {
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
AccessorID string AccessorID string
SecretID string SecretID string
Description string Description string
Policies []*ACLTokenPolicyLink Policies []*ACLTokenPolicyLink `json:",omitempty"`
Local bool ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
ExpirationTTL time.Duration `json:",omitempty"` Local bool
ExpirationTime time.Time `json:",omitempty"` ExpirationTTL time.Duration `json:",omitempty"`
CreateTime time.Time `json:",omitempty"` ExpirationTime time.Time `json:",omitempty"`
Hash []byte `json:",omitempty"` CreateTime time.Time `json:",omitempty"`
Hash []byte `json:",omitempty"`
// DEPRECATED (ACL-Legacy-Compat) // DEPRECATED (ACL-Legacy-Compat)
// Rules will only be present for legacy tokens returned via the new APIs // Rules will only be present for legacy tokens returned via the new APIs
@ -40,16 +41,17 @@ type ACLToken struct {
} }
type ACLTokenListEntry struct { type ACLTokenListEntry struct {
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
AccessorID string AccessorID string
Description string Description string
Policies []*ACLTokenPolicyLink Policies []*ACLTokenPolicyLink `json:",omitempty"`
Local bool ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
ExpirationTime time.Time `json:",omitempty"` Local bool
CreateTime time.Time ExpirationTime time.Time `json:",omitempty"`
Hash []byte CreateTime time.Time
Legacy bool Hash []byte
Legacy bool
} }
// ACLEntry is used to represent a legacy ACL token // ACLEntry is used to represent a legacy ACL token
@ -75,6 +77,14 @@ type ACLReplicationStatus struct {
LastError time.Time 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. // ACLPolicy represents an ACL Policy.
type ACLPolicy struct { type ACLPolicy struct {
ID string ID string

View File

@ -27,6 +27,14 @@ func PrintToken(token *api.ACLToken, ui cli.Ui, showMeta bool) {
for _, policy := range token.Policies { for _, policy := range token.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) 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 != "" { if token.Rules != "" {
ui.Info(fmt.Sprintf("Rules:")) ui.Info(fmt.Sprintf("Rules:"))
ui.Info(token.Rules) ui.Info(token.Rules)
@ -51,6 +59,14 @@ func PrintTokenListEntry(token *api.ACLTokenListEntry, ui cli.Ui, showMeta bool)
for _, policy := range token.Policies { for _, policy := range token.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name)) 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) { 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 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
}

View File

@ -25,6 +25,7 @@ type cmd struct {
policyIDs []string policyIDs []string
policyNames []string policyNames []string
serviceIdents []string
expirationTTL time.Duration expirationTTL time.Duration
description string description string
local bool local bool
@ -41,6 +42,9 @@ func (c *cmd) init() {
"policy to use for this token. May be specified multiple times") "policy to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+ c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+
"policy to use for this token. May be specified multiple times") "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 "+ c.flags.DurationVar(&c.expirationTTL, "expires-ttl", 0, "Duration of time this "+
"token should be valid for") "token should be valid for")
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
@ -54,8 +58,9 @@ func (c *cmd) Run(args []string) int {
return 1 return 1
} }
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 { 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")) 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 return 1
} }
@ -73,6 +78,13 @@ func (c *cmd) Run(args []string) int {
newToken.ExpirationTTL = c.expirationTTL 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 { for _, policyName := range c.policyNames {
// We could resolve names to IDs here but there isn't any reason why its would be better // 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. // than allowing the agent to do it.
@ -119,4 +131,7 @@ Usage: consul acl token create [options]
$ consul acl token create -description "Replication token" \ $ consul acl token create -description "Replication token" \
-policy-id b52fc3de-5 \ -policy-id b52fc3de-5 \
-policy-name "acl-replication" -policy-name "acl-replication"
-policy-name "acl-replication" \
-service-identity "web" \
-service-identity "db:east,west"
` `

View File

@ -22,13 +22,15 @@ type cmd struct {
http *flags.HTTPFlags http *flags.HTTPFlags
help string help string
tokenID string tokenID string
policyIDs []string policyIDs []string
policyNames []string policyNames []string
description string serviceIdents []string
mergePolicies bool description string
showMeta bool mergePolicies bool
upgradeLegacy bool mergeServiceIdents bool
showMeta bool
upgradeLegacy bool
} }
func (c *cmd) init() { 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") "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 "+ c.flags.BoolVar(&c.mergePolicies, "merge-policies", false, "Merge the new policies "+
"with the existing 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. "+ 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 "+ "It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple token Accessor IDs") "matches multiple token Accessor IDs")
@ -45,6 +49,9 @@ func (c *cmd) init() {
"policy to use for this token. May be specified multiple times") "policy to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+ c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+
"policy to use for this token. May be specified multiple times") "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 "+ c.flags.BoolVar(&c.upgradeLegacy, "upgrade-legacy", false, "Add new polices "+
"to a legacy token replacing all existing rules. This will cause the legacy "+ "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"+ "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 token.Description = c.description
} }
parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
if c.mergePolicies { if c.mergePolicies {
for _, policyName := range c.policyNames { for _, policyName := range c.policyNames {
found := false 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) token, _, err = client.ACL().TokenUpdate(token, nil)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tokenID, err)) c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tokenID, err))