Skip to main content

MC 106880 — PR 325 V156 RLS Grant-Gap Closure

Fix report — MC #106880 / PR 325

Date: 2026-08-10
Canonical target branch: azdo/main at 64edfec05b6cee2d495f1c5e0c9c3bf2d662b5a1
Candidate: b9b373f9ca79835729c09f0ea80a55cfe7be1ef9
Worktree: /Users/makinja/projects/bilko-pr325-post323-r1
Status at this report revision: PR 325 merged no-fast-forward as 1e98d1c29b8a817eb917d01ac4a8de8ff07a43e2 after exact-candidate local and Azure gates passed. No deployment or connection-role cutover occurred.

Scope

PR 323 made V154 and V155 immutable main history. PR 325 therefore owns exactly one new migration, V156. The repository diff contains three directly required files:

  1. apps/api/src/main/resources/db/migration/V156__rls_bilko_app_grants_and_definer_search_path.sql
  2. apps/api/src/test/kotlin/no/alai/bilko/security/RlsBilkoAppGrantsV156IntegrationTest.kt
  3. apps/api/src/test/kotlin/no/alai/bilko/migrations/V134MigrationTest.kt

The third file is required because V156 intentionally changes organizations, invoices, and expenses from ACL-level permission denial to RLS-level zero visibility for bilko_app. The full integration gate caught the inherited test's obsolete permission-denied expectation as the sole unreviewed regression. The corrected test now requires each query to reach RLS and return zero rows under the unrelated support-agent GUC; no baseline entry was added.

No application connection setting, credential, role attribute, policy, deployment manifest, or baseline file changes in this PR.

V156 behavior

Exact 22-table privilege matrix

Table Exact direct privileges for bilko_app
accounts SELECT, INSERT, UPDATE, DELETE
bank_accounts SELECT, INSERT, UPDATE
bank_transactions SELECT, INSERT, UPDATE
contacts SELECT, INSERT, UPDATE, DELETE
expense_documents SELECT, INSERT
expenses SELECT, INSERT, UPDATE, DELETE
inbox_items SELECT, INSERT, UPDATE
invoice_items SELECT, INSERT, DELETE
invoices SELECT, INSERT, UPDATE, DELETE
logged_actions SELECT, INSERT
offer_items SELECT, INSERT, UPDATE
offers SELECT, INSERT, UPDATE
transactions SELECT, INSERT, UPDATE
travel_orders SELECT, INSERT, UPDATE
users SELECT, INSERT, UPDATE, DELETE
audit_log SELECT, INSERT
impersonation_sessions SELECT, INSERT, UPDATE
invoice_templates SELECT, INSERT, UPDATE, DELETE
notifications SELECT, INSERT, UPDATE
organizations SELECT, INSERT, UPDATE, DELETE
password_reset_tokens SELECT, INSERT, UPDATE, DELETE
user_invitations SELECT, INSERT, UPDATE

The migration:

  • fails if bilko_app or any target table is missing;
  • requires the migration identity to be a superuser or each table's owner;
  • applies only the reviewed matrix;
  • reads direct ACLs with aclexplode and compares each table's complete privilege set exactly;
  • rejects missing privileges, unexpected privileges, and WITH GRANT OPTION widening;
  • executes transactionally, so a failed post-condition does not leave partial grants.

Exact six SECURITY DEFINER functions

V156 resolves and hardens these exact signatures:

  • bilko_auth.find_org_by_registration_number(text)
  • public.purge_expired_partition(text,text)
  • bilko_auth.find_rejected_einvoices_for_period(timestamptz,timestamptz)
  • bilko_auth.mark_einvoice_report_transmission(uuid,varchar,timestamptz,varchar)
  • bilko_auth.find_user_by_email(text)
  • bilko_auth.find_user_by_id(uuid)

Before alteration it requires every exact OID to exist, remain SECURITY DEFINER, and be owned by the migration identity unless that identity is superuser. It rejects unexpected same-name overloads. After alteration it requires the exact catalog value search_path=public, pg_temp on every exact OID.

Forward-only rule

V156 has no down migration and must not be edited after application. An emergency reversal would require a new separately reviewed forward migration. This session does not apply V156 to any shared database.

Runtime proof

RlsBilkoAppGrantsV156IntegrationTest uses Testcontainers PostgreSQL 16 and repository migration resources. It:

  1. migrates the real chain to V155;
  2. proves representative reads fail at the ACL layer before V156;
  3. injects an unexpected TRIGGER grant and proves the real V156 migration fails transactionally and names the widening;
  4. removes only the test drift and runs the real V156 migration;
  5. requires one successful Flyway V156 row and one schema_version marker 156.0.0;
  6. checks exact direct ACL sets for all 22 tables;
  7. checks all six exact function OIDs, SECURITY DEFINER, owner resolution, and exact search path;
  8. proves Org A and Org B each see only their own rows, cannot see the other tenant, and empty context fails closed on direct and subquery policy shapes.

Focused exact-patch evidence was bound to the committed candidate by matching the pre-commit and committed binary-diff object IDs:

  • /Users/makinja/system/evidence/106880/pr325-candidate-commit-2026-08-10.json
  • /Users/makinja/system/evidence/106880/pr325-v134-v156-final-working-tree-2026-08-10.json

Gate results bound to b9b373f9ca79835729c09f0ea80a55cfe7be1ef9

  • Java 21 compile + CI-mode Detekt: PASS.
  • Full unit run: 1,903 tests; five existing failures; blocking exact baseline check PASS with zero unreviewed failures and no baseline growth.
  • Full integration run: 2,217 tests; 76 existing failures; blocking exact baseline check PASS with zero unreviewed failures and no baseline growth. V134: 7/7 PASS. V156: 1/1 PASS. Zero skipped in both security suites.
  • Gitleaks: PASS over the three-commit azdo/main..candidate range; three commits scanned, no leaks.
  • Trivy 0.72.0: PASS on FORGE against a digest-verified git archive of the exact candidate with --severity HIGH,CRITICAL --exit-code 1 --no-progress --ignorefile .trivyignore; no --ignore-unfixed or suppression change.

Evidence:

  • /Users/makinja/system/evidence/106880/pr325-exact-candidate-static-unit-b9b373f9-retry1-2026-08-10.json
  • /Users/makinja/system/evidence/106880/pr325-exact-candidate-full-integration-b9b373f9-2026-08-10.json
  • /Users/makinja/system/evidence/106880/pr325-gitleaks-b9b373f9-2026-08-10.json
  • /Users/makinja/system/evidence/106880/pr325-trivy-forge-b9b373f9-2026-08-10.json

Shared-target preflight

A single BEGIN TRANSACTION READ ONLY catalog probe against bilko-demo-pg/bilko confirmed:

  • transaction_read_only=true and mutations=0;
  • V156 is not in Flyway or schema_version history;
  • no failed Flyway rows;
  • all 22 tables exist, have RLS enabled, and are owned by bilko_admin;
  • all six exact function signatures exist;
  • no unexpected same-name overload;
  • bilko_app exists, cannot log in, is not superuser, and does not bypass RLS;
  • latest applied shared-target migration is V153.

Evidence: /Users/makinja/system/evidence/106880/pr325-v156-live-readonly-preflight.json.

No Flyway migrate/repair/clean, DDL, DML, SET ROLE, connection-role cutover, or deployment was executed against the shared target.

Governance gate

The corrected forge is /Users/makinja/system/prompts/forged/106880.md; semantic validation is /Users/makinja/system/evidence/106880/prompt-forge-corrected-semantic-validation-2026-08-10.json. It maps D1–D10 to executable AC#1–AC#10 and preserves attributed dissent. Mehanik returned CLEAR TO DISPATCH before implementation resumed.

Azure and merge completion

  • Non-force source push: refs/heads/fix/106880-rls-grantsb9b373f9ca79835729c09f0ea80a55cfe7be1ef9.
  • Azure PR validation build 1036: completed/succeeded, source version 64bd26b16754594494dfbc4072c2e210e8d310e3 (the exact synthetic merge of reviewed base and candidate).
  • Eight non-skipped Azure jobs succeeded: Node web quality, Kotlin compile, Kotest baseline, full integration baseline, security scans, sort contract, AI review, and Detekt.
  • Blocking Build policy: approved, current, not expired, build 1036.
  • Azure AI review for build 1036: CRITICAL none; SHOULD-FIX none; two non-blocking documentation nits.
  • Independent exact-diff review: PASS, P0=0/P1=0.
  • Merge: no-fast-forward commit 1e98d1c29b8a817eb917d01ac4a8de8ff07a43e2, parents are the reviewed base and candidate.
  • Merge message contains [skip ci]; source branch was deleted; canonical azdo/main equals the merge commit.
  • Thirty seconds after completion, Azure had no main build with the merge SHA. No deployment was requested or executed.

Evidence:

  • /Users/makinja/system/evidence/106880/pr325-final-independent-review.json
  • /Users/makinja/system/evidence/106880/pr325-build-1036-final.json
  • /Users/makinja/system/evidence/106880/pr325-build-1036-ai-review-summary.json
  • /Users/makinja/system/evidence/106880/pr325-final-premerge-gate.json
  • /Users/makinja/system/evidence/106880/pr325-completed.json

Explicitly excluded

The connection-role cutover remains plan-only in /Users/makinja/system/evidence/106880/connection-role-cutover-plan.md and requires a separate explicit CEO decision. Bez deploya: no deploy, traffic change, secret/config mutation, role cutover, or shared-database migration is part of this PR session.