fix(provisioning): keep the Owner seat single; clash on the operator name stays retryable
Two defects live-drilled in the break-glass staff provisioning: - An Owner reset that typed any username other than the occupied seat took UpsertOwner's insert arm and silently minted a SECOND owner row, leaving the existing seat — possibly the compromised account the reset was meant to replace — live; every owner row is undeletable through the panel, so the tier could never converge back to one. provisionOwner now refuses with ownerSeatTakenError naming the seat (recoverable: the TUI routes back to the form); bootstrap still mints, and the seat's own username still resets in place. PGRepo gains OwnerUsername for the guard. - InsertOperator returned the raw driver error on a taken username while the console keys its rename prompt off api.ErrConflict — the "choose another name" leg died with SQLSTATE 23505 against real Postgres (the fake encoded the contract; PGRepo had drifted). Map the unique violation to ErrConflict and pin it in pgint. Live (auditfix37): fresh username refused naming the seat; seat reset kept the id/email with still exactly one owner; taken operator name returned to the form with the retry note, and the retyped name succeeded (drill rows cleaned).
This commit is contained in:
6 files changed
+173
-27
No files matched your search
@@ -666,14 +666,37 @@ func TestOwnerProvisioningWritesOwnerRole(t *testing.T) {
|
||||
if ok, err := repo.AdminExists(ctx); err != nil || !ok {
|
||||
t.Fatalf("AdminExists = (%v, %v), want true (the owner counts as staff)", ok, err)
|
||||
}
|
||||
// OwnerUsername names the seat the console guard protects. The shared test
|
||||
// database may hold owner rows from earlier tests, so assert the returned name
|
||||
// IS an active owner rather than one specific row.
|
||||
seat, err := repo.OwnerUsername(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("OwnerUsername: %v", err)
|
||||
}
|
||||
if seat == "" {
|
||||
t.Fatal("OwnerUsername = empty, want an active owner seat")
|
||||
}
|
||||
var active int
|
||||
if err := db.QueryRowContext(ctx,
|
||||
`SELECT count(*) FROM users WHERE username = $1 AND role = 'owner' AND deleted_at IS NULL`, seat).
|
||||
Scan(&active); err != nil || active != 1 {
|
||||
t.Fatalf("OwnerUsername returned %q, not an active owner row (count=%d err=%v)", seat, active, err)
|
||||
}
|
||||
// Operators stay plain admins: the owner tier stays singular.
|
||||
opID := "usr-op-" + suffix(t)
|
||||
if err := repo.InsertOperator(ctx, opID, "op-"+suffix(t), ""); err != nil {
|
||||
opName := "op-" + suffix(t)
|
||||
if err := repo.InsertOperator(ctx, opID, opName, ""); err != nil {
|
||||
t.Fatalf("InsertOperator: %v", err)
|
||||
}
|
||||
if role := userRole(t, opID); role != "admin" {
|
||||
t.Fatalf("InsertOperator role = %q, want admin", role)
|
||||
}
|
||||
// A taken username must surface as api.ErrConflict: the console routes its
|
||||
// rename prompt off that sentinel (the cmd fake encoded the contract; PGRepo
|
||||
// returned the raw driver error until this arm was mapped).
|
||||
if err := repo.InsertOperator(ctx, "usr-op2-"+suffix(t), opName, ""); !errors.Is(err, api.ErrConflict) {
|
||||
t.Fatalf("InsertOperator on a taken username = %v, want ErrConflict", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The setup wizard's MC-bind path establishes THE Owner, so it writes the same
|
||||
|
||||
Reference in new issue
Block a user