Skip to content

Commit

Permalink
acl: binding rules evaluation (#15697)
Browse files Browse the repository at this point in the history
Binder provides an interface for binding claims and ACL roles/policies of Nomad.
  • Loading branch information
pkazmierczak authored Jan 10, 2023
1 parent 9db2d8a commit 4cd60c3
Show file tree
Hide file tree
Showing 8 changed files with 744 additions and 10 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
213 changes: 213 additions & 0 deletions lib/auth/oidc/binder.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 4cd60c3

Please sign in to comment.