Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/bootstrap/users/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,13 @@ func importFile(
return fmt.Errorf("create user: %w", err)
}

if err := passwordRepo.Create(ctx, pool, &domain.CreateUserPassword{
if err := passwordRepo.Set(ctx, pool, &domain.SetUserPassword{
ProjectID: doc.Header.ProjectID,
UserID: doc.Header.ID,
EncodedHash: pw.EncodedHash,
ChangeRequired: pw.ChangeRequired,
}); err != nil {
return fmt.Errorf("create password: %w", err)
return fmt.Errorf("set password: %w", err)
}

slog.Info("bootstrap user: loaded user", slog.String("path", path), slog.String("id", doc.Header.ID))
Expand Down
12 changes: 6 additions & 6 deletions internal/domain/flow_on_success_create_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,20 @@ type flowUserWriter interface {
Create(ctx context.Context, client database.QueryExecutor, user *CreateUser) error
}

type flowUserPasswordWriter interface {
Create(ctx context.Context, client database.QueryExecutor, password *CreateUserPassword) error
type flowUserPasswordSetter interface {
Set(ctx context.Context, client database.QueryExecutor, password *SetUserPassword) error
}

// FlowCreateUserHandler implements the `create_user` on_success:
// persist a new user from validated identifier + password fields.
type FlowCreateUserHandler struct {
ids idgen.Generator
users flowUserWriter
passwords flowUserPasswordWriter
passwords flowUserPasswordSetter
hasher FlowPasswordHasher
}

func NewFlowCreateUserHandler(ids idgen.Generator, users flowUserWriter, passwords flowUserPasswordWriter, hasher FlowPasswordHasher) *FlowCreateUserHandler {
func NewFlowCreateUserHandler(ids idgen.Generator, users flowUserWriter, passwords flowUserPasswordSetter, hasher FlowPasswordHasher) *FlowCreateUserHandler {
return &FlowCreateUserHandler{ids: ids, users: users, passwords: passwords, hasher: hasher}
}

Expand Down Expand Up @@ -88,12 +88,12 @@ func (h *FlowCreateUserHandler) Handle(ctx context.Context, client database.Quer
return FlowOnSuccessResult{}, fmt.Errorf("flow on_success create_user: insert user: %w", err)
}

if err := h.passwords.Create(ctx, client, &CreateUserPassword{
if err := h.passwords.Set(ctx, client, &SetUserPassword{
ProjectID: in.ProjectID,
UserID: userID,
EncodedHash: encodedHash,
}); err != nil {
return FlowOnSuccessResult{}, fmt.Errorf("flow on_success create_user: insert password: %w", err)
return FlowOnSuccessResult{}, fmt.Errorf("flow on_success create_user: set password: %w", err)
}

return FlowOnSuccessResult{UserID: userID}, nil
Expand Down
10 changes: 5 additions & 5 deletions internal/domain/flow_state_machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ func (f *fakeUserRepo) Create(_ context.Context, _ database.QueryExecutor, user

// fakeUserPasswordRepo records the password rows create_user persists.
type fakeUserPasswordRepo struct {
created []*domain.CreateUserPassword
set []*domain.SetUserPassword
}

func (f *fakeUserPasswordRepo) Create(_ context.Context, _ database.QueryExecutor, pw *domain.CreateUserPassword) error {
f.created = append(f.created, pw)
func (f *fakeUserPasswordRepo) Set(_ context.Context, _ database.QueryExecutor, pw *domain.SetUserPassword) error {
f.set = append(f.set, pw)
return nil
}

Expand Down Expand Up @@ -322,8 +322,8 @@ func TestFlowStateMachine_Process_RegistrationHappyPath(t *testing.T) {
wantUserID := "user_01TEST"
assert.Equal(t, wantUserID, w.users.created[0].ID)

require.Len(t, w.pws.created, 1)
assert.Equal(t, "hashed:correct-horse-battery-staple", w.pws.created[0].EncodedHash)
require.Len(t, w.pws.set, 1)
assert.Equal(t, "hashed:correct-horse-battery-staple", w.pws.set[0].EncodedHash)

// create_user pins the user ID and registers them on the attempt so the
// terminal step can issue a handoff token and auto-sign-in the new user.
Expand Down
4 changes: 2 additions & 2 deletions internal/domain/user_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (u *UserPassword) Verify(password string, verifier crypto.HashVerifier) err
return nil
}

type CreateUserPassword struct {
type SetUserPassword struct {
ProjectID string
UserID string
EncodedHash string
Expand All @@ -59,7 +59,7 @@ type UserPasswordRepository interface {
GetByUserID(ctx context.Context, client database.QueryExecutor, projectID string, userID string) (*UserPassword, error)
Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*UserPassword, error)
List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*UserPassword, error)
Create(ctx context.Context, client database.QueryExecutor, user *CreateUserPassword) error
Set(ctx context.Context, client database.QueryExecutor, user *SetUserPassword) error
Comment thread
adlerhurst marked this conversation as resolved.
Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) error
DeleteByUserID(ctx context.Context, client database.QueryExecutor, projectID string, userID string) error
}
Expand Down
9 changes: 2 additions & 7 deletions internal/service/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,7 @@ func (o *SetPasswordUserAction) Prepare(_ context.Context, _ database.QueryExecu
}

func (o *SetPasswordUserAction) Apply(ctx context.Context, db database.QueryExecutor) error {
err := o.passwordRepo.DeleteByUserID(ctx, db, o.ProjectID, o.UserID)
if err != nil {
return domain.ErrInternal(err).WithMessage("failed to remove old password from database")
}

err = o.passwordRepo.Create(ctx, db, &domain.CreateUserPassword{
err := o.passwordRepo.Set(ctx, db, &domain.SetUserPassword{
ProjectID: o.ProjectID,
UserID: o.UserID,
EncodedHash: o.hash,
Expand All @@ -257,7 +252,7 @@ func (o *SetPasswordUserAction) Apply(ctx context.Context, db database.QueryExec
if _, ok := errors.AsType[*database.ForeignKeyError](err); ok {
return domain.ErrUserNotFound()
}
return domain.ErrInternal(err).WithMessage("failed to set initial password")
return domain.ErrInternal(err).WithMessage("failed to set password")
}
return nil
}
59 changes: 57 additions & 2 deletions internal/storage/database/repository/user_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestUserPasswordRepository_CRUD(t *testing.T) {
insertProjectTeamSchemaUser(t, tx, pid, tid, schemaURL, userID)

vid := "verif-1"
require.NoError(t, repo.Create(ctx, tx, &domain.CreateUserPassword{
require.NoError(t, repo.Set(ctx, tx, &domain.SetUserPassword{
ProjectID: pid,
UserID: userID,
EncodedHash: "argon2id$v=19$m=65536,t=3,p=4$fake",
Expand Down Expand Up @@ -80,7 +80,7 @@ func TestUserPasswordRepository_CRUD(t *testing.T) {
_, err = repo.Get(ctx, tx, database.WithCondition(repo.UniqueCondition(pid, userID)))
require.ErrorIs(t, err, new(database.NoRowFoundError))

require.NoError(t, repo.Create(ctx, tx, &domain.CreateUserPassword{
require.NoError(t, repo.Set(ctx, tx, &domain.SetUserPassword{
ProjectID: pid,
UserID: userID,
EncodedHash: "argon2id$v=19$m=65536,t=3,p=4$fake2",
Expand All @@ -95,6 +95,61 @@ func TestUserPasswordRepository_CRUD(t *testing.T) {
require.ErrorIs(t, err, new(database.NoRowFoundError))
}

func TestUserPasswordRepository_SetUpsert(t *testing.T) {
skipIfSpanner(t)
repo := repository.NewUserPasswordRepository()
tx, rollback := transactionForRollback(t)
defer rollback()
ctx := t.Context()

const (
pid = "proj-cred-pw-upsert"
tid = "team-cred-pw-upsert"
schemaURL = "https://schemas.test/cred-pw-upsert.json"
userID = "usr_pw_upsert"
)

insertProjectTeamSchemaUser(t, tx, pid, tid, schemaURL, userID)

require.NoError(t, repo.Set(ctx, tx, &domain.SetUserPassword{
ProjectID: pid,
UserID: userID,
EncodedHash: "argon2id$v=19$m=65536,t=3,p=4$initial",
ChangeRequired: true,
}))

got, err := repo.Get(ctx, tx, database.WithCondition(repo.UniqueCondition(pid, userID)))
require.NoError(t, err)
initialID := got.ID
require.Equal(t, "argon2id$v=19$m=65536,t=3,p=4$initial", got.EncodedHash)
require.True(t, got.ChangeRequired)

_, err = tx.Exec(ctx,
fmt.Sprintf(`UPDATE %s SET failed_attempts = 3, last_successful_check = NOW() WHERE project_id = $1 AND user_id = $2`, dbTable("user_passwords")),
pid, userID,
)
require.NoError(t, err)

require.NoError(t, repo.Set(ctx, tx, &domain.SetUserPassword{
ProjectID: pid,
UserID: userID,
EncodedHash: "argon2id$v=19$m=65536,t=3,p=4$updated",
ChangeRequired: false,
}))

got2, err := repo.Get(ctx, tx, database.WithCondition(repo.UniqueCondition(pid, userID)))
require.NoError(t, err)
require.Equal(t, initialID, got2.ID)
require.Equal(t, "argon2id$v=19$m=65536,t=3,p=4$updated", got2.EncodedHash)
require.False(t, got2.ChangeRequired)
require.Zero(t, got2.FailedAttempts)
require.Nil(t, got2.LastSuccessfulCheck)

list, err := repo.List(ctx, tx, database.WithCondition(repo.ProjectIDCondition(pid)))
require.NoError(t, err)
require.Len(t, list, 1)
}

func TestUserTOTPRepository_CRUD(t *testing.T) {
skipIfSpanner(t)
repo := repository.NewUserTOTPRepository()
Expand Down
11 changes: 9 additions & 2 deletions internal/storage/database/repository/user_password.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (r *UserPasswordRepository) List(ctx context.Context, client database.Query
return out, nil
}

func (r *UserPasswordRepository) Create(ctx context.Context, client database.QueryExecutor, pw *domain.CreateUserPassword) error {
func (r *UserPasswordRepository) Set(ctx context.Context, client database.QueryExecutor, pw *domain.SetUserPassword) error {
builder := database.NewStatementBuilder("INSERT INTO ")
builder.WriteString(r.qualifiedTableName())
builder.WriteString(" (")
Expand All @@ -171,7 +171,14 @@ func (r *UserPasswordRepository) Create(ctx context.Context, client database.Que
}.WriteUnqualified(builder)
builder.WriteString(") VALUES (")
builder.WriteArgs(pw.ProjectID, pw.UserID, pw.EncodedHash, pw.ChangeRequired, pw.VerificationID)
builder.WriteString(")")
builder.WriteString(") ON CONFLICT (project_id, user_id) DO UPDATE SET")
builder.WriteString(" encoded_hash = EXCLUDED.encoded_hash,")
builder.WriteString(" change_required = EXCLUDED.change_required,")
builder.WriteString(" verification_id = EXCLUDED.verification_id,")
builder.WriteString(" changed_at = NOW(),")
builder.WriteString(" failed_attempts = 0,")
builder.WriteString(" last_successful_check = NULL,")
builder.WriteString(" updated_at = NOW()")
_, err := client.Exec(ctx, builder.String(), builder.Args()...)
return err
}
Expand Down
Loading