|
| 1 | +# RLS Security Audit — Captain's Log |
| 2 | + |
| 3 | +**Started:** 2026-03-13 |
| 4 | +**Status:** COMPLETE — All 38 tables secured |
| 5 | + |
| 6 | +## Summary |
| 7 | + |
| 8 | +38 out of 92 public tables had NO RLS enabled. Both `anon` and `authenticated` roles had full CRUD + TRUNCATE on all of them. This is a critical multi-tenant data leak. |
| 9 | + |
| 10 | +### Tables requiring RLS (have `tenant_id`, no RLS) — 12 CRITICAL |
| 11 | +| # | Table | Batch | Status | Notes | |
| 12 | +|---|-------|-------|--------|-------| |
| 13 | +| 1 | `transactions` | 1 | ✅ Done | Payment data — highest priority | |
| 14 | +| 2 | `courses` | 2 | ✅ Done | | |
| 15 | +| 3 | `enrollments` | 2 | ✅ Done | | |
| 16 | +| 4 | `products` | 3 | ✅ Done | | |
| 17 | +| 5 | `subscriptions` | 3 | ✅ Done | | |
| 18 | +| 6 | `plans` | 3 | ✅ Done | | |
| 19 | +| 7 | `exams` | 4 | ✅ Done | | |
| 20 | +| 8 | `exam_submissions` | 4 | ✅ Done | | |
| 21 | +| 9 | `exercises` | 5 | ✅ Done | | |
| 22 | +| 10 | `lessons` | 5 | ✅ Done | | |
| 23 | +| 11 | `product_courses` | 6 | ✅ Done | | |
| 24 | +| 12 | `course_categories` | 6 | ✅ Done | | |
| 25 | + |
| 26 | +### Tables requiring RLS (no `tenant_id`, need user-scoped or FK-based) — 26 HIGH |
| 27 | +| # | Table | Batch | Status | Notes | |
| 28 | +|---|-------|-------|--------|-------| |
| 29 | +| 1 | `grades` | 12 | ✅ Done | user_id scoped | |
| 30 | +| 2 | `exam_answers` | 7 | ✅ Done | | |
| 31 | +| 3 | `exam_questions` | 7 | ✅ Done | | |
| 32 | +| 4 | `exam_scores` | 7 | ✅ Done | student_id scoped | |
| 33 | +| 5 | `question_options` | 7 | ✅ Done | | |
| 34 | +| 6 | `exercise_code_student_submissions` | 8 | ✅ Done | user_id scoped | |
| 35 | +| 7 | `exercise_completions` | 8 | ✅ Done | user_id scoped | |
| 36 | +| 8 | `exercise_files` | 8 | ✅ Done | | |
| 37 | +| 9 | `exercise_messages` | 8 | ✅ Done | user_id scoped | |
| 38 | +| 10 | `lesson_comments` | 9 | ✅ Done | user_id scoped | |
| 39 | +| 11 | `lesson_passed` | 9 | ✅ Done | user_id scoped | |
| 40 | +| 12 | `lesson_views` | 9 | ✅ Done | user_id scoped | |
| 41 | +| 13 | `tickets` | 10 | ✅ Done | user_id scoped | |
| 42 | +| 14 | `ticket_messages` | 10 | ✅ Done | user_id scoped | |
| 43 | +| 15 | `submissions` | 10 | ✅ Done | student_id scoped | |
| 44 | +| 16 | `messages` | 11 | ✅ Done | | |
| 45 | +| 17 | `chats` | 11 | ✅ Done | user_id scoped | |
| 46 | +| 18 | `comment_flags` | 11 | ✅ Done | user_id scoped | |
| 47 | +| 19 | `comment_reactions` | 11 | ✅ Done | user_id scoped | |
| 48 | +| 20 | `assignments` | 12 | ✅ Done | | |
| 49 | +| 21 | `roles` | 12 | ✅ Done | Lookup — SELECT only | |
| 50 | +| 22 | `permissions` | 12 | ✅ Done | Lookup — SELECT only | |
| 51 | +| 23 | `role_permissions` | 12 | ✅ Done | Lookup — SELECT only | |
| 52 | +| 24 | `landing_page_templates` | 12 | ✅ Done | Super admin managed | |
| 53 | +| 25 | `plan_courses` | 12 | ✅ Done | Public read | |
| 54 | +| 26 | `exam_views` | 7 | ✅ Done | user_id scoped | |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## Batch Log |
| 59 | + |
| 60 | +### Batch 1 — `transactions` (2026-03-13) |
| 61 | + |
| 62 | +**Migration:** `rls_transactions` |
| 63 | + |
| 64 | +**Policies added:** |
| 65 | +- SELECT: Students see own transactions in tenant |
| 66 | +- SELECT: Teachers/admins see all transactions in tenant |
| 67 | +- SELECT: Super admins see all |
| 68 | +- INSERT: Authenticated users can create own transactions in tenant |
| 69 | +- UPDATE: Authenticated users can update own transactions in tenant (for stripe_payment_intent_id) |
| 70 | +- TRUNCATE revoked from anon + authenticated |
| 71 | + |
| 72 | +**Files that query `transactions`:** |
| 73 | +- `app/[locale]/dashboard/admin/transactions/page.tsx` — SELECT, admin, server client |
| 74 | +- `app/[locale]/dashboard/admin/page.tsx` — SELECT count, admin, server client |
| 75 | +- `app/[locale]/dashboard/student/profile/page.tsx` — SELECT, student, server client |
| 76 | +- `app/[locale]/dashboard/teacher/revenue/page.tsx` — SELECT, teacher, server client |
| 77 | +- `app/[locale]/dashboard/admin/users/[userId]/page.tsx` — SELECT, admin, server client |
| 78 | +- `app/api/stripe/webhook/route.ts` — UPDATE, system, service role (bypasses RLS) |
| 79 | +- `app/api/stripe/create-payment-intent/route.ts` — INSERT + UPDATE, authenticated, server client |
| 80 | +- `app/actions/payment-requests.ts` — INSERT, admin (uses both server + admin client) |
| 81 | +- `app/actions/admin/revenue.ts` — SELECT, admin, server client |
| 82 | +- `app/[locale]/(public)/checkout/actions.ts` — INSERT, authenticated, server client |
| 83 | +- `app/[locale]/platform/tenants/[tenantId]/page.tsx` — SELECT, super admin |
| 84 | +- `components/teacher/transaction-list.tsx` — client display component |
| 85 | + |
| 86 | +**Testing:** |
| 87 | +- [x] Supabase API: anon cannot SELECT — returns `[]` |
| 88 | +- [x] Supabase API: anon cannot INSERT — `permission denied` |
| 89 | +- [x] Supabase API: anon cannot DELETE — `permission denied` |
| 90 | +- [x] Supabase API: authenticated student sees only own transactions |
| 91 | +- [x] Supabase API: authenticated admin sees all tenant transactions |
| 92 | +- [x] Supabase API: student cannot INSERT for another user — `violates row-level security` |
| 93 | +- [x] Supabase API: student cannot UPDATE another user's transaction — returns `[]` |
| 94 | +- [x] Supabase API: service role (webhooks) bypasses RLS correctly |
| 95 | +- [ ] Playwright: admin transactions page loads (skipped — subdomain auth routing blocks Playwright browser; not an RLS issue) |
| 96 | + |
| 97 | +**Issues found:** |
| 98 | +- None — all API-level security tests passed |
| 99 | +- Playwright browser testing deferred: `proxy.ts` subdomain routing redirects unauthenticated Playwright sessions to root domain. This is expected behavior, not an RLS bug. Full E2E test will be done after all batches are complete. |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +### Batch 2 — `courses` + `enrollments` (2026-03-13) |
| 104 | + |
| 105 | +**Migration:** `rls_courses_enrollments` |
| 106 | + |
| 107 | +**Courses policies:** 4 SELECT (anon published, teacher own, admin tenant, super admin), 1 INSERT (teacher/admin), 2 UPDATE (teacher own, admin tenant), 1 DELETE (admin tenant). Anon revoked INSERT/UPDATE/DELETE/TRUNCATE. |
| 108 | + |
| 109 | +**Enrollments policies:** 3 SELECT (student own, teacher/admin tenant, super admin), 1 INSERT (own), 1 UPDATE (admin). Anon revoked all writes. |
| 110 | + |
| 111 | +**Testing:** |
| 112 | +- [x] Anon sees only published courses (4 returned, all `status=published`) |
| 113 | +- [x] Anon INSERT courses — `permission denied` |
| 114 | +- [x] Student sees published courses, cannot INSERT |
| 115 | +- [x] Admin sees all tenant courses |
| 116 | +- [x] Anon SELECT enrollments — empty |
| 117 | +- [x] Anon INSERT enrollments — `permission denied` |
| 118 | +- [x] Student sees only own enrollments (2 returned) |
| 119 | +- [x] Student INSERT for another user — `violates row-level security` |
| 120 | +- [x] Admin sees all tenant enrollments (4 returned) |
| 121 | + |
| 122 | +**Issues found:** None |
| 123 | + |
| 124 | +--- |
| 125 | + |
| 126 | +### Batch 3 — `products` + `subscriptions` + `plans` (2026-03-13) |
| 127 | + |
| 128 | +**Migration:** `rls_products_subscriptions_plans` |
| 129 | + |
| 130 | +**Testing:** |
| 131 | +- [x] Products: anon sees active only, anon INSERT blocked |
| 132 | +- [x] Plans: anon sees non-deleted only, anon INSERT blocked |
| 133 | +- [x] Subscriptions: anon empty, student sees own, admin sees tenant |
| 134 | + |
| 135 | +**Issues found:** None |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +### Batch 4 — `exams` + `exam_submissions` (2026-03-13) |
| 140 | +Migration: `rls_exams_exam_submissions`. Tenant-scoped SELECT/INSERT/UPDATE. Anon blocked. Tested OK. |
| 141 | + |
| 142 | +### Batch 5 — `exercises` + `lessons` (2026-03-13) |
| 143 | +Migration: `rls_exercises_lessons`. Tenant-scoped. Anon blocked. Tested OK. |
| 144 | + |
| 145 | +### Batch 6 — `product_courses` + `course_categories` (2026-03-13) |
| 146 | +Migration: `rls_product_courses_course_categories`. Public SELECT for catalog. Admin CRUD. Anon write blocked. Tested OK. |
| 147 | + |
| 148 | +### Batch 7 — Exam child tables (2026-03-13) |
| 149 | +Migration: `rls_exam_child_tables`. Tables: exam_questions, question_options, exam_answers, exam_scores, exam_views. User-scoped where applicable. Anon blocked. Tested OK. |
| 150 | + |
| 151 | +### Batch 8 — Exercise child tables (2026-03-13) |
| 152 | +Migration: `rls_exercise_child_tables`. Tables: exercise_code_student_submissions, exercise_completions, exercise_files, exercise_messages. User-scoped. Anon blocked. Tested OK. |
| 153 | + |
| 154 | +### Batch 9 — Lesson child tables (2026-03-13) |
| 155 | +Migration: `rls_lesson_child_tables`. Tables: lesson_comments, lesson_passed, lesson_views. User-scoped. Anon blocked. Tested OK. |
| 156 | + |
| 157 | +### Batch 10 — Tickets + submissions (2026-03-13) |
| 158 | +Migration: `rls_tickets_submissions`. Tables: tickets, ticket_messages, submissions. User-scoped. Anon blocked. Tested OK. |
| 159 | + |
| 160 | +### Batch 11 — Chats + comments (2026-03-13) |
| 161 | +Migration: `rls_chats_comments`. Tables: chats, messages, comment_flags, comment_reactions. User-scoped. Anon blocked. Tested OK. |
| 162 | + |
| 163 | +### Batch 12 — Remaining tables (2026-03-13) |
| 164 | +Migration: `rls_remaining_tables`. Tables: grades, assignments, plan_courses, roles, permissions, role_permissions, landing_page_templates. Lookup tables = SELECT only. Sensitive tables = user-scoped. Anon write blocked. Tested OK. |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## Final Verification (2026-03-13) |
| 169 | + |
| 170 | +**Result: 0 tables without RLS.** All 92 public tables now have RLS enabled. |
| 171 | + |
| 172 | +**Final anon security test:** All 38 previously unprotected tables reject anon INSERT with `permission denied` (HTTP 401) or schema validation (HTTP 400). All sensitive tables return 0 rows for anon SELECT. |
| 173 | + |
| 174 | +**Known items for future review:** |
| 175 | +- Some policies use `get_tenant_role()` from JWT claims which defaults to `'student'` — works correctly but could be tightened with `tenant_users` checks in the future |
| 176 | +- `messages` table had a pre-existing SELECT policy that was unused (RLS was disabled) — now active |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## Post-Audit Browser Testing (2026-03-13) |
| 181 | + |
| 182 | +Tested all critical flows via Playwright MCP browser: |
| 183 | + |
| 184 | +| Flow | Status | Notes | |
| 185 | +|------|--------|-------| |
| 186 | +| Student login + dashboard | PASS | Welcome, gamification, sidebar all load | |
| 187 | +| Student browse courses | PASS | 2 courses visible, categories work | |
| 188 | +| Teacher dashboard | PASS | Stats, courses, checklist all load with data | |
| 189 | +| Admin transactions | PASS | Table structure, stats cards | |
| 190 | +| Admin courses | PASS | 2 courses with full management table | |
| 191 | +| Admin enrollments | PASS | 2 enrollments visible with correct data | |
| 192 | +| Admin settings | PASS | All tabs and form fields render | |
| 193 | +| Pricing (public) | PASS | Page loads, FAQ section present | |
| 194 | +| Platform panel (super admin) | PASS | Overview with all stats | |
| 195 | +| Tenant isolation | PASS | Non-member correctly gets join-school redirect | |
| 196 | + |
| 197 | +**No RLS-related breakage found.** |
| 198 | + |
| 199 | +### Issues Found & Fixed |
| 200 | + |
| 201 | +1. **Missing i18n keys for teacher dashboard tour** — 10 MISSING_MESSAGE console errors |
| 202 | + - Keys: `dashboard.teacher.tour.{welcome,stats,courses,sidebarCourses,sidebarCreate}.{title,description}` |
| 203 | + - Fix: Added missing keys to `messages/en.json` and `messages/es.json` |
| 204 | + |
| 205 | +2. **Admin dashboard redirect to /onboarding** — Test users had `onboarding_completed = false` |
| 206 | + - `owner@e2etest.com` and `creator@codeacademy.com` hit the onboarding redirect on admin dashboard |
| 207 | + - Fix: Set `onboarding_completed = true` for admin/teacher test users in DB and updated `supabase/seed.sql` |
| 208 | + |
| 209 | +--- |
0 commit comments