Skip to content

security: enforce Organization ownership and fail-closed tenant sessions - #36

Merged
stone16 merged 2 commits into
mainfrom
codex/issue-8-organization-rls
Jul 20, 2026
Merged

security: enforce Organization ownership and fail-closed tenant sessions#36
stone16 merged 2 commits into
mainfrom
codex/issue-8-organization-rls

Conversation

@stone16

@stone16 stone16 commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Closes #8

Outcome

Implements the first complete PostgreSQL Organization-isolation evidence slice:

  • adds the global organization security root and exactly one representative tenant-owned organization_record table;
  • enforces explicit Organization ownership, an Organization-inclusive primary key, a no-orphan FK, and an Organization-inclusive self-FK;
  • enables and forces RLS under the non-owner runtime role with matching USING and WITH CHECK predicates;
  • adds a bounded statement-level write-context guard so a missing Organization rejects INSERT/UPDATE/DELETE even when no row reaches RLS;
  • exposes one engine-internal organization_transaction(engine, organization_id) boundary that begins, binds, verifies, and owns the transaction before yielding a connection;
  • classifies every current table in the versioned schema security manifest;
  • keeps Membership, complete ActorContext, Runtime delivery, worker behavior, and generalized future-table RLS tooling inactive.

ADR-0020 records why this issue proves only the Organization GUC slice. DB-009 remains NOT_ACTIVE until the closed ActorContext protocol and its owning negative test exist.

Two-Organization matrix

Database context Own row visible Other row visible Other-row update Other-row delete Wrong-owner insert / reassignment
Organization A 1 0 0 effects 0 effects deterministic RLS error
Organization B 1 0 0 effects 0 effects deterministic RLS error
No Organization (runtime) 0 0 deterministic 42501 deterministic 42501 deterministic 42501

Hard-oracle output for the slice is wrong-Organization effect = 0 and missing-context fallback = 0. Missing-context SELECT returns zero tenant rows; every write is rejected before row filtering, including INSERT ... SELECT ... WHERE false, UPDATE ... WHERE false, and DELETE ... WHERE false.

The pool-reuse case runs 12 alternating Organization/context-free cycles on one backend PID without retries. Each context-free cycle proves both zero visible rows and a 42501 zero-candidate write rejection. Normal completion and raised-exception rollback leave the GUC absent/empty; rollback also leaves zero durable rows.

Database evidence

  • runtime owns no relation and is NOSUPERUSER, NOBYPASSRLS, NOINHERIT;
  • migrator owns organization, organization_record, and the invoker-security guard function;
  • runtime has exactly SELECT, INSERT, UPDATE, DELETE on the representative table and only EXECUTE on the trigger function; it has no access to the Organization root;
  • worker and PUBLIC have zero table/function grants;
  • catalog assertions prove ENABLE ROW LEVEL SECURITY, FORCE ROW LEVEL SECURITY, policy role/command/USING/WITH CHECK, statement trigger timing/events, function body/owner/security/search path, nullability, exact PK/FKs, and manifest/table equality;
  • the historical empty revision remains upgradeable, and 20260720_0002 cleanly downgrades/reapplies.

Mutation evidence

Each mutation was applied only to the disposable test database, observed red, restored, and followed by a clean-volume green suite:

  • NO FORCE ROW LEVEL SECURITY -> owner behavior and catalog tests fail;
  • WITH CHECK (true) -> wrong-Organization insert test fails;
  • drop the Organization-inclusive self-FK -> cross-Organization parent test fails;
  • disable the statement-level write-context trigger -> all three zero-candidate write oracles fail.

Verification

  • make db-reset from an empty disposable database volume
  • make check
    • build: wheel + sdist
    • Ruff: pass
    • strict mypy: 32 source files
    • unit: 49 passed
    • catalog: 35 passed; validator reports 15 invariants / 12 fixtures
    • process: 3 passed
    • real PostgreSQL integration/security: 26 passed
  • two independent final fixed-point reviews: P0/P1/P2 = 0

Scope check

No User, Membership, Source, Revision, Fragment, Package, EffectiveScope, provider, index, cache, HTTP authorization, or generalized authorization framework is added.

Summary by CodeRabbit

  • New Features

    • Added organization-level data isolation with enforced security boundaries for organization-owned records.
    • Added transaction-scoped organization context handling with validation and automatic cleanup.
    • Added migration support for the new organization data structures.
  • Documentation

    • Updated status and evidence documentation to reflect verified organization isolation capabilities.
    • Added an architectural decision record describing the staged rollout.
  • Tests

    • Added coverage for isolation, rollback behavior, context leakage, ownership constraints, and migration reversibility.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@stone16, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4844d153-ee11-4955-90f9-2a6b2fbb304d

📥 Commits

Reviewing files that changed from the base of the PR and between bb219fe and 5bd51e7.

📒 Files selected for processing (7)
  • README.md
  • docs/decisions/0015-rls-transaction-context-and-schema-manifest.md
  • docs/decisions/0020-stage-organization-rls-before-actor-context.md
  • engine/persistence/schema_security_manifest.yaml
  • migrations/versions/20260720_0002_organization_rls.py
  • tests/integration/test_organization_isolation.py
  • tests/unit/test_schema_security_manifest.py
📝 Walkthrough

Walkthrough

Adds a bounded Organization security slice: transaction-local tenant context, Organization and organization_record schema enforcement with FORCE RLS, migration coverage, fail-closed isolation tests, and documentation that keeps broader runtime authorization behavior inactive.

Changes

Organization RLS isolation

Layer / File(s) Summary
Organization schema and RLS enforcement
engine/persistence/schema_security_manifest.yaml, migrations/versions/...
Defines Organization ownership, composite constraints, forced RLS policies, roles, grants, and security metadata.
Transaction-local Organization context
engine/persistence/tenant_context.py, engine/persistence/__init__.py, tests/unit/test_tenant_context.py
Adds UUID validation, transaction-local GUC binding and readback verification, public exports, and commit/rollback unit tests.
Migration and isolation verification
tests/integration/conftest.py, tests/integration/test_migrations.py, tests/integration/test_organization_isolation.py, tests/unit/test_schema_security_manifest.py
Tests migration reversibility, tenant isolation, ownership constraints, missing-context failures, pool reuse, rollback cleanup, FORCE RLS, catalog properties, and manifest consistency.
Staged evidence scope
AGENTS.md, README.md, docs/decisions/*
Documents the Organization RLS evidence boundary and keeps ActorContext, Runtime delivery, Membership, and worker behavior inactive.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested Organization security-root, tenant-owned record, forced RLS, tenant context boundary, and the required isolation tests.
Out of Scope Changes check ✅ Passed The added docs, manifest, context helper, migration, and tests all support the same Organization-isolation slice and do not introduce unrelated scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main changes: Organization ownership enforcement and fail-closed tenant session handling.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-8-organization-rls

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb219fe231

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread migrations/versions/20260720_0002_organization_rls.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
engine/persistence/schema_security_manifest.yaml (1)

1-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

File is strict JSON but carries a .yaml extension.

The content is pure JSON and every consumer parses it with json.loads (tests/unit/test_schema_security_manifest.py line 12 and tests/integration/test_organization_isolation.py line 886). This works today, but a future YAML-style edit (a # comment, anchor, or unquoted key) in a file named .yaml will silently break json.loads at runtime. Either rename to .json (and update the two reader paths) or switch the readers to a YAML loader.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@engine/persistence/schema_security_manifest.yaml` around lines 1 - 90,
Resolve the format-extension mismatch for schema_security_manifest by either
renaming the strict JSON manifest to a .json file and updating both readers in
test_schema_security_manifest and test_organization_isolation, or converting
those readers to a YAML loader while preserving current parsing behavior. Apply
the same choice consistently across the manifest and both consumer paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 12-14: Update every README evidence/status statement that
hardcodes PostgreSQL 17, including the referenced sections, to derive or defer
the version claim to compose.yaml and the recorded evidence report. Keep the
existing validation and NOT_ACTIVE status descriptions unchanged while ensuring
future harness version changes require updating only the source-of-truth files.

---

Nitpick comments:
In `@engine/persistence/schema_security_manifest.yaml`:
- Around line 1-90: Resolve the format-extension mismatch for
schema_security_manifest by either renaming the strict JSON manifest to a .json
file and updating both readers in test_schema_security_manifest and
test_organization_isolation, or converting those readers to a YAML loader while
preserving current parsing behavior. Apply the same choice consistently across
the manifest and both consumer paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50db834e-4d1c-4993-bb63-46f619abb766

📥 Commits

Reviewing files that changed from the base of the PR and between 5679963 and bb219fe.

📒 Files selected for processing (13)
  • AGENTS.md
  • README.md
  • docs/decisions/0020-stage-organization-rls-before-actor-context.md
  • docs/decisions/README.md
  • engine/persistence/__init__.py
  • engine/persistence/schema_security_manifest.yaml
  • engine/persistence/tenant_context.py
  • migrations/versions/20260720_0002_organization_rls.py
  • tests/integration/conftest.py
  • tests/integration/test_migrations.py
  • tests/integration/test_organization_isolation.py
  • tests/unit/test_schema_security_manifest.py
  • tests/unit/test_tenant_context.py

Comment thread README.md Outdated
@stone16

stone16 commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

CodeRabbit manifest-format nit assessed and intentionally not renamed: engine/persistence/schema_security_manifest.yaml is JSON-compatible YAML by repository contract, parsed with the standard-library JSON decoder so bootstrap/migration security checks remain deterministic and dependency-free. YAML-only syntax fails the existing unit/integration gates immediately, and the exact .yaml path is fixed by the implementation design. ADR-0015 now states this contract explicitly in 5bd51e7; adding PyYAML or renaming the design-authority path would be broader, unnecessary churn.

@stone16

stone16 commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

@codex review

Please re-review commit 5bd51e7, especially the statement-level missing-context write guard, zero-candidate write tests, catalog privilege/function assertions, and updated evidence matrix.

@stone16

stone16 commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 5bd51e751b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@stone16
stone16 merged commit 9fa1f03 into main Jul 20, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: enforce Organization ownership and fail-closed tenant sessions

1 participant