Repository navigation
kernel(tenancy): activity gets legal_entity_id and the RESTRICTIVE le_scope policy (#1716) - #1727
Conversation
…_scope policy (#1716) A member limited to one legal entity read another entity's activity rows (voucher codes, amounts) through the proposal timeline and the overview. crove_kernel.activity now carries a nullable legal_entity_id, filled by the BEFORE INSERT trigger b_legal_entity from the record for entity-scoped record types (vouchers, journal entries, purchase documents, fixed assets, portal invoice lists, bank statement imports), backfilled for existing rows, and the RESTRICTIVE le_scope policy that keeps org-level (NULL) rows visible. Proposal rows stay org-level until #1538, so agents keep writing theirs. The RLS matrix checks every nullable legal_entity_id is a listed scope or a listed exception, and probes activity's entity scope; a db test covers the backfill, the timeline and the trigger's rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…guarded (#1716) Review follow-ups: the trigger takes the entity column as a fourth argument, so kernel.legal_entity rows take their own entity; the kernel manifest lists the new column; a unit test keeps every ctx.activity record type either in the trigger's list or org-level with a reason; the header, timeline comment and tests are tightened. Per-entity rows that name no entity-scoped record are follow-up #1725. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 20 minutes. View limit details
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request scopes the activity timeline per legal entity for records that belong to one. It adds a nullable legal_entity_id column to the crove_kernel.activity table, introduces a trigger b_legal_entity to automatically populate this column based on the associated record's entity, and applies a restrictive Row-Level Security (RLS) policy (le_scope) to filter activity rows based on the user's legal entity scope. Additionally, it updates the database schema, manifest, documentation, and adds comprehensive tests to verify the new scoping behavior and RLS policies. Feedback on the tests suggests adding a unique tie-breaker (such as id DESC) to the non-unique timestamp sorting (occurred_at DESC) in a test query to ensure deterministic results.
… id tie-breaker in the overview test (#1716) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Coordinator's independent security review: APPROVE. Stated plainly for the record: this closes the leak through activity rows, but a member limited to entity A can still read entity B's voucher content (lines, amounts, description) through the proposal itself ( |
Closes #1716
A member limited to one legal entity could read another entity's activity rows, such as voucher codes, amounts and descriptions, through the proposal timeline (
kernel.proposal.timeline) and the org overview. This PR scopescrove_kernel.activityby legal entity.What changes
20261103120000_crove_kernel_activity_legal_entity.sql.activitygets a nullablelegal_entity_id, with the foreign key(org_id, legal_entity_id)tolegal_entityand the index(legal_entity_id, org_id)(G3).le_scopeis added. NULL means an org-level row that every member reads. The predicate is the same asrole_assignment_scope.b_legal_entity(crove_kernel.activity_legal_entity_fill, BEFORE INSERT, ENABLE ALWAYS, SECURITY INVOKER). Its arguments come in fours: record type, table, record-id column, entity column. A row of a listed type takes its record's entity, read as the writer.finance.cash_voucher,journal_voucher,fixed_asset,journal_entry,purchase_document,portal_invoice_list,finance.bank_account(viabank_line.import_id) andkernel.legal_entity(via its own id).LEGAL_ENTITY_MISMATCH.packages/kernel-runtime/srcis untouched.UPDATE ... FROMjoin, which only touches rows that are still NULL. Rows whose record is gone stay org-level.legal_entity_idto be either a listed scope (NULLABLE_ENTITY_SCOPED: activity) or a reasoned exception (proposaluntil Legal-entity scope for agents' positions, then proposals per legal entity (#1280) #1538,role_assignment). For activity it checks the exact policy and probes the scoped member: no row of another entity, every org-level row, an insert naming another entity refused with 42501, an org-level insert accepted.apps/kernel/test/activity-legal-entity.db.test.tscovers the backfill, the trigger, the timeline and the overview for a member scoped to A versus one scoped to B, direct reads, the mismatch and fallback rules, and new entities.apps/kernel/test/activity-record-types.test.tsreads everyctx.activity(record type in apps, modules and packages. Each must be either in the trigger's list or org-level with a stated reason, and computed types must be explained.pnpm db:typesoutput (packages/db/src/generated/database.ts), the kernel manifest column, the timeline doc comment, and plan §4.6.What changes for agents
Agents have an empty
le_scope(#1538). No agent query or tool readsactivity; only the user-facing timeline and overview do. Agents writekernel.proposalrows (proposal.created and proposal.approved) and the T18 test rows. Those types are not listed, so the rows stay NULL and passle_scope's USING and WITH CHECK. Agent approvals run in the system context, whose scope is*. Nothing changes for agents, and no agent flow breaks.kernel.proposalstays unlisted on purpose: tagging it would make agents' proposal rows fail WITH CHECK.Acceptance
activity-legal-entity.db.test.ts: timeline, direct counts and overview for A, B and the owner, the legacy backfilled row, and new entities.rls-matrix.db.test.ts: the new nullable-scope case.dband invariant jobs on this head. See "Tests run" below.agents.db.test.tsis unchanged, and it runs in CI (base copy and PR copy).codex review --base origin/main): no findings.manifest.db.test.ts), which is fixed.kernel.legal_entityrows carried B's name; they now take the entity's own id through the fourth trigger argument.finance.period,finance.archiveandkernel.mail_messagerows, and the role summaries inkernel.memberthat name an entity, stay org-level. None of them shows in the timeline.bank_line, so its row ("0 new lines", with the account code) stays org-level unless the writer is limited to one entity.ADD COLUMNholds activity's ACCESS EXCLUSIVE lock until commit, so commands wait for the backfill: one indexed join per listed type, short at pilot size.Tests run
pnpm lint,pnpm typecheck,pnpm docs:checkandnode tools/ci/migration-lint.mjson the file passed.pnpm test: 404 files passed. These failed on Windows only and are untouched by this PR:module-screens,extractnetwork isolation,route-coverageandstatus-page(STATUS_FILE path config), andinvariant-summary(paths).db:typeschecks are verified by CI on this head.db:typesdiff confirms it.Security review (session's own, protected paths: the migration and the invariant suite)
tenant_isolation, so it only narrows. NULL rows were already readable org-wide before this PR. No existing control is loosened.search_path = pg_catalog, pg_tempand EXECUTE revoked from every role. It reads the record through the writer's own policies, so it reveals nothing the writer could not already read.LEGAL_ENTITY_MISMATCHfires only when the writer can read the record, so it does not reveal anything either.%L,%Iand::regclass.agent_write_guard.🤖 Generated with Claude Code