Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions docs/qa/platform-checklist/areas/access-security.json
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,7 @@
"title": "CRUD × permission-set matrix: every access-matrix.json row holds — allowed verbs succeed, withheld verbs 403, VAMA bounded",
"since": "v15",
"status": "active",
"revision": 9,
"revision": 10,
"priority": "P0",
"surface": "api",
"personas": [
Expand All @@ -510,7 +510,7 @@
"knownGaps": [
"every authenticated member ALSO holds the everyone baseline showcase_member_default additively (ADR-0090 D5) — the effective expectation per cell is the UNION of the tested set's row and the baseline's row for that object; compute the union before judging a cell, or a baseline-granted read will look like a matrix violation. Measured on the committed table: 9 of the 164 showcase_* cells (41 rows × 4 verbs at revision 4) are granted ONLY by the baseline (showcase_auditor and showcase_executive can CREATE an inquiry and create+edit a private note; guest_portal can READ an inquiry; showcase_manager and showcase_ops can CREATE an inquiry). showcase-crud-persona-matrix.dogfood.test.ts computes the union and asserts those 9 as allowed.",
"access-matrix.json is the APP's declared matrix; the platform's own baseline permission sets are additionally in force and grant some sys_* surface the app matrix never mentions (measured: a plain member reads /data/sys_user 200 — the organization's whole user directory, not a self-scoped row: member_default's sys_user_org_members policy selects `id in current_user.org_user_ids`; sys_account and sys_session are the self-scoped ones). So the absence of a row is NOT a prediction of denial on sys_* objects, and the automated sweep judges the showcase_* rows only — see automated.ref.",
"clause 5 (guest_portal create-without-read) is UNOBSERVABLE on a stock boot — record it blocked(fixture), never pass or fail. The guest anchor ships unbound (binding guest → showcase_guest_portal is an admin action; the app only ships the capability, permission-sets.ts GuestPortalPermissionSet), a member persona holding the set also holds the baseline's read on showcase_inquiry, and QA run #21056 measured REST anonymous create AND read both answering 401 even with the anchor bound. The only anonymous create lane is the public form (publicFormGrant on the inquiry form view, ADR-0056 Option A), whose grant is create plus read-back of the row it created — a different contract from this clause's",
"clause 5 (guest_portal create-without-read) is UNOBSERVABLE on a stock boot, and not for want of a fixture: no door binds a set to the guest anchor. Record it blocked(dependency) with ref #23267, never pass or fail. ADR-0138 D3 item 5, amended implementation-first by ruling C on #23267 (comment 6159279205; the maintainer: no public site or portal is planned), builds the bind door only when a named deployment needs one; until then the guest anchor stays at the deny-all empty state. Every route a runner could try is refused: PUT /api/v1/meta/position/guest answers 403 NOT_OVERRIDABLE (the anchor is packaged metadata; packages/qa/dogfood/test/guest-grants-channel.dogfood.test.ts, the case 'the metadata door refuses a save of the packaged `guest` position'), packages/plugins/plugin-security/src/suggested-audience-bindings.ts#confirmAudienceBindingSuggestion refuses a guest row (the suggestion sync never writes one), and the package door refuses a built-in position name. #26623 found no recipe to write for that reason: a provisioning recipe needs a replayable door, and an area recipe no item can use is refused by check:platform-checklist. The app ships only the capability (permission-sets.ts GuestPortalPermissionSet), and a member persona holding the set also holds the baseline's read on showcase_inquiry, so a member cannot stand in for the guest. Even with the anchor bound, /api/v1/data would not serve the guest: ADR-0138 D2 admits an anonymous request only at its closed list of door classes (a data operation only through an authRequired:false endpoint, classes 4 and 5) and everything else answers 401, which QA run #21056 measured for REST anonymous create and read. The clause's shape is held in-process on an inline fixture whose binding the test installs through the registry's bare slot (guest-grants-channel.dogfood.test.ts: a bound create lands with no owner, and a bound read set reads 0 rows under every sharing model); that file drives no showcase object, so it scores nothing here. The only anonymous create lane on the showcase is the public form (publicFormGrant on the inquiry form view, ADR-0056 Option A), whose grant is create plus read-back of the row it created, a different contract from this clause's. When the #23267 door is built, re-read this line and write the recipe it then allows",
"sys_user create and delete answer 405 OBJECT_API_METHOD_NOT_ALLOWED for EVERY caller, admin included: sys_user declares apiMethods ['get','list','update','bulk'] (identity writes belong to better-auth, ADR-0092), and the API-exposure gate answers before any permission is judged. Score those two delegate × sys_user cells against 405, not 403"
]
},
Expand All @@ -520,7 +520,7 @@
"for every row of access-matrix.json drive all four verbs as that persona: POST /api/v1/data/<object> {minimal valid payload}, GET /api/v1/data/<object> + GET /api/v1/data/<object>/<probe id>, PATCH /api/v1/data/<object>/<own or probe id>, DELETE /api/v1/data/<object>/<id> — capture status + error.code for each cell",
"for rows with viewAllRecords:true (auditor on showcase_inquiry/showcase_invoice/showcase_invoice_line/showcase_private_note): GET the foreign probe by id and list; for the SAME objects re-run as a persona whose row says viewAllRecords:false",
"for rows with modifyAllRecords:true (ops on showcase_announcement): PATCH the foreign-owned announcement; re-run the same PATCH as member_default (modifyAllRecords:false)",
"for the guest_portal row pair on showcase_inquiry (create:true, read:false): record clause 5 blocked(fixture) per knownGaps — a member holding the set reads inquiries through the baseline, and REST anonymous answers 401 to both verbs",
"for the guest_portal row pair on showcase_inquiry (create:true, read:false): record clause 5 blocked(dependency, #23267) per knownGaps[2]: no door binds a set to the guest anchor (ADR-0138 D3 item 5, ruling C), a member holding the set reads inquiries through the baseline, and REST anonymous answers 401 to both verbs",
"for the 4 showcase_field_ops_delegate × sys_* rows, drive the delegate by hand; for its sys_user_position edit and delete cells, probe a row the DELEGATE created — a row another principal created meets the platform's write ownership floor (member_default owner_only_writes / owner_only_deletes, created_by == current_user.id), so its verdict does not read the delegate's grant",
"record the full verb × set × object verdict matrix and diff it against access-matrix.json (unioned with the baseline per the known gap above)"
],
Expand Down Expand Up @@ -585,7 +585,7 @@
],
"automated": {
"kind": "verify",
"ref": "`objectstack verify --rls` (packages/verify/src/verify.ts runCrudVerification + packages/verify/src/rls.ts runRlsProofs) for the OBJECT-level CRUD and cross-owner RLS half — a CLI proof a runner invokes, not a test file: no pin in this ref drives it over the showcase (rls-fixture and rls-multitenant dogfood run runRlsProofs on purpose-built fixtures only), so that half scores only from a run that executes the command (the flag is load-bearing: bare verify prints no RLS section) + packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts for the PERSONA-grained cells, through the in-process HTTP stack. The sweep drives every showcase_* row of access-matrix.json (41 at revision 4) over all four verbs with one fresh member per permission set — 164 cells, 87 effective-allow and 77 effective-deny, each judged against the BASELINE-UNIONED expectation (row OR showcase_member_default row, ADR-0090 D5), which is also asserted as load-bearing on the 9 cells whose only grant is the baseline; every denial is checked to be 403 PERMISSION_DENIED, every allow to persist, and an admin payload control runs first so a persona 403 cannot be a 400 in disguise. That reaches clauses 0, 1, 2 and 4 for those rows. The same file carries: clause 3 on all four objects the auditor's viewAllRecords names — showcase_private_note, showcase_inquiry, showcase_invoice and showcase_invoice_line, by id and in a filtered list, against a showcase_contributor member that holds read without the bit and gets none of the foreign rows (private OWD, owner RLS and the master-derived scope respectively); negative[0] — the first denied cell of every set, re-run as the admin, answers 2xx; and the 4 showcase_field_ops_delegate × sys_* rows as one case ('the 16 showcase_field_ops_delegate × sys_* cells answer the matrix'): a member holding exactly that set reads sys_business_unit, sys_business_unit_member and sys_user (200), is refused their withheld verbs 403 PERMISSION_DENIED, meets 405 OBJECT_API_METHOD_NOT_ALLOWED on sys_user create and delete (the admin control answers 405 too: sys_user declares apiMethods ['get','list','update','bulk']), and runs sys_user_position's four verbs 2xx on a row it made inside its Field Operations subtree (the out-of-subtree side of that ADR-0090 D12 gate is showcase-permission-zoo.dogfood.test.ts's, which asserts 403 DELEGATION_SCOPE_VIOLATION). So clause 6 is pinned over the whole table: 45 rows × 4 verbs. STILL MANUAL: clause 5's guest-anchor intake asymmetry (guest_portal create-without-read) — NOT observable on a member persona at all, because the everyone baseline grants read on showcase_inquiry and capability is a union, so the member-persona expectation is READ ALLOWED; the asymmetry lives on the unauthenticated guest anchor, a different admission lane (knownGaps[2] records it blocked(fixture))"
"ref": "`objectstack verify --rls` (packages/verify/src/verify.ts runCrudVerification + packages/verify/src/rls.ts runRlsProofs) for the OBJECT-level CRUD and cross-owner RLS half — a CLI proof a runner invokes, not a test file: no pin in this ref drives it over the showcase (rls-fixture and rls-multitenant dogfood run runRlsProofs on purpose-built fixtures only), so that half scores only from a run that executes the command (the flag is load-bearing: bare verify prints no RLS section) + packages/qa/dogfood/test/showcase-crud-persona-matrix.dogfood.test.ts for the PERSONA-grained cells, through the in-process HTTP stack. The sweep drives every showcase_* row of access-matrix.json (41 at revision 4) over all four verbs with one fresh member per permission set — 164 cells, 87 effective-allow and 77 effective-deny, each judged against the BASELINE-UNIONED expectation (row OR showcase_member_default row, ADR-0090 D5), which is also asserted as load-bearing on the 9 cells whose only grant is the baseline; every denial is checked to be 403 PERMISSION_DENIED, every allow to persist, and an admin payload control runs first so a persona 403 cannot be a 400 in disguise. That reaches clauses 0, 1, 2 and 4 for those rows. The same file carries: clause 3 on all four objects the auditor's viewAllRecords names — showcase_private_note, showcase_inquiry, showcase_invoice and showcase_invoice_line, by id and in a filtered list, against a showcase_contributor member that holds read without the bit and gets none of the foreign rows (private OWD, owner RLS and the master-derived scope respectively); negative[0] — the first denied cell of every set, re-run as the admin, answers 2xx; and the 4 showcase_field_ops_delegate × sys_* rows as one case ('the 16 showcase_field_ops_delegate × sys_* cells answer the matrix'): a member holding exactly that set reads sys_business_unit, sys_business_unit_member and sys_user (200), is refused their withheld verbs 403 PERMISSION_DENIED, meets 405 OBJECT_API_METHOD_NOT_ALLOWED on sys_user create and delete (the admin control answers 405 too: sys_user declares apiMethods ['get','list','update','bulk']), and runs sys_user_position's four verbs 2xx on a row it made inside its Field Operations subtree (the out-of-subtree side of that ADR-0090 D12 gate is showcase-permission-zoo.dogfood.test.ts's, which asserts 403 DELEGATION_SCOPE_VIOLATION). So clause 6 is pinned over the whole table: 45 rows × 4 verbs. STILL MANUAL: clause 5's guest-anchor intake asymmetry (guest_portal create-without-read) — NOT observable on a member persona at all, because the everyone baseline grants read on showcase_inquiry and capability is a union, so the member-persona expectation is READ ALLOWED; the asymmetry lives on the unauthenticated guest anchor, a different admission lane that no door can bind today (knownGaps[2] records it blocked(dependency), #23267)"
},
"traps": [
"wrong-persona",
Expand Down Expand Up @@ -646,6 +646,12 @@
"date": "2026-10-27",
"change": "Access-security pin upgrade, slice 1 (#26571; Tier 1 record #26468 scored the item PARTIAL). showcase-crud-persona-matrix.dogfood.test.ts gains three cases at the api oracle's layer (in-process HTTP): the auditor's viewAllRecords over the foreign inquiry, invoice and line with a contributor as the contrast (acceptance[3] on all four named objects); one denied cell per set re-run as the admin (negative[0]); and the 16 showcase_field_ops_delegate × sys_* cells, sys_user create and delete scored 405 OBJECT_API_METHOD_NOT_ALLOWED for the delegate and the admin (acceptance[0], [1] and [6] over the whole table). acceptance[5] stays blocked(fixture) as knownGaps[2] says. automated.ref restated; it no longer calls the delegate rows hand-driven. acceptance[6].verify and steps[6] still describe those 16 cells as hand-driven, outside this revision's edit surface. No clause or status changed",
"ref": "#26571"
},
{
"revision": 10,
"date": "2026-10-27",
"change": "Card #26623 asked for an area-level provisioning recipe that binds the guest anchor so acceptance[5] can be scored. No such recipe can be written, because no door binds a set to the guest anchor: ADR-0138 D3 item 5, amended by ruling C on #23267 (comment 6159279205), builds that door only when a named deployment needs one. So no area recipe and no fixtures.provisioning reference were added, and the product card is the existing, ruled #23267 rather than a new one. acceptance[5] was re-driven once over REST on a fresh stock showcase boot at main 1708115e2f: the admin's GET /api/v1/meta/position/guest answered 200 with the packaged anchor (provenance package, no permissionSets); PUT /api/v1/meta/position/guest naming showcase_guest_portal answered 403 NOT_OVERRIDABLE and the re-read was unchanged; GET /api/v1/security/suggested-bindings listed one row, the confirmed everyone suggestion for showcase_member_default, and no guest row; anonymous POST and GET /api/v1/data/showcase_inquiry each answered 401 UNAUTHENTICATED. Verdict: blocked(dependency, #23267). knownGaps[2] now records the clause as a ruled product limit, not a fixture gap, with the refusals and the pin that holds them (guest-grants-channel.dogfood.test.ts); steps[5] and the closing words of automated.ref say blocked(dependency) to match. No other clause or status changed",
"ref": "#26623"
}
]
},
Expand Down
Loading