Skip to content

Commit

Permalink
[CE] Add workload bind type and templated policy (#19077)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris S. Kim authored Oct 5, 2023
1 parent ca4ff6b commit ad26494
Show file tree
Hide file tree
Showing 11 changed files with 172 additions and 128 deletions.
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

0 comments on commit ad26494

Please sign in to comment.