package iam import ( "context" "errors" "fmt" "strings" "github.com/hashicorp/boundary/internal/db" dbcommon "github.com/hashicorp/boundary/internal/db/common" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/hashicorp/boundary/internal/types/scope" ) // 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) { if user == nil { return nil, fmt.Errorf("create user: missing user %w", db.ErrInvalidParameter) } if user.PublicId != "" { return nil, fmt.Errorf("create user: public id is not empty %w", db.ErrInvalidParameter) } u := user.Clone().(*User) opts := getOpts(opt...) if opts.withPublicId != "" { if !strings.HasPrefix(opts.withPublicId, UserPrefix+"_") { return nil, fmt.Errorf("create user: passed-in public ID %q has wrong prefix, should be %q: %w", opts.withPublicId, UserPrefix, db.ErrInvalidPublicId) } u.PublicId = opts.withPublicId } else { id, err := newUserId() if err != nil { return nil, fmt.Errorf("create user: %w", err) } u.PublicId = id } resource, err := r.create(ctx, u) if err != nil { if db.IsUniqueError(err) { return nil, fmt.Errorf("create user: user %s already exists in org %s: %w", user.Name, user.ScopeId, err) } return nil, fmt.Errorf("create user: %w for %s", err, u.PublicId) } return resource.(*User), err } // 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) { if user == nil { return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: missing user %w", db.ErrInvalidParameter) } if user.PublicId == "" { return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: missing user public id %w", db.ErrInvalidParameter) } for _, f := range fieldMaskPaths { switch { case strings.EqualFold("name", f): case strings.EqualFold("description", f): default: return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: field: %s: %w", f, db.ErrInvalidFieldMask) } } var dbMask, nullFields []string dbMask, nullFields = dbcommon.BuildUpdatePaths( map[string]interface{}{ "name": user.Name, "description": user.Description, }, fieldMaskPaths, nil, ) if len(dbMask) == 0 && len(nullFields) == 0 { return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: %w", db.ErrEmptyFieldMask) } u := user.Clone().(*User) metadata, err := r.stdMetadata(ctx, u) if err != nil { return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: error getting metadata for update: %w", err) } 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 { return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: unable to get scope: %w", err) } oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog) if err != nil { return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: unable to get oplog wrapper: %w", err) } 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 && rowsUpdated > 1 { // return err, which will result in a rollback of the update return errors.New("error more than 1 resource would have been updated ") } if err != nil { return err } // 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 } currentAccountIds, err = txRepo.ListUserAccounts(ctx, user.PublicId) if err != nil { return fmt.Errorf("unable to retrieve current account ids after update: %w", err) } return nil }, ) if err != nil { if db.IsUniqueError(err) { return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: user %s already exists in org %s", user.Name, user.ScopeId) } return nil, nil, db.NoRowsAffected, fmt.Errorf("update user: %w for %s", err, user.PublicId) } return returnedUser, currentAccountIds, rowsUpdated, err } // 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. func (r *Repository) LookupUser(ctx context.Context, userId string, opt ...Option) (*User, []string, error) { if userId == "" { return nil, nil, fmt.Errorf("lookup user: missing public id %w", db.ErrInvalidParameter) } user := allocUser() user.PublicId = userId if err := r.reader.LookupByPublicId(ctx, &user); err != nil { if errors.Is(err, db.ErrRecordNotFound) { return nil, nil, nil } return nil, nil, fmt.Errorf("lookup user: failed %w for %s", err, userId) } currentAccountIds, err := r.ListUserAccounts(ctx, userId) if err != nil { return nil, nil, fmt.Errorf("lookup user: unable to retrieve current account ids: %w", err) } return &user, currentAccountIds, nil } // DeleteUser will delete a user from the repository func (r *Repository) DeleteUser(ctx context.Context, withPublicId string, opt ...Option) (int, error) { if withPublicId == "" { return db.NoRowsAffected, fmt.Errorf("delete user: missing public id %w", db.ErrInvalidParameter) } user := allocUser() user.PublicId = withPublicId if err := r.reader.LookupByPublicId(ctx, &user); err != nil { return db.NoRowsAffected, fmt.Errorf("delete user: failed %w for %s", err, withPublicId) } rowsDeleted, err := r.delete(ctx, &user) if err != nil { return db.NoRowsAffected, fmt.Errorf("delete user: failed %w for %s", err, withPublicId) } return rowsDeleted, nil } // ListUsers in an org and supports the WithLimit option. func (r *Repository) ListUsers(ctx context.Context, withOrgId string, opt ...Option) ([]*User, error) { if withOrgId == "" { return nil, fmt.Errorf("list users: missing org id %w", db.ErrInvalidParameter) } var users []*User err := r.list(ctx, &users, "scope_id = ?", []interface{}{withOrgId}, opt...) if err != nil { return nil, fmt.Errorf("list users: %w", err) } return users, nil } // LookupUserWithLogin will attempt to lookup the user with a matching // account id and return the user if found. If a user is not found and the // WithAutoVivify() option is true, then a new iam User will be // created in the scope of the account, and associated with the // account. If a new user is auto vivified, then the WithName and // WithDescription options are supported as well. func (r *Repository) LookupUserWithLogin(ctx context.Context, accountId string, opt ...Option) (*User, error) { opts := getOpts(opt...) if accountId == "" { return nil, fmt.Errorf("lookup user with login: missing account id %w", db.ErrInvalidParameter) } u, err := r.getUserWithAccount(ctx, accountId) if err != nil { return nil, fmt.Errorf("lookup user with login: %w", err) } if u != nil { return u, nil } if !opts.withAutoVivify { return nil, fmt.Errorf("lookup user with login: user not found for account %s: %w", accountId, db.ErrRecordNotFound) } acct := allocAccount() acct.PublicId = accountId err = r.reader.LookupByPublicId(context.Background(), &acct) if err != nil { return nil, fmt.Errorf("lookup user with login: unable to lookup account %s: %w", accountId, err) } metadata := oplog.Metadata{ "resource-public-id": []string{accountId}, "scope-id": []string{acct.ScopeId}, "scope-type": []string{scope.Org.String()}, "resource-type": []string{"auth-account"}, } oplogWrapper, err := r.kms.GetWrapper(ctx, acct.GetScopeId(), kms.KeyPurposeOplog) if err != nil { return nil, fmt.Errorf("lookup user with login: unable to get oplog wrapper: %w", err) } // We will create a new user and associate the user with the account // within one retryable transaction using writer.DoTx var obtainedUser *User _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(_ db.Reader, w db.Writer) error { msgs := make([]*oplog.Message, 0, 2) ticket, err := w.GetTicket(&acct) if err != nil { return err } obtainedUser, err = NewUser(acct.ScopeId, opt...) if err != nil { return err } id, err := newUserId() if err != nil { return err } var createMsg oplog.Message obtainedUser.PublicId = id err = w.Create(ctx, obtainedUser, db.NewOplogMsg(&createMsg)) if err != nil { return err } msgs = append(msgs, &createMsg) var updateMsg oplog.Message updateAcct := acct.Clone().(*authAccount) updateAcct.IamUserId = id updatedRows, err := w.Update(ctx, updateAcct, []string{"IamUserId"}, nil, db.NewOplogMsg(&updateMsg)) if err != nil { return err } if updatedRows != 1 { return fmt.Errorf("account update affected %d rows", updatedRows) } msgs = append(msgs, &updateMsg) if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { return err } return nil }, ) if err != nil { return nil, fmt.Errorf("lookup user with login: unable to associate user and account: %w", err) } return obtainedUser, nil } func (r *Repository) getUserWithAccount(ctx context.Context, withAccountId string, opt ...Option) (*User, error) { if withAccountId == "" { return nil, fmt.Errorf("missing account id %w", db.ErrInvalidParameter) } rows, err := r.reader.Query(ctx, whereUserAccount, []interface{}{withAccountId}) if err != nil { return nil, fmt.Errorf("unable to query account %s", withAccountId) } defer rows.Close() u := allocUser() if rows.Next() { err = r.reader.ScanRows(rows, &u) if err != nil { return nil, fmt.Errorf("unable to scan rows for account %s: %w", withAccountId, err) } } else { if err := rows.Err(); err != nil { return nil, fmt.Errorf("unable to get next account: %w", err) } return nil, nil } if err != nil { return nil, fmt.Errorf("unable to get next row for accounts %s: %w", withAccountId, err) } return &u, nil } // ListUserAccounts returns the account ids for the userId and supports the // WithLimit option. Returns nil, nil when no associated accounts are found. func (r *Repository) ListUserAccounts(ctx context.Context, userId string, opt ...Option) ([]string, error) { if userId == "" { return nil, fmt.Errorf("list auth account ids: missing user id: %w", db.ErrInvalidParameter) } var accounts []*authAccount if err := r.list(ctx, &accounts, "iam_user_id = ?", []interface{}{userId}, opt...); err != nil { return nil, fmt.Errorf("list auth account ids: %w", err) } if len(accounts) == 0 { return nil, nil } ids := make([]string, 0, len(accounts)) for _, aa := range accounts { ids = append(ids, aa.PublicId) } return ids, nil } // AddUserAccounts will associate a user with existing accounts and // 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. func (r *Repository) AddUserAccounts(ctx context.Context, userId string, userVersion uint32, accountIds []string, opt ...Option) ([]string, error) { if userId == "" { return nil, fmt.Errorf("associate accounts: missing user public id %w", db.ErrInvalidParameter) } if userVersion == 0 { return nil, fmt.Errorf("associate accounts: missing user version %w", db.ErrInvalidParameter) } if len(accountIds) == 0 { return nil, fmt.Errorf("associate accounts: missing account id %w", db.ErrInvalidParameter) } user := allocUser() user.PublicId = userId err := r.reader.LookupById(ctx, &user) if err != nil { return nil, fmt.Errorf("associate accounts: unable to lookup user %s: %w", userId, err) } oplogWrapper, err := r.kms.GetWrapper(ctx, user.ScopeId, kms.KeyPurposeOplog) if err != nil { return nil, fmt.Errorf("associate accounts: unable to get oplog wrapper: %w", err) } var currentAccountIds []string _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { userTicket, err := w.GetTicket(&user) if err != nil { return fmt.Errorf("associate accounts: unable to get ticket: %w", err) } updatedUser := allocUser() 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 { return fmt.Errorf("associate accounts: unable to update user version: %w", err) } if rowsUpdated != 1 { return fmt.Errorf("associate accounts: updated user and %d rows updated", rowsUpdated) } if err := associateUserWithAccounts(ctx, r.kms, reader, w, user.PublicId, accountIds); err != nil { return fmt.Errorf("associate accounts: %w", err) } 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 { return fmt.Errorf("associate accounts: unable to write oplog: %w", err) } // 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 } currentAccountIds, err = txRepo.ListUserAccounts(ctx, user.PublicId) if err != nil { return fmt.Errorf("associate accounts: unable to retrieve current account ids after adds: %w", err) } return nil }, ) if err != nil { return nil, fmt.Errorf("associate accounts: error associating account ids: %w", err) } return currentAccountIds, nil } // DeleteUserAccounts will disassociate a user from existing accounts and // 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. func (r *Repository) DeleteUserAccounts(ctx context.Context, userId string, userVersion uint32, accountIds []string, opt ...Option) ([]string, error) { if userId == "" { return nil, fmt.Errorf("disassociate accounts: missing user public id %w", db.ErrInvalidParameter) } if userVersion == 0 { return nil, fmt.Errorf("disassociate accounts: missing user version %w", db.ErrInvalidParameter) } if len(accountIds) == 0 { return nil, fmt.Errorf("disassociate accounts: missing account id %w", db.ErrInvalidParameter) } user := allocUser() user.PublicId = userId err := r.reader.LookupById(ctx, &user) if err != nil { return nil, fmt.Errorf("disassociate accounts: unable to lookup user %s: %w", userId, err) } oplogWrapper, err := r.kms.GetWrapper(ctx, user.ScopeId, kms.KeyPurposeOplog) if err != nil { return nil, fmt.Errorf("disassociate accounts: unable to get oplog wrapper: %w", err) } var currentAccountIds []string _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { userTicket, err := w.GetTicket(&user) if err != nil { return fmt.Errorf("disassociate accounts: unable to get ticket: %w", err) } updatedUser := allocUser() 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 { return fmt.Errorf("disassociate accounts: unable to update user version: %w", err) } if rowsUpdated != 1 { return fmt.Errorf("disassociate accounts: updated user and %d rows updated", rowsUpdated) } if err := dissociateUserFromAccounts(ctx, r.kms, reader, w, user.PublicId, accountIds); err != nil { return fmt.Errorf("disassociate accounts: %w", err) } 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 { return fmt.Errorf("disassociate accounts: unable to write oplog: %w", err) } // 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 } currentAccountIds, err = txRepo.ListUserAccounts(ctx, user.PublicId) if err != nil { return fmt.Errorf("disassociate accounts: unable to retrieve current account ids after adds: %w", err) } return nil }, ) if err != nil { return nil, fmt.Errorf("disassociate accounts: error associating account ids: %w", err) } return currentAccountIds, nil } // SetUserAccounts will associate a user with existing accounts and // 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. func (r *Repository) SetUserAccounts(ctx context.Context, userId string, userVersion uint32, accountIds []string, opt ...Option) ([]string, error) { if userId == "" { return nil, fmt.Errorf("set associated accounts: missing user public id %w", db.ErrInvalidParameter) } if userVersion == 0 { return nil, fmt.Errorf("set associated accounts: missing user version %w", db.ErrInvalidParameter) } user := allocUser() user.PublicId = userId err := r.reader.LookupById(ctx, &user) if err != nil { return nil, fmt.Errorf("set associated accounts: unable to lookup user %s: %w", userId, err) } oplogWrapper, err := r.kms.GetWrapper(ctx, user.ScopeId, kms.KeyPurposeOplog) if err != nil { return nil, fmt.Errorf("set associated accounts: unable to get oplog wrapper: %w", err) } var currentAccountIds []string _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, func(reader db.Reader, w db.Writer) error { associateIds, disassociateIds, err := associationChanges(ctx, reader, userId, accountIds) if err != nil { return fmt.Errorf("set associated accounts: unable to determine changes: %w", err) } // 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 } currentAccountIds, err = txRepo.ListUserAccounts(ctx, userId) if err != nil { return fmt.Errorf("set associated accounts: unable to retrieve current account ids after set: %w", err) } return nil } userTicket, err := w.GetTicket(&user) if err != nil { return fmt.Errorf("set associated accounts: unable to get ticket: %w", err) } updatedUser := allocUser() 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 { return fmt.Errorf("set associated accounts: unable to update user version: %w", err) } if rowsUpdated != 1 { return fmt.Errorf("set associated accounts: updated user and %d rows updated", rowsUpdated) } if len(associateIds) > 0 { if err := associateUserWithAccounts(ctx, r.kms, reader, w, userId, associateIds); err != nil { return fmt.Errorf("set associated accounts: unable to associate ids: %w", err) } } if len(disassociateIds) > 0 { if err := dissociateUserFromAccounts(ctx, r.kms, reader, w, userId, disassociateIds); err != nil { return fmt.Errorf("set associated accounts: unable to disassociate ids: %w", err) } } 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{userId}, } if err := w.WriteOplogEntryWith(ctx, oplogWrapper, userTicket, metadata, []*oplog.Message{&userOplogMsg}); err != nil { return fmt.Errorf("set associated accounts: unable to write oplog: %w", err) } // 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 } currentAccountIds, err = txRepo.ListUserAccounts(ctx, user.PublicId) if err != nil { return fmt.Errorf("set associated accounts: unable to retrieve current account ids after set: %w", err) } return nil }, ) if err != nil { return nil, fmt.Errorf("set associated accounts: %w", err) } return currentAccountIds, nil } // associateUserWithAccounts will associate the accounts (accountIds) with // the user (userId) within the writer's database func associateUserWithAccounts(ctx context.Context, repoKms *kms.Kms, reader db.Reader, writer db.Writer, userId string, accountIds []string, opt ...Option) error { if repoKms == nil { return fmt.Errorf("associate user with accounts: kms is nil: %w", db.ErrInvalidParameter) } if reader == nil { return fmt.Errorf("associate user with accounts: db reader is nil: %w", db.ErrInvalidParameter) } if writer == nil { return fmt.Errorf("associate user with accounts: db writer is nil: %w", db.ErrInvalidParameter) } if userId == "" { return fmt.Errorf("associate user with accounts: user id is empty: %w", db.ErrInvalidParameter) } if len(accountIds) == 0 { return fmt.Errorf("associate user with accounts: missing account id %w", db.ErrInvalidParameter) } authAccounts := make([]*authAccount, 0, len(accountIds)) for _, accountId := range accountIds { acct := allocAccount() acct.PublicId = accountId err := reader.LookupByPublicId(context.Background(), &acct) if err != nil { return fmt.Errorf("associate user with accounts: unable to lookup account %s: %w", accountId, err) } if acct.IamUserId != "" && acct.IamUserId != userId { return fmt.Errorf("associate user with accounts: %s account is associated with a user %s: %w", accountId, acct.IamUserId, db.ErrInvalidParameter) } 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 { return fmt.Errorf("associate user with accounts: unable to get oplog wrapper: %w", err) } 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 { return fmt.Errorf("associate user with accouts: failed to associate %s account: %w", aa.PublicId, err) } if updatedRows == 0 { return fmt.Errorf("associate user with accounts: failed to associate %s account: it is already associated with another user", aa.PublicId) } if updatedRows > 1 { return fmt.Errorf("associate user with accounts: failed to associate %s account: would have updated too many accounts %d", aa.PublicId, updatedRows) } } 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. func dissociateUserFromAccounts(ctx context.Context, repoKms *kms.Kms, reader db.Reader, writer db.Writer, userId string, accountIds []string, opt ...Option) error { if repoKms == nil { return fmt.Errorf("dissociate user from accounts: kms is nil: %w", db.ErrInvalidParameter) } if reader == nil { return fmt.Errorf("dissociate user from accounts: db reader is nil: %w", db.ErrInvalidParameter) } if writer == nil { return fmt.Errorf("dissociate user from accounts: db writer is nil: %w", db.ErrInvalidParameter) } if userId == "" { return fmt.Errorf("dissociate user from accounts: missing user public id %w", db.ErrInvalidParameter) } if len(accountIds) == 0 { return fmt.Errorf("dissociate user from accounts: missing account id %w", db.ErrInvalidParameter) } authAccounts := make([]*authAccount, 0, len(accountIds)) for _, accountId := range accountIds { acct := allocAccount() acct.PublicId = accountId err := reader.LookupByPublicId(context.Background(), &acct) if err != nil { return fmt.Errorf("dissociate user from accounts: unable to lookup account %s: %w", accountId, err) } if acct.IamUserId != userId { return fmt.Errorf("dissociate user from accounts: %s account is not associated with user %s: %w", accountId, userId, db.ErrInvalidParameter) } 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 { return fmt.Errorf("dissociate user from accounts: unable to get oplog wrapper: %w", err) } 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 { return fmt.Errorf("dissociate user from accounts: failed to disassociate %s account: %w", aa.PublicId, err) } if updatedRows == 0 { return fmt.Errorf("dissociate user from accounts: failed to disassociate %s account: it is associated with another user", aa.PublicId) } if updatedRows > 1 { return fmt.Errorf("dissociate user from accounts: failed to disassociate %s account: would have updated too many accounts %d", aa.PublicId, updatedRows) } } 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) { var inClauseSpots []string // starts at 2 because there is already a $1 in the query for i := 2; i < len(accountIds)+2; i++ { inClauseSpots = append(inClauseSpots, fmt.Sprintf("$%d", i)) } inClause := strings.Join(inClauseSpots, ",") if inClause == "" { inClause = "''" } query := fmt.Sprintf(accountChangesQuery, inClause) var params []interface{} params = append(params, userId) for _, v := range accountIds { params = append(params, v) } rows, err := reader.Query(ctx, query, params) if err != nil { return nil, nil, fmt.Errorf("changes: query failed: %w", err) } defer rows.Close() type change struct { Action string AccountId string } var changes []*change for rows.Next() { var chg change if err := reader.ScanRows(rows, &chg); err != nil { return nil, nil, fmt.Errorf("changes: scan row failed: %w", err) } changes = append(changes, &chg) } var associateIds, disassociateIds []string for _, c := range changes { if c.AccountId == "" { return nil, nil, fmt.Errorf("changes: missing account id in change result") } switch c.Action { case "associate": associateIds = append(associateIds, c.AccountId) case "disassociate": disassociateIds = append(disassociateIds, c.AccountId) default: return nil, nil, fmt.Errorf("changes: unknown action %s for %s", c.Action, c.AccountId) } } return associateIds, disassociateIds, nil }