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

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:

  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.

# 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

  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/