2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-03-16 13:50:20 +00:00
|
|
|
package auth
|
2023-01-10 15:08:08 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2023-01-26 08:57:44 +00:00
|
|
|
Management bool
|
|
|
|
Roles []*structs.ACLTokenRoleLink
|
|
|
|
Policies []string
|
2023-01-10 15:08:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
2023-01-26 08:57:44 +00:00
|
|
|
case structs.ACLBindingRuleBindTypeManagement:
|
|
|
|
bindings.Management = true
|
|
|
|
bindings.Policies = nil
|
|
|
|
bindings.Roles = nil
|
|
|
|
return &bindings, nil
|
2023-01-10 15:08:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
2023-01-26 08:57:44 +00:00
|
|
|
case structs.ACLManagementToken:
|
|
|
|
valid = true
|
2023-01-10 15:08:08 +00:00
|
|
|
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
|
|
|
|
}
|