Files
resolutionflow/docs/plans/2026-02-05-permissions-audit-design.md
Michael Chihlas 02bd97948e docs: add permissions audit design doc and implementation plan
Full-stack RBAC audit covering frontend UX, backend architecture,
and adversarial analysis. Implementation plan phased by severity
(Critical → High → Medium → Low).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 17:42:38 -05:00

228 lines
9.2 KiB
Markdown

# Permissions & RBAC Audit — Design Document
> **Date:** 2026-02-05
> **Status:** Draft
> **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.
**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
---
## Current State Summary
### 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)
- **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
### What's Broken
The permission system has critical vulnerabilities, significant gaps, and inconsistencies across resource types.
---
## Findings by Severity
### CRITICAL
#### 1. Self-Assignable Admin Role at Registration
**Location:** `backend/app/schemas/user.py:14`, `backend/app/api/endpoints/auth.py:74-78`
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.
```python
# Current — VULNERABLE
class UserCreate(UserBase):
role: str = Field(default="engineer", ...)
# Registration blindly trusts client input
new_user = User(role=user_data.role, ...)
```
**Impact:** Complete authorization bypass. A single request escalates to admin.
#### 2. XSS in HTML Export
**Location:** `backend/app/api/endpoints/sessions.py:349-405`
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>')
```
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.
**Impact:** Arbitrary JavaScript execution in any system that renders the exported HTML.
#### 3. Default Secret Key in Source Code
**Location:** `backend/app/core/config.py:29`
If `.env` is missing or `SECRET_KEY` isn't set, JWTs are signed with a publicly known default string. Anyone can forge admin tokens.
**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 |
---
## 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.
---
## Challenged Assumptions
| 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) |
| "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 |
---
## Test Coverage Gaps
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
- Team-based access controls (no team fixtures exist)
- Step library permission checks (no step tests at all)
- Category/tag/folder cross-user access
- Concurrent session access across users
---
## References
- Frontend source: `frontend/src/`
- Backend endpoints: `backend/app/api/endpoints/`
- Auth dependencies: `backend/app/api/deps.py`
- Models: `backend/app/models/`
- Tests: `backend/tests/`