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.
410 lines
14 KiB
410 lines
14 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)
|
|
}
|
|
id, err := newUserId()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create user: %w", err)
|
|
}
|
|
u := user.Clone()
|
|
u.(*User).PublicId = id
|
|
resource, err := r.create(ctx, u.(*User))
|
|
if err != nil {
|
|
if db.IsUniqueError(err) {
|
|
return nil, fmt.Errorf("create user: user %s already exists in org %s", user.Name, user.ScopeId)
|
|
}
|
|
return nil, fmt.Errorf("create user: %w for %s", err, u.(*User).PublicId)
|
|
}
|
|
return resource.(*User), err
|
|
}
|
|
|
|
// UpdateUser will update a user in the repository and return the written user.
|
|
// 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, int, error) {
|
|
if user == nil {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("update user: missing user %w", db.ErrInvalidParameter)
|
|
}
|
|
if user.PublicId == "" {
|
|
return 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, 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,
|
|
)
|
|
if len(dbMask) == 0 && len(nullFields) == 0 {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("update user: %w", db.ErrEmptyFieldMask)
|
|
}
|
|
|
|
u := user.Clone()
|
|
resource, rowsUpdated, err := r.update(ctx, u.(*User), version, dbMask, nullFields, opt...)
|
|
if err != nil {
|
|
if db.IsUniqueError(err) {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("update user: user %s already exists in org %s", user.Name, user.ScopeId)
|
|
}
|
|
return nil, db.NoRowsAffected, fmt.Errorf("update user: %w for %s", err, user.PublicId)
|
|
}
|
|
return resource.(*User), rowsUpdated, err
|
|
}
|
|
|
|
// LookupUser will look up a user in the repository. If the user is not
|
|
// found, it will return nil, nil.
|
|
func (r *Repository) LookupUser(ctx context.Context, withPublicId string, opt ...Option) (*User, error) {
|
|
if withPublicId == "" {
|
|
return nil, fmt.Errorf("lookup user: missing public id %w", db.ErrInvalidParameter)
|
|
}
|
|
|
|
user := allocUser()
|
|
user.PublicId = withPublicId
|
|
if err := r.reader.LookupByPublicId(ctx, &user); err != nil {
|
|
if errors.Is(err, db.ErrRecordNotFound) {
|
|
return nil, nil
|
|
}
|
|
return nil, fmt.Errorf("lookup user: failed %w for %s", err, withPublicId)
|
|
}
|
|
return &user, 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)
|
|
}
|
|
underlying, err := r.reader.DB()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get underlying db for account search: %w", err)
|
|
}
|
|
rows, err := underlying.Query(whereUserAccount, 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
|
|
}
|
|
|
|
// AssociateUserWithAccount will associate a user with an existing account.
|
|
// The account must not already be associated with a different user. No
|
|
// options are currently supported.
|
|
func (r *Repository) AssociateUserWithAccount(ctx context.Context, userPublicId, accountId string, opt ...Option) (*User, error) {
|
|
opts := getOpts(opt...)
|
|
if userPublicId == "" {
|
|
return nil, fmt.Errorf("associate user with account: missing user public id %w", db.ErrInvalidParameter)
|
|
}
|
|
if accountId == "" {
|
|
return nil, fmt.Errorf("associate user with account: missing account id %w", db.ErrInvalidParameter)
|
|
}
|
|
|
|
user := allocUser()
|
|
user.PublicId = userPublicId
|
|
|
|
err := r.reader.LookupById(ctx, &user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("associate user with account: unable to lookup user %s: %w", userPublicId, err)
|
|
}
|
|
|
|
acct := allocAccount()
|
|
acct.PublicId = accountId
|
|
err = r.reader.LookupByPublicId(context.Background(), &acct)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("associate user with account: unable to lookup account %s: %w", accountId, err)
|
|
}
|
|
// first, let's handle the case where the account is already
|
|
// associated with the user, so we're done!
|
|
if acct.IamUserId == userPublicId {
|
|
return &user, nil
|
|
}
|
|
|
|
if !opts.withDisassociate {
|
|
if acct.IamUserId != "" && acct.IamUserId != userPublicId {
|
|
return nil, fmt.Errorf("associate user with account: %s account is already associated with a user: %w", accountId, db.ErrInvalidParameter)
|
|
}
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, acct.GetScopeId(), kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("associate user with account: unable to get oplog wrapper: %w", err)
|
|
}
|
|
|
|
// validate, associated the user with the account, and then read the
|
|
// user back in the same tx for consistency.
|
|
_, err = r.writer.DoTx(
|
|
ctx,
|
|
db.StdRetryCnt,
|
|
db.ExpBackoff{},
|
|
func(txReader db.Reader, w db.Writer) error {
|
|
metadata := oplog.Metadata{
|
|
"resource-public-id": []string{accountId},
|
|
"scope-id": []string{acct.ScopeId},
|
|
"scope-type": []string{scope.Org.String()},
|
|
"resource-type": []string{"auth-account"},
|
|
}
|
|
var updatedRows int
|
|
updatedAcct := acct.Clone().(*authAccount)
|
|
updatedAcct.IamUserId = userPublicId
|
|
// we are using WithWhere to make sure the account is not
|
|
// associated with a user (handling race conditions with concurrent
|
|
// transactions)
|
|
switch {
|
|
case opts.withDisassociate:
|
|
updatedRows, err = w.Update(ctx, updatedAcct, []string{"IamUserId"}, nil, db.WithOplog(oplogWrapper, metadata), db.WithWhere("iam_user_id = ?", acct.IamUserId))
|
|
default:
|
|
updatedRows, err = w.Update(ctx, updatedAcct, []string{"IamUserId"}, nil, db.WithOplog(oplogWrapper, metadata), db.WithWhere("iam_user_id is NULL"))
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if updatedRows != 1 {
|
|
return fmt.Errorf("account update affected %d rows", updatedRows)
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("associate user with account: %w", err)
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
// DissociateUserWithAccount will dissociate a user with its existing account.
|
|
// An error is returned if account is associated with a different user. No
|
|
// options are currently supported.
|
|
func (r *Repository) DissociateUserWithAccount(ctx context.Context, userPublicId, accountId string, opt ...Option) (*User, error) {
|
|
if userPublicId == "" {
|
|
return nil, fmt.Errorf("dissociate user with account: missing user public id %w", db.ErrInvalidParameter)
|
|
}
|
|
if accountId == "" {
|
|
return nil, fmt.Errorf("dissociate user with account: missing account id %w", db.ErrInvalidParameter)
|
|
}
|
|
|
|
user := allocUser()
|
|
user.PublicId = userPublicId
|
|
err := r.reader.LookupById(ctx, &user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dissociate user with account: unable to lookup user %s: %w", userPublicId, err)
|
|
}
|
|
|
|
acct := allocAccount()
|
|
acct.PublicId = accountId
|
|
err = r.reader.LookupByPublicId(ctx, &acct)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dissociate user with account: unable to lookup account %s: %w", accountId, err)
|
|
}
|
|
// first, let's handle the case where the account is not associated
|
|
// with any user, so we're done!
|
|
if acct.IamUserId == "" {
|
|
return &user, nil
|
|
}
|
|
// before proceeding with an update, is the account associated with the different user?
|
|
if acct.IamUserId != userPublicId {
|
|
return nil, fmt.Errorf("dissociate user with account: %s account is not associated with a user: %w", accountId, db.ErrInvalidParameter)
|
|
}
|
|
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, acct.GetScopeId(), kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("disassociate user with account: unable to get oplog wrapper: %w", err)
|
|
}
|
|
|
|
// validate, dissociate the user with the account and then read the user back in
|
|
// the same tx for consistency.
|
|
_, err = r.writer.DoTx(
|
|
ctx,
|
|
db.StdRetryCnt,
|
|
db.ExpBackoff{},
|
|
func(txReader db.Reader, w db.Writer) error {
|
|
metadata := oplog.Metadata{
|
|
"resource-public-id": []string{accountId},
|
|
"scope-id": []string{acct.ScopeId},
|
|
"scope-type": []string{scope.Org.String()},
|
|
"resource-type": []string{"auth-account"},
|
|
}
|
|
updatedAcct := acct.Clone().(*authAccount)
|
|
updatedAcct.IamUserId = ""
|
|
// set the user id to null and use WithWhere to ensure that the auth
|
|
// account is associated with the user (handling race conditions
|
|
// with other concurrent transactions)
|
|
updatedRows, err := w.Update(ctx, updatedAcct, nil, []string{"IamUserId"}, db.WithOplog(oplogWrapper, metadata), db.WithWhere("iam_user_id = ?", userPublicId))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if updatedRows != 1 {
|
|
return fmt.Errorf("account update affected %d rows", updatedRows)
|
|
}
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dissociate user with account: %w", err)
|
|
}
|
|
return &user, nil
|
|
}
|