open-consul/agent/consul/auth/binder_test.go

376 lines
9.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package auth
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
)
func TestBindings_None(t *testing.T) {
var b *Bindings
require.True(t, b.None())
b = &Bindings{}
require.True(t, b.None())
b = &Bindings{Roles: []structs.ACLTokenRoleLink{{ID: generateID(t)}}}
require.False(t, b.None())
b = &Bindings{ServiceIdentities: []*structs.ACLServiceIdentity{{ServiceName: "web"}}}
require.False(t, b.None())
b = &Bindings{NodeIdentities: []*structs.ACLNodeIdentity{{NodeName: "node-123"}}}
require.False(t, b.None())
}
func TestBinder_Roles_Success(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
targetRole := &structs.ACLRole{
ID: generateID(t),
Name: "vim-role",
}
require.NoError(t, store.ACLRoleSet(0, targetRole))
otherRole := &structs.ACLRole{
ID: generateID(t),
Name: "frontend-engineers",
}
require.NoError(t, store.ACLRoleSet(0, otherRole))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "role==engineer",
BindType: structs.BindingRuleBindTypeRole,
BindName: "${editor}-role",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "role==engineer",
BindType: structs.BindingRuleBindTypeRole,
BindName: "this-role-does-not-exist",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "language==js",
BindType: structs.BindingRuleBindTypeRole,
BindName: otherRole.Name,
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
result, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{
SelectableFields: map[string]string{
"role": "engineer",
"language": "go",
},
ProjectedVars: map[string]string{
"editor": "vim",
},
})
require.NoError(t, err)
require.Equal(t, []structs.ACLTokenRoleLink{
{ID: targetRole.ID},
}, result.Roles)
}
func TestBinder_Roles_NameValidation(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "",
BindType: structs.BindingRuleBindTypeRole,
BindName: "INVALID!",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
}
func TestBinder_ServiceIdentities_Success(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "tier==web",
BindType: structs.BindingRuleBindTypeService,
BindName: "web-service-${name}",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "tier==db",
BindType: structs.BindingRuleBindTypeService,
BindName: "database-${name}",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
result, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{
SelectableFields: map[string]string{
"tier": "web",
},
ProjectedVars: map[string]string{
"name": "billing",
},
})
require.NoError(t, err)
require.Equal(t, []*structs.ACLServiceIdentity{
{ServiceName: "web-service-billing"},
}, result.ServiceIdentities)
}
func TestBinder_ServiceIdentities_NameValidation(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "",
BindType: structs.BindingRuleBindTypeService,
BindName: "INVALID!",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
}
func TestBinder_NodeIdentities_Success(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store, datacenter: "dc1"}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "provider==gcp",
BindType: structs.BindingRuleBindTypeNode,
BindName: "gcp-${os}",
AuthMethod: authMethod.Name,
},
{
ID: generateID(t),
Selector: "provider==aws",
BindType: structs.BindingRuleBindTypeNode,
BindName: "aws-${os}",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
result, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{
SelectableFields: map[string]string{
"provider": "gcp",
},
ProjectedVars: map[string]string{
"os": "linux",
},
})
require.NoError(t, err)
require.Equal(t, []*structs.ACLNodeIdentity{
{NodeName: "gcp-linux", Datacenter: "dc1"},
}, result.NodeIdentities)
}
func TestBinder_NodeIdentities_NameValidation(t *testing.T) {
store := testStateStore(t)
binder := &Binder{store: store}
authMethod := &structs.ACLAuthMethod{
Name: "test-auth-method",
Type: "testing",
}
require.NoError(t, store.ACLAuthMethodSet(0, authMethod))
bindingRules := structs.ACLBindingRules{
{
ID: generateID(t),
Selector: "",
BindType: structs.BindingRuleBindTypeNode,
BindName: "INVALID!",
AuthMethod: authMethod.Name,
},
}
require.NoError(t, store.ACLBindingRuleBatchSet(0, bindingRules))
_, err := binder.Bind(&structs.ACLAuthMethod{}, &authmethod.Identity{})
require.Error(t, err)
require.Contains(t, err.Error(), "bind name for bind target is invalid")
}
func Test_IsValidBindName(t *testing.T) {
type testcase struct {
name string
bindType string
bindName string
fields string
valid bool // valid HIL, invalid contents
err bool // invalid HIL
}
for _, test := range []testcase{
{"no bind type",
"", "", "", false, false},
{"bad bind type",
"invalid", "blah", "", false, true},
// valid HIL, invalid name
{"empty",
"both", "", "", false, false},
{"just end",
"both", "}", "", false, false},
{"var without start",
"both", " item }", "item", false, false},
{"two vars missing second start",
"both", "before-${ item }after--more }", "item,more", false, false},
// names for the two types are validated differently
{"@ is disallowed",
"both", "bad@name", "", false, false},
{"leading dash",
"role", "-name", "", true, false},
{"leading dash",
"service", "-name", "", false, false},
{"trailing dash",
"role", "name-", "", true, false},
{"trailing dash",
"service", "name-", "", false, false},
{"inner dash",
"both", "name-end", "", true, false},
{"upper case",
"role", "NAME", "", true, false},
{"upper case",
"service", "NAME", "", false, false},
// valid HIL, valid name
{"no vars",
"both", "nothing", "", true, false},
{"just var",
"both", "${item}", "item", true, false},
{"var in middle",
"both", "before-${item}after", "item", true, false},
{"two vars",
"both", "before-${item}after-${more}", "item,more", true, false},
// bad
{"no bind name",
"both", "", "", false, false},
{"just start",
"both", "${", "", false, true},
{"backwards",
"both", "}${", "", false, true},
{"no varname",
"both", "${}", "", false, true},
{"missing map key",
"both", "${item}", "", false, true},
{"var without end",
"both", "${ item ", "item", false, true},
{"two vars missing first end",
"both", "before-${ item after-${ more }", "item,more", false, true},
} {
var cases []testcase
if test.bindType == "both" {
test1 := test
test1.bindType = "role"
test2 := test
test2.bindType = "service"
cases = []testcase{test1, test2}
} else {
cases = []testcase{test}
}
for _, test := range cases {
test := test
t.Run(test.bindType+"--"+test.name, func(t *testing.T) {
t.Parallel()
valid, err := IsValidBindName(
test.bindType,
test.bindName,
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)
}
})
}
}
}
func generateID(t *testing.T) string {
t.Helper()
id, err := uuid.GenerateUUID()
require.NoError(t, err)
return id
}
func testStateStore(t *testing.T) *state.Store {
t.Helper()
gc, err := state.NewTombstoneGC(time.Second, time.Millisecond)
require.NoError(t, err)
return state.NewStateStore(gc)
}