mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
796 lines
29 KiB
796 lines
29 KiB
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
|
|
}
|