Staff Tooling Update & Bug Fixes
·
62 commits
to production
since this release
Changed
- Every
Differs[T]config key (DB tokens, site URLs, etc.) previously
required both astagingand aprodvalue to be set regardless of
which environment a given box actually runs — acurrent-env: prodbox
was rejected at startup for a missingstagingvalue it would never
read, and vice versa.ValidateDiffersnow only requires whichever value
Parse()will actually resolve forCurrentEnv(prodneedsprod,
stagingneedsstaging;beta/devstill accept either their own
value or astagingfallback, unchanged), so a config file only needs to
fill in what the box it's deployed to actually uses. - Staff roles and permissions can now be managed interactively from the staff
bot:/staffroles edit [role]and/staffperms edit <user>open a select
menu editor (arcadia/bot/permeditor.go) with a role picker, a category
picker and a multi-select of that category's permissions, preselected to
what the role or member currently holds — ticking grants, unticking revokes,
and everything outside the open category is carried through untouched.
Dangerous permissions are marked⚠️ and ones the caller cannot manage 🔒.
The existing one-at-a-timegrant/revokesubcommands are unchanged and
still work; both paths share the same rank check andperms.CheckPatch
rule, and the editor re-checks both at the moment of the write rather than
only when it opened, since a session lives for ten minutes. Every render
reloads the target from the database, so two people editing the same role
see each other's changes instead of saving a stale picture over them.
Alongside the menus are buttons: "Grant all"/"Revoke all" for the open
category (which leave permissions the caller cannot manage exactly as they
are, so one locked permission doesn't make the button useless), "Pick
another role" to switch targets without closing, and "Close".editis
registered as the first subcommand of both commands, since Discord lists
them in registration order and never lets the parent command
(/staffroleson its own) be invoked at all.
Fixed
DELETE /teams/{tid}/members/{mid}had its last-owner safety check
inverted (introduced by the kittycat→internalpermspackage refactor):
it fired when the member being removed was not an owner instead of when
they were, so removing any regular member from a team with only one owner
(the common case) 400'd with "There needs to be one other global owner
before you can remove yourself from owner" — while actually removing the
team's last real owner sailed through with no check at all, the exact
case this was meant to prevent. Condition un-inverted.- Every staff bot slash command appeared twice in every server. The bot
registers its commands per guild (arcadia/bot.SyncCommands), but the
application still carried global registrations of the same commands from an
earlier deployment, and Discord lists a global command alongside a guild
command of the same name rather than letting the guild copy take its place.
SyncCommandsnow finishes by deleting the global registration of any
command it registers per guild (pruneGlobalCommands), so the duplicates
clear themselves on the next sync (startup, or/register). Global
commands whose names the bot does not register are left alone and only
logged as a warning, since they belong to something else sharing the
application. - The
server/teamauth types were never registered as OpenAPI security
schemes (onlyUser/Botwere, viadocs.AddSecuritySchemain
main.go) even thoughAuthorize()has always fully supported them —
every one of the 41 operations requiringserverorteamauth
(PUT /bots,PUT /servers, bothPATCH .../settingsendpoints,
reviews, sessions, etc.) referenced a security scheme name absent from
components.securitySchemes. Harmless to the API itself, but any tool
that resolves the requirement against registered schemes crashes outright
on the unresolved reference — including the docs site's OpenAPI reference
pages (fumadocs-openapi'sAPIPage, which throws
Cannot read properties of undefined (reading 'type')). Registered both
(docs.AddSecuritySchema("server", ...)/("team", ...), lowercase to
matchAuthTypeMap's self-mapping for these two types). - Presence still never actually got set even after 1.0.0's fix, now logging
error while setting presence err="no gateway configured"from inside
OnGuildsReadyinstead of right on startup — that fix only addressed the
timing, not the actual cause: Popplio runs sharded (OpenShardManager),
andDiscord.SetPresenceonly ever checks disgo's single-gateway field
(populated byOpenGateway, notOpenShardManager), so it returns
ErrNoGatewayunconditionally on a sharded bot regardless of readiness.
OnGuildsReadyalso fires once per shard, not once globally. Now uses
Discord.SetPresenceForShard(ctx, event.ShardID(), ...)instead. POST /auth/test("Test Auth") 500ed on every call that reached an actual
authorization check —api.AuthorizereadsPERMISSION_CHECK_KEYout of
the route'sExtDataunconditionally, but the syntheticuapi.Route{}
this endpoint builds to call it never setExtDataat all, so any request
with a syntactically valid token failed with a 500
(permissionCheck not found in route.ExtData) instead of returning
whether the token is actually valid. Only requests with a token that
failed even earlier (nonexistent inapi_sessions) ever got a real
response (401). Now sets a no-opPermissionCheck(NeededPermission
always returnsnil), since this endpoint has no permission model of its
own to enforce — it's purely "is this token valid for this target."
Removed
- The
use_borealisstaff permission. Borealis was removed from the platform
during the port (arcadia/CONFORMANCE.mdD11a — thearcadia.borealis_url
config key, the client and theApprovecall to it are all long gone), so
the permission has gated nothing since and only added a line to
/permissionsand a row to every permission picker.exp/rewrite/flatperms.sql
now lists the oldborealis.*inretired_perm(dropped on purpose)
instead of mapping it ontouse_borealis, and
exp/rewrite/remove_borealis_perm.sqlstrips it from
staff_positions.perms,staff_members.perm_overridesand
staff_disciplinary_types.perm_limitsfor databases the old migration
already ran against. That cleanup is needed rather than cosmetic: the
permission model deliberately keeps names it does not declare, since they
may belong to another service, souse_borealiswould otherwise sit in
those columns for good and show up under "Other services".
Security
- Bot accounts can no longer hold staff permissions at all — not through a
staff role, not through a direct grant, and not througharcadia.owners
(perms.ErrBotAccount). Previously nothing stopped one:StaffResync
walks every member of the staff server and creates astaff_membersrow
for anyone holding a position's Discord role, and it never looked at
whether that member was a bot, so giving a bot a staff role in Discord
handed it that role's permissions — including through the panel session
and RPC paths, which only ever asked what the row said. A bot is a token
that can be handed to another program, which is exactly what the staff
model's accountability assumes cannot happen, and nothing needs it: the
staff bot and the panel both act under a staff member's identity, never
their own. Enforced on both sides:- Reads:
perms.StaffGrantscarries aBotAccountflag, joined in from
dovewing's user cache byLoadStaffat no extra cost, andResolve()
returns nothing andRank()returnsNoRankwhen it is set. The panel's
session check (impls.CheckAuthInsecure), its login
(ops_authorize.go) and its member view (impls.GetStaffMember, whose
additory disciplinaries could otherwise add permissions on top of an
empty set) all apply the same rule. These paths stay database-only, so
they keep working when Discord does not. - Writes:
perms.RejectBotAccountresolves through dovewing all the way
to Discord if the account has never been seen, and fails closed if it
cannot tell.StaffResyncnow skips bot members entirely, which also
means an existing bot's staff row is cleaned up by the same pass that
handles members who left; the panel'seditMemberand the staff bot's
/staffperms grant/revoke/editrefuse a bot target outright.
- Reads: