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:
apps/api/src/main/resources/db/migration/V156__rls_bilko_app_grants_and_definer_search_path.sqlapps/api/src/test/kotlin/no/alai/bilko/security/RlsBilkoAppGrantsV156IntegrationTest.ktapps/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_appor 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
aclexplodeand compares each table's complete privilege set exactly; - rejects missing privileges, unexpected privileges, and
WITH GRANT OPTIONwidening; - 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:
- migrates the real chain to V155;
- proves representative reads fail at the ACL layer before V156;
- injects an unexpected
TRIGGERgrant and proves the real V156 migration fails transactionally and names the widening; - removes only the test drift and runs the real V156 migration;
- requires one successful Flyway V156 row and one
schema_versionmarker156.0.0; - checks exact direct ACL sets for all 22 tables;
- checks all six exact function OIDs,
SECURITY DEFINER, owner resolution, and exact search path; - 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..candidaterange; three commits scanned, no leaks. - Trivy 0.72.0: PASS on FORGE against a digest-verified
git archiveof the exact candidate with--severity HIGH,CRITICAL --exit-code 1 --no-progress --ignorefile .trivyignore; no--ignore-unfixedor 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=trueandmutations=0;- V156 is not in Flyway or
schema_versionhistory; - 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_appexists, 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-grants→b9b373f9ca79835729c09f0ea80a55cfe7be1ef9. - Azure PR validation build 1036:
completed/succeeded, source version64bd26b16754594494dfbc4072c2e210e8d310e3(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; canonicalazdo/mainequals 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.
No comments to display
No comments to display