feat(auth): granular API token scopes, team binding, and a token modal #87

Merged
kreativmonkey merged 2 commits from feat/api-token-scoping into main 2026-06-14 09:42:18 +02:00

Closes #81

Macht das API-Token-Scoping granular, erzwingt die Team-Beschränkung und ersetzt das Anlege-UI durch ein Modal.

⚠️ Breaking change

Die alten ai:*-Scopes wurden entfernt. Bestehende AI-Tokens müssen neu erstellt werden (die AI-/MCP-Endpoints verlangen jetzt die neuen Scopes).

Scope-Modell (internal/auth/scopes.go)

read, write, write:draft, write:schedule, delete, delete:draft, delete:schedulewrite ist Superset von write:draft/write:schedule, delete von delete:draft/delete:schedule, read unabhängig.

Bestätigte Entscheidungen:

  • Leere Scope-Liste = voller Zugriff (rückwärtskompatibel). Nur Tokens mit explizit gesetzten Scopes werden eingeschränkt. Browser-/OIDC-Sessions sind ausgenommen.
  • ai: entfernt*, AI-/MCP-Endpoints auf die neuen Scopes gemappt.

Sicherheits-Fix: Team-Bindung

api_tokens.team_id wurde bisher zwar geladen, aber nie durchgesetzt — ein „team-scoped" Token konnte auf alle Teams des Users zugreifen. Jetzt verweigert PrincipalHasTeamAccess jeden fremden Team-Zugriff (auch für Admins), wenn der Token an ein Team gebunden ist.

Durchsetzung

  • Read-Endpoints (Posts, Analytics, Media, Accounts, ai-context) verlangen read.
  • Create/Update/Delete/Cancel-Post wählen write:draft/write:schedule bzw. delete:draft/delete:schedule anhand des Post-Status (in-Handler).
  • MCP-Gate verlangt read; Write-/Delete-Tools prüfen ihren Scope.
  • Unbekannte Scopes → 400 unknown_scope.

Store

api_tokens bekommt eine description-Spalte (sqlite + postgres, create table + alter … if not exists-Migration). CreateUserAPIToken nimmt eine Beschreibung; das Listing liefert nun scopes + description.

Frontend

Neues ApiTokenManager: „+ New Token" öffnet ein Modal (Name, Beschreibung, Team-Auswahl, gruppierte Scope-Checkboxes, Ablaufdatum). Der erzeugte Token wird einmalig in einem Reveal-Modal angezeigt und per Klick in die Zwischenablage kopiert. Die Liste zeigt Beschreibung, Team und Scope-Chips.

Tests

  • auth: Scope-Hierarchie, PrincipalAllows (unscoped=full, OIDC-exempt), Team-Bindung
  • store (sqlite + postgres): description/scope/team-Roundtrip, neue Spalte
  • api: Create-Post ohne write:schedule → 403 / mit → 201, team-bound Token auf fremdem Team → 403, Read-Gate
  • mcp: Gate (read) + Write-Tool-Scope
  • Playwright-e2e: Token-Anlage über das Modal inkl. Reveal

go build/vet/test ./..., make test-postgres, pnpm build + lint (0 errors), Locales key-identisch — alles grün.

🤖 Generated with Claude Code

Closes #81 Macht das API-Token-Scoping granular, erzwingt die Team-Beschränkung und ersetzt das Anlege-UI durch ein Modal. ## ⚠️ Breaking change Die alten `ai:*`-Scopes wurden entfernt. Bestehende AI-Tokens müssen neu erstellt werden (die AI-/MCP-Endpoints verlangen jetzt die neuen Scopes). ## Scope-Modell (`internal/auth/scopes.go`) `read`, `write`, `write:draft`, `write:schedule`, `delete`, `delete:draft`, `delete:schedule` — `write` ist Superset von `write:draft`/`write:schedule`, `delete` von `delete:draft`/`delete:schedule`, `read` unabhängig. **Bestätigte Entscheidungen:** - **Leere Scope-Liste = voller Zugriff** (rückwärtskompatibel). Nur Tokens mit explizit gesetzten Scopes werden eingeschränkt. Browser-/OIDC-Sessions sind ausgenommen. - **ai:* entfernt**, AI-/MCP-Endpoints auf die neuen Scopes gemappt. ## Sicherheits-Fix: Team-Bindung `api_tokens.team_id` wurde bisher zwar geladen, aber **nie durchgesetzt** — ein „team-scoped" Token konnte auf alle Teams des Users zugreifen. Jetzt verweigert `PrincipalHasTeamAccess` jeden fremden Team-Zugriff (auch für Admins), wenn der Token an ein Team gebunden ist. ## Durchsetzung - Read-Endpoints (Posts, Analytics, Media, Accounts, ai-context) verlangen `read`. - Create/Update/Delete/Cancel-Post wählen `write:draft`/`write:schedule` bzw. `delete:draft`/`delete:schedule` anhand des Post-Status (in-Handler). - MCP-Gate verlangt `read`; Write-/Delete-Tools prüfen ihren Scope. - Unbekannte Scopes → `400 unknown_scope`. ## Store `api_tokens` bekommt eine `description`-Spalte (sqlite + postgres, create table + `alter … if not exists`-Migration). `CreateUserAPIToken` nimmt eine Beschreibung; das Listing liefert nun `scopes` + `description`. ## Frontend Neues `ApiTokenManager`: **„+ New Token"** öffnet ein Modal (Name, Beschreibung, Team-Auswahl, gruppierte Scope-Checkboxes, Ablaufdatum). Der erzeugte Token wird einmalig in einem Reveal-Modal angezeigt und per Klick in die Zwischenablage kopiert. Die Liste zeigt Beschreibung, Team und Scope-Chips. ## Tests - `auth`: Scope-Hierarchie, `PrincipalAllows` (unscoped=full, OIDC-exempt), Team-Bindung - `store` (sqlite + postgres): description/scope/team-Roundtrip, neue Spalte - `api`: Create-Post ohne `write:schedule` → 403 / mit → 201, team-bound Token auf fremdem Team → 403, Read-Gate - `mcp`: Gate (read) + Write-Tool-Scope - Playwright-e2e: Token-Anlage über das Modal inkl. Reveal `go build/vet/test ./...`, `make test-postgres`, `pnpm build` + `lint` (0 errors), Locales key-identisch — alles grün. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(auth): granular API token scopes, team binding, and a token modal
Some checks failed
CI/CD Workflow / Test Backend (Go) (pull_request) Has been cancelled
CI/CD Workflow / E2E (Playwright + Go) (pull_request) Has been cancelled
CI/CD Workflow / Test Frontend (Node.js) (pull_request) Has been cancelled
CI/CD Workflow / Test Postgres Store (pull_request) Has been cancelled
CI/CD Workflow / Build and Publish Docker Image (pull_request) Has been cancelled
CI/CD Workflow / Trigger Dockhand Deployment (pull_request) Has been cancelled
Website CI / Build website (pull_request) Successful in 52s
1a188c151f
Reworks API token scoping per issue #81.

Backend:
- new scope vocabulary read / write{,:draft,:schedule} / delete{,:draft,:schedule}
  with a write/delete hierarchy (internal/auth/scopes.go); the AI-specific
  ai:* scopes are removed and the AI + MCP endpoints map onto the new ones
- empty scope list keeps full access (backward compatible); only tokens with an
  explicit scope list are restricted. Browser/OIDC sessions are exempt
- enforce the previously-ignored per-token team binding in PrincipalHasTeamAccess
  (a team-scoped token can no longer reach other teams, even as admin)
- gate read endpoints with read; create/update/delete/cancel post handlers pick
  write:draft/write:schedule or delete:draft/delete:schedule from the post state;
  MCP gate requires read and write/delete tools check their scope
- api_tokens gains a description column (sqlite + postgres, create + migration);
  CreateUserAPIToken takes a description and the listing returns scopes/description
- requests with an unknown scope are rejected (400 unknown_scope)

Frontend:
- new ApiTokenManager: a "+ New Token" modal (name, description, team, grouped
  scope checkboxes, expiry) replacing the inline form; the created token is shown
  once in a reveal modal with click-to-copy
- token list shows description, team, and scope chips

Tests: scope hierarchy + PrincipalAllows + team-binding (auth), description/scope
round-trip (sqlite + postgres), post create/team-binding enforcement and the read
gate (api), updated MCP gate + tool scope tests, and a Playwright e2e that creates
a token through the modal.

Closes #81

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Merge main into feat/api-token-scoping
All checks were successful
CI/CD Workflow / Test Backend (Go) (pull_request) Successful in 2m57s
CI/CD Workflow / Test Frontend (Node.js) (pull_request) Successful in 1m24s
CI/CD Workflow / Test Postgres Store (pull_request) Successful in 1m20s
CI/CD Workflow / E2E (Playwright + Go) (pull_request) Successful in 7m16s
CI/CD Workflow / Build and Publish Docker Image (pull_request) Has been skipped
CI/CD Workflow / Trigger Dockhand Deployment (pull_request) Has been skipped
bca976fd17
# Conflicts:
#	internal/store/postgres/postgres_test.go
#	internal/webui/dist/index.html
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
kreativmonkey/goloom!87
No description provided.