mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-18 06:17:21 +00:00
- Add revenue_events table to track org/project revenue with source tracking - Add Drizzle schema for revenue_events with proper org/project references - Create migration 0006_revenue_events.sql with indexes - Fix migration 0004: Remove FK constraints to profiles.id (auth schema incompatibility) - Document auth.users/profiles.id type mismatch (UUID vs VARCHAR) - Harden profile update authorization (self-update or org admin/owner only) - Complete org-scoping security audit implementation (42 gaps closed)
10 KiB
10 KiB
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.orgIdto all queries - Require org context middleware
- Admin override for cross-org view
- Add
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.orgIdon 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)
-
Storage.ts refactor:
- Add
orgIdparameter to allget*methods for entities with org_id - Add org verification to all
create*/update*/delete*methods
- Add
-
Routes.ts updates:
- Apply
attachOrgContextmiddleware globally - Add org_id validation to all POST endpoints
- Add org ownership checks to all PATCH/DELETE endpoints
- Apply
-
WebSocket updates:
- Filter alerts by user's org memberships
- Scope metrics to current org when org context available
Priority 2 (Phase 2)
- Add optional org_id query param to public endpoints (opportunities, events)
- Implement cross-org read permissions for "public" entities
- Add audit logging for cross-org access attempts
Priority 3 (Future)
- Implement row-level security (RLS) policies in Supabase
- Add org-scoped analytics and rate limiting
- Create org transfer/merge capabilities with audit trail
Code Pattern Examples
❌ Current (Vulnerable)
async getSites(): Promise<AethexSite[]> {
const { data, error } = await supabase
.from('aethex_sites')
.select('*')
.order('last_check', { ascending: false });
return data as AethexSite[];
}
✅ Recommended
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.