[CE] Add workload bind type and templated policy (#19077)

This commit is contained in:
Chris S. Kim 2023-10-05 15:45:41 -04:00 committed by GitHub
parent ca4ff6ba1d
commit ad26494016
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 132 deletions

3
.changelog/19077.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
acl: Adds workload identity templated policy
```

View File

@ -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,

View File

@ -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{

View File

@ -4,6 +4,7 @@
package auth
import (
"errors"
"fmt"
"github.com/hashicorp/go-bexpr"
@ -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
@ -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
@ -131,11 +141,11 @@ 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)
@ -143,63 +153,66 @@ func IsValidBindNameOrBindVars(bindType, bindName string, bindVars *structs.ACLT
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, fmt.Errorf("Bind name for templated-policy bind type does not match existing template name: %s", bindName)
}
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)
computedBindVars, err := computeBindVars(bindVars, projectedVars)
if err != nil {
return nil, fmt.Errorf("failed to interpret templated policy variables: %w", err)
}
out := &structs.ACLTemplatedPolicy{
@ -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) {

View File

@ -119,7 +119,7 @@ func TestBinder_Roles_NameValidation(t *testing.T) {
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
require.Contains(t, err.Error(), "invalid bind name")
}
func TestBinder_ServiceIdentities_Success(t *testing.T) {
@ -187,7 +187,7 @@ func TestBinder_ServiceIdentities_NameValidation(t *testing.T) {
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
require.Contains(t, err.Error(), "invalid bind name")
}
func TestBinder_NodeIdentities_Success(t *testing.T) {
@ -255,88 +255,87 @@ func TestBinder_NodeIdentities_NameValidation(t *testing.T) {
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
require.Contains(t, err.Error(), "invalid bind name")
}
func Test_IsValidBindNameOrBindVars(t *testing.T) {
func Test_IsValidBindingRule(t *testing.T) {
type testcase struct {
name string
bindType string
bindName string
bindVars *structs.ACLTemplatedPolicyVariables
fields string
valid bool // valid HIL, invalid contents
err bool // invalid HIL
err bool
}
for _, test := range []testcase{
{"no bind type",
"", "", nil, "", false, false},
"", "", nil, "", true},
{"bad bind type",
"invalid", "blah", nil, "", false, true},
"invalid", "blah", nil, "", true},
// valid HIL, invalid name
{"empty",
"both", "", nil, "", false, false},
"both", "", nil, "", true},
{"just end",
"both", "}", nil, "", false, false},
"both", "}", nil, "", true},
{"var without start",
"both", " item }", nil, "item", false, false},
"both", " item }", nil, "item", true},
{"two vars missing second start",
"both", "before-${ item }after--more }", nil, "item,more", false, false},
"both", "before-${ item }after--more }", nil, "item,more", true},
// names for the two types are validated differently
{"@ is disallowed",
"both", "bad@name", nil, "", false, false},
"both", "bad@name", nil, "", true},
{"leading dash",
"role", "-name", nil, "", true, false},
"role", "-name", nil, "", false},
{"leading dash",
"service", "-name", nil, "", false, false},
"service", "-name", nil, "", true},
{"trailing dash",
"role", "name-", nil, "", true, false},
"role", "name-", nil, "", false},
{"trailing dash",
"service", "name-", nil, "", false, false},
"service", "name-", nil, "", true},
{"inner dash",
"both", "name-end", nil, "", true, false},
"both", "name-end", nil, "", false},
{"upper case",
"role", "NAME", nil, "", true, false},
"role", "NAME", nil, "", false},
{"upper case",
"service", "NAME", nil, "", false, false},
"service", "NAME", nil, "", true},
// valid HIL, valid name
{"no vars",
"both", "nothing", nil, "", true, false},
"both", "nothing", nil, "", false},
{"just var",
"both", "${item}", nil, "item", true, false},
"both", "${item}", nil, "item", false},
{"var in middle",
"both", "before-${item}after", nil, "item", true, false},
"both", "before-${item}after", nil, "item", false},
{"two vars",
"both", "before-${item}after-${more}", nil, "item,more", true, false},
"both", "before-${item}after-${more}", nil, "item,more", false},
// bad
{"no bind name",
"both", "", nil, "", false, false},
"both", "", nil, "", true},
{"just start",
"both", "${", nil, "", false, true},
"both", "${", nil, "", true},
{"backwards",
"both", "}${", nil, "", false, true},
"both", "}${", nil, "", true},
{"no varname",
"both", "${}", nil, "", false, true},
"both", "${}", nil, "", true},
{"missing map key",
"both", "${item}", nil, "", false, true},
"both", "${item}", nil, "", true},
{"var without end",
"both", "${ item ", nil, "item", false, true},
"both", "${ item ", nil, "item", true},
{"two vars missing first end",
"both", "before-${ item after-${ more }", nil, "item,more", false, true},
"both", "before-${ item after-${ more }", nil, "item,more", true},
// bind type: templated policy - bad input
{"templated-policy missing bindvars", "templated-policy", "builtin/service", nil, "", false, true},
{"templated-policy missing bindvars", "templated-policy", "builtin/service", nil, "", true},
{"templated-policy with unknown templated policy name",
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${item}after-${more}"}, "", false, true},
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${item}after-${more}"}, "", true},
{"templated-policy with correct bindvars and unknown vars",
"templated-policy", "builtin/fake", &structs.ACLTemplatedPolicyVariables{Name: "test"}, "", false, true},
"templated-policy", "builtin/fake", &structs.ACLTemplatedPolicyVariables{Name: "test"}, "", true},
{"templated-policy with correct bindvars but incorrect HIL",
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${ item }after--more }"}, "", false, true},
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${ item }after--more }"}, "", true},
// bind type: templated policy - good input
{"templated-policy with appropriate bindvars",
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${item}after-${more}"}, "item,more", true, false},
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${item}after-${more}"}, "item,more", false},
} {
var cases []testcase
if test.bindType == "both" {
@ -353,19 +352,13 @@ func Test_IsValidBindNameOrBindVars(t *testing.T) {
test := test
t.Run(test.bindType+"--"+test.name, func(t *testing.T) {
t.Parallel()
valid, err := IsValidBindNameOrBindVars(
err := IsValidBindingRule(
test.bindType,
test.bindName,
test.bindVars,
strings.Split(test.fields, ","),
)
if test.err {
require.NotNil(t, err)
require.False(t, valid)
} else {
require.NoError(t, err)
require.Equal(t, test.valid, valid)
}
require.Equal(t, test.err, err != nil)
})
}
}

View File

@ -11,12 +11,13 @@ import (
"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"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib/stringslice"
)
//go:embed acltemplatedpolicy/schemas/node.json
@ -25,6 +26,9 @@ var ACLTemplatedPolicyNodeSchema string
//go:embed acltemplatedpolicy/schemas/service.json
var ACLTemplatedPolicyServiceSchema string
//go:embed acltemplatedpolicy/schemas/workload-identity.json
var ACLTemplatedPolicyWorkloadIdentitySchema string
type ACLTemplatedPolicies []*ACLTemplatedPolicy
const (
@ -32,6 +36,7 @@ const (
ACLTemplatedPolicyNodeID = "00000000-0000-0000-0000-000000000004"
ACLTemplatedPolicyDNSID = "00000000-0000-0000-0000-000000000005"
ACLTemplatedPolicyNomadServerID = "00000000-0000-0000-0000-000000000006"
ACLTemplatedPolicyWorkloadIdentityID = "00000000-0000-0000-0000-000000000007"
ACLTemplatedPolicyNoRequiredVariablesSchema = "" // catch-all schema for all templated policy that don't require a schema
)
@ -73,6 +78,12 @@ var (
Schema: ACLTemplatedPolicyNoRequiredVariablesSchema,
Template: ACLTemplatedPolicyNomadServer,
},
api.ACLTemplatedPolicyWorkloadIdentityName: {
TemplateID: ACLTemplatedPolicyWorkloadIdentityID,
TemplateName: api.ACLTemplatedPolicyWorkloadIdentityName,
Schema: ACLTemplatedPolicyWorkloadIdentitySchema,
Template: ACLTemplatedPolicyWorkloadIdentity,
},
}
)

View File

@ -19,6 +19,9 @@ var ACLTemplatedPolicyDNS string
//go:embed acltemplatedpolicy/policies/ce/nomad-server.hcl
var ACLTemplatedPolicyNomadServer string
//go:embed acltemplatedpolicy/policies/ce/workload-identity.hcl
var ACLTemplatedPolicyWorkloadIdentity string
func (t *ACLToken) TemplatedPolicyList() []*ACLTemplatedPolicy {
if len(t.TemplatedPolicies) == 0 {
return nil

View File

@ -8,8 +8,9 @@ package structs
import (
"testing"
"github.com/hashicorp/consul/api"
"github.com/stretchr/testify/require"
"github.com/hashicorp/consul/api"
)
func TestStructs_ACLTemplatedPolicy_SyntheticPolicy(t *testing.T) {
@ -79,6 +80,21 @@ service_prefix "" {
}
query_prefix "" {
policy = "read"
}`,
},
},
"workload-identity-template": {
templatedPolicy: &ACLTemplatedPolicy{
TemplateID: ACLTemplatedPolicyWorkloadIdentityID,
TemplateName: api.ACLTemplatedPolicyWorkloadIdentityName,
TemplateVariables: &ACLTemplatedPolicyVariables{
Name: "api",
},
},
expectedPolicy: &ACLPolicy{
Description: "synthetic policy generated from templated policy: builtin/workload-identity",
Rules: `identity "api" {
policy = "write"
}`,
},
},

View File

@ -0,0 +1,3 @@
identity "{{.Name}}" {
policy = "write"
}

View File

@ -0,0 +1,13 @@
{
"type": "object",
"properties": {
"name": { "type": "string", "$ref": "#/definitions/min-length-one" }
},
"required": ["name"],
"definitions": {
"min-length-one": {
"type": "string",
"minLength": 1
}
}
}

View File

@ -25,6 +25,7 @@ const (
ACLTemplatedPolicyNodeName = "builtin/node"
ACLTemplatedPolicyDNSName = "builtin/dns"
ACLTemplatedPolicyNomadServerName = "builtin/nomad-server"
ACLTemplatedPolicyWorkloadIdentityName = "builtin/workload-identity"
)
type ACLLink struct {