docs: update permissions audit with re-audit findings
Re-audited after RBAC commit (34daa26). Key findings:
- permissions.py is dead code (no endpoint imports it)
- require_engineer_or_admin blocks team admins with viewer role
- 49 endpoints bypass get_current_active_user
- 3 critical issues still open (role field, XSS, secret key)
- Updated implementation plan with new Phase B items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,194 +1,151 @@
|
||||
# Permissions & RBAC Audit — Design Document
|
||||
|
||||
> **Date:** 2026-02-05
|
||||
> **Status:** Draft
|
||||
> **Status:** Updated after re-audit
|
||||
> **Scope:** Full-stack permissions audit across trees, steps, sessions, teams, and admin features
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
ResolutionFlow has a role-based access control system with three roles (`admin`, `engineer`, `viewer`), a `is_team_admin` boolean, and team-scoped resources. A comprehensive audit was performed across the entire codebase — frontend UX, backend architecture, and adversarial analysis — to assess the current state of permissions enforcement.
|
||||
ResolutionFlow has a role-based access control system with a role hierarchy (`super_admin` > `team_admin` > `engineer` > `viewer`), backed by `is_super_admin` and `is_team_admin` boolean flags on the User model, plus team-scoped resources.
|
||||
|
||||
**Audit methodology:** Three independent reviews were conducted:
|
||||
1. **Frontend UX audit** — every page, component, API client, store, and router
|
||||
2. **Backend architecture audit** — every endpoint, dependency, model, schema, and test
|
||||
3. **Devil's advocate critique** — security gaps, edge cases, challenged assumptions
|
||||
Two rounds of audit were performed:
|
||||
1. **Initial audit** — three independent reviews (UX, architecture, adversarial)
|
||||
2. **Re-audit** — after RBAC implementation commit (`34daa26`) added `is_super_admin`, `permissions.py`, `usePermissions.ts`, and updated endpoint permission checks
|
||||
|
||||
**Audit methodology:** Each round used three specialized reviewers:
|
||||
- Frontend UX audit — every page, component, API client, store, and router
|
||||
- Backend architecture audit — every endpoint, dependency, model, schema, and test
|
||||
- Gap analysis / devil's advocate — security gaps, edge cases, challenged assumptions
|
||||
|
||||
---
|
||||
|
||||
## Current State Summary
|
||||
## Current Architecture (Post-RBAC Commit)
|
||||
|
||||
### What Was Added in Commit `34daa26`
|
||||
|
||||
- `is_super_admin` boolean on User model (migration 010)
|
||||
- `backend/app/core/permissions.py` — centralized permission functions (role hierarchy, content guards)
|
||||
- `frontend/src/hooks/usePermissions.ts` — permission hook with role hierarchy
|
||||
- `require_admin` dependency now checks `is_super_admin` instead of `role == "admin"`
|
||||
- `require_engineer_or_admin` dependency blocks viewers from tree/step creation
|
||||
- `usePermissions` hook used in TreeLibraryPage (edit/create gating) and TreeEditorPage (access guard)
|
||||
- Role badge display in AppLayout header
|
||||
- `TreeListItem` type includes `team_id` for frontend permission checks
|
||||
|
||||
### What Works
|
||||
|
||||
- **Session ownership** is properly enforced — users can only see/modify their own sessions
|
||||
- **Folder ownership** is properly scoped to the creating user
|
||||
- **Tree CRUD** has reasonable backend permission checks (engineer+ to create, author/admin/team-admin to edit, admin-only delete)
|
||||
- **Tree CRUD** has backend permission checks (engineer+ to create, author/admin/team-admin to edit, super-admin-only delete)
|
||||
- **Step Library** enforces visibility levels (private/team/public) and ownership for edit/delete
|
||||
- **Category/Tag management** is properly gated to admin and team admin roles
|
||||
- **Invite code management** is admin-only
|
||||
- **Category/Tag management** is properly gated to super admin and team admin roles
|
||||
- **Invite code management** is super-admin-only
|
||||
- **Frontend gating (partial):** Create Tree button hidden from viewers, Edit button checks ownership/team/admin
|
||||
- **`is_super_admin` flag** prevents the old admin role escalation (flag has `server_default=false`)
|
||||
|
||||
### What's Broken
|
||||
### What's Still Broken
|
||||
|
||||
The permission system has critical vulnerabilities, significant gaps, and inconsistencies across resource types.
|
||||
The permission system still has critical vulnerabilities, a dead-code centralized module, and significant gaps.
|
||||
|
||||
---
|
||||
|
||||
## Findings by Severity
|
||||
## Findings — Combined Status Table
|
||||
|
||||
### CRITICAL
|
||||
### Legend
|
||||
- **FIXED** — Issue fully resolved
|
||||
- **PARTIAL** — Infrastructure exists but incomplete or not fully wired
|
||||
- **OPEN** — No change from original audit
|
||||
|
||||
#### 1. Self-Assignable Admin Role at Registration
|
||||
| # | Severity | Finding | Status | Evidence |
|
||||
|---|----------|---------|--------|----------|
|
||||
| 1 | CRITICAL | Self-assignable role at registration | **OPEN** | `UserCreate` still has `role` field (`schemas/user.py:14`). `auth.py:78` passes `user_data.role` to User. No enum validation — arbitrary strings accepted. The `is_super_admin` escalation is blocked (server default false), but `role` field is unvalidated. |
|
||||
| 2 | CRITICAL | XSS in HTML export | **OPEN** | `sessions.py:349-405` — raw f-string interpolation without `html.escape()`. All user content (tree_name, ticket_number, client_name, question, answer, notes, scratchpad) injected unescaped. |
|
||||
| 3 | CRITICAL | Default secret key in source code | **OPEN** | `config.py:29` — default string with no production validation. |
|
||||
| 4 | HIGH | No access control on `start_session` | **OPEN** | `sessions.py:70-109` — checks tree existence and active status only. No tree access check. Any authenticated user can read any tree's full structure via session snapshot. |
|
||||
| 5 | HIGH | No token revocation | **OPEN** | Logout (`auth.py:188-193`) is still a no-op. JWTs valid until expiry. |
|
||||
| 6 | HIGH | No account deactivation (`is_active`) | **OPEN** | No `is_active` field. `get_current_active_user` (`deps.py:66-72`) is a no-op. |
|
||||
| 7 | HIGH | No rate limiting | **OPEN** | No rate limiting on any endpoint. |
|
||||
| 8 | HIGH | Admin flags cannot be set via API | **PARTIAL** | `is_super_admin` field added with migration 010. But no API endpoint exists to set `is_super_admin` or `is_team_admin`. Must be set via direct DB access. |
|
||||
| 9 | MEDIUM | Frontend role awareness | **PARTIAL** | `usePermissions` hook exists, used in 3 files (TreeLibraryPage, TreeEditorPage, AppLayout). But: `ProtectedRoute` still auth-only, no route-level role guards, no 403 handling in Axios, no admin pages. |
|
||||
| 10 | MEDIUM | `None == None` team visibility bug | **PARTIAL** | Fixed in `trees.py:33` (`if current_user.team_id else False`). Still present in local helpers: `steps.py:36`, `categories.py:31`, `step_categories.py:28`. |
|
||||
| 11 | MEDIUM | Admin list vs get tree inconsistency | **OPEN** | `build_tree_access_filter` has no admin bypass. `get_tree` checks `is_super_admin`. |
|
||||
| 12 | MEDIUM | No audit logging | **OPEN** | No change. |
|
||||
| 13 | MEDIUM | No delete UI for trees or steps | **OPEN** | API methods exist, no buttons wired. `canDeleteTree` exists in hook but unused. |
|
||||
| 14 | MEDIUM | Node deletion without confirmation | **OPEN** | No change. |
|
||||
| 15 | LOW | Viewer role barely enforced | **PARTIAL** | `require_engineer_or_admin` blocks tree/step creation. Viewers can still create folders, start sessions, etc. |
|
||||
| 16 | LOW | No password complexity | **OPEN** | No change. |
|
||||
| 17 | LOW | Soft delete cascade cleanup | **OPEN** | No change. |
|
||||
| 18 | LOW | Debug endpoint in production | **OPEN** | No change. |
|
||||
| 19 | LOW | Tag search wildcard escaping | **OPEN** | No change. |
|
||||
| 20 | LOW | No team existence validation | **OPEN** | No change. |
|
||||
|
||||
**Location:** `backend/app/schemas/user.py:14`, `backend/app/api/endpoints/auth.py:74-78`
|
||||
**Summary: 0 FIXED / 4 PARTIAL / 16 OPEN**
|
||||
|
||||
The `UserCreate` schema accepts a `role` field from client input. The registration endpoint passes it directly to the User model. Any user with a valid invite code can register with `role: "admin"` and gain full admin privileges.
|
||||
---
|
||||
|
||||
## New Issues Found in Re-Audit
|
||||
|
||||
### N1. `permissions.py` is Dead Code (HIGH)
|
||||
|
||||
**Location:** `backend/app/core/permissions.py`
|
||||
|
||||
The centralized permissions module defines `can_edit_tree`, `can_delete_tree`, `can_edit_step`, `can_manage_category`, `can_manage_tree_tags`, `has_minimum_role`, `get_effective_role`, and `can_create_content`.
|
||||
|
||||
**None of these functions are imported or called by any endpoint.** Zero imports across the entire backend.
|
||||
|
||||
Each endpoint file (`trees.py`, `steps.py`, `categories.py`, `tags.py`, `step_categories.py`, `folders.py`) defines its own inline permission helpers that are similar but not identical. Key differences:
|
||||
|
||||
| Function in `permissions.py` | Local equivalent | Difference |
|
||||
|------------------------------|-----------------|------------|
|
||||
| `can_edit_tree` | `trees.py:403-407` inline | Local version doesn't call `can_create_content` |
|
||||
| `can_manage_category` | `categories.py:25-33` inline | Local version lacks `team_id is not None` guard |
|
||||
| `can_manage_step_category` | `step_categories.py:22-41` inline | Local version lacks `team_id is not None` guard |
|
||||
| `has_minimum_role` / `get_effective_role` | Not used anywhere | Entirely dead code |
|
||||
|
||||
**Risk:** False confidence that permissions are centralized. Maintaining two diverging copies.
|
||||
|
||||
### N2. `require_engineer_or_admin` Blocks Team Admins with Viewer Role (BUG)
|
||||
|
||||
**Location:** `backend/app/api/deps.py:87-98`
|
||||
|
||||
```python
|
||||
# Current — VULNERABLE
|
||||
class UserCreate(UserBase):
|
||||
role: str = Field(default="engineer", ...)
|
||||
|
||||
# Registration blindly trusts client input
|
||||
new_user = User(role=user_data.role, ...)
|
||||
async def require_engineer_or_admin(current_user):
|
||||
if current_user.is_super_admin:
|
||||
return current_user
|
||||
if current_user.role not in ("engineer",):
|
||||
raise HTTPException(403)
|
||||
```
|
||||
|
||||
**Impact:** Complete authorization bypass. A single request escalates to admin.
|
||||
Checks `is_super_admin` but NOT `is_team_admin`. A user with `role="viewer"` + `is_team_admin=True` would be blocked from creating trees — contradicting the role hierarchy where team_admin > engineer.
|
||||
|
||||
#### 2. XSS in HTML Export
|
||||
### N3. 49 Endpoints Bypass `get_current_active_user` (HIGH)
|
||||
|
||||
**Location:** `backend/app/api/endpoints/sessions.py:349-405`
|
||||
**Location:** All endpoint files
|
||||
|
||||
The HTML export function directly interpolates user-provided strings without escaping:
|
||||
```python
|
||||
html.append(f'<h1>{tree_name}</h1>')
|
||||
html.append(f'<p><strong>Ticket:</strong> {session.ticket_number}</p>')
|
||||
html.append(f'<p><strong>Client:</strong> {session.client_name}</p>')
|
||||
```
|
||||
Most endpoints depend directly on `get_current_user`, bypassing `get_current_active_user`. If `is_active` were implemented, deactivated users could still: list/view sessions, export, update scratchpads, manage folders, list/view categories, tags, steps, rate steps, etc.
|
||||
|
||||
If any field contains `<script>` tags, the exported HTML becomes an XSS payload. Since exports are likely pasted into ticketing systems or emailed, this is a stored XSS vector.
|
||||
### N4. `role` Field Accepts Arbitrary Strings (MEDIUM)
|
||||
|
||||
**Impact:** Arbitrary JavaScript execution in any system that renders the exported HTML.
|
||||
**Location:** `backend/app/schemas/user.py:14`, `backend/app/models/user.py:27`
|
||||
|
||||
#### 3. Default Secret Key in Source Code
|
||||
The `role` field is `String(50)` with no DB constraint or schema validation. Users could register with `role="superadmin"`, `role="root"`, etc. While these don't grant permissions (only `is_super_admin` flag does), it creates inconsistent data. The `UserCreate` schema describes "engineer or viewer" but doesn't enforce it.
|
||||
|
||||
**Location:** `backend/app/core/config.py:29`
|
||||
### N5. TreeEditorPage Checks `canCreateTrees` Not `canEditTree` (MEDIUM)
|
||||
|
||||
If `.env` is missing or `SECRET_KEY` isn't set, JWTs are signed with a publicly known default string. Anyone can forge admin tokens.
|
||||
**Location:** `frontend/src/pages/TreeEditorPage.tsx:81-85`
|
||||
|
||||
**Impact:** Complete authentication bypass if deployed without proper `.env` configuration.
|
||||
|
||||
---
|
||||
|
||||
### HIGH
|
||||
|
||||
#### 4. No Access Control on `start_session`
|
||||
|
||||
**Location:** `backend/app/api/endpoints/sessions.py:69-109`
|
||||
|
||||
The endpoint checks tree existence and active status but does NOT check if the user has permission to access the tree. Any authenticated user can start a session on any active tree (including private/team-only) if they know the UUID, receiving the full `tree_structure` in the session's `tree_snapshot`.
|
||||
|
||||
**Impact:** Information disclosure — bypasses all tree visibility rules.
|
||||
|
||||
#### 5. No Token Revocation
|
||||
|
||||
**Location:** `backend/app/api/endpoints/auth.py:188-193`
|
||||
|
||||
Logout is a client-side no-op. JWTs remain valid until expiration (15 min access, 7 day refresh). Combined with no account deactivation, a terminated employee retains 7 days of access.
|
||||
|
||||
**Impact:** Cannot immediately revoke access to client troubleshooting data.
|
||||
|
||||
#### 6. No Account Deactivation (`is_active`)
|
||||
|
||||
**Location:** `backend/app/api/deps.py:66-72`
|
||||
|
||||
The `get_current_active_user` dependency is a no-op — no `is_active` field exists on the User model. The only way to block a user is to delete their database row, which cascades all their data.
|
||||
|
||||
**Impact:** No graceful way to disable accounts.
|
||||
|
||||
#### 7. No Rate Limiting on Auth Endpoints
|
||||
|
||||
Login, registration, and invite code validation have no rate limiting. The unauthenticated `/invites/validate/{code}` endpoint allows unlimited invite code enumeration.
|
||||
|
||||
**Impact:** Brute force attacks and invite code enumeration.
|
||||
|
||||
#### 8. `is_team_admin` Cannot Be Set
|
||||
|
||||
**Location:** `backend/app/models/user.py:28`
|
||||
|
||||
The field exists and permission checks reference it, but no API endpoint can set it. The entire "team admin" permission tier is dead code in production.
|
||||
|
||||
**Impact:** Category management, step category management, and team-scoped tag management are admin-only in practice.
|
||||
|
||||
---
|
||||
|
||||
### MEDIUM
|
||||
|
||||
#### 9. Frontend Ignores All Roles
|
||||
|
||||
**Location:** `frontend/src/components/layout/ProtectedRoute.tsx`, `frontend/src/router.tsx`
|
||||
|
||||
`ProtectedRoute` only checks `isAuthenticated`. No role-based route guards, no conditional UI rendering, no admin pages. All users see the same interface regardless of role.
|
||||
|
||||
Specific issues:
|
||||
- Edit button appears on ALL tree cards for ALL users (`TreeLibraryPage.tsx:309-318`)
|
||||
- "Create Tree" button visible to viewers who will get 403 (`TreeLibraryPage.tsx:146-155`)
|
||||
- No 403 error handling in the Axios interceptor
|
||||
- No admin dashboard despite backend admin APIs existing
|
||||
|
||||
#### 10. `None == None` Team Visibility Bug
|
||||
|
||||
**Location:** `backend/app/api/endpoints/steps.py:29-37`
|
||||
|
||||
```python
|
||||
if step.visibility == 'team':
|
||||
return step.team_id == user.team_id or user.role == 'admin'
|
||||
```
|
||||
|
||||
When both `step.team_id` and `user.team_id` are `None`, this evaluates to `True`. Users with no team can see "team" steps that also have no team.
|
||||
|
||||
#### 11. Admin List vs. Get Tree Inconsistency
|
||||
|
||||
`build_tree_access_filter` (used in `list_trees`) does NOT include an admin bypass, but `get_tree` does. Admins can access individual trees by ID but cannot discover them via the list endpoint.
|
||||
|
||||
#### 12. No Audit Logging
|
||||
|
||||
No record of administrative actions: tree deletion, invite code creation/revocation, category changes, permission modifications, or failed login attempts.
|
||||
|
||||
#### 13. No Delete UI for Trees or Steps
|
||||
|
||||
`treesApi.delete()` and `stepsApi.delete()` exist in the frontend API client but are never wired to any button or UI component. Trees and steps cannot be deleted from the frontend.
|
||||
|
||||
#### 14. Tree Node Deletion Without Confirmation
|
||||
|
||||
**Location:** `frontend/src/components/tree-editor/NodeList.tsx:288-298`
|
||||
|
||||
Clicking the trash icon in the tree editor immediately deletes the node with no confirmation dialog.
|
||||
|
||||
---
|
||||
|
||||
### LOW
|
||||
|
||||
| # | Issue | Location |
|
||||
|---|-------|----------|
|
||||
| 15 | `viewer` role accepted but barely enforced | `schemas/user.py`, all endpoints |
|
||||
| 16 | No password complexity beyond 10-char minimum | `schemas/user.py:13` |
|
||||
| 17 | Soft delete doesn't cascade-clean tags, folder memberships | `trees.py` delete endpoint |
|
||||
| 18 | Debug CORS endpoint exposed in production | `main.py:86-94` |
|
||||
| 19 | Tag search `ilike` doesn't escape `%` and `_` wildcards | `tags.py:97` |
|
||||
| 20 | No team existence validation for admin step creation | `steps.py` create endpoint |
|
||||
The editor page uses `canCreateTrees` (a blanket role check) instead of `canEditTree(tree)` (ownership/team check). A non-author engineer can navigate to `/trees/{someone-else's-id}/edit` and the frontend won't redirect them — the backend will block the save, but the UX is poor (loads editor, then fails on save).
|
||||
|
||||
---
|
||||
|
||||
## MSP-Specific Concerns
|
||||
|
||||
1. **Weak multi-tenancy:** Admin role transcends all teams. Team A's admin could access Team B's client troubleshooting data (ticket numbers, client names, IP addresses in scratchpads).
|
||||
|
||||
2. **No data retention/purge:** Session scratchpads and decision notes contain sensitive client information with no cleanup mechanism.
|
||||
|
||||
3. **No team-level session visibility:** Supervisors can't review their team's sessions — only the session owner can view them.
|
||||
|
||||
4. **Step Library IP leakage:** Public steps expose team-specific troubleshooting procedures (commands, scripts, procedures) to all teams with no approval workflow.
|
||||
1. **Weak multi-tenancy:** Super admin transcends all teams. Team A's super admin could access Team B's client troubleshooting data.
|
||||
2. **No data retention/purge:** Session scratchpads and notes contain sensitive client information with no cleanup mechanism.
|
||||
3. **No team-level session visibility:** Supervisors can't review their team's sessions.
|
||||
4. **Step Library IP leakage:** Public steps expose team-specific troubleshooting procedures to all teams.
|
||||
|
||||
---
|
||||
|
||||
@@ -196,11 +153,11 @@ Clicking the trash icon in the tree editor immediately deletes the node with no
|
||||
|
||||
| Assumption | Reality |
|
||||
|-----------|---------|
|
||||
| "Invite codes protect registration" | They don't prevent role escalation — the role field is independent |
|
||||
| "Team scoping = tenant isolation" | Admin role transcends all teams with no boundary |
|
||||
| "Soft delete is sufficient" | Leaves data artifacts (sessions with tree_snapshot, folder memberships, tag assignments) |
|
||||
| "Invite codes protect registration" | They don't prevent arbitrary role strings — the role field is unvalidated |
|
||||
| "Team scoping = tenant isolation" | Super admin transcends all teams with no boundary |
|
||||
| "`permissions.py` centralizes permissions" | It's dead code — no endpoint imports it |
|
||||
| "JWT statelessness is acceptable" | For an MSP tool with client data, inability to immediately revoke access is a liability |
|
||||
| "Frontend doesn't need role awareness" | Without it, every user sees affordances they can't use — confusing UX and attacker map |
|
||||
| "`usePermissions` hook covers the frontend" | Only used in 3 of ~10 components that need it |
|
||||
|
||||
---
|
||||
|
||||
@@ -210,18 +167,22 @@ The existing test suite does NOT cover:
|
||||
- Cross-user resource access (user A accessing user B's sessions/folders/steps)
|
||||
- Non-admin attempting tree deletion
|
||||
- Viewer role restrictions
|
||||
- Admin role escalation at registration
|
||||
- Arbitrary role string at registration
|
||||
- Team-based access controls (no team fixtures exist)
|
||||
- Step library permission checks (no step tests at all)
|
||||
- Category/tag/folder cross-user access
|
||||
- `start_session` tree access bypass
|
||||
- Concurrent session access across users
|
||||
- Deactivated user access (blocked by no `is_active` field)
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- Frontend source: `frontend/src/`
|
||||
- Backend endpoints: `backend/app/api/endpoints/`
|
||||
- Centralized permissions (dead code): `backend/app/core/permissions.py`
|
||||
- Frontend permissions hook: `frontend/src/hooks/usePermissions.ts`
|
||||
- Auth dependencies: `backend/app/api/deps.py`
|
||||
- RBAC questions doc: `docs/RBAC-IMPLEMENTATION-QUESTIONS.md`
|
||||
- Backend endpoints: `backend/app/api/endpoints/`
|
||||
- Models: `backend/app/models/`
|
||||
- Tests: `backend/tests/`
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Permissions & RBAC Fixes — Implementation Plan
|
||||
|
||||
> **Date:** 2026-02-05
|
||||
> **Status:** Draft
|
||||
> **Status:** Updated after re-audit
|
||||
> **Depends on:** [2026-02-05-permissions-audit-design.md](2026-02-05-permissions-audit-design.md)
|
||||
> **Scope:** Phased fix of all permission issues identified in the audit
|
||||
> **Scope:** Phased fix of all permission issues identified in audit and re-audit
|
||||
|
||||
---
|
||||
|
||||
@@ -17,6 +17,17 @@ Fixes are grouped into four phases by severity and dependency:
|
||||
|
||||
Each phase is independently deployable. Phase A should be done as a single PR.
|
||||
|
||||
### What Changed Since Initial Plan
|
||||
|
||||
The RBAC commit (`34daa26`) added infrastructure (`permissions.py`, `usePermissions.ts`, `is_super_admin`), but the re-audit revealed:
|
||||
- `permissions.py` is dead code — no endpoint imports it
|
||||
- `require_engineer_or_admin` has a bug blocking team admins with viewer role
|
||||
- 49 endpoints bypass `get_current_active_user`
|
||||
- The `role` field still accepts arbitrary strings
|
||||
- TreeEditorPage uses wrong permission check for edit mode
|
||||
|
||||
These new findings are integrated into the phases below.
|
||||
|
||||
---
|
||||
|
||||
## Phase A: Critical Security Fixes
|
||||
@@ -24,33 +35,38 @@ Each phase is independently deployable. Phase A should be done as a single PR.
|
||||
**Branch:** `fix/critical-security`
|
||||
**Priority:** Immediate — blocks all other work
|
||||
|
||||
### A1. Remove Self-Assignable Admin Role
|
||||
### A1. Lock Down Registration Role Field
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/schemas/user.py` — Remove `role` from `UserCreate`
|
||||
- `backend/app/schemas/user.py` — Constrain `role` to `Literal["engineer", "viewer"]` or remove entirely
|
||||
- `backend/app/api/endpoints/auth.py` — Hardcode `role="engineer"` in registration
|
||||
|
||||
**Changes:**
|
||||
```python
|
||||
# schemas/user.py — Remove role from UserCreate
|
||||
# schemas/user.py — Option A: Remove role from UserCreate entirely
|
||||
class UserCreate(UserBase):
|
||||
password: str = Field(..., min_length=10)
|
||||
# role field REMOVED — always defaults to "engineer"
|
||||
|
||||
# schemas/user.py — Option B: Constrain with Literal (if viewer self-registration is desired)
|
||||
from typing import Literal
|
||||
class UserCreate(UserBase):
|
||||
role: Literal["engineer", "viewer"] = "engineer"
|
||||
password: str = Field(..., min_length=10)
|
||||
|
||||
# auth.py — Hardcode role
|
||||
# auth.py — If using Option A, hardcode:
|
||||
new_user = User(
|
||||
...
|
||||
role="engineer", # Always engineer; admin set via admin endpoint only
|
||||
role="engineer", # Always engineer; changed only via admin endpoint
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
**New endpoint (optional, can defer to Phase B):**
|
||||
- `PUT /api/v1/admin/users/{user_id}/role` — Admin-only endpoint to change user roles
|
||||
|
||||
**Tests to add:**
|
||||
- Attempt to register with `role: "admin"` → should still be engineer
|
||||
- Verify no field in registration request can escalate privileges
|
||||
- Register with `role: "admin"` → should still be engineer (or 422 with Literal)
|
||||
- Register with `role: "super_admin"` → rejected
|
||||
- Register with arbitrary string → rejected
|
||||
- Register with default → engineer
|
||||
|
||||
### A2. Escape HTML Export Output
|
||||
|
||||
@@ -61,16 +77,21 @@ new_user = User(
|
||||
```python
|
||||
from html import escape
|
||||
|
||||
# Escape all user-provided content
|
||||
# Escape ALL user-provided content in _generate_html_export()
|
||||
html.append(f'<title>{escape(tree_name)}</title>')
|
||||
html.append(f'<h1>{escape(tree_name)}</h1>')
|
||||
html.append(f'<p><strong>Ticket:</strong> {escape(session.ticket_number or "")}</p>')
|
||||
html.append(f'<p><strong>Client:</strong> {escape(session.client_name or "")}</p>')
|
||||
# ... escape all interpolated values
|
||||
html.append(f'<div class="scratchpad">{escape(scratchpad)}</div>')
|
||||
html.append(f'<h3>Step {i}: {escape(question)}</h3>')
|
||||
html.append(f'<p class="answer">Answer: {escape(answer)}</p>')
|
||||
html.append(f'<p class="notes">Notes: {escape(notes)}</p>')
|
||||
```
|
||||
|
||||
**Tests to add:**
|
||||
- Export session with `<script>` in ticket number → verify escaped in output
|
||||
- Export session with HTML entities in notes → verify escaped
|
||||
- Export session with `<script>alert('xss')</script>` in ticket_number → verify `<script>` in output
|
||||
- Export session with `<img onerror=...>` in notes → verify escaped
|
||||
- Export session with `&`, `"`, `'` in fields → verify entities
|
||||
|
||||
### A3. Fail on Default Secret Key in Production
|
||||
|
||||
@@ -81,28 +102,33 @@ html.append(f'<p><strong>Client:</strong> {escape(session.client_name or "")}</p
|
||||
```python
|
||||
@validator("SECRET_KEY")
|
||||
def validate_secret_key(cls, v):
|
||||
if not v or v == "your-secret-key-change-in-production-use-openssl-rand-hex-32":
|
||||
default = "your-secret-key-change-in-production-use-openssl-rand-hex-32"
|
||||
if not v or v == default:
|
||||
import os
|
||||
if os.getenv("DEBUG", "true").lower() != "true":
|
||||
raise ValueError("SECRET_KEY must be set to a secure value in production")
|
||||
return v
|
||||
```
|
||||
|
||||
### A4. Add Role Enum Constraint
|
||||
### A4. Add Role Enum Constraint to Database
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/models/user.py` — Add check constraint or use SQLAlchemy Enum
|
||||
- `backend/app/models/user.py` — Add check constraint
|
||||
- New migration
|
||||
|
||||
**Changes:**
|
||||
```python
|
||||
from sqlalchemy import Enum as SAEnum
|
||||
from sqlalchemy import CheckConstraint
|
||||
|
||||
role = Column(SAEnum("admin", "engineer", "viewer", name="user_role"),
|
||||
default="engineer", nullable=False)
|
||||
class User(Base):
|
||||
__table_args__ = (
|
||||
CheckConstraint("role IN ('engineer', 'viewer')", name="ck_user_role"),
|
||||
)
|
||||
```
|
||||
|
||||
**Migration:** `alembic revision --autogenerate -m "Add role enum constraint"`
|
||||
Note: Role is now only `engineer` or `viewer`. The old `admin` role is fully replaced by `is_super_admin` flag (migration 010 already converted existing admin users).
|
||||
|
||||
**Migration:** `alembic revision --autogenerate -m "Add role check constraint"`
|
||||
|
||||
---
|
||||
|
||||
@@ -116,13 +142,13 @@ role = Column(SAEnum("admin", "engineer", "viewer", name="user_role"),
|
||||
**Files to modify:**
|
||||
- `backend/app/api/endpoints/sessions.py`
|
||||
|
||||
**Changes:** After fetching the tree, add the same access check used in `get_tree`:
|
||||
**Changes:** After fetching the tree, add access check matching `get_tree`:
|
||||
```python
|
||||
can_access = (
|
||||
tree.is_default or tree.is_public or
|
||||
tree.author_id == current_user.id or
|
||||
(tree.team_id == current_user.team_id and current_user.team_id is not None) or
|
||||
current_user.role == "admin"
|
||||
current_user.is_super_admin
|
||||
)
|
||||
if not can_access:
|
||||
raise HTTPException(status_code=403, detail="You don't have access to this tree")
|
||||
@@ -132,54 +158,90 @@ if not can_access:
|
||||
- User starts session on own tree → success
|
||||
- User starts session on public tree → success
|
||||
- User starts session on private team tree they don't belong to → 403
|
||||
- Admin starts session on any tree → success
|
||||
- Super admin starts session on any tree → success
|
||||
|
||||
### B2. Add `is_active` Field and Account Deactivation
|
||||
### B2. Wire `permissions.py` Into Endpoints (or Delete It)
|
||||
|
||||
**Decision needed:** Wire the centralized module into all endpoints, or delete it and keep local helpers.
|
||||
|
||||
**Recommended: Wire it in.** Replace all 6 local permission helpers with imports from `permissions.py`.
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/models/user.py` — Add `is_active = Column(Boolean, default=True)`
|
||||
- `backend/app/api/endpoints/trees.py` — Import and use `can_edit_tree`, `can_delete_tree`
|
||||
- `backend/app/api/endpoints/steps.py` — Import and use `can_edit_step`, `can_view_step`
|
||||
- `backend/app/api/endpoints/tags.py` — Import and use `can_manage_tree_tags`, `can_create_tag`
|
||||
- `backend/app/api/endpoints/categories.py` — Import and use `can_manage_category`, `can_create_category`
|
||||
- `backend/app/api/endpoints/step_categories.py` — Import and use `can_manage_step_category`, `can_create_step_category`
|
||||
- `backend/app/api/endpoints/folders.py` — Import and use `can_access_tree`
|
||||
- `backend/app/core/permissions.py` — Add any missing functions (`can_view_step`, `can_create_tag`, `can_access_tree`)
|
||||
|
||||
**This also fixes:**
|
||||
- N1 (dead code)
|
||||
- Finding #10 partial (`None` guards from centralized module applied everywhere)
|
||||
|
||||
### B3. Fix `require_engineer_or_admin` Team Admin Bug
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/api/deps.py`
|
||||
|
||||
**Changes:**
|
||||
```python
|
||||
async def require_engineer_or_admin(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)]
|
||||
) -> User:
|
||||
if current_user.is_super_admin:
|
||||
return current_user
|
||||
if current_user.is_team_admin and current_user.team_id is not None:
|
||||
return current_user
|
||||
if current_user.role not in ("engineer",):
|
||||
raise HTTPException(status_code=403, detail="Requires engineer or higher role")
|
||||
return current_user
|
||||
```
|
||||
|
||||
### B4. Add `is_active` Field and Update All Dependencies
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/models/user.py` — Add `is_active = Column(Boolean, default=True, server_default="true")`
|
||||
- `backend/app/api/deps.py` — Implement `get_current_active_user` check
|
||||
- **All endpoint files** — Change `Depends(get_current_user)` to `Depends(get_current_active_user)` (49 usages)
|
||||
- New migration
|
||||
|
||||
**Changes:**
|
||||
```python
|
||||
# deps.py
|
||||
async def get_current_active_user(current_user = Depends(get_current_user)):
|
||||
async def get_current_active_user(
|
||||
current_user: Annotated[User, Depends(get_current_user)]
|
||||
) -> User:
|
||||
if not current_user.is_active:
|
||||
raise HTTPException(status_code=403, detail="Account has been deactivated")
|
||||
return current_user
|
||||
```
|
||||
|
||||
**Update all endpoints** to use `get_current_active_user` instead of `get_current_user` as their base dependency.
|
||||
|
||||
**New endpoint:**
|
||||
- `PUT /api/v1/admin/users/{user_id}/deactivate` — Admin-only
|
||||
|
||||
### B3. Add User Management Endpoints
|
||||
### B5. Create Admin User Management Endpoints
|
||||
|
||||
**New file:** `backend/app/api/endpoints/admin.py`
|
||||
|
||||
**Endpoints:**
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/admin/users` | List all users (admin only) |
|
||||
| GET | `/api/v1/admin/users/{id}` | Get user details (admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/role` | Change user role (admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/team-admin` | Toggle is_team_admin (admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/deactivate` | Deactivate account (admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/activate` | Reactivate account (admin only) |
|
||||
| GET | `/api/v1/admin/users` | List all users (super admin only) |
|
||||
| GET | `/api/v1/admin/users/{id}` | Get user details (super admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/role` | Change user role (super admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/team-admin` | Toggle is_team_admin (super admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/deactivate` | Deactivate account (super admin only) |
|
||||
| PUT | `/api/v1/admin/users/{id}/activate` | Reactivate account (super admin only) |
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/api/router.py` — Register admin router
|
||||
- `backend/app/schemas/user.py` — Add admin response/update schemas
|
||||
|
||||
### B4. Add Rate Limiting
|
||||
### B6. Add Rate Limiting
|
||||
|
||||
**New dependency:** `slowapi` or custom middleware
|
||||
**New dependency:** `slowapi`
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/requirements.txt` — Add `slowapi`
|
||||
- `backend/app/main.py` — Add rate limiter
|
||||
- `backend/app/main.py` — Add rate limiter middleware
|
||||
- `backend/app/api/endpoints/auth.py` — Apply rate limits
|
||||
|
||||
**Rate limits:**
|
||||
@@ -190,13 +252,13 @@ async def get_current_active_user(current_user = Depends(get_current_user)):
|
||||
| `POST /auth/refresh` | 10/minute per IP |
|
||||
| `GET /invites/validate/{code}` | 5/minute per IP |
|
||||
|
||||
### B5. Token Revocation (Short-Term Fix)
|
||||
### B7. Token Revocation (Short-Term Fix)
|
||||
|
||||
**Approach:** Switch to short-lived access tokens (5 min) and store refresh tokens in the database so they can be revoked.
|
||||
**Approach:** Store refresh tokens in DB so they can be revoked. Reduce access token TTL.
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/models/` — New `RefreshToken` model (token hash, user_id, expires_at, revoked_at)
|
||||
- `backend/app/api/endpoints/auth.py` — Store refresh tokens on login, validate on refresh, delete on logout
|
||||
- `backend/app/api/endpoints/auth.py` — Store on login, validate on refresh, revoke on logout
|
||||
- `backend/app/core/config.py` — Reduce `ACCESS_TOKEN_EXPIRE_MINUTES` to 5
|
||||
- New migration
|
||||
|
||||
@@ -204,7 +266,6 @@ async def get_current_active_user(current_user = Depends(get_current_user)):
|
||||
```python
|
||||
@router.post("/logout")
|
||||
async def logout(payload = Depends(get_refresh_token_payload), db = Depends(get_db)):
|
||||
# Revoke the refresh token in the database
|
||||
await db.execute(
|
||||
update(RefreshToken)
|
||||
.where(RefreshToken.token_hash == hash_token(payload["jti"]))
|
||||
@@ -220,25 +281,14 @@ async def logout(payload = Depends(get_refresh_token_payload), db = Depends(get_
|
||||
**Branch:** `feat/permissions-ux`
|
||||
**Priority:** Better UX and consistency
|
||||
|
||||
### C1. Frontend Role-Based UI Gating
|
||||
### C1. Frontend: Add 403 Handling and Route Guards
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/api/client.ts` — Add 403 interceptor
|
||||
- `frontend/src/components/layout/ProtectedRoute.tsx` — Add `requiredRole` prop
|
||||
- `frontend/src/router.tsx` — Apply role guards to admin routes
|
||||
- `frontend/src/pages/TreeLibraryPage.tsx` — Conditionally show create/edit buttons
|
||||
- `frontend/src/api/client.ts` — Add 403 handling in Axios interceptor
|
||||
|
||||
**New component:**
|
||||
```typescript
|
||||
// ProtectedRoute with role checking
|
||||
interface ProtectedRouteProps {
|
||||
requiredRole?: UserRole | UserRole[]
|
||||
requireTeamAdmin?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
**403 interceptor pattern:**
|
||||
**403 interceptor:**
|
||||
```typescript
|
||||
if (error.response?.status === 403) {
|
||||
toast.error("You don't have permission to perform this action")
|
||||
@@ -246,37 +296,56 @@ if (error.response?.status === 403) {
|
||||
}
|
||||
```
|
||||
|
||||
### C2. Add Delete UI for Trees and Steps
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/pages/TreeLibraryPage.tsx` — Add delete button (admin/owner only)
|
||||
- `frontend/src/components/step-library/StepCard.tsx` — Add delete button (owner/admin only)
|
||||
- `frontend/src/components/common/ConfirmDialog.tsx` — New reusable confirmation modal
|
||||
|
||||
**Visibility rules:**
|
||||
- Tree delete button: visible to admin only (matches backend)
|
||||
- Step delete button: visible to step owner and admin
|
||||
- Both require confirmation dialog before API call
|
||||
|
||||
### C3. Fix `None == None` Team Visibility Bug
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/api/endpoints/steps.py`
|
||||
|
||||
**Change:**
|
||||
```python
|
||||
def can_view_step(user: User, step: StepLibrary) -> bool:
|
||||
if step.visibility == 'team':
|
||||
return (step.team_id is not None
|
||||
and step.team_id == user.team_id) or user.role == 'admin'
|
||||
**ProtectedRoute enhancement:**
|
||||
```typescript
|
||||
interface ProtectedRouteProps {
|
||||
requiredRole?: UserRole | UserRole[]
|
||||
requireTeamAdmin?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
```
|
||||
|
||||
### C4. Fix Admin Tree List Inconsistency
|
||||
### C2. Frontend: Fix TreeEditorPage Permission Check
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/pages/TreeEditorPage.tsx`
|
||||
|
||||
**Change:** After loading the tree in edit mode, check `canEditTree(tree)` instead of just `canCreateTrees`:
|
||||
```typescript
|
||||
useEffect(() => {
|
||||
if (tree && id && !canEditTree({ author_id: tree.author_id, team_id: tree.team_id })) {
|
||||
navigate('/trees')
|
||||
}
|
||||
}, [tree, id])
|
||||
```
|
||||
|
||||
### C3. Frontend: Add Delete UI for Trees and Steps
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/pages/TreeLibraryPage.tsx` — Add delete button gated by `canDeleteTree`
|
||||
- `frontend/src/components/step-library/StepCard.tsx` — Add delete button gated by `canEditStep`
|
||||
- `frontend/src/components/common/ConfirmDialog.tsx` — New reusable confirmation modal
|
||||
|
||||
### C4. Frontend: Wire `usePermissions` Into All Relevant Components
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/components/step-library/StepLibraryBrowser.tsx` — Use `canCreateSteps` for create button
|
||||
- `frontend/src/components/step-library/CustomStepModal.tsx` — Check `canCreateSteps`
|
||||
- `frontend/src/components/tree-editor/NodeList.tsx` — Add confirmation dialog for node deletion
|
||||
|
||||
### C5. Backend: Fix `None == None` in All Local Helpers
|
||||
|
||||
**Files to modify (if not already fixed by B2):**
|
||||
- `backend/app/api/endpoints/steps.py` — `can_view_step()` null guard
|
||||
- `backend/app/api/endpoints/categories.py` — `can_manage_category()` null guard
|
||||
- `backend/app/api/endpoints/step_categories.py` — `can_manage_step_category()` null guard
|
||||
|
||||
### C6. Backend: Fix Admin Tree List Inconsistency
|
||||
|
||||
**Files to modify:**
|
||||
- `backend/app/api/endpoints/trees.py`
|
||||
|
||||
**Change:** Add admin bypass to `build_tree_access_filter`:
|
||||
**Change:** Add super admin bypass to `build_tree_access_filter`:
|
||||
```python
|
||||
def build_tree_access_filter(current_user: User):
|
||||
conditions = [
|
||||
@@ -286,17 +355,12 @@ def build_tree_access_filter(current_user: User):
|
||||
]
|
||||
if current_user.team_id:
|
||||
conditions.append(Tree.team_id == current_user.team_id)
|
||||
if current_user.role == "admin":
|
||||
conditions.append(True) # Admin sees all
|
||||
if current_user.is_super_admin:
|
||||
conditions.append(sqlalchemy.true())
|
||||
return or_(*conditions)
|
||||
```
|
||||
|
||||
### C5. Add Node Deletion Confirmation
|
||||
|
||||
**Files to modify:**
|
||||
- `frontend/src/components/tree-editor/NodeList.tsx` — Add confirmation before `deleteNode()`
|
||||
|
||||
### C6. Add Audit Log Table
|
||||
### C7. Backend: Add Audit Log Table
|
||||
|
||||
**New file:** `backend/app/models/audit_log.py`
|
||||
|
||||
@@ -324,8 +388,8 @@ Integrate into admin endpoints and destructive actions.
|
||||
### D1. Define and Enforce Viewer Role Permissions
|
||||
|
||||
Decide what viewers can/cannot do and enforce consistently:
|
||||
- **Proposed:** Viewers can browse trees, start sessions, and view steps. Cannot create trees, steps, or manage folders/tags/categories.
|
||||
- Add `require_engineer_or_admin` to step creation, folder creation endpoints.
|
||||
- **Proposed:** Viewers can browse trees, start sessions, view steps, and rate steps. Cannot create trees, steps, folders, or manage tags/categories.
|
||||
- Add `require_engineer_or_admin` to folder creation and tag creation endpoints.
|
||||
|
||||
### D2. Add Password Complexity Validation
|
||||
|
||||
@@ -361,25 +425,28 @@ query = select(TreeTag).where(TreeTag.name.ilike(f"%{q_escaped}%"))
|
||||
|
||||
```
|
||||
Phase A (Critical) ──── Single PR, immediate
|
||||
A1. Remove self-assignable admin role
|
||||
A1. Lock down registration role field
|
||||
A2. Escape HTML export
|
||||
A3. Fail on default secret key
|
||||
A4. Add role enum constraint
|
||||
A4. Add role DB constraint
|
||||
|
||||
Phase B (High) ──── 2-3 PRs
|
||||
Phase B (High) ──── 3-4 PRs
|
||||
B1. Tree access check on start_session
|
||||
B2. is_active field + account deactivation
|
||||
B3. User management admin endpoints
|
||||
B4. Rate limiting
|
||||
B5. Token revocation
|
||||
B2. Wire permissions.py into endpoints (fixes dead code + None guards)
|
||||
B3. Fix require_engineer_or_admin team admin bug
|
||||
B4. is_active field + update all 49 endpoint dependencies
|
||||
B5. Admin user management endpoints
|
||||
B6. Rate limiting
|
||||
B7. Token revocation
|
||||
|
||||
Phase C (Medium) ──── 3-4 PRs
|
||||
C1. Frontend role-based UI gating
|
||||
C2. Delete UI for trees and steps
|
||||
C3. Fix None==None team visibility
|
||||
C4. Fix admin tree list inconsistency
|
||||
C5. Node deletion confirmation
|
||||
C6. Audit log table
|
||||
C1. Frontend 403 handling + route guards
|
||||
C2. Fix TreeEditorPage canEditTree check
|
||||
C3. Delete UI for trees and steps
|
||||
C4. Wire usePermissions into all components
|
||||
C5. Fix None==None in all local helpers (if not done in B2)
|
||||
C6. Fix admin tree list inconsistency
|
||||
C7. Audit log table
|
||||
|
||||
Phase D (Low) ──── As convenient
|
||||
D1. Viewer role enforcement
|
||||
@@ -396,8 +463,8 @@ Phase D (Low) ──── As convenient
|
||||
| Phase | Backend Changes | Frontend Changes | Migrations | New Tests |
|
||||
|-------|----------------|-----------------|------------|-----------|
|
||||
| A | 4 files modified | 0 | 1 | ~8 tests |
|
||||
| B | 6 files modified, 2 new | 0 | 2 | ~15 tests |
|
||||
| C | 3 files modified, 2 new | 5 files modified, 1 new | 1 | ~10 tests |
|
||||
| B | 10+ files modified, 2 new | 0 | 2 | ~20 tests |
|
||||
| C | 3 files modified, 2 new | 6 files modified, 1 new | 1 | ~10 tests |
|
||||
| D | 4 files modified | 0 | 0 | ~5 tests |
|
||||
|
||||
---
|
||||
@@ -405,25 +472,30 @@ Phase D (Low) ──── As convenient
|
||||
## Testing Strategy
|
||||
|
||||
Each phase should include:
|
||||
1. **Unit tests** for new permission checks
|
||||
1. **Unit tests** for new/changed permission checks
|
||||
2. **Integration tests** for cross-user access scenarios
|
||||
3. **Negative tests** — verify unauthorized access returns 403, not 500
|
||||
4. Manual verification of frontend behavior per role
|
||||
|
||||
**Priority test scenarios:**
|
||||
- Register with `role: admin` → must be engineer
|
||||
- Register with `role: "admin"` → rejected or ignored
|
||||
- Register with arbitrary role string → rejected
|
||||
- User A accesses User B's session → 403
|
||||
- Viewer creates a tree → 403
|
||||
- Team admin with viewer role creates a tree → success (B3 fix)
|
||||
- Start session on private tree without access → 403
|
||||
- Login with deactivated account → 403
|
||||
- Exported HTML with `<script>` tag → escaped
|
||||
- Brute force login → rate limited after 5 attempts
|
||||
- `permissions.py` functions match endpoint behavior (after B2)
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Phase A changes are backward-compatible — no frontend changes needed
|
||||
- Phase B introduces new admin endpoints — frontend admin pages can follow in Phase C
|
||||
- Token revocation (B5) is the most complex change — consider deferring to a dedicated PR
|
||||
- All new endpoints should follow existing patterns in `deps.py` for consistency
|
||||
- Phase B is larger than originally planned due to dead code wiring (B2) and dependency chain fix (B4)
|
||||
- B2 (wiring permissions.py) and B4 (is_active dependency chain) touch many files — plan for careful review
|
||||
- Token revocation (B7) is the most complex change — consider deferring to a dedicated PR
|
||||
- Phase C partially started: `usePermissions` hook exists, TreeLibraryPage and TreeEditorPage use it
|
||||
- All `require_admin` checks should use `is_super_admin` (never `role == "admin"`)
|
||||
|
||||
Reference in New Issue
Block a user