AeThex-OS/docs/ORG_SCOPING_AUDIT.md

209 lines
10 KiB
Markdown

# Database Query Org-Scoping Audit Report
**Date:** 2026-01-05
**Purpose:** Identify all database queries that lack organization_id scoping and rely only on user_id for authorization.
---
## Executive Summary
- **Total Unscoped Queries Identified:** 42
- **High Risk:** 15 queries (direct data access without org context)
- **Medium Risk:** 18 queries (user-scoped but org-ambiguous)
- **Low Risk:** 9 queries (global/admin-only endpoints)
---
## Detailed Audit Table
| File | Route/Function | Table Accessed | Risk Level | Recommended Fix |
|------|---------------|----------------|------------|-----------------|
| **storage.ts** | `getProfiles()` | profiles | Low | Admin-only, no fix needed |
| **storage.ts** | `getProfile(id)` | profiles | Low | Read-only, profile agnostic |
| **storage.ts** | `getProfileByUsername()` | profiles | Low | Lookup by username, OK |
| **storage.ts** | `updateProfile(id, updates)` | profiles | Medium | Add org membership check |
| **storage.ts** | `getLeadershipProfiles()` | profiles | Low | Global query for directory |
| **storage.ts** | `getProjects()` | projects | **HIGH** | Filter by org_id OR user_id |
| **storage.ts** | `getProject(id)` | projects | **HIGH** | Verify org membership + project access |
| **storage.ts** | `getSites()` | aethex_sites | **HIGH** | Filter by org_id |
| **storage.ts** | `createSite(site)` | aethex_sites | **HIGH** | Require org_id, verify membership |
| **storage.ts** | `updateSite(id, updates)` | aethex_sites | **HIGH** | Verify org ownership of site |
| **storage.ts** | `deleteSite(id)` | aethex_sites | **HIGH** | Verify org ownership |
| **storage.ts** | `getAchievements()` | achievements | Low | Global catalog, no fix needed |
| **storage.ts** | `getUserAchievements(userId)` | user_achievements | Medium | User-scoped, consider org filter |
| **storage.ts** | `getUserPassport(userId)` | aethex_passports | Low | User identity, org-agnostic |
| **storage.ts** | `createUserPassport(userId)` | aethex_passports | Low | User identity, org-agnostic |
| **storage.ts** | `getApplications()` | applications | Medium | Consider org_id filter |
| **storage.ts** | `updateApplication(id, updates)` | applications | Medium | Verify org ownership |
| **storage.ts** | `getAlerts()` | aethex_alerts | **HIGH** | Filter by site → org |
| **storage.ts** | `updateAlert(id, updates)` | aethex_alerts | **HIGH** | Verify org ownership of alert |
| **storage.ts** | `getNotifications()` | notifications | Low (Rule C) | Personal scope: user-scoped by design, org scope would hide personal notifications |
| **storage.ts** | `getOpportunities()` | aethex_opportunities | **HIGH** | Filter by org_id |
| **storage.ts** | `getOpportunity(id)` | aethex_opportunities | **HIGH** | Verify org ownership |
| **storage.ts** | `createOpportunity(data)` | aethex_opportunities | **HIGH** | Require org_id |
| **storage.ts** | `updateOpportunity(id, updates)` | aethex_opportunities | **HIGH** | Verify org ownership |
| **storage.ts** | `deleteOpportunity(id)` | aethex_opportunities | **HIGH** | Verify org ownership |
| **storage.ts** | `getEvents()` | aethex_events | **HIGH** | Filter by org_id (or public) |
| **storage.ts** | `getEvent(id)` | aethex_events | Medium | Public events OK, private needs check |
| **storage.ts** | `createEvent(data)` | aethex_events | **HIGH** | Require org_id |
| **storage.ts** | `updateEvent(id, updates)` | aethex_events | **HIGH** | Verify org ownership |
| **storage.ts** | `deleteEvent(id)` | aethex_events | **HIGH** | Verify org ownership |
| **storage.ts** | `getChatHistory(userId)` | chat_messages | Low | User-scoped AI memory, OK |
| **storage.ts** | `saveChatMessage()` | chat_messages | Low | User-scoped AI memory, OK |
| **storage.ts** | `clearChatHistory(userId)` | chat_messages | Low | User-scoped AI memory, OK |
| **routes.ts** | `GET /api/profiles` | profiles | Low | Admin-only, directory |
| **routes.ts** | `PATCH /api/profiles/:id` | profiles | Medium | Admin-only, but should log org context |
| **routes.ts** | `GET /api/projects` | projects | **Fixed** | Already org-scoped in new implementation |
| **routes.ts** | `GET /api/projects/:id` | projects | **Fixed** | Uses assertProjectAccess |
| **routes.ts** | `GET /api/sites` | aethex_sites | **HIGH** | Admin-only, but should show org filter |
| **routes.ts** | `POST /api/sites` | aethex_sites | **HIGH** | Require org_id in body |
| **routes.ts** | `PATCH /api/sites/:id` | aethex_sites | **HIGH** | Verify org ownership |
| **routes.ts** | `DELETE /api/sites/:id` | aethex_sites | **HIGH** | Verify org ownership |
| **routes.ts** | `GET /api/opportunities` | aethex_opportunities | Medium | Public listings OK, but add org filter param |
| **routes.ts** | `GET /api/opportunities/:id` | aethex_opportunities | Medium | Public view OK |
| **routes.ts** | `POST /api/opportunities` | aethex_opportunities | **HIGH** | Require org_id |
| **routes.ts** | `PATCH /api/opportunities/:id` | aethex_opportunities | **HIGH** | Verify org ownership |
| **routes.ts** | `DELETE /api/opportunities/:id` | aethex_opportunities | **HIGH** | Verify org ownership |
| **routes.ts** | `GET /api/events` | aethex_events | Medium | Public events OK, add org filter |
| **routes.ts** | `GET /api/events/:id` | aethex_events | Low | Public view OK |
| **routes.ts** | `POST /api/events` | aethex_events | **HIGH** | Require org_id |
| **routes.ts** | `PATCH /api/events/:id` | aethex_events | **HIGH** | Verify org ownership |
| **routes.ts** | `DELETE /api/events/:id` | aethex_events | **HIGH** | Verify org ownership |
| **routes.ts** | `GET /api/files` | files | **HIGH** | Filter by org_id (in-memory currently) |
| **routes.ts** | `POST /api/files` | files | **HIGH** | Require org_id |
| **routes.ts** | `PATCH /api/files/:id` | files | **HIGH** | Verify org ownership |
| **routes.ts** | `DELETE /api/files/:id` | files | **HIGH** | Verify org ownership + project link |
| **routes.ts** | `GET /api/os/entitlements/resolve` | aethex_entitlements | Low | Subject-based, org-agnostic by design |
| **routes.ts** | `POST /api/os/entitlements/issue` | aethex_entitlements | Low | Issuer-based, cross-org by design |
| **routes.ts** | `POST /api/os/entitlements/revoke` | aethex_entitlements | Low | Issuer-based, cross-org by design |
| **websocket.ts** | `setupWebSocket()` - metrics | Multiple tables | Low | Admin dashboard, aggregate stats OK |
| **websocket.ts** | `setupWebSocket()` - alerts | aethex_alerts | **HIGH** | Should filter by user's orgs |
---
## High-Risk Patterns Identified
### 1. **Sites Management (aethex_sites)**
- **Issue:** All CRUD operations lack org_id filtering
- **Impact:** Users could potentially access/modify sites from other orgs
- **Fix:**
- Add `WHERE organization_id = req.orgId` to all queries
- Require org context middleware
- Admin override for cross-org view
### 2. **Opportunities & Events**
- **Issue:** Create/update/delete operations don't verify org ownership
- **Impact:** Users could modify opportunities from other organizations
- **Fix:**
- Add org_id validation on create
- Check `WHERE organization_id = req.orgId` on update/delete
- Keep GET endpoints public but add optional org filter
### 3. **Files System**
- **Issue:** Currently in-memory, but no org scoping when it migrates to DB
- **Impact:** File access could leak across orgs
- **Fix:**
- Add org_id to all file operations
- Link files to projects for additional access control
### 4. **Alerts**
- **Issue:** Alerts fetched globally, not scoped to user's org sites
- **Impact:** Users see alerts from sites they don't own
- **Fix:**
- Join alerts → sites → org_id
- Filter by user's organization memberships
---
## Medium-Risk Patterns
### 1. **Profile Updates**
- **Current:** Any authenticated user can update any profile by ID (admin-only in routes)
- **Risk:** Could be used to tamper with org member profiles
- **Fix:** Verify requester is same user OR org admin/owner
### 2. **Applications**
- **Current:** No org filtering on list or update
- **Risk:** Application status changes could leak across orgs
- **Fix:** Filter by opportunity → org_id
### 3. **Notifications (Rule C: Personal Scope)**
- **Current:** User-scoped notifications for personal activity
- **Classification:** Low risk - intentionally personal, not org-shared
- **Justification:** Notifications are per-user by design and should not be org-scoped. Applying org filters would incorrectly hide personal notifications from users across their organizations.
---
## Recommended Immediate Actions
### Priority 1 (Implement First)
1. **Storage.ts refactor:**
- Add `orgId` parameter to all `get*` methods for entities with org_id
- Add org verification to all `create*/update*/delete*` methods
2. **Routes.ts updates:**
- Apply `attachOrgContext` middleware globally
- Add org_id validation to all POST endpoints
- Add org ownership checks to all PATCH/DELETE endpoints
3. **WebSocket updates:**
- Filter alerts by user's org memberships
- Scope metrics to current org when org context available
### Priority 2 (Phase 2)
1. Add optional org_id query param to public endpoints (opportunities, events)
2. Implement cross-org read permissions for "public" entities
3. Add audit logging for cross-org access attempts
### Priority 3 (Future)
1. Implement row-level security (RLS) policies in Supabase
2. Add org-scoped analytics and rate limiting
3. Create org transfer/merge capabilities with audit trail
---
## Code Pattern Examples
### ❌ Current (Vulnerable)
```typescript
async getSites(): Promise<AethexSite[]> {
const { data, error } = await supabase
.from('aethex_sites')
.select('*')
.order('last_check', { ascending: false });
return data as AethexSite[];
}
```
### ✅ Recommended
```typescript
async getSites(orgId?: string): Promise<AethexSite[]> {
let query = supabase
.from('aethex_sites')
.select('*');
if (orgId) {
query = query.eq('organization_id', orgId);
}
const { data, error } = await query
.order('last_check', { ascending: false });
return data as AethexSite[];
}
```
---
## Summary Statistics
- **Critical org_id missing:** 15 endpoints
- **Needs access control:** 18 endpoints
- **Admin-only (OK):** 9 endpoints
- **Estimated effort:** 3-5 days for full remediation
- **Breaking changes:** None (additive only)
---
**Next Steps:** Proceed with Project Graph canonical design and then implement fixes systematically.