[NET-5325] ACL templated policies support in tokens and roles (#18708)

* [NET-5325] ACL templated policies support in tokens and roles
- Add API support for creating tokens/roles with templated-policies
- Add CLI support for creating tokens/roles with templated-policies

* adding changelog
This commit is contained in:
Ronald 2023-09-08 08:45:24 -04:00 committed by GitHub
parent 993fe9a6a6
commit bbef879f85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1731 additions and 68 deletions

7
.changelog/18708.txt Normal file
View File

@ -0,0 +1,7 @@
```release-note:feature
acl: Added ACL Templated policies to simplify getting the right ACL token.
```
```release-note:improvement
cli: Added `-templated-policy`, `-templated-policy-file`, `-replace-templated-policy`, `-append-templated-policy`, `-replace-templated-policy-file`, `-append-templated-policy-file` and `-var` flags for creating or updating tokens/roles.
```

View File

@ -102,6 +102,10 @@ func (id *missingIdentity) NodeIdentityList() []*structs.ACLNodeIdentity {
return nil return nil
} }
func (id *missingIdentity) TemplatedPolicyList() []*structs.ACLTemplatedPolicy {
return nil
}
func (id *missingIdentity) IsExpired(asOf time.Time) bool { func (id *missingIdentity) IsExpired(asOf time.Time) bool {
return false return false
} }
@ -596,9 +600,11 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
roleIDs = identity.RoleIDs() roleIDs = identity.RoleIDs()
serviceIdentities = structs.ACLServiceIdentities(identity.ServiceIdentityList()) serviceIdentities = structs.ACLServiceIdentities(identity.ServiceIdentityList())
nodeIdentities = structs.ACLNodeIdentities(identity.NodeIdentityList()) nodeIdentities = structs.ACLNodeIdentities(identity.NodeIdentityList())
templatedPolicies = structs.ACLTemplatedPolicies(identity.TemplatedPolicyList())
) )
if len(policyIDs) == 0 && len(serviceIdentities) == 0 && len(roleIDs) == 0 && len(nodeIdentities) == 0 { if len(policyIDs) == 0 && len(serviceIdentities) == 0 &&
len(roleIDs) == 0 && len(nodeIdentities) == 0 && len(templatedPolicies) == 0 {
// In this case the default policy will be all that is in effect. // In this case the default policy will be all that is in effect.
return nil, nil return nil, nil
} }
@ -616,16 +622,19 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
} }
serviceIdentities = append(serviceIdentities, role.ServiceIdentities...) serviceIdentities = append(serviceIdentities, role.ServiceIdentities...)
nodeIdentities = append(nodeIdentities, role.NodeIdentityList()...) nodeIdentities = append(nodeIdentities, role.NodeIdentityList()...)
templatedPolicies = append(templatedPolicies, role.TemplatedPolicyList()...)
} }
// Now deduplicate any policies or service identities that occur more than once. // Now deduplicate any policies or service identities that occur more than once.
policyIDs = dedupeStringSlice(policyIDs) policyIDs = dedupeStringSlice(policyIDs)
serviceIdentities = serviceIdentities.Deduplicate() serviceIdentities = serviceIdentities.Deduplicate()
nodeIdentities = nodeIdentities.Deduplicate() nodeIdentities = nodeIdentities.Deduplicate()
templatedPolicies = templatedPolicies.Deduplicate()
// Generate synthetic policies for all service identities in effect. // Generate synthetic policies for all service identities in effect.
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, identity.EnterpriseMetadata()) syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, identity.EnterpriseMetadata())
syntheticPolicies = append(syntheticPolicies, r.synthesizePoliciesForNodeIdentities(nodeIdentities, identity.EnterpriseMetadata())...) syntheticPolicies = append(syntheticPolicies, r.synthesizePoliciesForNodeIdentities(nodeIdentities, identity.EnterpriseMetadata())...)
syntheticPolicies = append(syntheticPolicies, r.synthesizePoliciesForTemplatedPolicies(templatedPolicies, identity.EnterpriseMetadata())...)
// For the new ACLs policy replication is mandatory for correct operation on servers. Therefore // For the new ACLs policy replication is mandatory for correct operation on servers. Therefore
// we only attempt to resolve policies locally // we only attempt to resolve policies locally
@ -669,6 +678,24 @@ func (r *ACLResolver) synthesizePoliciesForNodeIdentities(nodeIdentities []*stru
return syntheticPolicies return syntheticPolicies
} }
func (r *ACLResolver) synthesizePoliciesForTemplatedPolicies(templatedPolicies []*structs.ACLTemplatedPolicy, entMeta *acl.EnterpriseMeta) []*structs.ACLPolicy {
if len(templatedPolicies) == 0 {
return nil
}
syntheticPolicies := make([]*structs.ACLPolicy, 0, len(templatedPolicies))
for _, tp := range templatedPolicies {
policy, err := tp.SyntheticPolicy(entMeta)
if err != nil {
r.logger.Warn(fmt.Sprintf("could not generate synthetic policy for templated policy: %q", tp.TemplateName), "error", err)
continue
}
syntheticPolicies = append(syntheticPolicies, policy)
}
return syntheticPolicies
}
func mergeStringSlice(a, b []string) []string { func mergeStringSlice(a, b []string) []string {
out := make([]string, 0, len(a)+len(b)) out := make([]string, 0, len(a)+len(b))
out = append(out, a...) out = append(out, a...)

View File

@ -350,9 +350,10 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
policyIDs := make(map[string]struct{}) policyIDs := make(map[string]struct{})
roleIDs := make(map[string]struct{}) roleIDs := make(map[string]struct{})
identityPolicies := make(map[string]*structs.ACLPolicy) identityPolicies := make(map[string]*structs.ACLPolicy)
templatedPolicies := make(map[string]*structs.ACLPolicy)
tokenInfo := structs.ExpandedTokenInfo{} tokenInfo := structs.ExpandedTokenInfo{}
// Add the token's policies and node/service identity policies // Add the token's policies, templated policies and node/service identity policies
for _, policy := range token.Policies { for _, policy := range token.Policies {
policyIDs[policy.ID] = struct{}{} policyIDs[policy.ID] = struct{}{}
} }
@ -368,6 +369,14 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
policy := identity.SyntheticPolicy(&token.EnterpriseMeta) policy := identity.SyntheticPolicy(&token.EnterpriseMeta)
identityPolicies[policy.ID] = policy identityPolicies[policy.ID] = policy
} }
for _, templatedPolicy := range token.TemplatedPolicies {
policy, err := templatedPolicy.SyntheticPolicy(&token.EnterpriseMeta)
if err != nil {
a.logger.Warn(fmt.Sprintf("could not generate synthetic policy for templated policy: %q", templatedPolicy.TemplateName), "error", err)
continue
}
templatedPolicies[policy.ID] = policy
}
// Get any namespace default roles/policies to look up // Get any namespace default roles/policies to look up
nsPolicies, nsRoles, err := getTokenNamespaceDefaults(ws, state, &token.EnterpriseMeta) nsPolicies, nsRoles, err := getTokenNamespaceDefaults(ws, state, &token.EnterpriseMeta)
@ -405,6 +414,14 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
policy := identity.SyntheticPolicy(&role.EnterpriseMeta) policy := identity.SyntheticPolicy(&role.EnterpriseMeta)
identityPolicies[policy.ID] = policy identityPolicies[policy.ID] = policy
} }
for _, templatedPolicy := range role.TemplatedPolicies {
policy, err := templatedPolicy.SyntheticPolicy(&role.EnterpriseMeta)
if err != nil {
a.logger.Warn(fmt.Sprintf("could not generate synthetic policy for templated policy: %q", templatedPolicy.TemplateName), "error", err)
continue
}
templatedPolicies[policy.ID] = policy
}
tokenInfo.ExpandedRoles = append(tokenInfo.ExpandedRoles, role) tokenInfo.ExpandedRoles = append(tokenInfo.ExpandedRoles, role)
} }
@ -423,6 +440,9 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
for _, policy := range identityPolicies { for _, policy := range identityPolicies {
policies = append(policies, policy) policies = append(policies, policy)
} }
for _, policy := range templatedPolicies {
policies = append(policies, policy)
}
tokenInfo.ExpandedPolicies = policies tokenInfo.ExpandedPolicies = policies
tokenInfo.AgentACLDefaultPolicy = a.srv.config.ACLResolverSettings.ACLDefaultPolicy tokenInfo.AgentACLDefaultPolicy = a.srv.config.ACLResolverSettings.ACLDefaultPolicy
@ -486,6 +506,7 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
Roles: token.Roles, Roles: token.Roles,
ServiceIdentities: token.ServiceIdentities, ServiceIdentities: token.ServiceIdentities,
NodeIdentities: token.NodeIdentities, NodeIdentities: token.NodeIdentities,
TemplatedPolicies: token.TemplatedPolicies,
Local: token.Local, Local: token.Local,
Description: token.Description, Description: token.Description,
ExpirationTime: token.ExpirationTime, ExpirationTime: token.ExpirationTime,
@ -1364,6 +1385,27 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
} }
role.NodeIdentities = role.NodeIdentities.Deduplicate() role.NodeIdentities = role.NodeIdentities.Deduplicate()
for _, templatedPolicy := range role.TemplatedPolicies {
if templatedPolicy.TemplateName == "" {
return fmt.Errorf("templated policy is missing the template name field on this role")
}
baseTemplate, ok := structs.GetACLTemplatedPolicyBase(templatedPolicy.TemplateName)
if !ok {
return fmt.Errorf("templated policy with an invalid templated name: %s for this role", templatedPolicy.TemplateName)
}
if templatedPolicy.TemplateID == "" {
templatedPolicy.TemplateID = baseTemplate.TemplateID
}
err := templatedPolicy.ValidateTemplatedPolicy(baseTemplate.Schema)
if err != nil {
return fmt.Errorf("encountered role with invalid templated policy: %w", err)
}
}
role.TemplatedPolicies = role.TemplatedPolicies.Deduplicate()
// calculate the hash for this role // calculate the hash for this role
role.SetHash(true) role.SetHash(true)

View File

@ -23,6 +23,7 @@ import (
"github.com/hashicorp/consul/agent/consul/authmethod/testauth" "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/structs/aclfilter" "github.com/hashicorp/consul/agent/structs/aclfilter"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest" "github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/sdk/testutil/retry"
@ -376,7 +377,7 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
require.ElementsMatch(t, []*structs.ACLRole{r1, r2}, resp.ExpandedRoles) require.ElementsMatch(t, []*structs.ACLRole{r1, r2}, resp.ExpandedRoles)
}) })
t.Run("expanded output with node/service identities", func(t *testing.T) { t.Run("expanded output with node/service identities and templated policies", func(t *testing.T) {
setReq := structs.ACLTokenSetRequest{ setReq := structs.ACLTokenSetRequest{
Datacenter: "dc1", Datacenter: "dc1",
ACLToken: structs.ACLToken{ ACLToken: structs.ACLToken{
@ -401,6 +402,22 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
Datacenter: "dc1", Datacenter: "dc1",
}, },
}, },
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
Name: "web",
},
Datacenters: []string{"dc1"},
},
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
Name: "foo",
},
Datacenters: []string{"dc1"},
},
},
Local: false, Local: false,
}, },
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken}, WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
@ -414,6 +431,11 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
for _, serviceIdentity := range setReq.ACLToken.NodeIdentities { for _, serviceIdentity := range setReq.ACLToken.NodeIdentities {
expectedPolicies = append(expectedPolicies, serviceIdentity.SyntheticPolicy(entMeta)) expectedPolicies = append(expectedPolicies, serviceIdentity.SyntheticPolicy(entMeta))
} }
for _, templatedPolicy := range setReq.ACLToken.TemplatedPolicies {
pol, tmplError := templatedPolicy.SyntheticPolicy(entMeta)
require.NoError(t, tmplError)
expectedPolicies = append(expectedPolicies, pol)
}
setResp := structs.ACLToken{} setResp := structs.ACLToken{}
err := msgpackrpc.CallWithCodec(codec, "ACL.TokenSet", &setReq, &setResp) err := msgpackrpc.CallWithCodec(codec, "ACL.TokenSet", &setReq, &setResp)
@ -468,6 +490,10 @@ func TestACLEndpoint_TokenClone(t *testing.T) {
t.NodeIdentities = []*structs.ACLNodeIdentity{ t.NodeIdentities = []*structs.ACLNodeIdentity{
{NodeName: "foo", Datacenter: "bar"}, {NodeName: "foo", Datacenter: "bar"},
} }
t.TemplatedPolicies = []*structs.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "foo"}, Datacenters: []string{"bar"}},
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "node"}},
}
}) })
require.NoError(t, err) require.NoError(t, err)
@ -490,6 +516,7 @@ func TestACLEndpoint_TokenClone(t *testing.T) {
require.Equal(t, t1.Roles, t2.Roles) require.Equal(t, t1.Roles, t2.Roles)
require.Equal(t, t1.ServiceIdentities, t2.ServiceIdentities) require.Equal(t, t1.ServiceIdentities, t2.ServiceIdentities)
require.Equal(t, t1.NodeIdentities, t2.NodeIdentities) require.Equal(t, t1.NodeIdentities, t2.NodeIdentities)
require.Equal(t, t1.TemplatedPolicies, t2.TemplatedPolicies)
require.Equal(t, t1.Local, t2.Local) require.Equal(t, t1.Local, t2.Local)
require.NotEqual(t, t1.AccessorID, t2.AccessorID) require.NotEqual(t, t1.AccessorID, t2.AccessorID)
require.NotEqual(t, t1.SecretID, t2.SecretID) require.NotEqual(t, t1.SecretID, t2.SecretID)
@ -548,6 +575,10 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
Datacenter: "dc1", Datacenter: "dc1",
}, },
}, },
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "foo"}, Datacenters: []string{"bar"}},
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "node"}},
},
}, },
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken}, WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
} }
@ -570,6 +601,19 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
require.Equal(t, "foo", token.NodeIdentities[0].NodeName) require.Equal(t, "foo", token.NodeIdentities[0].NodeName)
require.Equal(t, "dc1", token.NodeIdentities[0].Datacenter) require.Equal(t, "dc1", token.NodeIdentities[0].Datacenter)
require.Len(t, token.TemplatedPolicies, 2)
require.Contains(t, token.TemplatedPolicies, &structs.ACLTemplatedPolicy{
TemplateID: structs.ACLTemplatedPolicyServiceID,
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "foo"},
Datacenters: []string{"bar"},
})
require.Contains(t, token.TemplatedPolicies, &structs.ACLTemplatedPolicy{
TemplateID: structs.ACLTemplatedPolicyNodeID,
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "node"},
})
accessorID = token.AccessorID accessorID = token.AccessorID
}) })
@ -2183,6 +2227,39 @@ func TestACLEndpoint_PolicySet_CustomID(t *testing.T) {
require.Error(t, err) require.Error(t, err)
} }
func TestACLEndpoint_TemplatedPolicySet_UnknownTemplateName(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
t.Parallel()
_, srv, _ := testACLServerWithConfig(t, nil, false)
waitForLeaderEstablishment(t, srv)
aclEp := ACL{srv: srv}
t.Run("unknown template name", func(t *testing.T) {
req := structs.ACLTokenSetRequest{
Datacenter: "dc1",
ACLToken: structs.ACLToken{
Description: "foobar",
Policies: nil,
Local: false,
TemplatedPolicies: []*structs.ACLTemplatedPolicy{{TemplateName: "fake-builtin"}},
},
Create: true,
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
}
resp := structs.ACLToken{}
err := aclEp.TokenSet(&req, &resp)
require.Error(t, err)
require.ErrorContains(t, err, "no such ACL templated policy with Name \"fake-builtin\"")
})
}
func TestACLEndpoint_PolicySet_builtins(t *testing.T) { func TestACLEndpoint_PolicySet_builtins(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("too slow for testing.Short") t.Skip("too slow for testing.Short")

View File

@ -21,6 +21,7 @@ import (
"github.com/hashicorp/consul/acl/resolver" "github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry" "github.com/hashicorp/consul/sdk/testutil/retry"
) )
@ -1978,6 +1979,48 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega
}, },
}, },
}, },
&structs.ACLToken{
AccessorID: "359b9927-25fd-46b9-84c2-3470f848ec65",
SecretID: "found-synthetic-policy-5",
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
Name: "templated-test-node1",
},
Datacenters: []string{"dc1"},
},
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
Name: "templated-test-node2",
},
// as the resolver is in dc1 this identity should be ignored
Datacenters: []string{"dc2"},
},
},
},
&structs.ACLToken{
AccessorID: "359b9927-25fd-46b9-84c2-3470f848ec65",
SecretID: "found-synthetic-policy-6",
TemplatedPolicies: []*structs.ACLTemplatedPolicy{
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
Name: "templated-test-node3",
},
Datacenters: []string{"dc1"},
},
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &structs.ACLTemplatedPolicyVariables{
Name: "templated-test-node4",
},
// as the resolver is in dc1 this identity should be ignored
Datacenters: []string{"dc2"},
},
},
},
}) })
// We resolve these tokens in the same cache session // We resolve these tokens in the same cache session
@ -2043,6 +2086,22 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega
// ensure node identity for other DC is ignored // ensure node identity for other DC is ignored
require.Equal(t, acl.Deny, authz.NodeWrite("test-node-dc2", nil)) require.Equal(t, acl.Deny, authz.NodeWrite("test-node-dc2", nil))
}) })
t.Run("synthetic-policy-6", func(t *testing.T) { // templated policy
authz, err := r.ResolveToken("found-synthetic-policy-6")
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("templated-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("templated-test-node3", nil))
// ensure template identity for other DC is ignored
require.Equal(t, acl.Deny, authz.NodeWrite("templated-test-node4", nil))
})
}) })
runTwiceAndReset("Anonymous", func(t *testing.T) { runTwiceAndReset("Anonymous", func(t *testing.T) {

View File

@ -309,6 +309,12 @@ func (w *TokenWriter) write(token, existing *structs.ACLToken, fromLogin bool) (
} }
token.NodeIdentities = nodeIdentities token.NodeIdentities = nodeIdentities
templatedPolicies, err := w.normalizeTemplatedPolicies(token.TemplatedPolicies)
if err != nil {
return nil, err
}
token.TemplatedPolicies = templatedPolicies
if err := w.enterpriseValidation(token, existing); err != nil { if err := w.enterpriseValidation(token, existing); err != nil {
return nil, err return nil, err
} }
@ -442,3 +448,32 @@ func (w *TokenWriter) normalizeNodeIdentities(nodeIDs structs.ACLNodeIdentities)
} }
return nodeIDs.Deduplicate(), nil return nodeIDs.Deduplicate(), nil
} }
func (w *TokenWriter) normalizeTemplatedPolicies(templatedPolicies structs.ACLTemplatedPolicies) (structs.ACLTemplatedPolicies, error) {
if len(templatedPolicies) == 0 {
return templatedPolicies, nil
}
finalPolicies := make(structs.ACLTemplatedPolicies, 0, len(templatedPolicies))
for _, templatedPolicy := range templatedPolicies {
if templatedPolicy.TemplateName == "" {
return nil, errors.New("templated policy is missing the template name field on this token")
}
tmp, ok := structs.GetACLTemplatedPolicyBase(templatedPolicy.TemplateName)
if !ok {
return nil, fmt.Errorf("no such ACL templated policy with Name %q", templatedPolicy.TemplateName)
}
out := templatedPolicy.Clone()
out.TemplateID = tmp.TemplateID
err := templatedPolicy.ValidateTemplatedPolicy(tmp.Schema)
if err != nil {
return nil, fmt.Errorf("validation error for templated policy %q: %w", templatedPolicy.TemplateName, err)
}
finalPolicies = append(finalPolicies, out)
}
return finalPolicies.Deduplicate(), nil
}

View File

@ -15,6 +15,7 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
) )
func TestTokenWriter_Create_Validation(t *testing.T) { func TestTokenWriter_Create_Validation(t *testing.T) {
@ -357,6 +358,59 @@ func TestTokenWriter_NodeIdentities(t *testing.T) {
} }
} }
func TestTokenWriter_TemplatedPolicies(t *testing.T) {
aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)
store := testStateStore(t)
writer := buildTokenWriter(store, aclCache)
testCases := map[string]struct {
input []*structs.ACLTemplatedPolicy
output []*structs.ACLTemplatedPolicy
errorContains string
}{
"missing templated policy name": {
input: []*structs.ACLTemplatedPolicy{{TemplateName: ""}},
errorContains: "templated policy is missing the template name field on this token",
},
"invalid templated policy name": {
input: []*structs.ACLTemplatedPolicy{{TemplateName: "faketemplate"}},
errorContains: "no such ACL templated policy with Name \"faketemplate\"",
},
"missing required template variable: name": {
input: []*structs.ACLTemplatedPolicy{{TemplateName: api.ACLTemplatedPolicyServiceName, Datacenters: []string{"dc1"}}},
errorContains: "validation error for templated policy \"builtin/service\"",
},
"duplicate templated policies are removed and ids are set": {
input: []*structs.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "web"}},
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "web"}},
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "api"}},
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "nodename"}},
},
output: []*structs.ACLTemplatedPolicy{
{TemplateID: structs.ACLTemplatedPolicyServiceID, TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "web"}},
{TemplateID: structs.ACLTemplatedPolicyServiceID, TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "api"}},
{TemplateID: structs.ACLTemplatedPolicyNodeID, TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &structs.ACLTemplatedPolicyVariables{Name: "nodename"}},
},
},
}
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
updated, err := writer.Create(&structs.ACLToken{TemplatedPolicies: tc.input}, false)
if tc.errorContains == "" {
require.NoError(t, err)
require.ElementsMatch(t, tc.output, updated.TemplatedPolicies)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errorContains)
}
})
}
}
func TestTokenWriter_Create_Expiration(t *testing.T) { func TestTokenWriter_Create_Expiration(t *testing.T) {
aclCache := &MockACLCache{} aclCache := &MockACLCache{}
aclCache.On("RemoveIdentityWithSecretToken", mock.Anything) aclCache.On("RemoveIdentityWithSecretToken", mock.Anything)

View File

@ -1177,6 +1177,26 @@ func aclRoleSetTxn(tx WriteTxn, idx uint64, role *structs.ACLRole, allowMissing
} }
} }
for _, templatedPolicy := range role.TemplatedPolicies {
if templatedPolicy.TemplateName == "" {
return fmt.Errorf("encountered a Role %s (%s) with an empty templated policy name in the state store", role.Name, role.ID)
}
baseTemplate, ok := structs.GetACLTemplatedPolicyBase(templatedPolicy.TemplateName)
if !ok {
return fmt.Errorf("encountered a Role %s (%s) with an invalid templated policy name %q", role.Name, role.ID, templatedPolicy.TemplateName)
}
if templatedPolicy.TemplateID == "" {
templatedPolicy.TemplateID = baseTemplate.TemplateID
}
err := templatedPolicy.ValidateTemplatedPolicy(baseTemplate.Schema)
if err != nil {
return fmt.Errorf("encountered a Role %s (%s) with an invalid templated policy: %w", role.Name, role.ID, err)
}
}
if err := aclRoleUpsertValidateEnterprise(tx, role, existing); err != nil { if err := aclRoleUpsertValidateEnterprise(tx, role, existing); err != nil {
return err return err
} }

View File

@ -126,6 +126,7 @@ type ACLIdentity interface {
RoleIDs() []string RoleIDs() []string
ServiceIdentityList() []*ACLServiceIdentity ServiceIdentityList() []*ACLServiceIdentity
NodeIdentityList() []*ACLNodeIdentity NodeIdentityList() []*ACLNodeIdentity
TemplatedPolicyList() []*ACLTemplatedPolicy
IsExpired(asOf time.Time) bool IsExpired(asOf time.Time) bool
IsLocal() bool IsLocal() bool
EnterpriseMetadata() *acl.EnterpriseMeta EnterpriseMetadata() *acl.EnterpriseMeta
@ -314,6 +315,9 @@ type ACLToken struct {
// The node identities that this token should be allowed to manage. // The node identities that this token should be allowed to manage.
NodeIdentities ACLNodeIdentities `json:",omitempty"` NodeIdentities ACLNodeIdentities `json:",omitempty"`
// The templated policies to generate synthetic policies for.
TemplatedPolicies ACLTemplatedPolicies `json:",omitempty"`
// Whether this token is DC local. This means that it will not be synced // Whether this token is DC local. This means that it will not be synced
// to the ACL datacenter and replicated to others. // to the ACL datacenter and replicated to others.
Local bool Local bool
@ -394,6 +398,7 @@ func (t *ACLToken) Clone() *ACLToken {
t2.Roles = nil t2.Roles = nil
t2.ServiceIdentities = nil t2.ServiceIdentities = nil
t2.NodeIdentities = nil t2.NodeIdentities = nil
t2.TemplatedPolicies = nil
if len(t.Policies) > 0 { if len(t.Policies) > 0 {
t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies)) t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies))
@ -415,6 +420,12 @@ func (t *ACLToken) Clone() *ACLToken {
t2.NodeIdentities[i] = n.Clone() t2.NodeIdentities[i] = n.Clone()
} }
} }
if len(t.TemplatedPolicies) > 0 {
t2.TemplatedPolicies = make([]*ACLTemplatedPolicy, len(t.TemplatedPolicies))
for idx, tp := range t.TemplatedPolicies {
t2.TemplatedPolicies[idx] = tp.Clone()
}
}
return &t2 return &t2
} }
@ -523,6 +534,10 @@ func (t *ACLToken) SetHash(force bool) []byte {
nodeID.AddToHash(hash) nodeID.AddToHash(hash)
} }
for _, templatedPolicy := range t.TemplatedPolicies {
templatedPolicy.AddToHash(hash)
}
t.EnterpriseMeta.AddToHash(hash, false) t.EnterpriseMeta.AddToHash(hash, false)
// Finalize the hash // Finalize the hash
@ -549,6 +564,9 @@ func (t *ACLToken) EstimateSize() int {
for _, nodeID := range t.NodeIdentities { for _, nodeID := range t.NodeIdentities {
size += nodeID.EstimateSize() size += nodeID.EstimateSize()
} }
for _, templatedPolicy := range t.TemplatedPolicies {
size += templatedPolicy.EstimateSize()
}
return size + t.EnterpriseMeta.EstimateSize() return size + t.EnterpriseMeta.EstimateSize()
} }
@ -563,6 +581,7 @@ type ACLTokenListStub struct {
Roles []ACLTokenRoleLink `json:",omitempty"` Roles []ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities ACLServiceIdentities `json:",omitempty"` ServiceIdentities ACLServiceIdentities `json:",omitempty"`
NodeIdentities ACLNodeIdentities `json:",omitempty"` NodeIdentities ACLNodeIdentities `json:",omitempty"`
TemplatedPolicies ACLTemplatedPolicies `json:",omitempty"`
Local bool Local bool
AuthMethod string `json:",omitempty"` AuthMethod string `json:",omitempty"`
ExpirationTime *time.Time `json:",omitempty"` ExpirationTime *time.Time `json:",omitempty"`
@ -585,6 +604,7 @@ func (token *ACLToken) Stub() *ACLTokenListStub {
Roles: token.Roles, Roles: token.Roles,
ServiceIdentities: token.ServiceIdentities, ServiceIdentities: token.ServiceIdentities,
NodeIdentities: token.NodeIdentities, NodeIdentities: token.NodeIdentities,
TemplatedPolicies: token.TemplatedPolicies,
Local: token.Local, Local: token.Local,
AuthMethod: token.AuthMethod, AuthMethod: token.AuthMethod,
ExpirationTime: token.ExpirationTime, ExpirationTime: token.ExpirationTime,
@ -870,6 +890,9 @@ type ACLRole struct {
// List of nodes to generate synthetic policies for. // List of nodes to generate synthetic policies for.
NodeIdentities ACLNodeIdentities `json:",omitempty"` NodeIdentities ACLNodeIdentities `json:",omitempty"`
// List of templated policies to generate synthethic policies for.
TemplatedPolicies ACLTemplatedPolicies `json:",omitempty"`
// Hash of the contents of the role // Hash of the contents of the role
// This does not take into account the ID (which is immutable) // This does not take into account the ID (which is immutable)
// nor the raft metadata. // nor the raft metadata.
@ -909,6 +932,7 @@ func (r *ACLRole) Clone() *ACLRole {
r2.Policies = nil r2.Policies = nil
r2.ServiceIdentities = nil r2.ServiceIdentities = nil
r2.NodeIdentities = nil r2.NodeIdentities = nil
r2.TemplatedPolicies = nil
if len(r.Policies) > 0 { if len(r.Policies) > 0 {
r2.Policies = make([]ACLRolePolicyLink, len(r.Policies)) r2.Policies = make([]ACLRolePolicyLink, len(r.Policies))
@ -926,6 +950,12 @@ func (r *ACLRole) Clone() *ACLRole {
r2.NodeIdentities[i] = n.Clone() r2.NodeIdentities[i] = n.Clone()
} }
} }
if len(r.TemplatedPolicies) > 0 {
r2.TemplatedPolicies = make([]*ACLTemplatedPolicy, len(r.TemplatedPolicies))
for i, n := range r.TemplatedPolicies {
r2.TemplatedPolicies[i] = n.Clone()
}
}
return &r2 return &r2
} }
@ -957,6 +987,9 @@ func (r *ACLRole) SetHash(force bool) []byte {
for _, nodeID := range r.NodeIdentities { for _, nodeID := range r.NodeIdentities {
nodeID.AddToHash(hash) nodeID.AddToHash(hash)
} }
for _, templatedPolicy := range r.TemplatedPolicies {
templatedPolicy.AddToHash(hash)
}
r.EnterpriseMeta.AddToHash(hash, false) r.EnterpriseMeta.AddToHash(hash, false)
@ -984,6 +1017,9 @@ func (r *ACLRole) EstimateSize() int {
for _, nodeID := range r.NodeIdentities { for _, nodeID := range r.NodeIdentities {
size += nodeID.EstimateSize() size += nodeID.EstimateSize()
} }
for _, templatedPolicy := range r.TemplatedPolicies {
size += templatedPolicy.EstimateSize()
}
return size + r.EnterpriseMeta.EstimateSize() return size + r.EnterpriseMeta.EstimateSize()
} }
@ -1845,6 +1881,10 @@ func (id *AgentRecoveryTokenIdentity) NodeIdentityList() []*ACLNodeIdentity {
return nil return nil
} }
func (id *AgentRecoveryTokenIdentity) TemplatedPolicyList() []*ACLTemplatedPolicy {
return nil
}
func (id *AgentRecoveryTokenIdentity) IsExpired(asOf time.Time) bool { func (id *AgentRecoveryTokenIdentity) IsExpired(asOf time.Time) bool {
return false return false
} }
@ -1893,6 +1933,10 @@ func (i *ACLServerIdentity) NodeIdentityList() []*ACLNodeIdentity {
return nil return nil
} }
func (i *ACLServerIdentity) TemplatedPolicyList() []*ACLTemplatedPolicy {
return nil
}
func (i *ACLServerIdentity) IsExpired(asOf time.Time) bool { func (i *ACLServerIdentity) IsExpired(asOf time.Time) bool {
return false return false
} }

View File

@ -0,0 +1,269 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"bytes"
"fmt"
"hash"
"hash/fnv"
"html/template"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib/stringslice"
"github.com/hashicorp/go-multierror"
"github.com/xeipuuv/gojsonschema"
"golang.org/x/exp/slices"
)
type ACLTemplatedPolicies []*ACLTemplatedPolicy
const (
ACLTemplatedPolicyNodeID = "00000000-0000-0000-0000-000000000004"
ACLTemplatedPolicyServiceID = "00000000-0000-0000-0000-000000000003"
ACLTemplatedPolicyIdentitiesSchema = `{
"type": "object",
"properties": {
"name": { "type": "string", "$ref": "#/definitions/min-length-one" }
},
"required": ["name"],
"definitions": {
"min-length-one": {
"type": "string",
"minLength": 1
}
}
}`
ACLTemplatedPolicyDNSID = "00000000-0000-0000-0000-000000000005"
ACLTemplatedPolicyDNSSchema = "" // empty schema as it does not require variables
)
// ACLTemplatedPolicyBase contains basic information about builtin templated policies
// template name, id, template code and schema
type ACLTemplatedPolicyBase struct {
TemplateName string
TemplateID string
Schema string
Template string
}
var (
// TODO(Ronald): add other templates
// This supports: node, service and dns templates
aclTemplatedPoliciesList = map[string]*ACLTemplatedPolicyBase{
api.ACLTemplatedPolicyServiceName: {
TemplateID: ACLTemplatedPolicyServiceID,
TemplateName: api.ACLTemplatedPolicyServiceName,
Schema: ACLTemplatedPolicyIdentitiesSchema,
Template: ACLTemplatedPolicyService,
},
api.ACLTemplatedPolicyNodeName: {
TemplateID: ACLTemplatedPolicyNodeID,
TemplateName: api.ACLTemplatedPolicyNodeName,
Schema: ACLTemplatedPolicyIdentitiesSchema,
Template: ACLTemplatedPolicyNode,
},
api.ACLTemplatedPolicyDNSName: {
TemplateID: ACLTemplatedPolicyDNSID,
TemplateName: api.ACLTemplatedPolicyDNSName,
Schema: ACLTemplatedPolicyDNSSchema,
Template: ACLTemplatedPolicyDNS,
},
}
)
// ACLTemplatedPolicy represents a template used to generate a `synthetic` policy
// given some input variables.
type ACLTemplatedPolicy struct {
// TemplateID are hidden from all displays and should not be exposed to the users.
TemplateID string `json:",omitempty"`
// TemplateName is used for display purposes mostly and should not be used for policy rendering.
TemplateName string `json:",omitempty"`
// TemplateVariables are input variables required to render templated policies.
TemplateVariables *ACLTemplatedPolicyVariables `json:",omitempty"`
// Datacenters that the synthetic policy will be valid within.
// - No wildcards allowed
// - If empty then the synthetic policy is valid within all datacenters
//
// This is kept for legacy reasons to enable us to replace Node/Service Identities by templated policies.
//
// Only valid for global tokens. It is an error to specify this for local tokens.
Datacenters []string `json:",omitempty"`
}
// ACLTemplatedPolicyVariables are input variables required to render templated policies.
type ACLTemplatedPolicyVariables struct {
Name string `json:"name,omitempty"`
}
func (tp *ACLTemplatedPolicy) Clone() *ACLTemplatedPolicy {
tp2 := *tp
tp2.TemplateVariables = nil
if tp.TemplateVariables != nil {
tp2.TemplateVariables = tp.TemplateVariables.Clone()
}
tp2.Datacenters = stringslice.CloneStringSlice(tp.Datacenters)
return &tp2
}
func (tp *ACLTemplatedPolicy) AddToHash(h hash.Hash) {
h.Write([]byte(tp.TemplateID))
h.Write([]byte(tp.TemplateName))
if tp.TemplateVariables != nil {
tp.TemplateVariables.AddToHash(h)
}
for _, dc := range tp.Datacenters {
h.Write([]byte(dc))
}
}
func (tv *ACLTemplatedPolicyVariables) AddToHash(h hash.Hash) {
h.Write([]byte(tv.Name))
}
func (tv *ACLTemplatedPolicyVariables) Clone() *ACLTemplatedPolicyVariables {
tv2 := *tv
return &tv2
}
// validates templated policy variables against schema.
func (tp *ACLTemplatedPolicy) ValidateTemplatedPolicy(schema string) error {
if schema == "" {
return nil
}
loader := gojsonschema.NewStringLoader(schema)
dataloader := gojsonschema.NewGoLoader(tp.TemplateVariables)
res, err := gojsonschema.Validate(loader, dataloader)
if err != nil {
return fmt.Errorf("failed to load json schema for validation %w", err)
}
if res.Valid() {
return nil
}
var merr *multierror.Error
for _, resultError := range res.Errors() {
merr = multierror.Append(merr, fmt.Errorf(resultError.Description()))
}
return merr.ErrorOrNil()
}
func (tp *ACLTemplatedPolicy) EstimateSize() int {
size := len(tp.TemplateName) + len(tp.TemplateID) + tp.TemplateVariables.EstimateSize()
for _, dc := range tp.Datacenters {
size += len(dc)
}
return size
}
func (tv *ACLTemplatedPolicyVariables) EstimateSize() int {
return len(tv.Name)
}
// SyntheticPolicy generates a policy based on templated policies' ID and variables
//
// Given that we validate this string name before persisting, we do not
// have to escape it before doing the following interpolation.
func (tp *ACLTemplatedPolicy) SyntheticPolicy(entMeta *acl.EnterpriseMeta) (*ACLPolicy, error) {
rules, err := tp.aclTemplatedPolicyRules(entMeta)
if err != nil {
return nil, err
}
hasher := fnv.New128a()
hashID := fmt.Sprintf("%x", hasher.Sum([]byte(rules)))
policy := &ACLPolicy{
Rules: rules,
ID: hashID,
Name: fmt.Sprintf("synthetic-policy-%s", hashID),
Datacenters: tp.Datacenters,
Description: fmt.Sprintf("synthetic policy generated from templated policy: %s", tp.TemplateName),
}
policy.EnterpriseMeta.Merge(entMeta)
policy.SetHash(true)
return policy, nil
}
func (tp *ACLTemplatedPolicy) aclTemplatedPolicyRules(entMeta *acl.EnterpriseMeta) (string, error) {
if entMeta == nil {
entMeta = DefaultEnterpriseMetaInDefaultPartition()
}
entMeta.Normalize()
tpl := template.New(tp.TemplateName)
tmplCode, ok := aclTemplatedPoliciesList[tp.TemplateName]
if !ok {
return "", fmt.Errorf("acl templated policy does not exist: %s", tp.TemplateName)
}
parsedTpl, err := tpl.Parse(tmplCode.Template)
if err != nil {
return "", fmt.Errorf("an error occured when parsing template structs: %w", err)
}
var buf bytes.Buffer
err = parsedTpl.Execute(&buf, struct {
*ACLTemplatedPolicyVariables
Namespace string
Partition string
}{
Namespace: entMeta.NamespaceOrDefault(),
Partition: entMeta.PartitionOrDefault(),
ACLTemplatedPolicyVariables: tp.TemplateVariables,
})
if err != nil {
return "", fmt.Errorf("an error occured when executing on templated policy variables: %w", err)
}
return buf.String(), nil
}
// Deduplicate returns a new list of templated policies without duplicates.
// compares values of template variables to ensure no duplicates
func (tps ACLTemplatedPolicies) Deduplicate() ACLTemplatedPolicies {
list := make(map[string][]ACLTemplatedPolicyVariables)
var out ACLTemplatedPolicies
for _, tp := range tps {
// checks if template name already in the unique list
_, found := list[tp.TemplateName]
if !found {
list[tp.TemplateName] = make([]ACLTemplatedPolicyVariables, 0)
}
templateSchema := aclTemplatedPoliciesList[tp.TemplateName].Schema
// if schema is empty, template does not require variables
if templateSchema == "" {
if !found {
out = append(out, tp)
}
continue
}
if !slices.Contains(list[tp.TemplateName], *tp.TemplateVariables) {
list[tp.TemplateName] = append(list[tp.TemplateName], *tp.TemplateVariables)
out = append(out, tp)
}
}
return out
}
func GetACLTemplatedPolicyBase(templateName string) (*ACLTemplatedPolicyBase, bool) {
baseTemplate, found := aclTemplatedPoliciesList[templateName]
return baseTemplate, found
}

View File

@ -0,0 +1,65 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package structs
const (
ACLTemplatedPolicyService = `
service "{{.Name}}" {
policy = "write"
}
service "{{.Name}}-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}`
ACLTemplatedPolicyNode = `
node "{{.Name}}" {
policy = "write"
}
service_prefix "" {
policy = "read"
}`
ACLTemplatedPolicyDNS = `
node_prefix "" {
policy = "read"
}
service_prefix "" {
policy = "read"
}
query_prefix "" {
policy = "read"
}`
)
func (t *ACLToken) TemplatedPolicyList() []*ACLTemplatedPolicy {
if len(t.TemplatedPolicies) == 0 {
return nil
}
out := make([]*ACLTemplatedPolicy, 0, len(t.TemplatedPolicies))
for _, n := range t.TemplatedPolicies {
out = append(out, n.Clone())
}
return out
}
func (t *ACLRole) TemplatedPolicyList() []*ACLTemplatedPolicy {
if len(t.TemplatedPolicies) == 0 {
return nil
}
out := make([]*ACLTemplatedPolicy, 0, len(t.TemplatedPolicies))
for _, n := range t.TemplatedPolicies {
out = append(out, n.Clone())
}
return out
}

View File

@ -0,0 +1,99 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !consulent
package structs
import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
)
func TestStructs_ACLTemplatedPolicy_SyntheticPolicy(t *testing.T) {
type testCase struct {
templatedPolicy *ACLTemplatedPolicy
expectedPolicy *ACLPolicy
}
testCases := map[string]testCase{
"service-identity-template": {
templatedPolicy: &ACLTemplatedPolicy{
TemplateID: ACLTemplatedPolicyServiceID,
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
expectedPolicy: &ACLPolicy{
Description: "synthetic policy generated from templated policy: builtin/service",
Rules: `
service "api" {
policy = "write"
}
service "api-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}`,
},
},
"node-identity-template": {
templatedPolicy: &ACLTemplatedPolicy{
TemplateID: ACLTemplatedPolicyNodeID,
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "web",
},
},
expectedPolicy: &ACLPolicy{
Description: "synthetic policy generated from templated policy: builtin/node",
Rules: `
node "web" {
policy = "write"
}
service_prefix "" {
policy = "read"
}`,
},
},
"dns-template": {
templatedPolicy: &ACLTemplatedPolicy{
TemplateID: ACLTemplatedPolicyDNSID,
TemplateName: api.ACLTemplatedPolicyDNSName,
},
expectedPolicy: &ACLPolicy{
Description: "synthetic policy generated from templated policy: builtin/dns",
Rules: `
node_prefix "" {
policy = "read"
}
service_prefix "" {
policy = "read"
}
query_prefix "" {
policy = "read"
}`,
},
},
}
for name, tcase := range testCases {
t.Run(name, func(t *testing.T) {
policy, err := tcase.templatedPolicy.SyntheticPolicy(nil)
require.NoError(t, err)
require.Equal(t, tcase.expectedPolicy.Description, policy.Description)
require.Equal(t, tcase.expectedPolicy.Rules, policy.Rules)
require.Contains(t, policy.Name, "synthetic-policy-")
require.NotEmpty(t, policy.Hash)
require.NotEmpty(t, policy.ID)
})
}
}

View File

@ -0,0 +1,103 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package structs
import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
)
func TestDeduplicate(t *testing.T) {
type testCase struct {
templatedPolicies ACLTemplatedPolicies
expectedCount int
}
tcases := map[string]testCase{
"multiple-of-the-same-template": {
templatedPolicies: ACLTemplatedPolicies{
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
},
expectedCount: 1,
},
"separate-templates-with-matching-variables": {
templatedPolicies: ACLTemplatedPolicies{
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
},
expectedCount: 2,
},
"separate-templates-with-multiple-matching-variables": {
templatedPolicies: ACLTemplatedPolicies{
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "web",
},
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyDNSName,
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "web",
},
},
&ACLTemplatedPolicy{
TemplateName: api.ACLTemplatedPolicyDNSName,
},
},
expectedCount: 5,
},
}
for name, tcase := range tcases {
t.Run(name, func(t *testing.T) {
policies := tcase.templatedPolicies.Deduplicate()
require.Equal(t, tcase.expectedCount, len(policies))
})
}
}

View File

@ -19,6 +19,11 @@ const (
// ACLManagementType is the management type token // ACLManagementType is the management type token
ACLManagementType = "management" ACLManagementType = "management"
// ACLTemplatedPolicy names
ACLTemplatedPolicyServiceName = "builtin/service"
ACLTemplatedPolicyNodeName = "builtin/node"
ACLTemplatedPolicyDNSName = "builtin/dns"
) )
type ACLLink struct { type ACLLink struct {
@ -40,6 +45,7 @@ type ACLToken struct {
Roles []*ACLTokenRoleLink `json:",omitempty"` Roles []*ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"` NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
TemplatedPolicies []*ACLTemplatedPolicy `json:",omitempty"`
Local bool Local bool
AuthMethod string `json:",omitempty"` AuthMethod string `json:",omitempty"`
ExpirationTTL time.Duration `json:",omitempty"` ExpirationTTL time.Duration `json:",omitempty"`
@ -88,6 +94,7 @@ type ACLTokenListEntry struct {
Roles []*ACLTokenRoleLink `json:",omitempty"` Roles []*ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"` NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
TemplatedPolicies []*ACLTemplatedPolicy `json:",omitempty"`
Local bool Local bool
AuthMethod string `json:",omitempty"` AuthMethod string `json:",omitempty"`
ExpirationTime *time.Time `json:",omitempty"` ExpirationTime *time.Time `json:",omitempty"`
@ -148,6 +155,21 @@ type ACLNodeIdentity struct {
Datacenter string Datacenter string
} }
// ACLTemplatedPolicy represents a template used to generate a `synthetic` policy
// given some input variables.
type ACLTemplatedPolicy struct {
TemplateName string
TemplateVariables *ACLTemplatedPolicyVariables `json:",omitempty"`
// Datacenters are an artifact of Nodeidentity & ServiceIdentity.
// It is used to facilitate the future migration away from both
Datacenters []string `json:",omitempty"`
}
type ACLTemplatedPolicyVariables struct {
Name string
}
// ACLPolicy represents an ACL Policy. // ACLPolicy represents an ACL Policy.
type ACLPolicy struct { type ACLPolicy struct {
ID string ID string
@ -196,6 +218,7 @@ type ACLRole struct {
Policies []*ACLRolePolicyLink `json:",omitempty"` Policies []*ACLRolePolicyLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"` ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
NodeIdentities []*ACLNodeIdentity `json:",omitempty"` NodeIdentities []*ACLNodeIdentity `json:",omitempty"`
TemplatedPolicies []*ACLTemplatedPolicy `json:",omitempty"`
Hash []byte Hash []byte
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64

View File

@ -10,6 +10,9 @@ import (
"github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/helpers"
"github.com/hashicorp/hcl"
"github.com/mitchellh/mapstructure"
) )
func GetTokenAccessorIDFromPartial(client *api.Client, partialAccessorID string) (string, error) { func GetTokenAccessorIDFromPartial(client *api.Client, partialAccessorID string) (string, error) {
@ -218,6 +221,70 @@ func ExtractNodeIdentities(nodeIdents []string) ([]*api.ACLNodeIdentity, error)
return out, nil return out, nil
} }
func ExtractTemplatedPolicies(templatedPolicy string, templatedPolicyFile string, templatedPolicyVariables []string) ([]*api.ACLTemplatedPolicy, error) {
var out []*api.ACLTemplatedPolicy
if templatedPolicy == "" && templatedPolicyFile == "" {
return out, nil
}
if templatedPolicy != "" {
parsedVariables, err := getTemplatedPolicyVariables(templatedPolicyVariables)
if err != nil {
return nil, err
}
out = append(out, &api.ACLTemplatedPolicy{
TemplateName: templatedPolicy,
TemplateVariables: parsedVariables,
})
}
if templatedPolicyFile != "" {
fileData, err := helpers.LoadFromFile(templatedPolicyFile)
if err != nil {
return nil, err
}
var config map[string]map[string][]api.ACLTemplatedPolicyVariables
err = hcl.Decode(&config, fileData)
if err != nil {
return nil, err
}
for templateName, templateVariables := range config["TemplatedPolicy"] {
for _, tp := range templateVariables {
out = append(out, &api.ACLTemplatedPolicy{
TemplateName: templateName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{
Name: tp.Name,
},
})
}
}
}
return out, nil
}
func getTemplatedPolicyVariables(variables []string) (*api.ACLTemplatedPolicyVariables, error) {
if len(variables) == 0 {
return nil, nil
}
out := &api.ACLTemplatedPolicyVariables{}
jsonVariables := make(map[string]string)
for _, variable := range variables {
parts := strings.Split(variable, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("malformed -var argument: %q, expecting VariableName:Value", variable)
}
jsonVariables[parts[0]] = parts[1]
}
err := mapstructure.Decode(jsonVariables, out)
return out, err
}
// TestKubernetesJWT_A is a valid service account jwt extracted from a minikube setup. // TestKubernetesJWT_A is a valid service account jwt extracted from a minikube setup.
// //
// { // {

View File

@ -71,7 +71,7 @@ func (c *cmd) Run(args []string) int {
} }
if c.name == "" { if c.name == "" {
c.UI.Error(fmt.Sprintf("Missing require '-name' flag")) c.UI.Error(fmt.Sprintf("Missing required '-name' flag"))
c.UI.Error(c.Help()) c.UI.Error(c.Help())
return 1 return 1
} }

View File

@ -28,12 +28,15 @@ type cmd struct {
http *flags.HTTPFlags http *flags.HTTPFlags
help string help string
name string name string
description string description string
policyIDs []string policyIDs []string
policyNames []string policyNames []string
serviceIdents []string serviceIdents []string
nodeIdents []string nodeIdents []string
templatedPolicy string
templatedPolicyFile string
templatedPolicyVariables []string
showMeta bool showMeta bool
format string format string
@ -55,6 +58,12 @@ func (c *cmd) init() {
c.flags.Var((*flags.AppendSliceValue)(&c.nodeIdents), "node-identity", "Name of a "+ 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 "+ "node identity to use for this role. May be specified multiple times. Format is "+
"NODENAME:DATACENTER") "NODENAME:DATACENTER")
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
" Must be used in combination with -templated-policy flag to specify required variables."+
" May be specified multiple times with different variables."+
" Format is VariableName:Value")
c.flags.StringVar(&c.templatedPolicy, "templated-policy", "", "The templated policy name. Use -var flag to specify variables when required.")
c.flags.StringVar(&c.templatedPolicyFile, "templated-policy-file", "", "Path to a file containing templated policy names and variables.")
c.flags.StringVar( c.flags.StringVar(
&c.format, &c.format,
"format", "format",
@ -74,13 +83,14 @@ func (c *cmd) Run(args []string) int {
} }
if c.name == "" { if c.name == "" {
c.UI.Error(fmt.Sprintf("Missing require '-name' flag")) c.UI.Error("Missing required '-name' flag")
c.UI.Error(c.Help()) c.UI.Error(c.Help())
return 1 return 1
} }
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 { 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")) len(c.templatedPolicy) == 0 && len(c.templatedPolicyFile) == 0 {
c.UI.Error("Cannot create a role without specifying -policy-name, -policy-id, -service-identity, -node-identity, -templated-policy-file or -templated-policy at least once")
return 1 return 1
} }
@ -124,6 +134,13 @@ func (c *cmd) Run(args []string) int {
} }
newRole.NodeIdentities = parsedNodeIdents newRole.NodeIdentities = parsedNodeIdents
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.templatedPolicy, c.templatedPolicyFile, c.templatedPolicyVariables)
if err != nil {
c.UI.Error(err.Error())
return 1
}
newRole.TemplatedPolicies = parsedTemplatedPolicies
r, _, err := client.ACL().RoleCreate(newRole, nil) r, _, err := client.ACL().RoleCreate(newRole, nil)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create new role: %v", err)) c.UI.Error(fmt.Sprintf("Failed to create new role: %v", err))
@ -167,5 +184,7 @@ Usage: consul acl role create -name NAME [options]
-policy-id b52fc3de-5 \ -policy-id b52fc3de-5 \
-policy-name "acl-replication" \ -policy-name "acl-replication" \
-service-identity "web" \ -service-identity "web" \
-service-identity "db:east,west" -service-identity "db:east,west" \
-templated-policy "builtin/service" \
-var "name:api"
` `

View File

@ -89,6 +89,20 @@ func (f *prettyFormatter) FormatRole(role *api.ACLRole) (string, error) {
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter)) buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
} }
} }
if len(role.TemplatedPolicies) > 0 {
buffer.WriteString(fmt.Sprintln("Templated Policies:"))
for _, templatedPolicy := range role.TemplatedPolicies {
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
}
if len(templatedPolicy.Datacenters) > 0 {
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
} else {
buffer.WriteString(" Datacenters: all\n")
}
}
}
return buffer.String(), nil return buffer.String(), nil
} }
@ -144,6 +158,21 @@ func (f *prettyFormatter) formatRoleListEntry(role *api.ACLRole) string {
} }
} }
if len(role.TemplatedPolicies) > 0 {
buffer.WriteString(fmt.Sprintln(" Templated Policies:"))
for _, templatedPolicy := range role.TemplatedPolicies {
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
}
if len(templatedPolicy.Datacenters) > 0 {
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
} else {
buffer.WriteString(" Datacenters: all\n")
}
}
}
return buffer.String() return buffer.String()
} }

View File

@ -83,6 +83,14 @@ func TestFormatRole(t *testing.T) {
Datacenter: "middleearth-northwest", Datacenter: "middleearth-northwest",
}, },
}, },
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "gardener"},
Datacenters: []string{"middleearth-northwest", "somewhere-east"},
},
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "bagend"}},
},
}, },
}, },
} }
@ -114,7 +122,7 @@ func TestFormatRole(t *testing.T) {
} }
} }
func TestFormatTokenList(t *testing.T) { func TestFormatRoleList(t *testing.T) {
type testCase struct { type testCase struct {
roles []*api.ACLRole roles []*api.ACLRole
overrideGoldenName string overrideGoldenName string
@ -165,6 +173,10 @@ func TestFormatTokenList(t *testing.T) {
Datacenter: "middleearth-northwest", Datacenter: "middleearth-northwest",
}, },
}, },
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "gardener"}},
{TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "bagend"}},
},
}, },
}, },
}, },

View File

@ -26,6 +26,24 @@
"Datacenter": "middleearth-northwest" "Datacenter": "middleearth-northwest"
} }
], ],
"TemplatedPolicies": [
{
"TemplateName": "builtin/service",
"TemplateVariables": {
"Name": "gardener"
},
"Datacenters": [
"middleearth-northwest",
"somewhere-east"
]
},
{
"TemplateName": "builtin/node",
"TemplateVariables": {
"Name": "bagend"
}
}
],
"Hash": "YWJjZGVmZ2g=", "Hash": "YWJjZGVmZ2g=",
"CreateIndex": 5, "CreateIndex": 5,
"ModifyIndex": 10, "ModifyIndex": 10,

View File

@ -12,3 +12,10 @@ Service Identities:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: gardener
Datacenters: middleearth-northwest, somewhere-east
builtin/node
Name: bagend
Datacenters: all

View File

@ -9,3 +9,10 @@ Service Identities:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: gardener
Datacenters: middleearth-northwest, somewhere-east
builtin/node
Name: bagend
Datacenters: all

View File

@ -27,6 +27,20 @@
"Datacenter": "middleearth-northwest" "Datacenter": "middleearth-northwest"
} }
], ],
"TemplatedPolicies": [
{
"TemplateName": "builtin/service",
"TemplateVariables": {
"Name": "gardener"
}
},
{
"TemplateName": "builtin/node",
"TemplateVariables": {
"Name": "bagend"
}
}
],
"Hash": "YWJjZGVmZ2g=", "Hash": "YWJjZGVmZ2g=",
"CreateIndex": 5, "CreateIndex": 5,
"ModifyIndex": 10, "ModifyIndex": 10,

View File

@ -12,3 +12,10 @@ complex:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: gardener
Datacenters: all
builtin/node
Name: bagend
Datacenters: all

View File

@ -9,3 +9,10 @@ complex:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: gardener
Datacenters: all
builtin/node
Name: bagend
Datacenters: all

View File

@ -28,13 +28,18 @@ type cmd struct {
http *flags.HTTPFlags http *flags.HTTPFlags
help string help string
roleID string roleID string
name string name string
description string description string
policyIDs []string policyIDs []string
policyNames []string policyNames []string
serviceIdents []string serviceIdents []string
nodeIdents []string nodeIdents []string
appendTemplatedPolicy string
replaceTemplatedPolicy string
appendTemplatedPolicyFile string
replaceTemplatedPolicyFile string
templatedPolicyVariables []string
noMerge bool noMerge bool
showMeta bool showMeta bool
@ -69,6 +74,16 @@ func (c *cmd) init() {
role.PrettyFormat, role.PrettyFormat,
fmt.Sprintf("Output format {%s}", strings.Join(role.GetSupportedFormats(), "|")), fmt.Sprintf("Output format {%s}", strings.Join(role.GetSupportedFormats(), "|")),
) )
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
" Must be used in combination with -append-templated-policy or -replace-templated-policy flags to specify required variables."+
" May be specified multiple times with different variables."+
" Format is VariableName:Value")
c.flags.StringVar(&c.appendTemplatedPolicy, "append-templated-policy", "", "The templated policy name to attach to the role's existing templated policies list. Use -var flag to specify variables when required."+
" The role retains existing templated policies.")
c.flags.StringVar(&c.replaceTemplatedPolicy, "replace-templated-policy", "", "The templated policy name to replace the existing templated policies list with. Use -var flag to specify variables when required."+
" Overwrites the role's existing templated policies.")
c.flags.StringVar(&c.appendTemplatedPolicyFile, "append-templated-policy-file", "", "Path to a file containing templated policies and variables. Works like `-append-templated-policy`. The role retains existing templated policies.")
c.flags.StringVar(&c.replaceTemplatedPolicyFile, "replace-templated-policy-file", "", "Path to a file containing templated policies and variables. Works like `-replace-templated-policy`. Overwrites the role's existing templated policies.")
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ClientFlags())
@ -111,6 +126,24 @@ func (c *cmd) Run(args []string) int {
return 1 return 1
} }
hasAppendTemplatedPolicies := len(c.appendTemplatedPolicy) > 0 || len(c.appendTemplatedPolicyFile) > 0
hasReplaceTemplatedPolicies := len(c.replaceTemplatedPolicy) > 0 || len(c.replaceTemplatedPolicyFile) > 0
if hasReplaceTemplatedPolicies && hasAppendTemplatedPolicies {
c.UI.Error("Cannot combine the use of append-templated-policy flags with replace-templated-policy. " +
"To set or overwrite existing templated policies, use -replace-templated-policy or -replace-templated-policy-file. " +
"To append to existing templated policies, use -append-templated-policy or -append-templated-policy-file.")
return 1
}
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.replaceTemplatedPolicy, c.replaceTemplatedPolicyFile, c.templatedPolicyVariables)
if hasAppendTemplatedPolicies {
parsedTemplatedPolicies, err = acl.ExtractTemplatedPolicies(c.appendTemplatedPolicy, c.appendTemplatedPolicyFile, c.templatedPolicyVariables)
}
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. // Read the current role in both cases so we can fail better if not found.
currentRole, _, err := client.ACL().RoleRead(roleID, nil) currentRole, _, err := client.ACL().RoleRead(roleID, nil)
if err != nil { if err != nil {
@ -129,6 +162,7 @@ func (c *cmd) Run(args []string) int {
Description: c.description, Description: c.description,
ServiceIdentities: parsedServiceIdents, ServiceIdentities: parsedServiceIdents,
NodeIdentities: parsedNodeIdents, NodeIdentities: parsedNodeIdents,
TemplatedPolicies: parsedTemplatedPolicies,
} }
for _, policyName := range c.policyNames { for _, policyName := range c.policyNames {
@ -221,6 +255,12 @@ func (c *cmd) Run(args []string) int {
r.NodeIdentities = append(r.NodeIdentities, nodeid) r.NodeIdentities = append(r.NodeIdentities, nodeid)
} }
} }
if hasReplaceTemplatedPolicies {
r.TemplatedPolicies = parsedTemplatedPolicies
} else {
r.TemplatedPolicies = append(r.TemplatedPolicies, parsedTemplatedPolicies...)
}
} }
r, _, err = client.ACL().RoleUpdate(r, nil) r, _, err = client.ACL().RoleUpdate(r, nil)
@ -273,6 +313,8 @@ Usage: consul acl role update [options]
-name "better-name" \ -name "better-name" \
-description "replication" \ -description "replication" \
-policy-name "token-replication" \ -policy-name "token-replication" \
-service-identity "web" -service-identity "web" \
-templated-policy "builtin/service" \
-var "name:api"
` `
) )

View File

@ -68,6 +68,9 @@ func TestRoleUpdateCommand(t *testing.T) {
ServiceName: "fake", ServiceName: "fake",
}, },
}, },
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "fake"}},
},
}, },
&api.WriteOptions{Token: "root"}, &api.WriteOptions{Token: "root"},
) )
@ -122,6 +125,7 @@ func TestRoleUpdateCommand(t *testing.T) {
require.Equal(t, "test role edited", role.Description) require.Equal(t, "test role edited", role.Description)
require.Len(t, role.Policies, 1) require.Len(t, role.Policies, 1)
require.Len(t, role.ServiceIdentities, 1) require.Len(t, role.ServiceIdentities, 1)
require.Len(t, role.TemplatedPolicies, 1)
}) })
t.Run("update with policy by id", func(t *testing.T) { t.Run("update with policy by id", func(t *testing.T) {
@ -198,6 +202,47 @@ func TestRoleUpdateCommand(t *testing.T) {
require.Len(t, role.ServiceIdentities, 3) require.Len(t, role.ServiceIdentities, 3)
require.Len(t, role.NodeIdentities, 1) require.Len(t, role.NodeIdentities, 1)
}) })
t.Run("update with append templated policies", func(t *testing.T) {
_ = run(t, []string{
"-id=" + role.ID,
"-token=root",
"-append-templated-policy=builtin/service",
"-var=name:api",
})
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)
require.Len(t, role.TemplatedPolicies, 2)
})
t.Run("update with replace templated policies", func(t *testing.T) {
_ = run(t, []string{
"-id=" + role.ID,
"-token=root",
"-replace-templated-policy=builtin/service",
"-var=name:api",
})
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)
require.Len(t, role.TemplatedPolicies, 1)
})
} }
func TestRoleUpdateCommand_JSON(t *testing.T) { func TestRoleUpdateCommand_JSON(t *testing.T) {
@ -335,6 +380,9 @@ func TestRoleUpdateCommand_noMerge(t *testing.T) {
ServiceName: "fake", ServiceName: "fake",
}, },
}, },
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "fake"}},
},
Policies: []*api.ACLRolePolicyLink{ Policies: []*api.ACLRolePolicyLink{
{ {
ID: policy3.ID, ID: policy3.ID,
@ -482,4 +530,64 @@ func TestRoleUpdateCommand_noMerge(t *testing.T) {
require.Len(t, role.Policies, 0) require.Len(t, role.Policies, 0)
require.Len(t, role.ServiceIdentities, 1) require.Len(t, role.ServiceIdentities, 1)
}) })
t.Run("update with templated policy append", func(t *testing.T) {
role := createRole(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-name=" + role.Name,
"-token=root",
"-no-merge",
"-append-templated-policy=builtin/service",
"-var=name:api",
}
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,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "", role.Description)
require.Len(t, role.Policies, 0)
require.Len(t, role.TemplatedPolicies, 1)
})
t.Run("update with replace templated policy", func(t *testing.T) {
role := createRole(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-name=" + role.Name,
"-token=root",
"-no-merge",
"-replace-templated-policy=builtin/service",
"-var=name:api",
}
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,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "", role.Description)
require.Len(t, role.Policies, 0)
require.Len(t, role.TemplatedPolicies, 1)
})
} }

View File

@ -165,6 +165,7 @@ func TestTokenCloneCommand_Pretty(t *testing.T) {
require.Equal(t, cloned.Description, apiToken.Description) require.Equal(t, cloned.Description, apiToken.Description)
require.Equal(t, cloned.Local, apiToken.Local) require.Equal(t, cloned.Local, apiToken.Local)
require.Equal(t, cloned.Policies, apiToken.Policies) require.Equal(t, cloned.Policies, apiToken.Policies)
require.Equal(t, cloned.TemplatedPolicies, apiToken.TemplatedPolicies)
}) })
} }
@ -198,7 +199,13 @@ func TestTokenCloneCommand_JSON(t *testing.T) {
// create a token // create a token
token, _, err := client.ACL().TokenCreate( token, _, err := client.ACL().TokenCreate(
&api.ACLToken{Description: "test", Policies: []*api.ACLTokenPolicyLink{{Name: "test-policy"}}}, &api.ACLToken{
Description: "test",
Policies: []*api.ACLTokenPolicyLink{{Name: "test-policy"}},
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "web"}},
},
},
&api.WriteOptions{Token: "root"}, &api.WriteOptions{Token: "root"},
) )
require.NoError(t, err) require.NoError(t, err)

View File

@ -29,19 +29,22 @@ type cmd struct {
http *flags.HTTPFlags http *flags.HTTPFlags
help string help string
accessor string accessor string
secret string secret string
policyIDs []string policyIDs []string
policyNames []string policyNames []string
description string description string
roleIDs []string roleIDs []string
roleNames []string roleNames []string
serviceIdents []string serviceIdents []string
nodeIdents []string nodeIdents []string
expirationTTL time.Duration templatedPolicy string
local bool templatedPolicyFile string
showMeta bool templatedPolicyVariables []string
format string expirationTTL time.Duration
local bool
showMeta bool
format string
} }
func (c *cmd) init() { func (c *cmd) init() {
@ -76,6 +79,12 @@ func (c *cmd) init() {
token.PrettyFormat, token.PrettyFormat,
fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")), fmt.Sprintf("Output format {%s}", strings.Join(token.GetSupportedFormats(), "|")),
) )
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
" Must be used in combination with -templated-policy flag to specify required variables."+
" May be specified multiple times with different variables."+
" Format is VariableName:Value")
c.flags.StringVar(&c.templatedPolicy, "templated-policy", "", "The templated policy name. Use -var flag to specify variables when required.")
c.flags.StringVar(&c.templatedPolicyFile, "templated-policy-file", "", "Path to a file containing templated policies and variables.")
c.http = &flags.HTTPFlags{} c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags()) flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags()) flags.Merge(c.flags, c.http.ServerFlags())
@ -90,8 +99,9 @@ func (c *cmd) Run(args []string) int {
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && if len(c.policyNames) == 0 && len(c.policyIDs) == 0 &&
len(c.roleNames) == 0 && len(c.roleIDs) == 0 && len(c.roleNames) == 0 && len(c.roleIDs) == 0 &&
len(c.serviceIdents) == 0 && len(c.nodeIdents) == 0 { 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")) len(c.templatedPolicy) == 0 && len(c.templatedPolicyFile) == 0 {
c.UI.Error("Cannot create a token without specifying -policy-name, -policy-id, -role-name, -role-id, -service-identity, -node-identity, -templated-policy, or -templated-policy-file at least once")
return 1 return 1
} }
@ -125,6 +135,13 @@ func (c *cmd) Run(args []string) int {
} }
newToken.NodeIdentities = parsedNodeIdents newToken.NodeIdentities = parsedNodeIdents
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.templatedPolicy, c.templatedPolicyFile, c.templatedPolicyVariables)
if err != nil {
c.UI.Error(err.Error())
return 1
}
newToken.TemplatedPolicies = parsedTemplatedPolicies
for _, policyName := range c.policyNames { for _, policyName := range c.policyNames {
// We could resolve names to IDs here but there isn't any reason why its would be better // We could resolve names to IDs here but there isn't any reason why its would be better
// than allowing the agent to do it. // than allowing the agent to do it.
@ -203,6 +220,8 @@ Usage: consul acl token create [options]
-role-id c630d4ef-6 \ -role-id c630d4ef-6 \
-role-name "db-updater" \ -role-name "db-updater" \
-service-identity "web" \ -service-identity "web" \
-service-identity "db:east,west" -service-identity "db:east,west" \
-templated-policy "builtin/service" \
-var "name:web"
` `
) )

View File

@ -107,6 +107,27 @@ func TestTokenCreateCommand_Pretty(t *testing.T) {
require.Equal(t, a.Config.NodeName, nodes[0].Node) require.Equal(t, a.Config.NodeName, nodes[0].Node)
}) })
// templated policy
t.Run("templated-policy", func(t *testing.T) {
token := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-templated-policy=builtin/node",
"-var=name:" + a.Config.NodeName,
})
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 // create with accessor and secret
t.Run("predefined-ids", func(t *testing.T) { t.Run("predefined-ids", func(t *testing.T) {
token := run(t, []string{ token := run(t, []string{

View File

@ -109,6 +109,20 @@ func (f *prettyFormatter) FormatToken(token *api.ACLToken) (string, error) {
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter)) buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
} }
} }
if len(token.TemplatedPolicies) > 0 {
buffer.WriteString(fmt.Sprintln("Templated Policies:"))
for _, templatedPolicy := range token.TemplatedPolicies {
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
}
if len(templatedPolicy.Datacenters) > 0 {
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
} else {
buffer.WriteString(" Datacenters: all\n")
}
}
}
return buffer.String(), nil return buffer.String(), nil
} }
@ -174,10 +188,7 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
} }
identity := structs.ACLServiceIdentity{ServiceName: svcIdentity.ServiceName, Datacenters: svcIdentity.Datacenters} identity := structs.ACLServiceIdentity{ServiceName: svcIdentity.ServiceName, Datacenters: svcIdentity.Datacenters}
policy := identity.SyntheticPolicy(&entMeta) policy := identity.SyntheticPolicy(&entMeta)
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Description: %s\n", policy.Description)) displaySyntheticPolicy(policy, &buffer, indent)
buffer.WriteString(indent + WHITESPACE_2 + "Rules:")
buffer.WriteString(strings.ReplaceAll(policy.Rules, "\n", "\n"+indent+WHITESPACE_4))
buffer.WriteString("\n\n")
} }
if len(token.ACLToken.ServiceIdentities) > 0 { if len(token.ACLToken.ServiceIdentities) > 0 {
buffer.WriteString("Service Identities:\n") buffer.WriteString("Service Identities:\n")
@ -190,10 +201,7 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
buffer.WriteString(fmt.Sprintf(indent+"Name: %s (Datacenter: %s)\n", nodeIdentity.NodeName, nodeIdentity.Datacenter)) buffer.WriteString(fmt.Sprintf(indent+"Name: %s (Datacenter: %s)\n", nodeIdentity.NodeName, nodeIdentity.Datacenter))
identity := structs.ACLNodeIdentity{NodeName: nodeIdentity.NodeName, Datacenter: nodeIdentity.Datacenter} identity := structs.ACLNodeIdentity{NodeName: nodeIdentity.NodeName, Datacenter: nodeIdentity.Datacenter}
policy := identity.SyntheticPolicy(&entMeta) policy := identity.SyntheticPolicy(&entMeta)
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Description: %s\n", policy.Description)) displaySyntheticPolicy(policy, &buffer, indent)
buffer.WriteString(indent + WHITESPACE_2 + "Rules:")
buffer.WriteString(strings.ReplaceAll(policy.Rules, "\n", "\n"+indent+WHITESPACE_4))
buffer.WriteString("\n\n")
} }
if len(token.ACLToken.NodeIdentities) > 0 { if len(token.ACLToken.NodeIdentities) > 0 {
buffer.WriteString("Node Identities:\n") buffer.WriteString("Node Identities:\n")
@ -202,6 +210,34 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
} }
} }
formatTemplatedPolicy := func(templatedPolicy *api.ACLTemplatedPolicy, indent string) {
buffer.WriteString(fmt.Sprintf(indent+"%s\n", templatedPolicy.TemplateName))
tp := structs.ACLTemplatedPolicy{
TemplateName: templatedPolicy.TemplateName,
Datacenters: templatedPolicy.Datacenters,
}
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
tp.TemplateVariables = &structs.ACLTemplatedPolicyVariables{
Name: templatedPolicy.TemplateVariables.Name,
}
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Name: %s\n", templatedPolicy.TemplateVariables.Name))
}
if len(templatedPolicy.Datacenters) > 0 {
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
} else {
buffer.WriteString(fmt.Sprintf(indent + WHITESPACE_2 + "Datacenters: all\n"))
}
policy, _ := tp.SyntheticPolicy(&entMeta)
displaySyntheticPolicy(policy, &buffer, indent)
}
if len(token.ACLToken.TemplatedPolicies) > 0 {
buffer.WriteString("Templated Policies:\n")
for _, templatedPolicy := range token.ACLToken.TemplatedPolicies {
formatTemplatedPolicy(templatedPolicy, WHITESPACE_2)
}
}
formatRole := func(role api.ACLRole, indent string) { formatRole := func(role api.ACLRole, indent string) {
buffer.WriteString(fmt.Sprintf(indent+"Role Name: %s\n", role.Name)) buffer.WriteString(fmt.Sprintf(indent+"Role Name: %s\n", role.Name))
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"ID: %s\n", role.ID)) buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"ID: %s\n", role.ID))
@ -266,6 +302,13 @@ func (f *prettyFormatter) FormatTokenExpanded(token *api.ACLTokenExpanded) (stri
return buffer.String(), nil return buffer.String(), nil
} }
func displaySyntheticPolicy(policy *structs.ACLPolicy, buffer *bytes.Buffer, indent string) {
buffer.WriteString(fmt.Sprintf(indent+WHITESPACE_2+"Description: %s\n", policy.Description))
buffer.WriteString(indent + WHITESPACE_2 + "Rules:")
buffer.WriteString(strings.ReplaceAll(policy.Rules, "\n", "\n"+indent+WHITESPACE_4))
buffer.WriteString("\n\n")
}
func (f *prettyFormatter) FormatTokenList(tokens []*api.ACLTokenListEntry) (string, error) { func (f *prettyFormatter) FormatTokenList(tokens []*api.ACLTokenListEntry) (string, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
@ -335,6 +378,21 @@ func (f *prettyFormatter) formatTokenListEntry(token *api.ACLTokenListEntry) str
buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter)) buffer.WriteString(fmt.Sprintf(" %s (Datacenter: %s)\n", nodeid.NodeName, nodeid.Datacenter))
} }
} }
if len(token.TemplatedPolicies) > 0 {
buffer.WriteString(fmt.Sprintln("Templated Policies:"))
for _, templatedPolicy := range token.TemplatedPolicies {
buffer.WriteString(fmt.Sprintf(" %s\n", templatedPolicy.TemplateName))
if templatedPolicy.TemplateVariables != nil && templatedPolicy.TemplateVariables.Name != "" {
buffer.WriteString(fmt.Sprintf(" Name: %s\n", templatedPolicy.TemplateVariables.Name))
}
if len(templatedPolicy.Datacenters) > 0 {
buffer.WriteString(fmt.Sprintf(" Datacenters: %s\n", strings.Join(templatedPolicy.Datacenters, ", ")))
} else {
buffer.WriteString(" Datacenters: all\n")
}
}
}
return buffer.String() return buffer.String()
} }

View File

@ -106,6 +106,21 @@ func TestFormatToken(t *testing.T) {
Datacenter: "middleearth-northwest", Datacenter: "middleearth-northwest",
}, },
}, },
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{
Name: "web",
},
Datacenters: []string{"middleearth-northwest", "somewhere-east"},
},
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{
Name: "api",
},
},
},
}, },
}, },
} }
@ -209,6 +224,21 @@ func TestFormatTokenList(t *testing.T) {
Datacenter: "middleearth-northwest", Datacenter: "middleearth-northwest",
}, },
}, },
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{
Name: "web",
},
Datacenters: []string{"middleearth-northwest"},
},
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{
Name: "api",
},
},
},
}, },
}, },
}, },
@ -444,6 +474,21 @@ var expandedTokenTestCases = map[string]testCase{
Datacenter: "middleearth-northwest", Datacenter: "middleearth-northwest",
}, },
}, },
TemplatedPolicies: []*api.ACLTemplatedPolicy{
{
TemplateName: api.ACLTemplatedPolicyServiceName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{
Name: "web",
},
Datacenters: []string{"middleearth-northwest"},
},
{
TemplateName: api.ACLTemplatedPolicyNodeName,
TemplateVariables: &api.ACLTemplatedPolicyVariables{
Name: "api",
},
},
},
}, },
}, },
}, },

View File

@ -38,6 +38,24 @@
"Datacenter": "middleearth-northwest" "Datacenter": "middleearth-northwest"
} }
], ],
"TemplatedPolicies": [
{
"TemplateName": "builtin/service",
"TemplateVariables": {
"Name": "web"
},
"Datacenters": [
"middleearth-northwest",
"somewhere-east"
]
},
{
"TemplateName": "builtin/node",
"TemplateVariables": {
"Name": "api"
}
}
],
"Local": false, "Local": false,
"AuthMethod": "bar", "AuthMethod": "bar",
"ExpirationTime": "2020-05-22T19:52:31Z", "ExpirationTime": "2020-05-22T19:52:31Z",

View File

@ -19,3 +19,10 @@ Service Identities:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: web
Datacenters: middleearth-northwest, somewhere-east
builtin/node
Name: api
Datacenters: all

View File

@ -16,3 +16,10 @@ Service Identities:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: web
Datacenters: middleearth-northwest, somewhere-east
builtin/node
Name: api
Datacenters: all

View File

@ -181,6 +181,23 @@
"Datacenter": "middleearth-northwest" "Datacenter": "middleearth-northwest"
} }
], ],
"TemplatedPolicies": [
{
"TemplateName": "builtin/service",
"TemplateVariables": {
"Name": "web"
},
"Datacenters": [
"middleearth-northwest"
]
},
{
"TemplateName": "builtin/node",
"TemplateVariables": {
"Name": "api"
}
}
],
"Local": false, "Local": false,
"AuthMethod": "bar", "AuthMethod": "bar",
"ExpirationTime": "2020-05-22T19:52:31Z", "ExpirationTime": "2020-05-22T19:52:31Z",

View File

@ -52,6 +52,37 @@ Node Identities:
policy = "read" policy = "read"
} }
Templated Policies:
builtin/service
Name: web
Datacenters: middleearth-northwest
Description: synthetic policy generated from templated policy: builtin/service
Rules:
service "web" {
policy = "write"
}
service "web-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}
builtin/node
Name: api
Datacenters: all
Description: synthetic policy generated from templated policy: builtin/node
Rules:
node "api" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
Roles: Roles:
Role Name: shire Role Name: shire
ID: 3b0a78fe-b9c3-40de-b8ea-7d4d6674b366 ID: 3b0a78fe-b9c3-40de-b8ea-7d4d6674b366

View File

@ -49,6 +49,37 @@ Node Identities:
policy = "read" policy = "read"
} }
Templated Policies:
builtin/service
Name: web
Datacenters: middleearth-northwest
Description: synthetic policy generated from templated policy: builtin/service
Rules:
service "web" {
policy = "write"
}
service "web-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}
builtin/node
Name: api
Datacenters: all
Description: synthetic policy generated from templated policy: builtin/node
Rules:
node "api" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
Roles: Roles:
Role Name: shire Role Name: shire
ID: 3b0a78fe-b9c3-40de-b8ea-7d4d6674b366 ID: 3b0a78fe-b9c3-40de-b8ea-7d4d6674b366

View File

@ -39,6 +39,23 @@
"Datacenter": "middleearth-northwest" "Datacenter": "middleearth-northwest"
} }
], ],
"TemplatedPolicies": [
{
"TemplateName": "builtin/service",
"TemplateVariables": {
"Name": "web"
},
"Datacenters": [
"middleearth-northwest"
]
},
{
"TemplateName": "builtin/node",
"TemplateVariables": {
"Name": "api"
}
}
],
"Local": false, "Local": false,
"AuthMethod": "bar", "AuthMethod": "bar",
"ExpirationTime": "2020-05-22T19:52:31Z", "ExpirationTime": "2020-05-22T19:52:31Z",

View File

@ -19,3 +19,10 @@ Service Identities:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: web
Datacenters: middleearth-northwest
builtin/node
Name: api
Datacenters: all

View File

@ -16,3 +16,10 @@ Service Identities:
gardener (Datacenters: middleearth-northwest) gardener (Datacenters: middleearth-northwest)
Node Identities: Node Identities:
bagend (Datacenter: middleearth-northwest) bagend (Datacenter: middleearth-northwest)
Templated Policies:
builtin/service
Name: web
Datacenters: middleearth-northwest
builtin/node
Name: api
Datacenters: all

View File

@ -28,22 +28,27 @@ type cmd struct {
http *flags.HTTPFlags http *flags.HTTPFlags
help string help string
tokenAccessorID string tokenAccessorID string
policyIDs []string policyIDs []string
appendPolicyIDs []string appendPolicyIDs []string
policyNames []string policyNames []string
appendPolicyNames []string appendPolicyNames []string
roleIDs []string roleIDs []string
appendRoleIDs []string appendRoleIDs []string
roleNames []string roleNames []string
appendRoleNames []string appendRoleNames []string
serviceIdents []string serviceIdents []string
nodeIdents []string nodeIdents []string
appendNodeIdents []string appendNodeIdents []string
appendServiceIdents []string appendServiceIdents []string
description string appendTemplatedPolicy string
showMeta bool replaceTemplatedPolicy string
format string appendTemplatedPolicyFile string
replaceTemplatedPolicyFile string
templatedPolicyVariables []string
description string
showMeta bool
format string
// DEPRECATED // DEPRECATED
mergeServiceIdents bool mergeServiceIdents bool
@ -89,6 +94,19 @@ func (c *cmd) init() {
c.flags.Var((*flags.AppendSliceValue)(&c.appendNodeIdents), "append-node-identity", "Name of a "+ c.flags.Var((*flags.AppendSliceValue)(&c.appendNodeIdents), "append-node-identity", "Name of a "+
"node identity to use for this token. This token retains existing node identities. May be "+ "node identity to use for this token. This token retains existing node identities. May be "+
"specified multiple times. Format is NODENAME:DATACENTER") "specified multiple times. Format is NODENAME:DATACENTER")
c.flags.Var((*flags.AppendSliceValue)(&c.templatedPolicyVariables), "var", "Templated policy variables."+
" Must be used in combination with -replace-templated-policy or -append-templated-policy flags to specify required variables."+
" May be specified multiple times with different variables."+
" Format is VariableName:Value")
c.flags.StringVar(&c.appendTemplatedPolicy, "append-templated-policy", "", "The templated policy name to attach to the token's existing templated policies."+
"Use -var flag to specify variables when required. Token retains existing templated policies.")
c.flags.StringVar(&c.replaceTemplatedPolicy, "replace-templated-policy", "", "The templated policy name to use replace token's existing templated policies."+
" Use -var flag to specify variables when required. Overwrites token's existing templated policies.")
c.flags.StringVar(&c.appendTemplatedPolicyFile, "append-templated-policy-file", "", "Path to a file containing templated policy names and variables."+
" Works similarly to `-append-templated-policy`. The token retains existing templated policies.")
c.flags.StringVar(&c.replaceTemplatedPolicyFile, "replace-templated-policy-file", "", "Path to a file containing templated policy names and variables."+
" Works similarly to `-replace-templated-policy`. Overwrites the token's existing templated policies.")
c.flags.StringVar( c.flags.StringVar(
&c.format, &c.format,
"format", "format",
@ -195,6 +213,24 @@ func (c *cmd) Run(args []string) int {
return 1 return 1
} }
hasAppendTemplatedPolicies := len(c.appendTemplatedPolicy) > 0 || len(c.appendTemplatedPolicyFile) > 0
hasReplaceTemplatedPolicies := len(c.replaceTemplatedPolicy) > 0 || len(c.replaceTemplatedPolicyFile) > 0
if hasReplaceTemplatedPolicies && hasAppendTemplatedPolicies {
c.UI.Error("Cannot combine the use of -append-templated-policy flags with replace-templated-policy. " +
"To set or overwrite existing templated policies, use -replace-templated-policy or -replace-templated-policy-file. " +
"To append to existing templated policies, use -append-templated-policy or -append-templated-policy-file.")
return 1
}
parsedTemplatedPolicies, err := acl.ExtractTemplatedPolicies(c.replaceTemplatedPolicy, c.replaceTemplatedPolicyFile, c.templatedPolicyVariables)
if hasAppendTemplatedPolicies {
parsedTemplatedPolicies, err = acl.ExtractTemplatedPolicies(c.appendTemplatedPolicy, c.appendTemplatedPolicyFile, c.templatedPolicyVariables)
}
if err != nil {
c.UI.Error(err.Error())
return 1
}
if c.mergePolicies { if c.mergePolicies {
c.UI.Warn("merge-policies is deprecated and will be removed in a future Consul version. " + c.UI.Warn("merge-policies is deprecated and will be removed in a future Consul version. " +
"Use `append-policy-name` or `append-policy-id` instead.") "Use `append-policy-name` or `append-policy-id` instead.")
@ -384,6 +420,12 @@ func (c *cmd) Run(args []string) int {
t.NodeIdentities = parsedNodeIdents t.NodeIdentities = parsedNodeIdents
} }
if hasReplaceTemplatedPolicies {
t.TemplatedPolicies = parsedTemplatedPolicies
} else {
t.TemplatedPolicies = append(t.TemplatedPolicies, parsedTemplatedPolicies...)
}
t, _, err = client.ACL().TokenUpdate(t, nil) t, _, err = client.ACL().TokenUpdate(t, nil)
if err != nil { if err != nil {
c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tok, err)) c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tok, err))
@ -432,6 +474,8 @@ Usage: consul acl token update [options]
$ consul acl token update -accessor-id abcd \ $ consul acl token update -accessor-id abcd \
-description "replication" \ -description "replication" \
-policy-name "token-replication" \ -policy-name "token-replication" \
-role-name "db-updater" -role-name "db-updater" \
-templated-policy "builtin/service" \
-var "name:web"
` `
) )

View File

@ -120,6 +120,51 @@ func TestTokenUpdateCommand(t *testing.T) {
require.ElementsMatch(t, expected, responseToken.NodeIdentities) require.ElementsMatch(t, expected, responseToken.NodeIdentities)
}) })
t.Run("replace-templated-policy", func(t *testing.T) {
token := create_token(
t,
client, &api.ACLToken{Description: "test", TemplatedPolicies: []*api.ACLTemplatedPolicy{
{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "api"}},
}},
&api.WriteOptions{Token: "root"},
)
responseToken := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-accessor-id=" + token.AccessorID,
"-token=root",
"-replace-templated-policy=builtin/node",
"-description=test token",
"-var=name:web",
})
require.Len(t, responseToken.TemplatedPolicies, 1)
require.Equal(t, api.ACLTemplatedPolicyNodeName, responseToken.TemplatedPolicies[0].TemplateName)
require.Equal(t, "web", responseToken.TemplatedPolicies[0].TemplateVariables.Name)
})
t.Run("append-templated-policy", func(t *testing.T) {
templatedPolicy := &api.ACLTemplatedPolicy{TemplateName: api.ACLTemplatedPolicyServiceName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "api"}}
token := create_token(
t,
client, &api.ACLToken{Description: "test", TemplatedPolicies: []*api.ACLTemplatedPolicy{
templatedPolicy,
}},
&api.WriteOptions{Token: "root"},
)
responseToken := run(t, []string{
"-http-addr=" + a.HTTPAddr(),
"-accessor-id=" + token.AccessorID,
"-token=root",
"-append-templated-policy=builtin/node",
"-description=test token",
"-var=name:web",
})
require.Len(t, responseToken.TemplatedPolicies, 2)
require.ElementsMatch(t, responseToken.TemplatedPolicies,
[]*api.ACLTemplatedPolicy{templatedPolicy, {TemplateName: api.ACLTemplatedPolicyNodeName, TemplateVariables: &api.ACLTemplatedPolicyVariables{Name: "web"}}})
})
// update with policy by name // update with policy by name
t.Run("policy-name", func(t *testing.T) { t.Run("policy-name", func(t *testing.T) {
token := create_token(t, client, &api.ACLToken{Description: "test"}, &api.WriteOptions{Token: "root"}) token := create_token(t, client, &api.ACLToken{Description: "test"}, &api.WriteOptions{Token: "root"})

View File

@ -18,7 +18,7 @@ import (
"github.com/hashicorp/go-multierror" "github.com/hashicorp/go-multierror"
) )
func loadFromFile(path string) (string, error) { func LoadFromFile(path string) (string, error) {
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to read file: %v", err) return "", fmt.Errorf("Failed to read file: %v", err)
@ -47,7 +47,7 @@ func LoadDataSource(data string, testStdin io.Reader) (string, error) {
switch data[0] { switch data[0] {
case '@': case '@':
return loadFromFile(data[1:]) return LoadFromFile(data[1:])
case '-': case '-':
if len(data) > 1 { if len(data) > 1 {
return data, nil return data, nil
@ -67,7 +67,7 @@ func LoadDataSourceNoRaw(data string, testStdin io.Reader) (string, error) {
return loadFromStdin(testStdin) return loadFromStdin(testStdin)
} }
return loadFromFile(data) return LoadFromFile(data)
} }
func ParseConfigEntry(data string) (api.ConfigEntry, error) { func ParseConfigEntry(data string) (api.ConfigEntry, error) {

2
go.mod
View File

@ -97,6 +97,7 @@ require (
github.com/ryanuber/columnize v2.1.2+incompatible github.com/ryanuber/columnize v2.1.2+incompatible
github.com/shirou/gopsutil/v3 v3.22.8 github.com/shirou/gopsutil/v3 v3.22.8
github.com/stretchr/testify v1.8.3 github.com/stretchr/testify v1.8.3
github.com/xeipuuv/gojsonschema v1.2.0
github.com/zclconf/go-cty v1.2.0 github.com/zclconf/go-cty v1.2.0
go.etcd.io/bbolt v1.3.7 go.etcd.io/bbolt v1.3.7
go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel v1.16.0
@ -245,6 +246,7 @@ require (
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
github.com/vmware/govmomi v0.18.0 // indirect github.com/vmware/govmomi v0.18.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
go.mongodb.org/mongo-driver v1.11.0 // indirect go.mongodb.org/mongo-driver v1.11.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect

5
go.sum
View File

@ -937,8 +937,13 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=

View File

@ -190,6 +190,8 @@ require (
github.com/testcontainers/testcontainers-go v0.22.0 // indirect github.com/testcontainers/testcontainers-go v0.22.0 // indirect
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/zclconf/go-cty v1.12.1 // indirect github.com/zclconf/go-cty v1.12.1 // indirect
go.etcd.io/bbolt v1.3.7 // indirect go.etcd.io/bbolt v1.3.7 // indirect
go.mongodb.org/mongo-driver v1.11.0 // indirect go.mongodb.org/mongo-driver v1.11.0 // indirect

View File

@ -774,8 +774,13 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

View File

@ -186,6 +186,8 @@ require (
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
go.etcd.io/bbolt v1.3.7 // indirect go.etcd.io/bbolt v1.3.7 // indirect
go.mongodb.org/mongo-driver v1.11.0 // indirect go.mongodb.org/mongo-driver v1.11.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect

View File

@ -763,8 +763,13 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=