Files
teamspeak-music-bot/docs/superpowers/specs/2026-05-30-account-permissions-design.md
T
saopig1andClaude Opus 4.8 f09a589940 docs(spec): fine-grained account permissions design (#79-E)
Capability flags (player.control/player.queue/bot.manage/platform.auth/quality) + per-member bot allow-list, layered under the existing member role; admin is super-user. Backend-enforced via requirePermission/requireBotAccess; existing members backfilled to full on upgrade, new members get a basic tier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:44:24 +08:00

8.7 KiB
Raw Blame History

Fine-grained account permissions — design

Issue: #79 (item E — the maintainer's permission-management idea) Date: 2026-05-30 Status: Approved (brainstorm), pending implementation plan

Scope

Issue #79 bundles five things. This spec covers only item E: allow an admin to grant each non-admin (member) account a set of capabilities and a list of bots they may control. The other items are handled in separate PRs and are out of scope here:

  • #1 Guest mode (login-less playback)
  • #2 Dedicated-link hides other bots (subsumed conceptually by E's bot allow-list, but the link-specific UX is separate)
  • #3 Auto-pause when channel empty
  • #4 Dedicated link loses bot binding on refresh (a bug)

Problem

Today the bot has a coarse two-role system: admin | member (single role column, read live per request). requireAdmin gates only /api/users and /api/audit. Every other action — create/edit/delete bots, start/stop, all playback & queue control, set platform login cookies, set audio quality — is open to any logged-in member, on every bot. Admins want to delegate limited control to members without handing them full power.

Decisions (from brainstorm)

  1. Model = capability flags + per-member bot allow-list (not a per-bot×per-action matrix, not role templates).
  2. Defaults: on upgrade, existing members are backfilled with full capabilities + all bots (no behavior change); newly-created members get a basic tier.
  3. Bot allow-list semantics: an explicit "all bots" toggle OR a specific list; empty list = no bots controllable. Members cannot see bots outside their allow-list (hidden, not merely disabled).
  4. Capability set (5 toggles) — see below; basic tier = playback + queue + all bots.
  5. Admin is a super-user (bypasses all checks). The last admin cannot be demoted (existing invariant preserved). Permission grants/revokes are written to the existing audit log.

Capability taxonomy

Capability token Covers Scope
player.control play/pause/resume/next/prev/stop/seek/volume/mode per-bot (allow-list)
player.queue search-add / clear / remove / play-at / playlist / album / play-song per-bot (allow-list)
bot.manage create / edit / delete / start / stop / avatar / profile / idle settings global (create) + per-bot (operate a specific bot)
platform.auth set NetEase/QQ/Bilibili cookie, QR, SMS global (shared credentials)
quality set audio quality per platform global

Bot scope is independent of capabilities: a member with player.control can only exercise it on bots in their allow-list (or all, if the "all bots" flag is set). platform.auth and quality are global capabilities with no bot scope.

Basic tier (new members): { player.control, player.queue } + bots.all = true. A new member can play/queue on every bot but cannot manage bots, change credentials, or change quality.

Data model (SQLite, additive — follows existing CREATE TABLE IF NOT EXISTS pattern)

-- capability tokens + the "all bots" flag (stored as token 'bots.all')
CREATE TABLE IF NOT EXISTS user_permissions (
  userId     TEXT NOT NULL,
  permission TEXT NOT NULL,
  PRIMARY KEY (userId, permission),
  FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);

-- specific bot allow-list (only consulted when 'bots.all' is NOT present)
CREATE TABLE IF NOT EXISTS user_bot_access (
  userId TEXT NOT NULL,
  botId  TEXT NOT NULL,
  PRIMARY KEY (userId, botId),
  FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_user_bot_access_userId ON user_bot_access(userId);
  • Admins have no rows (they bypass). Only members are constrained.
  • bots.all present ⇒ all bots (incl. future ones). Absent ⇒ only user_bot_access rows; empty ⇒ none.
  • foreign_keys = ON and WAL are already enabled; cascade-on-user-delete works.
  • user_bot_access.botId references bot instance ids; when a bot is deleted, its access rows should be cleaned up (either an FK to the bot table if one exists, or an explicit cleanup in BotManager.removeBot / PermissionStore.pruneBot(botId)).

New PermissionStore in src/data/permissions.ts (mirrors createUserStore / createSessionStore: prepared statements + an interface). Methods: getCapabilities(userId), getBotAccess(userId) → 'all' | string[], setPermissions(userId, { capabilities, bots }), pruneBot(botId).

Backend enforcement (real 403 — not just hidden UI)

  • req.user widened to carry permissions: Set<string> and bot access. Loaded in requireAuth (one extra lookup, or a JOIN in the session query). The same {id,username,role,permissions,...} shape must be kept in sync in the three places it is built today: requireAuth.ts, session.ts requireAuthInline, and the WS upgrade handler in server.ts (WS only needs it if a push action becomes gated). Because it's read live, permission changes take effect immediately (no re-login).
  • requirePermission(cap) middleware (new, mirrors requireAdmin.ts): 401 if no user; allow if role === 'admin' or permissions.has(cap); else 403.
  • requireBotAccess helper: allow if admin or bots.all or botId ∈ access list; else 403. Mounted on the player router's existing /:botId choke-point (src/web/api/player.ts) and on each :id route in src/web/api/bot.ts (start/stop/edit/delete/avatar/profile).
  • Route → capability mapping:
    • /api/player/:botId/* playback actions → player.control (+ requireBotAccess)
    • /api/player/:botId/* queue actions → player.queue (+ requireBotAccess)
    • /api/bot create, /api/bot/:id edit/delete, /api/bot/:id/start|stop|avatar|profile, /api/bot/settings → bot.manage (+ requireBotAccess for the :id ones)
    • /api/auth/* (cookie/QR/SMS) → platform.auth
    • /api/music/quality POST → quality
  • GET /api/bot filters its result to the caller's allowed bots for members (admins see all). This is what "hides" disallowed bots in the UI.

Management API (admin-only, added to the existing users router)

  • GET /api/users/:id/permissions → { capabilities: string[], bots: 'all' | string[] }
  • PUT /api/users/:id/permissions → body { capabilities, bots }; validates tokens against the known set and botIds against existing bots; writes audit user.permissions_changed.
  • GET /api/session/me is extended to include the current user's { capabilities, bots } so the frontend can gate UI. (admins report effectively-all.)

Frontend

  • useSession extends User with capabilities + bot scope and exposes can(cap) and canControlBot(botId) helpers.
  • Navbar bot selector filters store.bots to controllable bots (others hidden); activeBot fallback and fetchBots default only ever land on an allowed bot.
  • Player / Settings hide controls and whole sections a member lacks: platform login, audio quality, and bot create/edit/delete are hidden without the matching capability; playback/queue buttons hidden without player.control / player.queue.
  • Admin permission editor: in the Settings → User Management list, each member row gets a "权限" editor — capability checkboxes + a bot allow-list with an "全部机器人" toggle. Saving calls PUT /api/users/:id/permissions.

Defaults & migration

  • New tables created idempotently in initTables.
  • One-time backfill (guarded so it runs once): every existing member gets all five capabilities + bots.all. Admins are skipped (they bypass). This preserves current behavior for existing members on upgrade.
  • New member default (POST /api/users with role member): capabilities { player.control, player.queue } + bots.all (basic tier).
  • Pre-existing accounts default to role = 'admin' per the current schema — those are super-users and unaffected.

Testing (TDD)

  • PermissionStore unit tests (set/get capabilities + bot access; 'all' vs list vs empty; pruneBot).
  • requirePermission / requireBotAccess middleware tests (admin bypass; has/lacks cap → 200/403; bot in/out of allow-list; bots.all).
  • API tests: member without cap → 403; with cap → 200; bot not allowed → 403/hidden; GET /api/bot filtered for members, full for admin; PUT .../permissions validates
    • audits.
  • Migration test: existing members backfilled to full + bots.all; new member gets basic tier.

Non-goals

  • No per-bot×per-capability matrix, no custom role templates (YAGNI).
  • Guest mode, dedicated-link UX, auto-pause, and the refresh bug (#1–#4) are separate.
  • No change to the admin/member role concept itself; this layers capabilities under the existing member role.