Skip to content

Fix API-key scope wiring: polls UI, dedicated messages scope, no key self-minting#79

Merged
SiteRelEnby merged 1 commit into
mainfrom
fix-api-key-scopes
May 25, 2026
Merged

Fix API-key scope wiring: polls UI, dedicated messages scope, no key self-minting#79
SiteRelEnby merged 1 commit into
mainfrom
fix-api-key-scopes

Conversation

@SiteRelEnby
Copy link
Copy Markdown
Contributor

Summary

Found while load-testing with an API key: poll creation always 403'd. The backend supports polls:*, but the key-creation UI never offered the scope, so a key could never be granted it. Fixed that, then swept the rest of the API-key scope system.

Changes

  • Polls scope in the key UI. Add polls to the scope picker so keys can actually be granted polls:read/write/delete (backend already enforced them).
  • Dedicated messages:* scope. Board messages were reusing members:*. Posting/deleting messages is a distinct capability from editing members, so they get their own messages:read/write/delete (router mount + the 8 endpoints + _VALID_SCOPES + the UI, updated together). Least privilege: a member-management key no longer implies message access.
  • Key management is session/JWT-only. create / list / revoke key endpoints now refuse API-key auth. A key minting another key is privilege escalation: a leaked read-only key could create a write/delete key and outlive its own revocation. Matches the existing account + async-export posture.
  • Remove dead _ALL_SCOPES in auth.dependencies - unused and drifted out of sync; _VALID_SCOPES in auth.py is the real grantable set.

Sweep result (no other gaps)

  • Every require_scope() the API enforces (per-endpoint and at router mount) is now in _VALID_SCOPES. A new static test asserts this so the polls/messages class of bug can't recur.
  • Admin scopes are properly enforced via get_admin_user / get_admin_write_user.
  • Import/export gated at the router mount; account / async-export / webhooks correctly refuse or don't need key scopes.

Tests

  • Static guard: every required scope is grantable.
  • An API key cannot create / list / revoke keys.
  • polls:write / messages:write are grantable and enforced (a key lacking them is refused).
  • 55 passed across api-keys + scope-coverage + messages; ruff / tsc / eslint clean.

Compatibility

API-key scoping only; session/JWT (the web app) bypass scopes, so UI behavior is unchanged. The messages:* split is a behavior change for any existing key that relied on members:* to reach messages (messages are new and pre-1.0, so likely none).

Migrations

None.

Polls were unreachable via API keys: the backend supports polls:* but the
key-creation UI never offered the scope, so keys couldn't be granted it
(403 on every poll write). Add polls to the scope picker.

Sweep of the rest of the scope system:
- Give board messages their own messages:read/write/delete instead of
  reusing members:* - posting/deleting messages is a distinct capability
  from editing members and shouldn't be granted via it. Router mount +
  endpoints + _VALID_SCOPES + the key UI updated together.
- Refuse API-key auth on the key-management endpoints (create / list /
  revoke). A key minting another key is privilege escalation: a leaked
  read-only key could create a write/delete key and outlive its own
  revocation. Key management is session/JWT-only now, matching the
  account + async-export posture.
- Remove the dead, drifted _ALL_SCOPES in auth.dependencies (unused;
  _VALID_SCOPES in auth.py is the real list).

Tests: a static guard that every require_scope() the API enforces is in
_VALID_SCOPES (would have caught polls/messages), plus coverage that an
API key cannot create/list/revoke keys.
@SiteRelEnby SiteRelEnby merged commit 9023c0f into main May 25, 2026
4 checks passed
@SiteRelEnby SiteRelEnby deleted the fix-api-key-scopes branch May 25, 2026 20:49
@SiteRelEnby SiteRelEnby mentioned this pull request May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant