mirror of https://github.com/status-im/consul.git
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:
parent
2144bd7fbd
commit
db43fc3a20
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
52
api/acl.go
52
api/acl.go
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
`
|
`
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in New Issue