Skip to content

Staff Tooling Update & Bug Fixes

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 05 Aug 07:16
· 62 commits to production since this release
1709b0c

Changed

  • Every Differs[T] config key (DB tokens, site URLs, etc.) previously
    required both a staging and a prod value to be set regardless of
    which environment a given box actually runs — a current-env: prod box
    was rejected at startup for a missing staging value it would never
    read, and vice versa. ValidateDiffers now only requires whichever value
    Parse() will actually resolve for CurrentEnv (prod needs prod,
    staging needs staging; beta/dev still accept either their own
    value or a staging fallback, 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-time grant/revoke subcommands are unchanged and
    still work; both paths share the same rank check and perms.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". edit is
    registered as the first subcommand of both commands, since Discord lists
    them in registration order and never lets the parent command
    (/staffroles on its own) be invoked at all.

Fixed

  • DELETE /teams/{tid}/members/{mid} had its last-owner safety check
    inverted (introduced by the kittycat→internal perms package 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.
    SyncCommands now 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/team auth types were never registered as OpenAPI security
    schemes (only User/Bot were, via docs.AddSecuritySchema in
    main.go) even though Authorize() has always fully supported them —
    every one of the 41 operations requiring server or team auth
    (PUT /bots, PUT /servers, both PATCH .../settings endpoints,
    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's APIPage, which throws
    Cannot read properties of undefined (reading 'type')). Registered both
    (docs.AddSecuritySchema("server", ...) / ("team", ...), lowercase to
    match AuthTypeMap'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
    OnGuildsReady instead of right on startup — that fix only addressed the
    timing, not the actual cause: Popplio runs sharded (OpenShardManager),
    and Discord.SetPresence only ever checks disgo's single-gateway field
    (populated by OpenGateway, not OpenShardManager), so it returns
    ErrNoGateway unconditionally on a sharded bot regardless of readiness.
    OnGuildsReady also 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.Authorize reads PERMISSION_CHECK_KEY out of
    the route's ExtData unconditionally, but the synthetic uapi.Route{}
    this endpoint builds to call it never set ExtData at 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 in api_sessions) ever got a real
    response (401). Now sets a no-op PermissionCheck (NeededPermission
    always returns nil), 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_borealis staff permission. Borealis was removed from the platform
    during the port (arcadia/CONFORMANCE.md D11a — the arcadia.borealis_url
    config key, the client and the Approve call to it are all long gone), so
    the permission has gated nothing since and only added a line to
    /permissions and a row to every permission picker. exp/rewrite/flatperms.sql
    now lists the old borealis.* in retired_perm (dropped on purpose)
    instead of mapping it onto use_borealis, and
    exp/rewrite/remove_borealis_perm.sql strips it from
    staff_positions.perms, staff_members.perm_overrides and
    staff_disciplinary_types.perm_limits for 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, so use_borealis would 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 through arcadia.owners
    (perms.ErrBotAccount). Previously nothing stopped one: StaffResync
    walks every member of the staff server and creates a staff_members row
    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.StaffGrants carries a BotAccount flag, joined in from
      dovewing's user cache by LoadStaff at no extra cost, and Resolve()
      returns nothing and Rank() returns NoRank when 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.RejectBotAccount resolves through dovewing all the way
      to Discord if the account has never been seen, and fails closed if it
      cannot tell. StaffResync now 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's editMember and the staff bot's
      /staffperms grant/revoke/edit refuse a bot target outright.