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/iam/repository_role_grant_scope.go

862 lines
38 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package iam
import (
"context"
"fmt"
"slices"
"strings"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/iam/store"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/boundary/internal/util"
)
// AddRoleGrantScopes will add role grant scopes associated with the role ID in
// the repository. No options are currently supported. Zero is not a valid value
// for the WithVersion option and will return an error.
func (r *Repository) AddRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, _ ...Option) ([]*RoleGrantScope, error) {
const op = "iam.(Repository).AddRoleGrantScopes"
switch {
case roleId == "":
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case len(grantScopes) == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grant scopes")
case roleVersion == 0:
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
if slices.Contains(grantScopes, globals.GrantScopeDescendants) && slices.Contains(grantScopes, globals.GrantScopeChildren) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "only one of descendants or children grant scope can be specified")
}
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
// Find existing grant scopes to find duplicate grants
originalGrantScopes, err := listRoleGrantScopes(ctx, r.reader, []string{roleId})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grant scopes"))
}
originalGrantScopeMap := map[string]struct{}{}
for _, rgs := range originalGrantScopes {
originalGrantScopeMap[rgs.ScopeIdOrSpecial] = struct{}{}
}
// deduplicate and create a map that contains only new grant scope we need to add
toAdd := make(map[string]struct{})
for _, scopeId := range grantScopes {
if _, ok := originalGrantScopeMap[scopeId]; ok {
delete(originalGrantScopeMap, scopeId)
continue
}
toAdd[scopeId] = struct{}{}
}
// no new scope to add so we're returning early
if len(toAdd) == 0 {
return []*RoleGrantScope{}, nil
}
// Allocate a subtype-specific role and manually bump version to ensure that version gets updated
// even when only individual grant scopes, which are stored in separate tables, are modified
updateRole, err := allocRoleScopeGranter(ctx, roleId, scp.GetType())
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to allocate role subtype to add role grant scopes"))
}
updateRole.setVersion(roleVersion + 1)
updateMask := []string{"Version"}
_, addThis := toAdd[globals.GrantScopeThis]
if addThis {
updateRole.setGrantThisRoleScope(true)
updateMask = append(updateMask, "GrantThisRoleScope")
}
// finalGrantScope is used to determine 'grant_scope' value for individualGlobalRoleGrantScopes.
// This only matters if the role is a global role and the grantScopes contains individual project scope, because
// there are 2 possible values: ['individual', 'children'].
// If no grant scope is being added, we must use the current value of iam_role_<type>.grant_scope column
finalGrantScope := globals.GrantScopeIndividual
_, addChildren := toAdd[globals.GrantScopeChildren]
_, addDescendants := toAdd[globals.GrantScopeDescendants]
switch {
case addDescendants:
// cannot add 'descendants' when children is already set, only one hierarchical grant scope can be set
if _, ok := originalGrantScopeMap[globals.GrantScopeChildren]; ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "grant scope children already exists, only one of descendants or children grant scope can be specified")
}
err = updateRole.setGrantScope(ctx, globals.GrantScopeDescendants)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
finalGrantScope = globals.GrantScopeDescendants
case addChildren:
// cannot add 'children' when descendant is already set, only one hierarchical grant scope can be set
if _, ok := originalGrantScopeMap[globals.GrantScopeDescendants]; ok {
return nil, errors.New(ctx, errors.InvalidParameter, op, "grant scope descendants already exists, only one of descendants or children grant scope can be specified")
}
err = updateRole.setGrantScope(ctx, globals.GrantScopeChildren)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
finalGrantScope = globals.GrantScopeChildren
default:
// if no hierarchical grant scope is being added, we need to check if 'children' grant is set
// to properly set 'grant_scope' on global role's individual project grant scopes value which has
// two possible values: ['children', 'individual']
// this is an edge case for global role which can have both children and individual project grant scopes
if _, ok := originalGrantScopeMap[globals.GrantScopeChildren]; ok {
finalGrantScope = globals.GrantScopeChildren
}
}
// generate a list of 'individual' scopes that need to be inserted to the database
// excluding the non-individual grant scopes [this, descendants, children]
individualScopesToAdd := make([]string, 0, len(toAdd))
for scopeId := range toAdd {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToAdd = append(individualScopesToAdd, scopeId)
}
}
// return early because there's no new scope to add
if !addDescendants && !addChildren && !addThis && len(individualScopesToAdd) == 0 {
return []*RoleGrantScope{}, nil
}
var retRoleGrantScopes []*RoleGrantScope
var globalRoleOrgGrantScopes []*globalRoleIndividualOrgGrantScope
var globalRoleProjectGrantScopes []*globalRoleIndividualProjectGrantScope
var orgRoleGrantScopes []*orgRoleIndividualGrantScope
switch scp.GetType() {
case scope.Global.String():
globalRoleOrgGrantScopes, globalRoleProjectGrantScopes, err = individualGlobalRoleGrantScope(ctx, roleId, finalGrantScope, individualScopesToAdd)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split global roles grant scopes"))
}
case scope.Org.String():
orgRoleGrantScopes, err = individualOrgGrantScope(ctx, roleId, individualScopesToAdd)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split org roles grant scopes"))
}
default:
// granting individual grant scope to roles is only allowed for roles in global and org scopes
if len(individualScopesToAdd) > 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "individual role grant scope can only be set for global roles or org roles")
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, updateRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, updateRole, updateMask, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role"))
}
if addThis {
if g, ok := updateRole.grantThisRoleScope(); ok {
retRoleGrantScopes = append(retRoleGrantScopes, g)
}
}
if addDescendants || addChildren {
if g, ok := updateRole.grantScope(); ok {
retRoleGrantScopes = append(retRoleGrantScopes, g)
}
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
if len(globalRoleOrgGrantScopes) > 0 {
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleOrgGrantScopes))
if err := w.CreateItems(ctx, globalRoleOrgGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual org grant scopes for global role"))
}
for _, gro := range globalRoleOrgGrantScopes {
retRoleGrantScopes = append(retRoleGrantScopes, gro.roleGrantScope())
}
}
if len(globalRoleProjectGrantScopes) > 0 {
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleProjectGrantScopes))
if err := w.CreateItems(ctx, globalRoleProjectGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scopes for global role"))
}
for _, grp := range globalRoleProjectGrantScopes {
retRoleGrantScopes = append(retRoleGrantScopes, grp.roleGrantScope())
}
}
if len(orgRoleGrantScopes) > 0 {
roleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(orgRoleGrantScopes))
if err := w.CreateItems(ctx, orgRoleGrantScopes, db.NewOplogMsgs(&roleGrantScopesOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scopes for global role"))
}
for _, or := range orgRoleGrantScopes {
retRoleGrantScopes = append(retRoleGrantScopes, or.roleGrantScope())
}
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return retRoleGrantScopes, nil
}
// DeleteRoleGrantScopes will delete role grant scopes associated with the role ID in
// the repository. No options are currently supported. Zero is not a valid value
// for the WithVersion option and will return an error.
// This function returns an 'int' representing number of rows deleted.
func (r *Repository) DeleteRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, _ ...Option) (int, error) {
const op = "iam.(Repository).DeleteRoleGrantScopes"
switch {
case roleId == "":
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case len(grantScopes) == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grant scopes")
case roleVersion == 0:
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
originalGrantScopes, err := listRoleGrantScopes(ctx, r.reader, []string{roleId})
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to list role grant scopes"))
}
originalGrantScopeMap := map[string]struct{}{}
for _, rgs := range originalGrantScopes {
originalGrantScopeMap[rgs.ScopeIdOrSpecial] = struct{}{}
}
toRemove := map[string]struct{}{}
// finding grants
for _, s := range grantScopes {
// grants doesn't exist in the original grant scopes so no need to delete
if _, ok := originalGrantScopeMap[s]; !ok {
continue
}
toRemove[s] = struct{}{}
}
// return early if there's nothing to remove
if len(toRemove) == 0 {
return db.NoRowsAffected, nil
}
// totalGrantScopeRemoved for special grant scopes ['this', 'descendants', 'children'] must be counted manually,
// as they are not stored as individual rows. They are stored as columns in the role entry.
// Individual grant scopes still rely on 'rowsDeleted' number returned from 'DeleteItems' calls, so we calculate
// a number that simulates the number of scopes removed.
var totalGrantScopeRemoved int
updateRole, err := allocRoleScopeGranter(ctx, roleId, scp.GetType())
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to allocate role resource"))
}
updateMask := []string{"Version"}
updateRole.setVersion(roleVersion + 1)
_, removeThis := toRemove[globals.GrantScopeThis]
_, removeChildren := toRemove[globals.GrantScopeChildren]
_, removeDescendants := toRemove[globals.GrantScopeDescendants]
// handle case where 'this' grant scope is removed
if removeThis {
updateRole.setGrantThisRoleScope(false)
updateMask = append(updateMask, "GrantThisRoleScope")
// manually bump rows deleted when for deleting 'this' grant scope since this is now
// a DB row update instead of deleting a row.
totalGrantScopeRemoved += 1
}
// handle case where hierarchical grant scope ['children', 'descendants'] is removed
// these grants are mutually exclusive so an OR operation is safe here
if (removeChildren || removeDescendants) && scp.Type != scope.Project.String() {
updateRole.removeGrantScope()
updateMask = append(updateMask, "GrantScope")
// manually bump rows deleted when for deleting hierarchical grant scope since this is now
// a DB row update instead of deleting a row.
totalGrantScopeRemoved += 1
}
// Generate a list of individual grant scopes that need to be removed from the database
// excluding non-individual grant scopes [this, descendants, children]
individualScopesToRemove := make([]string, 0, len(toRemove))
for scopeId := range toRemove {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToRemove = append(individualScopesToRemove, scopeId)
}
}
// split the list of individual scope to remove into type-specific slices
var globalRoleOrgToRemove []*globalRoleIndividualOrgGrantScope
var globalRoleProjToRemove []*globalRoleIndividualProjectGrantScope
var orgRoleProjToRemove []*orgRoleIndividualGrantScope
switch scp.GetType() {
case scope.Global.String():
// projGrantScope can be hardcoded since we're deleting entries, the foreign key check does not apply here
projGrantScope := globals.GrantScopeIndividual
globalRoleOrgToRemove, globalRoleProjToRemove, err = individualGlobalRoleGrantScope(ctx, roleId, projGrantScope, individualScopesToRemove)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split global roles grant scopes"))
}
case scope.Org.String():
orgRoleProjToRemove, err = individualOrgGrantScope(ctx, roleId, individualScopesToRemove)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to split org roles grant scopes"))
}
default:
// granting individual grant scope to roles is only allowed for roles in global and org scopes
// but deleting individual grant scopes when the grant scope doesn't exist on a role is allowed
// so we don't return an error here and treat this as a no-op
}
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, updateRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, updateRole, updateMask, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
if len(globalRoleOrgToRemove) > 0 {
globalRoleOrgGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleOrgToRemove))
rowsDeleted, err := w.DeleteItems(ctx, globalRoleOrgToRemove, db.NewOplogMsgs(&globalRoleOrgGrantScopesOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to remove global role individual org grants"))
}
totalGrantScopeRemoved += rowsDeleted
msgs = append(msgs, globalRoleOrgGrantScopesOplogMsgs...)
}
if len(globalRoleProjToRemove) > 0 {
globalRoleProjGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(globalRoleOrgToRemove))
rowsDeleted, err := w.DeleteItems(ctx, globalRoleProjToRemove, db.NewOplogMsgs(&globalRoleProjGrantScopesOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to remove global role individual project grants"))
}
totalGrantScopeRemoved += rowsDeleted
msgs = append(msgs, globalRoleProjGrantScopesOplogMsgs...)
}
if len(orgRoleProjToRemove) > 0 {
orgRoleGrantScopesOplogMsgs := make([]*oplog.Message, 0, len(orgRoleProjToRemove))
rowsDeleted, err := w.DeleteItems(ctx, orgRoleProjToRemove, db.NewOplogMsgs(&orgRoleGrantScopesOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to remove org role individual project grants"))
}
totalGrantScopeRemoved += rowsDeleted
msgs = append(msgs, orgRoleGrantScopesOplogMsgs...)
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return totalGrantScopeRemoved, nil
}
// SetRoleGrantScopes sets grant scopes on a role (roleId). The role's current
// db version
// must match the roleVersion or an error will be returned. Zero is not a valid
// value for the WithVersion option and will return an error.
func (r *Repository) SetRoleGrantScopes(ctx context.Context, roleId string, roleVersion uint32, grantScopes []string, opt ...Option) ([]*RoleGrantScope, int, error) {
const op = "iam.(Repository).SetRoleGrantScopes"
switch {
case roleId == "":
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
case roleVersion == 0:
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
case grantScopes == nil:
// Explicitly set to zero clears, but treat nil as a mistake
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grants")
}
reader := r.reader
writer := r.writer
needFreshReaderWriter := true
opts := getOpts(opt...)
if !util.IsNil(opts.withReader) && !util.IsNil(opts.withWriter) {
reader = opts.withReader
writer = opts.withWriter
needFreshReaderWriter = false
}
// fetch current role scopes
scp, err := getRoleScope(ctx, r.reader, roleId)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope id", roleId)))
}
// NOTE: Set calculation can safely take place out of the transaction since
// we are using roleVersion to ensure that we end up operating on the same
// set of data from this query to the final set in the transaction function
// Find existing grant scopes
originalGrantScopes, err := listRoleGrantScopes(ctx, reader, []string{roleId})
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grant scopes"))
}
originalGrantScopeMap := map[string]struct{}{}
for _, rgs := range originalGrantScopes {
originalGrantScopeMap[rgs.ScopeIdOrSpecial] = struct{}{}
}
// finalGrantScopeForIndividualGrantScope is the final value of what 'grant_scope' column of the role will be.
// this is only important for global roles with individual scopes IDs granted to the role.
// A foreign key between iam_role_global.grant_scope and iam_role_global_individual_project_grant_scope.grant_scope
// can either be ['children', 'individual'] which will prevents inserting if the values don't match.
// Assuming that the value is 'individual' and only set it to 'children' if we're adding a 'children' grant
// the current value in the database does not matter and will be overridden by this method
finalGrantScopeForIndividualGrantScope := globals.GrantScopeIndividual
toAdd := make(map[string]struct{})
for _, scopeId := range grantScopes {
if scopeId == globals.GrantScopeChildren {
// set final grant scope to 'children'. finalGrantScopeForIndividualGrantScope will be used later
// when constructing entries for individual project grant scope
// We have to do this before removing the already exist grants in case the role already contains 'children'
// and the caller attempts to set value to 'children' again
finalGrantScopeForIndividualGrantScope = scopeId
}
if _, ok := originalGrantScopeMap[scopeId]; ok {
delete(originalGrantScopeMap, scopeId)
continue
}
toAdd[scopeId] = struct{}{}
}
toRemove := make(map[string]struct{})
for scopeId := range originalGrantScopeMap {
toRemove[scopeId] = struct{}{}
}
// return early since there's no grant scope to add or remove
if len(toAdd) == 0 && len(toRemove) == 0 {
return []*RoleGrantScope{}, db.NoRowsAffected, nil
}
// totalRowsDeleted has to be calculated manually, especially for non-individual grant scopes [this, children, descendants]
// because those are deleted by updating 'GrantThisRoleScope' and 'GrantScope' column on the role table
// totalRowsDeleted are kept in place to maintain the existing contract
totalRowsDeleted := 0
updateRole, err := allocRoleScopeGranter(ctx, roleId, scp.GetType())
if err != nil {
return nil, 0, errors.Wrap(ctx, err, op, errors.WithMsg("unable to allocate role resource"))
}
// bump version manually to force version to change when the role entry doesn't
// version still needs to be bumped when a grant scope is added or removed
updateMask := []string{"Version"}
updateRole.setVersion(roleVersion + 1)
// handle 'this' grant scope - which is now stored in 'GrantThisRoleScope' column
_, removeThis := toRemove[globals.GrantScopeThis]
_, addThis := toAdd[globals.GrantScopeThis]
switch {
case addThis:
updateRole.setGrantThisRoleScope(true)
updateMask = append(updateMask, "GrantThisRoleScope")
case removeThis:
updateRole.setGrantThisRoleScope(false)
updateMask = append(updateMask, "GrantThisRoleScope")
// manually count row deleted since removing 'this' is done by an update to 'grant_this_role_scope' column
// on the role record
totalRowsDeleted++
}
// return early if the there's a conflict in grant_scopes we're trying to add
_, addDescendants := toAdd[globals.GrantScopeDescendants]
_, addChildren := toAdd[globals.GrantScopeChildren]
if addDescendants && addChildren {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "only one of ['children', 'descendants'] can be specified")
}
_, removeDescendants := toRemove[globals.GrantScopeDescendants]
_, removeChildren := toRemove[globals.GrantScopeChildren]
// children and descendants are mutually exclusive so we only need to count row once
if removeDescendants || removeChildren {
// manually count row deleted since removing 'descendants' or 'children' is done by an update to 'grant_scope' column
// on the role record
totalRowsDeleted++
}
// determine the final hierarchical grant scopes stored in `grant_scope` column [`descendants`, `children`]
// depending on if we're adding or removing grants
// if descendants or children is being added, set finalGrantScope to the grant-to-be-added
// to resolve the
switch {
case addDescendants:
err := updateRole.setGrantScope(ctx, globals.GrantScopeDescendants)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
case addChildren:
err := updateRole.setGrantScope(ctx, globals.GrantScopeChildren)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
updateMask = append(updateMask, "GrantScope")
case removeDescendants || removeChildren:
updateRole.removeGrantScope()
updateMask = append(updateMask, "GrantScope")
}
// generate a list of 'individual' scopes that need to be inserted to the database
// excluding the non-individual grant scopes ['this', 'descendants', 'children']
individualScopesToAdd := make([]string, 0, len(toAdd))
for scopeId := range toAdd {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToAdd = append(individualScopesToAdd, scopeId)
}
}
// generate a list of 'individual' scopes that need to be removed from the database
// excluding the non-individual grant scopes ['this', 'descendants', 'children']
individualScopesToRemove := make([]string, 0, len(toRemove))
for scopeId := range toRemove {
switch scopeId {
case globals.GrantScopeThis, globals.GrantScopeChildren, globals.GrantScopeDescendants:
continue
default:
individualScopesToRemove = append(individualScopesToRemove, scopeId)
}
}
// convert list of individual grant scopes to add and scopes to removed into their respective structs
// these lists will be passed to CreateItems and DeleteItems to create or remove
// individual grant scope entries
var globalRoleIndividualOrgToAdd []*globalRoleIndividualOrgGrantScope
var globalRoleIndividualProjToAdd []*globalRoleIndividualProjectGrantScope
var globalRoleIndividualOrgToRemove []*globalRoleIndividualOrgGrantScope
var globalRoleIndividualProjToRemove []*globalRoleIndividualProjectGrantScope
var orgRoleIndividualScopeToAdd []*orgRoleIndividualGrantScope
var orgRoleIndividualScopeToRemove []*orgRoleIndividualGrantScope
switch scp.Type {
case scope.Global.String():
globalRoleIndividualOrgToAdd, globalRoleIndividualProjToAdd, err = individualGlobalRoleGrantScope(ctx, roleId, finalGrantScopeForIndividualGrantScope, individualScopesToAdd)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert global role individual org scopes to scope specific scope object for grant scope addition"))
}
globalRoleIndividualOrgToRemove, globalRoleIndividualProjToRemove, err = individualGlobalRoleGrantScope(ctx, roleId, finalGrantScopeForIndividualGrantScope, individualScopesToRemove)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert global role individual proj scopes to scope specific scope object for grant scope removal"))
}
case scope.Org.String():
orgRoleIndividualScopeToAdd, err = individualOrgGrantScope(ctx, roleId, individualScopesToAdd)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert org role individual proj scopes to scope specific scope object for grant scope addition"))
}
orgRoleIndividualScopeToRemove, err = individualOrgGrantScope(ctx, roleId, individualScopesToRemove)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("failed to convert org role individual proj scopes to scope specific scope object for grant scope removal"))
}
default:
if len(individualScopesToRemove) > 0 || len(individualScopesToAdd) > 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op,
fmt.Sprintf("roles in scope type %s does not allow individual role grant scope", scp.Type))
}
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scp.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wra pper"))
}
var retGrantScopes []*RoleGrantScope
txFunc := func(rdr db.Reader, wtr db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := wtr.GetTicket(ctx, updateRole)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
if len(globalRoleIndividualOrgToRemove) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualOrgToRemove))
rowsDeleted, err := wtr.DeleteItems(ctx, globalRoleIndividualOrgToRemove, db.NewOplogMsgs(&roleGrantScopeOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete role global role individual org grant scope"))
}
if rowsDeleted != len(globalRoleIndividualOrgToRemove) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grant scope deleted %d did not match request for %d", rowsDeleted, len(globalRoleIndividualOrgToRemove)))
}
totalRowsDeleted += rowsDeleted
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(globalRoleIndividualProjToRemove) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualProjToRemove))
rowsDeleted, err := wtr.DeleteItems(ctx, globalRoleIndividualProjToRemove, db.NewOplogMsgs(&roleGrantScopeOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete global role individual project grant scope"))
}
if rowsDeleted != len(globalRoleIndividualProjToRemove) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grant scope deleted %d did not match request for %d", rowsDeleted, len(globalRoleIndividualProjToRemove)))
}
totalRowsDeleted += rowsDeleted
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(orgRoleIndividualScopeToRemove) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(orgRoleIndividualScopeToRemove))
rowsDeleted, err := wtr.DeleteItems(ctx, orgRoleIndividualScopeToRemove, db.NewOplogMsgs(&roleGrantScopeOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete org role individual project grant scope"))
}
if rowsDeleted != len(orgRoleIndividualScopeToRemove) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grant scope deleted %d did not match request for %d", rowsDeleted, len(orgRoleIndividualScopeToRemove)))
}
totalRowsDeleted += rowsDeleted
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
var roleOplogMsg oplog.Message
rowsUpdated, err := wtr.Update(ctx, updateRole, updateMask, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
if len(globalRoleIndividualOrgToAdd) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualOrgToAdd))
if err := wtr.CreateItems(ctx, globalRoleIndividualOrgToAdd, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual org grant scope for global role during set"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(globalRoleIndividualProjToAdd) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(globalRoleIndividualProjToAdd))
if err := wtr.CreateItems(ctx, globalRoleIndividualProjToAdd, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scope for global role during set"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
if len(orgRoleIndividualScopeToAdd) > 0 {
roleGrantScopeOplogMsgs := make([]*oplog.Message, 0, len(orgRoleIndividualScopeToAdd))
if err := wtr.CreateItems(ctx, orgRoleIndividualScopeToAdd, db.NewOplogMsgs(&roleGrantScopeOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add individual project grant scope for org role during set"))
}
msgs = append(msgs, roleGrantScopeOplogMsgs...)
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scp.PublicId},
"scope-type": []string{scp.Type},
"resource-public-id": []string{roleId},
}
if err := wtr.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
allGrantScopes, err := listRoleGrantScopes(ctx, rdr, []string{roleId})
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve current role grant scopes after set"))
}
retGrantScopes = allGrantScopes
return nil
}
if !needFreshReaderWriter {
err = txFunc(reader, writer)
} else {
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
txFunc,
)
}
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return retGrantScopes, totalRowsDeleted, nil
}
// listRoleGrantScopes returns the grant scopes for the roleId
func listRoleGrantScopes(ctx context.Context, reader db.Reader, roleIds []string) ([]*RoleGrantScope, error) {
const op = "iam.(Repository).listRoleGrantScopes"
if len(roleIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids")
}
rows, err := reader.Query(ctx, roleGrantsScopeQuery, []any{roleIds})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query role grant scopes"))
}
if rows.Err() != nil {
return nil, errors.Wrap(ctx, rows.Err(), op, errors.WithMsg("role grant scope rows error"))
}
var result []*RoleGrantScope
for rows.Next() {
if err := reader.ScanRows(ctx, rows, &result); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying role scope for: %s", roleIds)))
}
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying role scope for: %s", roleIds)))
}
return result, nil
}
// individualGlobalRoleGrantScope parses a list individual grant scope IDs to their corresponding struct representation
// projGrantScope (value of iam_role_global.grant_scope) is required because for individually granted project scope
// has a foreign key enforcement that the iam_role_global_individual_project_grant_scope.grant_scope matches iam_role_global.grant_scope)
// which has two possible values: ['individual', 'children']
func individualGlobalRoleGrantScope(ctx context.Context, roleId string, projGrantScope string, grantScopeIds []string) ([]*globalRoleIndividualOrgGrantScope, []*globalRoleIndividualProjectGrantScope, error) {
const op = "iam.(Repository).individualGlobalRoleGrantScope"
org := make([]*globalRoleIndividualOrgGrantScope, 0, len(grantScopeIds))
proj := make([]*globalRoleIndividualProjectGrantScope, 0, len(grantScopeIds))
for _, rgs := range grantScopeIds {
switch {
case strings.HasPrefix(rgs, globals.OrgPrefix):
org = append(org, &globalRoleIndividualOrgGrantScope{
GlobalRoleIndividualOrgGrantScope: &store.GlobalRoleIndividualOrgGrantScope{
RoleId: roleId,
ScopeId: rgs,
GrantScope: globals.GrantScopeIndividual,
},
})
case strings.HasPrefix(rgs, globals.ProjectPrefix):
proj = append(proj, &globalRoleIndividualProjectGrantScope{
GlobalRoleIndividualProjectGrantScope: &store.GlobalRoleIndividualProjectGrantScope{
RoleId: roleId,
ScopeId: rgs,
GrantScope: projGrantScope,
},
})
default:
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "grant scope cannot be added to project roles")
}
}
return org, proj, nil
}
// individualOrgGrantScope converts a list of scope IDs into a slice of *orgRoleIndividualGrantScope
func individualOrgGrantScope(ctx context.Context, roleId string, grantScopeIds []string) ([]*orgRoleIndividualGrantScope, error) {
const op = "iam.(Repository).individualOrgGrantScope"
grantScopes := make([]*orgRoleIndividualGrantScope, 0, len(grantScopeIds))
for _, rgs := range grantScopeIds {
if !strings.HasPrefix(rgs, globals.ProjectPrefix) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "individually granted scopes must be project for org role")
}
grantScopes = append(grantScopes, &orgRoleIndividualGrantScope{
OrgRoleIndividualGrantScope: &store.OrgRoleIndividualGrantScope{
RoleId: roleId,
ScopeId: rgs,
// only individual' is allowed here since so we can hard-code this value
GrantScope: globals.GrantScopeIndividual,
},
})
}
return grantScopes, nil
}
// allocRoleScopeGranter allocates an in-memory instance scope-type specific Role with
func allocRoleScopeGranter(ctx context.Context, roleId string, scopeType string) (roleGrantScopeUpdater, error) {
const op = "iam.(Repository).allocRoleScopeGranter"
var res roleGrantScopeUpdater
switch scopeType {
case scope.Global.String():
g := allocGlobalRole()
g.PublicId = roleId
res = &g
case scope.Org.String():
o := allocOrgRole()
o.PublicId = roleId
res = &o
case scope.Project.String():
p := allocProjectRole()
p.PublicId = roleId
res = &p
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid role scope")
}
return res, nil
}