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:
Lemon-miaow committed 2026-09-23 07:48:54 +08:00
1 parent f6dbfd3625
commit 55d515d41f
6 files changed
+173 -27

No files matched your search

+47 -8
View File
@@ -19,14 +19,15 @@ import (
// terminal. The design is passwordless: accounts carry no credential, and the
// Owner completes first-login through the setup-token web flow.
type fakeOwnerStore struct {
upserts []upsertCall
inserts []upsertCall
settings map[string][]byte
audits []api.AuditEntry
tokens []setupTokenCall
redeems []redeemCall
users map[string]*api.StaffUser // keyed by username
admins bool // AdminExists answer
upserts []upsertCall
inserts []upsertCall
settings map[string][]byte
audits []api.AuditEntry
tokens []setupTokenCall
redeems []redeemCall
users map[string]*api.StaffUser // keyed by username
admins bool // AdminExists answer
ownerSeat string // OwnerUsername answer: the occupied seat, "" when none
// CompleteOwnerSetup's success result. redeemUserID defaults to the fresh id
// the caller passes (the unlinked-UUID case) when left empty.
@@ -40,6 +41,7 @@ type fakeOwnerStore struct {
auditErr error
userErr error // non-not-found error from UserByUsername
adminErr error
seatErr error
redeemErr error
createTokenErr error
}
@@ -80,6 +82,15 @@ func (f *fakeOwnerStore) UserByUsername(_ context.Context, username string) (*ap
return nil, api.ErrNotFound
}
// OwnerUsername reports the single active Owner seat. Tests set ownerSeat; the
// zero value models a fresh install where bootstrap is free to mint.
func (f *fakeOwnerStore) OwnerUsername(_ context.Context) (string, error) {
if f.seatErr != nil {
return "", f.seatErr
}
return f.ownerSeat, nil
}
func (f *fakeOwnerStore) UpsertOwner(_ context.Context, id, username, email string) error {
if f.upsertErr != nil {
return f.upsertErr
@@ -201,6 +212,34 @@ func TestProvisionOwner(t *testing.T) {
}
})
t.Run("an occupied seat refuses any other username", func(t *testing.T) {
// The seat is the single owner row: upserting a fresh name would take the
// insert arm and mint a SECOND owner, while the existing seat — possibly the
// compromised account this reset was meant to replace — stays live, and no
// supported path can delete an owner row.
f := &fakeOwnerStore{ownerSeat: "seat-holder"}
err := provisionOwner(ctx, f, "someone-else", "")
if !errors.Is(err, api.ErrConflict) {
t.Fatalf("error = %v, want it to wrap api.ErrConflict so the TUI routes back to the form", err)
}
if !strings.Contains(err.Error(), `"seat-holder"`) {
t.Errorf("error = %q, want it to name the occupied seat", err)
}
if len(f.upserts) != 0 {
t.Errorf("want no write against an occupied seat, got %d", len(f.upserts))
}
})
t.Run("the occupied seat's own username still resets", func(t *testing.T) {
f := &fakeOwnerStore{ownerSeat: "seat-holder"}
if err := provisionOwner(ctx, f, "seat-holder", "[email protected]"); err != nil {
t.Fatalf("provisionOwner(reset): %v", err)
}
if len(f.upserts) != 1 || f.upserts[0].username != "seat-holder" || f.upserts[0].email != "[email protected]" {
t.Fatalf("want 1 reset upsert for the seat, got %+v", f.upserts)
}
})
t.Run("propagates a store error", func(t *testing.T) {
f := &fakeOwnerStore{upsertErr: errors.New("boom")}
if err := provisionOwner(ctx, f, "owner", ""); err == nil {