acl: binding rules evaluation (#15697)
Binder provides an interface for binding claims and ACL roles/policies of Nomad.
This commit is contained in:
parent
dee653d459
commit
be36a1924f
3
go.mod
3
go.mod
|
@ -72,6 +72,7 @@ require (
|
|||
github.com/hashicorp/golang-lru v0.5.4
|
||||
github.com/hashicorp/hcl v1.0.1-vault-3
|
||||
github.com/hashicorp/hcl/v2 v2.9.2-0.20220525143345-ab3cae0737bc
|
||||
github.com/hashicorp/hil v0.0.0-20210521165536-27a72121fd40
|
||||
github.com/hashicorp/logutils v1.0.0
|
||||
github.com/hashicorp/memberlist v0.5.0
|
||||
github.com/hashicorp/net-rpc-msgpackrpc v0.0.0-20151116020338-a14192a58a69
|
||||
|
@ -231,7 +232,7 @@ require (
|
|||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/pointerstructure v1.2.1 // indirect
|
||||
github.com/mitchellh/pointerstructure v1.2.1
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mrunalp/fileutils v0.5.0 // indirect
|
||||
github.com/muesli/reflow v0.3.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -768,6 +768,8 @@ github.com/hashicorp/hcl v1.0.1-0.20201016140508-a07e7d50bbee h1:8B4HqvMUtYSjsGk
|
|||
github.com/hashicorp/hcl v1.0.1-0.20201016140508-a07e7d50bbee/go.mod h1:gwlu9+/P9MmKtYrMsHeFRZPXj2CTPm11TDnMeaRHS7g=
|
||||
github.com/hashicorp/hcl/v2 v2.9.2-0.20220525143345-ab3cae0737bc h1:32lGaCPq5JPYNgFFTjl/cTIar9UWWxCbimCs5G2hMHg=
|
||||
github.com/hashicorp/hcl/v2 v2.9.2-0.20220525143345-ab3cae0737bc/go.mod h1:odKNpEeZv3COD+++SQcPyACuKOlM5eBoQlzRyN5utIQ=
|
||||
github.com/hashicorp/hil v0.0.0-20210521165536-27a72121fd40 h1:ExwaL+hUy1ys2AWDbsbh/lxQS2EVCYxuj0LoyLTdB3Y=
|
||||
github.com/hashicorp/hil v0.0.0-20210521165536-27a72121fd40/go.mod h1:n2TSygSNwsLJ76m8qFXTSc7beTb+auJxYdqrnoqwZWE=
|
||||
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
|
||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
||||
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-bexpr"
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/hil"
|
||||
"github.com/hashicorp/hil/ast"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
// Binder is responsible for collecting the ACL roles and policies to be
|
||||
// assigned to a token generated as a result of "logging in" via an auth method.
|
||||
//
|
||||
// It does so by applying the auth method's configured binding rules.
|
||||
type Binder struct {
|
||||
store BinderStateStore
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
// Claims is the format of this Identity suitable for selection
|
||||
// with a binding rule.
|
||||
Claims interface{}
|
||||
|
||||
// ClaimMappings is the format of this Identity suitable for interpolation in a
|
||||
// bind name within a binding rule.
|
||||
ClaimMappings map[string]string
|
||||
}
|
||||
|
||||
// NewBinder creates a Binder with the given state store.
|
||||
func NewBinder(store BinderStateStore) *Binder {
|
||||
return &Binder{store}
|
||||
}
|
||||
|
||||
// BinderStateStore is the subset of state store methods used by the binder.
|
||||
type BinderStateStore interface {
|
||||
GetACLBindingRulesByAuthMethod(ws memdb.WatchSet, authMethod string) (memdb.ResultIterator, error)
|
||||
GetACLRoleByName(ws memdb.WatchSet, roleName string) (*structs.ACLRole, error)
|
||||
ACLPolicyByName(ws memdb.WatchSet, name string) (*structs.ACLPolicy, error)
|
||||
}
|
||||
|
||||
// Bindings contains the ACL roles and policies to be assigned to the created
|
||||
// token.
|
||||
type Bindings struct {
|
||||
Roles []*structs.ACLTokenRoleLink
|
||||
Policies []string
|
||||
}
|
||||
|
||||
// None indicates that the resulting bindings would not give the created token
|
||||
// access to any resources.
|
||||
func (b *Bindings) None() bool {
|
||||
if b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return len(b.Policies) == 0 && len(b.Roles) == 0
|
||||
}
|
||||
|
||||
// Bind collects the ACL roles and policies to be assigned to the created token.
|
||||
func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, identity *Identity) (*Bindings, error) {
|
||||
var (
|
||||
bindings Bindings
|
||||
err error
|
||||
)
|
||||
|
||||
// Load the auth method's binding rules.
|
||||
rulesIterator, err := b.store.GetACLBindingRulesByAuthMethod(nil, authMethod.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find the rules with selectors that match the identity's fields.
|
||||
matchingRules := []*structs.ACLBindingRule{}
|
||||
for {
|
||||
raw := rulesIterator.Next()
|
||||
if raw == nil {
|
||||
break
|
||||
}
|
||||
rule := raw.(*structs.ACLBindingRule)
|
||||
if doesSelectorMatch(rule.Selector, identity.Claims) {
|
||||
matchingRules = append(matchingRules, rule)
|
||||
}
|
||||
}
|
||||
if len(matchingRules) == 0 {
|
||||
return &bindings, nil
|
||||
}
|
||||
|
||||
// Compute role or policy names by interpolating the identity's claim
|
||||
// mappings into the rule BindName templates.
|
||||
for _, rule := range matchingRules {
|
||||
bindName, valid, err := computeBindName(rule.BindType, rule.BindName, identity.ClaimMappings)
|
||||
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.ACLBindingRuleBindTypeRole:
|
||||
role, err := b.store.GetACLRoleByName(nil, bindName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if role != nil {
|
||||
bindings.Roles = append(bindings.Roles, &structs.ACLTokenRoleLink{
|
||||
ID: role.ID,
|
||||
})
|
||||
}
|
||||
case structs.ACLBindingRuleBindTypePolicy:
|
||||
policy, err := b.store.ACLPolicyByName(nil, bindName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if policy != nil {
|
||||
bindings.Policies = append(bindings.Policies, policy.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &bindings, nil
|
||||
}
|
||||
|
||||
// computeBindName processes the HIL for the provided bind type+name using the
|
||||
// projected variables.
|
||||
//
|
||||
// - 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, claimMappings map[string]string) (string, bool, error) {
|
||||
bindName, err := interpolateHIL(bindName, claimMappings, true)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
var valid bool
|
||||
switch bindType {
|
||||
case structs.ACLBindingRuleBindTypePolicy:
|
||||
valid = structs.ValidPolicyName.MatchString(bindName)
|
||||
case structs.ACLBindingRuleBindTypeRole:
|
||||
valid = structs.ValidACLRoleName.MatchString(bindName)
|
||||
default:
|
||||
return "", false, fmt.Errorf("unknown binding rule bind type: %s", bindType)
|
||||
}
|
||||
|
||||
return bindName, valid, nil
|
||||
}
|
||||
|
||||
// doesSelectorMatch checks that a single selector matches the provided vars.
|
||||
func doesSelectorMatch(selector string, selectableVars interface{}) bool {
|
||||
if selector == "" {
|
||||
return true // catch-all
|
||||
}
|
||||
|
||||
eval, err := bexpr.CreateEvaluator(selector)
|
||||
if err != nil {
|
||||
return false // fails to match if selector is invalid
|
||||
}
|
||||
|
||||
result, err := eval.Evaluate(selectableVars)
|
||||
if err != nil {
|
||||
return false // fails to match if evaluation fails
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// interpolateHIL processes the string as if it were HIL and interpolates only
|
||||
// the provided string->string map as possible variables.
|
||||
func interpolateHIL(s string, vars map[string]string, lowercase bool) (string, error) {
|
||||
if !strings.Contains(s, "${") {
|
||||
// Skip going to the trouble of parsing something that has no HIL.
|
||||
return s, nil
|
||||
}
|
||||
|
||||
tree, err := hil.Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vm := make(map[string]ast.Variable)
|
||||
for k, v := range vars {
|
||||
if lowercase {
|
||||
v = strings.ToLower(v)
|
||||
}
|
||||
vm[k] = ast.Variable{
|
||||
Type: ast.TypeString,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
|
||||
config := &hil.EvalConfig{
|
||||
GlobalScope: &ast.BasicScope{
|
||||
VarMap: vm,
|
||||
},
|
||||
}
|
||||
|
||||
result, err := hil.Eval(tree, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if result.Type != hil.TypeString {
|
||||
return "", fmt.Errorf("generated unexpected hil type: %s", result.Type)
|
||||
}
|
||||
|
||||
return result.Value.(string), nil
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/state"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
func TestBinder_Bind(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testStore := state.TestStateStore(t)
|
||||
testBind := NewBinder(testStore)
|
||||
|
||||
// create an authMethod method and insert into the state store
|
||||
authMethod := mock.ACLAuthMethod()
|
||||
must.NoError(t, testStore.UpsertACLAuthMethods(0, []*structs.ACLAuthMethod{authMethod}))
|
||||
|
||||
// create some roles and insert into the state store
|
||||
targetRole := &structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "vim-role",
|
||||
}
|
||||
otherRole := &structs.ACLRole{
|
||||
ID: uuid.Generate(),
|
||||
Name: "frontend-engineers",
|
||||
}
|
||||
must.NoError(t, testStore.UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 0, []*structs.ACLRole{targetRole, otherRole}, true,
|
||||
))
|
||||
|
||||
// create binding rules and insert into the state store
|
||||
bindingRules := []*structs.ACLBindingRule{
|
||||
{
|
||||
ID: uuid.Generate(),
|
||||
Selector: "role==engineer",
|
||||
BindType: structs.ACLBindingRuleBindTypeRole,
|
||||
BindName: "${editor}-role",
|
||||
AuthMethod: authMethod.Name,
|
||||
},
|
||||
{
|
||||
ID: uuid.Generate(),
|
||||
Selector: "role==engineer",
|
||||
BindType: structs.ACLBindingRuleBindTypeRole,
|
||||
BindName: "this-role-does-not-exist",
|
||||
AuthMethod: authMethod.Name,
|
||||
},
|
||||
{
|
||||
ID: uuid.Generate(),
|
||||
Selector: "language==js",
|
||||
BindType: structs.ACLBindingRuleBindTypeRole,
|
||||
BindName: otherRole.Name,
|
||||
AuthMethod: authMethod.Name,
|
||||
},
|
||||
}
|
||||
must.NoError(t, testStore.UpsertACLBindingRules(0, bindingRules, true))
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
authMethod *structs.ACLAuthMethod
|
||||
identity *Identity
|
||||
want *Bindings
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"empty identity",
|
||||
authMethod,
|
||||
&Identity{},
|
||||
&Bindings{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"role",
|
||||
authMethod,
|
||||
&Identity{
|
||||
Claims: map[string]string{
|
||||
"role": "engineer",
|
||||
"language": "go",
|
||||
},
|
||||
ClaimMappings: map[string]string{
|
||||
"editor": "vim",
|
||||
},
|
||||
},
|
||||
&Bindings{Roles: []*structs.ACLTokenRoleLink{{ID: targetRole.ID}}},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := testBind.Bind(tt.authMethod, tt.identity)
|
||||
if tt.wantErr {
|
||||
must.Error(t, err)
|
||||
} else {
|
||||
must.NoError(t, err)
|
||||
}
|
||||
must.Eq(t, got, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_computeBindName(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
bindType string
|
||||
bindName string
|
||||
claimMappings map[string]string
|
||||
wantName string
|
||||
wantTrue bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"valid bind name and type",
|
||||
structs.ACLBindingRuleBindTypeRole,
|
||||
"cluster-admin",
|
||||
map[string]string{"cluster-admin": "root"},
|
||||
"cluster-admin",
|
||||
true,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid type",
|
||||
"amazing",
|
||||
"cluster-admin",
|
||||
map[string]string{"cluster-admin": "root"},
|
||||
"",
|
||||
false,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, got1, err := computeBindName(tt.bindType, tt.bindName, tt.claimMappings)
|
||||
if tt.wantErr {
|
||||
must.NotNil(t, err)
|
||||
}
|
||||
must.Eq(t, got, tt.wantName)
|
||||
must.Eq(t, got1, tt.wantTrue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_doesSelectorMatch(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
selector string
|
||||
selectableVars interface{}
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"catch-all",
|
||||
"",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"valid selector but no selectable vars",
|
||||
"nomad_engineering_team in Groups",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"valid selector and successful evaluation",
|
||||
"nomad_engineering_team in Groups",
|
||||
map[string][]string{"Groups": {"nomad_sales_team", "nomad_engineering_team"}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
must.Eq(t, doesSelectorMatch(tt.selector, tt.selectableVars), tt.want)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/pointerstructure"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
// SelectorData returns the data for go-bexpr for selector evaluation.
|
||||
func SelectorData(
|
||||
am *structs.ACLAuthMethod, idClaims, userClaims json.RawMessage) (*structs.ACLAuthClaims, error) {
|
||||
|
||||
// Extract the claims into a map[string]interface{}
|
||||
var all map[string]interface{}
|
||||
if err := json.Unmarshal(idClaims, &all); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure the issuer and subscriber data does not get overwritten.
|
||||
if len(userClaims) > 0 {
|
||||
|
||||
iss, issOk := all["iss"]
|
||||
sub, subOk := all["sub"]
|
||||
|
||||
if err := json.Unmarshal(userClaims, &all); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if issOk {
|
||||
all["iss"] = iss
|
||||
}
|
||||
if subOk {
|
||||
all["sub"] = sub
|
||||
}
|
||||
}
|
||||
|
||||
return extractClaims(am, all)
|
||||
}
|
||||
|
||||
// extractClaims takes the claim mapping configuration of the OIDC auth method,
|
||||
// extracts the claims, and returns a map of data that can be used with
|
||||
// go-bexpr.
|
||||
func extractClaims(
|
||||
am *structs.ACLAuthMethod, all map[string]interface{}) (*structs.ACLAuthClaims, error) {
|
||||
|
||||
values, err := extractMappings(all, am.Config.ClaimMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list, err := extractListMappings(all, am.Config.ListClaimMappings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &structs.ACLAuthClaims{
|
||||
Value: values,
|
||||
List: list,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractMappings extracts the string value mappings.
|
||||
func extractMappings(
|
||||
all map[string]interface{}, mapping map[string]string) (map[string]string, error) {
|
||||
|
||||
result := make(map[string]string)
|
||||
for source, target := range mapping {
|
||||
rawValue := getClaim(all, source)
|
||||
if rawValue == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
strValue, ok := stringifyClaimValue(rawValue)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error converting claim '%s' to string from unknown type %T",
|
||||
source, rawValue)
|
||||
}
|
||||
|
||||
result[target] = strValue
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractListMappings builds a metadata map of string list values from a set
|
||||
// of claims and claims mappings. The referenced claims must be strings and
|
||||
// the claims mappings must be of the structure:
|
||||
//
|
||||
// {
|
||||
// "/some/claim/pointer": "metadata_key1",
|
||||
// "another_claim": "metadata_key2",
|
||||
// ...
|
||||
// }
|
||||
func extractListMappings(
|
||||
all map[string]interface{}, mappings map[string]string) (map[string][]string, error) {
|
||||
|
||||
result := make(map[string][]string)
|
||||
for source, target := range mappings {
|
||||
rawValue := getClaim(all, source)
|
||||
if rawValue == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
rawList, ok := normalizeList(rawValue)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%q list claim could not be converted to string list", source)
|
||||
}
|
||||
|
||||
list := make([]string, 0, len(rawList))
|
||||
for _, raw := range rawList {
|
||||
value, ok := stringifyClaimValue(raw)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("value %v in %q list claim could not be parsed as string",
|
||||
raw, source)
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
list = append(list, value)
|
||||
}
|
||||
|
||||
result[target] = list
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getClaim returns a claim value from allClaims given a provided claim string.
|
||||
// If this string is a valid JSONPointer, it will be interpreted as such to
|
||||
// locate the claim. Otherwise, the claim string will be used directly.
|
||||
//
|
||||
// There is no fixup done to the returned data type here. That happens a layer
|
||||
// up in the caller.
|
||||
func getClaim(all map[string]interface{}, claim string) interface{} {
|
||||
if !strings.HasPrefix(claim, "/") {
|
||||
return all[claim]
|
||||
}
|
||||
|
||||
val, err := pointerstructure.Get(all, claim)
|
||||
if err != nil {
|
||||
// We silently drop the error since keys that are invalid
|
||||
// just have no values.
|
||||
return nil
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
// stringifyClaimValue will try to convert the provided raw value into a
|
||||
// faithful string representation of that value per these rules:
|
||||
//
|
||||
// - strings => unchanged
|
||||
// - bool => "true" / "false"
|
||||
// - json.Number => String()
|
||||
// - float32/64 => truncated to int64 and then formatted as an ascii string
|
||||
// - intXX/uintXX => casted to int64 and then formatted as an ascii string
|
||||
//
|
||||
// If successful the string value and true are returned. otherwise an empty
|
||||
// string and false are returned.
|
||||
func stringifyClaimValue(rawValue interface{}) (string, bool) {
|
||||
switch v := rawValue.(type) {
|
||||
case string:
|
||||
return v, true
|
||||
case bool:
|
||||
return strconv.FormatBool(v), true
|
||||
case json.Number:
|
||||
return v.String(), true
|
||||
case float64:
|
||||
// The claims unmarshalled by go-oidc don't use UseNumber, so
|
||||
// they'll come in as float64 instead of an integer or json.Number.
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
|
||||
// The numerical type cases following here are only here for the sake
|
||||
// of numerical type completion. Everything is truncated to an integer
|
||||
// before being stringified.
|
||||
case float32:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case int8:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case int16:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case int32:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10), true
|
||||
case int:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case uint8:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case uint16:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case uint32:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case uint64:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
case uint:
|
||||
return strconv.FormatInt(int64(v), 10), true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeList takes an item or a slice and returns a slice. This is useful
|
||||
// when providers are expected to return a list (typically of strings) but
|
||||
// reduce it to a non-slice type when the list count is 1.
|
||||
//
|
||||
// There is no fixup done to elements of the returned slice here. That happens
|
||||
// a layer up in the caller.
|
||||
func normalizeList(raw interface{}) ([]interface{}, bool) {
|
||||
switch v := raw.(type) {
|
||||
case []interface{}:
|
||||
return v, true
|
||||
case string, // note: this list should be the same as stringifyClaimValue
|
||||
bool,
|
||||
json.Number,
|
||||
float64,
|
||||
float32,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
int64,
|
||||
int,
|
||||
uint8,
|
||||
uint16,
|
||||
uint32,
|
||||
uint64,
|
||||
uint:
|
||||
return []interface{}{v}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package oidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/shoenig/test/must"
|
||||
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
)
|
||||
|
||||
func TestSelectorData(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Mapping map[string]string
|
||||
ListMapping map[string]string
|
||||
Data map[string]interface{}
|
||||
Expected *structs.ACLAuthClaims
|
||||
}{
|
||||
{
|
||||
"no mappings",
|
||||
nil,
|
||||
nil,
|
||||
map[string]interface{}{"iss": "https://hashicorp.com"},
|
||||
&structs.ACLAuthClaims{
|
||||
Value: map[string]string{},
|
||||
List: map[string][]string{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"key",
|
||||
map[string]string{"iss": "issuer"},
|
||||
nil,
|
||||
map[string]interface{}{"iss": "https://hashicorp.com"},
|
||||
&structs.ACLAuthClaims{
|
||||
Value: map[string]string{"issuer": "https://hashicorp.com"},
|
||||
List: map[string][]string{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"key doesn't exist",
|
||||
map[string]string{"iss": "issuer"},
|
||||
nil,
|
||||
map[string]interface{}{"nope": "https://hashicorp.com"},
|
||||
&structs.ACLAuthClaims{
|
||||
Value: map[string]string{},
|
||||
List: map[string][]string{},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
"list",
|
||||
nil,
|
||||
map[string]string{"groups": "g"},
|
||||
map[string]interface{}{
|
||||
"groups": []interface{}{
|
||||
"A", 42, false,
|
||||
},
|
||||
},
|
||||
&structs.ACLAuthClaims{
|
||||
Value: map[string]string{},
|
||||
List: map[string][]string{
|
||||
"g": {"A", "42", "false"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.Name, func(t *testing.T) {
|
||||
|
||||
am := &structs.ACLAuthMethod{
|
||||
Config: &structs.ACLAuthMethodConfig{
|
||||
ClaimMappings: tt.Mapping,
|
||||
ListClaimMappings: tt.ListMapping,
|
||||
},
|
||||
}
|
||||
|
||||
// Marshal our test data
|
||||
jsonRaw, err := json.Marshal(tt.Data)
|
||||
must.NoError(t, err)
|
||||
|
||||
// Get real selector data
|
||||
actual, err := SelectorData(am, jsonRaw, nil)
|
||||
must.NoError(t, err)
|
||||
must.Eq(t, actual, tt.Expected)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -168,11 +168,11 @@ const (
|
|||
)
|
||||
|
||||
var (
|
||||
// validACLRoleName is used to validate an ACL role name.
|
||||
validACLRoleName = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
// ValidACLRoleName is used to validate an ACL role name.
|
||||
ValidACLRoleName = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
|
||||
// validACLAuthMethodName is used to validate an ACL auth method name.
|
||||
validACLAuthMethod = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
ValidACLAuthMethod = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
)
|
||||
|
||||
// ACLTokenRoleLink is used to link an ACL token to an ACL role. The ACL token
|
||||
|
@ -406,7 +406,7 @@ func (a *ACLRole) Validate() error {
|
|||
|
||||
var mErr multierror.Error
|
||||
|
||||
if !validACLRoleName.MatchString(a.Name) {
|
||||
if !ValidACLRoleName.MatchString(a.Name) {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
|
||||
}
|
||||
|
||||
|
@ -777,7 +777,7 @@ func (a *ACLAuthMethod) Merge(b *ACLAuthMethod) {
|
|||
func (a *ACLAuthMethod) Validate(minTTL, maxTTL time.Duration) error {
|
||||
var mErr multierror.Error
|
||||
|
||||
if !validACLAuthMethod.MatchString(a.Name) {
|
||||
if !ValidACLAuthMethod.MatchString(a.Name) {
|
||||
mErr.Errors = append(mErr.Errors, fmt.Errorf("invalid name '%s'", a.Name))
|
||||
}
|
||||
|
||||
|
@ -829,6 +829,14 @@ func (a *ACLAuthMethodConfig) Copy() *ACLAuthMethodConfig {
|
|||
return c
|
||||
}
|
||||
|
||||
// ACLAuthClaims is the claim mapping of the OIDC auth method in a format that
|
||||
// can be used with go-bexpr. This structure is used during rule binding
|
||||
// evaluation.
|
||||
type ACLAuthClaims struct {
|
||||
Value map[string]string
|
||||
List map[string][]string
|
||||
}
|
||||
|
||||
// ACLAuthMethodStub is used for listing ACL auth methods
|
||||
type ACLAuthMethodStub struct {
|
||||
Name string
|
||||
|
@ -916,7 +924,7 @@ type ACLWhoAmIResponse struct {
|
|||
// ACL Roles and Policies.
|
||||
type ACLBindingRule struct {
|
||||
|
||||
// ID is an internally generated UUID for this role and is controlled by
|
||||
// ID is an internally generated UUID for this rule and is controlled by
|
||||
// Nomad.
|
||||
ID string
|
||||
|
||||
|
|
|
@ -50,8 +50,8 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// validPolicyName is used to validate a policy name
|
||||
validPolicyName = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
// ValidPolicyName is used to validate a policy name
|
||||
ValidPolicyName = regexp.MustCompile("^[a-zA-Z0-9-]{1,128}$")
|
||||
|
||||
// b32 is a lowercase base32 encoding for use in URL friendly service hashes
|
||||
b32 = base32.NewEncoding(strings.ToLower("abcdefghijklmnopqrstuvwxyz234567"))
|
||||
|
@ -11975,7 +11975,7 @@ func (a *ACLPolicy) Stub() *ACLPolicyListStub {
|
|||
|
||||
func (a *ACLPolicy) Validate() error {
|
||||
var mErr multierror.Error
|
||||
if !validPolicyName.MatchString(a.Name) {
|
||||
if !ValidPolicyName.MatchString(a.Name) {
|
||||
err := fmt.Errorf("invalid name '%s'", a.Name)
|
||||
mErr.Errors = append(mErr.Errors, err)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue