From 4cd60c3560f194b9ca28c00ed9129ea8287f47cc Mon Sep 17 00:00:00 2001 From: Piotr Kazmierczak <470696+pkazmierczak@users.noreply.github.com> Date: Tue, 10 Jan 2023 16:08:08 +0100 Subject: [PATCH] acl: binding rules evaluation (#15697) Binder provides an interface for binding claims and ACL roles/policies of Nomad. --- go.mod | 3 +- go.sum | 2 + lib/auth/oidc/binder.go | 213 +++++++++++++++++++++++++++++++ lib/auth/oidc/binder_test.go | 181 ++++++++++++++++++++++++++ lib/auth/oidc/claims.go | 238 +++++++++++++++++++++++++++++++++++ lib/auth/oidc/claims_test.go | 91 ++++++++++++++ nomad/structs/acl.go | 20 ++- nomad/structs/structs.go | 6 +- 8 files changed, 744 insertions(+), 10 deletions(-) create mode 100644 lib/auth/oidc/binder.go create mode 100644 lib/auth/oidc/binder_test.go create mode 100644 lib/auth/oidc/claims.go create mode 100644 lib/auth/oidc/claims_test.go diff --git a/go.mod b/go.mod index 9fdbf288e..4ac872854 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 0e5c08e9d..59ac8c921 100644 --- a/go.sum +++ b/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= diff --git a/lib/auth/oidc/binder.go b/lib/auth/oidc/binder.go new file mode 100644 index 000000000..ceb733be0 --- /dev/null +++ b/lib/auth/oidc/binder.go @@ -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 +} diff --git a/lib/auth/oidc/binder_test.go b/lib/auth/oidc/binder_test.go new file mode 100644 index 000000000..953959710 --- /dev/null +++ b/lib/auth/oidc/binder_test.go @@ -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) + }) + } +} diff --git a/lib/auth/oidc/claims.go b/lib/auth/oidc/claims.go new file mode 100644 index 000000000..e214a7656 --- /dev/null +++ b/lib/auth/oidc/claims.go @@ -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 + } +} diff --git a/lib/auth/oidc/claims_test.go b/lib/auth/oidc/claims_test.go new file mode 100644 index 000000000..fe584330f --- /dev/null +++ b/lib/auth/oidc/claims_test.go @@ -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) + }) + } +} diff --git a/nomad/structs/acl.go b/nomad/structs/acl.go index f0eeb962d..8ae730f52 100644 --- a/nomad/structs/acl.go +++ b/nomad/structs/acl.go @@ -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 diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index f91a23591..288dd5145 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -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) }