2022-04-29 09:52:01 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2021-01-25 19:37:40 -05:00
package scopeids
import (
"context"
2022-04-29 15:55:27 -04:00
"fmt"
2021-01-25 19:37:40 -05:00
2022-04-29 09:52:01 -04:00
"github.com/hashicorp/boundary/internal/boundary"
2022-05-09 12:45:10 -07:00
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
2021-03-24 15:33:04 -04:00
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam"
2021-01-25 19:37:40 -05:00
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
2021-03-24 15:33:04 -04:00
"github.com/hashicorp/boundary/internal/types/scope"
2021-08-26 10:44:41 -04:00
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
2021-01-25 19:37:40 -05:00
)
2022-04-29 09:52:01 -04:00
type authzProtectedEntityProvider interface {
// Fetches basic resource info for the given scopes. Note that this is a
// "where clause" style of argument: if the set of scopes is populated these
// are the scopes to limit to (e.g. to put in a where clause). An empty set
// of scopes means to look in *all* scopes, not none!
2022-07-20 16:24:46 -07:00
FetchAuthzProtectedEntitiesByScope ( ctx context . Context , projectIds [] string ) ( map [ string ][] boundary . AuthzProtectedEntity , error )
2022-04-29 09:52:01 -04:00
}
// ResourceInfo contains information about a particular resource
type ResourceInfo struct {
AuthorizedActions action . ActionSet
}
// ScopeInfoWithResourceIds contains information about a scope and the resources
// found within it
type ScopeInfoWithResourceIds struct {
* scopes . ScopeInfo
Resources map [ string ] ResourceInfo
}
// GetListingResourceInformationInput contains input parameters to the function
type GetListingResourceInformationInput struct {
// An IAM repo function to use for a scope listing call
IamRepoFn common . IamRepoFactory
// The original auth results from the list command
AuthResults auth . VerifyResults
// The scope ID to use, or the starting point for a recursive search
RootScopeId string
// The type of resource being listed
Type resource . Type
// Whether the search is recursive
Recursive bool
// A repo to fetch resources
AuthzProtectedEntityProvider authzProtectedEntityProvider
// The available actions for the resource type
ActionSet action . ActionSet
}
// GetListingResourceInformationOutput contains results from the function
type GetListingResourceInformationOutput struct {
// The calculated list of relevant scope IDs
ScopeIds [] string
// The specific resource IDs calculated to be authorized for listing
ResourceIds [] string
// A map of scope ID to scope information and a map of resource IDs in that
// scope and specific information about that resource, such as available
// actions
ScopeResourceMap map [ string ] * ScopeInfoWithResourceIds
}
// GetListingResourceInformation, given common parameters for List calls,
// returns useful information: the set of scope IDs in which to search for
// resources; the IDs of the resources known to be authorized for that user; and
// a memoized map of the scopes to their info for populating returned values.
func GetListingResourceInformation (
2021-01-25 19:37:40 -05:00
// The context to use when listing in the DB, if required
ctx context . Context ,
2022-04-29 09:52:01 -04:00
// The input struct
input GetListingResourceInformationInput ,
) ( * GetListingResourceInformationOutput , error ) {
const op = "scopeids.GetListingResourceInformation"
output := new ( GetListingResourceInformationOutput )
2021-03-24 15:33:04 -04:00
// Validation
switch {
2022-04-29 09:52:01 -04:00
case input . Type == resource . Unknown :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "unknown resource" )
case input . IamRepoFn == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "nil iam repo" )
case input . RootScopeId == "" :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing root scope id" )
case input . AuthResults . Scope == nil :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "nil scope in auth results" )
case ! input . Recursive && input . AuthResults . Scope . Id != input . RootScopeId :
return nil , errors . New ( ctx , errors . InvalidParameter , op , "non-recursive search but auth results scope does not match root scope" )
2021-03-24 15:33:04 -04:00
}
2022-04-29 09:52:01 -04:00
// This will be used to memoize scope info so we can put the right scope
// info for each returned value
output . ScopeResourceMap = map [ string ] * ScopeInfoWithResourceIds {}
2021-03-24 15:33:04 -04:00
// Base case: if not recursive, return the scope we were given and the
// already-looked-up info
2022-04-29 09:52:01 -04:00
if ! input . Recursive {
output . ScopeResourceMap [ input . AuthResults . Scope . Id ] = & ScopeInfoWithResourceIds { ScopeInfo : input . AuthResults . Scope }
// If we don't have information do to the resource lookup ourselves,
// return what we have
if input . AuthzProtectedEntityProvider == nil {
output . ScopeIds = [] string { input . AuthResults . Scope . Id }
return output , nil
}
// Otherwise filter on this one scope and return
if err := filterAuthorizedResourceIds ( ctx , input , output ); err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error filtering to only authorized resources" ))
}
return output , nil
2021-03-24 15:33:04 -04:00
}
2022-04-29 09:52:01 -04:00
repo , err := input . IamRepoFn ()
2021-03-24 15:33:04 -04:00
if err != nil {
2022-04-29 09:52:01 -04:00
return nil , err
2021-01-25 19:37:40 -05:00
}
2021-03-24 15:33:04 -04:00
// Get all scopes recursively. Start at global because we need to take into
// account permissions in parent scopes even if they want to scale back the
// returned information to a child scope and its children.
scps , err := repo . ListScopesRecursively ( ctx , scope . Global . String ())
if err != nil {
2022-04-29 09:52:01 -04:00
return nil , err
2021-03-24 15:33:04 -04:00
}
res := perms . Resource {
2022-04-29 09:52:01 -04:00
Type : input . Type ,
2021-03-24 15:33:04 -04:00
}
// For each scope, see if we have permission to list that type in that
// scope
var deferredScopes [] * iam . Scope
// Store whether global has list permission
var globalHasList bool
for _ , scp := range scps {
scpId := scp . GetPublicId ()
res . ScopeId = scpId
2022-04-29 09:52:01 -04:00
aSet := input . AuthResults . FetchActionSetForType ( ctx ,
2021-03-24 15:33:04 -04:00
// This is overridden by WithResource
resource . Unknown ,
2023-10-31 13:44:17 +00:00
action . NewActionSet ( action . List ),
2021-03-24 15:33:04 -04:00
auth . WithResource ( & res ),
)
switch len ( aSet ) {
case 0 :
// Defer until we've read all scopes. We do this because if the
2022-04-29 09:52:01 -04:00
// ordering coming back isn't in parent-first ordering our map
2021-03-24 15:33:04 -04:00
// lookup might fail.
2022-04-29 09:52:01 -04:00
deferredScopes = append ( deferredScopes , scp )
2021-03-24 15:33:04 -04:00
case 1 :
2023-10-31 13:44:17 +00:00
if ! aSet . HasAction ( action . List ) {
2022-04-29 09:52:01 -04:00
return nil , errors . New ( ctx , errors . Internal , op , "unexpected action in set" )
2021-03-24 15:33:04 -04:00
}
2022-04-29 09:52:01 -04:00
if output . ScopeResourceMap [ scpId ] == nil {
2021-03-24 15:33:04 -04:00
scopeInfo := & scopes . ScopeInfo {
Id : scp . GetPublicId (),
Type : scp . GetType (),
Name : scp . GetName (),
Description : scp . GetDescription (),
ParentScopeId : scp . GetParentId (),
}
2022-04-29 09:52:01 -04:00
output . ScopeResourceMap [ scpId ] = & ScopeInfoWithResourceIds { ScopeInfo : scopeInfo }
2021-03-24 15:33:04 -04:00
}
if scpId == scope . Global . String () {
globalHasList = true
}
default :
2022-04-29 09:52:01 -04:00
return nil , errors . New ( ctx , errors . Internal , op , "unexpected number of actions back in set" )
2021-01-25 19:37:40 -05:00
}
2021-03-24 15:33:04 -04:00
}
// Now go through these and see if a parent matches
for _ , scp := range deferredScopes {
// If they had list on global scope anything else is automatically
// included; otherwise if they had list on the parent scope, this
// scope is included in the map and is sufficient here.
2022-04-29 09:52:01 -04:00
if globalHasList || output . ScopeResourceMap [ scp . GetParentId ()] != nil {
2021-01-25 19:37:40 -05:00
scpId := scp . GetPublicId ()
2022-04-29 09:52:01 -04:00
if output . ScopeResourceMap [ scpId ] == nil {
2021-03-24 15:33:04 -04:00
scopeInfo := & scopes . ScopeInfo {
Id : scp . GetPublicId (),
Type : scp . GetType (),
Name : scp . GetName (),
Description : scp . GetDescription (),
ParentScopeId : scp . GetParentId (),
2021-01-25 19:37:40 -05:00
}
2022-04-29 09:52:01 -04:00
output . ScopeResourceMap [ scpId ] = & ScopeInfoWithResourceIds { ScopeInfo : scopeInfo }
2021-01-25 19:37:40 -05:00
}
}
2021-03-24 15:33:04 -04:00
}
// Now elide out any that aren't under the root scope ID
2022-04-29 09:52:01 -04:00
elideScopes := make ([] string , 0 , len ( output . ScopeResourceMap ))
for scpId , scp := range output . ScopeResourceMap {
switch input . RootScopeId {
2021-03-24 15:33:04 -04:00
// If the root is global, it matches
case scope . Global . String ():
// If the current scope matches the root, it matches
case scpId :
// Or if the parent of this scope is the root (for orgs that would mean
// a root scope ID which is covered in the case above, so this is really
// projects matching an org used as the root)
case scp . GetParentScopeId ():
default :
elideScopes = append ( elideScopes , scpId )
}
}
for _ , scpId := range elideScopes {
2022-04-29 09:52:01 -04:00
delete ( output . ScopeResourceMap , scpId )
}
// If we have nothing in scopeInfoMap at this point, we aren't authorized
// anywhere so return 403.
if len ( output . ScopeResourceMap ) == 0 {
return nil , handlers . ForbiddenError ()
2021-03-24 15:33:04 -04:00
}
2021-01-25 19:37:40 -05:00
2022-04-29 09:52:01 -04:00
if input . AuthzProtectedEntityProvider == nil {
output . populateScopeIdsFromScopeResourceMap ()
return output , nil
2021-01-25 19:37:40 -05:00
}
2022-04-29 09:52:01 -04:00
if err := filterAuthorizedResourceIds ( ctx , input , output ); err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "error filtering to only authorized resources" ))
}
return output , nil
}
// filterAuthorizedResourceIds calls the passed in function to get IDs for
// resources in the given scopes and then figures out which ones are actually
// authorized for listing by the user.
//
// It also populates the scope IDs in the output
func filterAuthorizedResourceIds (
// The context to use when listing in the DB, if required
ctx context . Context ,
// The input struct
input GetListingResourceInformationInput ,
// The scope information to fill out
output * GetListingResourceInformationOutput ,
) error {
const op = "scopeids.filterAuthorizedResources"
// Populate scopeIds and determine if we found global
output . populateScopeIdsFromScopeResourceMap ()
// The calling function is giving us a complete set with any recursive
// lookup already performed (that's the point of the function).
scopedResourceInfo , err := input . AuthzProtectedEntityProvider . FetchAuthzProtectedEntitiesByScope ( ctx , output . ScopeIds )
if err != nil {
return errors . Wrap ( ctx , err , op )
}
res := perms . Resource {
2022-04-29 15:55:27 -04:00
Type : input . Type ,
2022-04-29 09:52:01 -04:00
}
// Now run authorization checks against each so we know if there is a point
// in fetching the full resource, and cache the authorized actions
for scopeId , resourceInfos := range scopedResourceInfo {
for _ , resourceInfo := range resourceInfos {
res . Id = resourceInfo . GetPublicId ()
res . ScopeId = scopeId
authorizedActions := input . AuthResults . FetchActionSetForId ( ctx , resourceInfo . GetPublicId (), input . ActionSet , auth . WithResource ( & res ))
if len ( authorizedActions ) == 0 {
continue
}
if resourceInfo . GetUserId () != "" {
if authorizedActions . OnlySelf () && resourceInfo . GetUserId () != input . AuthResults . UserId {
continue
}
}
2022-04-29 15:55:27 -04:00
if output . ScopeResourceMap [ scopeId ] == nil {
return errors . New ( ctx , errors . Internal , op , fmt . Sprintf ( "scope id %s returned from fetching authz protected entities not found in scope resource map" , scopeId ))
}
2022-04-29 09:52:01 -04:00
if output . ScopeResourceMap [ scopeId ]. Resources == nil {
output . ScopeResourceMap [ scopeId ]. Resources = make ( map [ string ] ResourceInfo )
}
output . ScopeResourceMap [ scopeId ]. Resources [ resourceInfo . GetPublicId ()] = ResourceInfo { AuthorizedActions : authorizedActions }
output . ResourceIds = append ( output . ResourceIds , resourceInfo . GetPublicId ())
}
}
return nil
}
// populateScopeIdsFromScopeResourceMap populates the ScopeIds field and returns
// whether global scope was found
func ( i * GetListingResourceInformationOutput ) populateScopeIdsFromScopeResourceMap () {
for k := range i . ScopeResourceMap {
i . ScopeIds = append ( i . ScopeIds , k )
}
}
// GetListingScopeIds is provided for backwards compatibility with existing
// services; services should eventually migrate to
// GetListingResourceInformation.
func GetListingScopeIds (
// The context to use when listing in the DB, if required
ctx context . Context ,
// An IAM repo function to use for a listing call, if required
repoFn common . IamRepoFactory ,
// The original auth results from the list command
authResults auth . VerifyResults ,
// The scope ID to use, or to use as the starting point for a recursive
// search
rootScopeId string ,
// The type of resource we are listing
typ resource . Type ,
// Whether or not the search should be recursive
recursive bool ,
) ([] string , map [ string ] * scopes . ScopeInfo , error ) {
const op = "scopeids.GetListingScopeIds"
scopeResourceInfo , err := GetListingResourceInformation ( ctx ,
GetListingResourceInformationInput {
IamRepoFn : repoFn ,
AuthResults : authResults ,
RootScopeId : rootScopeId ,
Type : typ ,
Recursive : recursive ,
},
)
if err != nil {
if err == handlers . ForbiddenError () {
return nil , nil , err
}
return nil , nil , errors . Wrap ( ctx , err , op )
}
if len ( scopeResourceInfo . ScopeIds ) == 0 {
// This should have already happened in the other function, but...
return nil , nil , handlers . ForbiddenError ()
}
scopeMap := make ( map [ string ] * scopes . ScopeInfo , len ( scopeResourceInfo . ScopeResourceMap ))
for k , v := range scopeResourceInfo . ScopeResourceMap {
scopeMap [ k ] = v . ScopeInfo
}
return scopeResourceInfo . ScopeIds , scopeMap , nil
2021-01-25 19:37:40 -05:00
}