Tenancy RLS enforcement enablement (Sprint S9)
Status: PLANNING / TEST-SCAFFOLD — do not set
tenancy_rls_enforce to permissive or strict in shared / production
envs from this ticket. Default remains off.
Companion: principal plan S9, AGENTS hard rule 51, Track D+F data-layer
work, .cursor/rules/tenancy-strategy.mdc, ADR 036.
1. Flag inventory (verified)
Source of truth: alphaswarm/config/settings.py.
| Settings field | Env var | Default | Modes |
|---|---|---|---|
tenancy_rls_enforce | ALPHASWARM_TENANCY_RLS_ENFORCE | "off" | off | permissive | strict |
tenancy_default_strategy | ALPHASWARM_TENANCY_DEFAULT_STRATEGY | "shared_schema_rls" | Strategy kind when org has no override |
tenancy_db_per_enterprise_pool_ttl_seconds | ALPHASWARM_TENANCY_DB_PER_ENTERPRISE_POOL_TTL_SECONDS | 1800 | Engine cache TTL for db-per-enterprise |
Mode semantics (from settings + tenancy-strategy rule)
| Mode | Runtime role behaviour | App effect |
|---|---|---|
off | Connect as BYPASSRLS role (policies installed by Alembic 0063 still exist) | Existing routes keep working; defense-in-depth RLS not exercised |
permissive | Connect as non-BYPASSRLS app_runtime; log when app.current_organization_id / workspace GUC is missing | Isolation active; missing GUC logged, not always fatal |
strict | Same as permissive but missing GUC → permission_denied | Isolation + fail-closed |
Related implementation:
- GUC writers:
alphaswarm/tenancy/strategies/shared_schema_rls.py(set_configforapp.current_organization_id,app.current_workspace_id,app.current_cell_id) - Contextvar:
alphaswarm/tenancy/runtime_context.py - Policy registry:
alphaswarm/tenancy/rls_policies.py+ Alembic0063_tenancy_strategy.py - Roles:
app_runtime(no BYPASSRLS),app_migrator(BYPASSRLS) created in 0063
Note: As of S9 planning, tenancy_rls_enforce is defined on
Settings and documented for rollout; application code that switches
DB roles based on the mode must be verified before staging enablement.
Do not flip shared envs until that wiring + §4 tests pass.
Hermetic unit coverage that already exists (no Postgres RLS required):
tests/tenancy/test_strategies.py— metaclass, factory, runtime contextvar, schema naming
2. Gap — missing RLS-on integration tests (Track D+F)
| Gap | Detail |
|---|---|
No tests/tenancy/test_rls_isolation.py body | test_strategies.py docstring already pointed at this module; S9 adds the scaffold only |
| SQLite no-ops GUCs | SharedSchemaRLSStrategy skips set_config when dialect ≠ PostgreSQL — default CI cannot prove cross-tenant deny |
Default CI must stay tenancy_rls_enforce=off | Enabling RLS in the shared pytest Postgres (if any) would break hermetic assumptions |
| Staff / migrator override untested | BYPASSRLS app_migrator path lacks an automated negative/positive pair |
3. Proposed concrete test cases
Target module: tests/tenancy/test_rls_isolation.py (scaffold landed in S9).
| ID | Case | Assert | Needs live Postgres+RLS? |
|---|---|---|---|
| R1 | Settings default | settings.tenancy_rls_enforce == "off" | No — hermetic |
| R2 | Cross-tenant deny | Org A session cannot SELECT Org B workspace_id rows on an RLS_TABLES member (e.g. paper_trading_runs) | Yes |
| R3 | Workspace GUC | With org GUC set but wrong/missing workspace GUC under workspace-scoped policy, row invisible or denied | Yes |
| R4 | Organization GUC | Missing app.current_organization_id under strict → permission_denied / empty set per policy | Yes |
| R5 | Staff / migrator override | Connection as BYPASSRLS app_migrator (or documented admin DSN) can read across tenants for maintenance | Yes |
| R6 | public_data carve-out | Table under public_data remains readable with permissive USING (true) | Yes |
| R7 | Contextvar → GUC | set_runtime_context workspace/cell ids appear in current_setting after strategy session checkout | Yes (Postgres); hermetic covers contextvar only |
Marker: requires_postgres_rls. Opt-in env:
ALPHASWARM_RUN_POSTGRES_RLS=1.
4. Scaffold policy (S9 deliverable)
- Default
pytest: runs hermetic assertions only (R1 + documentation skips for R2–R7). - Opt-in recipe (local / staging CI job — not default CI):
# 1) Postgres with migration 0063+ applied and app_runtime role
docker compose -f alphaswarm_platform/compose/docker-compose.yml up -d postgres
docker exec alphaswarm-api alembic upgrade head
# 2) Connect app as app_runtime with TENANCY_RLS_ENFORCE=permissive
# (staging overlay only — do not change shared defaults)
# 3) Run marker-gated suite
ALPHASWARM_RUN_POSTGRES_RLS=1 \
pytest tests/tenancy/test_rls_isolation.py -m requires_postgres_rls -q
Until the live fixture exists, R2–R7 bodies pytest.skip with a
message pointing at this runbook — they must not fail default CI and
must not enable RLS for the whole suite.
5. Staging enablement path (after tests exist)
- Keep checked-in default
tenancy_rls_enforce="off". - Staging overlay:
ALPHASWARM_TENANCY_RLS_ENFORCE=permissive. - Monitor logs for missing-GUC warnings for ≥ 24 h.
- Promote staging to
strictonly after zero unexplained denials. - Prod enablement is a separate change request — out of scope for S9.
Rollback
Set ALPHASWARM_TENANCY_RLS_ENFORCE=off and restart API/workers so
connections return to the BYPASSRLS role. Policies remain installed
(safe).
6. Operator checklist
- Confirmed
settings.tenancy_rls_enforcedefault is still"off" - Did not enable RLS in default CI
- Reviewed scaffold at
tests/tenancy/test_rls_isolation.py - (Later) Staging
permissivesoak recorded - (Later) Cross-tenant deny + staff override tests green under
ALPHASWARM_RUN_POSTGRES_RLS=1 - (Later) Prod left at
offuntil signed enablement CR