Skip to content
Merged
Show file tree
Hide file tree
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
154 changes: 154 additions & 0 deletions examples/app-showcase/access-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,39 @@
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_contributor",
"object": "showcase_expense_line",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "controlled_by_parent"
},
{
"permissionSet": "showcase_contributor",
"object": "showcase_expense_report",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_contributor",
"object": "showcase_field_zoo",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_contributor",
"object": "showcase_invoice",
Expand Down Expand Up @@ -275,6 +308,61 @@
"modifyAllRecords": false,
"sharingModel": "public_read"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_business_unit",
"create": false,
"read": true,
"edit": false,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_cascade",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_category",
"create": false,
"read": true,
"edit": false,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_expense_report",
"create": false,
"read": true,
"edit": false,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_field_zoo",
"create": false,
"read": true,
"edit": false,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_inquiry",
Expand All @@ -286,6 +374,17 @@
"modifyAllRecords": false,
"sharingModel": "private"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_preference",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_private_note",
Expand Down Expand Up @@ -330,6 +429,17 @@
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_member_default",
"object": "showcase_team",
"create": false,
"read": true,
"edit": false,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_ops",
"object": "showcase_announcement",
Expand All @@ -341,6 +451,28 @@
"modifyAllRecords": true,
"sharingModel": "public_read"
},
{
"permissionSet": "showcase_ops",
"object": "showcase_business_unit",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_ops",
"object": "showcase_category",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_ops",
"object": "showcase_inquiry",
Expand All @@ -364,6 +496,28 @@
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_ops",
"object": "showcase_project_membership",
"create": true,
"read": true,
"edit": true,
"delete": true,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
},
{
"permissionSet": "showcase_ops",
"object": "showcase_team",
"create": true,
"read": true,
"edit": true,
"delete": false,
"viewAllRecords": false,
"modifyAllRecords": false,
"sharingModel": "public_read_write"
}
]
}
1 change: 1 addition & 0 deletions examples/app-showcase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@objectstack/cli": "workspace:*",
"@objectstack/core": "workspace:*",
"@objectstack/formula": "workspace:*",
"@objectstack/lint": "workspace:*",
"@objectstack/objectql": "workspace:*",
"@objectstack/plugin-approvals": "workspace:*",
"@objectstack/plugin-email": "workspace:*",
Expand Down
17 changes: 13 additions & 4 deletions examples/app-showcase/src/data/objects/cascading-select.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,19 @@ import { P } from '@objectstack/spec';
* unbound (a system write) — the acting user is bound from the request on
* authenticated writes (engine `buildEvalUser`).
*
* `sharingModel: 'public_read_write'` so the seeded admin (and the live e2e,
* objectui `e2e/live/cascading-options.spec.ts`) can create records without a
* bespoke permission set; belonging to no permission set, it is intentionally
* absent from the ADR-0090 access-matrix snapshot.
* `sharingModel: 'public_read_write'` is the RECORD baseline (gate ②) — no
* sharing rule is needed for one persona to see another's row. It is NOT what
* makes the object reachable: object-level CRUD (gate ①) comes only from a
* permission set and is checked first. This comment used to say the wide OWD let
* the seeded admin and the live e2e create records "without a bespoke permission
* set". What actually carried the admin was their built-in WILDCARD set: with no
* grant anywhere, `nav-object-ungranted` warned on every build and reports the
* `nav_cascade` entry as permission-denied for every principal that does not
* hold that wildcard set. `showcase_member_default` now grants read/create/edit,
* so the object is in the access-matrix snapshot and a plain member holds the
* capability the navigation entry already claimed for them. Note where the
* narrowing really lives: on one OPTION (`tier: 'restricted'`, admin-only),
* not on the object.
*
* The server verdict is unit-covered by objectql
* `rule-validator.option-visibility.test.ts`; this object is the served fixture
Expand Down
18 changes: 14 additions & 4 deletions examples/app-showcase/src/data/objects/expense-report.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,20 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
*/
export const ExpenseReport = ObjectSchema.create({
name: 'showcase_expense_report',
// [ADR-0090 D1] grandfather stamp: world-writable demo object so any seeded
// persona (and the browser e2e) can create/edit reports without a bespoke
// permission set. Belonging to no permission set, it is intentionally absent
// from the access-matrix snapshot (cf. showcase_cascade).
// [ADR-0090 D1] grandfather stamp: the RECORD baseline is world-writable, so
// no sharing rule or scope depth is needed to see another persona's report.
//
// That is gate ② and it is the whole of what this dial buys. It does NOT make
// the object reachable: object-level CRUD is gate ①, it is granted only by a
// permission set, and it is checked FIRST. This comment used to claim the wide
// OWD let "any seeded persona create/edit reports without a bespoke permission
// set" — measurably false, and the app shipped the proof: the object sat in
// the shared navigation with no grant anywhere, so `nav-object-ungranted`
// warned on every build, reporting the entry as permission-denied for every
// principal except one holding the platform's built-in wildcard admin set.
// The seeded admin holds exactly that set, which is why it looked fine.
// The grants now live on `showcase_contributor` (file/edit) and
// `showcase_member_default` (read), and both appear in access-matrix.json.
sharingModel: 'public_read_write',
label: 'Expense Report',
pluralLabel: 'Expense Reports',
Expand Down
63 changes: 63 additions & 0 deletions examples/app-showcase/src/security/permission-sets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ export const ContributorPermissionSet = definePermissionSet({
// are `controlled_by_parent` — no line RLS is authored (ADR-0055).
showcase_invoice: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
showcase_invoice_line: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
// Expense graph — the SAME master/detail shape as the invoice graph above,
// and the reason the CHILD is listed explicitly rather than inherited.
// `controlled_by_parent` derives the child's RECORD scope from its master;
// it does NOT derive the child's OBJECT-level CRUD. That is a separate gate
// (`checkObjectPermission`) and it runs FIRST, so a set that grants the
// report and forgets the line denies 403 before the parent-derived access is
// ever consulted — the silent "can't fill in / can't submit the subtable"
// trap (`security-master-detail-ungranted`, ADR-0055).
showcase_expense_report: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
showcase_expense_line: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
// Same rule, one object further from intuition: `showcase_field_zoo` is a
// master_detail CHILD of `showcase_project`, which this set grants above.
// Being a synthetic catalogue changes nothing — a detail object still needs
// its own object-level grant to be writable from its master's page.
showcase_field_zoo: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: false },
},
// Field-level security — contributors can read but not edit budget figures.
// Keys MUST be `<object>.<field>` qualified: the runtime evaluator matches
Expand Down Expand Up @@ -183,6 +198,20 @@ export const OpsPermissionSet = definePermissionSet({
showcase_announcement: { allowRead: true, allowCreate: true, allowEdit: true, modifyAllRecords: true },
showcase_inquiry: { allowRead: true, allowEdit: true, readScope: 'org', writeScope: 'org' },
showcase_invoice: { allowRead: true },
// Shared reference data — the org geometry and the tagging tree every
// member READS from the baseline set below, and that back-office CURATES
// here. The split is the point: nav-wide readability is a baseline
// question, authorship is a persona question, and they are different
// grants on the same objects.
showcase_team: { allowRead: true, allowCreate: true, allowEdit: true },
showcase_category: { allowRead: true, allowCreate: true, allowEdit: true },
showcase_business_unit: { allowRead: true, allowCreate: true, allowEdit: true },
// `showcase_project_membership` is the team↔project junction — a detail of
// `showcase_team` (its FIRST master_detail field), granted here beside that
// master for the ADR-0055 reason above. `allowDelete` is what un-staffs a
// team, and is also why the junction cannot live on the `everyone` baseline:
// delete is a high-privilege bit the D7 anchor gate refuses there.
showcase_project_membership: { allowRead: true, allowCreate: true, allowEdit: true, allowDelete: true },
},
// `setup.access` is a platform capability; `showcase.export_data` is a
// PACKAGE capability this app DECLARES via defineCapability (see
Expand Down Expand Up @@ -279,6 +308,40 @@ export const MemberDefaultPermissionSet = definePermissionSet({
// The D7 linter flags this owner-only read as `security-private-no-
// readscope` (info) — here it is exactly the intent.
showcase_private_note: { allowRead: true, allowCreate: true, allowEdit: true },

// ── Navigation is an ACCESS CLAIM (ADR-0090 D6) ──────────────────────
//
// Everything below sits in `showcase_app`'s SHARED navigation, which every
// authenticated member sees. Putting an object there asserts that every
// member may open it, so the baseline set is where that assertion has to be
// paid for — one read grant per shared nav entry, and no further.
//
// None of these needed a WIDER `sharingModel` to become readable and none
// got one: all seven are already `public_read_write`, the widest RECORD
// baseline the model has, and all seven were still unreachable. Object-level
// CRUD (gate ①) and the OWD (gate ②) are independent, gate ① runs first, and
// the only principal it admits with no grant at all is the platform's
// built-in wildcard admin set — which is exactly who an author browses as
// while building the app, and why this class survives to production.
// `nav-object-ungranted` is the check that the two declarations agree.
//
// Read-only here, deliberately. Write lives on the persona that owns the
// data: reference-data curation on `showcase_ops`, expense filing on
// `showcase_contributor`. The two exceptions below carry write because the
// member genuinely owns the row.
showcase_expense_report: { allowRead: true },
showcase_team: { allowRead: true },
showcase_category: { allowRead: true },
showcase_business_unit: { allowRead: true },
showcase_field_zoo: { allowRead: true },
// Every member keeps their own workspace settings (nav "Settings").
showcase_preference: { allowRead: true, allowCreate: true, allowEdit: true },
// The B3 dynamic-options fixture is only a fixture if a member can CREATE a
// row and watch the cascade re-filter. Note where the narrowing actually
// is: not on the object, but on one OPTION — `tier: 'restricted'` is offered
// only to admins and re-checked server-side by the objectql rule validator.
// Object grants and option visibility are different instruments.
showcase_cascade: { allowRead: true, allowCreate: true, allowEdit: true },
},
});

Expand Down
Loading
Loading