You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/perms/acl.go

604 lines
21 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package perms
import (
"strings"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
)
// AclGrant is used to decouple API-based grants from those we utilize for ACLs.
// Notably it uses a single ID per grant instead of multiple IDs.
type AclGrant struct {
// The scope ID of the role that sourced this grant
RoleScopeId string
// The parent scope ID of the role that sourced this grant
RoleParentScopeId string
// The grant's applied scope ID
GrantScopeId string
// The ID to use
Id string
// The type, if provided
Type resource.Type
// The set of actions being granted
ActionSet ActionSet
// The set of output fields granted
OutputFields *OutputFields
}
// Actions returns the actions as a slice from the internal map, along with the
// string representations of those actions.
func (ag AclGrant) Actions() ([]action.Type, []string) {
return ag.ActionSet.Actions()
}
func (ag AclGrant) Clone() AclGrant {
ret := AclGrant{
RoleScopeId: ag.RoleScopeId,
RoleParentScopeId: ag.RoleParentScopeId,
GrantScopeId: ag.GrantScopeId,
Id: ag.Id,
Type: ag.Type,
}
if ag.ActionSet != nil {
ret.ActionSet = make(map[action.Type]bool, len(ag.ActionSet))
for k, v := range ag.ActionSet {
ret.ActionSet[k] = v
}
}
if ag.OutputFields != nil {
ret.OutputFields = new(OutputFields)
ret.OutputFields.fields = make(map[string]bool, len(ag.OutputFields.fields))
for k, v := range ag.OutputFields.fields {
ret.OutputFields.fields[k] = v
}
}
return ret
}
// ACL provides an entry point into the permissions engine for determining if an
// action is allowed on a resource based on a principal's (user or group)
// grants.
type ACL struct {
// directScopeMap is a map of scope IDs to grants valid for that scope ID
// where the grant scope ID was specified directly
directScopeMap map[string][]AclGrant
// childrenScopeMap is a map of _parent_ scope IDs to grants, so that when
// we are checking a resource we can see if there were any "children" grant
// scope IDs that match
childrenScopeMap map[string][]AclGrant
// descendantsGrants is a list of grants that apply to all descendants of
// global
descendantsGrants []AclGrant
}
// ACLResults provides a type for the permission's engine results so that we can
// pass more detailed information along in the future if we want. It was useful
// in Vault, may be useful here.
type ACLResults struct {
AuthenticationFinished bool
Authorized bool
OutputFields *OutputFields
// This is included but unexported for testing/debugging
directScopeMap map[string][]AclGrant
childrenScopeMap map[string][]AclGrant
descendantsGrants []AclGrant
}
// Permission provides information about the specific
// resources that a user has been granted access to for a given scope, resource, and action.
type Permission struct {
RoleScopeId string // The scope id of the granting role
RoleParentScopeId string // The parent scope id of the granting role
GrantScopeId string // Same as the scope ID unless "children" or "descendants" was used.
Resource resource.Type
Action action.Type
ResourceIds []string // Any specific resource ids that have been referred in the grant's `id` field, if applicable.
OnlySelf bool // The grant only allows actions against the user's own resources.
All bool // We got a wildcard in the grant string's `id` field.
}
// UserPermissions is a set of Permissions for a User.
type UserPermissions struct {
UserId string
Permissions []Permission
}
// Resource defines something within boundary that requires authorization
// capabilities. Resources must have a ScopeId.
type Resource struct {
// ScopeId is the scope that contains the Resource.
ScopeId string `json:"scope_id,omitempty"`
// Id is the public id of the resource.
Id string `json:"id,omitempty"`
// Type of resource.
Type resource.Type `json:"type,omitempty"`
// Pin if defined would constrain the resource within the collection of the
// pin id.
Pin string `json:"pin,omitempty"`
// ParentScopeId is the parent scope of the resource.
ParentScopeId string `json:"-"`
}
// NewACL creates an ACL from the grants provided. Note that this converts the
// API-based Grants to AclGrants.
func NewACL(grants ...Grant) ACL {
ret := ACL{
directScopeMap: make(map[string][]AclGrant, len(grants)),
childrenScopeMap: make(map[string][]AclGrant, len(grants)),
descendantsGrants: make([]AclGrant, 0, len(grants)),
}
for _, grant := range grants {
ids := grant.ids
if len(ids) == 0 {
// This handles the no-ID case as well as the deprecated single-ID case
ids = []string{grant.id}
}
for _, id := range ids {
switch grant.grantScopeId {
case globals.GrantScopeDescendants:
ret.descendantsGrants = append(ret.descendantsGrants, aclGrantFromGrant(grant, id))
case globals.GrantScopeChildren:
// We use the role's scope here because we're evaluating the
// grants themselves, not the resource, so we want to know the
// scope of the role that said "children"
ret.childrenScopeMap[grant.roleScopeId] = append(ret.childrenScopeMap[grant.roleScopeId], aclGrantFromGrant(grant, id))
default:
ret.directScopeMap[grant.grantScopeId] = append(ret.directScopeMap[grant.grantScopeId], aclGrantFromGrant(grant, id))
}
}
}
return ret
}
func (a ACL) DirectScopeGrantMap() map[string][]AclGrant {
ret := make(map[string][]AclGrant, len(a.directScopeMap))
for k, v := range a.directScopeMap {
newSlice := make([]AclGrant, len(v))
for i, g := range v {
newSlice[i] = g.Clone()
}
ret[k] = newSlice
}
return ret
}
func (a ACL) ChildrenScopeGrantMap() map[string][]AclGrant {
ret := make(map[string][]AclGrant, len(a.childrenScopeMap))
for k, v := range a.childrenScopeMap {
newSlice := make([]AclGrant, len(v))
for i, g := range v {
newSlice[i] = g.Clone()
}
ret[k] = newSlice
}
return ret
}
func (a ACL) DescendantsGrants() []AclGrant {
ret := make([]AclGrant, len(a.descendantsGrants))
for i, v := range a.descendantsGrants {
ret[i] = v.Clone()
}
return ret
}
func aclGrantFromGrant(grant Grant, id string) AclGrant {
return AclGrant{
RoleScopeId: grant.roleScopeId,
RoleParentScopeId: grant.roleParentScopeId,
GrantScopeId: grant.grantScopeId,
Id: id,
Type: grant.typ,
ActionSet: grant.actions,
OutputFields: grant.OutputFields,
}
}
// Allowed determines if the grants for an ACL allow an action for a resource.
func (a ACL) Allowed(r Resource, aType action.Type, userId string, opt ...Option) (results ACLResults) {
opts := getOpts(opt...)
// First, get the grants within the specified scopes
grants := a.directScopeMap[r.ScopeId]
grants = append(grants, a.childrenScopeMap[r.ParentScopeId]...)
if r.ScopeId != scope.Global.String() {
// Descendants grants do not apply to global!
grants = append(grants, a.descendantsGrants...)
}
results.directScopeMap = a.directScopeMap
results.childrenScopeMap = a.childrenScopeMap
results.descendantsGrants = a.descendantsGrants
var parentAction action.Type
split := strings.Split(aType.String(), ":")
if len(split) == 2 {
parentAction = action.Map[split[0]]
}
// Now, go through and check whether grants match
for _, grant := range grants {
var outputFieldsOnly bool
switch {
case len(grant.ActionSet) == 0:
// Continue with the next grant, unless we have output fields
// specified in which case we continue to be able to apply the
// output fields depending on ID and type.
if _, hasSetFields := grant.OutputFields.Fields(); hasSetFields {
outputFieldsOnly = true
} else {
continue
}
case grant.ActionSet[aType]:
// We have this action
case grant.ActionSet[parentAction]:
// We don't have this action, but it's a subaction and we have the
// parent action. As an example, if we are looking for "read:self"
// and have "read", this is sufficient.
case grant.ActionSet[action.All]:
// All actions are allowed
default:
// No actions in the grant match what we're looking for, so continue
// with the next grant
continue
}
// We step through all grants in order to fetch the full list of output
// fields, even if we find a match. However, we shortcut if we find *
// for output fields, which is the default.
//
// If the action was not found above but we did find output fields in
// patterns that match, we do not authorize the request, but we do build
// up the output fields patterns.
//
// Note that when using IsActionOrParent it is merely to test whether it
// is an allowed format since some formats operate ony on collections
// (or don't operate at all on collections) and we want to ensure that
// it is/isn't a create or list command or subcommand to know whether
// that form is valid. The actual checking of whether the given action
// is granted to the user already happened above.
var found bool
switch {
// Case 1: We only allow specific actions on specific types for the
// anonymous user. ID being supplied or not doesn't matter in this case,
// it must be an explicit type and action(s); adding this as an explicit
// case here prevents duplicating logic in two of the other more
// general-purpose cases below (3 and 4). See notes there about ID being
// present or not.
case !opts.withSkipAnonymousUserRestrictions &&
(userId == globals.AnonymousUserId || userId == ""):
switch {
// Allow discovery of scopes, so that auth methods within can be
// discovered
case grant.Type == r.Type &&
grant.Type == resource.Scope &&
(aType == action.List || aType == action.NoOp):
found = true
// Allow discovery of and authenticating to auth methods
case grant.Type == r.Type &&
grant.Type == resource.AuthMethod &&
(aType == action.List || aType == action.NoOp || aType == action.Authenticate):
found = true
}
// Case 2: id=<resource.Id>;actions=<action> where ID cannot be a
// wildcard. Type is optional but if present must match the resource.
// This will also allow matching an id with specific output fields
// (handled later). Cannot be a list or create action as those do not
// operate on specific IDs, only types.
case grant.Id == r.Id &&
grant.Id != "" &&
grant.Id != "*" &&
(grant.Type == resource.Unknown || grant.Type == globals.ResourceInfoFromPrefix(grant.Id).Type) &&
!action.List.IsActionOrParent(aType) &&
!action.Create.IsActionOrParent(aType):
found = true
// Case 3: type=<resource.Type>;actions=<action> when action is list or
// create (cannot be a wildcard). Must be a top level collection; if not
// it's handled in cases 4 or 5. This is more of a semantic difference
// compared to case 4 more than a security difference; this type is for
// clarity as it ties more closely to the concept of create and list as
// actions on a collection, operating on a collection directly. The
// format in case 4 will still work for create/list on collections but
// that's more of a shortcut to allow things like id=*;type=*;actions=*
// for admin flows so that you don't need to separate out explicit
// collection actions into separate typed grants for each collection
// within a role. This does mean there are "two ways of doing things"
// but it's a reasonable UX tradeoff given that "all IDs" can reasonably
// be construed to include "and the one I'm making" and "all of them for
// listing".
case grant.Id == "" &&
r.Id == "" &&
grant.Type == r.Type &&
grant.Type != resource.Unknown &&
resource.TopLevelType(r.Type) &&
(action.List.IsActionOrParent(aType) ||
action.Create.IsActionOrParent(aType)):
found = true
// Case 4:
// id=*;type=<resource.Type>;actions=<action> where type cannot be
// unknown but can be a wildcard to allow any resource at all; or
// id=*;type=<resource.Type>;output_fields=<fields> with no action.
case grant.Id == "*" &&
grant.Type != resource.Unknown &&
(grant.Type == r.Type ||
grant.Type == resource.All):
found = true
// Case 5:
// id=<pin>;type=<resource.Type>;actions=<action> where type can be a
// wildcard and this this is operating on a non-top-level type.
case grant.Id != "" &&
grant.Id == r.Pin &&
grant.Type != resource.Unknown &&
(grant.Type == r.Type || grant.Type == resource.All) &&
!resource.TopLevelType(r.Type):
found = true
}
if found {
if !outputFieldsOnly {
results.Authorized = true
}
fields, _ := grant.OutputFields.Fields()
results.OutputFields = results.OutputFields.AddFields(fields)
if results.OutputFields.Has("*") && results.Authorized {
return
}
}
}
return
}
// ListResolvableAliasesPermissions builds a set of Permissions based on the
// grants in the ACL. The permissions will only be created if there is at least
// one grant of the provided resource type that includes at least one of the
// provided actions in the action set. Note that unlike the ListPermissions
// method, this method does not attempt to generate permissions for the
// u_recovery user. To get the resolvable aliases for u_recovery, the user could
// simply query all aliases with a destination id.
func (a ACL) ListResolvableAliasesPermissions(requestedType resource.Type, actions action.ActionSet) []Permission {
perms := make([]Permission, 0, len(a.directScopeMap)+len(a.childrenScopeMap)+len(a.descendantsGrants))
childScopeMap := a.childrenScopeMap
scopeMap := a.directScopeMap
// Unilaterally add the descendants grants, if any. Not specifying an Id or
// ParentScopeId in ScopeInfo means that the only grants that might match
// are descendants, and we tell buildPermission to include descendants.
p := Permission{
RoleScopeId: scope.Global.String(),
GrantScopeId: globals.GrantScopeDescendants,
Resource: requestedType,
Action: action.ListResolvableAliases,
OnlySelf: true, // default to only self to be restrictive
}
if a.buildPermission(&scopes.ScopeInfo{}, requestedType, actions, true, &p) {
perms = append(perms, p)
// Shortcut here because this is all we need -- this will turn into all
// scopes. We only need to check for "global" in the direct map.
if _, ok := a.directScopeMap[scope.Global.String()]; !ok {
return perms
}
childScopeMap = nil
scopeMap = map[string][]AclGrant{scope.Global.String(): a.directScopeMap[scope.Global.String()]}
}
// Next look at children grants; provide only the parent scope ID and tell
// buildPermission to ignore descendants so that we know that the
// permissions being looked at come from a child relationship. Cache the
// scope IDs so we can ignore direct grants.
childrenScopes := map[string]struct{}{}
for scopeId := range childScopeMap {
p := Permission{
RoleScopeId: scopeId,
GrantScopeId: globals.GrantScopeChildren,
Resource: requestedType,
Action: action.ListResolvableAliases,
OnlySelf: true, // default to only self to be restrictive
}
if scopeId != scope.Global.String() { // Must be an org then so global is parent
p.RoleParentScopeId = scope.Global.String()
}
if a.buildPermission(&scopes.ScopeInfo{ParentScopeId: scopeId}, requestedType, actions, false, &p) {
perms = append(perms, p)
childrenScopes[scopeId] = struct{}{}
}
}
// Now look at direct grants; provide only Id so that we know the
// permissions being looked at will include those specific scopes.
for grantScopeId, grants := range scopeMap {
p := Permission{
GrantScopeId: grantScopeId,
Resource: requestedType,
Action: action.ListResolvableAliases,
OnlySelf: true, // default to only self to be restrictive
}
if len(grants) > 0 {
// Since scopeIds will be the same for all of these grants, and it's
// not children or descendants, we can get it from any of the grants
p.RoleParentScopeId = grants[0].RoleParentScopeId
p.RoleScopeId = grants[0].RoleScopeId
}
switch {
case grantScopeId == p.RoleScopeId:
// If the role and grant scope IDs are the same, they share a
// parent, so we can look at the role's parent scope ID in the
// children scopes map
if _, ok := childrenScopes[p.RoleParentScopeId]; ok {
// We already looked at this scope in the children grants, so skip it
continue
}
case strings.HasPrefix(p.RoleScopeId, scope.Org.Prefix()):
// Since direct grants must be in the same scope or downstream, if
// the role scope ID is an org and the role and grant scopes are
// different, the grant is on a project, so look for children from
// the org
if _, ok := childrenScopes[p.RoleScopeId]; ok {
// We already found grants at this scope in the children grants,
// so skip it
continue
}
default:
// Since direct grants must be the same scope or downstream, the
// only possibility left for a children grant is that the parent is
// global and the grant is on the org -- if it was for projects it
// would need to be a descendants grant
if _, ok := childrenScopes[scope.Global.String()]; ok {
// We already looked at this scope in the children grants, so skip it
continue
}
}
if a.buildPermission(&scopes.ScopeInfo{Id: grantScopeId}, requestedType, actions, false, &p) {
perms = append(perms, p)
}
}
return perms
}
// ListPermissions builds a set of Permissions based on the grants in the ACL.
// Permissions are determined for the given resource for each of the provided scopes.
// There must be a grant for a given resource for one of the provided "id actions"
// or for action.All in order for a Permission to be created for the scope.
// The set of "id actions" is resource dependant, but will generally include all
// actions that can be taken on an individual resource.
func (a ACL) ListPermissions(
requestedScopes map[string]*scopes.ScopeInfo,
requestedType resource.Type,
idActions action.ActionSet,
userId string,
) []Permission {
perms := make([]Permission, 0, len(requestedScopes))
for scopeId, scopeInfo := range requestedScopes {
if scopeInfo == nil {
continue
}
// Note: this function is called either with the scope resulting from
// authentication (which would have the scope info for the specific
// resource) or recursive scopes, which are fully resolved. The scopes
// included have already been run through acl.Allowed() to see if the
// user has access to the resource, so the grant scope ID can correctly
// be set here to be the same as the role scope ID even if it's
// technically coming from children/descendants grants.
p := Permission{
RoleScopeId: scopeId,
RoleParentScopeId: scopeInfo.ParentScopeId,
GrantScopeId: scopeId,
Resource: requestedType,
Action: action.List,
OnlySelf: true, // default to only self to be restrictive
}
if userId == globals.RecoveryUserId {
p.All = true
p.OnlySelf = false
perms = append(perms, p)
continue
}
if a.buildPermission(scopeInfo, requestedType, idActions, false, &p) {
perms = append(perms, p)
}
}
return perms
}
// buildPermission populates the provided permission with either the resource ids
// or marking All to true if there are grants that have an action that match
// one of the provided idActions for the provided type
func (a ACL) buildPermission(
scopeInfo *scopes.ScopeInfo,
requestedType resource.Type,
idActions action.ActionSet,
includeDescendants bool,
p *Permission,
) bool {
// Get grants for a specific scope id from the source of truth.
if scopeInfo == nil {
return false
}
var grants []AclGrant
if scopeInfo.Id != "" {
grants = a.directScopeMap[scopeInfo.Id]
}
if scopeInfo.ParentScopeId != "" {
grants = append(grants, a.childrenScopeMap[scopeInfo.ParentScopeId]...)
}
// If the scope is global it needs to be a direct grant; descendants doesn't
// include global
if includeDescendants || (scopeInfo.Id != "" && scopeInfo.Id != scope.Global.String()) {
grants = append(grants, a.descendantsGrants...)
}
for _, grant := range grants {
// This grant doesn't match what we're looking for, ignore.
if grant.Type != requestedType && grant.Type != resource.All && globals.ResourceInfoFromPrefix(grant.Id).Type != requestedType {
continue
}
// We found a grant that matches the requested resource type:
// Search to see if one or all actions in the action set have been granted.
found := false
if ok := grant.ActionSet[action.All]; ok {
found = true
} else {
for idA := range idActions {
if ok := grant.ActionSet[idA]; ok {
found = true
break
}
}
}
if !found { // In this case, none of the requested actions were granted for the given scope id.
continue
}
actions, _ := grant.Actions()
excludeList := make(action.ActionSet, len(actions))
for _, aa := range actions {
if aa != action.List {
excludeList.Add(aa)
}
}
p.OnlySelf = p.OnlySelf && excludeList.OnlySelf()
switch grant.Id {
case "*":
p.All = true
case "":
continue
default:
p.ResourceIds = append(p.ResourceIds, grant.Id)
}
}
return p.All || len(p.ResourceIds) > 0
}