fix(auth): make the owner role real — provisioning, staff doors, panel guards
Found live while verifying the admin email-edit fix: the Owner account could
not load /api/v1/users at all. Root cause: migration 0011 adds the 'owner'
role and gates every user-administration route on it, but NOTHING ever wrote
it. break-glass (UpsertOwner), the setup MC-bind (CompleteOwnerSetup), and the
re-provision path all forced 'admin', so in a fresh install the entire
owner tier — list/create/edit/disable/delete users, quotas, sessions — was
unreachable. The role was a dead letter in the other direction too: staff
predicates that predate the role did not know it.
- UpsertOwner and CompleteOwnerSetup now write role='owner'; the username-
conflict arm re-asserts it, which is also the documented pre-0011 promotion
path ("re-provision via break-glass"). InsertOperator stays plain 'admin'.
- Staff doors learn the role: op-login start/finish admit the Owner; the
player email door refuses it like any staff account; the in-game approver
check already used staffRole.
- Reclaim protection: IsProtectedAdminLink (and the break-glass bootstrap
switch AdminExists) count admin OR owner — the Owner must never be displaced
by a Mojang-priority reclaim.
- Panel guards make migration 0011's claim true now that owner rows exist: an
owner can never be demoted, deleted, or disabled through the API (only the
local break-glass console resets the identity); username/email edits still
work.
Tests: pgint pins both provisioning paths, the protected-link predicate and
the reset/promote semantics; hermetic suites cover the owner-admitting staff
door, the owner-refusing player door, the three panel guards, and break-glass
attribution.
This commit is contained in:
16 files changed
+349
-80
No files matched your search
@@ -170,6 +170,44 @@ func TestOpLoginVertical(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpLoginOwnerAdmitted pins that the staff door admits the Owner (role=owner),
|
||||
// not just plain admins: the Owner is the primary op.console identity, so a
|
||||
// role check of "admin only" would strand it outside its own console.
|
||||
func TestOpLoginOwnerAdmitted(t *testing.T) {
|
||||
repo := newFakeRepo()
|
||||
repo.settings[LocalAuthEnabledKey] = []byte("true")
|
||||
repo.staff["owner"] = &StaffUser{
|
||||
ID: "o1", Username: "owner", Email: "[email protected]",
|
||||
Role: "owner", EmailVerified: true,
|
||||
}
|
||||
repo.links[opUUID] = "o1"
|
||||
mailer := &captureMailer{}
|
||||
api := newTestAPI(repo, newFakeCluster())
|
||||
api.Mailer = mailer
|
||||
eh := api.ExternalHandler()
|
||||
ih := api.InternalHandler()
|
||||
|
||||
w := startOp(eh, "[email protected]")
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Fatalf("owner start: code = %d, want 202 (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
reqID, _ := acctBody(t, w)["request_id"].(string)
|
||||
if reqID == "" || mailer.calls != 1 || len(repo.opLogins) != 1 {
|
||||
t.Fatalf("owner start must mint a request + mail a code: req=%q mails=%d rows=%d",
|
||||
reqID, mailer.calls, len(repo.opLogins))
|
||||
}
|
||||
if w := approveOp(ih, reqID, opUUID); w.Code != http.StatusOK {
|
||||
t.Fatalf("approve: code = %d (%s)", w.Code, w.Body.String())
|
||||
}
|
||||
w = finishOp(eh, reqID, mailer.code)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("owner finish: code = %d body %s, want 200", w.Code, w.Body.String())
|
||||
}
|
||||
if vb := acctBody(t, w); vb["role"] != "owner" {
|
||||
t.Fatalf("finish role = %v, want owner", vb["role"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpLoginStartNeutral pins the start-side anti-enumeration contract: op.console is
|
||||
// the STAFF door, so a non-admin account AND an unknown address both get a 202 carrying
|
||||
// a request_id + expires_at, mint/mail nothing, and still burn the per-recipient
|
||||
|
||||
Reference in new issue
Block a user