Every object already states its OWD; this is the layer that opens the right doors on top of private.
Files you own
src/security/positions.ts, src/security/permission-sets.ts, src/security/sharing-rules.ts (new), pushed into the three arrays in src/security/index.ts
test/security.test.ts (new)
Positions (flat — ADR-0090 D3, there is no position tree)
| Position |
Who |
duly_member |
everyone who owns duties |
duly_manager |
anyone with reports or a unit |
duly_admin |
catalog owners and rollout admins |
Permission sets
Author with definePermissionSet({ name, label, objects: { … }, fields, tabPermissions, systemPermissions }).
duly_member
duly_task: create/read/edit, readScope: 'own', writeScope: 'own'
duly_duty: read own; create own (source: 'self')
duly_log_entry: full control of own, readScope: 'own'
duly_catalog_item: read only
duly_assignment: read the ones they are on
duly_manager — member, plus
duly_task, duly_duty: readScope: 'unit_and_below', writeScope stays 'own'. A manager reads everything below them and writes none of it. This is not an oversight to close later: the product's position is that a manager's only write is assigning, and the permission set is where that gets enforced rather than merely designed.
duly_assignment: create/edit own
duly_admin — manager, plus
duly_catalog_item: full control
duly_duty: readScope: 'org', and edit for corrections
duly_log_entry — the one that must not leak
readScope: 'own' for every position including admin. The only widening is the record's own visibility: 'manager', which a criteria sharing rule grants to that person's manager and nobody else. No unit scope, no org scope, no admin override.
A log people believe their skip-level can read is a log nobody keeps, and the module stops producing the record it exists to produce.
Sharing rules
defineSharingRule({ type: 'criteria', … }):
duly_log_entry where visibility = 'manager' → recipient: the owner's manager
duly_catalog_item is already public_read; no rule needed
The open-edition caveat — read before you debug
readScope: 'own_and_reports' | 'unit' | 'unit_and_below' | 'org' (ADR-0057) are resolved by @objectstack/security-enterprise, which enterprise deployments carry. This repo runs on the open edition, where they fall back to owner-only, silently, with no error.
That means a manager view in your dev environment will show you only your own rows. That is expected, not a bug — do not chase it, do not build an application-level fallback, and do not add hierarchy-security to requires (it would fail an open-edition boot). Write the metadata correctly for the enterprise runtime and assert the authored scopes in tests rather than the resolved rows.
Acceptance
- every permission set asserts its declared
readScope/writeScope per object in test/security.test.ts
- no position anywhere grants
duly_log_entry a scope wider than own
- no position grants a write scope on
duly_task/duly_duty wider than own
duly_manager and duly_admin inherit rather than restate duly_member's grants
- a short section in
docs/product/data-model.md or a new docs/deployment/security.md states the enterprise-edition requirement for a real rollout
Gates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.
Every object already states its OWD; this is the layer that opens the right doors on top of
private.Files you own
src/security/positions.ts,src/security/permission-sets.ts,src/security/sharing-rules.ts(new), pushed into the three arrays insrc/security/index.tstest/security.test.ts(new)Positions (flat — ADR-0090 D3, there is no position tree)
duly_memberduly_managerduly_adminPermission sets
Author with
definePermissionSet({ name, label, objects: { … }, fields, tabPermissions, systemPermissions }).duly_memberduly_task: create/read/edit,readScope: 'own',writeScope: 'own'duly_duty: read own; create own (source: 'self')duly_log_entry: full control of own,readScope: 'own'duly_catalog_item: read onlyduly_assignment: read the ones they are onduly_manager— member, plusduly_task,duly_duty:readScope: 'unit_and_below',writeScopestays'own'. A manager reads everything below them and writes none of it. This is not an oversight to close later: the product's position is that a manager's only write is assigning, and the permission set is where that gets enforced rather than merely designed.duly_assignment: create/edit ownduly_admin— manager, plusduly_catalog_item: full controlduly_duty:readScope: 'org', and edit for correctionsduly_log_entry— the one that must not leakreadScope: 'own'for every position including admin. The only widening is the record's ownvisibility: 'manager', which a criteria sharing rule grants to that person's manager and nobody else. No unit scope, no org scope, no admin override.A log people believe their skip-level can read is a log nobody keeps, and the module stops producing the record it exists to produce.
Sharing rules
defineSharingRule({ type: 'criteria', … }):duly_log_entrywherevisibility = 'manager'→ recipient: the owner's managerduly_catalog_itemis alreadypublic_read; no rule neededThe open-edition caveat — read before you debug
readScope: 'own_and_reports' | 'unit' | 'unit_and_below' | 'org'(ADR-0057) are resolved by@objectstack/security-enterprise, which enterprise deployments carry. This repo runs on the open edition, where they fall back to owner-only, silently, with no error.That means a manager view in your dev environment will show you only your own rows. That is expected, not a bug — do not chase it, do not build an application-level fallback, and do not add
hierarchy-securitytorequires(it would fail an open-edition boot). Write the metadata correctly for the enterprise runtime and assert the authored scopes in tests rather than the resolved rows.Acceptance
readScope/writeScopeper object intest/security.test.tsduly_log_entrya scope wider thanownduly_task/duly_dutywider thanownduly_managerandduly_admininherit rather than restateduly_member's grantsdocs/product/data-model.mdor a newdocs/deployment/security.mdstates the enterprise-edition requirement for a real rolloutGates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.