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>
9.2 KiB
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:
- Frontend UX audit — every page, component, API client, store, and router
- Backend architecture audit — every endpoint, dependency, model, schema, and test
- 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.
# 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:
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
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
-
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).
-
No data retention/purge: Session scratchpads and decision notes contain sensitive client information with no cleanup mechanism.
-
No team-level session visibility: Supervisors can't review their team's sessions — only the session owner can view them.
-
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/