[NET-5330] Support templated policies in Binding rules (#18719)

* [NET-5330] Support templated policies in Binding rules

* changelog for templated policy support in binding rules
This commit is contained in:
Ronald 2023-09-08 14:39:09 -04:00 committed by GitHub
parent 576ffdf705
commit 40d7ebc318
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 550 additions and 89 deletions

7
.changelog/18719.txt Normal file
View File

@ -0,0 +1,7 @@
```release-note:feature
acl: Add BindRule support for templated policies. Add new BindType: templated-policy and BindVar field for templated policy variables.
```
```release-note:feature
cli: Add `bind-var` flag to `consul acl binding-rule` for templated policy variables.
```

View File

@ -1719,18 +1719,23 @@ func (a *ACL) BindingRuleSet(args *structs.ACLBindingRuleSetRequest, reply *stru
return fmt.Errorf("Invalid Binding Rule: no BindName is set")
}
if rule.BindType != structs.BindingRuleBindTypeTemplatedPolicy && rule.BindVars != nil {
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.IsValidBindName(rule.BindType, rule.BindName, blankID.ProjectedVarNames()); err != nil {
return fmt.Errorf("Invalid Binding Rule: invalid BindName: %v", err)
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")
return fmt.Errorf("Invalid Binding Rule: invalid BindName or BindVars")
}
req := &structs.ACLBindingRuleBatchSetRequest{

View File

@ -3573,7 +3573,7 @@ func TestACLEndpoint_BindingRuleSet(t *testing.T) {
}
}
requireSetErrors := func(t *testing.T, reqRule structs.ACLBindingRule) {
requireSetErrors := func(t *testing.T, reqRule structs.ACLBindingRule, msg ...string) {
req := structs.ACLBindingRuleSetRequest{
Datacenter: "dc1",
BindingRule: reqRule,
@ -3583,6 +3583,10 @@ func TestACLEndpoint_BindingRuleSet(t *testing.T) {
err := aclEp.BindingRuleSet(&req, &resp)
require.Error(t, err)
for _, s := range msg {
require.Contains(t, err.Error(), s)
}
}
requireOK := func(t *testing.T, reqRule structs.ACLBindingRule) *structs.ACLBindingRule {
@ -3659,6 +3663,40 @@ func TestACLEndpoint_BindingRuleSet(t *testing.T) {
require.Equal(t, "test-node", rule.BindName)
})
t.Run("templated policy", func(t *testing.T) {
req := structs.ACLBindingRuleSetRequest{
Datacenter: "dc1",
BindingRule: structs.ACLBindingRule{
Description: "templated policy binding rule",
AuthMethod: testAuthMethod.Name,
Selector: "serviceaccount.name==abc",
BindType: structs.BindingRuleBindTypeTemplatedPolicy,
BindName: api.ACLTemplatedPolicyNodeName,
BindVars: &structs.ACLTemplatedPolicyVariables{
Name: "test-node",
},
},
WriteRequest: structs.WriteRequest{Token: TestDefaultInitialManagementToken},
}
var resp structs.ACLBindingRule
err := aclEp.BindingRuleSet(&req, &resp)
require.NoError(t, err)
require.NotNil(t, resp.ID)
// Get the rule directly to validate that it exists
ruleResp, err := retrieveTestBindingRule(codec, TestDefaultInitialManagementToken, "dc1", resp.ID)
require.NoError(t, err)
rule := ruleResp.BindingRule
require.NotEmpty(t, rule.ID)
require.Equal(t, rule.Description, "templated policy binding rule")
require.Equal(t, rule.AuthMethod, testAuthMethod.Name)
require.Equal(t, "serviceaccount.name==abc", rule.Selector)
require.Equal(t, structs.BindingRuleBindTypeTemplatedPolicy, rule.BindType)
require.Equal(t, api.ACLTemplatedPolicyNodeName, rule.BindName)
})
t.Run("Update fails; cannot change method name", func(t *testing.T) {
reqRule := newRule()
reqRule.ID = ruleID
@ -3775,10 +3813,35 @@ func TestACLEndpoint_BindingRuleSet(t *testing.T) {
requireSetErrors(t, reqRule)
})
t.Run("Create fails; when bind vars is set for non templated policy", func(t *testing.T) {
reqRule := newRule()
reqRule.BindVars = &structs.ACLTemplatedPolicyVariables{
Name: "test",
}
requireSetErrors(t, reqRule, "invalid Binding Rule: BindVars cannot be set when bind type is not templated-policy.")
})
t.Run("Create fails; when missing required bindvars", func(t *testing.T) {
reqRule := newRule()
reqRule.BindName = api.ACLTemplatedPolicyServiceName
reqRule.BindType = structs.BindingRuleBindTypeTemplatedPolicy
requireSetErrors(t, reqRule, "Invalid Binding Rule: invalid BindName or BindVars")
})
t.Run("Create fails; when bindvars contains unknown vars", func(t *testing.T) {
reqRule := newRule()
reqRule.BindName = api.ACLTemplatedPolicyServiceName
reqRule.BindType = structs.BindingRuleBindTypeTemplatedPolicy
reqRule.BindVars = &structs.ACLTemplatedPolicyVariables{
Name: "method-${serviceaccount.bizarroname}",
}
requireSetErrors(t, reqRule, "Invalid Binding Rule: invalid BindName or BindVars")
})
t.Run("Create fails; invalid bind type", func(t *testing.T) {
reqRule := newRule()
reqRule.BindType = "invalid"
requireSetErrors(t, reqRule)
requireSetErrors(t, reqRule, "Invalid Binding Rule: unknown BindType")
})
t.Run("Create fails; bind name with unknown vars", func(t *testing.T) {

View File

@ -43,6 +43,7 @@ type Bindings struct {
Roles []structs.ACLTokenRoleLink
ServiceIdentities []*structs.ACLServiceIdentity
NodeIdentities []*structs.ACLNodeIdentity
TemplatedPolicies structs.ACLTemplatedPolicies
EnterpriseMeta acl.EnterpriseMeta
}
@ -55,6 +56,7 @@ func (b *Bindings) None() bool {
return len(b.ServiceIdentities) == 0 &&
len(b.NodeIdentities) == 0 &&
len(b.TemplatedPolicies) == 0 &&
len(b.Roles) == 0
}
@ -86,10 +88,10 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, verifiedIdentity *authm
return &bindings, nil
}
// Compute role, service identity, or node identity names by interpolating
// 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, valid, err := computeBindName(rule.BindType, rule.BindName, verifiedIdentity.ProjectedVars)
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)
@ -109,6 +111,9 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, verifiedIdentity *authm
Datacenter: b.datacenter,
})
case structs.BindingRuleBindTypeTemplatedPolicy:
bindings.TemplatedPolicies = append(bindings.TemplatedPolicies, templatedPolicy)
case structs.BindingRuleBindTypeRole:
_, role, err := b.store.ACLRoleGetByName(nil, bindName, &bindings.EnterpriseMeta)
if err != nil {
@ -126,9 +131,9 @@ func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, verifiedIdentity *authm
return &bindings, nil
}
// IsValidBindName returns whether the given BindName template produces valid
// IsValidBindNameOrBindVars returns whether the given BindName and/or BindVars template produces valid
// results when interpolating the auth method's available variables.
func IsValidBindName(bindType, bindName string, availableVariables []string) (bool, error) {
func IsValidBindNameOrBindVars(bindType, bindName string, bindVars *structs.ACLTemplatedPolicyVariables, availableVariables []string) (bool, error) {
if bindType == "" || bindName == "" {
return false, nil
}
@ -138,25 +143,32 @@ func IsValidBindName(bindType, bindName string, availableVariables []string) (bo
fakeVarMap[v] = "fake"
}
_, valid, err := computeBindName(bindType, bindName, fakeVarMap)
_, _, valid, err := computeBindNameAndVars(bindType, bindName, bindVars, fakeVarMap)
if err != nil {
return false, err
}
return valid, nil
}
// computeBindName processes the HIL for the provided bind type+name using the
// projected variables.
// 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
//
// - If the HIL is invalid ("", false, AN_ERROR) is returned.
// - If the computed name is not valid for the type ("INVALID_NAME", false, nil) is returned.
// - If the computed name is valid for the type ("VALID_NAME", true, nil) is returned.
func computeBindName(bindType, bindName string, projectedVars map[string]string) (string, bool, error) {
// 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 "", false, err
return "", nil, false, err
}
var templatedPolicy *structs.ACLTemplatedPolicy
var valid bool
switch bindType {
case structs.BindingRuleBindTypeService:
@ -165,11 +177,60 @@ func computeBindName(bindType, bindName string, projectedVars map[string]string)
valid = acl.IsValidNodeIdentityName(bindName)
case structs.BindingRuleBindTypeRole:
valid = acl.IsValidRoleName(bindName)
case structs.BindingRuleBindTypeTemplatedPolicy:
templatedPolicy, valid, err = generateTemplatedPolicies(bindName, bindVars, projectedVars)
if err != nil {
return "", nil, false, err
}
default:
return "", false, fmt.Errorf("unknown binding rule bind type: %s", bindType)
return "", nil, false, fmt.Errorf("unknown binding rule bind type: %s", bindType)
}
return bindName, valid, nil
return bindName, templatedPolicy, valid, nil
}
func generateTemplatedPolicies(bindName string, bindVars *structs.ACLTemplatedPolicyVariables, projectedVars map[string]string) (*structs.ACLTemplatedPolicy, bool, error) {
computedBindVars, err := computeBindVars(bindVars, projectedVars)
if err != nil {
return nil, false, err
}
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)
}
out := &structs.ACLTemplatedPolicy{
TemplateName: bindName,
TemplateVariables: computedBindVars,
TemplateID: baseTemplate.TemplateID,
}
err = out.ValidateTemplatedPolicy(baseTemplate.Schema)
if err != nil {
return nil, false, fmt.Errorf("templated policy failed validation. Error: %v", err)
}
return out, true, nil
}
func computeBindVars(bindVars *structs.ACLTemplatedPolicyVariables, projectedVars map[string]string) (*structs.ACLTemplatedPolicyVariables, error) {
if bindVars == nil {
return nil, nil
}
out := &structs.ACLTemplatedPolicyVariables{}
if bindVars.Name != "" {
nameValue, err := template.InterpolateHIL(bindVars.Name, projectedVars, true)
if err != nil {
return nil, err
}
out.Name = nameValue
}
return out, nil
}
// doesSelectorMatch checks that a single selector matches the provided vars.

View File

@ -258,11 +258,12 @@ func TestBinder_NodeIdentities_NameValidation(t *testing.T) {
require.Contains(t, err.Error(), "bind name for bind target is invalid")
}
func Test_IsValidBindName(t *testing.T) {
func Test_IsValidBindNameOrBindVars(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
@ -270,59 +271,72 @@ func Test_IsValidBindName(t *testing.T) {
for _, test := range []testcase{
{"no bind type",
"", "", "", false, false},
"", "", nil, "", false, false},
{"bad bind type",
"invalid", "blah", "", false, true},
"invalid", "blah", nil, "", false, true},
// valid HIL, invalid name
{"empty",
"both", "", "", false, false},
"both", "", nil, "", false, false},
{"just end",
"both", "}", "", false, false},
"both", "}", nil, "", false, false},
{"var without start",
"both", " item }", "item", false, false},
"both", " item }", nil, "item", false, false},
{"two vars missing second start",
"both", "before-${ item }after--more }", "item,more", false, false},
"both", "before-${ item }after--more }", nil, "item,more", false, false},
// names for the two types are validated differently
{"@ is disallowed",
"both", "bad@name", "", false, false},
"both", "bad@name", nil, "", false, false},
{"leading dash",
"role", "-name", "", true, false},
"role", "-name", nil, "", true, false},
{"leading dash",
"service", "-name", "", false, false},
"service", "-name", nil, "", false, false},
{"trailing dash",
"role", "name-", "", true, false},
"role", "name-", nil, "", true, false},
{"trailing dash",
"service", "name-", "", false, false},
"service", "name-", nil, "", false, false},
{"inner dash",
"both", "name-end", "", true, false},
"both", "name-end", nil, "", true, false},
{"upper case",
"role", "NAME", "", true, false},
"role", "NAME", nil, "", true, false},
{"upper case",
"service", "NAME", "", false, false},
"service", "NAME", nil, "", false, false},
// valid HIL, valid name
{"no vars",
"both", "nothing", "", true, false},
"both", "nothing", nil, "", true, false},
{"just var",
"both", "${item}", "item", true, false},
"both", "${item}", nil, "item", true, false},
{"var in middle",
"both", "before-${item}after", "item", true, false},
"both", "before-${item}after", nil, "item", true, false},
{"two vars",
"both", "before-${item}after-${more}", "item,more", true, false},
"both", "before-${item}after-${more}", nil, "item,more", true, false},
// bad
{"no bind name",
"both", "", "", false, false},
"both", "", nil, "", false, false},
{"just start",
"both", "${", "", false, true},
"both", "${", nil, "", false, true},
{"backwards",
"both", "}${", "", false, true},
"both", "}${", nil, "", false, true},
{"no varname",
"both", "${}", "", false, true},
"both", "${}", nil, "", false, true},
{"missing map key",
"both", "${item}", "", false, true},
"both", "${item}", nil, "", false, true},
{"var without end",
"both", "${ item ", "item", false, true},
"both", "${ item ", nil, "item", false, true},
{"two vars missing first end",
"both", "before-${ item after-${ more }", "item,more", false, true},
"both", "before-${ item after-${ more }", nil, "item,more", false, true},
// bind type: templated policy - bad input
{"templated-policy missing bindvars", "templated-policy", "builtin/service", nil, "", false, true},
{"templated-policy with unknown templated policy name",
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${item}after-${more}"}, "", false, true},
{"templated-policy with correct bindvars and unknown vars",
"templated-policy", "builtin/fake", &structs.ACLTemplatedPolicyVariables{Name: "test"}, "", false, true},
{"templated-policy with correct bindvars but incorrect HIL",
"templated-policy", "builtin/service", &structs.ACLTemplatedPolicyVariables{Name: "before-${ item }after--more }"}, "", false, 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},
} {
var cases []testcase
if test.bindType == "both" {
@ -339,9 +353,10 @@ func Test_IsValidBindName(t *testing.T) {
test := test
t.Run(test.bindType+"--"+test.name, func(t *testing.T) {
t.Parallel()
valid, err := IsValidBindName(
valid, err := IsValidBindNameOrBindVars(
test.bindType,
test.bindName,
test.bindVars,
strings.Split(test.fields, ","),
)
if test.err {

View File

@ -46,6 +46,7 @@ func (l *Login) TokenForVerifiedIdentity(identity *authmethod.Identity, authMeth
ExpirationTTL: authMethod.MaxTokenTTL,
ServiceIdentities: bindings.ServiceIdentities,
NodeIdentities: bindings.NodeIdentities,
TemplatedPolicies: bindings.TemplatedPolicies,
Roles: bindings.Roles,
EnterpriseMeta: bindings.EnterpriseMeta,
}

View File

@ -526,7 +526,8 @@ func aclTokenSetTxn(tx WriteTxn, idx uint64, token *structs.ACLToken, opts ACLTo
}
if opts.ProhibitUnprivileged {
if numValidRoles == 0 && numValidPolicies == 0 && len(token.ServiceIdentities) == 0 && len(token.NodeIdentities) == 0 {
if numValidRoles == 0 && numValidPolicies == 0 && len(token.ServiceIdentities) == 0 &&
len(token.NodeIdentities) == 0 && len(token.TemplatedPolicies) == 0 {
return ErrTokenHasNoPrivileges
}
}

View File

@ -1068,6 +1068,21 @@ const (
// }
// }
BindingRuleBindTypeNode = "node"
// BindingRuleBindTypeTemplatedPolicy is the binding rule bind type that
// assigns a TemplatedPolicy to the token that is created using the value
// of the computed BindVars as template variables and BindName as template name like:
//
// &ACLToken{
// ...other fields...
// TemplatedPolicies: []*ACLTemplatedPolicy{
// &ACLTemplatedPolicy{
// TemplateName: "<BindName>",
// TemplateVariables: &ACLTemplatedPolicyVariables{<computed BindVars>}
// },
// },
// }
BindingRuleBindTypeTemplatedPolicy = "templated-policy"
)
type ACLBindingRule struct {
@ -1087,8 +1102,10 @@ type ACLBindingRule struct {
// BindType adjusts how this binding rule is applied at login time. The
// valid values are:
//
// - BindingRuleBindTypeService = "service"
// - BindingRuleBindTypeRole = "role"
// - BindingRuleBindTypeService = "service"
// - BindingRuleBindTypeNode = "node"
// - BindingRuleBindTypeRole = "role"
// - BindingRuleBindTypeTemplatedPolicy = "templated-policy"
BindType string
// BindName is the target of the binding. Can be lightly templated using
@ -1096,6 +1113,10 @@ type ACLBindingRule struct {
// upon the BindType.
BindName string
// BindVars is a the variables used when binding rule type is `templated-policy`. Can be lightly
// templated using HIL ${foo} syntax from available field names.
BindVars *ACLTemplatedPolicyVariables `json:",omitempty"`
// Embedded Enterprise ACL metadata
acl.EnterpriseMeta `mapstructure:",squash"`

View File

@ -241,6 +241,12 @@ const (
// BindingRuleBindTypeRole binds to pre-existing roles with the given name.
BindingRuleBindTypeRole BindingRuleBindType = "role"
// BindingRuleBindTypeNode binds to a node identity with given name.
BindingRuleBindTypeNode BindingRuleBindType = "node"
// BindingRuleBindTypeTemplatedPolicy binds to a templated policy with given template name and variables.
BindingRuleBindTypeTemplatedPolicy BindingRuleBindType = "templated-policy"
)
type ACLBindingRule struct {
@ -250,6 +256,7 @@ type ACLBindingRule struct {
Selector string
BindType BindingRuleBindType
BindName string
BindVars *ACLTemplatedPolicyVariables `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64

View File

@ -265,6 +265,15 @@ func ExtractTemplatedPolicies(templatedPolicy string, templatedPolicyFile string
return out, nil
}
func ExtractBindVars(bindVars map[string]string) (*api.ACLTemplatedPolicyVariables, error) {
if len(bindVars) == 0 {
return nil, nil
}
out := &api.ACLTemplatedPolicyVariables{}
err := mapstructure.Decode(bindVars, out)
return out, err
}
func getTemplatedPolicyVariables(variables []string) (*api.ACLTemplatedPolicyVariables, error) {
if len(variables) == 0 {
return nil, nil

View File

@ -9,6 +9,7 @@ import (
"strings"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/acl/bindingrule"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
@ -31,6 +32,7 @@ type cmd struct {
selector string
bindType string
bindName string
bindVars map[string]string
showMeta bool
format string
@ -71,7 +73,14 @@ func (c *cmd) init() {
&c.bindType,
"bind-type",
string(api.BindingRuleBindTypeService),
"Type of binding to perform (\"service\" or \"role\").",
"Type of binding to perform (\"service\", \"role\", \"node\" or \"templated-policy\").",
)
c.flags.Var(
(*flags.FlagMapValue)(&c.bindVars),
"bind-vars",
"Templated policy variables. Can only be used when -bind-type is templated-policy."+
" May be specified multiple times with different variables. Can use ${var} interpolation."+
" Format is VariableName=Value",
)
c.flags.StringVar(
&c.bindName,
@ -100,15 +109,28 @@ func (c *cmd) Run(args []string) int {
}
if c.authMethodName == "" {
c.UI.Error(fmt.Sprintf("Missing required '-method' flag"))
c.UI.Error("Missing required '-method' flag")
c.UI.Error(c.Help())
return 1
} else if c.bindType == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-type' flag"))
c.UI.Error("Missing required '-bind-type' flag")
c.UI.Error(c.Help())
return 1
} else if c.bindName == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-name' flag"))
c.UI.Error("Missing required '-bind-name' flag")
c.UI.Error(c.Help())
return 1
}
if api.BindingRuleBindType(c.bindType) != api.BindingRuleBindTypeTemplatedPolicy && len(c.bindVars) > 0 {
c.UI.Error("Cannot specify -bind-vars when -bind-type is not templated-policy")
c.UI.Error(c.Help())
return 1
}
processBindVars, err := acl.ExtractBindVars(c.bindVars)
if err != nil {
c.UI.Error("Failed to decode '-bind-vars'")
c.UI.Error(c.Help())
return 1
}
@ -118,6 +140,7 @@ func (c *cmd) Run(args []string) int {
AuthMethod: c.authMethodName,
BindType: api.BindingRuleBindType(c.bindType),
BindName: c.bindName,
BindVars: processBindVars,
Selector: c.selector,
}

View File

@ -106,6 +106,24 @@ func TestBindingRuleCreateCommand(t *testing.T) {
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-bind-name' flag")
})
t.Run("bind vars specified when by bindtype is not templated-policy", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-name=web",
"-bind-type=service",
"-bind-vars", "name=test",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Cannot specify -bind-vars when -bind-type is not templated-policy")
})
t.Run("must use roughly valid selector", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
@ -176,6 +194,41 @@ func TestBindingRuleCreateCommand(t *testing.T) {
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
t.Run("create it with type templated policy", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=templated-policy",
"-bind-name=builtin/service",
"-bind-vars", "name=api",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
t.Run("cannot create when missing bind-vars", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=templated-policy",
"-bind-name=builtin/service",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "templated policy failed validation")
})
}
func TestBindingRuleCreateCommand_JSON(t *testing.T) {

View File

@ -63,6 +63,10 @@ func (f *prettyFormatter) FormatBindingRule(rule *api.ACLBindingRule) (string, e
buffer.WriteString(fmt.Sprintf("Description: %s\n", rule.Description))
buffer.WriteString(fmt.Sprintf("BindType: %s\n", rule.BindType))
buffer.WriteString(fmt.Sprintf("BindName: %s\n", rule.BindName))
if rule.BindVars != nil && rule.BindVars.Name != "" {
buffer.WriteString(fmt.Sprintf("BindVars: \n - Name: %s\n", rule.BindVars.Name))
}
buffer.WriteString(fmt.Sprintf("Selector: %s\n", rule.Selector))
if f.showMeta {
buffer.WriteString(fmt.Sprintf("Create Index: %d\n", rule.CreateIndex))
@ -96,6 +100,9 @@ func (f *prettyFormatter) formatBindingRuleListEntry(rule *api.ACLBindingRule) s
buffer.WriteString(fmt.Sprintf(" Description: %s\n", rule.Description))
buffer.WriteString(fmt.Sprintf(" BindType: %s\n", rule.BindType))
buffer.WriteString(fmt.Sprintf(" BindName: %s\n", rule.BindName))
if rule.BindVars != nil && rule.BindVars.Name != "" {
buffer.WriteString(fmt.Sprintf(" BindVars: \n - Name: %s\n", rule.BindVars.Name))
}
buffer.WriteString(fmt.Sprintf(" Selector: %s\n", rule.Selector))
if f.showMeta {
buffer.WriteString(fmt.Sprintf(" Create Index: %d\n", rule.CreateIndex))

View File

@ -34,6 +34,7 @@ type cmd struct {
selector string
bindType string
bindName string
bindVars map[string]string
noMerge bool
showMeta bool
@ -102,6 +103,14 @@ func (c *cmd) init() {
fmt.Sprintf("Output format {%s}", strings.Join(bindingrule.GetSupportedFormats(), "|")),
)
c.flags.Var(
(*flags.FlagMapValue)(&c.bindVars),
"bind-vars",
"Templated policy variables. Can only be used when -bind-type is templated-policy."+
" May be specified multiple times with different variables. Can use ${var} interpolation."+
" Format is VariableName=Value",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
@ -115,7 +124,7 @@ func (c *cmd) Run(args []string) int {
}
if c.ruleID == "" {
c.UI.Error(fmt.Sprintf("Cannot update a binding rule without specifying the -id parameter"))
c.UI.Error("Cannot update a binding rule without specifying the -id parameter")
return 1
}
@ -141,14 +150,25 @@ func (c *cmd) Run(args []string) int {
return 1
}
processBindVars, err := acl.ExtractBindVars(c.bindVars)
if err != nil {
c.UI.Error("Failed to decode '-bind-vars'")
c.UI.Error(c.Help())
return 1
}
var rule *api.ACLBindingRule
if c.noMerge {
if c.bindType == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-type' flag"))
c.UI.Error("Missing required '-bind-type' flag")
c.UI.Error(c.Help())
return 1
} else if c.bindName == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-name' flag"))
c.UI.Error("Missing required '-bind-name' flag")
c.UI.Error(c.Help())
return 1
} else if len(c.bindVars) > 0 && api.BindingRuleBindType(c.bindType) != api.BindingRuleBindTypeTemplatedPolicy {
c.UI.Error("-bind-vars cannot be specified when -bind-type is not templated-policy")
c.UI.Error(c.Help())
return 1
}
@ -158,6 +178,7 @@ func (c *cmd) Run(args []string) int {
AuthMethod: currentRule.AuthMethod, // immutable
Description: c.description,
BindType: api.BindingRuleBindType(c.bindType),
BindVars: processBindVars,
BindName: c.bindName,
Selector: c.selector,
}
@ -174,6 +195,14 @@ func (c *cmd) Run(args []string) int {
if c.bindName != "" {
rule.BindName = c.bindName
}
if len(c.bindVars) > 0 {
rule.BindVars = processBindVars
}
// remove bind vars for non templated-policy binding rules types
if api.BindingRuleBindType(c.bindType) != api.BindingRuleBindTypeTemplatedPolicy {
rule.BindVars = nil
}
if isFlagSet(c.flags, "selector") {
rule.Selector = c.selector // empty is valid
}

View File

@ -127,15 +127,24 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
require.Contains(t, ui.ErrorWriter.String(), "Binding rule not found with ID")
})
createRule := func(t *testing.T) string {
createRule := func(t *testing.T, isTemplatedPolicy bool) string {
bindingRule := &api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeService,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
}
if isTemplatedPolicy {
bindingRule.BindType = api.BindingRuleBindTypeTemplatedPolicy
bindingRule.BindName = api.ACLTemplatedPolicyServiceName
bindingRule.BindVars = &api.ACLTemplatedPolicyVariables{
Name: "test-${serviceaccount.name}",
}
}
rule, _, err := client.ACL().BindingRuleCreate(
&api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeService,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
bindingRule,
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
@ -161,7 +170,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
m[c] = struct{}{}
}
_ = createRule(t)
_ = createRule(t, false)
}
}
@ -183,7 +192,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("must use roughly valid selector", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
args := []string{
"-http-addr=" + a.HTTPAddr(),
@ -201,7 +210,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("update all fields", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -233,10 +242,79 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields with templated policy", func(t *testing.T) {
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-description=test rule edited",
"-bind-type", "templated-policy",
"-bind-name=builtin/service",
"-bind-vars", "name=api",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.ACLTemplatedPolicyServiceName, rule.BindName)
require.Equal(t, api.BindingRuleBindTypeTemplatedPolicy, rule.BindType)
require.Equal(t, &api.ACLTemplatedPolicyVariables{Name: "api"}, rule.BindVars)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update bind type to something other than templated-policy unsets bindvars", func(t *testing.T) {
id := createRule(t, true)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-description=test rule edited",
"-bind-type", "role",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType)
require.Empty(t, rule.BindVars)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields - partial", func(t *testing.T) {
deleteRules(t) // reset since we created a bunch that might be dupes
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -269,7 +347,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("update all fields but description", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -301,7 +379,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("update all fields but bind name", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -333,7 +411,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("update all fields but must exist", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -365,7 +443,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("update all fields but selector", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -397,7 +475,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("update all fields clear selector", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -430,7 +508,7 @@ func TestBindingRuleUpdateCommand(t *testing.T) {
})
t.Run("update all fields json formatted", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -571,15 +649,25 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
require.Contains(t, ui.ErrorWriter.String(), "Binding rule not found with ID")
})
createRule := func(t *testing.T) string {
createRule := func(t *testing.T, isTemplatedPolicy bool) string {
bindingRule := &api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeRole,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
}
if isTemplatedPolicy {
bindingRule.BindType = api.BindingRuleBindTypeTemplatedPolicy
bindingRule.BindName = "builtin/service"
bindingRule.BindVars = &api.ACLTemplatedPolicyVariables{
Name: "test-${serviceaccount.name}",
}
}
rule, _, err := client.ACL().BindingRuleCreate(
&api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeRole,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
bindingRule,
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
@ -605,7 +693,7 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
m[c] = struct{}{}
}
_ = createRule(t)
_ = createRule(t, false)
}
}
@ -628,7 +716,7 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
})
t.Run("must use roughly valid selector", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -650,7 +738,7 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
})
t.Run("update all fields", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -683,10 +771,81 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields after initial binding rule is templated-policy", func(t *testing.T) {
id := createRule(t, true)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id", id,
"-description=test rule edited",
"-bind-type", "service",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeService, rule.BindType)
require.Empty(t, rule.BindVars)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields with templated-policy bind type", func(t *testing.T) {
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id", id,
"-description=test rule edited",
"-bind-type=templated-policy",
"-bind-name=builtin/service",
"-bind-vars", "name=api",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeTemplatedPolicy, rule.BindType)
require.Equal(t, api.ACLTemplatedPolicyServiceName, rule.BindName)
require.Equal(t, &api.ACLTemplatedPolicyVariables{Name: "api"}, rule.BindVars)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields - partial", func(t *testing.T) {
deleteRules(t) // reset since we created a bunch that might be dupes
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -720,7 +879,7 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
})
t.Run("update all fields but description", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -753,7 +912,7 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
})
t.Run("missing bind name", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)
@ -773,7 +932,7 @@ func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
})
t.Run("update all fields but selector", func(t *testing.T) {
id := createRule(t)
id := createRule(t, false)
ui := cli.NewMockUi()
cmd := New(ui)