feat(api): let in-game staff manage any server without claiming it
The web face has always granted staff the run of the fleet (isOwnerOrAdmin passes an admin for stop/command/console/access on any node), but the internal face explicitly had "no admin tier": a linked administrator in game could only wake servers they owned or that autostartPolicy permitted. The only way to manage another player's (or an unclaimed) server from inside the game was to claim it — seizing ownership and burning the admin's own quota. Give authorizeWakeByUUID the admin tier on the same trust anchor the op-login approve already uses: verified online-mode UUID -> account link -> stored role. A linked staff member now wakes ANY node under any policy (so `/felis go` works fleet-wide without claiming); the owner bypass and the policy gates are unchanged, and an unlinked UUID still fails safe. Centralize the staff-role rule while at it: staffRole(role) in auth.go (admin, plus owner as its superset) now backs Principal.IsAdmin, the session ViaAdminAccess grading, the op-login approve gate and the new wake tier. That also fixes a real hole in the approve gate, which required role=admin exactly: an Owner manually promoted to role='owner' per migration 0011's upgrade note would have been refused by their own in-game approval door. The lobby menu still renders "Claim & Start" on ownerless tiles — claiming becomes optional for staff rather than the only entry — so the velocity plugin needs no change.
This commit is contained in:
6 files changed
+77
-23
No files matched your search
@@ -329,9 +329,10 @@ func TestOpLoginFinishUniform(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestOpLoginApproveGate pins the in-game approval gate: only a linked role=admin UUID
|
||||
// may vouch (all refusals share one 403 not_admin), a missing/no-longer-pending request
|
||||
// is 404, and a bare request without an approver UUID is 400.
|
||||
// TestOpLoginApproveGate pins the in-game approval gate: only a linked staff UUID
|
||||
// (role admin or owner) may vouch (all refusals share one 403 not_admin), a
|
||||
// missing/no-longer-pending request is 404, and a bare request without an
|
||||
// approver UUID is 400.
|
||||
func TestOpLoginApproveGate(t *testing.T) {
|
||||
plantPending := func(repo *fakeRepo) string {
|
||||
repo.opLogins["r1"] = &fakeOpLogin{
|
||||
@@ -362,6 +363,18 @@ func TestOpLoginApproveGate(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a linked owner-role approver vouches too", func(t *testing.T) {
|
||||
// The owner role is a superset of admin (auth.go staffRole), so a manually
|
||||
// promoted Owner (migration 0011) must pass the in-game approve gate.
|
||||
api, repo, _ := seedOpLoginAPI(t)
|
||||
repo.staff["boss"] = &StaffUser{ID: "b1", Username: "boss", Role: "owner"}
|
||||
repo.links["cccccccc-cccc-cccc-cccc-cccccccccccc"] = "b1"
|
||||
id := plantPending(repo)
|
||||
if w := approveOp(api.InternalHandler(), id, "cccccccc-cccc-cccc-cccc-cccccccccccc"); w.Code != http.StatusOK {
|
||||
t.Fatalf("owner-role approver: code = %d body %s, want 200", w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing approver_uuid -> 400", func(t *testing.T) {
|
||||
api, repo, _ := seedOpLoginAPI(t)
|
||||
id := plantPending(repo)
|
||||
|
||||
Reference in new issue
Block a user