Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/18708.txt
Original file line number Diff line number Diff line change
@@ -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.
```
29 changes: 28 additions & 1 deletion agent/consul/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ func (id *missingIdentity) NodeIdentityList() []*structs.ACLNodeIdentity {
return nil
}

func (id *missingIdentity) TemplatedPolicyList() []*structs.ACLTemplatedPolicy {
return nil
}

func (id *missingIdentity) IsExpired(asOf time.Time) bool {
return false
}
Expand Down Expand Up @@ -596,9 +600,11 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
roleIDs = identity.RoleIDs()
serviceIdentities = structs.ACLServiceIdentities(identity.ServiceIdentityList())
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.
return nil, nil
}
Expand All @@ -616,16 +622,19 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
}
serviceIdentities = append(serviceIdentities, role.ServiceIdentities...)
nodeIdentities = append(nodeIdentities, role.NodeIdentityList()...)
templatedPolicies = append(templatedPolicies, role.TemplatedPolicyList()...)
}

// Now deduplicate any policies or service identities that occur more than once.
policyIDs = dedupeStringSlice(policyIDs)
serviceIdentities = serviceIdentities.Deduplicate()
nodeIdentities = nodeIdentities.Deduplicate()
templatedPolicies = templatedPolicies.Deduplicate()

// Generate synthetic policies for all service identities in effect.
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities, 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
// we only attempt to resolve policies locally
Expand Down Expand Up @@ -669,6 +678,24 @@ func (r *ACLResolver) synthesizePoliciesForNodeIdentities(nodeIdentities []*stru
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 {
out := make([]string, 0, len(a)+len(b))
out = append(out, a...)
Expand Down
44 changes: 43 additions & 1 deletion agent/consul/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,9 +350,10 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
policyIDs := make(map[string]struct{})
roleIDs := make(map[string]struct{})
identityPolicies := make(map[string]*structs.ACLPolicy)
templatedPolicies := make(map[string]*structs.ACLPolicy)
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 {
policyIDs[policy.ID] = struct{}{}
}
Expand All @@ -368,6 +369,14 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
policy := identity.SyntheticPolicy(&token.EnterpriseMeta)
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
nsPolicies, nsRoles, err := getTokenNamespaceDefaults(ws, state, &token.EnterpriseMeta)
Expand Down Expand Up @@ -405,6 +414,14 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
policy := identity.SyntheticPolicy(&role.EnterpriseMeta)
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)
}
Expand All @@ -423,6 +440,9 @@ func (a *ACL) lookupExpandedTokenInfo(ws memdb.WatchSet, state *state.Store, tok
for _, policy := range identityPolicies {
policies = append(policies, policy)
}
for _, policy := range templatedPolicies {
policies = append(policies, policy)
}

tokenInfo.ExpandedPolicies = policies
tokenInfo.AgentACLDefaultPolicy = a.srv.config.ACLResolverSettings.ACLDefaultPolicy
Expand Down Expand Up @@ -486,6 +506,7 @@ func (a *ACL) TokenClone(args *structs.ACLTokenSetRequest, reply *structs.ACLTok
Roles: token.Roles,
ServiceIdentities: token.ServiceIdentities,
NodeIdentities: token.NodeIdentities,
TemplatedPolicies: token.TemplatedPolicies,
Local: token.Local,
Description: token.Description,
ExpirationTime: token.ExpirationTime,
Expand Down Expand Up @@ -1364,6 +1385,27 @@ func (a *ACL) RoleSet(args *structs.ACLRoleSetRequest, reply *structs.ACLRole) e
}
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
role.SetHash(true)

Expand Down
79 changes: 78 additions & 1 deletion agent/consul/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/agent/structs"
"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/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
Expand Down Expand Up @@ -376,7 +377,7 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
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{
Datacenter: "dc1",
ACLToken: structs.ACLToken{
Expand All @@ -401,6 +402,22 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
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,
},
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
Expand All @@ -414,6 +431,11 @@ func TestACLEndpoint_TokenRead(t *testing.T) {
for _, serviceIdentity := range setReq.ACLToken.NodeIdentities {
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{}
err := msgpackrpc.CallWithCodec(codec, "ACL.TokenSet", &setReq, &setResp)
Expand Down Expand Up @@ -468,6 +490,10 @@ func TestACLEndpoint_TokenClone(t *testing.T) {
t.NodeIdentities = []*structs.ACLNodeIdentity{
{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)

Expand All @@ -490,6 +516,7 @@ func TestACLEndpoint_TokenClone(t *testing.T) {
require.Equal(t, t1.Roles, t2.Roles)
require.Equal(t, t1.ServiceIdentities, t2.ServiceIdentities)
require.Equal(t, t1.NodeIdentities, t2.NodeIdentities)
require.Equal(t, t1.TemplatedPolicies, t2.TemplatedPolicies)
require.Equal(t, t1.Local, t2.Local)
require.NotEqual(t, t1.AccessorID, t2.AccessorID)
require.NotEqual(t, t1.SecretID, t2.SecretID)
Expand Down Expand Up @@ -548,6 +575,10 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
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},
}
Expand All @@ -570,6 +601,19 @@ func TestACLEndpoint_TokenSet(t *testing.T) {
require.Equal(t, "foo", token.NodeIdentities[0].NodeName)
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
})

Expand Down Expand Up @@ -2183,6 +2227,39 @@ func TestACLEndpoint_PolicySet_CustomID(t *testing.T) {
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) {
if testing.Short() {
t.Skip("too slow for testing.Short")
Expand Down
59 changes: 59 additions & 0 deletions agent/consul/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/hashicorp/consul/acl/resolver"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2043,6 +2086,22 @@ func testACLResolver_variousTokens(t *testing.T, delegate *ACLResolverTestDelega
// ensure node identity for other DC is ignored
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) {
Expand Down
Loading