ACL Node Identities (#7970)

A Node Identity is very similar to a service identity. Its main targeted use is to allow creating tokens for use by Consul agents that will grant the necessary permissions for all the typical agent operations (node registration, coordinate updates, anti-entropy).

Half of this commit is for golden file based tests of the acl token and role cli output. Another big updates was to refactor many of the tests in agent/consul/acl_endpoint_test.go to use the same style of tests and the same helpers. Besides being less boiler plate in the tests it also uses a common way of starting a test server with ACLs that should operate without any warnings regarding deprecated non-uuid master tokens etc.
This commit is contained in:
Matt Keeler 2020-06-16 12:54:27 -04:00 committed by GitHub
parent ef37628e97
commit d3881dd754
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 2595 additions and 1423 deletions

View File

@ -408,6 +408,12 @@ func TestACL_HTTP(t *testing.T) {
Name: policyMap[idMap["policy-read-all-nodes"]].Name,
},
},
NodeIdentities: []*structs.ACLNodeIdentity{
&structs.ACLNodeIdentity{
NodeName: "web-node",
Datacenter: "foo",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/role?token=root", jsonBody(roleInput))
@ -423,6 +429,7 @@ func TestACL_HTTP(t *testing.T) {
require.Equal(t, roleInput.Name, role.Name)
require.Equal(t, roleInput.Description, role.Description)
require.Equal(t, roleInput.Policies, role.Policies)
require.Equal(t, roleInput.NodeIdentities, role.NodeIdentities)
require.True(t, role.CreateIndex > 0)
require.Equal(t, role.CreateIndex, role.ModifyIndex)
require.NotNil(t, role.Hash)
@ -502,6 +509,12 @@ func TestACL_HTTP(t *testing.T) {
ServiceName: "web-indexer",
},
},
NodeIdentities: []*structs.ACLNodeIdentity{
&structs.ACLNodeIdentity{
NodeName: "web-node",
Datacenter: "foo",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/role/"+idMap["role-test"]+"?token=root", jsonBody(roleInput))
@ -518,6 +531,7 @@ func TestACL_HTTP(t *testing.T) {
require.Equal(t, roleInput.Description, role.Description)
require.Equal(t, roleInput.Policies, role.Policies)
require.Equal(t, roleInput.ServiceIdentities, role.ServiceIdentities)
require.Equal(t, roleInput.NodeIdentities, role.NodeIdentities)
require.True(t, role.CreateIndex > 0)
require.True(t, role.CreateIndex < role.ModifyIndex)
require.NotNil(t, role.Hash)
@ -623,6 +637,12 @@ func TestACL_HTTP(t *testing.T) {
Name: policyMap[idMap["policy-read-all-nodes"]].Name,
},
},
NodeIdentities: []*structs.ACLNodeIdentity{
&structs.ACLNodeIdentity{
NodeName: "foo",
Datacenter: "bar",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/token?token=root", jsonBody(tokenInput))
@ -638,6 +658,7 @@ func TestACL_HTTP(t *testing.T) {
require.Len(t, token.SecretID, 36)
require.Equal(t, tokenInput.Description, token.Description)
require.Equal(t, tokenInput.Policies, token.Policies)
require.Equal(t, tokenInput.NodeIdentities, token.NodeIdentities)
require.True(t, token.CreateIndex > 0)
require.Equal(t, token.CreateIndex, token.ModifyIndex)
require.NotNil(t, token.Hash)
@ -741,6 +762,12 @@ func TestACL_HTTP(t *testing.T) {
Name: policyMap[idMap["policy-read-all-nodes"]].Name,
},
},
NodeIdentities: []*structs.ACLNodeIdentity{
&structs.ACLNodeIdentity{
NodeName: "foo",
Datacenter: "bar",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/token/"+originalToken.AccessorID+"?token=root", jsonBody(tokenInput))
@ -754,6 +781,7 @@ func TestACL_HTTP(t *testing.T) {
require.Equal(t, originalToken.SecretID, token.SecretID)
require.Equal(t, tokenInput.Description, token.Description)
require.Equal(t, tokenInput.Policies, token.Policies)
require.Equal(t, tokenInput.NodeIdentities, token.NodeIdentities)
require.True(t, token.CreateIndex > 0)
require.True(t, token.CreateIndex < token.ModifyIndex)
require.NotNil(t, token.Hash)

View File

@ -99,6 +99,10 @@ func (id *missingIdentity) ServiceIdentityList() []*structs.ACLServiceIdentity {
return nil
}
func (id *missingIdentity) NodeIdentityList() []*structs.ACLNodeIdentity {
return nil
}
func (id *missingIdentity) IsExpired(asOf time.Time) bool {
return false
}
@ -648,8 +652,9 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
policyIDs := identity.PolicyIDs()
roleIDs := identity.RoleIDs()
serviceIdentities := identity.ServiceIdentityList()
nodeIdentities := identity.NodeIdentityList()
if len(policyIDs) == 0 && len(serviceIdentities) == 0 && len(roleIDs) == 0 {
if len(policyIDs) == 0 && len(serviceIdentities) == 0 && len(roleIDs) == 0 && len(nodeIdentities) == 0 {
policy := identity.EmbeddedPolicy()
if policy != nil {
return []*structs.ACLPolicy{policy}, nil
@ -671,14 +676,17 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
policyIDs = append(policyIDs, link.ID)
}
serviceIdentities = append(serviceIdentities, role.ServiceIdentities...)
nodeIdentities = append(nodeIdentities, role.NodeIdentityList()...)
}
// Now deduplicate any policies or service identities that occur more than once.
policyIDs = dedupeStringSlice(policyIDs)
serviceIdentities = dedupeServiceIdentities(serviceIdentities)
nodeIdentities = dedupeNodeIdentities(nodeIdentities)
// Generate synthetic policies for all service identities in effect.
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, identity.EnterpriseMetadata())
syntheticPolicies = append(syntheticPolicies, r.synthesizePoliciesForNodeIdentities(nodeIdentities)...)
// For the new ACLs policy replication is mandatory for correct operation on servers. Therefore
// we only attempt to resolve policies locally
@ -705,6 +713,19 @@ func (r *ACLResolver) synthesizePoliciesForServiceIdentities(serviceIdentities [
return syntheticPolicies
}
func (r *ACLResolver) synthesizePoliciesForNodeIdentities(nodeIdentities []*structs.ACLNodeIdentity) []*structs.ACLPolicy {
if len(nodeIdentities) == 0 {
return nil
}
syntheticPolicies := make([]*structs.ACLPolicy, 0, len(nodeIdentities))
for _, n := range nodeIdentities {
syntheticPolicies = append(syntheticPolicies, n.SyntheticPolicy())
}
return syntheticPolicies
}
func dedupeServiceIdentities(in []*structs.ACLServiceIdentity) []*structs.ACLServiceIdentity {
// From: https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable
@ -739,6 +760,38 @@ func dedupeServiceIdentities(in []*structs.ACLServiceIdentity) []*structs.ACLSer
return in[:j+1]
}
func dedupeNodeIdentities(in []*structs.ACLNodeIdentity) []*structs.ACLNodeIdentity {
// 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 {
if in[i].NodeName < in[j].NodeName {
return true
}
return in[i].Datacenter < in[j].Datacenter
})
j := 0
for i := 1; i < len(in); i++ {
if in[j].NodeName == in[i].NodeName && in[j].Datacenter == in[i].Datacenter {
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...)

View File

@ -36,6 +36,12 @@ func (s *Server) loadAuthMethodValidator(idx uint64, method *structs.ACLAuthMeth
return v, nil
}
type aclBindings struct {
roles []structs.ACLTokenRoleLink
serviceIdentities []*structs.ACLServiceIdentity
nodeIdentities []*structs.ACLNodeIdentity
}
// evaluateRoleBindings evaluates all current binding rules associated with the
// given auth method against the verified data returned from the authentication
// process.
@ -46,13 +52,13 @@ func (s *Server) evaluateRoleBindings(
verifiedIdentity *authmethod.Identity,
methodMeta *structs.EnterpriseMeta,
targetMeta *structs.EnterpriseMeta,
) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) {
) (*aclBindings, error) {
// Only fetch rules that are relevant for this method.
_, rules, err := s.fsm.State().ACLBindingRuleList(nil, validator.Name(), methodMeta)
if err != nil {
return nil, nil, err
return nil, err
} else if len(rules) == 0 {
return nil, nil, nil
return nil, nil
}
// Find all binding rules that match the provided fields.
@ -63,36 +69,39 @@ func (s *Server) evaluateRoleBindings(
}
}
if len(matchingRules) == 0 {
return nil, nil, nil
return nil, nil
}
// For all matching rules compute the attributes of a token.
var (
roleLinks []structs.ACLTokenRoleLink
serviceIdentities []*structs.ACLServiceIdentity
)
var bindings aclBindings
for _, rule := range matchingRules {
bindName, valid, err := computeBindingRuleBindName(rule.BindType, rule.BindName, verifiedIdentity.ProjectedVars)
if err != nil {
return nil, nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
return nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
} else if !valid {
return nil, nil, fmt.Errorf("computed %q bind name for bind target is invalid: %q", rule.BindType, bindName)
return nil, fmt.Errorf("computed %q bind name for bind target is invalid: %q", rule.BindType, bindName)
}
switch rule.BindType {
case structs.BindingRuleBindTypeService:
serviceIdentities = append(serviceIdentities, &structs.ACLServiceIdentity{
bindings.serviceIdentities = append(bindings.serviceIdentities, &structs.ACLServiceIdentity{
ServiceName: bindName,
})
case structs.BindingRuleBindTypeNode:
bindings.nodeIdentities = append(bindings.nodeIdentities, &structs.ACLNodeIdentity{
NodeName: bindName,
Datacenter: s.config.Datacenter,
})
case structs.BindingRuleBindTypeRole:
_, role, err := s.fsm.State().ACLRoleGetByName(nil, bindName, targetMeta)
if err != nil {
return nil, nil, err
return nil, err
}
if role != nil {
roleLinks = append(roleLinks, structs.ACLTokenRoleLink{
bindings.roles = append(bindings.roles, structs.ACLTokenRoleLink{
ID: role.ID,
})
}
@ -102,7 +111,7 @@ func (s *Server) evaluateRoleBindings(
}
}
return serviceIdentities, roleLinks, nil
return &bindings, nil
}
// doesSelectorMatch checks that a single selector matches the provided vars.

View File

@ -34,6 +34,8 @@ var (
validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
validServiceIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
serviceIdentityNameMaxLength = 256
validNodeIdentityName = regexp.MustCompile(`^[a-z0-9]([a-z0-9\-_]*[a-z0-9])?$`)
nodeIdentityNameMaxLength = 256
validRoleName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,256}$`)
validAuthMethod = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
)
@ -319,6 +321,7 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
Policies: token.Policies,
Roles: token.Roles,
ServiceIdentities: token.ServiceIdentities,
NodeIdentities: token.NodeIdentities,
Local: token.Local,
Description: token.Description,
ExpirationTime: token.ExpirationTime,
@ -615,6 +618,19 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
}
token.ServiceIdentities = dedupeServiceIdentities(token.ServiceIdentities)
for _, nodeid := range token.NodeIdentities {
if nodeid.NodeName == "" {
return fmt.Errorf("Node identity is missing the node name field on this token")
}
if nodeid.Datacenter == "" {
return fmt.Errorf("Node identity is missing the datacenter field on this token")
}
if !isValidNodeIdentityName(nodeid.NodeName) {
return fmt.Errorf("Node identity has an invalid name. Only alphanumeric characters, '-' and '_' are allowed")
}
}
token.NodeIdentities = dedupeNodeIdentities(token.NodeIdentities)
if token.Rules != "" {
return fmt.Errorf("Rules cannot be specified for this token")
}
@ -700,7 +716,8 @@ func computeBindingRuleBindName(bindType, bindName string, projectedVars map[str
switch bindType {
case structs.BindingRuleBindTypeService:
valid = isValidServiceIdentityName(bindName)
case structs.BindingRuleBindTypeNode:
valid = isValidNodeIdentityName(bindName)
case structs.BindingRuleBindTypeRole:
valid = validRoleName.MatchString(bindName)
@ -722,6 +739,17 @@ func isValidServiceIdentityName(name string) bool {
return validServiceIdentityName.MatchString(name)
}
// isValidNodeIdentityName returns true if the provided name can be used as
// an ACLNodeIdentity NodeName. This is more restrictive than standard
// catalog registration, which basically takes the view that "everything is
// valid".
func isValidNodeIdentityName(name string) bool {
if len(name) < 1 || len(name) > nodeIdentityNameMaxLength {
return false
}
return validNodeIdentityName.MatchString(name)
}
func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) error {
if err := a.aclPreCheck(); err != nil {
return err
@ -1572,6 +1600,19 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
}
role.ServiceIdentities = dedupeServiceIdentities(role.ServiceIdentities)
for _, nodeid := range role.NodeIdentities {
if nodeid.NodeName == "" {
return fmt.Errorf("Node identity is missing the node name field on this role")
}
if nodeid.Datacenter == "" {
return fmt.Errorf("Node identity is missing the datacenter field on this role")
}
if !isValidNodeIdentityName(nodeid.NodeName) {
return fmt.Errorf("Node identity has an invalid name. Only alphanumeric characters, '-' and '_' are allowed")
}
}
role.NodeIdentities = dedupeNodeIdentities(role.NodeIdentities)
// calculate the hash for this role
role.SetHash(true)
@ -1892,6 +1933,7 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
switch rule.BindType {
case structs.BindingRuleBindTypeService:
case structs.BindingRuleBindTypeNode:
case structs.BindingRuleBindTypeRole:
default:
return fmt.Errorf("Invalid Binding Rule: unknown BindType %q", rule.BindType)
@ -2365,14 +2407,14 @@ func (a *ACL) tokenSetFromAuthMethod(
}
// 3. send map through role bindings
serviceIdentities, roleLinks, err := a.srv.evaluateRoleBindings(validator, verifiedIdentity, entMeta, targetMeta)
bindings, err := a.srv.evaluateRoleBindings(validator, verifiedIdentity, entMeta, targetMeta)
if err != nil {
return err
}
// We try to prevent the creation of a useless token without taking a trip
// through the state store if we can.
if len(serviceIdentities) == 0 && len(roleLinks) == 0 {
if bindings == nil || (len(bindings.serviceIdentities) == 0 && len(bindings.nodeIdentities) == 0 && len(bindings.roles) == 0) {
return acl.ErrPermissionDenied
}
@ -2392,8 +2434,9 @@ func (a *ACL) tokenSetFromAuthMethod(
Description: description,
Local: true,
AuthMethod: method.Name,
ServiceIdentities: serviceIdentities,
Roles: roleLinks,
ServiceIdentities: bindings.serviceIdentities,
NodeIdentities: bindings.nodeIdentities,
Roles: bindings.roles,
ExpirationTTL: method.MaxTokenTTL,
EnterpriseMeta: *targetMeta,
}

File diff suppressed because it is too large Load Diff

View File

@ -179,6 +179,48 @@ func testIdentityForToken(token string) (bool, structs.ACLIdentity, error) {
},
},
}, nil
case "found-synthetic-policy-3":
return true, &structs.ACLToken{
AccessorID: "bebccc92-3987-489d-84c2-ffd00d93ef93",
SecretID: "de70f2e2-69d9-4e88-9815-f91c03c6bcb1",
NodeIdentities: []*structs.ACLNodeIdentity{
&structs.ACLNodeIdentity{
NodeName: "test-node1",
Datacenter: "dc1",
},
// as the resolver is in dc1 this identity should be ignored
&structs.ACLNodeIdentity{
NodeName: "test-node-dc2",
Datacenter: "dc2",
},
},
}, nil
case "found-synthetic-policy-4":
return true, &structs.ACLToken{
AccessorID: "359b9927-25fd-46b9-bd14-3470f848ec65",
SecretID: "83c4d500-847d-49f7-8c08-0483f6b4156e",
NodeIdentities: []*structs.ACLNodeIdentity{
&structs.ACLNodeIdentity{
NodeName: "test-node2",
Datacenter: "dc1",
},
// as the resolver is in dc1 this identity should be ignored
&structs.ACLNodeIdentity{
NodeName: "test-node-dc2",
Datacenter: "dc2",
},
},
}, nil
case "found-role-node-identity":
return true, &structs.ACLToken{
AccessorID: "f3f47a09-de29-4c57-8f54-b65a9be79641",
SecretID: "e96aca00-5951-4b97-b0e5-5816f42dfb93",
Roles: []structs.ACLTokenRoleLink{
{
ID: "node-identity",
},
},
}, nil
case "acl-ro":
return true, &structs.ACLToken{
AccessorID: "435a75af-1763-4980-89f4-f0951dda53b4",
@ -443,6 +485,22 @@ func testRoleForID(roleID string) (bool, *structs.ACLRole, error) {
},
},
}, nil
case "node-identity":
return true, &structs.ACLRole{
ID: "node-identity",
Name: "node-identity",
Description: "node-identity",
NodeIdentities: []*structs.ACLNodeIdentity{
&structs.ACLNodeIdentity{
NodeName: "test-node",
Datacenter: "dc1",
},
&structs.ACLNodeIdentity{
NodeName: "test-node-dc2",
Datacenter: "dc2",
},
},
}, nil
default:
return testRoleForIDEnterprise(roleID)
}
@ -1729,8 +1787,18 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega
require.Equal(t, acl.Allow, authz.ServiceRead("bar", nil))
})
runTwiceAndReset("Role With Node Identity", func(t *testing.T) {
authz, err := r.ResolveToken("found-role-node-identity")
require.NoError(t, err)
require.NotNil(t, authz)
require.Equal(t, acl.Allow, authz.NodeWrite("test-node", nil))
require.Equal(t, acl.Deny, authz.NodeWrite("test-node-dc2", nil))
require.Equal(t, acl.Allow, authz.ServiceRead("something", nil))
require.Equal(t, acl.Deny, authz.ServiceWrite("something", nil))
})
runTwiceAndReset("Synthetic Policies Independently Cache", func(t *testing.T) {
// We resolve both of these tokens in the same cache session
// We resolve these tokens in the same cache session
// to verify that the keys for caching synthetic policies don't bleed
// over between each other.
{
@ -1761,6 +1829,38 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega
require.Equal(t, acl.Allow, authz.ServiceRead("literally-anything", nil))
require.Equal(t, acl.Allow, authz.NodeRead("any-node", nil))
}
{
authz, err := r.ResolveToken("found-synthetic-policy-3")
require.NoError(t, err)
require.NotNil(t, authz)
// spot check some random perms
require.Equal(t, acl.Deny, authz.ACLRead(nil))
require.Equal(t, acl.Deny, authz.NodeWrite("foo", nil))
// ensure we didn't bleed over to the other synthetic policy
require.Equal(t, acl.Deny, authz.NodeWrite("test-node2", nil))
// check our own synthetic policy
require.Equal(t, acl.Allow, authz.ServiceRead("literally-anything", nil))
require.Equal(t, acl.Allow, authz.NodeWrite("test-node1", nil))
// ensure node identity for other DC is ignored
require.Equal(t, acl.Deny, authz.NodeWrite("test-node-dc2", nil))
}
{
authz, err := r.ResolveToken("found-synthetic-policy-4")
require.NoError(t, err)
require.NotNil(t, authz)
// spot check some random perms
require.Equal(t, acl.Deny, authz.ACLRead(nil))
require.Equal(t, acl.Deny, authz.NodeWrite("foo", nil))
// ensure we didn't bleed over to the other synthetic policy
require.Equal(t, acl.Deny, authz.NodeWrite("test-node1", nil))
// check our own synthetic policy
require.Equal(t, acl.Allow, authz.ServiceRead("literally-anything", nil))
require.Equal(t, acl.Allow, authz.NodeWrite("test-node2", nil))
// ensure node identity for other DC is ignored
require.Equal(t, acl.Deny, authz.NodeWrite("test-node-dc2", nil))
}
})
runTwiceAndReset("Anonymous", func(t *testing.T) {

View File

@ -392,13 +392,8 @@ service "foo" {
func TestIntention_WildcardACLEnforcement(t *testing.T) {
t.Parallel()
dir, srv := testACLServerWithConfig(t, nil, false)
defer os.RemoveAll(dir)
defer srv.Shutdown()
codec := rpcClient(t, srv)
defer codec.Close()
testrpc.WaitForLeader(t, srv.RPC, "dc1")
_, srv, codec := testACLServerWithConfig(t, nil, false)
waitForLeaderEstablishment(t, srv)
// create some test policies.
@ -1222,13 +1217,8 @@ func TestIntentionMatch_good(t *testing.T) {
func TestIntentionMatch_acl(t *testing.T) {
t.Parallel()
dir1, s1 := testACLServerWithConfig(t, nil, false)
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
_, srv, codec := testACLServerWithConfig(t, nil, false)
waitForLeaderEstablishment(t, srv)
token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service "bar" { policy = "write" }`)
require.NoError(t, err)
@ -1464,13 +1454,8 @@ service "bar" {
func TestIntentionCheck_match(t *testing.T) {
t.Parallel()
dir1, s1 := testACLServerWithConfig(t, nil, false)
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
_, srv, codec := testACLServerWithConfig(t, nil, false)
waitForLeaderEstablishment(t, srv)
token, err := upsertTestTokenWithPolicyRules(codec, TestDefaultMasterToken, "dc1", `service "api" { policy = "read" }`)
require.NoError(t, err)

View File

@ -730,6 +730,7 @@ func (s *Server) legacyACLTokenUpgrade(ctx context.Context) error {
// Assign the global-management policy to legacy management tokens
if len(newToken.Policies) == 0 &&
len(newToken.ServiceIdentities) == 0 &&
len(newToken.NodeIdentities) == 0 &&
len(newToken.Roles) == 0 &&
newToken.Type == structs.ACLTokenTypeManagement {
newToken.Policies = append(newToken.Policies, structs.ACLTokenPolicyLink{ID: structs.ACLPolicyGlobalManagementID})

View File

@ -1216,9 +1216,7 @@ func TestLeader_ACLLegacyReplication(t *testing.T) {
c.Datacenter = "dc2"
c.ACLTokenReplication = true
}
dir, srv := testACLServerWithConfig(t, cb, true)
defer os.RemoveAll(dir)
defer srv.Shutdown()
_, srv, _ := testACLServerWithConfig(t, cb, true)
waitForLeaderEstablishment(t, srv)
require.True(t, srv.leaderRoutineManager.IsRunning(legacyACLReplicationRoutineName))

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"net"
"net/rpc"
"os"
"strings"
"sync/atomic"
@ -235,14 +236,19 @@ func testServerWithConfig(t *testing.T, cb func(*Config)) (string, *Server) {
}
// cb is a function that can alter the test servers configuration prior to the server starting.
func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToken bool) (string, *Server) {
func testACLServerWithConfig(t *testing.T, cb func(*Config), initReplicationToken bool) (string, *Server, rpc.ClientCodec) {
dir, srv := testServerWithConfig(t, testServerACLConfig(cb))
t.Cleanup(func() { os.RemoveAll(dir) })
t.Cleanup(func() { srv.Shutdown() })
if initReplicationToken {
// setup some tokens here so we get less warnings in the logs
srv.tokens.UpdateReplicationToken(TestDefaultMasterToken, token.TokenSourceConfig)
}
return dir, srv
codec := rpcClient(t, srv)
t.Cleanup(func() { codec.Close() })
return dir, srv, codec
}
func newServer(c *Config) (*Server, error) {

View File

@ -742,8 +742,17 @@ func (s *Store) aclTokenSetTxn(tx *memdb.Txn, idx uint64, token *structs.ACLToke
}
}
for _, nodeid := range token.NodeIdentities {
if nodeid.NodeName == "" {
return fmt.Errorf("Encountered a Token with an empty node identity name in the state store")
}
if nodeid.Datacenter == "" {
return fmt.Errorf("Encountered a Token with an empty node identity datacenter in the state store")
}
}
if prohibitUnprivileged {
if numValidRoles == 0 && numValidPolicies == 0 && len(token.ServiceIdentities) == 0 {
if numValidRoles == 0 && numValidPolicies == 0 && len(token.ServiceIdentities) == 0 && len(token.NodeIdentities) == 0 {
return ErrTokenHasNoPrivileges
}
}
@ -1369,6 +1378,15 @@ func (s *Store) aclRoleSetTxn(tx *memdb.Txn, idx uint64, role *structs.ACLRole,
}
}
for _, nodeid := range role.NodeIdentities {
if nodeid.NodeName == "" {
return fmt.Errorf("Encountered a Role with an empty node identity name in the state store")
}
if nodeid.Datacenter == "" {
return fmt.Errorf("Encountered a Role with an empty node identity datacenter in the state store")
}
}
if err := s.aclRoleUpsertValidateEnterprise(tx, role, existing); err != nil {
return err
}

View File

@ -119,6 +119,7 @@ type ACLIdentity interface {
RoleIDs() []string
EmbeddedPolicy() *ACLPolicy
ServiceIdentityList() []*ACLServiceIdentity
NodeIdentityList() []*ACLNodeIdentity
IsExpired(asOf time.Time) bool
IsLocal() bool
EnterpriseMetadata() *EnterpriseMeta
@ -189,6 +190,50 @@ func (s *ACLServiceIdentity) SyntheticPolicy(entMeta *EnterpriseMeta) *ACLPolicy
return policy
}
// ACLNodeIdentity represents a high-level grant of all privileges
// necessary to assume the identity of that node and manage it.
type ACLNodeIdentity struct {
// NodeName identities the Node that this identity authorizes access to
NodeName string
// Datacenter is required and specifies the datacenter of the node.
Datacenter string
}
func (s *ACLNodeIdentity) Clone() *ACLNodeIdentity {
s2 := *s
return &s2
}
func (s *ACLNodeIdentity) AddToHash(h hash.Hash) {
h.Write([]byte(s.NodeName))
h.Write([]byte(s.Datacenter))
}
func (s *ACLNodeIdentity) EstimateSize() int {
return len(s.NodeName) + len(s.Datacenter)
}
func (s *ACLNodeIdentity) 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(aclPolicyTemplateNodeIdentity, s.NodeName)
hasher := fnv.New128a()
hashID := fmt.Sprintf("%x", hasher.Sum([]byte(rules)))
policy := &ACLPolicy{}
policy.ID = hashID
policy.Name = fmt.Sprintf("synthetic-policy-%s", hashID)
policy.Description = "synthetic policy"
policy.Rules = rules
policy.Syntax = acl.SyntaxCurrent
policy.Datacenters = []string{s.Datacenter}
policy.EnterpriseMeta = *DefaultEnterpriseMeta()
policy.SetHash(true)
return policy
}
type ACLToken struct {
// This is the UUID used for tracking and management purposes
AccessorID string
@ -212,6 +257,9 @@ type ACLToken struct {
// List of services to generate synthetic policies for.
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
// The node identities that this token should be allowed to manage.
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
// Type is the V1 Token Type
// DEPRECATED (ACL-Legacy-Compat) - remove once we no longer support v1 ACL compat
// Even though we are going to auto upgrade management tokens we still
@ -302,6 +350,7 @@ func (t *ACLToken) Clone() *ACLToken {
t2.Policies = nil
t2.Roles = nil
t2.ServiceIdentities = nil
t2.NodeIdentities = nil
if len(t.Policies) > 0 {
t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies))
@ -317,6 +366,13 @@ func (t *ACLToken) Clone() *ACLToken {
t2.ServiceIdentities[i] = s.Clone()
}
}
if len(t.NodeIdentities) > 0 {
t2.NodeIdentities = make([]*ACLNodeIdentity, len(t.NodeIdentities))
for i, n := range t.NodeIdentities {
t2.NodeIdentities[i] = n.Clone()
}
}
return &t2
}
@ -382,6 +438,7 @@ func (t *ACLToken) HasExpirationTime() bool {
func (t *ACLToken) UsesNonLegacyFields() bool {
return len(t.Policies) > 0 ||
len(t.ServiceIdentities) > 0 ||
len(t.NodeIdentities) > 0 ||
len(t.Roles) > 0 ||
t.Type == "" ||
t.HasExpirationTime() ||
@ -462,6 +519,10 @@ func (t *ACLToken) SetHash(force bool) []byte {
srvid.AddToHash(hash)
}
for _, nodeID := range t.NodeIdentities {
nodeID.AddToHash(hash)
}
t.EnterpriseMeta.addToHash(hash, false)
// Finalize the hash
@ -485,6 +546,9 @@ func (t *ACLToken) EstimateSize() int {
for _, srvid := range t.ServiceIdentities {
size += srvid.EstimateSize()
}
for _, nodeID := range t.NodeIdentities {
size += nodeID.EstimateSize()
}
return size + t.EnterpriseMeta.estimateSize()
}
@ -497,6 +561,7 @@ type ACLTokenListStub struct {
Policies []ACLTokenPolicyLink `json:",omitempty"`
Roles []ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
Local bool
AuthMethod string `json:",omitempty"`
ExpirationTime *time.Time `json:",omitempty"`
@ -517,6 +582,7 @@ func (token *ACLToken) Stub() *ACLTokenListStub {
Policies: token.Policies,
Roles: token.Roles,
ServiceIdentities: token.ServiceIdentities,
NodeIdentities: token.NodeIdentities,
Local: token.Local,
AuthMethod: token.AuthMethod,
ExpirationTime: token.ExpirationTime,
@ -811,6 +877,9 @@ type ACLRole struct {
// List of services to generate synthetic policies for.
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
// List of nodes to generate synthetic policies for.
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
// Hash of the contents of the role
// This does not take into account the ID (which is immutable)
// nor the raft metadata.
@ -849,6 +918,7 @@ func (r *ACLRole) Clone() *ACLRole {
r2 := *r
r2.Policies = nil
r2.ServiceIdentities = nil
r2.NodeIdentities = nil
if len(r.Policies) > 0 {
r2.Policies = make([]ACLRolePolicyLink, len(r.Policies))
@ -860,6 +930,12 @@ func (r *ACLRole) Clone() *ACLRole {
r2.ServiceIdentities[i] = s.Clone()
}
}
if len(r.NodeIdentities) > 0 {
r2.NodeIdentities = make([]*ACLNodeIdentity, len(r.NodeIdentities))
for i, n := range r.NodeIdentities {
r2.NodeIdentities[i] = n.Clone()
}
}
return &r2
}
@ -888,6 +964,9 @@ func (r *ACLRole) SetHash(force bool) []byte {
for _, srvid := range r.ServiceIdentities {
srvid.AddToHash(hash)
}
for _, nodeID := range r.NodeIdentities {
nodeID.AddToHash(hash)
}
r.EnterpriseMeta.addToHash(hash, false)
@ -912,6 +991,9 @@ func (r *ACLRole) EstimateSize() int {
for _, srvid := range r.ServiceIdentities {
size += srvid.EstimateSize()
}
for _, nodeID := range r.NodeIdentities {
size += nodeID.EstimateSize()
}
return size + r.EnterpriseMeta.estimateSize()
}
@ -945,6 +1027,21 @@ const (
//
// If it does not exist at login-time the rule is ignored.
BindingRuleBindTypeRole = "role"
// BindingRuleBindTypeNode is the binding rule bind type that assigns
// a Node Identity to the token that is created using the value of
// the computed BindName as the NodeName like:
//
// &ACLToken{
// ...other fields...
// NodeIdentities: []*ACLNodeIdentity{
// &ACLNodeIdentity{
// NodeName: "<computed BindName>",
// Datacenter: "<local datacenter of the binding rule>"
// }
// }
// }
BindingRuleBindTypeNode = "node"
)
type ACLBindingRule struct {

View File

@ -78,6 +78,7 @@ func (a *ACL) Convert() *ACLToken {
Description: a.Name,
Policies: nil,
ServiceIdentities: nil,
NodeIdentities: nil,
Type: a.Type,
Rules: a.Rules,
Local: false,

View File

@ -26,6 +26,24 @@ service_prefix "" {
node_prefix "" {
policy = "read"
}`
// A typical Consul node requires two permissions for itself.
// node:write
// - register itself in the catalog
// - update its network coordinates
// - potentially used to delete services during anti-entropy
// service:read
// - used during anti-entropy to discover all services that
// are registered to the node. That way the node can diff
// its local state against an accurate depiction of the
// remote state.
aclPolicyTemplateNodeIdentity = `
node "%[1]s" {
policy = "write"
}
service_prefix "" {
policy = "read"
}`
)
type ACLAuthMethodEnterpriseFields struct{}
@ -51,3 +69,27 @@ func (p *ACLPolicy) EnterprisePolicyMeta() *acl.EnterprisePolicyMeta {
func (m *ACLAuthMethod) TargetEnterpriseMeta(_ *EnterpriseMeta) *EnterpriseMeta {
return &m.EnterpriseMeta
}
func (t *ACLToken) NodeIdentityList() []*ACLNodeIdentity {
if len(t.NodeIdentities) == 0 {
return nil
}
out := make([]*ACLNodeIdentity, 0, len(t.NodeIdentities))
for _, n := range t.NodeIdentities {
out = append(out, n.Clone())
}
return out
}
func (r *ACLRole) NodeIdentityList() []*ACLNodeIdentity {
if len(r.NodeIdentities) == 0 {
return nil
}
out := make([]*ACLNodeIdentity, 0, len(r.NodeIdentities))
for _, n := range r.NodeIdentities {
out = append(out, n.Clone())
}
return out
}

View File

@ -37,6 +37,7 @@ type ACLToken struct {
Policies []*ACLTokenPolicyLink `json:",omitempty"`
Roles []*ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
Local bool
AuthMethod string `json:",omitempty"`
ExpirationTTL time.Duration `json:",omitempty"`
@ -61,6 +62,7 @@ type ACLTokenListEntry struct {
Policies []*ACLTokenPolicyLink `json:",omitempty"`
Roles []*ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
Local bool
AuthMethod string `json:",omitempty"`
ExpirationTime *time.Time `json:",omitempty"`
@ -105,6 +107,13 @@ type ACLServiceIdentity struct {
Datacenters []string `json:",omitempty"`
}
// ACLNodeIdentity represents a high-level grant of all necessary privileges
// to assume the identity of the named Node in the Catalog and within Connect.
type ACLNodeIdentity struct {
NodeName string
Datacenter string
}
// ACLPolicy represents an ACL Policy.
type ACLPolicy struct {
ID string
@ -144,6 +153,7 @@ type ACLRole struct {
Description string
Policies []*ACLRolePolicyLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
Hash []byte
CreateIndex uint64
ModifyIndex uint64

View File

@ -217,6 +217,23 @@ func ExtractServiceIdentities(serviceIdents []string) ([]*api.ACLServiceIdentity
return out, nil
}
func ExtractNodeIdentities(nodeIdents []string) ([]*api.ACLNodeIdentity, error) {
var out []*api.ACLNodeIdentity
for _, nodeidRaw := range nodeIdents {
parts := strings.Split(nodeidRaw, ":")
switch len(parts) {
case 2:
out = append(out, &api.ACLNodeIdentity{
NodeName: parts[0],
Datacenter: parts[1],
})
default:
return nil, fmt.Errorf("Malformed -node-identity argument: %q", nodeidRaw)
}
}
return out, nil
}
// TestKubernetesJWT_A is a valid service account jwt extracted from a minikube setup.
//
// {

View File

@ -29,6 +29,7 @@ type cmd struct {
policyIDs []string
policyNames []string
serviceIdents []string
nodeIdents []string
showMeta bool
format string
@ -47,6 +48,9 @@ func (c *cmd) init() {
c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+
"service identity to use for this role. May be specified multiple times. Format is "+
"the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...")
c.flags.Var((*flags.AppendSliceValue)(&c.nodeIdents), "node-identity", "Name of a "+
"node identity to use for this role. May be specified multiple times. Format is "+
"NODENAME:DATACENTER")
c.flags.StringVar(
&c.format,
"format",
@ -71,8 +75,8 @@ func (c *cmd) Run(args []string) int {
return 1
}
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && len(c.serviceIdents) == 0 {
c.UI.Error(fmt.Sprintf("Cannot create a role without specifying -policy-name, -policy-id, or -service-identity at least once"))
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 {
c.UI.Error(fmt.Sprintf("Cannot create a role without specifying -policy-name, -policy-id, -service-identity, or -node-identity at least once"))
return 1
}
@ -109,6 +113,13 @@ func (c *cmd) Run(args []string) int {
}
newRole.ServiceIdentities = parsedServiceIdents
parsedNodeIdents, err := acl.ExtractNodeIdentities(c.nodeIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
newRole.NodeIdentities = parsedNodeIdents
r, _, err := client.ACL().RoleCreate(newRole, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create new role: %v", err))

View File

@ -41,8 +41,18 @@ func TestRoleCreateCommand_Pretty(t *testing.T) {
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi()
cmd := New(ui)
run := func(t *testing.T, args []string) *api.ACLRole {
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(append(args, "-format=json", "-http-addr="+a.HTTPAddr()))
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
var role api.ACLRole
require.NoError(t, json.Unmarshal(ui.OutputWriter.Bytes(), &role))
return &role
}
// Create a policy
client := a.Client()
@ -54,64 +64,55 @@ func TestRoleCreateCommand_Pretty(t *testing.T) {
require.NoError(t, err)
// create with policy by name
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
t.Run("policy-name", func(t *testing.T) {
_ = run(t, []string{
"-token=root",
"-name=role-with-policy-by-name",
"-description=test-role",
"-policy-name=" + policy.Name,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
})
})
// create with policy by id
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
t.Run("policy-id", func(t *testing.T) {
_ = run(t, []string{
"-token=root",
"-name=role-with-policy-by-id",
"-description=test-role",
"-policy-id=" + policy.ID,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
})
})
// create with service identity
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
t.Run("service-identity", func(t *testing.T) {
_ = run(t, []string{
"-token=root",
"-name=role-with-service-identity",
"-description=test-role",
"-service-identity=web",
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
})
})
// create with service identity scoped to 2 DCs
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
t.Run("dc-scoped-service-identity", func(t *testing.T) {
_ = run(t, []string{
"-token=root",
"-name=role-with-service-identity-in-2-dcs",
"-description=test-role",
"-service-identity=db:abc,xyz",
}
})
})
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
t.Run("node-identity", func(t *testing.T) {
role := run(t, []string{
"-token=root",
"-name=role-with-node-identity",
"-description=test-role",
"-node-identity=foo:bar",
})
require.Len(t, role.NodeIdentities, 1)
})
}
func TestRoleCreateCommand_JSON(t *testing.T) {

View File

@ -77,6 +77,12 @@ func (f *prettyFormatter) FormatRole(role *api.ACLRole) (string, error) {
}
}
}
if len(role.NodeIdentities) > 0 {
buffer.WriteString(fmt.Sprintln("Node Identities:"))
for _, nodeid := range role.NodeIdentities {
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
}
}
return buffer.String(), nil
}
@ -122,6 +128,13 @@ func (f *prettyFormatter) formatRoleListEntry(role *api.ACLRole) string {
}
}
if len(role.NodeIdentities) > 0 {
buffer.WriteString(fmt.Sprintln(" Node Identities:"))
for _, nodeid := range role.NodeIdentities {
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
}
}
return buffer.String()
}

View File

@ -0,0 +1,195 @@
package role
import (
"flag"
"fmt"
"io/ioutil"
"path"
"path/filepath"
"testing"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
)
// update allows golden files to be updated based on the current output.
var update = flag.Bool("update", false, "update golden files")
// golden reads and optionally writes the expected data to the golden file,
// returning the contents as a string.
func golden(t *testing.T, name, got string) string {
t.Helper()
golden := filepath.Join("testdata", name+".golden")
if *update && got != "" {
err := ioutil.WriteFile(golden, []byte(got), 0644)
require.NoError(t, err)
}
expected, err := ioutil.ReadFile(golden)
require.NoError(t, err)
return string(expected)
}
func TestFormatRole(t *testing.T) {
type testCase struct {
role api.ACLRole
overrideGoldenName string
}
cases := map[string]testCase{
"basic": {
role: api.ACLRole{
ID: "bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852",
Name: "basic",
Description: "test role",
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 42,
ModifyIndex: 100,
},
},
"complex": {
role: api.ACLRole{
ID: "c29c4ee4-bca6-474e-be37-7d9606f9582a",
Name: "complex",
Namespace: "foo",
Description: "test role complex",
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 5,
ModifyIndex: 10,
Policies: []*api.ACLLink{
&api.ACLLink{
ID: "beb04680-815b-4d7c-9e33-3d707c24672c",
Name: "hobbiton",
},
&api.ACLLink{
ID: "18788457-584c-4812-80d3-23d403148a90",
Name: "bywater",
},
},
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "gardener",
Datacenters: []string{"middleearth-northwest"},
},
},
NodeIdentities: []*api.ACLNodeIdentity{
&api.ACLNodeIdentity{
NodeName: "bagend",
Datacenter: "middleearth-northwest",
},
},
},
},
}
formatters := map[string]Formatter{
"pretty": newPrettyFormatter(false),
"pretty-meta": newPrettyFormatter(true),
// the JSON formatter ignores the showMeta
"json": newJSONFormatter(false),
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
for fmtName, formatter := range formatters {
t.Run(fmtName, func(t *testing.T) {
actual, err := formatter.FormatRole(&tcase.role)
require.NoError(t, err)
gName := fmt.Sprintf("%s.%s", name, fmtName)
if tcase.overrideGoldenName != "" {
gName = tcase.overrideGoldenName
}
expected := golden(t, path.Join("FormatRole", gName), actual)
require.Equal(t, expected, actual)
})
}
})
}
}
func TestFormatTokenList(t *testing.T) {
type testCase struct {
roles []*api.ACLRole
overrideGoldenName string
}
cases := map[string]testCase{
"basic": {
roles: []*api.ACLRole{
&api.ACLRole{
ID: "bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852",
Name: "basic",
Description: "test role",
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 42,
ModifyIndex: 100,
},
},
},
"complex": {
roles: []*api.ACLRole{
&api.ACLRole{
ID: "c29c4ee4-bca6-474e-be37-7d9606f9582a",
Name: "complex",
Namespace: "foo",
Description: "test role complex",
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 5,
ModifyIndex: 10,
Policies: []*api.ACLLink{
&api.ACLLink{
ID: "beb04680-815b-4d7c-9e33-3d707c24672c",
Name: "hobbiton",
},
&api.ACLLink{
ID: "18788457-584c-4812-80d3-23d403148a90",
Name: "bywater",
},
},
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "gardener",
Datacenters: []string{"middleearth-northwest"},
},
},
NodeIdentities: []*api.ACLNodeIdentity{
&api.ACLNodeIdentity{
NodeName: "bagend",
Datacenter: "middleearth-northwest",
},
},
},
},
},
}
formatters := map[string]Formatter{
"pretty": newPrettyFormatter(false),
"pretty-meta": newPrettyFormatter(true),
// the JSON formatter ignores the showMeta
"json": newJSONFormatter(false),
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
for fmtName, formatter := range formatters {
t.Run(fmtName, func(t *testing.T) {
actual, err := formatter.FormatRoleList(tcase.roles)
require.NoError(t, err)
gName := fmt.Sprintf("%s.%s", name, fmtName)
if tcase.overrideGoldenName != "" {
gName = tcase.overrideGoldenName
}
expected := golden(t, path.Join("FormatRoleList", gName), actual)
require.Equal(t, expected, actual)
})
}
})
}
}

View File

@ -0,0 +1,8 @@
{
"ID": "bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852",
"Name": "basic",
"Description": "test role",
"Hash": "YWJjZGVmZ2g=",
"CreateIndex": 42,
"ModifyIndex": 100
}

View File

@ -0,0 +1,6 @@
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
Name: basic
Description: test role
Hash: 6162636465666768
Create Index: 42
Modify Index: 100

View File

@ -0,0 +1,3 @@
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
Name: basic
Description: test role

View File

@ -0,0 +1,33 @@
{
"ID": "c29c4ee4-bca6-474e-be37-7d9606f9582a",
"Name": "complex",
"Description": "test role complex",
"Policies": [
{
"ID": "beb04680-815b-4d7c-9e33-3d707c24672c",
"Name": "hobbiton"
},
{
"ID": "18788457-584c-4812-80d3-23d403148a90",
"Name": "bywater"
}
],
"ServiceIdentities": [
{
"ServiceName": "gardener",
"Datacenters": [
"middleearth-northwest"
]
}
],
"NodeIdentities": [
{
"NodeName": "bagend",
"Datacenter": "middleearth-northwest"
}
],
"Hash": "YWJjZGVmZ2g=",
"CreateIndex": 5,
"ModifyIndex": 10,
"Namespace": "foo"
}

View File

@ -0,0 +1,14 @@
ID: c29c4ee4-bca6-474e-be37-7d9606f9582a
Name: complex
Namespace: foo
Description: test role complex
Hash: 6162636465666768
Create Index: 5
Modify Index: 10
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Service Identities:
gardener (Datacenters: middleearth-northwest)
Node Identities:
bagend (Datacenter: middleearth-northwest)

View File

@ -0,0 +1,11 @@
ID: c29c4ee4-bca6-474e-be37-7d9606f9582a
Name: complex
Namespace: foo
Description: test role complex
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Service Identities:
gardener (Datacenters: middleearth-northwest)
Node Identities:
bagend (Datacenter: middleearth-northwest)

View File

@ -0,0 +1,10 @@
[
{
"ID": "bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852",
"Name": "basic",
"Description": "test role",
"Hash": "YWJjZGVmZ2g=",
"CreateIndex": 42,
"ModifyIndex": 100
}
]

View File

@ -0,0 +1,6 @@
basic:
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
Description: test role
Hash: 6162636465666768
Create Index: 42
Modify Index: 100

View File

@ -0,0 +1,3 @@
basic:
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
Description: test role

View File

@ -0,0 +1,35 @@
[
{
"ID": "c29c4ee4-bca6-474e-be37-7d9606f9582a",
"Name": "complex",
"Description": "test role complex",
"Policies": [
{
"ID": "beb04680-815b-4d7c-9e33-3d707c24672c",
"Name": "hobbiton"
},
{
"ID": "18788457-584c-4812-80d3-23d403148a90",
"Name": "bywater"
}
],
"ServiceIdentities": [
{
"ServiceName": "gardener",
"Datacenters": [
"middleearth-northwest"
]
}
],
"NodeIdentities": [
{
"NodeName": "bagend",
"Datacenter": "middleearth-northwest"
}
],
"Hash": "YWJjZGVmZ2g=",
"CreateIndex": 5,
"ModifyIndex": 10,
"Namespace": "foo"
}
]

View File

@ -0,0 +1,14 @@
complex:
ID: c29c4ee4-bca6-474e-be37-7d9606f9582a
Namespace: foo
Description: test role complex
Hash: 6162636465666768
Create Index: 5
Modify Index: 10
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Service Identities:
gardener (Datacenters: middleearth-northwest)
Node Identities:
bagend (Datacenter: middleearth-northwest)

View File

@ -0,0 +1,11 @@
complex:
ID: c29c4ee4-bca6-474e-be37-7d9606f9582a
Namespace: foo
Description: test role complex
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Service Identities:
gardener (Datacenters: middleearth-northwest)
Node Identities:
bagend (Datacenter: middleearth-northwest)

View File

@ -30,6 +30,7 @@ type cmd struct {
policyIDs []string
policyNames []string
serviceIdents []string
nodeIdents []string
noMerge bool
showMeta bool
@ -52,6 +53,9 @@ func (c *cmd) init() {
c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+
"service identity to use for this role. May be specified multiple times. Format is "+
"the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...")
c.flags.Var((*flags.AppendSliceValue)(&c.nodeIdents), "node-identity", "Name of a "+
"node identity to use for this role. May be specified multiple times. Format is "+
"NODENAME:DATACENTER")
c.flags.BoolVar(&c.noMerge, "no-merge", false, "Do not merge the current role "+
"information with what is provided to the command. Instead overwrite all fields "+
"with the exception of the role ID which is immutable.")
@ -97,6 +101,12 @@ func (c *cmd) Run(args []string) int {
return 1
}
parsedNodeIdents, err := acl.ExtractNodeIdentities(c.nodeIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
// Read the current role in both cases so we can fail better if not found.
currentRole, _, err := client.ACL().RoleRead(roleID, nil)
if err != nil {
@ -114,6 +124,7 @@ func (c *cmd) Run(args []string) int {
Name: c.name,
Description: c.description,
ServiceIdentities: parsedServiceIdents,
NodeIdentities: parsedNodeIdents,
}
for _, policyName := range c.policyNames {
@ -192,6 +203,20 @@ func (c *cmd) Run(args []string) int {
r.ServiceIdentities = append(r.ServiceIdentities, svcid)
}
}
for _, nodeid := range parsedNodeIdents {
found := false
for _, link := range r.NodeIdentities {
if link.NodeName == nodeid.NodeName && link.Datacenter != nodeid.Datacenter {
found = true
break
}
}
if !found {
r.NodeIdentities = append(r.NodeIdentities, nodeid)
}
}
}
r, _, err = client.ACL().RoleUpdate(r, nil)

View File

@ -71,6 +71,19 @@ func TestRoleUpdateCommand(t *testing.T) {
)
require.NoError(t, err)
run := func(t *testing.T, args []string) *api.ACLRole {
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(append(args, "-format=json", "-http-addr="+a.HTTPAddr()))
require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
var role api.ACLRole
require.NoError(t, json.Unmarshal(ui.OutputWriter.Bytes(), &role))
return &role
}
t.Run("update a role that does not exist", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
@ -91,19 +104,12 @@ func TestRoleUpdateCommand(t *testing.T) {
})
t.Run("update with policy by name", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
_ = run(t, []string{
"-id=" + role.ID,
"-token=root",
"-policy-name=" + policy1.Name,
"-description=test role edited",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
})
role, _, err := client.ACL().RoleRead(
role.ID,
@ -119,18 +125,11 @@ func TestRoleUpdateCommand(t *testing.T) {
t.Run("update with policy by id", func(t *testing.T) {
// also update with no description shouldn't delete the current
// description
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
_ = run(t, []string{
"-id=" + role.ID,
"-token=root",
"-policy-id=" + policy2.ID,
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
})
role, _, err := client.ACL().RoleRead(
role.ID,
@ -144,18 +143,11 @@ func TestRoleUpdateCommand(t *testing.T) {
})
t.Run("update with service identity", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
_ = run(t, []string{
"-id=" + role.ID,
"-token=root",
"-service-identity=web",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
})
role, _, err := client.ACL().RoleRead(
role.ID,
@ -169,18 +161,11 @@ func TestRoleUpdateCommand(t *testing.T) {
})
t.Run("update with service identity scoped to 2 DCs", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
_ = run(t, []string{
"-id=" + role.ID,
"-token=root",
"-service-identity=db:abc,xyz",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
})
role, _, err := client.ACL().RoleRead(
role.ID,
@ -192,6 +177,25 @@ func TestRoleUpdateCommand(t *testing.T) {
require.Len(t, role.Policies, 2)
require.Len(t, role.ServiceIdentities, 3)
})
t.Run("update with node identity", func(t *testing.T) {
_ = run(t, []string{
"-id=" + role.ID,
"-token=root",
"-node-identity=foo:bar",
})
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "test role edited", role.Description)
require.Len(t, role.Policies, 2)
require.Len(t, role.ServiceIdentities, 3)
require.Len(t, role.NodeIdentities, 1)
})
}
func TestRoleUpdateCommand_JSON(t *testing.T) {

View File

@ -33,6 +33,7 @@ type cmd struct {
roleIDs []string
roleNames []string
serviceIdents []string
nodeIdents []string
expirationTTL time.Duration
local bool
showMeta bool
@ -60,6 +61,9 @@ func (c *cmd) init() {
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.Var((*flags.AppendSliceValue)(&c.nodeIdents), "node-identity", "Name of a "+
"node identity to use for this token. May be specified multiple times. Format is "+
"NODENAME:DATACENTER")
c.flags.DurationVar(&c.expirationTTL, "expires-ttl", 0, "Duration of time this "+
"token should be valid for")
c.flags.StringVar(
@ -82,8 +86,8 @@ func (c *cmd) Run(args []string) int {
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 &&
len(c.roleNames) == 0 && len(c.roleIDs) == 0 &&
len(c.serviceIdents) == 0 {
c.UI.Error(fmt.Sprintf("Cannot create a token without specifying -policy-name, -policy-id, -role-name, -role-id, or -service-identity at least once"))
len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 {
c.UI.Error(fmt.Sprintf("Cannot create a token without specifying -policy-name, -policy-id, -role-name, -role-id, -service-identity, or -node-identity at least once"))
return 1
}
@ -110,6 +114,13 @@ func (c *cmd) Run(args []string) int {
}
newToken.ServiceIdentities = parsedServiceIdents
parsedNodeIdents, err := acl.ExtractNodeIdentities(c.nodeIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
newToken.NodeIdentities = parsedNodeIdents
for _, policyName := range c.policyNames {
// We could resolve names to IDs here but there isn't any reason why its would be better
// than allowing the agent to do it.

View File

@ -24,13 +24,13 @@ func TestTokenCreateCommand_noTabs(t *testing.T) {
func TestTokenCreateCommand_Pretty(t *testing.T) {
t.Parallel()
require := require.New(t)
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, `
primary_datacenter = "dc1"
node_name = "test-node"
acl {
enabled = true
tokens {
@ -41,9 +41,6 @@ func TestTokenCreateCommand_Pretty(t *testing.T) {
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi()
cmd := New(ui)
// Create a policy
client := a.Client()
@ -51,66 +48,75 @@ func TestTokenCreateCommand_Pretty(t *testing.T) {
&api.ACLPolicy{Name: "test-policy"},
&api.WriteOptions{Token: "root"},
)
require.NoError(err)
require.NoError(t, err)
run := func(t *testing.T, args []string) *api.ACLToken {
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(append(args, "-format=json"))
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
var token api.ACLToken
require.NoError(t, json.Unmarshal(ui.OutputWriter.Bytes(), &token))
return &token
}
// create with policy by name
{
args := []string{
t.Run("policy-name", func(t *testing.T) {
_ = run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-policy-name=" + policy.Name,
"-description=test token",
}
code := cmd.Run(args)
require.Equal(code, 0)
require.Empty(ui.ErrorWriter.String())
}
})
})
// create with policy by id
{
args := []string{
t.Run("policy-id", func(t *testing.T) {
_ = run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-policy-id=" + policy.ID,
"-description=test token",
}
})
})
code := cmd.Run(args)
require.Empty(ui.ErrorWriter.String())
require.Equal(code, 0)
}
// create with a node identity
t.Run("node-identity", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-node-identity=" + a.Config.NodeName + ":" + a.Config.Datacenter,
})
conf := api.DefaultConfig()
conf.Address = a.HTTPAddr()
conf.Token = token.SecretID
client, err := api.NewClient(conf)
require.NoError(t, err)
nodes, _, err := client.Catalog().Nodes(nil)
require.NoError(t, err)
require.Len(t, nodes, 1)
require.Equal(t, a.Config.NodeName, nodes[0].Node)
})
// create with accessor and secret
{
args := []string{
t.Run("predefined-ids", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-policy-id=" + policy.ID,
"-description=test token",
"-accessor=3d852bb8-5153-4388-a3ca-8ca78661889f",
"-secret=3a69a8d8-c4d4-485d-9b19-b5b61648ea0c",
}
})
code := cmd.Run(args)
require.Empty(ui.ErrorWriter.String())
require.Equal(code, 0)
conf := api.DefaultConfig()
conf.Address = a.HTTPAddr()
conf.Token = "root"
// going to use the API client to grab the token - we could potentially try to grab the values
// out of the command output but this seems easier.
client, err := api.NewClient(conf)
require.NoError(err)
require.NotNil(client)
token, _, err := client.ACL().TokenRead("3d852bb8-5153-4388-a3ca-8ca78661889f", nil)
require.NoError(err)
require.Equal("3d852bb8-5153-4388-a3ca-8ca78661889f", token.AccessorID)
require.Equal("3a69a8d8-c4d4-485d-9b19-b5b61648ea0c", token.SecretID)
}
require.Equal(t, "3d852bb8-5153-4388-a3ca-8ca78661889f", token.AccessorID)
require.Equal(t, "3a69a8d8-c4d4-485d-9b19-b5b61648ea0c", token.SecretID)
})
}
func TestTokenCreateCommand_JSON(t *testing.T) {

View File

@ -91,6 +91,12 @@ func (f *prettyFormatter) FormatToken(token *api.ACLToken) (string, error) {
}
}
}
if len(token.NodeIdentities) > 0 {
buffer.WriteString(fmt.Sprintln("Node Identities:"))
for _, nodeid := range token.NodeIdentities {
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
}
}
if token.Rules != "" {
buffer.WriteString(fmt.Sprintln("Rules:"))
buffer.WriteString(fmt.Sprintln(token.Rules))
@ -159,6 +165,16 @@ func (f *prettyFormatter) formatTokenListEntry(token *api.ACLTokenListEntry) str
}
}
}
if len(token.NodeIdentities) > 0 {
buffer.WriteString(fmt.Sprintln("Service Identities:"))
for _, svcid := range token.ServiceIdentities {
if len(svcid.Datacenters) > 0 {
buffer.WriteString(fmt.Sprintf(" %s (Datacenters: %s)\n", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
} else {
buffer.WriteString(fmt.Sprintf(" %s (Datacenters: all)\n", svcid.ServiceName))
}
}
}
return buffer.String()
}

View File

@ -0,0 +1,251 @@
package token
import (
"flag"
"fmt"
"io/ioutil"
"path"
"path/filepath"
"testing"
"time"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
)
// update allows golden files to be updated based on the current output.
var update = flag.Bool("update", false, "update golden files")
// golden reads and optionally writes the expected data to the golden file,
// returning the contents as a string.
func golden(t *testing.T, name, got string) string {
t.Helper()
golden := filepath.Join("testdata", name+".golden")
if *update && got != "" {
err := ioutil.WriteFile(golden, []byte(got), 0644)
require.NoError(t, err)
}
expected, err := ioutil.ReadFile(golden)
require.NoError(t, err)
return string(expected)
}
func TestFormatToken(t *testing.T) {
type testCase struct {
token api.ACLToken
overrideGoldenName string
}
timeRef := func(in time.Time) *time.Time {
return &in
}
cases := map[string]testCase{
"basic": {
token: api.ACLToken{
AccessorID: "fbd2447f-7479-4329-ad13-b021d74f86ba",
SecretID: "869c6e91-4de9-4dab-b56e-87548435f9c6",
Description: "test token",
Local: false,
CreateTime: time.Date(2020, 5, 22, 18, 52, 31, 0, time.UTC),
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 42,
ModifyIndex: 100,
},
},
"legacy": {
token: api.ACLToken{
AccessorID: "8acc7486-ca54-4d3c-9aed-5cd85651b0ee",
SecretID: "legacy-secret",
Description: "legacy",
Rules: `operator = "read"`,
},
},
"complex": {
token: api.ACLToken{
AccessorID: "fbd2447f-7479-4329-ad13-b021d74f86ba",
SecretID: "869c6e91-4de9-4dab-b56e-87548435f9c6",
Namespace: "foo",
Description: "test token",
Local: false,
AuthMethod: "bar",
CreateTime: time.Date(2020, 5, 22, 18, 52, 31, 0, time.UTC),
ExpirationTime: timeRef(time.Date(2020, 5, 22, 19, 52, 31, 0, time.UTC)),
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 5,
ModifyIndex: 10,
Policies: []*api.ACLLink{
&api.ACLLink{
ID: "beb04680-815b-4d7c-9e33-3d707c24672c",
Name: "hobbiton",
},
&api.ACLLink{
ID: "18788457-584c-4812-80d3-23d403148a90",
Name: "bywater",
},
},
Roles: []*api.ACLLink{
&api.ACLLink{
ID: "3b0a78fe-b9c3-40de-b8ea-7d4d6674b366",
Name: "shire",
},
&api.ACLLink{
ID: "6c9d1e1d-34bc-4d55-80f3-add0890ad791",
Name: "west-farthing",
},
},
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "gardener",
Datacenters: []string{"middleearth-northwest"},
},
},
NodeIdentities: []*api.ACLNodeIdentity{
&api.ACLNodeIdentity{
NodeName: "bagend",
Datacenter: "middleearth-northwest",
},
},
},
},
}
formatters := map[string]Formatter{
"pretty": newPrettyFormatter(false),
"pretty-meta": newPrettyFormatter(true),
// the JSON formatter ignores the showMeta
"json": newJSONFormatter(false),
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
for fmtName, formatter := range formatters {
t.Run(fmtName, func(t *testing.T) {
actual, err := formatter.FormatToken(&tcase.token)
require.NoError(t, err)
gName := fmt.Sprintf("%s.%s", name, fmtName)
if tcase.overrideGoldenName != "" {
gName = tcase.overrideGoldenName
}
expected := golden(t, path.Join("FormatToken", gName), actual)
require.Equal(t, expected, actual)
})
}
})
}
}
func TestFormatTokenList(t *testing.T) {
type testCase struct {
tokens []*api.ACLTokenListEntry
overrideGoldenName string
}
timeRef := func(in time.Time) *time.Time {
return &in
}
cases := map[string]testCase{
"basic": {
tokens: []*api.ACLTokenListEntry{
&api.ACLTokenListEntry{
AccessorID: "fbd2447f-7479-4329-ad13-b021d74f86ba",
Description: "test token",
Local: false,
CreateTime: time.Date(2020, 5, 22, 18, 52, 31, 0, time.UTC),
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 42,
ModifyIndex: 100,
},
},
},
"legacy": {
tokens: []*api.ACLTokenListEntry{
&api.ACLTokenListEntry{
AccessorID: "8acc7486-ca54-4d3c-9aed-5cd85651b0ee",
Description: "legacy",
Legacy: true,
},
},
},
"complex": {
tokens: []*api.ACLTokenListEntry{
&api.ACLTokenListEntry{
AccessorID: "fbd2447f-7479-4329-ad13-b021d74f86ba",
Namespace: "foo",
Description: "test token",
Local: false,
AuthMethod: "bar",
CreateTime: time.Date(2020, 5, 22, 18, 52, 31, 0, time.UTC),
ExpirationTime: timeRef(time.Date(2020, 5, 22, 19, 52, 31, 0, time.UTC)),
Hash: []byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'},
CreateIndex: 5,
ModifyIndex: 10,
Policies: []*api.ACLLink{
&api.ACLLink{
ID: "beb04680-815b-4d7c-9e33-3d707c24672c",
Name: "hobbiton",
},
&api.ACLLink{
ID: "18788457-584c-4812-80d3-23d403148a90",
Name: "bywater",
},
},
Roles: []*api.ACLLink{
&api.ACLLink{
ID: "3b0a78fe-b9c3-40de-b8ea-7d4d6674b366",
Name: "shire",
},
&api.ACLLink{
ID: "6c9d1e1d-34bc-4d55-80f3-add0890ad791",
Name: "west-farthing",
},
},
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "gardener",
Datacenters: []string{"middleearth-northwest"},
},
},
NodeIdentities: []*api.ACLNodeIdentity{
&api.ACLNodeIdentity{
NodeName: "bagend",
Datacenter: "middleearth-northwest",
},
},
},
},
},
}
formatters := map[string]Formatter{
"pretty": newPrettyFormatter(false),
"pretty-meta": newPrettyFormatter(true),
// the JSON formatter ignores the showMeta
"json": newJSONFormatter(false),
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
for fmtName, formatter := range formatters {
t.Run(fmtName, func(t *testing.T) {
actual, err := formatter.FormatTokenList(tcase.tokens)
require.NoError(t, err)
gName := fmt.Sprintf("%s.%s", name, fmtName)
if tcase.overrideGoldenName != "" {
gName = tcase.overrideGoldenName
}
expected := golden(t, path.Join("FormatTokenList", gName), actual)
require.Equal(t, expected, actual)
})
}
})
}
}

View File

@ -0,0 +1,10 @@
{
"CreateIndex": 42,
"ModifyIndex": 100,
"AccessorID": "fbd2447f-7479-4329-ad13-b021d74f86ba",
"SecretID": "869c6e91-4de9-4dab-b56e-87548435f9c6",
"Description": "test token",
"Local": false,
"CreateTime": "2020-05-22T18:52:31Z",
"Hash": "YWJjZGVmZ2g="
}

View File

@ -0,0 +1,8 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
SecretID: 869c6e91-4de9-4dab-b56e-87548435f9c6
Description: test token
Local: false
Create Time: 2020-05-22 18:52:31 +0000 UTC
Hash: 6162636465666768
Create Index: 42
Modify Index: 100

View File

@ -0,0 +1,5 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
SecretID: 869c6e91-4de9-4dab-b56e-87548435f9c6
Description: test token
Local: false
Create Time: 2020-05-22 18:52:31 +0000 UTC

View File

@ -0,0 +1,47 @@
{
"CreateIndex": 5,
"ModifyIndex": 10,
"AccessorID": "fbd2447f-7479-4329-ad13-b021d74f86ba",
"SecretID": "869c6e91-4de9-4dab-b56e-87548435f9c6",
"Description": "test token",
"Policies": [
{
"ID": "beb04680-815b-4d7c-9e33-3d707c24672c",
"Name": "hobbiton"
},
{
"ID": "18788457-584c-4812-80d3-23d403148a90",
"Name": "bywater"
}
],
"Roles": [
{
"ID": "3b0a78fe-b9c3-40de-b8ea-7d4d6674b366",
"Name": "shire"
},
{
"ID": "6c9d1e1d-34bc-4d55-80f3-add0890ad791",
"Name": "west-farthing"
}
],
"ServiceIdentities": [
{
"ServiceName": "gardener",
"Datacenters": [
"middleearth-northwest"
]
}
],
"NodeIdentities": [
{
"NodeName": "bagend",
"Datacenter": "middleearth-northwest"
}
],
"Local": false,
"AuthMethod": "bar",
"ExpirationTime": "2020-05-22T19:52:31Z",
"CreateTime": "2020-05-22T18:52:31Z",
"Hash": "YWJjZGVmZ2g=",
"Namespace": "foo"
}

View File

@ -0,0 +1,21 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
SecretID: 869c6e91-4de9-4dab-b56e-87548435f9c6
Namespace: foo
Description: test token
Local: false
Auth Method: bar
Create Time: 2020-05-22 18:52:31 +0000 UTC
Expiration Time: 2020-05-22 19:52:31 +0000 UTC
Hash: 6162636465666768
Create Index: 5
Modify Index: 10
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Roles:
3b0a78fe-b9c3-40de-b8ea-7d4d6674b366 - shire
6c9d1e1d-34bc-4d55-80f3-add0890ad791 - west-farthing
Service Identities:
gardener (Datacenters: middleearth-northwest)
Node Identities:
bagend (Datacenter: middleearth-northwest)

View File

@ -0,0 +1,18 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
SecretID: 869c6e91-4de9-4dab-b56e-87548435f9c6
Namespace: foo
Description: test token
Local: false
Auth Method: bar
Create Time: 2020-05-22 18:52:31 +0000 UTC
Expiration Time: 2020-05-22 19:52:31 +0000 UTC
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Roles:
3b0a78fe-b9c3-40de-b8ea-7d4d6674b366 - shire
6c9d1e1d-34bc-4d55-80f3-add0890ad791 - west-farthing
Service Identities:
gardener (Datacenters: middleearth-northwest)
Node Identities:
bagend (Datacenter: middleearth-northwest)

View File

@ -0,0 +1,10 @@
{
"CreateIndex": 0,
"ModifyIndex": 0,
"AccessorID": "8acc7486-ca54-4d3c-9aed-5cd85651b0ee",
"SecretID": "legacy-secret",
"Description": "legacy",
"Local": false,
"CreateTime": "0001-01-01T00:00:00Z",
"Rules": "operator = \"read\""
}

View File

@ -0,0 +1,10 @@
AccessorID: 8acc7486-ca54-4d3c-9aed-5cd85651b0ee
SecretID: legacy-secret
Description: legacy
Local: false
Create Time: 0001-01-01 00:00:00 +0000 UTC
Hash:
Create Index: 0
Modify Index: 0
Rules:
operator = "read"

View File

@ -0,0 +1,7 @@
AccessorID: 8acc7486-ca54-4d3c-9aed-5cd85651b0ee
SecretID: legacy-secret
Description: legacy
Local: false
Create Time: 0001-01-01 00:00:00 +0000 UTC
Rules:
operator = "read"

View File

@ -0,0 +1,12 @@
[
{
"CreateIndex": 42,
"ModifyIndex": 100,
"AccessorID": "fbd2447f-7479-4329-ad13-b021d74f86ba",
"Description": "test token",
"Local": false,
"CreateTime": "2020-05-22T18:52:31Z",
"Hash": "YWJjZGVmZ2g=",
"Legacy": false
}
]

View File

@ -0,0 +1,8 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
Description: test token
Local: false
Create Time: 2020-05-22 18:52:31 +0000 UTC
Legacy: false
Hash: 6162636465666768
Create Index: 42
Modify Index: 100

View File

@ -0,0 +1,5 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
Description: test token
Local: false
Create Time: 2020-05-22 18:52:31 +0000 UTC
Legacy: false

View File

@ -0,0 +1,49 @@
[
{
"CreateIndex": 5,
"ModifyIndex": 10,
"AccessorID": "fbd2447f-7479-4329-ad13-b021d74f86ba",
"Description": "test token",
"Policies": [
{
"ID": "beb04680-815b-4d7c-9e33-3d707c24672c",
"Name": "hobbiton"
},
{
"ID": "18788457-584c-4812-80d3-23d403148a90",
"Name": "bywater"
}
],
"Roles": [
{
"ID": "3b0a78fe-b9c3-40de-b8ea-7d4d6674b366",
"Name": "shire"
},
{
"ID": "6c9d1e1d-34bc-4d55-80f3-add0890ad791",
"Name": "west-farthing"
}
],
"ServiceIdentities": [
{
"ServiceName": "gardener",
"Datacenters": [
"middleearth-northwest"
]
}
],
"NodeIdentities": [
{
"NodeName": "bagend",
"Datacenter": "middleearth-northwest"
}
],
"Local": false,
"AuthMethod": "bar",
"ExpirationTime": "2020-05-22T19:52:31Z",
"CreateTime": "2020-05-22T18:52:31Z",
"Hash": "YWJjZGVmZ2g=",
"Legacy": false,
"Namespace": "foo"
}
]

View File

@ -0,0 +1,21 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
Namespace: foo
Description: test token
Local: false
Auth Method: bar
Create Time: 2020-05-22 18:52:31 +0000 UTC
Expiration Time: 2020-05-22 19:52:31 +0000 UTC
Legacy: false
Hash: 6162636465666768
Create Index: 5
Modify Index: 10
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Roles:
3b0a78fe-b9c3-40de-b8ea-7d4d6674b366 - shire
6c9d1e1d-34bc-4d55-80f3-add0890ad791 - west-farthing
Service Identities:
gardener (Datacenters: middleearth-northwest)
Service Identities:
gardener (Datacenters: middleearth-northwest)

View File

@ -0,0 +1,18 @@
AccessorID: fbd2447f-7479-4329-ad13-b021d74f86ba
Namespace: foo
Description: test token
Local: false
Auth Method: bar
Create Time: 2020-05-22 18:52:31 +0000 UTC
Expiration Time: 2020-05-22 19:52:31 +0000 UTC
Legacy: false
Policies:
beb04680-815b-4d7c-9e33-3d707c24672c - hobbiton
18788457-584c-4812-80d3-23d403148a90 - bywater
Roles:
3b0a78fe-b9c3-40de-b8ea-7d4d6674b366 - shire
6c9d1e1d-34bc-4d55-80f3-add0890ad791 - west-farthing
Service Identities:
gardener (Datacenters: middleearth-northwest)
Service Identities:
gardener (Datacenters: middleearth-northwest)

View File

@ -0,0 +1,12 @@
[
{
"CreateIndex": 0,
"ModifyIndex": 0,
"AccessorID": "8acc7486-ca54-4d3c-9aed-5cd85651b0ee",
"Description": "legacy",
"Local": false,
"CreateTime": "0001-01-01T00:00:00Z",
"Hash": null,
"Legacy": true
}
]

View File

@ -0,0 +1,8 @@
AccessorID: 8acc7486-ca54-4d3c-9aed-5cd85651b0ee
Description: legacy
Local: false
Create Time: 0001-01-01 00:00:00 +0000 UTC
Legacy: true
Hash:
Create Index: 0
Modify Index: 0

View File

@ -0,0 +1,5 @@
AccessorID: 8acc7486-ca54-4d3c-9aed-5cd85651b0ee
Description: legacy
Local: false
Create Time: 0001-01-01 00:00:00 +0000 UTC
Legacy: true

View File

@ -30,10 +30,12 @@ type cmd struct {
roleIDs []string
roleNames []string
serviceIdents []string
nodeIdents []string
description string
mergePolicies bool
mergeRoles bool
mergeServiceIdents bool
mergeNodeIdents bool
showMeta bool
upgradeLegacy bool
format string
@ -49,6 +51,8 @@ func (c *cmd) init() {
"with the existing roles")
c.flags.BoolVar(&c.mergeServiceIdents, "merge-service-identities", false, "Merge the new service identities "+
"with the existing service identities")
c.flags.BoolVar(&c.mergeNodeIdents, "merge-node-identities", false, "Merge the new node identities "+
"with the existing node identities")
c.flags.StringVar(&c.tokenID, "id", "", "The Accessor ID of the token to update. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple token Accessor IDs")
@ -64,6 +68,9 @@ func (c *cmd) init() {
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.Var((*flags.AppendSliceValue)(&c.nodeIdents), "node-identity", "Name of a "+
"node identity to use for this token. May be specified multiple times. Format is "+
"NODENAME:DATACENTER")
c.flags.BoolVar(&c.upgradeLegacy, "upgrade-legacy", false, "Add new polices "+
"to a legacy token replacing all existing rules. This will cause the legacy "+
"token to behave exactly like a new token but keep the same Secret.\n"+
@ -139,6 +146,12 @@ func (c *cmd) Run(args []string) int {
return 1
}
parsedNodeIdents, err := acl.ExtractNodeIdentities(c.nodeIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
if c.mergePolicies {
for _, policyName := range c.policyNames {
found := false
@ -269,6 +282,24 @@ func (c *cmd) Run(args []string) int {
t.ServiceIdentities = parsedServiceIdents
}
if c.mergeNodeIdents {
for _, nodeid := range parsedNodeIdents {
found := false
for _, link := range t.NodeIdentities {
if link.NodeName == nodeid.NodeName && link.Datacenter == nodeid.Datacenter {
found = true
break
}
}
if !found {
t.NodeIdentities = append(t.NodeIdentities, nodeid)
}
}
} else {
t.NodeIdentities = parsedNodeIdents
}
t, _, err = client.ACL().TokenUpdate(t, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tokenID, err))

View File

@ -6,8 +6,6 @@ import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil"
@ -15,6 +13,7 @@ import (
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTokenUpdateCommand_noTabs(t *testing.T) {
@ -27,9 +26,6 @@ func TestTokenUpdateCommand_noTabs(t *testing.T) {
func TestTokenUpdateCommand(t *testing.T) {
t.Parallel()
assert := assert.New(t)
// Alias because we need to access require package in Retry below
req := require.New(t)
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
@ -46,8 +42,6 @@ func TestTokenUpdateCommand(t *testing.T) {
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi()
// Create a policy
client := a.Client()
@ -55,16 +49,17 @@ func TestTokenUpdateCommand(t *testing.T) {
&api.ACLPolicy{Name: "test-policy"},
&api.WriteOptions{Token: "root"},
)
req.NoError(err)
require.NoError(t, err)
// create a token
token, _, err := client.ACL().TokenCreate(
&api.ACLToken{Description: "test"},
&api.WriteOptions{Token: "root"},
)
req.NoError(err)
require.NoError(t, err)
// nolint: staticcheck // we want the deprecated legacy token
// create a legacy token
// nolint: staticcheck // we have to use the deprecated API to create a legacy token
legacyTokenSecretID, _, err := client.ACL().Create(&api.ACLEntry{
Name: "Legacy token",
Type: "client",
@ -72,79 +67,100 @@ func TestTokenUpdateCommand(t *testing.T) {
},
&api.WriteOptions{Token: "root"},
)
req.NoError(err)
require.NoError(t, err)
// We fetch the legacy token later to give server time to async background
// upgrade it.
// update with policy by name
{
run := func(t *testing.T, args []string) *api.ACLToken {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
code := cmd.Run(append(args, "-format=json"))
require.Equal(t, 0, code)
require.Empty(t, ui.ErrorWriter.String())
var token api.ACLToken
require.NoError(t, json.Unmarshal(ui.OutputWriter.Bytes(), &token))
return &token
}
// update with node identity
t.Run("node-identity", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + token.AccessorID,
"-token=root",
"-node-identity=foo:bar",
"-description=test token",
})
require.Len(t, token.NodeIdentities, 1)
require.Equal(t, "foo", token.NodeIdentities[0].NodeName)
require.Equal(t, "bar", token.NodeIdentities[0].Datacenter)
})
t.Run("node-identity-merge", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + token.AccessorID,
"-token=root",
"-node-identity=bar:baz",
"-description=test token",
"-merge-node-identities",
})
require.Len(t, token.NodeIdentities, 2)
expected := []*api.ACLNodeIdentity{
&api.ACLNodeIdentity{
NodeName: "foo",
Datacenter: "bar",
},
&api.ACLNodeIdentity{
NodeName: "bar",
Datacenter: "baz",
},
}
require.ElementsMatch(t, expected, token.NodeIdentities)
})
// update with policy by name
t.Run("policy-name", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + token.AccessorID,
"-token=root",
"-policy-name=" + policy.Name,
"-description=test token",
}
})
code := cmd.Run(args)
assert.Equal(code, 0)
assert.Empty(ui.ErrorWriter.String())
token, _, err := client.ACL().TokenRead(
token.AccessorID,
&api.QueryOptions{Token: "root"},
)
assert.NoError(err)
assert.NotNil(token)
}
require.Len(t, token.Policies, 1)
})
// update with policy by id
{
cmd := New(ui)
args := []string{
t.Run("policy-id", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + token.AccessorID,
"-token=root",
"-policy-id=" + policy.ID,
"-description=test token",
}
})
code := cmd.Run(args)
assert.Equal(code, 0)
assert.Empty(ui.ErrorWriter.String())
token, _, err := client.ACL().TokenRead(
token.AccessorID,
&api.QueryOptions{Token: "root"},
)
assert.NoError(err)
assert.NotNil(token)
}
require.Len(t, token.Policies, 1)
})
// update with no description shouldn't delete the current description
{
cmd := New(ui)
args := []string{
t.Run("merge-description", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + token.AccessorID,
"-token=root",
"-policy-name=" + policy.Name,
}
})
code := cmd.Run(args)
assert.Equal(code, 0)
assert.Empty(ui.ErrorWriter.String())
token, _, err := client.ACL().TokenRead(
token.AccessorID,
&api.QueryOptions{Token: "root"},
)
assert.NoError(err)
assert.NotNil(token)
assert.Equal("test token", token.Description)
}
require.Equal(t, "test token", token.Description)
})
// Need legacy token now, hopefully server had time to generate an accessor ID
// in the background but wait for it if not.
@ -153,39 +169,28 @@ func TestTokenUpdateCommand(t *testing.T) {
// Fetch the legacy token via new API so we can use it's accessor ID
legacyToken, _, err = client.ACL().TokenReadSelf(
&api.QueryOptions{Token: legacyTokenSecretID})
r.Check(err)
require.NoError(r, err)
require.NotEmpty(r, legacyToken.AccessorID)
})
// upgrade legacy token should replace rules and leave token in a "new" state!
{
cmd := New(ui)
args := []string{
t.Run("legacy-upgrade", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + legacyToken.AccessorID,
"-token=root",
"-policy-name=" + policy.Name,
"-upgrade-legacy",
}
})
code := cmd.Run(args)
assert.Equal(code, 0)
assert.Empty(ui.ErrorWriter.String())
gotToken, _, err := client.ACL().TokenRead(
legacyToken.AccessorID,
&api.QueryOptions{Token: "root"},
)
assert.NoError(err)
assert.NotNil(gotToken)
// Description shouldn't change
assert.Equal("Legacy token", gotToken.Description)
assert.Len(gotToken.Policies, 1)
require.Equal(t, "Legacy token", token.Description)
require.Len(t, token.Policies, 1)
// Rules should now be empty meaning this is no longer a legacy token
assert.Empty(gotToken.Rules)
require.Empty(t, token.Rules)
// Secret should not have changes
assert.Equal(legacyToken.SecretID, gotToken.SecretID)
}
require.Equal(t, legacyToken.SecretID, token.SecretID)
})
}
func TestTokenUpdateCommand_JSON(t *testing.T) {

View File

@ -63,6 +63,17 @@ The table below shows this endpoint's support for
]
}
```
- `BindType=node` - The computed bind name value is used as an
`ACLNodeIdentity.NodeName` field in the token that is created.
```json
{ ...other fields...
"NodeIdentities": [
{ "NodeName": "<computed BindName>", "Datacenter": "<local datacenter>" }
]
}
```
- `BindType=role` - The computed bind name value is used as a `RoleLink.Name`
field in the token that is created. This binding rule will only apply if a
@ -232,7 +243,18 @@ The table below shows this endpoint's support for
]
}
```
- `BindType=node` - The computed bind name value is used as an
`ACLNodeIdentity.NodeName` field in the token that is created.
```json
{ ...other fields...
"NodeIdentities": [
{ "NodeName": "<computed BindName>", "Datacenter": "<local datacenter>" }
]
}
```
- `BindType=role` - The computed bind name value is used as a `RoleLink.Name`
field in the token that is created. This binding rule will only apply if a
role with the given name exists at login-time. If it does not then this
@ -394,6 +416,7 @@ $ curl -X GET http://127.0.0.1:8500/v1/acl/binding-rules
"ID": "b4f0a0a3-69f2-7a4f-6bef-326034ace9fa",
"Description": "example 2",
"AuthMethod": "minikube-2",
"BindType": "service",
"Selector": "serviceaccount.namespace==default",
"BindName": "k8s-{{ serviceaccount.name }}",
"CreateIndex": 18,

View File

@ -62,6 +62,18 @@ The table below shows this endpoint's support for
policy is valid in all datacenters including those which do not yet exist
but may in the future.
- `NodeIdentities` `(array<NodeIdentity>)` - The list of [node
identities](/docs/acl/acl-system#acl-node-identities) that should be
applied to the role. Added in Consul 1.8.1.
- `NodeName` `(string: <required>)` - The name of the node. The name
must be no longer than 256 characters, must start and end with a lowercase
alphanumeric character, and can only contain lowercase alphanumeric
characters as well as `-` and `_`.
- `Datacenter` `(string: <required>)` - Specifies the nodes datacenter. This
will result in effective policy only being valid in that datacenter.
- `Namespace` `(string: "")` <EnterpriseAlert inline /> - Specifies the namespace to
create the role. If not provided in the JSON body, the value of
the `ns` URL query parameter or in the `X-Consul-Namespace` header will be used.
@ -90,6 +102,12 @@ The table below shows this endpoint's support for
"ServiceName": "db",
"Datacenters": ["dc1"]
}
],
"NodeIdentities": [
{
"NodeName": "node-1",
"Datacenter": "dc2"
}
]
}
```
@ -124,6 +142,12 @@ $ curl -X PUT \
"Datacenters": ["dc1"]
}
],
"NodeIdentities": [
{
"NodeName": "node-1",
"Datacenter": "dc2"
}
],
"Hash": "mBWMIeX9zyUTdDMq8vWB0iYod+mKBArJoAhj6oPz3BI=",
"CreateIndex": 57,
"ModifyIndex": 57
@ -188,6 +212,12 @@ $ curl -X GET http://127.0.0.1:8500/v1/acl/role/aa770e5b-8b0b-7fcf-e5a1-8535fcc3
"Datacenters": ["dc1"]
}
],
"NodeIdentities": [
{
"NodeName": "node-1",
"Datacenter": "dc2"
}
],
"Hash": "mBWMIeX9zyUTdDMq8vWB0iYod+mKBArJoAhj6oPz3BI=",
"CreateIndex": 57,
"ModifyIndex": 57
@ -252,6 +282,12 @@ $ curl -X GET http://127.0.0.1:8500/v1/acl/role/name/example-role
"Datacenters": ["dc1"]
}
],
"NodeIdentities": [
{
"NodeName": "node-1",
"Datacenter": "dc2"
}
],
"Hash": "mBWMIeX9zyUTdDMq8vWB0iYod+mKBArJoAhj6oPz3BI=",
"CreateIndex": 57,
"ModifyIndex": 57
@ -299,6 +335,10 @@ The table below shows this endpoint's support for
identities](/docs/acl/acl-system#acl-service-identities) that should be
applied to the role. Added in Consul 1.5.0.
- `NodeIdentities` `(array<NodeIdentity>)` - The list of [node
identities](/docs/acl/acl-system#acl-node-identities) that should be
applied to the role. Added in Consul 1.8.1.
- `Namespace` `(string: "")` <EnterpriseAlert inline /> - Specifies the namespace of
the role to update. If not provided in the JSON body, the value of
the `ns` URL query parameter or in the `X-Consul-Namespace` header will be used.
@ -320,6 +360,12 @@ The table below shows this endpoint's support for
{
"ServiceName": "db"
}
],
"NodeIdentities": [
{
"NodeName": "node-1",
"Datacenter": "dc2"
}
]
}
```
@ -349,6 +395,12 @@ $ curl -X PUT \
"ServiceName": "db"
}
],
"NodeIdentities": [
{
"NodeName": "node-1",
"Datacenter": "dc2"
}
],
"Hash": "OtZUUKhInTLEqTPfNSSOYbRiSBKm3c4vI2p6MxZnGWc=",
"CreateIndex": 14,
"ModifyIndex": 28
@ -475,6 +527,12 @@ $ curl -X GET http://127.0.0.1:8500/v1/acl/roles
"Datacenters": ["dc1"]
}
],
"NodeIdentities": [
{
"NodeName": "node-1",
"Datacenter": "dc2"
}
],
"Hash": "mBWMIeX9zyUTdDMq8vWB0iYod+mKBArJoAhj6oPz3BI=",
"CreateIndex": 57,
"ModifyIndex": 57

View File

@ -74,6 +74,18 @@ The table below shows this endpoint's support for
policy is valid in all datacenters including those which do not yet exist
but may in the future.
- `NodeIdentities` `(array<NodeIdentity>)` - The list of [node
identities](/docs/acl/acl-system#acl-node-identities) that should be
applied to the token. Added in Consul 1.8.1.
- `NodeName` `(string: <required>)` - The name of the node. The name
must be no longer than 256 characters, must start and end with a lowercase
alphanumeric character, and can only contain lowercase alphanumeric
characters as well as `-` and `_`.
- `Datacenter` `(string: <required>)` - Specifies the nodes datacenter. This
will result in effective policy only being valid in that datacenter.
- `Local` `(bool: false)` - If true, indicates that the token should not be
replicated globally and instead be local to the current datacenter.
@ -323,6 +335,18 @@ The table below shows this endpoint's support for
policy is valid in all datacenters including those which do not yet exist
but may in the future.
- `NodeIdentities` `(array<NodeIdentity>)` - The list of [node
identities](/docs/acl/acl-system#acl-node-identities) that should be
applied to the token. Added in Consul 1.8.1.
- `NodeName` `(string: <required>)` - The name of the node. The name
must be no longer than 256 characters, must start and end with a lowercase
alphanumeric character, and can only contain lowercase alphanumeric
characters as well as `-` and `_`.
- `Datacenter` `(string: <required>)` - Specifies the nodes datacenter. This
will result in effective policy only being valid in that datacenter.
- `Local` `(bool: false)` - If true, indicates that this token should not be
replicated globally and instead be local to the current datacenter. This
value must match the existing value or the request will return an error.

View File

@ -45,6 +45,13 @@ may benefit from additional components in the ACL system:
additional policy was attached, the contents of which are described further
below. These are directly attached to tokens and roles and are not
independently configured. (Added in Consul 1.5.0)
- **ACL Node Identities** - Node identities are a policy template for
expressing a link to a policy suitable for use as an [Consul `agent` token
](/docs/agent/options#acl_tokens_agent). At authorization time this acts like an
additional policy was attached, the contents of which are described further
below. These are directly attached to tokens and roles and are not
independently configured. (Added in Consul 1.8.1)
- **ACL Auth Methods and Binding Rules** - To learn more about these topics,
see the [auth methods documentation page](/docs/acl/auth-methods).
@ -123,6 +130,38 @@ examples of using a service identity.
-> **Consul Enterprise Namespacing** - Service Identity rules will be scoped to the single namespace that
the corresponding ACL Token or Role resides within.
### ACL Node Identities
-> Added in Consul 1.8.1
An ACL node identity is an [ACL policy](/docs/acl/acl-system#acl-policies) template for expressing a link to a policy
suitable for use as an [Consul `agent` token](/docs/agent/options#acl_tokens_agent). They are usable
on both tokens and roles and are composed of the following elements:
- **Node Name** - The name of the node to grant access to.
- **Datacenter** - The datacenter that the node resides within.
During the authorization process, the configured node identity is automatically
applied as a policy with the following preconfigured [ACL
rules](/docs/acl/acl-system#acl-rules-and-scope):
```hcl
# Allow the agent to register its own node in the Catalog and update its network coordinates
node "<Node Name>" {
policy = "write"
}
# Allows the agent to detect and diff services registered to itself. This is used during
# anti-entropy to reconcile difference between the agents knowledge of registered
# services and checks in comparison with what is known in the Catalog.
service_prefix "" {
policy = "read"
}
```
-> **Consul Enterprise Namespacing** - Node Identities can only be applied to tokens and roles in the `default` namespace.
The synthetic policy rules allow for `service:read` permissions on all services in all namespaces.
### ACL Roles
-> Added in Consul 1.5.0