Saltar al contenido principal

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 fieldEnv varDefaultModes
tenancy_rls_enforceALPHASWARM_TENANCY_RLS_ENFORCE"off"off | permissive | strict
tenancy_default_strategyALPHASWARM_TENANCY_DEFAULT_STRATEGY"shared_schema_rls"Strategy kind when org has no override
tenancy_db_per_enterprise_pool_ttl_secondsALPHASWARM_TENANCY_DB_PER_ENTERPRISE_POOL_TTL_SECONDS1800Engine cache TTL for db-per-enterprise

Mode semantics (from settings + tenancy-strategy rule)​

ModeRuntime role behaviourApp effect
offConnect as BYPASSRLS role (policies installed by Alembic 0063 still exist)Existing routes keep working; defense-in-depth RLS not exercised
permissiveConnect as non-BYPASSRLS app_runtime; log when app.current_organization_id / workspace GUC is missingIsolation active; missing GUC logged, not always fatal
strictSame as permissive but missing GUC → permission_deniedIsolation + fail-closed

Related implementation:

  • GUC writers: alphaswarm/tenancy/strategies/shared_schema_rls.py (set_config for app.current_organization_id, app.current_workspace_id, app.current_cell_id)
  • Contextvar: alphaswarm/tenancy/runtime_context.py
  • Policy registry: alphaswarm/tenancy/rls_policies.py + Alembic 0063_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)​

GapDetail
No tests/tenancy/test_rls_isolation.py bodytest_strategies.py docstring already pointed at this module; S9 adds the scaffold only
SQLite no-ops GUCsSharedSchemaRLSStrategy skips set_config when dialect ≠ PostgreSQL — default CI cannot prove cross-tenant deny
Default CI must stay tenancy_rls_enforce=offEnabling RLS in the shared pytest Postgres (if any) would break hermetic assumptions
Staff / migrator override untestedBYPASSRLS 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).

IDCaseAssertNeeds live Postgres+RLS?
R1Settings defaultsettings.tenancy_rls_enforce == "off"No — hermetic
R2Cross-tenant denyOrg A session cannot SELECT Org B workspace_id rows on an RLS_TABLES member (e.g. paper_trading_runs)Yes
R3Workspace GUCWith org GUC set but wrong/missing workspace GUC under workspace-scoped policy, row invisible or deniedYes
R4Organization GUCMissing app.current_organization_id under strict → permission_denied / empty set per policyYes
R5Staff / migrator overrideConnection as BYPASSRLS app_migrator (or documented admin DSN) can read across tenants for maintenanceYes
R6public_data carve-outTable under public_data remains readable with permissive USING (true)Yes
R7Contextvar → GUCset_runtime_context workspace/cell ids appear in current_setting after strategy session checkoutYes (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)​

  1. Keep checked-in default tenancy_rls_enforce="off".
  2. Staging overlay: ALPHASWARM_TENANCY_RLS_ENFORCE=permissive.
  3. Monitor logs for missing-GUC warnings for ≥ 24 h.
  4. Promote staging to strict only after zero unexplained denials.
  5. 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_enforce default is still "off"
  • Did not enable RLS in default CI
  • Reviewed scaffold at tests/tenancy/test_rls_isolation.py
  • (Later) Staging permissive soak recorded
  • (Later) Cross-tenant deny + staff override tests green under ALPHASWARM_RUN_POSTGRES_RLS=1
  • (Later) Prod left at off until signed enablement CR