AeThex-OS/ORG_SCOPING_AUDIT.md
MrPiglr 4b84eedbd3 feat: Add revenue_events table and fix migration FK constraints
- 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)
2026-01-05 04:54:12 +00:00

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.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.

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)

async getSites(): Promise<AethexSite[]> {
  const { data, error } = await supabase
    .from('aethex_sites')
    .select('*')
    .order('last_check', { ascending: false });
  return data as AethexSite[];
}
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.