2021-07-29 18:52:56 -06:00
// Copyright (c) HashiCorp, Inc.
2021-04-08 20:54:32 -04:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2020-06-04 20:05:08 -04:00
package iam
import (
"context"
2023-12-20 16:51:57 -08:00
"database/sql"
2020-06-04 20:05:08 -04:00
"fmt"
"strings"
2023-12-20 16:51:57 -08:00
"time"
2020-06-04 20:05:08 -04:00
2023-03-10 15:06:39 -05:00
"github.com/hashicorp/boundary/globals"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/db"
2023-12-20 16:51:57 -08:00
"github.com/hashicorp/boundary/internal/db/timestamp"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2020-08-19 16:47:18 -04:00
"github.com/hashicorp/boundary/internal/kms"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/types/scope"
2022-08-24 14:11:49 -04:00
"github.com/hashicorp/go-dbw"
2020-06-04 20:05:08 -04:00
)
// CreateUser will create a user in the repository and return the written user
func ( r * Repository ) CreateUser ( ctx context . Context , user * User , opt ... Option ) ( * User , error ) {
2020-12-23 11:00:00 -08:00
const op = "iam.(Repository).CreateUser"
2020-06-04 20:05:08 -04:00
if user == nil {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user" )
2020-06-04 20:05:08 -04:00
}
if user . PublicId != "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "public id is not empty" )
2020-06-04 20:05:08 -04:00
}
2020-09-24 03:31:26 -04:00
u := user . Clone ().( * User )
opts := getOpts ( opt ... )
if opts . withPublicId != "" {
2023-03-10 15:06:39 -05:00
if ! strings . HasPrefix ( opts . withPublicId , globals . UserPrefix + "_" ) {
return nil , errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "passed-in public ID %q has wrong prefix, should be %q" , opts . withPublicId , globals . UserPrefix ))
2020-09-24 03:31:26 -04:00
}
u . PublicId = opts . withPublicId
} else {
2023-06-06 17:09:08 -04:00
id , err := newUserId ( ctx )
2020-09-24 03:31:26 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-24 03:31:26 -04:00
}
u . PublicId = id
2020-06-04 20:05:08 -04:00
}
2020-09-24 03:31:26 -04:00
2021-04-20 20:33:56 -04:00
// There's no need to use r.lookupUser(...) here, because the new user cannot
// be associated with any accounts yet. Why would you typically want to
// call r.lookupUser(...) here vs returning the create resource? Well, the
// created resource doesn't include the user's primary account info (email,
// full name, etc), since you can't run DML against the view which does
// provide these output only attributes. But in this case, there's no way a
// newly created user could have any accounts, so we don't need to use
// r.lookupUser(...). I'm adding this comment so a future version of myself
// doesn't come along and decide to start using r.lookupUser(...) here which
// would just be an unnecessary database lookup. You're welcome future me.
2020-09-24 03:31:26 -04:00
resource , err := r . create ( ctx , u )
2020-06-04 20:05:08 -04:00
if err != nil {
2020-11-05 16:54:03 -05:00
if errors . IsUniqueError ( err ) {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . NotUnique , op , fmt . Sprintf ( "user %s already exists in org %s" , user . Name , user . ScopeId ))
2020-06-04 20:05:08 -04:00
}
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "for %s" , u . PublicId )))
2020-06-04 20:05:08 -04:00
}
2020-12-23 11:00:00 -08:00
return resource .( * User ), nil
2020-06-04 20:05:08 -04:00
}
2020-09-25 13:35:32 -04:00
// UpdateUser will update a user in the repository and return the written user
// plus its associated account ids. fieldMaskPaths provides field_mask.proto
// paths for fields that should be updated. Fields will be set to NULL if the
// field is a zero value and included in fieldMask. Name and Description are the
// only updatable fields, if no updatable fields are included in the
// fieldMaskPaths, then an error is returned.
func ( r * Repository ) UpdateUser ( ctx context . Context , user * User , version uint32 , fieldMaskPaths [] string , opt ... Option ) ( * User , [] string , int , error ) {
2020-12-23 11:00:00 -08:00
const op = "iam.(Repository).UpdateUser"
2020-06-04 20:05:08 -04:00
if user == nil {
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing user" )
2020-06-04 20:05:08 -04:00
}
if user . PublicId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing public id" )
2020-06-04 20:05:08 -04:00
}
for _ , f := range fieldMaskPaths {
switch {
case strings . EqualFold ( "name" , f ):
case strings . EqualFold ( "description" , f ):
default :
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . InvalidFieldMask , op , fmt . Sprintf ( "invalid field mask: %s" , f ))
2020-06-04 20:05:08 -04:00
}
}
var dbMask , nullFields [] string
2022-08-24 14:11:49 -04:00
dbMask , nullFields = dbw . BuildUpdatePaths (
2022-10-25 18:29:38 -05:00
map [ string ] any {
2020-06-04 20:05:08 -04:00
"name" : user . Name ,
"description" : user . Description ,
},
fieldMaskPaths ,
2020-09-19 10:31:49 -04:00
nil ,
2020-06-04 20:05:08 -04:00
)
if len ( dbMask ) == 0 && len ( nullFields ) == 0 {
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . E ( ctx , errors . WithCode ( errors . EmptyFieldMask ), errors . WithOp ( op ))
2020-06-04 20:05:08 -04:00
}
2020-07-08 17:02:01 -04:00
2020-09-25 13:35:32 -04:00
u := user . Clone ().( * User )
metadata , err := r . stdMetadata ( ctx , u )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op )
2020-09-25 13:35:32 -04:00
}
metadata [ "op-type" ] = [] string { oplog . OpType_OP_TYPE_UPDATE . String ()}
dbOpts := [] db . Option {
db . WithVersion ( & version ),
}
opts := getOpts ( opt ... )
if opts . withSkipVetForWrite {
dbOpts = append ( dbOpts , db . WithSkipVetForWrite ( true ))
}
scope , err := u . GetScope ( ctx , r . reader )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get scope" ))
2020-09-25 13:35:32 -04:00
}
oplogWrapper , err := r . kms . GetWrapper ( ctx , scope . GetPublicId (), kms . KeyPurposeOplog )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2020-09-25 13:35:32 -04:00
}
dbOpts = append ( dbOpts , db . WithOplog ( oplogWrapper , metadata ))
var rowsUpdated int
var returnedUser * User
var currentAccountIds [] string
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
returnedUser = u . Clone ().( * User )
rowsUpdated , err = w . Update (
ctx ,
returnedUser ,
dbMask ,
nullFields ,
dbOpts ... ,
)
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-12-23 11:00:00 -08:00
}
if rowsUpdated > 1 {
// return err, which will result in a rollback of the update
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , "more than 1 resource would have been updated" )
2020-09-25 13:35:32 -04:00
}
txRepo := & Repository {
reader : reader ,
writer : w ,
kms : r . kms ,
2021-04-20 20:33:56 -04:00
// intentionally not setting the defaultLimit
}
returnedUser , err = txRepo . lookupUser ( ctx , user . PublicId )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current user after update" ))
2020-09-25 13:35:32 -04:00
}
2020-09-28 15:39:10 -04:00
currentAccountIds , err = txRepo . ListUserAccounts ( ctx , user . PublicId )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current account ids after update" ))
2020-09-25 13:35:32 -04:00
}
return nil
},
)
2020-06-04 20:05:08 -04:00
if err != nil {
2020-11-05 16:54:03 -05:00
if errors . IsUniqueError ( err ) {
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . New ( ctx , errors . NotUnique , op , fmt . Sprintf ( "user %s already exists in org %s" , user . Name , user . ScopeId ))
2020-06-04 20:05:08 -04:00
}
2021-07-29 18:52:56 -06:00
return nil , nil , db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "for %s" , user . PublicId )))
2020-06-04 20:05:08 -04:00
}
2020-12-23 11:00:00 -08:00
return returnedUser , currentAccountIds , rowsUpdated , nil
2020-06-04 20:05:08 -04:00
}
2020-09-25 13:35:32 -04:00
// LookupUser will look up a user and its associated account ids in the
// repository. If the user is not found, it will return nil, nil, nil.
2020-12-23 11:00:00 -08:00
func ( r * Repository ) LookupUser ( ctx context . Context , userId string , _ ... Option ) ( * User , [] string , error ) {
const op = "iam.(Repository).LookupUser"
2020-09-25 13:35:32 -04:00
if userId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing public id" )
2020-06-04 20:05:08 -04:00
}
2021-04-20 20:33:56 -04:00
user , err := r . lookupUser ( ctx , userId )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-06-04 20:05:08 -04:00
}
2020-09-28 15:39:10 -04:00
currentAccountIds , err := r . ListUserAccounts ( ctx , userId )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current account ids" ))
2020-09-25 13:35:32 -04:00
}
2021-04-20 20:33:56 -04:00
return user , currentAccountIds , nil
2020-06-04 20:05:08 -04:00
}
// DeleteUser will delete a user from the repository
2020-12-23 11:00:00 -08:00
func ( r * Repository ) DeleteUser ( ctx context . Context , withPublicId string , _ ... Option ) ( int , error ) {
const op = "iam.(Repository).DeleteUser"
2020-06-04 20:05:08 -04:00
if withPublicId == "" {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . New ( ctx , errors . InvalidParameter , op , "missing public id" )
2020-06-04 20:05:08 -04:00
}
2021-04-20 20:33:56 -04:00
user := AllocUser ()
2020-06-04 20:05:08 -04:00
user . PublicId = withPublicId
if err := r . reader . LookupByPublicId ( ctx , & user ); err != nil {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "for %s" , withPublicId )))
2020-06-04 20:05:08 -04:00
}
rowsDeleted , err := r . delete ( ctx , & user )
if err != nil {
2021-07-29 18:52:56 -06:00
return db . NoRowsAffected , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "for %s" , withPublicId )))
2020-06-04 20:05:08 -04:00
}
return rowsDeleted , nil
}
2020-06-16 18:59:30 -04:00
2020-08-10 15:17:15 -04:00
// LookupUserWithLogin will attempt to lookup the user with a matching
2020-06-29 08:18:57 -04:00
// account id and return the user if found. If a user is not found and the
2021-04-08 20:54:32 -04:00
// account's scope is not the PrimaryAuthMethod, then an error is returned.
// If the account's scope is the PrimaryAuthMethod, then a new iam User will be
// created (autovivified) in the scope of the account, and associated with the
2020-06-29 08:18:57 -04:00
// account. If a new user is auto vivified, then the WithName and
// WithDescription options are supported as well.
2020-08-10 15:17:15 -04:00
func ( r * Repository ) LookupUserWithLogin ( ctx context . Context , accountId string , opt ... Option ) ( * User , error ) {
2020-12-23 11:00:00 -08:00
const op = "iam.(Repository).LookupUserWithLogin"
2020-08-10 15:17:15 -04:00
if accountId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing account id" )
2020-06-29 08:18:57 -04:00
}
2020-08-10 15:17:15 -04:00
u , err := r . getUserWithAccount ( ctx , accountId )
2020-06-29 08:18:57 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
if u != nil {
return u , nil
}
2020-08-10 15:17:15 -04:00
acct := allocAccount ()
acct . PublicId = accountId
2020-06-29 08:18:57 -04:00
err = r . reader . LookupByPublicId ( context . Background (), & acct )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to lookup account %s" , accountId )))
2020-06-29 08:18:57 -04:00
}
2021-04-08 20:54:32 -04:00
allowed , err := r . allowUserAutoVivify ( ctx , & acct )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2021-04-08 20:54:32 -04:00
}
if ! allowed {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . RecordNotFound , op , fmt . Sprintf ( "user not found for account %s and auth method is not primary for the scope so refusing to auto-create user" , accountId ))
2021-04-08 20:54:32 -04:00
}
2020-06-29 08:18:57 -04:00
metadata := oplog . Metadata {
2020-08-10 15:17:15 -04:00
"resource-public-id" : [] string { accountId },
2020-06-29 08:18:57 -04:00
"scope-id" : [] string { acct . ScopeId },
2020-07-11 12:00:49 -04:00
"scope-type" : [] string { scope . Org . String ()},
2020-06-29 08:18:57 -04:00
"resource-type" : [] string { "auth-account" },
}
2020-08-19 16:47:18 -04:00
oplogWrapper , err := r . kms . GetWrapper ( ctx , acct . GetScopeId (), kms . KeyPurposeOplog )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2020-08-19 16:47:18 -04:00
}
2020-08-10 15:17:15 -04:00
// We will create a new user and associate the user with the account
2020-06-29 08:18:57 -04:00
// within one retryable transaction using writer.DoTx
var obtainedUser * User
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
2021-04-20 20:33:56 -04:00
func ( reader db . Reader , w db . Writer ) error {
2020-06-29 08:18:57 -04:00
msgs := make ([] * oplog . Message , 0 , 2 )
2022-01-05 14:38:38 -05:00
ticket , err := w . GetTicket ( ctx , & acct )
2020-06-29 08:18:57 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
2023-06-06 17:09:08 -04:00
obtainedUser , err = NewUser ( ctx , acct . ScopeId , opt ... )
2020-06-29 08:18:57 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
2023-06-06 17:09:08 -04:00
id , err := newUserId ( ctx )
2020-06-29 08:18:57 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
var createMsg oplog . Message
obtainedUser . PublicId = id
err = w . Create ( ctx , obtainedUser , db . NewOplogMsg ( & createMsg ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
msgs = append ( msgs , & createMsg )
var updateMsg oplog . Message
2020-08-10 15:17:15 -04:00
updateAcct := acct . Clone ().( * authAccount )
2020-06-29 08:18:57 -04:00
updateAcct . IamUserId = id
updatedRows , err := w . Update ( ctx , updateAcct , [] string { "IamUserId" }, nil , db . NewOplogMsg ( & updateMsg ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
if updatedRows != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "account update affected %d rows" , updatedRows ))
2020-06-29 08:18:57 -04:00
}
msgs = append ( msgs , & updateMsg )
2020-08-19 16:47:18 -04:00
if err := w . WriteOplogEntryWith ( ctx , oplogWrapper , ticket , metadata , msgs ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
2021-04-20 20:33:56 -04:00
txRepo := & Repository {
reader : reader ,
writer : w ,
kms : r . kms ,
// intentionally not setting the defaultLimit
}
obtainedUser , err = txRepo . lookupUser ( ctx , obtainedUser . PublicId )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve user" ))
2021-04-20 20:33:56 -04:00
}
2020-06-29 08:18:57 -04:00
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
return obtainedUser , nil
}
2021-04-08 20:54:32 -04:00
// allowUserAutoVivify determines if a user can be autovivified based on the account's scope
func ( r * Repository ) allowUserAutoVivify ( ctx context . Context , acct * authAccount ) ( bool , error ) {
const op = "iam.(Repository).allowUserAutoVivify"
if acct == nil {
2021-07-29 18:52:56 -06:00
return false , errors . New ( ctx , errors . InvalidParameter , op , "missing account" )
2021-04-08 20:54:32 -04:00
}
acctScope := AllocScope ()
acctScope . PublicId = acct . ScopeId
err := r . reader . LookupByPublicId ( context . Background (), & acctScope )
if err != nil {
2021-07-29 18:52:56 -06:00
return false , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to lookup account's scope %s" , acct . ScopeId )))
2021-04-08 20:54:32 -04:00
}
return acct . AuthMethodId == acctScope . PrimaryAuthMethodId , nil
}
2020-12-23 11:00:00 -08:00
func ( r * Repository ) getUserWithAccount ( ctx context . Context , withAccountId string , _ ... Option ) ( * User , error ) {
const op = "iam.(Repository).getUserWithAccount"
2020-08-10 15:17:15 -04:00
if withAccountId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing account id" )
2020-06-29 08:18:57 -04:00
}
2022-10-25 18:29:38 -05:00
rows , err := r . reader . Query ( ctx , whereUserAccount , [] any { withAccountId })
2020-06-29 08:18:57 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to query account %s" , withAccountId )))
2020-06-29 08:18:57 -04:00
}
defer rows . Close ()
2021-04-20 20:33:56 -04:00
u := AllocUser ()
2020-06-29 08:18:57 -04:00
if rows . Next () {
2022-01-05 14:38:38 -05:00
err = r . reader . ScanRows ( ctx , rows , & u )
2020-06-29 08:18:57 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to scan rows for account %s" , withAccountId )))
2020-06-29 08:18:57 -04:00
}
} else {
if err := rows . Err (); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get next account" ))
2020-06-29 08:18:57 -04:00
}
return nil , nil
}
return & u , nil
}
2020-09-28 15:39:10 -04:00
// ListUserAccounts returns the account ids for the userId and supports the
2020-09-25 13:35:32 -04:00
// WithLimit option. Returns nil, nil when no associated accounts are found.
2020-09-28 15:39:10 -04:00
func ( r * Repository ) ListUserAccounts ( ctx context . Context , userId string , opt ... Option ) ([] string , error ) {
2020-12-23 11:00:00 -08:00
const op = "iam.(Repository).ListUserAccounts"
2020-09-25 13:35:32 -04:00
if userId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user id" )
2020-06-29 08:18:57 -04:00
}
2020-09-25 13:35:32 -04:00
var accounts [] * authAccount
2022-10-25 18:29:38 -05:00
if err := r . list ( ctx , & accounts , "iam_user_id = ?" , [] any { userId }, opt ... ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-06-29 08:18:57 -04:00
}
2020-09-25 13:35:32 -04:00
if len ( accounts ) == 0 {
return nil , nil
}
ids := make ([] string , 0 , len ( accounts ))
for _ , aa := range accounts {
ids = append ( ids , aa . PublicId )
2020-06-29 08:18:57 -04:00
}
2020-09-25 13:35:32 -04:00
return ids , nil
}
2020-08-10 15:17:15 -04:00
2020-09-28 15:39:10 -04:00
// AddUserAccounts will associate a user with existing accounts and
2020-09-25 13:35:32 -04:00
// return a list of all associated account ids for the user. The accounts must
// not already be associated with different users. No options are currently
// supported.
2020-12-23 11:00:00 -08:00
func ( r * Repository ) AddUserAccounts ( ctx context . Context , userId string , userVersion uint32 , accountIds [] string , _ ... Option ) ([] string , error ) {
const op = "iam.(Repository).AddUserAccounts"
2020-09-25 13:35:32 -04:00
if userId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user id" )
2020-06-29 08:18:57 -04:00
}
2020-09-25 13:35:32 -04:00
if userVersion == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user version" )
2020-06-29 08:18:57 -04:00
}
2020-09-25 13:35:32 -04:00
if len ( accountIds ) == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing account ids" )
2020-06-29 08:18:57 -04:00
}
2020-08-10 15:17:15 -04:00
2021-04-20 20:33:56 -04:00
user , err := r . lookupUser ( ctx , userId )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to lookup user %s" , userId )))
2020-09-25 13:35:32 -04:00
}
oplogWrapper , err := r . kms . GetWrapper ( ctx , user . ScopeId , kms . KeyPurposeOplog )
2020-08-19 16:47:18 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2020-08-19 16:47:18 -04:00
}
2020-09-25 13:35:32 -04:00
var currentAccountIds [] string
2020-08-10 15:17:15 -04:00
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
2020-09-25 13:35:32 -04:00
func ( reader db . Reader , w db . Writer ) error {
2022-01-05 14:38:38 -05:00
userTicket , err := w . GetTicket ( ctx , user )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get ticket" ))
2020-09-25 13:35:32 -04:00
}
2021-04-20 20:33:56 -04:00
updatedUser := AllocUser ()
2020-09-25 13:35:32 -04:00
updatedUser . PublicId = userId
updatedUser . Version = userVersion + 1
var userOplogMsg oplog . Message
rowsUpdated , err := w . Update ( ctx , & updatedUser , [] string { "Version" }, nil , db . NewOplogMsg ( & userOplogMsg ), db . WithVersion ( & userVersion ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get user version" ))
2020-09-25 13:35:32 -04:00
}
if rowsUpdated != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated user and %d rows updated" , rowsUpdated ))
2020-09-25 13:35:32 -04:00
}
if err := associateUserWithAccounts ( ctx , r . kms , reader , w , user . PublicId , accountIds ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-25 13:35:32 -04:00
}
2020-08-10 15:17:15 -04:00
metadata := oplog . Metadata {
2020-09-25 13:35:32 -04:00
"op-type" : [] string { oplog . OpType_OP_TYPE_UPDATE . String ()},
"scope-id" : [] string { user . ScopeId },
2020-08-10 15:17:15 -04:00
"scope-type" : [] string { scope . Org . String ()},
2020-09-25 13:35:32 -04:00
"resource-public-id" : [] string { user . PublicId },
2020-08-10 15:17:15 -04:00
}
2020-09-25 13:35:32 -04:00
if err := w . WriteOplogEntryWith ( ctx , oplogWrapper , userTicket , metadata , [] * oplog . Message { & userOplogMsg }); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to write oplog" ))
2020-08-10 15:17:15 -04:00
}
2020-09-25 13:35:32 -04:00
// we need a new repo, that's using the same reader/writer as this TxHandler
txRepo := & Repository {
reader : reader ,
writer : w ,
kms : r . kms ,
// intentionally not setting the defaultLimit, so we'll get all
// the account ids without a limit
}
2020-09-28 15:39:10 -04:00
currentAccountIds , err = txRepo . ListUserAccounts ( ctx , user . PublicId )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current account ids after adds" ))
2020-08-10 15:17:15 -04:00
}
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-08-10 15:17:15 -04:00
}
2020-09-25 13:35:32 -04:00
return currentAccountIds , nil
2020-06-29 08:18:57 -04:00
}
2020-09-28 15:39:10 -04:00
// DeleteUserAccounts will disassociate a user from existing accounts and
2020-09-25 13:35:32 -04:00
// return a list of all associated account ids for the user. The accounts must
// not be associated with different users. No options are currently
// supported.
2020-12-23 11:00:00 -08:00
func ( r * Repository ) DeleteUserAccounts ( ctx context . Context , userId string , userVersion uint32 , accountIds [] string , _ ... Option ) ([] string , error ) {
const op = "iam.(Repository).DeleteUserAccounts"
2020-09-25 13:35:32 -04:00
if userId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing public id" )
2020-08-10 15:17:15 -04:00
}
2020-09-25 13:35:32 -04:00
if userVersion == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user version" )
2020-09-25 13:35:32 -04:00
}
if len ( accountIds ) == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing account ids" )
2020-06-29 08:18:57 -04:00
}
2020-08-10 15:17:15 -04:00
2021-04-20 20:33:56 -04:00
user , err := r . lookupUser ( ctx , userId )
2020-08-10 15:17:15 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to lookup user %s" , userId )))
2020-09-25 13:35:32 -04:00
}
oplogWrapper , err := r . kms . GetWrapper ( ctx , user . ScopeId , kms . KeyPurposeOplog )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2020-06-29 08:18:57 -04:00
}
2020-08-10 15:17:15 -04:00
2020-09-25 13:35:32 -04:00
var currentAccountIds [] string
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
func ( reader db . Reader , w db . Writer ) error {
2022-01-05 14:38:38 -05:00
userTicket , err := w . GetTicket ( ctx , user )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get ticket" ))
2020-09-25 13:35:32 -04:00
}
2021-04-20 20:33:56 -04:00
updatedUser := AllocUser ()
2020-09-25 13:35:32 -04:00
updatedUser . PublicId = userId
updatedUser . Version = userVersion + 1
var userOplogMsg oplog . Message
rowsUpdated , err := w . Update ( ctx , & updatedUser , [] string { "Version" }, nil , db . NewOplogMsg ( & userOplogMsg ), db . WithVersion ( & userVersion ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update user version" ))
2020-09-25 13:35:32 -04:00
}
if rowsUpdated != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated user and %d rows updated" , rowsUpdated ))
2020-09-25 13:35:32 -04:00
}
if err := dissociateUserFromAccounts ( ctx , r . kms , reader , w , user . PublicId , accountIds ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op )
2020-09-25 13:35:32 -04:00
}
metadata := oplog . Metadata {
"op-type" : [] string { oplog . OpType_OP_TYPE_UPDATE . String ()},
"scope-id" : [] string { user . ScopeId },
"scope-type" : [] string { scope . Org . String ()},
"resource-public-id" : [] string { user . PublicId },
}
if err := w . WriteOplogEntryWith ( ctx , oplogWrapper , userTicket , metadata , [] * oplog . Message { & userOplogMsg }); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to write oplog" ))
2020-09-25 13:35:32 -04:00
}
// we need a new repo, that's using the same reader/writer as this TxHandler
txRepo := & Repository {
reader : reader ,
writer : w ,
kms : r . kms ,
// intentionally not setting the defaultLimit, so we'll get all
// the account ids without a limit
}
2020-09-28 15:39:10 -04:00
currentAccountIds , err = txRepo . ListUserAccounts ( ctx , user . PublicId )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current account ids after adds" ))
2020-09-25 13:35:32 -04:00
}
return nil
},
)
2020-06-29 08:18:57 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-08-10 15:17:15 -04:00
}
2020-09-25 13:35:32 -04:00
return currentAccountIds , nil
}
2020-09-28 15:39:10 -04:00
// SetUserAccounts will associate a user with existing accounts and
2020-09-25 13:35:32 -04:00
// return a list of all associated account ids for the user. The accounts must
// not already be associated with different users. No options are currently
// supported.
2020-12-23 11:00:00 -08:00
func ( r * Repository ) SetUserAccounts ( ctx context . Context , userId string , userVersion uint32 , accountIds [] string , _ ... Option ) ([] string , error ) {
const op = "iam.(Repository).SetUserAccounts"
2020-09-25 13:35:32 -04:00
if userId == "" {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing public id" )
2020-06-29 08:18:57 -04:00
}
2020-09-25 13:35:32 -04:00
if userVersion == 0 {
2021-07-29 18:52:56 -06:00
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing version" )
2020-06-29 08:18:57 -04:00
}
2020-08-10 15:17:15 -04:00
2021-04-20 20:33:56 -04:00
user , err := r . lookupUser ( ctx , userId )
2020-08-19 16:47:18 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to lookup user %s" , userId )))
2020-08-19 16:47:18 -04:00
}
2020-09-25 13:35:32 -04:00
oplogWrapper , err := r . kms . GetWrapper ( ctx , user . ScopeId , kms . KeyPurposeOplog )
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2020-09-25 13:35:32 -04:00
}
var currentAccountIds [] string
2020-08-10 15:17:15 -04:00
_ , err = r . writer . DoTx (
ctx ,
db . StdRetryCnt ,
db . ExpBackoff {},
2020-09-25 13:35:32 -04:00
func ( reader db . Reader , w db . Writer ) error {
associateIds , disassociateIds , err := associationChanges ( ctx , reader , userId , accountIds )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to determine changes" ))
2020-09-25 13:35:32 -04:00
}
// just in case we've got nothing to do...
if len ( associateIds ) == 0 && len ( disassociateIds ) == 0 {
// we need a new repo, that's using the same reader/writer as this TxHandler
txRepo := & Repository {
reader : reader ,
writer : w ,
kms : r . kms ,
// intentionally not setting the defaultLimit, so we'll get all
// the account ids without a limit
}
2020-09-28 15:39:10 -04:00
currentAccountIds , err = txRepo . ListUserAccounts ( ctx , userId )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current account ids after set" ))
2020-09-25 13:35:32 -04:00
}
return nil
}
2022-01-05 14:38:38 -05:00
userTicket , err := w . GetTicket ( ctx , user )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get ticket" ))
2020-09-25 13:35:32 -04:00
}
2021-04-20 20:33:56 -04:00
updatedUser := AllocUser ()
2020-09-25 13:35:32 -04:00
updatedUser . PublicId = userId
updatedUser . Version = userVersion + 1
var userOplogMsg oplog . Message
rowsUpdated , err := w . Update ( ctx , & updatedUser , [] string { "Version" }, nil , db . NewOplogMsg ( & userOplogMsg ), db . WithVersion ( & userVersion ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to update user version" ))
2020-09-25 13:35:32 -04:00
}
if rowsUpdated != 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "updated user and %d rows updated" , rowsUpdated ))
2020-09-25 13:35:32 -04:00
}
if len ( associateIds ) > 0 {
if err := associateUserWithAccounts ( ctx , r . kms , reader , w , userId , associateIds ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to associate ids" ))
2020-09-25 13:35:32 -04:00
}
}
if len ( disassociateIds ) > 0 {
if err := dissociateUserFromAccounts ( ctx , r . kms , reader , w , userId , disassociateIds ); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to disassociate ids" ))
2020-09-25 13:35:32 -04:00
}
}
2020-08-10 15:17:15 -04:00
metadata := oplog . Metadata {
2020-09-25 13:35:32 -04:00
"op-type" : [] string { oplog . OpType_OP_TYPE_UPDATE . String ()},
"scope-id" : [] string { user . ScopeId },
2020-08-10 15:17:15 -04:00
"scope-type" : [] string { scope . Org . String ()},
2020-09-25 13:35:32 -04:00
"resource-public-id" : [] string { userId },
2020-08-10 15:17:15 -04:00
}
2020-09-25 13:35:32 -04:00
if err := w . WriteOplogEntryWith ( ctx , oplogWrapper , userTicket , metadata , [] * oplog . Message { & userOplogMsg }); err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to write oplog" ))
2020-09-25 13:35:32 -04:00
}
// we need a new repo, that's using the same reader/writer as this TxHandler
txRepo := & Repository {
reader : reader ,
writer : w ,
kms : r . kms ,
// intentionally not setting the defaultLimit, so we'll get all
// the account ids without a limit
}
2020-09-28 15:39:10 -04:00
currentAccountIds , err = txRepo . ListUserAccounts ( ctx , user . PublicId )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to retrieve current account ids after set" ))
2020-08-10 15:17:15 -04:00
}
return nil
},
)
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2020-09-25 13:35:32 -04:00
}
return currentAccountIds , nil
}
// associateUserWithAccounts will associate the accounts (accountIds) with
// the user (userId) within the writer's database
2020-12-23 11:00:00 -08:00
func associateUserWithAccounts ( ctx context . Context , repoKms * kms . Kms , reader db . Reader , writer db . Writer , userId string , accountIds [] string , _ ... Option ) error {
const op = "iam.associateUserWithAccounts"
2020-09-25 13:35:32 -04:00
if repoKms == nil {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "nil kms" )
2020-09-25 13:35:32 -04:00
}
if reader == nil {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "nil reader" )
2020-09-25 13:35:32 -04:00
}
if writer == nil {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "nil writer" )
2020-09-25 13:35:32 -04:00
}
if userId == "" {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing user id" )
2020-09-25 13:35:32 -04:00
}
if len ( accountIds ) == 0 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing account ids" )
2020-09-25 13:35:32 -04:00
}
authAccounts := make ([] * authAccount , 0 , len ( accountIds ))
for _ , accountId := range accountIds {
acct := allocAccount ()
acct . PublicId = accountId
2021-04-20 20:33:56 -04:00
err := reader . LookupByPublicId ( ctx , & acct )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to lookup account %s" , accountId )))
2020-09-25 13:35:32 -04:00
}
if acct . IamUserId != "" && acct . IamUserId != userId {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . AccountAlreadyAssociated , op , fmt . Sprintf ( "%s account is already associated with another user" , accountId ))
2020-09-25 13:35:32 -04:00
}
authAccounts = append ( authAccounts , & acct )
}
for _ , aa := range authAccounts {
// wrapper could be different for each authAccount depending on it's scope
oplogWrapper , err := repoKms . GetWrapper ( ctx , aa . GetScopeId (), kms . KeyPurposeOplog )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2020-09-25 13:35:32 -04:00
}
metadata := oplog . Metadata {
"resource-public-id" : [] string { aa . PublicId },
"scope-id" : [] string { aa . ScopeId },
"resource-type" : [] string { "auth-account" },
"op-type" : [] string { oplog . OpType_OP_TYPE_UPDATE . String ()},
}
var updatedRows int
updatedAcct := aa . Clone ().( * authAccount )
updatedAcct . IamUserId = userId
updatedRows , err = writer . Update ( ctx , updatedAcct , [] string { "IamUserId" }, nil , db . WithOplog ( oplogWrapper , metadata ), db . WithWhere ( "iam_user_id is NULL or iam_user_id = ?" , userId ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed to associate %s account" , aa . PublicId )))
2020-09-25 13:35:32 -04:00
}
if updatedRows == 0 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . AccountAlreadyAssociated , op , fmt . Sprintf ( "failed to associate %s account: it is already associated with another user" , aa . PublicId ))
2020-09-25 13:35:32 -04:00
}
if updatedRows > 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "failed to associate %s account: would have updated too many accounts %d" , aa . PublicId , updatedRows ))
2020-09-25 13:35:32 -04:00
}
}
return nil
}
// dissociateUserFromAccounts will dissociate a user with its existing accounts
// (accountIds). An error is returned if account is associated with a different
// user. No options are currently supported.
2020-12-23 11:00:00 -08:00
func dissociateUserFromAccounts ( ctx context . Context , repoKms * kms . Kms , reader db . Reader , writer db . Writer , userId string , accountIds [] string , _ ... Option ) error {
const op = "iam.dissociateUserFromAccounts"
2020-09-25 13:35:32 -04:00
if repoKms == nil {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "nil kms" )
2020-09-25 13:35:32 -04:00
}
if reader == nil {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "nil reader" )
2020-09-25 13:35:32 -04:00
}
if writer == nil {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "nil writer" )
2020-09-25 13:35:32 -04:00
}
if userId == "" {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing public id" )
2020-09-25 13:35:32 -04:00
}
if len ( accountIds ) == 0 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing account ids" )
2020-09-25 13:35:32 -04:00
}
authAccounts := make ([] * authAccount , 0 , len ( accountIds ))
for _ , accountId := range accountIds {
acct := allocAccount ()
acct . PublicId = accountId
err := reader . LookupByPublicId ( context . Background (), & acct )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "unable to lookup account %s" , accountId )))
2020-09-25 13:35:32 -04:00
}
2020-09-25 20:44:55 -07:00
if acct . IamUserId != userId {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . AccountAlreadyAssociated , op , fmt . Sprintf ( "%s account is not associated with user %s" , accountId , userId ))
2020-09-25 13:35:32 -04:00
}
authAccounts = append ( authAccounts , & acct )
}
for _ , aa := range authAccounts {
// wrapper could be different for each authAccount depending on it's scope
oplogWrapper , err := repoKms . GetWrapper ( ctx , aa . GetScopeId (), kms . KeyPurposeOplog )
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to get oplog wrapper" ))
2020-09-25 13:35:32 -04:00
}
metadata := oplog . Metadata {
"resource-public-id" : [] string { aa . PublicId },
"scope-id" : [] string { aa . ScopeId },
"resource-type" : [] string { "auth-account" },
"op-type" : [] string { oplog . OpType_OP_TYPE_UPDATE . String ()},
}
var updatedRows int
updatedAcct := aa . Clone ().( * authAccount )
updatedAcct . IamUserId = userId
// update IamUserId to null
updatedRows , err = writer . Update ( ctx , updatedAcct , nil , [] string { "IamUserId" }, db . WithOplog ( oplogWrapper , metadata ), db . WithWhere ( "iam_user_id is NULL or iam_user_id = ?" , userId ))
if err != nil {
2021-07-29 18:52:56 -06:00
return errors . Wrap ( ctx , err , op , errors . WithMsg ( fmt . Sprintf ( "failed to disassociate %s account" , aa . PublicId )))
2020-09-25 13:35:32 -04:00
}
if updatedRows == 0 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . AccountAlreadyAssociated , op , fmt . Sprintf ( "failed to disassociate %s account: it is already associated with another user" , aa . PublicId ))
2020-09-25 13:35:32 -04:00
}
if updatedRows > 1 {
2021-07-29 18:52:56 -06:00
return errors . New ( ctx , errors . MultipleRecords , op , fmt . Sprintf ( "failed to disassociate %s account: would have updated too many accounts %d" , aa . PublicId , updatedRows ))
2020-09-25 13:35:32 -04:00
}
}
return nil
}
// associationChanges returns two slices: accounts to associate and disassociate
func associationChanges ( ctx context . Context , reader db . Reader , userId string , accountIds [] string ) ([] string , [] string , error ) {
2020-12-23 11:00:00 -08:00
const op = "iam.associationChanges"
2021-09-30 08:47:34 -04:00
2020-09-25 13:35:32 -04:00
var inClauseSpots [] string
2021-09-30 08:47:34 -04:00
// starts at 2 because there is already a ? in the query
2020-09-25 13:35:32 -04:00
for i := 2 ; i < len ( accountIds ) + 2 ; i ++ {
2021-09-30 08:47:34 -04:00
// inClauseSpots = append(inClauseSpots, fmt.Sprintf("$%d", i))
inClauseSpots = append ( inClauseSpots , "?" )
2020-09-25 13:35:32 -04:00
}
inClause := strings . Join ( inClauseSpots , "," )
if inClause == "" {
inClause = "''"
}
query := fmt . Sprintf ( accountChangesQuery , inClause )
2022-10-25 18:29:38 -05:00
var params [] any
2020-09-25 13:35:32 -04:00
for _ , v := range accountIds {
params = append ( params , v )
}
2021-09-30 08:47:34 -04:00
params = append ( params , userId )
2020-09-30 18:11:59 -04:00
rows , err := reader . Query ( ctx , query , params )
2020-09-25 13:35:32 -04:00
if err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-09-25 13:35:32 -04:00
}
defer rows . Close ()
type change struct {
Action string
AccountId string
}
var changes [] * change
for rows . Next () {
var chg change
2022-01-05 14:38:38 -05:00
if err := reader . ScanRows ( ctx , rows , & chg ); err != nil {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . Wrap ( ctx , err , op )
2020-09-25 13:35:32 -04:00
}
changes = append ( changes , & chg )
}
2024-01-22 14:03:40 -05:00
if err := rows . Err (); err != nil {
return nil , nil , errors . Wrap ( ctx , err , op )
}
2020-09-25 13:35:32 -04:00
var associateIds , disassociateIds [] string
for _ , c := range changes {
if c . AccountId == "" {
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , "missing account id in change result" )
2020-09-25 13:35:32 -04:00
}
switch c . Action {
case "associate" :
associateIds = append ( associateIds , c . AccountId )
case "disassociate" :
disassociateIds = append ( disassociateIds , c . AccountId )
default :
2021-07-29 18:52:56 -06:00
return nil , nil , errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unknown action %s for %s" , c . Action , c . AccountId ))
2020-09-25 13:35:32 -04:00
}
2020-06-29 08:18:57 -04:00
}
2020-09-25 13:35:32 -04:00
return associateIds , disassociateIds , nil
2020-06-29 08:18:57 -04:00
}
2021-04-20 20:33:56 -04:00
// lookupUser will lookup a single user and returns nil, nil when no user is found.
2023-12-20 16:51:57 -08:00
// no options are currently supported
2021-04-20 20:33:56 -04:00
func ( r * Repository ) lookupUser ( ctx context . Context , userId string , opt ... Option ) ( * User , error ) {
const op = "iam.(Repository).lookupUser"
2023-12-20 16:51:57 -08:00
if userId == "" {
return nil , errors . New ( ctx , errors . InvalidParameter , op , "missing user id" )
}
ret := allocUserAccountInfo ()
ret . PublicId = userId
if err := r . reader . LookupById ( ctx , ret ); err != nil {
if errors . IsNotFoundError ( err ) {
return nil , nil
}
2021-07-29 18:52:56 -06:00
return nil , errors . Wrap ( ctx , err , op )
2021-04-20 20:33:56 -04:00
}
2023-12-20 16:51:57 -08:00
return ret . shallowConversion (), nil
}
2023-12-21 12:54:27 -08:00
// ListUsers lists users in the given scopes and supports WithLimit option.
2023-12-20 16:51:57 -08:00
func ( r * Repository ) ListUsers ( ctx context . Context , withScopeIds [] string , opt ... Option ) ([] * User , time . Time , error ) {
const op = "iam.(Repository).listUsers"
if len ( withScopeIds ) == 0 {
return nil , time . Time {}, errors . New ( ctx , errors . InvalidParameter , op , "missing scope id" )
}
opts := getOpts ( opt ... )
limit := r . defaultLimit
2021-04-20 20:33:56 -04:00
switch {
2023-12-20 16:51:57 -08:00
case opts . withLimit > 0 :
// non-zero signals an override of the default limit for the repo.
limit = opts . withLimit
case opts . withLimit < 0 :
return nil , time . Time {}, errors . New ( ctx , errors . InvalidParameter , op , "limit must be non-negative" )
2021-04-20 20:33:56 -04:00
}
2023-12-20 16:51:57 -08:00
var args [] any
whereClause := "scope_id in @scope_ids"
args = append ( args , sql . Named ( "scope_ids" , withScopeIds ))
if opts . withStartPageAfterItem != nil {
whereClause = fmt . Sprintf ( "(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s" , whereClause )
args = append ( args ,
sql . Named ( "last_item_create_time" , opts . withStartPageAfterItem . GetCreateTime ()),
sql . Named ( "last_item_id" , opts . withStartPageAfterItem . GetPublicId ()),
)
}
dbOpts := [] db . Option { db . WithLimit ( limit ), db . WithOrder ( "create_time desc, public_id desc" )}
return r . queryUsers ( ctx , whereClause , args , dbOpts ... )
2021-04-20 20:33:56 -04:00
}
2023-12-20 16:51:57 -08:00
// listUsersRefresh lists users in the given scopes and supports the
// WithLimit and WithStartPageAfterItem options.
func ( r * Repository ) listUsersRefresh ( ctx context . Context , updatedAfter time . Time , withScopeIds [] string , opt ... Option ) ([] * User , time . Time , error ) {
const op = "iam.(Repository).listUsersRefresh"
switch {
case updatedAfter . IsZero ():
return nil , time . Time {}, errors . New ( ctx , errors . InvalidParameter , op , "missing updated after time" )
case len ( withScopeIds ) == 0 :
return nil , time . Time {}, errors . New ( ctx , errors . InvalidParameter , op , "missing scope id" )
2021-04-20 20:33:56 -04:00
}
opts := getOpts ( opt ... )
2023-12-20 16:51:57 -08:00
2021-04-20 20:33:56 -04:00
limit := r . defaultLimit
2023-12-20 16:51:57 -08:00
switch {
case opts . withLimit > 0 :
2021-04-20 20:33:56 -04:00
// non-zero signals an override of the default limit for the repo.
limit = opts . withLimit
2023-12-20 16:51:57 -08:00
case opts . withLimit < 0 :
return nil , time . Time {}, errors . New ( ctx , errors . InvalidParameter , op , "limit must be non-negative" )
2021-04-20 20:33:56 -04:00
}
2022-10-25 18:29:38 -05:00
var args [] any
2023-12-20 16:51:57 -08:00
whereClause := "update_time > @updated_after_time and scope_id in @scope_ids"
args = append ( args ,
sql . Named ( "updated_after_time" , timestamp . New ( updatedAfter )),
sql . Named ( "scope_ids" , withScopeIds ),
)
if opts . withStartPageAfterItem != nil {
whereClause = fmt . Sprintf ( "(update_time, public_id) < (@last_item_update_time, @last_item_id) and %s" , whereClause )
args = append ( args ,
sql . Named ( "last_item_update_time" , opts . withStartPageAfterItem . GetUpdateTime ()),
sql . Named ( "last_item_id" , opts . withStartPageAfterItem . GetPublicId ()),
)
2021-04-20 20:33:56 -04:00
}
2023-12-20 16:51:57 -08:00
dbOpts := [] db . Option { db . WithLimit ( limit ), db . WithOrder ( "update_time desc, public_id desc" )}
return r . queryUsers ( ctx , whereClause , args , dbOpts ... )
}
func ( r * Repository ) queryUsers ( ctx context . Context , whereClause string , args [] any , opt ... db . Option ) ([] * User , time . Time , error ) {
const op = "iam.(Repository).queryUsers"
var transactionTimestamp time . Time
var ret [] * userAccountInfo
if _ , err := r . writer . DoTx ( ctx , db . StdRetryCnt , db . ExpBackoff {}, func ( rd db . Reader , w db . Writer ) error {
var inRet [] * userAccountInfo
if err := rd . SearchWhere ( ctx , & inRet , whereClause , args , opt ... ); err != nil {
return errors . Wrap ( ctx , err , op )
}
ret = inRet
var err error
transactionTimestamp , err = rd . Now ( ctx )
return err
}); err != nil {
return nil , time . Time {}, err
2021-04-20 20:33:56 -04:00
}
2023-12-20 16:51:57 -08:00
users := make ([] * User , 0 , len ( ret ))
for _ , u := range ret {
2021-04-20 20:33:56 -04:00
users = append ( users , u . shallowConversion ())
}
2023-12-20 16:51:57 -08:00
return users , transactionTimestamp , nil
}
// listUserDeletedIds lists the public IDs of any users deleted since the timestamp provided.
func ( r * Repository ) listUserDeletedIds ( ctx context . Context , since time . Time ) ([] string , time . Time , error ) {
const op = "iam.(Repository).listUserDeletedIds"
var deletedResources [] * deletedUser
var transactionTimestamp time . Time
if _ , err := r . writer . DoTx ( ctx , db . StdRetryCnt , db . ExpBackoff {}, func ( r db . Reader , _ db . Writer ) error {
if err := r . SearchWhere ( ctx , & deletedResources , "delete_time >= ?" , [] any { since }); err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to query deleted users" ))
}
var err error
transactionTimestamp , err = r . Now ( ctx )
if err != nil {
return errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to get transaction timestamp" ))
}
return nil
}); err != nil {
return nil , time . Time {}, err
}
var dIds [] string
for _ , res := range deletedResources {
dIds = append ( dIds , res . PublicId )
}
return dIds , transactionTimestamp , nil
}
// estimatedUserCount returns an estimate of the total number of items in the iam_user table.
func ( r * Repository ) estimatedUserCount ( ctx context . Context ) ( int , error ) {
const op = "iam.(Repository).estimatedUserCount"
rows , err := r . reader . Query ( ctx , estimateCountUsers , nil )
if err != nil {
return 0 , errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to query total users" ))
}
var count int
for rows . Next () {
if err := r . reader . ScanRows ( ctx , rows , & count ); err != nil {
return 0 , errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to query total users" ))
}
}
2024-01-22 14:03:40 -05:00
if err := rows . Err (); err != nil {
return 0 , errors . Wrap ( ctx , err , op , errors . WithMsg ( "failed to query total users" ))
}
2023-12-20 16:51:57 -08:00
return count , nil
2021-04-20 20:33:56 -04:00
}