devaanand commited on
Commit
8cc4299
·
1 Parent(s): 5ad96fc

Enable Row-Level Security on public tables in Supabase at startup

Browse files
Files changed (3) hide show
  1. MANUAL_TESTING.md +3 -0
  2. backend/app/db.py +29 -0
  3. backend/app/main.py +10 -1
MANUAL_TESTING.md CHANGED
@@ -164,6 +164,9 @@ Checklist before giving the URL to real users:
164
  - [ ] `CCR_COOKIE_SECURE=1` (HTTPS only)
165
  - [ ] `CCR_DATA_DIR` on a persistent volume (default /tmp is ephemeral)
166
  - [ ] `CCR_ANON_TTL_HOURS=24` (default in the image)
 
 
 
167
  - [ ] Smoke test: sections 2, 4, 6, 7 above
168
 
169
  ## 12. Admin page (/admin)
 
164
  - [ ] `CCR_COOKIE_SECURE=1` (HTTPS only)
165
  - [ ] `CCR_DATA_DIR` on a persistent volume (default /tmp is ephemeral)
166
  - [ ] `CCR_ANON_TTL_HOURS=24` (default in the image)
167
+ - [ ] Supabase: RLS shows enabled on all public tables (the app enables it
168
+ automatically at startup; the Supabase dashboard linter should report
169
+ zero `rls_disabled_in_public` issues)
170
  - [ ] Smoke test: sections 2, 4, 6, 7 above
171
 
172
  ## 12. Admin page (/admin)
backend/app/db.py CHANGED
@@ -92,6 +92,35 @@ def _default_literal(value, dialect_name: str) -> str:
92
  return "'" + str(value).replace("'", "''") + "'"
93
 
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def auto_migrate_sqlite(target_engine, metadata) -> list[str]:
96
  """Add ORM columns missing from existing tables (additive only).
97
 
 
92
  return "'" + str(value).replace("'", "''") + "'"
93
 
94
 
95
+ def lock_down_public_schema(target_engine, metadata) -> list[str]:
96
+ """Enable Row-Level Security on every app table (Postgres only).
97
+
98
+ Supabase auto-exposes the public schema through its REST API (PostgREST):
99
+ any table WITHOUT RLS is readable AND writable by anyone holding the
100
+ project URL + anon key - which for this app would mean users (password
101
+ hashes), corpora, jobs, everything. This app never uses that REST API:
102
+ the backend talks to Postgres directly as the table owner, and owners
103
+ bypass RLS. So RLS-with-no-policies cleanly closes the public door
104
+ without touching app behavior (Supabase linter: rls_disabled_in_public).
105
+
106
+ Runs at every startup AFTER create_all, so tables added later are locked
107
+ down the day they appear, not when someone remembers. Idempotent.
108
+ """
109
+ import logging
110
+
111
+ from sqlalchemy import text
112
+
113
+ if target_engine.dialect.name != "postgresql":
114
+ return [] # SQLite has no exposed REST surface (and no RLS)
115
+ locked = []
116
+ with target_engine.begin() as conn:
117
+ for table in metadata.sorted_tables:
118
+ conn.execute(text(f'ALTER TABLE "{table.name}" ENABLE ROW LEVEL SECURITY'))
119
+ locked.append(table.name)
120
+ logging.getLogger("ccr.db").info("RLS enabled on: %s", ", ".join(locked))
121
+ return locked
122
+
123
+
124
  def auto_migrate_sqlite(target_engine, metadata) -> list[str]:
125
  """Add ORM columns missing from existing tables (additive only).
126
 
backend/app/main.py CHANGED
@@ -30,7 +30,15 @@ from . import registry
30
  from .ccr import FAKE_MODEL_NAME
31
  from .construct_files import parse_construct_file
32
  from .construct_lib import sync_library
33
- from .db import DATA_DIR, Base, SessionLocal, auto_migrate_sqlite, engine, get_db
 
 
 
 
 
 
 
 
34
  from .ingest import IngestError, load_corpus, max_rows as corpus_max_rows, suggest_text_column
35
  from .models import AdminAudit, Construct, Corpus, Invite, Job, Project, RoleAssignment, User
36
  from .reproducibility import (
@@ -104,6 +112,7 @@ async def lifespan(_: FastAPI):
104
  """Create tables and sync the construct library (YAML source of truth) at startup."""
105
  Base.metadata.create_all(engine)
106
  auto_migrate_sqlite(engine, Base.metadata) # additive column adds for existing dev DBs
 
107
  registry.list_models() # fail fast on an invalid models.yaml
108
  db = SessionLocal()
109
  try:
 
30
  from .ccr import FAKE_MODEL_NAME
31
  from .construct_files import parse_construct_file
32
  from .construct_lib import sync_library
33
+ from .db import (
34
+ DATA_DIR,
35
+ Base,
36
+ SessionLocal,
37
+ auto_migrate_sqlite,
38
+ engine,
39
+ get_db,
40
+ lock_down_public_schema,
41
+ )
42
  from .ingest import IngestError, load_corpus, max_rows as corpus_max_rows, suggest_text_column
43
  from .models import AdminAudit, Construct, Corpus, Invite, Job, Project, RoleAssignment, User
44
  from .reproducibility import (
 
112
  """Create tables and sync the construct library (YAML source of truth) at startup."""
113
  Base.metadata.create_all(engine)
114
  auto_migrate_sqlite(engine, Base.metadata) # additive column adds for existing dev DBs
115
+ lock_down_public_schema(engine, Base.metadata) # Supabase: RLS on, REST surface closed
116
  registry.list_models() # fail fast on an invalid models.yaml
117
  db = SessionLocal()
118
  try: