2024-01-23 14:33:09 -05:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package iam
import (
"context"
"fmt"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
2025-02-06 12:20:39 -08:00
"github.com/hashicorp/boundary/internal/util"
2024-01-23 14:33:09 -05:00
)
// 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"
2024-01-24 15:43:49 -05:00
switch {
case roleId == "" :
2024-01-23 14:33:09 -05:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing role id" )
2024-01-24 15:43:49 -05:00
case len ( grantScopes ) == 0 :
2024-01-23 14:33:09 -05:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing grant scopes" )
2024-01-24 15:43:49 -05:00
case roleVersion == 0 :
2024-01-23 14:33:09 -05:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing version" )
}
role := allocRole ()
role . PublicId = roleId
scope , err := role . GetScope ( ctx , r . reader )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to get role %s scope" , roleId )))
}
role . ScopeId = scope . PublicId
// Find existing grant scopes
roleGrantScopes := [] * RoleGrantScope {}
if err := r . reader . SearchWhere ( ctx , & roleGrantScopes , "role_id = ?" , [] any { roleId }); err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to search for grant scopes" ))
}
found := map [ string ] * RoleGrantScope {}
for _ , rgs := range roleGrantScopes {
found [ rgs . ScopeIdOrSpecial ] = rgs
}
// Check incoming grant scopes to see if they exist so we don't try to add
// again and cause an integrity error
addRoleGrantScopes := make ([] any , 0 , len ( grantScopes ))
for _ , grantScope := range grantScopes {
if _ , ok := found [ grantScope ]; ! ok {
addRoleGrantScopes = append ( addRoleGrantScopes , grantScope )
}
}
2024-08-01 11:18:53 -04:00
newRoleGrantScopes := make ([] * RoleGrantScope , 0 , len ( addRoleGrantScopes ))
2024-01-23 14:33:09 -05:00
for _ , grantScope := range grantScopes {
roleGrantScope , err := NewRoleGrantScope ( ctx , role . GetPublicId (), grantScope )
if err != nil {
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create in memory role grant scope" ))
}
newRoleGrantScopes = append ( newRoleGrantScopes , roleGrantScope )
}
oplogWrapper , err := r . kms . GetWrapper ( ctx , role . GetScopeId (), 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 , & role )
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get ticket" ))
}
// We need to update the role version as that's the aggregate
updatedRole := allocRole ()
updatedRole . PublicId = role . GetPublicId ()
updatedRole . Version = uint32 ( roleVersion + 1 )
var roleOplogMsg oplog . Message
rowsUpdated , err := w . Update ( ctx , & updatedRole , [] string { "Version" }, nil , db . NewOplogMsg ( & roleOplogMsg ), db . WithVersion ( & roleVersion ))
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update role version" ))
}
if rowsUpdated != 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated role and %d rows updated" , rowsUpdated ))
}
msgs = append ( msgs , & roleOplogMsg )
roleGrantScopesOplogMsgs := make ([] * oplog . Message , 0 , len ( newRoleGrantScopes ))
if err := w . CreateItems ( ctx , newRoleGrantScopes , db . NewOplogMsgs ( & roleGrantScopesOplogMsgs )); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to add grants" ))
}
msgs = append ( msgs , roleGrantScopesOplogMsgs ... )
metadata := oplog . Metadata {
"op-type" : [] string { oplog . OpType_OP_TYPE_CREATE . String ()},
"scope-id" : [] string { scope . PublicId },
"scope-type" : [] string { scope . Type },
"resource-public-id" : [] string { role . PublicId },
}
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 )
}
2024-08-01 11:18:53 -04:00
return newRoleGrantScopes , nil
2024-01-23 14:33:09 -05:00
}
// 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.
func ( r * Repository ) DeleteRoleGrantScopes ( ctx context . Context , roleId string , roleVersion uint32 , grantScopes [] string , _ ... Option ) ( int , error ) {
const op = "iam.(Repository).DeleteRoleGrantScopes"
2024-01-24 15:43:49 -05:00
switch {
case roleId == "" :
2024-01-23 14:33:09 -05:00
return db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing role id" )
2024-01-24 15:43:49 -05:00
case len ( grantScopes ) == 0 :
2024-01-23 14:33:09 -05:00
return db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing grant scopes" )
2024-01-24 15:43:49 -05:00
case roleVersion == 0 :
2024-01-23 14:33:09 -05:00
return db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing version" )
}
role := allocRole ()
role . PublicId = roleId
scope , err := role . GetScope ( ctx , r . reader )
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to get role %s scope" , roleId )))
}
role . ScopeId = scope . PublicId
oplogWrapper , err := r . kms . GetWrapper ( ctx , role . GetScopeId (), kms . KeyPurposeOplog )
if err != nil {
return db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
}
var totalRowsDeleted int
_ , 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 , & role )
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get ticket" ))
}
// We need to update the role version as that's the aggregate
updatedRole := allocRole ()
updatedRole . PublicId = role . GetPublicId ()
updatedRole . Version = uint32 ( roleVersion + 1 )
var roleOplogMsg oplog . Message
rowsUpdated , err := w . Update ( ctx , & updatedRole , [] string { "Version" }, nil , db . NewOplogMsg ( & roleOplogMsg ), db . WithVersion ( & roleVersion ))
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update role version" ))
}
if rowsUpdated != 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated role and %d rows updated" , rowsUpdated ))
}
msgs = append ( msgs , & roleOplogMsg )
2024-08-01 11:18:53 -04:00
deleteRoleGrantScopes := make ([] * RoleGrantScope , 0 , len ( grantScopes ))
2024-01-23 14:33:09 -05:00
for _ , grantScope := range grantScopes {
roleGrantScope , err := NewRoleGrantScope ( ctx , roleId , grantScope )
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create in memory role grant scope" ))
}
deleteRoleGrantScopes = append ( deleteRoleGrantScopes , roleGrantScope )
}
if len ( deleteRoleGrantScopes ) == 0 {
return nil
}
roleGrantScopesOplogMsgs := make ([] * oplog . Message , 0 , len ( deleteRoleGrantScopes ))
rowsDeleted , err := w . DeleteItems ( ctx , deleteRoleGrantScopes , db . NewOplogMsgs ( & roleGrantScopesOplogMsgs ))
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to add grants" ))
}
totalRowsDeleted = rowsDeleted
msgs = append ( msgs , roleGrantScopesOplogMsgs ... )
metadata := oplog . Metadata {
"op-type" : [] string { oplog . OpType_OP_TYPE_CREATE . String ()},
"scope-id" : [] string { scope . PublicId },
"scope-type" : [] string { scope . Type },
"resource-public-id" : [] string { role . PublicId },
}
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 totalRowsDeleted , 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"
2024-01-24 15:43:49 -05:00
switch {
case roleId == "" :
2024-01-23 14:33:09 -05:00
return nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing role id" )
2024-01-24 15:43:49 -05:00
case roleVersion == 0 :
2024-01-23 14:33:09 -05:00
return nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing version" )
2024-01-24 15:43:49 -05:00
case grantScopes == nil :
// Explicitly set to zero clears, but treat nil as a mistake
2024-01-23 14:33:09 -05:00
return nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing grants" )
}
reader := r . reader
writer := r . writer
needFreshReaderWriter := true
opts := getOpts ( opt ... )
2025-02-06 12:20:39 -08:00
if ! util . IsNil ( opts . withReader ) && ! util . IsNil ( opts . withWriter ) {
2024-01-23 14:33:09 -05:00
reader = opts . withReader
writer = opts . withWriter
needFreshReaderWriter = false
}
role := allocRole ()
role . PublicId = 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
roleGrantScopes := [] * RoleGrantScope {}
if err := reader . SearchWhere ( ctx , & roleGrantScopes , "role_id = ?" , [] any { roleId }); err != nil {
return nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to search for grant scopes" ))
}
found := map [ string ] * RoleGrantScope {}
for _ , rgs := range roleGrantScopes {
found [ rgs . ScopeIdOrSpecial ] = rgs
}
// Check incoming grant scopes to see if they exist and if so act appropriately
currentRoleGrantScopes := make ([] * RoleGrantScope , 0 , len ( grantScopes ) + len ( found ))
2024-08-01 11:18:53 -04:00
addRoleGrantScopes := make ([] * RoleGrantScope , 0 , len ( grantScopes ))
deleteRoleGrantScopes := make ([] * RoleGrantScope , 0 , len ( grantScopes ))
2024-01-23 14:33:09 -05:00
for _ , grantScope := range grantScopes {
rgs , ok := found [ grantScope ]
if ok {
// If we have an exact match, do nothing, we want to keep
// it, but remove from found
currentRoleGrantScopes = append ( currentRoleGrantScopes , rgs )
delete ( found , grantScope )
continue
}
// Not found, so add
rgs , err := NewRoleGrantScope ( ctx , roleId , grantScope )
if err != nil {
return nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to create in memory role grant" ))
}
addRoleGrantScopes = append ( addRoleGrantScopes , rgs )
currentRoleGrantScopes = append ( currentRoleGrantScopes , rgs )
}
if len ( found ) > 0 {
for _ , rgs := range found {
deleteRoleGrantScopes = append ( deleteRoleGrantScopes , rgs )
}
}
if len ( addRoleGrantScopes ) == 0 && len ( deleteRoleGrantScopes ) == 0 {
return currentRoleGrantScopes , db . NoRowsAffected , nil
}
scope , err := role . GetScope ( ctx , reader )
if err != nil {
return nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to get role %s scope" , roleId )))
}
oplogWrapper , err := r . kms . GetWrapper ( ctx , scope . GetPublicId (), kms . KeyPurposeOplog )
if err != nil {
return nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
}
var totalRowsDeleted int
currentRoleGrantScopes = currentRoleGrantScopes [: 0 ]
txFunc := func ( rdr db . Reader , wtr db . Writer ) error {
msgs := make ([] * oplog . Message , 0 , 2 )
roleTicket , err := wtr . GetTicket ( ctx , & role )
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get ticket" ))
}
updatedRole := allocRole ()
updatedRole . PublicId = roleId
updatedRole . Version = roleVersion + 1
var roleOplogMsg oplog . Message
rowsUpdated , err := wtr . Update ( ctx , & updatedRole , [] string { "Version" }, nil , db . NewOplogMsg ( & roleOplogMsg ), db . WithVersion ( & roleVersion ))
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update role version" ))
}
if rowsUpdated != 1 {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated role and %d rows updated" , rowsUpdated ))
}
msgs = append ( msgs , & roleOplogMsg )
// Anything we didn't take out of found needs to be removed. This needs
// to come before writing in new ones because otherwise we may hit some
// validation issues.
if len ( deleteRoleGrantScopes ) > 0 {
roleGrantScopeOplogMsgs := make ([] * oplog . Message , 0 , len ( deleteRoleGrantScopes ))
rowsDeleted , err := wtr . DeleteItems ( ctx , deleteRoleGrantScopes , db . NewOplogMsgs ( & roleGrantScopeOplogMsgs ))
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to delete role grant scope" ))
}
if rowsDeleted != len ( deleteRoleGrantScopes ) {
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "role grant scope deleted %d did not match request for %d" , rowsDeleted , len ( deleteRoleGrantScopes )))
}
totalRowsDeleted = rowsDeleted
msgs = append ( msgs , roleGrantScopeOplogMsgs ... )
}
// Write the new ones in
if len ( addRoleGrantScopes ) > 0 {
roleGrantScopeOplogMsgs := make ([] * oplog . Message , 0 , len ( addRoleGrantScopes ))
if err := wtr . CreateItems ( ctx , addRoleGrantScopes , db . NewOplogMsgs ( & roleGrantScopeOplogMsgs )); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to add grant scope 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 { scope . PublicId },
"scope-type" : [] string { scope . 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" ))
}
if err := r . list ( ctx , & currentRoleGrantScopes , "role_id = ?" , [] any { roleId }, opt ... ); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current role grant scopes after set" ))
}
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 currentRoleGrantScopes , totalRowsDeleted , nil
}