Phase B: RLS kernel (alloy_app + FORCE RLS + PG isolation) #1

Merged
parley merged 2 commits from finish/phase-b-rls into develop 2026-08-02 04:56:26 +00:00
Owner

Summary

Phase B multi-tenant security kernel (roadmap B1–B6):

  • Non-superuser DB role alloy_app (no superuser / no BYPASSRLS) + grants
  • FORCE ROW LEVEL SECURITY on all tenant tables
  • Single RLS GUC path: set_config in backend/app/core/rls.py only
  • Auth bootstrap under RLS (app.auth_user_by_email, password-reset SECURITY DEFINER helpers)
  • Postgres isolation tests that fail if the app connects as superuser
  • CI: migrate as alloy, pytest as alloy_app (service host postgres)
  • Also lands docs/repo-conventions.md (merge/PR protocol for finish spine)

Not in this PR (residuals): B7 authz unify, B8 refresh/logout, B9 RBAC matrix — see docs/residual-review-findings/finish-phase-b-rls.md.

Evidence

  • docs/plans/2026-08-01-phase-b-rls/VERIFY.md
  • Local: ruff green; 204 unit (not postgres); 5 postgres (alloy_app)
  • Red check: superuser ALLOY_DATABASE_URL fails test_app_role_is_not_superuser

Ops after merge

# App runtime
ALLOY_DATABASE_URL=postgresql://alloy_app:alloy_app_dev@…/alloy
# Migrate / seed
ALLOY_DATABASE_ADMIN_URL=postgresql://alloy:…@…/alloy
(cd backend && uv run alembic upgrade head)  # as admin
# Production: ALTER ROLE alloy_app PASSWORD '…'  (do not keep bootstrap)

Test plan

  • CI: Backend (ruff + pytest) green
  • CI: Backend (Postgres RLS) green
  • After merge: update env URLs; re-seed if needed

Spec / plan

  • docs/specs/2026-08-01-phase-b-rls/SPEC.md
  • Factory: spec → plan → tdd → verify → security residuals
## Summary Phase B multi-tenant security kernel (roadmap B1–B6): - Non-superuser DB role `alloy_app` (no superuser / no BYPASSRLS) + grants - `FORCE ROW LEVEL SECURITY` on all tenant tables - Single RLS GUC path: `set_config` in `backend/app/core/rls.py` only - Auth bootstrap under RLS (`app.auth_user_by_email`, password-reset SECURITY DEFINER helpers) - Postgres isolation tests that **fail** if the app connects as superuser - CI: migrate as `alloy`, pytest as `alloy_app` (service host `postgres`) - Also lands `docs/repo-conventions.md` (merge/PR protocol for finish spine) **Not in this PR (residuals):** B7 authz unify, B8 refresh/logout, B9 RBAC matrix — see `docs/residual-review-findings/finish-phase-b-rls.md`. ## Evidence - `docs/plans/2026-08-01-phase-b-rls/VERIFY.md` - Local: ruff green; `204` unit (`not postgres`); `5` postgres (alloy_app) - Red check: superuser `ALLOY_DATABASE_URL` fails `test_app_role_is_not_superuser` ## Ops after merge ```bash # App runtime ALLOY_DATABASE_URL=postgresql://alloy_app:alloy_app_dev@…/alloy # Migrate / seed ALLOY_DATABASE_ADMIN_URL=postgresql://alloy:…@…/alloy (cd backend && uv run alembic upgrade head) # as admin # Production: ALTER ROLE alloy_app PASSWORD '…' (do not keep bootstrap) ``` ## Test plan - [ ] CI: Backend (ruff + pytest) green - [ ] CI: Backend (Postgres RLS) green - [ ] After merge: update env URLs; re-seed if needed ## Spec / plan - `docs/specs/2026-08-01-phase-b-rls/SPEC.md` - Factory: spec → plan → tdd → verify → security residuals
Provision non-superuser alloy_app role with grants, FORCE ROW LEVEL
SECURITY on tenant tables, and auth SECURITY DEFINER helpers via
migration 004. Single GUC path uses set_config in core/rls.py.
Postgres-lane tests fail if the app connects as superuser or isolation
breaks. Runtime uses ALLOY_DATABASE_URL (app); migrations/seeds use
ALLOY_DATABASE_ADMIN_URL. B7–B9 residual.

Evidence: docs/plans/2026-08-01-phase-b-rls/VERIFY.md
docs: binding repo merge conventions (develop spine, PR protocol)
All checks were successful
CI / Backend (Postgres RLS) (pull_request) Successful in 25s
CI / Backend (ruff + pytest) (pull_request) Successful in 1m0s
65319dc9f8
Establish docs/repo-conventions.md so sessions do not invent branch/merge
state. Clarify Phase A is on develop; Phase B is branch-only until PR merges.
Wire AGENTS, discipline, HANDOFF, rebuild constitution to the canonical file.
parley merged commit f0ed1ddbed into develop 2026-08-02 04:56:26 +00:00
parley referenced this pull request from a commit 2026-08-02 04:56:27 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
numencore/alloy!1
No description provided.