6bfdb48560
Introduces two new public gRPC endpoints (`Login` and `Logout`) and includes refactoring of the equivalent net/rpc endpoints to enable the majority of logic to be reused (i.e. by extracting the `Binder` and `TokenWriter` types). This contains the OSS portions of the following enterprise commits: - 75fcdbfcfa6af21d7128cb2544829ead0b1df603 - bce14b714151af74a7f0110843d640204082630a - cc508b70fbf58eda144d9af3d71bd0f483985893
373 lines
9.4 KiB
Go
373 lines
9.4 KiB
Go
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)
|
|
}
|