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
@@ -140,6 +140,18 @@ func (a *API) handlePatchUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Owner protection (migration 0011): the owner row is the one identity the
|
||||
// panel may never demote — only the local break-glass console resets it.
|
||||
// Username/email edits on it stay allowed. A failed detail read falls through;
|
||||
// UpdateUser then answers the real 404.
|
||||
if body.Role != nil && *body.Role != "owner" {
|
||||
if d, err := a.Repo.UserDetail(r.Context(), id); err == nil && d.Role == "owner" {
|
||||
writeError(w, r, newError(http.StatusForbidden, "forbidden",
|
||||
"the owner account's role cannot be changed from the panel"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if body.Username != nil {
|
||||
if err := validateUsername(*body.Username); err != nil {
|
||||
writeError(w, r, err)
|
||||
@@ -186,6 +198,14 @@ func (a *API) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Same owner protection as the role guard above: only break-glass retires the
|
||||
// owner identity. A failed detail read falls through to the real 404.
|
||||
if d, err := a.Repo.UserDetail(r.Context(), id); err == nil && d.Role == "owner" {
|
||||
writeError(w, r, newError(http.StatusForbidden, "forbidden",
|
||||
"the owner account cannot be deleted from the panel"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.Repo.DeleteUser(r.Context(), id, p.Email); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
writeError(w, r, newError(http.StatusNotFound, "not_found", "user not found"))
|
||||
@@ -222,6 +242,17 @@ func (a *API) handleDisableUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Owner protection (migration 0011): disabling locks the owner out and revokes
|
||||
// its sessions — effectively a demotion, so the panel refuses it; only
|
||||
// break-glass touches the owner identity. Re-enabling stays allowed.
|
||||
if body.Disabled {
|
||||
if d, err := a.Repo.UserDetail(r.Context(), id); err == nil && d.Role == "owner" {
|
||||
writeError(w, r, newError(http.StatusForbidden, "forbidden",
|
||||
"the owner account cannot be disabled from the panel"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := a.Repo.SetUserDisabled(r.Context(), id, body.Disabled); err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
writeError(w, r, newError(http.StatusNotFound, "not_found", "user not found"))
|
||||
|
||||
Reference in new issue
Block a user