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/daemon/controller/common/scopeids/scope_ids.go

360 lines
13 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package scopeids
import (
"context"
"fmt"
"github.com/hashicorp/boundary/internal/boundary"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/perms"
"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"
)
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!
FetchAuthzProtectedEntitiesByScope(ctx context.Context, projectIds []string) (map[string][]boundary.AuthzProtectedEntity, error)
}
// 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(
// The context to use when listing in the DB, if required
ctx context.Context,
// The input struct
input GetListingResourceInformationInput,
) (*GetListingResourceInformationOutput, error) {
const op = "scopeids.GetListingResourceInformation"
output := new(GetListingResourceInformationOutput)
// Validation
switch {
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")
}
// 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{}
// Base case: if not recursive, return the scope we were given and the
// already-looked-up info
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
}
repo, err := input.IamRepoFn()
if err != nil {
return nil, err
}
// 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 {
return nil, err
}
res := perms.Resource{
Type: input.Type,
}
// 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
res.ParentScopeId = scp.GetParentId()
aSet := input.AuthResults.FetchActionSetForType(ctx,
// This is overridden by WithResource
resource.Unknown,
action.NewActionSet(action.List),
auth.WithResource(&res),
)
switch len(aSet) {
case 0:
// Defer until we've read all scopes. We do this because if the
// ordering coming back isn't in parent-first ordering our map
// lookup might fail.
deferredScopes = append(deferredScopes, scp)
case 1:
if !aSet.HasAction(action.List) {
return nil, errors.New(ctx, errors.Internal, op, "unexpected action in set")
}
if output.ScopeResourceMap[scpId] == nil {
scopeInfo := &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
output.ScopeResourceMap[scpId] = &ScopeInfoWithResourceIds{ScopeInfo: scopeInfo}
}
if scpId == scope.Global.String() {
globalHasList = true
}
default:
return nil, errors.New(ctx, errors.Internal, op, "unexpected number of actions back in set")
}
}
// 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.
if globalHasList || output.ScopeResourceMap[scp.GetParentId()] != nil {
scpId := scp.GetPublicId()
if output.ScopeResourceMap[scpId] == nil {
scopeInfo := &scopes.ScopeInfo{
Id: scp.GetPublicId(),
Type: scp.GetType(),
Name: scp.GetName(),
Description: scp.GetDescription(),
ParentScopeId: scp.GetParentId(),
}
output.ScopeResourceMap[scpId] = &ScopeInfoWithResourceIds{ScopeInfo: scopeInfo}
}
}
}
// Now elide out any that aren't under the root scope ID
elideScopes := make([]string, 0, len(output.ScopeResourceMap))
for scpId, scp := range output.ScopeResourceMap {
switch input.RootScopeId {
// 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 {
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()
}
if input.AuthzProtectedEntityProvider == nil {
output.populateScopeIdsFromScopeResourceMap()
return output, nil
}
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{
Type: input.Type,
}
// 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
}
}
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))
}
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
}