Task: Grant rights at portfolio nodes
Table of Contents
This page documents a task in the Add sandboxes and virtual books story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Give sandboxes the node-level rights they need: who may open one at a node, and who may read what a node holds.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Add sandboxes and virtual books |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-03 |
3. Acceptance
- portfolio_right is generated in ores.refdata: an account, a portfolio node and a right.
- A function answers whether an account holds a right at a node, directly or through an ancestor.
- A test shows a right held at a desk applying to its sub-desks and not to a sibling desk.
4. Plan
- No rights at portfolio nodes existed: an account reached data only
through the parties it was linked to. The user chose the smallest model
that meets the sandbox story: one refdata entity,
portfolio_right, pairing an account, a node and a named right. - Two rights, the ones sandboxes need:
readandopen_sandbox, held to that list by a check. - A right held at a node applies below it. A SQL function,
ores_refdata_account_holds_portfolio_right_fn, walks up the tree from the node and answers whether the account holds the right on the way; the walk stops at a depth of 64 so a cycle cannot loop. The model's trailing paste block creates it and its extra drop statements remove it. - Wired by hand like the netting entities, with tenant row-level security.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
6. Notes
7. Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario)
that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
8. PRs
| PR | Title |
|---|---|
| #2544 | [refdata] Grant rights at portfolio nodes |
9. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | Security definer with a caller-supplied tenant | portfolio_right | Fix | The function now runs with the caller's rights, so row-level security confines it to the session's tenant. |
| 2 | Cycle guard emits up to 64 rows | portfolio_right | Keep | Portfolio inserts refuse cycles (:prevent_cycle:); the bound only guards a corrupt tree. |
| 3 | A closed account still holds its rights | portfolio_right | Fix | The function joins the live account row; a closed account holds nothing. |
| 4 | idx detection reads only generator expressions | core.py | Keep | The generator template renders only generator expressions; no other snippet reaches the generated generator. |
| 5 | Note the seed snippet replacing derived seeding | org_loader.py | Fix | The loader's comment now says the snippet replaces derived seeding for any parent. |
| 6 | (CI) Branch conflicted with main | — | Fix | Merged main; regenerated the ER diagram and recipe inventory. |
10. Result
portfolio_rightis generated inores.refdataand wired by hand like the netting entities, with tenant row-level security.ores_refdata_account_holds_portfolio_right_fnanswers whether an account holds a right at a node, directly or through an ancestor.- In Postgres, in a transaction that rolled back, with
open_sandboxgranted on desk A: the right held at A and at its sub-desk A1, not at sibling desk B;readwas not held at A1; an unknown right was refused (23514). - Two codegen defects surfaced and were fixed at the generator, not in the
model:
- A generator declared its
idxcounter for any text natural key, even one marked:no_generator_suffix:, leaving an unused variable that fails the build. It now declares it only when a suffixed key or a snippet reads it (test_generator_counter.py). - A foreign key's
parent_seedsnippet ran beside the derived parent seeding rather than in place of it. It now replaces it, as its documentation said. The generated test for this entity seeds a service account: a seeded user account ended the test tenant's bootstrap and made five other refdata tests fail. The one existing snippet user, a synthetic test, loses two redundant includes.
- A generator declared its
- The refdata core tests link
ores.iam.apiandores.iam.core, which the account seed needs;ores.iam.corealready depends on refdata, so no library cycle forms. - After review: the function runs with the caller's rights and honours only live accounts. In Postgres, once the account was closed the right held at A1 no longer held.
- Build clean; ctest 89 of 89 on a recreated database.