193 lines
5.9 KiB
Go
193 lines
5.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/go-bexpr"
|
|
"github.com/hashicorp/go-memdb"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/consul/authmethod"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/lib/template"
|
|
)
|
|
|
|
// Binder is responsible for collecting the ACL roles, service identities, node
|
|
// identities, and enterprise metadata 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 and in the
|
|
// case of enterprise, namespace rules.
|
|
type Binder struct {
|
|
store BinderStateStore
|
|
datacenter string
|
|
}
|
|
|
|
// NewBinder creates a Binder with the given state store and datacenter.
|
|
func NewBinder(store BinderStateStore, datacenter string) *Binder {
|
|
return &Binder{store, datacenter}
|
|
}
|
|
|
|
// BinderStateStore is the subset of state store methods used by the binder.
|
|
type BinderStateStore interface {
|
|
ACLBindingRuleList(ws memdb.WatchSet, methodName string, entMeta *acl.EnterpriseMeta) (uint64, structs.ACLBindingRules, error)
|
|
ACLRoleGetByName(ws memdb.WatchSet, roleName string, entMeta *acl.EnterpriseMeta) (uint64, *structs.ACLRole, error)
|
|
}
|
|
|
|
// Bindings contains the ACL roles, service identities, node identities and
|
|
// enterprise meta to be assigned to the created token.
|
|
type Bindings struct {
|
|
Roles []structs.ACLTokenRoleLink
|
|
ServiceIdentities []*structs.ACLServiceIdentity
|
|
NodeIdentities []*structs.ACLNodeIdentity
|
|
EnterpriseMeta acl.EnterpriseMeta
|
|
}
|
|
|
|
// 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.ServiceIdentities) == 0 &&
|
|
len(b.NodeIdentities) == 0 &&
|
|
len(b.Roles) == 0
|
|
}
|
|
|
|
// Bind collects the ACL roles, service identities, etc. to be assigned to the
|
|
// created token.
|
|
func (b *Binder) Bind(authMethod *structs.ACLAuthMethod, verifiedIdentity *authmethod.Identity) (*Bindings, error) {
|
|
var (
|
|
bindings Bindings
|
|
err error
|
|
)
|
|
if bindings.EnterpriseMeta, err = bindEnterpriseMeta(authMethod, verifiedIdentity); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Load the auth method's binding rules.
|
|
_, rules, err := b.store.ACLBindingRuleList(nil, authMethod.Name, &authMethod.EnterpriseMeta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Find the rules with selectors that match the identity's fields.
|
|
matchingRules := make(structs.ACLBindingRules, 0, len(rules))
|
|
for _, rule := range rules {
|
|
if doesSelectorMatch(rule.Selector, verifiedIdentity.SelectableFields) {
|
|
matchingRules = append(matchingRules, rule)
|
|
}
|
|
}
|
|
if len(matchingRules) == 0 {
|
|
return &bindings, nil
|
|
}
|
|
|
|
// Compute role, service identity, or node identity names by interpolating
|
|
// the identity's projected variables into the rule BindName templates.
|
|
for _, rule := range matchingRules {
|
|
bindName, valid, err := computeBindName(rule.BindType, rule.BindName, verifiedIdentity.ProjectedVars)
|
|
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.BindingRuleBindTypeService:
|
|
bindings.ServiceIdentities = append(bindings.ServiceIdentities, &structs.ACLServiceIdentity{
|
|
ServiceName: bindName,
|
|
})
|
|
|
|
case structs.BindingRuleBindTypeNode:
|
|
bindings.NodeIdentities = append(bindings.NodeIdentities, &structs.ACLNodeIdentity{
|
|
NodeName: bindName,
|
|
Datacenter: b.datacenter,
|
|
})
|
|
|
|
case structs.BindingRuleBindTypeRole:
|
|
_, role, err := b.store.ACLRoleGetByName(nil, bindName, &bindings.EnterpriseMeta)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if role != nil {
|
|
bindings.Roles = append(bindings.Roles, structs.ACLTokenRoleLink{
|
|
ID: role.ID,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return &bindings, nil
|
|
}
|
|
|
|
// IsValidBindName returns whether the given BindName template produces valid
|
|
// results when interpolating the auth method's available variables.
|
|
func IsValidBindName(bindType, bindName string, availableVariables []string) (bool, error) {
|
|
if bindType == "" || bindName == "" {
|
|
return false, nil
|
|
}
|
|
|
|
fakeVarMap := make(map[string]string)
|
|
for _, v := range availableVariables {
|
|
fakeVarMap[v] = "fake"
|
|
}
|
|
|
|
_, valid, err := computeBindName(bindType, bindName, fakeVarMap)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return valid, 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, projectedVars map[string]string) (string, bool, error) {
|
|
bindName, err := template.InterpolateHIL(bindName, projectedVars, true)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
|
|
var valid bool
|
|
switch bindType {
|
|
case structs.BindingRuleBindTypeService:
|
|
valid = acl.IsValidServiceIdentityName(bindName)
|
|
case structs.BindingRuleBindTypeNode:
|
|
valid = acl.IsValidNodeIdentityName(bindName)
|
|
case structs.BindingRuleBindTypeRole:
|
|
valid = acl.IsValidRoleName(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.CreateEvaluatorForType(selector, nil, selectableVars)
|
|
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
|
|
}
|