feat(permissions): add GET /permissions/can check endpoint - #533
Conversation
|
@claude-review |
|
Claude finished @zaira-bibi's task in 1m 25s —— View job Claude Code Review
Reviewed the full diff against Correctness ✅
Suggestions (non-blocking)Low — non-deterministic result order. .distinct()
.order_by(col(RolePermission.permission))Low — two round-trips on Test coverage note (optional)The cascade test covers the parent→child direction (a Overall: LGTM. 👍 Correct, tested, documented, and non-breaking. |
Milestone 1's last leg: login activity is visible in-app. A client component fetches GET /api/v1/analytics/login-activity once, zero-fills the sparse (zero-day-omitting) response into a continuous 30-day series, and renders it as a hand-rolled SVG bar chart plus two stat tiles (total logins, busiest day) — all derived from the same windowed series so the headline numbers always match the bars. The nav entry is gated on the analytics.read permission (surfaced on /user/me by #533), with a 403 in-page state for defense in depth. No new dependencies: BarChart and StatCard are added to the in-repo shadcn-style components/ui set (themed SVG, dark-mode-native). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8ee6696 to
6aa4eb3
Compare
d71551f to
065a006
Compare
The frontend needs to gate navigation on specific permissions (e.g. an analytics dashboard on analytics.read), but the permission engine has no admin bypass and the frontend only knew is_admin — the wrong signal. Expose the caller's effective GLOBAL-scope permissions so the UI can gate precisely. Add get_user_permissions(), the all-distinct-names sibling of can(): it reuses the same active-assignment selection (honouring the objectless-ancestor cascade), so it inherits identical user scoping and soft-delete semantics, and is re-exported from the sparkth.lib.permissions façade. /user/me computes it at GLOBAL scope alongside is_admin and attaches it without mutating the ORM object. The User schema's `permissions` defaults to [] so responses that return the ORM user directly (e.g. register) stay non-privileged — only /user/me computes the real value. Backend slice of the analytics-dashboard work (issue #487); the dashboard UI that consumes this field lands in a separate, stacked PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review feedback: rather than returning the caller's full permission list on
/user/me for the client to check with .includes(), expose a single-permission
check — GET /api/v1/permissions/can?permission=<name>&scope=<kind> ->
{"allowed": bool} — backed directly by the engine's can(). This keeps the
authorization decision on the server (one source of truth, incl. the scope
cascade), extends naturally to object-scoped checks, and discloses only the
answer asked for. It is a UI-gating convenience, not a security boundary:
endpoints still enforce their own permissions and return 403.
Removes get_user_permissions and the permissions field on the User schema/
/user/me; unknown permission or scope names map to 422 via the exception
registry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
065a006 to
71ac63d
Compare
Part of: #487 — Analytics are not visible anywhere in the product UI
What
Add a server-side single-permission check endpoint so a client (e.g. the web UI) can ask whether the authenticated user holds a specific permission and gate UI precisely — there is no admin bypass, so
is_adminalone is the wrong signal.Changes
GET /api/v1/permissions/can?permission=<name>&scope=<kind>&scope_object_id=<id?>→{"allowed": bool}, auth-only, backed directly by the engine'scan(); unknown permission/scope names map to422via the exception registrypermissionslist on/user/me+ aget_user_permissionshelper) in favour of the check endpoint, per review feedback — keeps the authorization decision on the server (one source of truth, incl. the scope cascade), extends to object-scoped checks, and discloses only the answer asked forHow to Test
uv run pytest tests/api/v1/test_permission_check.py tests/api/v1/test_user.py tests/permissions/ -vmake mypyandmake lint.backend→ clean.analytics.readat the global scope(
make cli -- roles assign-role <user> <role>), authenticate, thenGET /api/v1/permissions/can?permission=analytics.read→{"allowed": true};a user without the grant →
{"allowed": false}.?permission=does.not.exist) or scope (&scope=nope) →422; unauthenticated →403.Notes
/user/mepermissions-list design (reworked per TL feedback). No migration; no new dependencies.This PR description was written with the assistance of an LLM (Claude).