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
3 changes: 3 additions & 0 deletions .changelog/19077.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
acl: Adds workload identity templated policy
```
2 changes: 1 addition & 1 deletion agent/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1374,7 +1374,7 @@ func TestACL_HTTP(t *testing.T) {

var list map[string]api.ACLTemplatedPolicyResponse
require.NoError(t, json.NewDecoder(resp.Body).Decode(&list))
require.Len(t, list, 4)
require.Len(t, list, 5)

require.Equal(t, api.ACLTemplatedPolicyResponse{
TemplateName: api.ACLTemplatedPolicyServiceName,
Expand Down
15 changes: 2 additions & 13 deletions agent/consul/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -1723,19 +1723,8 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
return fmt.Errorf("invalid Binding Rule: BindVars cannot be set when bind type is not templated-policy.")
}

switch rule.BindType {
case structs.BindingRuleBindTypeService:
case structs.BindingRuleBindTypeNode:
case structs.BindingRuleBindTypeRole:
case structs.BindingRuleBindTypeTemplatedPolicy:
default:
return fmt.Errorf("Invalid Binding Rule: unknown BindType %q", rule.BindType)
}

if valid, err := auth.IsValidBindNameOrBindVars(rule.BindType, rule.BindName, rule.BindVars, blankID.ProjectedVarNames()); err != nil {
return fmt.Errorf("Invalid Binding Rule: invalid BindName or BindVars: %v", err)
} else if !valid {
return fmt.Errorf("Invalid Binding Rule: invalid BindName or BindVars")
if err := auth.IsValidBindingRule(rule.BindType, rule.BindName, rule.BindVars, blankID.ProjectedVarNames()); err != nil {
return fmt.Errorf("Invalid Binding Rule: invalid BindName or BindVars: %w", err)
}

req := &structs.ACLBindingRuleBatchSetRequest{
Expand Down
128 changes: 70 additions & 58 deletions agent/consul/auth/binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package auth

import (
"errors"
"fmt"

"github.com/hashicorp/go-bexpr"
Expand Down Expand Up @@ -37,8 +38,8 @@ type BinderStateStore interface {
ACLRoleGetByName(ws memdb.WatchSet, roleName string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLRole, error)
}

// Bindings contains the ACL roles, service identities, node identities and
// enterprise meta to be assigned to the created token.
// Bindings contains the ACL roles, service identities, node identities,
// templated policies, and enterprise meta to be assigned to the created token.
type Bindings struct {
Roles []structs.ACLTokenRoleLink
ServiceIdentities []*structs.ACLServiceIdentity
Expand Down Expand Up @@ -91,30 +92,39 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, verifiedIdentity *authm
// Compute role, service identity, node identity or templated policy names by interpolating
// the identity's projected variables into the rule BindName templates.
for _, rule := range matchingRules {
bindName, templatedPolicy, valid, err := computeBindNameAndVars(rule.BindType, rule.BindName, rule.BindVars, verifiedIdentity.ProjectedVars)
switch {
case err != nil:
return nil, fmt.Errorf("cannot compute %q bind name for bind target: %w", rule.BindType, err)
case !valid:
return nil, fmt.Errorf("computed %q bind name for bind target is invalid: %q", rule.BindType, bindName)
}

switch rule.BindType {
case structs.BindingRuleBindTypeService:
bindName, err := computeBindName(rule.BindName, verifiedIdentity.ProjectedVars, acl.IsValidServiceIdentityName)
if err != nil {
return nil, err
}
bindings.ServiceIdentities = append(bindings.ServiceIdentities, &structs.ACLServiceIdentity{
ServiceName: bindName,
})

case structs.BindingRuleBindTypeNode:
bindName, err := computeBindName(rule.BindName, verifiedIdentity.ProjectedVars, acl.IsValidNodeIdentityName)
if err != nil {
return nil, err
}
bindings.NodeIdentities = append(bindings.NodeIdentities, &structs.ACLNodeIdentity{
NodeName: bindName,
Datacenter: b.datacenter,
})

case structs.BindingRuleBindTypeTemplatedPolicy:
templatedPolicy, err := generateTemplatedPolicies(rule.BindName, rule.BindVars, verifiedIdentity.ProjectedVars)
if err != nil {
return nil, err
}
bindings.TemplatedPolicies = append(bindings.TemplatedPolicies, templatedPolicy)

case structs.BindingRuleBindTypeRole:
bindName, err := computeBindName(rule.BindName, verifiedIdentity.ProjectedVars, acl.IsValidRoleName)
if err != nil {
return nil, err
}

_, role, err := b.store.ACLRoleGetByName(nil, bindName, &bindings.EnterpriseMeta)
if err != nil {
return nil, err
Expand All @@ -131,75 +141,78 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, verifiedIdentity *authm
return &bindings, nil
}

// IsValidBindNameOrBindVars returns whether the given BindName and/or BindVars template produces valid
// IsValidBindingRule returns whether the given BindName and/or BindVars template produces valid
// results when interpolating the auth method's available variables.
func IsValidBindNameOrBindVars(bindType, bindName string, bindVars *structs.ACLTemplatedPolicyVariables, availableVariables []string) (bool, error) {
func IsValidBindingRule(bindType, bindName string, bindVars *structs.ACLTemplatedPolicyVariables, availableVariables []string) error {
if bindType == "" || bindName == "" {
return false, nil
return errors.New("bindType and bindName must not be empty")
}

fakeVarMap := make(map[string]string)
for _, v := range availableVariables {
fakeVarMap[v] = "fake"
}

_, _, valid, err := computeBindNameAndVars(bindType, bindName, bindVars, fakeVarMap)
if err != nil {
return false, err
}
return valid, nil
}

// computeBindNameAndVars processes the HIL for the provided bind type+name+vars using the
// projected variables. When bindtype is templated-policy, it returns the resulting templated policy
// otherwise, returns nil
//
// when bindtype is not templated-policy: it evaluates bindName
// - If the HIL is invalid ("", nil, false, AN_ERROR) is returned.
// - If the computed name is not valid for the type ("INVALID_NAME", nil, false, nil) is returned.
// - If the computed name is valid for the type ("VALID_NAME", nil, true, nil) is returned.
// when bindtype is templated-policy: it evalueates both bindName and bindVars
// - If the computed bindvars(failing templated policy schema validation) are invalid ("", nil, false, AN_ERROR) is returned.
// - if the HIL in bindvars is invalid it returns ("", nil, false, AN_ERROR)
// - if the computed bindvars are valid and templated policy validation is successful it returns (bindName, TemplatedPolicy, true, nil)
func computeBindNameAndVars(bindType, bindName string, bindVars *structs.ACLTemplatedPolicyVariables, projectedVars map[string]string) (string, *structs.ACLTemplatedPolicy, bool, error) {
bindName, err := template.InterpolateHIL(bindName, projectedVars, true)
if err != nil {
return "", nil, false, err
}

var templatedPolicy *structs.ACLTemplatedPolicy
var valid bool
switch bindType {
case structs.BindingRuleBindTypeService:
valid = acl.IsValidServiceIdentityName(bindName)
if _, err := computeBindName(bindName, fakeVarMap, acl.IsValidServiceIdentityName); err != nil {
return fmt.Errorf("failed to validate bindType %q: %w", bindType, err)
}

case structs.BindingRuleBindTypeNode:
valid = acl.IsValidNodeIdentityName(bindName)
case structs.BindingRuleBindTypeRole:
valid = acl.IsValidRoleName(bindName)
if _, err := computeBindName(bindName, fakeVarMap, acl.IsValidNodeIdentityName); err != nil {
return fmt.Errorf("failed to validate bindType %q: %w", bindType, err)
}

case structs.BindingRuleBindTypeTemplatedPolicy:
templatedPolicy, valid, err = generateTemplatedPolicies(bindName, bindVars, projectedVars)
if err != nil {
return "", nil, false, err
// If user-defined templated policies are supported in the future,
// we will need to lookup state to ensure a template exists for given
// bindName. A possible solution is to rip out the check for templated
// policy into its own step which has access to the state store.
if _, err := generateTemplatedPolicies(bindName, bindVars, fakeVarMap); err != nil {
return fmt.Errorf("failed to validate bindType %q: %w", bindType, err)
}

case structs.BindingRuleBindTypeRole:
if _, err := computeBindName(bindName, fakeVarMap, acl.IsValidRoleName); err != nil {
return fmt.Errorf("failed to validate bindType %q: %w", bindType, err)
}
default:
return "", nil, false, fmt.Errorf("unknown binding rule bind type: %s", bindType)
return fmt.Errorf("Invalid Binding Rule: unknown BindType %q", bindType)
}

return bindName, templatedPolicy, valid, nil
return nil
}

func generateTemplatedPolicies(bindName string, bindVars *structs.ACLTemplatedPolicyVariables, projectedVars map[string]string) (*structs.ACLTemplatedPolicy, bool, error) {
computedBindVars, err := computeBindVars(bindVars, projectedVars)
// computeBindName interprets given HIL bindName with any given variables in projectedVars.
// validate (if not nil) will be called on the interpreted string.
func computeBindName(bindName string, projectedVars map[string]string, validate func(string) bool) (string, error) {
computed, err := template.InterpolateHIL(bindName, projectedVars, true)
if err != nil {
return nil, false, err
return "", fmt.Errorf("error interpreting template: %w", err)
}
if validate != nil && !validate(computed) {
return "", fmt.Errorf("invalid bind name: %q", computed)
}
return computed, nil
}

// generateTemplatedPolicies fetches a templated policy by bindName then attempts to interpret
// bindVars with any given variables in projectedVars. The resulting template is validated
// by the template's schema.
func generateTemplatedPolicies(
bindName string,
bindVars *structs.ACLTemplatedPolicyVariables,
projectedVars map[string]string,
) (*structs.ACLTemplatedPolicy, error) {
baseTemplate, ok := structs.GetACLTemplatedPolicyBase(bindName)

if !ok {
return nil, false, fmt.Errorf("Bind name for templated-policy bind type does not match existing template name: %s", bindName)
return nil, fmt.Errorf("Bind name for templated-policy bind type does not match existing template name: %s", bindName)
}

computedBindVars, err := computeBindVars(bindVars, projectedVars)
if err != nil {
return nil, fmt.Errorf("failed to interpret templated policy variables: %w", err)
}

out := &structs.ACLTemplatedPolicy{
Expand All @@ -208,12 +221,11 @@ func generateTemplatedPolicies(bindName string, bindVars *structs.ACLTemplatedPo
TemplateID: baseTemplate.TemplateID,
}

err = out.ValidateTemplatedPolicy(baseTemplate.Schema)
if err != nil {
return nil, false, fmt.Errorf("templated policy failed validation. Error: %v", err)
if err := out.ValidateTemplatedPolicy(baseTemplate.Schema); err != nil {
return nil, fmt.Errorf("templated policy failed validation: %w", err)
}

return out, true, nil
return out, nil
}

func computeBindVars(bindVars *structs.ACLTemplatedPolicyVariables, projectedVars map[string]string) (*structs.ACLTemplatedPolicyVariables, error) {
Expand Down
Loading