553 lines
16 KiB
Go
553 lines
16 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package vault
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
"github.com/hashicorp/vault/helper/identity"
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
"github.com/hashicorp/vault/sdk/helper/hclutil"
|
|
"github.com/hashicorp/vault/sdk/helper/identitytpl"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/mitchellh/copystructure"
|
|
)
|
|
|
|
const (
|
|
DenyCapability = "deny"
|
|
CreateCapability = "create"
|
|
ReadCapability = "read"
|
|
UpdateCapability = "update"
|
|
DeleteCapability = "delete"
|
|
ListCapability = "list"
|
|
SudoCapability = "sudo"
|
|
RootCapability = "root"
|
|
PatchCapability = "patch"
|
|
|
|
// Backwards compatibility
|
|
OldDenyPathPolicy = "deny"
|
|
OldReadPathPolicy = "read"
|
|
OldWritePathPolicy = "write"
|
|
OldSudoPathPolicy = "sudo"
|
|
)
|
|
|
|
const (
|
|
DenyCapabilityInt uint32 = 1 << iota
|
|
CreateCapabilityInt
|
|
ReadCapabilityInt
|
|
UpdateCapabilityInt
|
|
DeleteCapabilityInt
|
|
ListCapabilityInt
|
|
SudoCapabilityInt
|
|
PatchCapabilityInt
|
|
)
|
|
|
|
// Error constants for testing
|
|
const (
|
|
// ControlledCapabilityPolicySubsetError is thrown when a control group's controlled capabilities
|
|
// are not a subset of the policy's capabilities.
|
|
ControlledCapabilityPolicySubsetError = "control group factor capabilities must be a subset of the policy's capabilities"
|
|
)
|
|
|
|
type PolicyType uint32
|
|
|
|
const (
|
|
PolicyTypeACL PolicyType = iota
|
|
PolicyTypeRGP
|
|
PolicyTypeEGP
|
|
|
|
// Triggers a lookup in the map to figure out if ACL or RGP
|
|
PolicyTypeToken
|
|
)
|
|
|
|
func (p PolicyType) String() string {
|
|
switch p {
|
|
case PolicyTypeACL:
|
|
return "acl"
|
|
case PolicyTypeRGP:
|
|
return "rgp"
|
|
case PolicyTypeEGP:
|
|
return "egp"
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
var cap2Int = map[string]uint32{
|
|
DenyCapability: DenyCapabilityInt,
|
|
CreateCapability: CreateCapabilityInt,
|
|
ReadCapability: ReadCapabilityInt,
|
|
UpdateCapability: UpdateCapabilityInt,
|
|
DeleteCapability: DeleteCapabilityInt,
|
|
ListCapability: ListCapabilityInt,
|
|
SudoCapability: SudoCapabilityInt,
|
|
PatchCapability: PatchCapabilityInt,
|
|
}
|
|
|
|
type egpPath struct {
|
|
Path string `json:"path"`
|
|
Glob bool `json:"glob"`
|
|
}
|
|
|
|
// Policy is used to represent the policy specified by an ACL configuration.
|
|
type Policy struct {
|
|
sentinelPolicy
|
|
Name string `hcl:"name"`
|
|
Paths []*PathRules `hcl:"-"`
|
|
Raw string
|
|
Type PolicyType
|
|
Templated bool
|
|
namespace *namespace.Namespace
|
|
}
|
|
|
|
// ShallowClone returns a shallow clone of the policy. This should not be used
|
|
// if any of the reference-typed fields are going to be modified
|
|
func (p *Policy) ShallowClone() *Policy {
|
|
return &Policy{
|
|
sentinelPolicy: p.sentinelPolicy,
|
|
Name: p.Name,
|
|
Paths: p.Paths,
|
|
Raw: p.Raw,
|
|
Type: p.Type,
|
|
Templated: p.Templated,
|
|
namespace: p.namespace,
|
|
}
|
|
}
|
|
|
|
// PathRules represents a policy for a path in the namespace.
|
|
type PathRules struct {
|
|
Path string
|
|
Policy string
|
|
Permissions *ACLPermissions
|
|
IsPrefix bool
|
|
HasSegmentWildcards bool
|
|
Capabilities []string
|
|
|
|
// These keys are used at the top level to make the HCL nicer; we store in
|
|
// the ACLPermissions object though
|
|
MinWrappingTTLHCL interface{} `hcl:"min_wrapping_ttl"`
|
|
MaxWrappingTTLHCL interface{} `hcl:"max_wrapping_ttl"`
|
|
AllowedParametersHCL map[string][]interface{} `hcl:"allowed_parameters"`
|
|
DeniedParametersHCL map[string][]interface{} `hcl:"denied_parameters"`
|
|
RequiredParametersHCL []string `hcl:"required_parameters"`
|
|
MFAMethodsHCL []string `hcl:"mfa_methods"`
|
|
ControlGroupHCL *ControlGroupHCL `hcl:"control_group"`
|
|
}
|
|
|
|
type ControlGroupHCL struct {
|
|
TTL interface{} `hcl:"ttl"`
|
|
Factors map[string]*ControlGroupFactor `hcl:"factor"`
|
|
}
|
|
|
|
type ControlGroup struct {
|
|
TTL time.Duration
|
|
Factors []*ControlGroupFactor
|
|
}
|
|
|
|
func (c *ControlGroup) Clone() (*ControlGroup, error) {
|
|
clonedControlGroup, err := copystructure.Copy(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cg := clonedControlGroup.(*ControlGroup)
|
|
|
|
return cg, nil
|
|
}
|
|
|
|
type ControlGroupFactor struct {
|
|
Name string
|
|
Identity *IdentityFactor `hcl:"identity"`
|
|
ControlledCapabilities []string `hcl:"controlled_capabilities"`
|
|
}
|
|
|
|
type IdentityFactor struct {
|
|
GroupIDs []string `hcl:"group_ids"`
|
|
GroupNames []string `hcl:"group_names"`
|
|
ApprovalsRequired int `hcl:"approvals"`
|
|
}
|
|
|
|
type ACLPermissions struct {
|
|
CapabilitiesBitmap uint32
|
|
MinWrappingTTL time.Duration
|
|
MaxWrappingTTL time.Duration
|
|
AllowedParameters map[string][]interface{}
|
|
DeniedParameters map[string][]interface{}
|
|
RequiredParameters []string
|
|
MFAMethods []string
|
|
ControlGroup *ControlGroup
|
|
GrantingPoliciesMap map[uint32][]logical.PolicyInfo
|
|
}
|
|
|
|
func (p *ACLPermissions) Clone() (*ACLPermissions, error) {
|
|
ret := &ACLPermissions{
|
|
CapabilitiesBitmap: p.CapabilitiesBitmap,
|
|
MinWrappingTTL: p.MinWrappingTTL,
|
|
MaxWrappingTTL: p.MaxWrappingTTL,
|
|
RequiredParameters: p.RequiredParameters[:],
|
|
}
|
|
|
|
switch {
|
|
case p.AllowedParameters == nil:
|
|
case len(p.AllowedParameters) == 0:
|
|
ret.AllowedParameters = make(map[string][]interface{})
|
|
default:
|
|
clonedAllowed, err := copystructure.Copy(p.AllowedParameters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.AllowedParameters = clonedAllowed.(map[string][]interface{})
|
|
}
|
|
|
|
switch {
|
|
case p.DeniedParameters == nil:
|
|
case len(p.DeniedParameters) == 0:
|
|
ret.DeniedParameters = make(map[string][]interface{})
|
|
default:
|
|
clonedDenied, err := copystructure.Copy(p.DeniedParameters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.DeniedParameters = clonedDenied.(map[string][]interface{})
|
|
}
|
|
|
|
switch {
|
|
case p.MFAMethods == nil:
|
|
case len(p.MFAMethods) == 0:
|
|
ret.MFAMethods = []string{}
|
|
default:
|
|
clonedMFAMethods, err := copystructure.Copy(p.MFAMethods)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.MFAMethods = clonedMFAMethods.([]string)
|
|
}
|
|
|
|
switch {
|
|
case p.ControlGroup == nil:
|
|
default:
|
|
clonedControlGroup, err := copystructure.Copy(p.ControlGroup)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.ControlGroup = clonedControlGroup.(*ControlGroup)
|
|
}
|
|
|
|
switch {
|
|
case p.GrantingPoliciesMap == nil:
|
|
case len(p.GrantingPoliciesMap) == 0:
|
|
ret.GrantingPoliciesMap = make(map[uint32][]logical.PolicyInfo)
|
|
default:
|
|
clonedGrantingPoliciesMap, err := copystructure.Copy(p.GrantingPoliciesMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ret.GrantingPoliciesMap = clonedGrantingPoliciesMap.(map[uint32][]logical.PolicyInfo)
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func addGrantingPoliciesToMap(m map[uint32][]logical.PolicyInfo, policy *Policy, capabilitiesBitmap uint32) map[uint32][]logical.PolicyInfo {
|
|
if m == nil {
|
|
m = make(map[uint32][]logical.PolicyInfo)
|
|
}
|
|
|
|
// For all possible policies, check if the provided capabilities include
|
|
// them
|
|
for _, capability := range cap2Int {
|
|
if capabilitiesBitmap&capability == 0 {
|
|
continue
|
|
}
|
|
|
|
m[capability] = append(m[capability], logical.PolicyInfo{
|
|
Name: policy.Name,
|
|
NamespaceId: policy.namespace.ID,
|
|
NamespacePath: policy.namespace.Path,
|
|
Type: "acl",
|
|
})
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// ParseACLPolicy is used to parse the specified ACL rules into an
|
|
// intermediary set of policies, before being compiled into
|
|
// the ACL
|
|
func ParseACLPolicy(ns *namespace.Namespace, rules string) (*Policy, error) {
|
|
return parseACLPolicyWithTemplating(ns, rules, false, nil, nil)
|
|
}
|
|
|
|
// parseACLPolicyWithTemplating performs the actual work and checks whether we
|
|
// should perform substitutions. If performTemplating is true we know that it
|
|
// is templated so we don't check again, otherwise we check to see if it's a
|
|
// templated policy.
|
|
func parseACLPolicyWithTemplating(ns *namespace.Namespace, rules string, performTemplating bool, entity *identity.Entity, groups []*identity.Group) (*Policy, error) {
|
|
// Parse the rules
|
|
root, err := hcl.Parse(rules)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse policy: %w", err)
|
|
}
|
|
|
|
// Top-level item should be the object list
|
|
list, ok := root.Node.(*ast.ObjectList)
|
|
if !ok {
|
|
return nil, fmt.Errorf("failed to parse policy: does not contain a root object")
|
|
}
|
|
|
|
// Check for invalid top-level keys
|
|
valid := []string{
|
|
"name",
|
|
"path",
|
|
}
|
|
if err := hclutil.CheckHCLKeys(list, valid); err != nil {
|
|
return nil, fmt.Errorf("failed to parse policy: %w", err)
|
|
}
|
|
|
|
// Create the initial policy and store the raw text of the rules
|
|
p := Policy{
|
|
Raw: rules,
|
|
Type: PolicyTypeACL,
|
|
namespace: ns,
|
|
}
|
|
if err := hcl.DecodeObject(&p, list); err != nil {
|
|
return nil, fmt.Errorf("failed to parse policy: %w", err)
|
|
}
|
|
|
|
if o := list.Filter("path"); len(o.Items) > 0 {
|
|
if err := parsePaths(&p, o, performTemplating, entity, groups); err != nil {
|
|
return nil, fmt.Errorf("failed to parse policy: %w", err)
|
|
}
|
|
}
|
|
|
|
return &p, nil
|
|
}
|
|
|
|
func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, entity *identity.Entity, groups []*identity.Group) error {
|
|
paths := make([]*PathRules, 0, len(list.Items))
|
|
for _, item := range list.Items {
|
|
key := "path"
|
|
if len(item.Keys) > 0 {
|
|
key = item.Keys[0].Token.Value().(string)
|
|
}
|
|
|
|
// Check the path
|
|
if performTemplating {
|
|
_, templated, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
|
|
Mode: identitytpl.ACLTemplating,
|
|
String: key,
|
|
Entity: identity.ToSDKEntity(entity),
|
|
Groups: identity.ToSDKGroups(groups),
|
|
NamespaceID: result.namespace.ID,
|
|
})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
key = templated
|
|
} else {
|
|
hasTemplating, _, err := identitytpl.PopulateString(identitytpl.PopulateStringInput{
|
|
Mode: identitytpl.ACLTemplating,
|
|
ValidityCheckOnly: true,
|
|
String: key,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to validate policy templating: %w", err)
|
|
}
|
|
if hasTemplating {
|
|
result.Templated = true
|
|
}
|
|
}
|
|
|
|
valid := []string{
|
|
"comment",
|
|
"policy",
|
|
"capabilities",
|
|
"allowed_parameters",
|
|
"denied_parameters",
|
|
"required_parameters",
|
|
"min_wrapping_ttl",
|
|
"max_wrapping_ttl",
|
|
"mfa_methods",
|
|
"control_group",
|
|
}
|
|
if err := hclutil.CheckHCLKeys(item.Val, valid); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("path %q:", key))
|
|
}
|
|
|
|
var pc PathRules
|
|
|
|
// allocate memory so that DecodeObject can initialize the ACLPermissions struct
|
|
pc.Permissions = new(ACLPermissions)
|
|
|
|
pc.Path = key
|
|
|
|
if err := hcl.DecodeObject(&pc, item.Val); err != nil {
|
|
return multierror.Prefix(err, fmt.Sprintf("path %q:", key))
|
|
}
|
|
|
|
// Strip a leading '/' as paths in Vault start after the / in the API path
|
|
if len(pc.Path) > 0 && pc.Path[0] == '/' {
|
|
pc.Path = pc.Path[1:]
|
|
}
|
|
|
|
// Ensure we are using the full request path internally
|
|
pc.Path = result.namespace.Path + pc.Path
|
|
|
|
if strings.Contains(pc.Path, "+*") {
|
|
return fmt.Errorf("path %q: invalid use of wildcards ('+*' is forbidden)", pc.Path)
|
|
}
|
|
|
|
if pc.Path == "+" || strings.Count(pc.Path, "/+") > 0 || strings.HasPrefix(pc.Path, "+/") {
|
|
pc.HasSegmentWildcards = true
|
|
}
|
|
|
|
if strings.HasSuffix(pc.Path, "*") {
|
|
// If there are segment wildcards, don't actually strip the
|
|
// trailing asterisk, but don't want to hit the default case
|
|
if !pc.HasSegmentWildcards {
|
|
// Strip the glob character if found
|
|
pc.Path = strings.TrimSuffix(pc.Path, "*")
|
|
pc.IsPrefix = true
|
|
}
|
|
}
|
|
|
|
// Map old-style policies into capabilities
|
|
if len(pc.Policy) > 0 {
|
|
switch pc.Policy {
|
|
case OldDenyPathPolicy:
|
|
pc.Capabilities = []string{DenyCapability}
|
|
case OldReadPathPolicy:
|
|
pc.Capabilities = append(pc.Capabilities, []string{ReadCapability, ListCapability}...)
|
|
case OldWritePathPolicy:
|
|
pc.Capabilities = append(pc.Capabilities, []string{CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability}...)
|
|
case OldSudoPathPolicy:
|
|
pc.Capabilities = append(pc.Capabilities, []string{CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability}...)
|
|
default:
|
|
return fmt.Errorf("path %q: invalid policy %q", key, pc.Policy)
|
|
}
|
|
}
|
|
|
|
// Initialize the map
|
|
pc.Permissions.CapabilitiesBitmap = 0
|
|
for _, cap := range pc.Capabilities {
|
|
switch cap {
|
|
// If it's deny, don't include any other capability
|
|
case DenyCapability:
|
|
pc.Capabilities = []string{DenyCapability}
|
|
pc.Permissions.CapabilitiesBitmap = DenyCapabilityInt
|
|
goto PathFinished
|
|
case CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability, PatchCapability:
|
|
pc.Permissions.CapabilitiesBitmap |= cap2Int[cap]
|
|
default:
|
|
return fmt.Errorf("path %q: invalid capability %q", key, cap)
|
|
}
|
|
}
|
|
|
|
if pc.AllowedParametersHCL != nil {
|
|
pc.Permissions.AllowedParameters = make(map[string][]interface{}, len(pc.AllowedParametersHCL))
|
|
for k, v := range pc.AllowedParametersHCL {
|
|
pc.Permissions.AllowedParameters[strings.ToLower(k)] = v
|
|
}
|
|
}
|
|
if pc.DeniedParametersHCL != nil {
|
|
pc.Permissions.DeniedParameters = make(map[string][]interface{}, len(pc.DeniedParametersHCL))
|
|
|
|
for k, v := range pc.DeniedParametersHCL {
|
|
pc.Permissions.DeniedParameters[strings.ToLower(k)] = v
|
|
}
|
|
}
|
|
if pc.MinWrappingTTLHCL != nil {
|
|
dur, err := parseutil.ParseDurationSecond(pc.MinWrappingTTLHCL)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing min_wrapping_ttl: %w", err)
|
|
}
|
|
pc.Permissions.MinWrappingTTL = dur
|
|
}
|
|
if pc.MaxWrappingTTLHCL != nil {
|
|
dur, err := parseutil.ParseDurationSecond(pc.MaxWrappingTTLHCL)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing max_wrapping_ttl: %w", err)
|
|
}
|
|
pc.Permissions.MaxWrappingTTL = dur
|
|
}
|
|
if pc.MFAMethodsHCL != nil {
|
|
pc.Permissions.MFAMethods = make([]string, len(pc.MFAMethodsHCL))
|
|
copy(pc.Permissions.MFAMethods, pc.MFAMethodsHCL)
|
|
}
|
|
if pc.ControlGroupHCL != nil {
|
|
pc.Permissions.ControlGroup = new(ControlGroup)
|
|
if pc.ControlGroupHCL.TTL != nil {
|
|
dur, err := parseutil.ParseDurationSecond(pc.ControlGroupHCL.TTL)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing control group max ttl: %w", err)
|
|
}
|
|
pc.Permissions.ControlGroup.TTL = dur
|
|
}
|
|
var factors []*ControlGroupFactor
|
|
if pc.ControlGroupHCL.Factors != nil {
|
|
for key, factor := range pc.ControlGroupHCL.Factors {
|
|
// Although we only have one factor here, we need to check to make sure there is at least
|
|
// one factor defined in this factor block.
|
|
if factor.Identity == nil {
|
|
return errors.New("no control_group factor provided")
|
|
}
|
|
|
|
if factor.Identity.ApprovalsRequired <= 0 ||
|
|
(len(factor.Identity.GroupIDs) == 0 && len(factor.Identity.GroupNames) == 0) {
|
|
return errors.New("must provide more than one identity group and approvals > 0")
|
|
}
|
|
|
|
// Ensure that configured ControlledCapabilities for factor are a subset of the
|
|
// Capabilities of the policy.
|
|
if len(factor.ControlledCapabilities) > 0 {
|
|
var found bool
|
|
for _, controlledCapability := range factor.ControlledCapabilities {
|
|
found = false
|
|
for _, policyCap := range pc.Capabilities {
|
|
if controlledCapability == policyCap {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return errors.New(ControlledCapabilityPolicySubsetError)
|
|
}
|
|
}
|
|
}
|
|
|
|
factors = append(factors, &ControlGroupFactor{
|
|
Name: key,
|
|
Identity: factor.Identity,
|
|
ControlledCapabilities: factor.ControlledCapabilities,
|
|
})
|
|
}
|
|
}
|
|
if len(factors) == 0 {
|
|
return errors.New("no control group factors provided")
|
|
}
|
|
pc.Permissions.ControlGroup.Factors = factors
|
|
}
|
|
if pc.Permissions.MinWrappingTTL != 0 &&
|
|
pc.Permissions.MaxWrappingTTL != 0 &&
|
|
pc.Permissions.MaxWrappingTTL < pc.Permissions.MinWrappingTTL {
|
|
return errors.New("max_wrapping_ttl cannot be less than min_wrapping_ttl")
|
|
}
|
|
if len(pc.RequiredParametersHCL) > 0 {
|
|
pc.Permissions.RequiredParameters = pc.RequiredParametersHCL[:]
|
|
}
|
|
|
|
PathFinished:
|
|
paths = append(paths, &pc)
|
|
}
|
|
|
|
result.Paths = paths
|
|
return nil
|
|
}
|