Repository navigation
Epic: Permission sets — Studio designs every facet, Setup assigns users (ADR-0056) #2398
Description
Epic: Permission sets — Studio designs every facet, Setup assigns users (ADR-0056)
ADR: objectui ADR-0056 — Permission editing belongs in Studio; Setup keeps user management and assignment (#2399 — being revised from "Option B" to the pure model below).
Relates to (framework): ADR-0090 (Permission Model v2), ADR-0092 (identity write guard), ADR-0086 (authz metadata↔config boundary), ADR-0066 (sys_capability registry), ADR-0049 (no unenforced security properties), ADR-0084 (app-builder IA).
⚠️ Direction reset (2026-07-12). This epic previously encoded Option B, which keptsystem_permissionseditable in Setup as an exception. That exception is dropped. The authoritative model is now pure separation:
- Studio (
/studio/<pkg>/access,studio.access) = where an app developer DESIGNS the permission set — every facet (object, field, system capabilities, RLS, tab visibility, admin scope) via structured editors.- Setup (
com.objectstack.setup,setup.access) = where a system admin ASSIGNS users to a set and sees each facet as a read-only summary with a "Design in Studio →" deep-link.- Setup shows NO JSON and NO
[Object]. Nothing permission-shaped is free-text-editable in Setup.
Problem
A permission set (sys_permission_set) is authored across two surfaces over the same record, and the Setup surface is actively broken UX:
- Setup renders the
sys_permission_setobject with its six authorization columns as raw values:object_permissionsandfield_permissionsshow as[Object], andsystem_permissions/row_level_security/tab_permissions/admin_scopeshow as raw JSONField.textareas. An admin on.../sys_permission_set/record/<id>sees对象权限 [Object],字段权限 [Object], and hand-editable JSON blobs. - Studio (
studio.access) Access pillar has a structuredPermissionMatrixEditor, but it edits onlyobjects(CRUD/VAMA/lifecycle) andfields(FLS). The other four concerns are authorable only as raw JSON in Setup.
Raw/[Object] authorization metadata in Setup is: unreadable to an admin, invisible to the ADR-0090 explain engine (D6) and publish linter (D7), unvalidated at author time, duplicated (objects/fields editable in two surfaces), and a silent-incident footgun (ADR-0049).
Decision (pure model)
Design → Studio. Assign → Setup. Setup is read-only for every permission facet.
- Studio gets structured editors for all six facets (objects + fields already exist; capabilities, RLS, tab, admin_scope are new).
- Setup's
sys_permission_setrecord page becomes: identity fields + user assignment + a read-only summary card per facet, each with a deep-link into the structured (Studio-grade) editor. No JSON. No[Object]. system_permissionsis designed in Studio like every other facet (this is the change from the old Option B — there is no longer a "capabilities stay editable in Setup" carve-out). Setup only displays the granted capabilities read-only + deep-links to Studio.
Deep-link target. Package-owned sets → the package's Studio Access pillar. Environment-authored sets (created in Setup, not owned by a package — so they have no /studio/<pkg>/access home) → the environment-scope structured editor via the metadata-admin permission route: /apps/com.objectstack.setup/metadata/permission/<setName> (packageId optional ⇒ env scope). Either target renders the structured editor, never JSON. (Reconciling these two entry points is open Q1.)
Already exists (reuse, don't rebuild): PermissionMatrixEditor (objects+fields) + its PermissionMatrixEditPage env-scope route; sys_capability registry object + seeded platform caps (setup.access/studio.access/manage_*); AdminScopeSchema (ADR-0090 D12); RowLevelSecurityPolicySchema; AccessExplainPanel (D6); AssignedUsersSection (add/remove sys_user_permission_set, shows sys_position_permission_set).
Target end-state (facet by facet)
sys_permission_set column |
Structured editor (design) | Design home | Setup shows |
|---|---|---|---|
object_permissions |
PermissionMatrixEditor (exists) |
Studio | read-only summary + deep-link |
field_permissions |
PermissionMatrixEditor (exists) |
Studio | read-only summary + deep-link |
system_permissions |
sys_capability multi-select (new — from #2400) |
Studio | read-only capability chips + deep-link |
admin_scope |
Delegated-admin editor (new) | Studio | read-only summary + deep-link |
row_level_security |
Structured RLS editor (new) | Studio | read-only summary + deep-link |
tab_permissions |
Tab-visibility editor (new) | Studio | read-only summary + deep-link |
| (assignment) | AssignedUsersSection (exists) |
Setup | the assignment UI itself |
Phases (supersedes the prior P1–P5 scoping)
-
P1 — Setup: kill
[Object]/JSON; render read-only summary + deep-link for all six facets; surface user assignment. (repo: objectui)
On Setup'ssys_permission_setrecord page, replace the raw display of all six permission columns with a read-only summary card per facet (e.g. "5 objects granted", capability chips, "3 RLS policies") + a "Design in Studio →" deep-link to the structured editor for that set; and surfaceAssignedUsersSection(add/remove users) on the same page. Reuses the feat(fields,plugin-form,plugin-detail): Setup system_permissions → capability multi-select (ADR-0056 P2) #2400 groundwork (singlegetObjectSchemawidget-override choke point + form/detail widget-honoring), but points the fields at a summary+deep-link renderer instead of an editable widget.
Acceptance: Setup shows no[Object]and no editable JSON for any permission facet; each facet has a read-only summary + a deep-link that opens the correct set in the structured editor at the right scope and returns to the Setup row; admins can add/remove users from the set in Setup; package-managed sets render read-only per ADR-0086; ADR-0054 browser proof. -
P2 — Studio: System Capabilities editor. (repo: objectui)
Add a System Capabilities editor to Studio's Access pillar (and the env-scopePermissionMatrixEditPage) using theCapabilityMultiSelectFieldalready built on the#2400branch (feat/perm-setup-capability-picker): the livesys_capabilityregistry (active = true) as scope-grouped, labelled chips withdescriptiontooltips; editssystemPermissionsin the metadata draft; round-trips byte-equivalent to the storedstring[]of capability names; unknown/legacy names preserved.
Acceptance: granting/revokingstudio.accessand other capabilities is done in Studio via the picker, never JSON; the value persists byte-equivalent; Setup only reflects it read-only (P1); ADR-0054 proof. -
P3 — Studio: structured editors for RLS, tab visibility, and admin scope. (repo: objectui)
- RLS: per-policy rows (object · operation · USING · CHECK) with a predicate field that round-trips CEL (simple-rule builder + raw-CEL escape hatch — open Q2); validated against the RLS predicate surface.
- Tab visibility: per-app/tab selector →
visible | hidden | default_on | default_off, enumerating installed apps/tabs. - Admin scope: BU-subtree picker (
businessUnit+includeSubtree),manageAssignments/manageBindings/authorEnvironmentSetstoggles,assignablePermissionSets[]allowlist (ADR-0090 D12).
Acceptance:row_level_security,tab_permissions, andadmin_scopeare authorable structurally in Studio; editors read the canonical spec shapes (RowLevelSecurityPolicySchema,tabPermissionsenum,AdminScopeSchema); malformed legacy JSON degrades to a guarded raw view rather than data loss; Studio-only (behindstudio.access); ADR-0054 proofs.
-
P4 — Move assignment fully into Setup; retire residual JSON; explain/lint alignment. (repo: objectui; possible framework companion)
MoveAssignedUsersSectionout of Studio'sPermissionMatrixEditor(currently embedded there) so assignment lives only in Setup (per the pure model); confirm every facet is now homed in a structured editor + summarized read-only in Setup, and remove any remaining raw-JSON rendering from Setup; verify each facet is legible to the ADR-0090 explain engine + publish linter.
Acceptance: no permission concern is editable as free-text JSON anywhere in Setup; assignment UI appears in Setup, not Studio; Setup permission-set page = identity + assignment + read-only summaries with deep-links; legacy rows with irrecoverable JSON keep a guarded read-only escape (open Q5); if any field-widget narrowing is done framework-side, a companionplugin-securitychangeset cites ADR-0056; ADR-0054 proof.
Reusable groundwork from #2400 (do not rebuild)
The drafted P2 PR (#2400, branch feat/perm-setup-capability-picker) built plumbing that this epic reuses, even though its original placement (capability picker in Setup) is superseded by the pure model:
CapabilityMultiSelectField(field:capability-multiselect,@object-ui/fields) + its 9 passing unit tests → moves to Studio (P2).- Single widget-override choke point —
ObjectStackAdapter.getObjectSchemastamps a fieldwidget, honored by bothObjectFormandDetailView/DetailSection→ reused by P1 to stamp the six permission fields with a summary+deep-link renderer (instead of the capability picker). form.tsxDATA_SOURCE_FIELD_TYPESforwarding +DetailSectionwidget enrichment → reused for the P1 renderer.
Disposition: #2400 is parked (draft) as superseded-in-placement; its code is preserved on-branch and re-homed by P1/P2 above. ADR #2399 is being revised from Option B to this pure model.
Open questions
- Deep-link target/scope (P1): package-owned sets → package Studio Access pillar; env-authored sets → env-scope
PermissionMatrixEditPage(/apps/com.objectstack.setup/metadata/permission/<setName>). Confirm the two entry points, package-managed read-only presentation, and back-navigation to the Setup row. - RLS editor depth (P3): simple-rule builder vs raw-CEL hybrid in v1; where
contextVariables(onPermissionSetSchema, not asys_permission_setcolumn) is edited. - Tab-visibility source (P3): which apps/tabs are enumerated; package vs environment scope; reconciliation when apps change.
- Summary renderers (P1): how much per-facet summary detail (counts vs first-N chips vs a compact matrix preview) before the deep-link.
- Backward-compat (P3/P4): parse-and-render legacy JSON; guarded read-only + raw escape for malformed/unrepresentable values; whether a one-time normalization/lint pass stamps legacy rows.
— RESOLVED: capabilities are designed in Studio like every other facet; Setup displays them read-only (no Setup-editable exception).system_permissionsvisibility
Notes
- All phases are objectui UI work. Framework touchpoints are limited to how the
sys_permission_setform renders and reading spec shapes; the storage columns are unchanged. - Sequencing: P1 is the highest-value fix (it kills the visible
[Object]/JSON UX the admin sees today) and is independent of P2/P3. P2 reuses feat(fields,plugin-form,plugin-detail): Setup system_permissions → capability multi-select (ADR-0056 P2) #2400 directly. P4 depends on P1–P3 homing every facet.
Activity
[-]Epic: Consolidate permission-set editing into Studio; Setup keeps assignment (ADR-0056)[/-][+]Epic: Permission sets — Studio designs every facet, Setup assigns users (ADR-0056)[/+]feat(app-shell): Studio System Capabilities editor on the permission …
feat(app-shell): structured RLS / tab-visibility / admin-scope editor…
feat(app-shell): move user assignment out of the permission design ed…
os-zhuang commentedon Jul 12, 2026
All phases implemented + browser-verified in #2403 (pure model).
[Object]/JSON)sys_capabilitymulti-select)Verified live (app-showcase):
sales_repfacet summaries + deep-link;organization_admin18 RLS policies as structured rows;showcase_field_ops_delegateadmin-scope form; capability + admin-scope save round-trips persist.Framework follow-up (open Q + new): env-scope metadata saves of these facets don't project onto the queryable
sys_permission_setdata record the Setup summary reads, so a fresh Studio edit isn't reflected in Setup until the projection refreshes (enforcement reads the authoritative metadata). This is the one remaining gap for full closed-loop coherence.feat(app-shell,plugin-detail): permission sets — Studio designs, Setu…
os-zhuang commentedon Jul 13, 2026
Shipped + follow-ups filed:
The objectui UI work for the pure model is complete; the one remaining gap for full closed-loop coherence is the framework projection (#2857).
docs(adr): ADR-0056 — permission editing belongs in Studio, Setup kee…
os-zhuang commentedon Jul 13, 2026
Framework projection fix is up: objectstack-ai/objectstack#2867 (closes framework#2857). Projects env-scope permission-set edits onto the queryable
sys_permission_setrecord via the protocol'sonMetadataMutationhook — closing the last gap so a Studio edit reflects in Setup's read-only summary. 21 unit tests + no regression; live E2E to be confirmed by framework CI (local isolated-backend repro was blocked by a worktree/pnpm resolution artifact).os-zhuang commentedon Jul 13, 2026
Follow-ups surfaced during go-live UAT (post-merge)
With the pure model shipped and browser-verified end-to-end (Setup read-only + Studio designs all facets), two durable follow-ups came out of the walkthrough:
USING/CHECKpredicates with no validation; RLS is security-critical and some paths fail open.sys_permission_setdata record a pure projection. The fix(page,field): consume the spec's type/label/maxLength keys (framework#1878 §3 recheck) #2867 metadata→data-record projection was a sync band-aid; the durable fix is a single source of truth so no write path can desync the two stores.Both are non-blocking; the shipped model works. Filing here so they're tracked against this epic.