Files
threshold/internal/daemon/controller/common/scopeids/scope_ids.go
T
hashicorp-copywrite[bot]andJohan Brandhorst-Satzkorn 29da0bcb92 [COMPLIANCE] License changes (#3567)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* Update copyright file headers to BUS-1.1

* Rerun make gen

This will pick up the last of the license changes

* Revert "Rerun make gen"

This reverts commit dbe0968bffd92745d8663c6940debae098395a6b.

* Remove copywrite from generation script

The current version of copywrite is not sophisticated enough
to only apply the copywrite changes to certain subdirectories,
so to avoid accidentally relicensing our api and sdk modules,
we will stop running the copywrite script until such time
that it can do the right thing.

* License protobuf API as MPL

The protobuf API files are used to generate the SDK,
since we want the SDK to be MPL, the protobuf files
must also be MPL. This extends to all protobuf files
whose sources end up in the SDK directory, including:

- internal/proto/controller/api
- internal/proto/controller/custom_options
- internal/proto/plugin

It also includes the generated service directory in
internal/gen/controller/api/services, since they are
generated from the protobuf sources used for the SDK.

* Change website license file name

This makes the naming scheme consistent with the rest
of the repository.

* Add note on licensing to README

* Set copywrite license to BUSL

* Revert "Add note on licensing to README"

This reverts commit 4d865a91a7a4f11605f0dc4609355d7e3c3cfffe.

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
2023-08-11 10:31:53 -07:00

359 lines
13 KiB
Go

// 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
aSet := input.AuthResults.FetchActionSetForType(ctx,
// This is overridden by WithResource
resource.Unknown,
action.ActionSet{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[0] != 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
}