Files
threshold/internal/daemon/controller/handlers/scopes/scope_service.go
T
Jeff Mitchell 62d4f8453b Don't explode grants from the DB (#5104)
This removes the Cartesian product in the DB for GrantsForUser in favor
of returning the actual grant scope information and dealing with it in
application code.

(cherry picked from commit 913cd536823d73994acc4f4d833208c1cc05f91b)
2024-09-24 14:18:53 +00:00

1192 lines
41 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package scopes
import (
"context"
"fmt"
"sort"
"strings"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/common/scopeids"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/aliases"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authtokens"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/credentialstores"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/groups"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/host_catalogs"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/policies"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/roles"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/session_recordings"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/sessions"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/storage_buckets"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/targets"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/users"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/workers"
"github.com/hashicorp/boundary/internal/errors"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/listtoken"
"github.com/hashicorp/boundary/internal/pagination"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/requests"
"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/internal/util"
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
var (
maskManager handlers.MaskManager
// IdActions contains the set of actions that can be performed on
// individual resources
IdActions = action.NewActionSet(
action.NoOp,
action.Read,
action.Update,
action.Delete,
action.AttachStoragePolicy,
action.DetachStoragePolicy,
)
// CollectionActions contains the set of actions that can be performed on
// this collection
CollectionActions = action.NewActionSet(
action.Create,
action.List,
action.ListScopeKeys,
action.RotateScopeKeys,
action.ListScopeKeyVersionDestructionJobs,
action.DestroyScopeKeyVersion,
)
// TODO: get this from action registry
scopeCollectionTypeMapMap = map[string]map[resource.Type]action.ActionSet{
scope.Global.String(): {
resource.Alias: aliases.CollectionActions,
resource.AuthMethod: authmethods.CollectionActions,
resource.StorageBucket: storage_buckets.CollectionActions,
resource.AuthToken: authtokens.CollectionActions,
resource.Group: groups.CollectionActions,
resource.Role: roles.CollectionActions,
resource.Scope: CollectionActions,
resource.User: users.CollectionActions,
resource.Worker: workers.CollectionActions,
resource.SessionRecording: session_recordings.CollectionActions,
resource.Policy: policies.CollectionActions,
},
scope.Org.String(): {
resource.AuthMethod: authmethods.CollectionActions,
resource.StorageBucket: storage_buckets.CollectionActions,
resource.AuthToken: authtokens.CollectionActions,
resource.Group: groups.CollectionActions,
resource.Role: roles.CollectionActions,
resource.Scope: CollectionActions,
resource.User: users.CollectionActions,
resource.SessionRecording: session_recordings.CollectionActions,
resource.Policy: policies.CollectionActions,
},
scope.Project.String(): {
resource.CredentialStore: credentialstores.CollectionActions,
resource.Group: groups.CollectionActions,
resource.HostCatalog: host_catalogs.CollectionActions,
resource.Role: roles.CollectionActions,
resource.Scope: action.NewActionSet(
action.ListScopeKeys,
action.RotateScopeKeys,
action.ListScopeKeyVersionDestructionJobs,
action.DestroyScopeKeyVersion,
), // Only Scope key actions are allowed on the project level
resource.Session: sessions.CollectionActions,
resource.Target: targets.CollectionActions,
},
}
)
func init() {
var err error
if maskManager, err = handlers.NewMaskManager(
context.Background(),
handlers.MaskDestination{&store.Scope{}},
handlers.MaskSource{&pb.Scope{}},
); err != nil {
panic(err)
}
// TODO: refactor to remove IdActions and CollectionActions package variables
action.RegisterResource(resource.Scope, IdActions, CollectionActions)
}
// Service handles requests as described by the pbs.ScopeServiceServer interface.
type Service struct {
pbs.UnsafeScopeServiceServer
repoFn common.IamRepoFactory
kmsRepo *kms.Kms
maxPageSize uint
}
var _ pbs.ScopeServiceServer = (*Service)(nil)
// NewServiceFn returns a service which handles scope related requests to boundary.
var NewServiceFn = func(ctx context.Context, repo common.IamRepoFactory, kmsRepo *kms.Kms, maxPageSize uint) (pbs.ScopeServiceServer, error) {
const op = "scopes.(Service).NewService"
if util.IsNil(repo) {
return &Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing iam repository")
}
if kmsRepo == nil {
return &Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing kms")
}
if maxPageSize == 0 {
maxPageSize = uint(globals.DefaultMaxPageSize)
}
return &Service{repoFn: repo, kmsRepo: kmsRepo, maxPageSize: maxPageSize}, nil
}
// ListScopes implements the interface pbs.ScopeServiceServer.
func (s *Service) ListScopes(ctx context.Context, req *pbs.ListScopesRequest) (*pbs.ListScopesResponse, error) {
const op = "scopes.(Service).ListScopes"
if req.GetScopeId() == "" {
req.ScopeId = scope.Global.String()
}
if err := validateListRequest(ctx, req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.List)
if authResults.Error != nil {
// If it's forbidden, and it's a recursive request, and they're
// successfully authenticated but just not authorized, keep going as we
// may have authorization on downstream scopes. Or, if they've not
// authenticated, still process in case u_anon has permissions.
if (authResults.Error == handlers.ForbiddenError() || authResults.Error == handlers.UnauthenticatedError()) &&
req.GetRecursive() &&
authResults.AuthenticationFinished {
} else {
return nil, authResults.Error
}
}
scopeIds, scopeInfoMap, err := scopeids.GetListingScopeIds(
ctx, s.repoFn, authResults, req.GetScopeId(), resource.Scope, req.GetRecursive())
if err != nil {
return nil, err
}
pageSize := int(s.maxPageSize)
// Use the requested page size only if it is smaller than
// the configured max.
if req.GetPageSize() != 0 && uint(req.GetPageSize()) < s.maxPageSize {
pageSize = int(req.GetPageSize())
}
var filterItemFn func(ctx context.Context, item *iam.Scope) (bool, error)
switch {
case req.GetFilter() != "":
// Only use a filter if we need to
filter, err := handlers.NewFilter(ctx, req.GetFilter())
if err != nil {
return nil, err
}
filterItemFn = func(ctx context.Context, item *iam.Scope) (bool, error) {
outputOpts, ok, err := newOutputOpts(ctx, item, authResults, scopeInfoMap)
if err != nil {
return false, err
}
if !ok {
return false, nil
}
pbItem, err := ToProto(ctx, item, outputOpts...)
if err != nil {
return false, err
}
return filter.Match(pbItem), nil
}
default:
filterItemFn = func(ctx context.Context, item *iam.Scope) (bool, error) {
return true, nil
}
}
grantsHash, err := authResults.GrantsHash(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
repo, err := s.repoFn()
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
var listResp *pagination.ListResponse[*iam.Scope]
var sortBy string
if req.GetListToken() == "" {
sortBy = "created_time"
listResp, err = iam.ListScopes(ctx, grantsHash, pageSize, filterItemFn, repo, scopeIds)
if err != nil {
return nil, err
}
} else {
listToken, err := handlers.ParseListToken(ctx, req.GetListToken(), resource.Scope, grantsHash)
if err != nil {
return nil, err
}
switch st := listToken.Subtype.(type) {
case *listtoken.PaginationToken:
sortBy = "created_time"
listResp, err = iam.ListScopesPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
case *listtoken.StartRefreshToken:
sortBy = "updated_time"
listResp, err = iam.ListScopesRefresh(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
case *listtoken.RefreshToken:
sortBy = "updated_time"
listResp, err = iam.ListScopesRefreshPage(ctx, grantsHash, pageSize, filterItemFn, listToken, repo, scopeIds)
if err != nil {
return nil, err
}
default:
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "unexpected list token subtype: %T", st)
}
}
finalItems := make([]*pb.Scope, 0, len(listResp.Items))
for _, item := range listResp.Items {
outputOpts, ok, err := newOutputOpts(ctx, item, authResults, scopeInfoMap)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if !ok {
continue
}
item, err := ToProto(ctx, item, outputOpts...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
finalItems = append(finalItems, item)
}
respType := "delta"
if listResp.CompleteListing {
respType = "complete"
}
resp := &pbs.ListScopesResponse{
Items: finalItems,
EstItemCount: uint32(listResp.EstimatedItemCount),
RemovedIds: listResp.DeletedIds,
ResponseType: respType,
SortBy: sortBy,
SortDir: "desc",
}
if listResp.ListToken != nil {
resp.ListToken, err = handlers.MarshalListToken(ctx, listResp.ListToken, pbs.ResourceType_RESOURCE_TYPE_SCOPE)
if err != nil {
return nil, err
}
}
return resp, nil
}
// GetScopes implements the interface pbs.ScopeServiceServer.
func (s *Service) GetScope(ctx context.Context, req *pbs.GetScopeRequest) (*pbs.GetScopeResponse, error) {
const op = "scopes.(Service).GetScope"
if err := validateGetRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Read)
if authResults.Error != nil {
return nil, authResults.Error
}
p, err := s.getFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), idActionsById(p.GetPublicId())).Strings()))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
scopeInfo := &pb.ScopeInfo{
Id: p.GetPublicId(),
Type: p.Type,
Name: p.GetName(),
Description: p.GetDescription(),
ParentScopeId: p.GetParentId(),
}
collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], scopeInfo, "")
if err != nil {
return nil, err
}
outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions))
}
item, err := ToProto(ctx, p, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.GetScopeResponse{Item: item}, nil
}
// CreateScope implements the interface pbs.ScopeServiceServer.
func (s *Service) CreateScope(ctx context.Context, req *pbs.CreateScopeRequest) (*pbs.CreateScopeResponse, error) {
const op = "scopes.(Service).CreateScope"
if err := validateCreateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetItem().GetScopeId(), action.Create)
if authResults.Error != nil {
return nil, authResults.Error
}
p, err := s.createInRepo(ctx, authResults, req)
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), idActionsById(p.GetPublicId())).Strings()))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
scopeInfo := &pb.ScopeInfo{
Id: p.GetPublicId(),
Type: p.Type,
Name: p.GetName(),
Description: p.GetDescription(),
ParentScopeId: p.GetParentId(),
}
collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], scopeInfo, "")
if err != nil {
return nil, err
}
outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions))
}
item, err := ToProto(ctx, p, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.CreateScopeResponse{Item: item, Uri: fmt.Sprintf("scopes/%s", item.GetId())}, nil
}
// UpdateScope implements the interface pbs.ScopeServiceServer.
func (s *Service) UpdateScope(ctx context.Context, req *pbs.UpdateScopeRequest) (*pbs.UpdateScopeResponse, error) {
const op = "scopes.(Service).UpdateScope"
if err := validateUpdateRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Update)
if authResults.Error != nil {
return nil, authResults.Error
}
p, err := s.updateInRepo(ctx, authResults.Scope, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
if err != nil {
return nil, err
}
outputFields, ok := requests.OutputFields(ctx)
if !ok {
return nil, errors.New(ctx, errors.Internal, op, "no request context found")
}
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authResults.FetchActionSetForId(ctx, p.GetPublicId(), idActionsById(p.GetPublicId())).Strings()))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
scopeInfo := &pb.ScopeInfo{
Id: p.GetPublicId(),
Type: p.Type,
Name: p.GetName(),
Description: p.GetDescription(),
ParentScopeId: p.GetParentId(),
}
collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[p.Type], scopeInfo, "")
if err != nil {
return nil, err
}
outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions))
}
item, err := ToProto(ctx, p, outputOpts...)
if err != nil {
return nil, err
}
return &pbs.UpdateScopeResponse{Item: item}, nil
}
// DeleteScope implements the interface pbs.ScopeServiceServer.
func (s *Service) DeleteScope(ctx context.Context, req *pbs.DeleteScopeRequest) (*pbs.DeleteScopeResponse, error) {
if err := validateDeleteRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.Delete)
if authResults.Error != nil {
return nil, authResults.Error
}
_, err := s.deleteFromRepo(ctx, req.GetId())
if err != nil {
return nil, err
}
return nil, nil
}
// ListKeys implements the interface pbs.ScopeServiceServer.
func (s *Service) ListKeys(ctx context.Context, req *pbs.ListKeysRequest) (*pbs.ListKeysResponse, error) {
if req.GetId() == "" {
req.Id = scope.Global.String()
}
if err := validateListKeysRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetId(), action.ListScopeKeys)
if authResults.Error != nil {
return nil, authResults.Error
}
items, err := s.kmsRepo.ListKeys(ctx, req.GetId())
if err != nil {
if errors.IsNotFoundError(err) {
return nil, handlers.NotFoundErrorf("unknown scope_id %q", req.Id)
}
return nil, err
}
finalItems := make([]*pb.Key, 0, len(items))
res := perms.Resource{
Type: resource.Scope,
}
for _, item := range items {
res.Id = item.Id
res.ScopeId = item.Scope
outputFields := authResults.FetchOutputFields(res, action.ListScopeKeys).SelfOrDefaults(authResults.UserId)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
protoItem, err := keyToProto(ctx, item, outputOpts...)
if err != nil {
return nil, err
}
finalItems = append(finalItems, protoItem)
}
sortKeys(finalItems)
return &pbs.ListKeysResponse{Items: finalItems}, nil
}
// RotateKeys implements the interface pbs.ScopeServiceServer.
func (s *Service) RotateKeys(ctx context.Context, req *pbs.RotateKeysRequest) (*pbs.RotateKeysResponse, error) {
if req.GetScopeId() == "" {
req.ScopeId = scope.Global.String()
}
if err := validateRotateKeysRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.RotateScopeKeys)
if authResults.Error != nil {
return nil, authResults.Error
}
if err := s.kmsRepo.RotateKeys(ctx, req.GetScopeId(), kms.WithRewrap(req.GetRewrap())); err != nil {
return nil, err
}
return nil, nil
}
// ListKeyVersionDestructionJobs implements the interface pbs.ScopeServiceServer.
func (s *Service) ListKeyVersionDestructionJobs(ctx context.Context, req *pbs.ListKeyVersionDestructionJobsRequest) (*pbs.ListKeyVersionDestructionJobsResponse, error) {
if req.GetScopeId() == "" {
req.ScopeId = scope.Global.String()
}
if err := validateListKeyVersionDestructionJobsRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.ListScopeKeyVersionDestructionJobs)
if authResults.Error != nil {
return nil, authResults.Error
}
jobs, err := s.kmsRepo.ListDataKeyVersionDestructionJobs(ctx, req.GetScopeId())
if err != nil {
return nil, err
}
res := perms.Resource{
Type: resource.Scope,
}
finalJobs := make([]*pb.KeyVersionDestructionJob, 0, len(jobs))
for _, job := range jobs {
res.Id = job.ScopeId
res.ScopeId = job.ScopeId
outputFields := authResults.FetchOutputFields(res, action.ListScopeKeyVersionDestructionJobs).SelfOrDefaults(authResults.UserId)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(authResults.Scope))
}
protoJob, err := destructionJobToProto(ctx, job, outputOpts...)
if err != nil {
return nil, err
}
finalJobs = append(finalJobs, protoJob)
}
return &pbs.ListKeyVersionDestructionJobsResponse{
Items: finalJobs,
}, nil
}
// DestroyKeyVersion implements the interface pbs.ScopeServiceServer.
func (s *Service) DestroyKeyVersion(ctx context.Context, req *pbs.DestroyKeyVersionRequest) (*pbs.DestroyKeyVersionResponse, error) {
if req.GetScopeId() == "" {
req.ScopeId = scope.Global.String()
}
if err := validateDestroyKeyVersionRequest(req); err != nil {
return nil, err
}
authResults := s.authResult(ctx, req.GetScopeId(), action.DestroyScopeKeyVersion)
if authResults.Error != nil {
return nil, authResults.Error
}
destroyed, err := s.kmsRepo.DestroyKeyVersion(ctx, req.GetScopeId(), req.GetKeyVersionId())
if err != nil {
if errors.Match(errors.T(errors.KeyNotFound), err) {
return nil, handlers.NotFoundErrorf("unknown key_version_id %q", req.KeyVersionId)
}
return nil, err
}
state := "completed"
if !destroyed {
state = "pending"
}
return &pbs.DestroyKeyVersionResponse{
State: state,
}, nil
}
// AttachStoragePolicy implements the interface pbs.ScopeServiceServer.
func (s *Service) AttachStoragePolicy(ctx context.Context, req *pbs.AttachStoragePolicyRequest) (*pbs.AttachStoragePolicyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "Policies are an Enterprise-only feature")
}
// DetachStoragePolicy implements the interface pbs.ScopeServiceServer.
func (s *Service) DetachStoragePolicy(ctx context.Context, req *pbs.DetachStoragePolicyRequest) (*pbs.DetachStoragePolicyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "Policies are an Enterprise-only feature")
}
func (s *Service) getFromRepo(ctx context.Context, id string) (*iam.Scope, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, err := repo.LookupScope(ctx, id)
if err != nil {
return nil, err
}
if out == nil {
return nil, handlers.NotFoundErrorf("Scope %q doesn't exist.", id)
}
return out, nil
}
func (s *Service) createInRepo(ctx context.Context, authResults auth.VerifyResults, req *pbs.CreateScopeRequest) (*iam.Scope, error) {
const op = "scopes.(Service).createInRepo"
item := req.GetItem()
var opts []iam.Option
if item.GetName() != nil {
opts = append(opts, iam.WithName(item.GetName().GetValue()))
}
if item.GetDescription() != nil {
opts = append(opts, iam.WithDescription(item.GetDescription().GetValue()))
}
opts = append(opts, iam.WithSkipAdminRoleCreation(req.GetSkipAdminRoleCreation()))
opts = append(opts, iam.WithSkipDefaultRoleCreation(req.GetSkipDefaultRoleCreation()))
parentScope := authResults.Scope
var iamScope *iam.Scope
var err error
switch parentScope.GetType() {
case scope.Global.String():
iamScope, err = iam.NewOrg(ctx, opts...)
case scope.Org.String():
iamScope, err = iam.NewProject(ctx, parentScope.GetId(), opts...)
}
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build new scope for creation: %v.", err)
}
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, err := repo.CreateScope(ctx, iamScope, authResults.UserId, opts...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create scope"))
}
if out == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create scope but no error returned from repository.")
}
return out, nil
}
func (s *Service) updateInRepo(ctx context.Context, parentScope *pb.ScopeInfo, scopeId string, mask []string, item *pb.Scope) (*iam.Scope, error) {
const op = "scope.(Service).updateInRepo"
var opts []iam.Option
var scopeDesc, scopeName, scopePrimaryAuthMethodId string
if desc := item.GetDescription(); desc != nil {
scopeDesc = desc.GetValue()
opts = append(opts, iam.WithDescription(scopeDesc))
}
if name := item.GetName(); name != nil {
scopeName = name.GetValue()
opts = append(opts, iam.WithName(scopeName))
}
if primaryAuthMethodId := item.GetPrimaryAuthMethodId(); primaryAuthMethodId != nil {
if !handlers.ValidId(handlers.Id(primaryAuthMethodId.GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) {
return nil, handlers.InvalidArgumentErrorf("Error in provided request.", map[string]string{"primary_auth_method_id": "Improperly formatted identifier"})
}
scopePrimaryAuthMethodId = primaryAuthMethodId.GetValue()
opts = append(opts, iam.WithPrimaryAuthMethodId(scopePrimaryAuthMethodId))
}
version := item.GetVersion()
var iamScope *iam.Scope
var err error
switch {
case scopeId == scope.Global.String():
// boundary does not allow you to create a new global scope, so
// we'll build the required scope by hand for the update.
s := iam.AllocScope()
s.PublicId = scopeId
iamScope = &s
iamScope.Description = scopeDesc
iamScope.Name = scopeName
iamScope.PrimaryAuthMethodId = scopePrimaryAuthMethodId
case parentScope.GetType() == scope.Global.String():
iamScope, err = iam.NewOrg(ctx, opts...)
case parentScope.GetType() == scope.Org.String():
iamScope, err = iam.NewProject(ctx, parentScope.GetId(), opts...)
}
if err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build scope for update: %v.", err)
}
iamScope.PublicId = scopeId
dbMask := maskManager.Translate(mask)
if len(dbMask) == 0 {
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
}
repo, err := s.repoFn()
if err != nil {
return nil, err
}
out, rowsUpdated, err := repo.UpdateScope(ctx, iamScope, version, dbMask)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update project"))
}
if rowsUpdated == 0 {
return nil, handlers.NotFoundErrorf("Scope %q doesn't exist or incorrect version provided.", scopeId)
}
return out, nil
}
func (s *Service) deleteFromRepo(ctx context.Context, scopeId string) (bool, error) {
const op = "scope.(Service).deleteFromRepo"
repo, err := s.repoFn()
if err != nil {
return false, err
}
rows, err := repo.DeleteScope(ctx, scopeId)
if err != nil {
return false, errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete scope"))
}
return rows > 0, nil
}
func sortKeys(keys []*pb.Key) {
// We stable sort here even though the database may not return things in
// sorted order, still nice to have them as consistent as possible.
sort.SliceStable(keys, func(i, j int) bool {
return keys[i].GetId() < keys[j].GetId()
})
// we also want to sort the key versions by version id so they are in
// descending order (newest first)
for _, key := range keys {
sort.Slice(key.Versions, func(i, j int) bool {
return key.Versions[i].Version > key.Versions[j].Version
})
}
}
func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults {
res := auth.VerifyResults{}
repo, err := s.repoFn()
if err != nil {
res.Error = err
return res
}
var parentId string
opts := []auth.Option{auth.WithType(resource.Scope), auth.WithAction(a)}
switch a {
case action.List, action.Create, action.ListScopeKeys, action.ListScopeKeyVersionDestructionJobs, action.DestroyScopeKeyVersion:
parentId = id
s, err := repo.LookupScope(ctx, parentId)
if err != nil {
res.Error = err
return res
}
if s == nil {
res.Error = handlers.NotFoundError()
return res
}
default:
s, err := repo.LookupScope(ctx, id)
if err != nil {
res.Error = err
return res
}
if s == nil {
res.Error = handlers.NotFoundError()
return res
}
parentId = s.GetParentId()
opts = append(opts, auth.WithId(id))
}
opts = append(opts, auth.WithScopeId(parentId))
return auth.Verify(ctx, opts...)
}
func ToProto(ctx context.Context, in *iam.Scope, opt ...handlers.Option) (*pb.Scope, error) {
opts := handlers.GetOpts(opt...)
if opts.WithOutputFields == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building scope proto")
}
outputFields := *opts.WithOutputFields
out := pb.Scope{}
if outputFields.Has(globals.IdField) {
out.Id = in.GetPublicId()
}
if outputFields.Has(globals.ScopeIdField) {
out.ScopeId = in.GetParentId()
}
if outputFields.Has(globals.TypeField) {
out.Type = in.GetType()
}
if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" {
out.Description = wrapperspb.String(in.GetDescription())
}
if outputFields.Has(globals.NameField) && in.GetName() != "" {
out.Name = wrapperspb.String(in.GetName())
}
if outputFields.Has(globals.CreatedTimeField) {
out.CreatedTime = in.GetCreateTime().GetTimestamp()
}
if outputFields.Has(globals.UpdatedTimeField) {
out.UpdatedTime = in.GetUpdateTime().GetTimestamp()
}
if outputFields.Has(globals.VersionField) {
out.Version = in.GetVersion()
}
if outputFields.Has(globals.ScopeField) {
out.Scope = opts.WithScope
}
if outputFields.Has(globals.AuthorizedActionsField) {
out.AuthorizedActions = opts.WithAuthorizedActions
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
out.AuthorizedCollectionActions = opts.WithAuthorizedCollectionActions
}
if outputFields.Has(globals.PrimaryAuthMethodIdField) && in.GetPrimaryAuthMethodId() != "" {
out.PrimaryAuthMethodId = &wrapperspb.StringValue{Value: in.GetPrimaryAuthMethodId()}
}
if outputFields.Has(globals.StoragePolicyIdField) {
out.StoragePolicyId = in.GetStoragePolicyId()
}
return &out, nil
}
func keyToProto(ctx context.Context, in wrappingKms.Key, opt ...handlers.Option) (*pb.Key, error) {
opts := handlers.GetOpts(opt...)
if opts.WithOutputFields == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building key proto")
}
outputFields := *opts.WithOutputFields
out := pb.Key{}
if outputFields.Has(globals.IdField) {
out.Id = in.Id
}
if outputFields.Has(globals.ScopeField) {
out.Scope = opts.WithScope
}
if outputFields.Has(globals.KeyPurposeField) {
out.Purpose = string(in.Purpose)
}
if outputFields.Has(globals.CreatedTimeField) {
out.CreatedTime = timestamppb.New(in.CreateTime)
}
if outputFields.Has(globals.TypeField) {
out.Type = string(in.Type)
}
if outputFields.Has(globals.KeyVersionsField) {
for _, keyVersion := range in.Versions {
out.Versions = append(out.Versions, &pb.KeyVersion{
Id: keyVersion.Id,
Version: uint32(keyVersion.Version),
CreatedTime: timestamppb.New(keyVersion.CreateTime),
})
}
}
return &out, nil
}
func destructionJobToProto(ctx context.Context, in *kms.DataKeyVersionDestructionJobProgress, opt ...handlers.Option) (*pb.KeyVersionDestructionJob, error) {
opts := handlers.GetOpts(opt...)
if opts.WithOutputFields == nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "output fields not found when building data key version destruction job proto")
}
outputFields := *opts.WithOutputFields
out := pb.KeyVersionDestructionJob{}
if outputFields.Has(globals.KeyVersionIdField) {
out.KeyVersionId = in.KeyId
}
if outputFields.Has(globals.ScopeField) {
out.Scope = opts.WithScope
}
if outputFields.Has(globals.CreatedTimeField) {
out.CreatedTime = in.CreateTime.Timestamp
}
if outputFields.Has(globals.CompletedCountField) {
out.CompletedCount = in.CompletedCount
}
if outputFields.Has(globals.TotalCountField) {
out.TotalCount = in.TotalCount
}
if outputFields.Has(globals.StatusField) {
out.Status = in.Status
}
return &out, nil
}
// A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce
// requirements on the structure of the request. They verify that:
// - The path passed in is correctly formatted
// - All required parameters are set
// - There are no conflicting parameters provided
func validateGetRequest(req *pbs.GetScopeRequest) error {
badFields := map[string]string{}
id := req.GetId()
switch {
case id == scope.Global.String():
case strings.HasPrefix(id, scope.Org.Prefix()):
if !handlers.ValidId(handlers.Id(id), scope.Org.Prefix()) {
badFields["id"] = "Invalidly formatted scope id."
}
case strings.HasPrefix(id, scope.Project.Prefix()):
if !handlers.ValidId(handlers.Id(id), scope.Project.Prefix()) {
badFields["id"] = "Invalidly formatted scope id."
}
default:
badFields["id"] = "Invalidly formatted scope id."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func validateCreateRequest(req *pbs.CreateScopeRequest) error {
return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string {
badFields := map[string]string{}
item := req.GetItem()
if item.GetScopeId() == "" {
badFields["scope_id"] = "Missing value for scope_id."
}
switch item.GetType() {
case scope.Global.String():
badFields["type"] = "Cannot create a global scope."
case scope.Org.String():
if !strings.EqualFold(scope.Global.String(), item.GetScopeId()) {
badFields["type"] = "Org scopes can only be created under the global scope."
}
case scope.Project.String():
if !handlers.ValidId(handlers.Id(item.GetScopeId()), scope.Org.Prefix()) {
badFields["type"] = "Project scopes can only be created under an org scope."
}
}
return badFields
})
}
func validateUpdateRequest(req *pbs.UpdateScopeRequest) error {
badFields := map[string]string{}
id := req.GetId()
switch {
case id == scope.Global.String():
case strings.HasPrefix(id, scope.Org.Prefix()):
if !handlers.ValidId(handlers.Id(id), scope.Org.Prefix()) {
badFields["id"] = "Invalidly formatted scope id."
}
if req.GetItem().GetType() != "" && !strings.EqualFold(scope.Org.String(), req.GetItem().GetType()) {
badFields["type"] = "Cannot modify the resource type."
}
case strings.HasPrefix(id, scope.Project.Prefix()):
if !handlers.ValidId(handlers.Id(id), scope.Project.Prefix()) {
badFields["id"] = "Invalidly formatted scope id."
}
if req.GetItem().GetType() != "" && !strings.EqualFold(scope.Project.String(), req.GetItem().GetType()) {
badFields["type"] = "Cannot modify the resource type."
}
default:
badFields["id"] = "Invalidly formatted scope id."
}
if req.GetUpdateMask() == nil {
badFields["update_mask"] = "UpdateMask not provided but is required to update a project."
}
item := req.GetItem()
if item == nil {
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
// It is legitimate for no item to be specified in an update request as it indicates all fields provided in
// the mask will be marked as unset.
return nil
}
if item.GetVersion() == 0 {
badFields["version"] = "Existing resource version is required for an update."
}
if item.GetId() != "" {
badFields["id"] = "This is a read only field and cannot be specified in an update request."
}
if item.GetCreatedTime() != nil {
badFields["created_time"] = "This is a read only field and cannot be specified in an update request."
}
if item.GetUpdatedTime() != nil {
badFields["updated_time"] = "This is a read only field and cannot be specified in an update request."
}
if item.GetPrimaryAuthMethodId().GetValue() != "" && !handlers.ValidId(handlers.Id(item.GetPrimaryAuthMethodId().GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) {
badFields["primary_auth_method_id"] = "Improperly formatted identifier."
}
if item.GetName() != nil {
trimmed := strings.TrimSpace(item.GetName().GetValue())
switch {
case trimmed == "":
badFields["name"] = "Cannot set empty string as name"
case !handlers.ValidName(trimmed):
badFields["name"] = "Name contains unprintable characters"
default:
item.GetName().Value = trimmed
}
}
if item.GetDescription() != nil {
trimmed := strings.TrimSpace(item.GetDescription().GetValue())
switch {
case trimmed == "":
badFields["description"] = "Cannot set empty string as description"
case !handlers.ValidDescription(trimmed):
badFields["description"] = "Description contains unprintable characters"
default:
item.GetDescription().Value = trimmed
}
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func validateDeleteRequest(req *pbs.DeleteScopeRequest) error {
badFields := map[string]string{}
id := req.GetId()
switch {
case id == scope.Global.String():
badFields["id"] = "Invalid to delete the global scope."
case strings.HasPrefix(id, scope.Org.Prefix()):
if !handlers.ValidId(handlers.Id(id), scope.Org.Prefix()) {
badFields["id"] = "Invalidly formatted scope id."
}
case strings.HasPrefix(id, scope.Project.Prefix()):
if !handlers.ValidId(handlers.Id(id), scope.Project.Prefix()) {
badFields["id"] = "Invalidly formatted scope id."
}
default:
badFields["id"] = "Invalidly formatted scope id."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func validateListRequest(ctx context.Context, req *pbs.ListScopesRequest) error {
badFields := map[string]string{}
if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) {
badFields["scope_id"] = "Must be 'global' or a valid org scope id when listing."
}
if _, err := handlers.NewFilter(ctx, req.GetFilter()); err != nil {
badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err)
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func validateListKeysRequest(req *pbs.ListKeysRequest) error {
badFields := map[string]string{}
if req.GetId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetId()), scope.Project.Prefix()) {
badFields["id"] = "Must be 'global', a valid org scope id or a valid project scope id when listing keys."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func validateRotateKeysRequest(req *pbs.RotateKeysRequest) error {
badFields := map[string]string{}
if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) {
badFields["id"] = "Must be 'global', a valid org scope id or a valid project scope id when listing keys."
}
// other field is just a bool so can't be validated
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func validateListKeyVersionDestructionJobsRequest(req *pbs.ListKeyVersionDestructionJobsRequest) error {
badFields := map[string]string{}
if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) {
badFields["scope_id"] = "Must be 'global', a valid org scope id or a valid project scope id when listing key version destruction jobs."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func validateDestroyKeyVersionRequest(req *pbs.DestroyKeyVersionRequest) error {
badFields := map[string]string{}
if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) {
badFields["scope_id"] = "Must be 'global', a valid org scope id or a valid project scope id when destroying a key version."
}
if !handlers.ValidId(handlers.Id(req.GetKeyVersionId()), "kdkv", "krkv") {
badFields["key_version_id"] = "Must be a valid KEK or DEK version ID."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Error in provided request.", badFields)
}
return nil
}
func newOutputOpts(ctx context.Context, item *iam.Scope, authResults auth.VerifyResults, scopeInfoMap map[string]*pb.ScopeInfo) ([]handlers.Option, bool, error) {
res := perms.Resource{
Type: resource.Scope,
Id: item.GetPublicId(),
ScopeId: item.GetParentId(),
}
authorizedActions := authResults.FetchActionSetForId(ctx, item.GetPublicId(), idActionsById(item.GetPublicId()), auth.WithResource(&res)).Strings()
if len(authorizedActions) == 0 {
return nil, false, nil
}
outputFields := authResults.FetchOutputFields(res, action.List).SelfOrDefaults(authResults.UserId)
outputOpts := make([]handlers.Option, 0, 3)
outputOpts = append(outputOpts, handlers.WithOutputFields(outputFields))
if outputFields.Has(globals.ScopeField) {
outputOpts = append(outputOpts, handlers.WithScope(scopeInfoMap[item.GetParentId()]))
}
if outputFields.Has(globals.AuthorizedActionsField) {
outputOpts = append(outputOpts, handlers.WithAuthorizedActions(authorizedActions))
}
if outputFields.Has(globals.AuthorizedCollectionActionsField) {
scopeInfo := &pb.ScopeInfo{
Id: item.GetPublicId(),
Type: item.Type,
Name: item.GetName(),
Description: item.GetDescription(),
ParentScopeId: item.GetParentId(),
}
collectionActions, err := auth.CalculateAuthorizedCollectionActions(ctx, authResults, scopeCollectionTypeMapMap[item.Type], scopeInfo, "")
if err != nil {
return nil, false, err
}
outputOpts = append(outputOpts, handlers.WithAuthorizedCollectionActions(collectionActions))
}
return outputOpts, true, nil
}
func idActionsById(id string) action.ActionSet {
act := IdActions
switch {
case id == scope.Global.String():
// Can't delete global so elide it
act = action.Difference(act, action.NewActionSet(action.Delete))
case strings.HasPrefix(id, fmt.Sprintf("%s_", scope.Project.Prefix())):
// Can't attach/detach storage policy to projects
act = action.Difference(act, action.NewActionSet(action.AttachStoragePolicy, action.DetachStoragePolicy))
}
return act
}