mirror of https://github.com/status-im/consul.git
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:
parent
ef37628e97
commit
d3881dd754
|
@ -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)
|
||||
|
|
|
@ -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...)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
10
api/acl.go
10
api/acl.go
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
// {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -41,9 +41,19 @@ func TestRoleCreateCommand_Pretty(t *testing.T) {
|
|||
defer a.Shutdown()
|
||||
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"ID": "bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852",
|
||||
"Name": "basic",
|
||||
"Description": "test role",
|
||||
"Hash": "YWJjZGVmZ2g=",
|
||||
"CreateIndex": 42,
|
||||
"ModifyIndex": 100
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
|
||||
Name: basic
|
||||
Description: test role
|
||||
Hash: 6162636465666768
|
||||
Create Index: 42
|
||||
Modify Index: 100
|
|
@ -0,0 +1,3 @@
|
|||
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
|
||||
Name: basic
|
||||
Description: test role
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,10 @@
|
|||
[
|
||||
{
|
||||
"ID": "bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852",
|
||||
"Name": "basic",
|
||||
"Description": "test role",
|
||||
"Hash": "YWJjZGVmZ2g=",
|
||||
"CreateIndex": 42,
|
||||
"ModifyIndex": 100
|
||||
}
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
basic:
|
||||
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
|
||||
Description: test role
|
||||
Hash: 6162636465666768
|
||||
Create Index: 42
|
||||
Modify Index: 100
|
|
@ -0,0 +1,3 @@
|
|||
basic:
|
||||
ID: bd6c9fb0-2d1a-4b96-acaf-669f5d7e7852
|
||||
Description: test role
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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="
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
|
@ -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\""
|
||||
}
|
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -64,6 +64,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
|
||||
role with the given name exists at login-time. If it does not then this
|
||||
|
@ -233,6 +244,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
|
||||
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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -46,6 +46,13 @@ may benefit from additional components in the ACL system:
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue