cabloy 5.1.159 → 5.1.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,339 @@
1
+ # Shared RBAC Architecture
2
+
3
+ This guide explains the shared RBAC foundation used by Cabloy Basic and Cabloy Start: the Vona `a-rbac` runtime, its extension boundary, and the Resource permission projection consumed by Zova.
4
+
5
+ Use this page when you need to author, extend, debug, or verify RBAC across the backend/frontend boundary. For Start's operational role, grant, Department, and policy-editor workflow, read [RBAC Authorization](/backend/rbac-authorization) after this guide.
6
+
7
+ > [!WARNING]
8
+ > RBAC authority is enforced on the backend. `ModelResource.permissions`, `$passport.checkPermission(...)`, table-action visibility, and SSR-rendered capability state are browser UX projections. None of them authorize a direct API request.
9
+
10
+ ## The shortest accurate mental model
11
+
12
+ ```text
13
+ @Passport.rbac route metadata
14
+ → shared a-rbac action catalog and guard
15
+ → edition/application policy resolver
16
+ → request-local decision and typed scope access
17
+ → protected backend action and persisted-data checks
18
+
19
+ same composed guards
20
+ → Resource permission projection endpoint
21
+ → OpenAPI permission DTO
22
+ → ModelSdk / ModelResource
23
+ → ModelPassport / action visibility
24
+ ```
25
+
26
+ The two paths intentionally share a decision source, but they have different purposes:
27
+
28
+ | Path | Purpose | Authority |
29
+ | ------------------------------- | --------------------------------------------- | ----------------- |
30
+ | Guard + `rbacScope` | Admit a request and constrain persisted data | Backend authority |
31
+ | Permission projection + matcher | Avoid offering browser actions that will fail | Frontend UX only |
32
+
33
+ ## What is shared and what is application policy
34
+
35
+ The shared `a-rbac` module does **not** prescribe a role table, grant table, Department model, Admin UI, or `systemAdmin` convention. It provides a reusable runtime contract:
36
+
37
+ - catalog protected route actions;
38
+ - run the RBAC guard and validate a decision;
39
+ - bind that decision to the current request;
40
+ - expose typed data-scope operations to controllers;
41
+ - emit resolver and invalidation events; and
42
+ - derive a minimized browser permission matcher from an allowed scope.
43
+
44
+ An edition or application supplies the policy facts and resolver. Cabloy Start's `admin-rbac` module is one specimen: it resolves roles, grants, and Department facts, persists policy revisions, and provides an administration UI. Those are Start policy choices, not requirements of `a-rbac`.
45
+
46
+ ## 1. Opt in explicitly and obtain a stable action identity
47
+
48
+ A controller action participates in dynamic RBAC only when it carries `@Passport.rbac(...)` metadata:
49
+
50
+ ```typescript
51
+ @Web.get()
52
+ @Passport.rbac({ dataScope: true })
53
+ async select(...) {
54
+ // consume the scope below
55
+ }
56
+ ```
57
+
58
+ The shared RBAC catalog scans registered controller routes for this explicit metadata. An ordinary route does not become a policy action merely because a Resource or frontend UI exposes it.
59
+
60
+ Each catalogued action has a stable identity:
61
+
62
+ ```text
63
+ <controllerBeanFullName>#<action>
64
+ ```
65
+
66
+ This identity is policy-facing. It is more stable and precise than a displayed menu item, a Resource name, or an HTTP path that might be mounted differently.
67
+
68
+ ### Action inheritance
69
+
70
+ An action can inherit another action's policy inside the same controller. The catalog resolves that relationship centrally and fails closed for:
71
+
72
+ - a missing target;
73
+ - a self-reference;
74
+ - an inheritance cycle; or
75
+ - an invalid cross-controller alias.
76
+
77
+ Backend policy inheritance is not the same operation as a frontend `permissionHint.actionInherit`. The former selects the action policy guarded on the server; the latter selects an emitted action key used by a UI renderer. Keep both metadata paths aligned, but do not assume their similar names make them equivalent automatically.
78
+
79
+ **Representative shared sources**
80
+
81
+ - `vona/src/suite-vendor/a-vona/modules/a-user/src/lib/passport.ts` — `Passport.rbac(...)` guard facade.
82
+ - `vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacCatalog.ts` — route discovery, canonical keys, and inheritance validation.
83
+ - `vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/lib/rbac.ts` — action-key and request-decision helpers.
84
+
85
+ ## 2. Guard execution and the policy-resolver boundary
86
+
87
+ `GuardRbac` executes for a protected route. Its job is not to query a particular role/grant schema; it orchestrates a valid decision:
88
+
89
+ 1. clear a prior RBAC decision from the current request context;
90
+ 2. find the current route in the shared catalog;
91
+ 3. merge the catalog/decorator options for that action;
92
+ 4. ask the configured scope adapter whether the subject is unrestricted;
93
+ 5. if not unrestricted, emit `a-rbac:resolvePolicy` for application policy code;
94
+ 6. validate that the returned decision is allowed, well formed, and bound to the expected action; and
95
+ 7. store only that validated decision on the current context.
96
+
97
+ The decision is request-local capability state, stored through the `SymbolRbacDecision` helpers. It is not a process-global flag, a browser claim, or reusable state for another route.
98
+
99
+ ```text
100
+ GuardRbac
101
+ → catalog action
102
+ → unrestricted adapter branch
103
+ or
104
+ a-rbac:resolvePolicy listener
105
+ → validate action-bound decision
106
+ → set request-local decision
107
+ ```
108
+
109
+ ### Extension contracts
110
+
111
+ The shared event contracts make policy implementation replaceable:
112
+
113
+ | Extension point | Shared responsibility | Application responsibility |
114
+ | -------------------------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
115
+ | `a-rbac:resolvePolicy` | Ask for a decision for a catalog action | Resolve roles, grants, ownership, organizational facts, or another policy model |
116
+ | Scope adapter | Ask whether the subject is unrestricted; derive trusted create values | Define unrestricted subjects and trusted business fields |
117
+ | `a-rbac:policyInvalidated` | Provide a common invalidation signal | Advance/clear mutable policy state and dependent caches |
118
+
119
+ The shared role-membership listener bridges `a-user:roleMembershipChanged` to `a-rbac:policyInvalidated`. An application that mutates additional policy facts must emit invalidation through its normal durable mutation path.
120
+
121
+ ## 3. A passing guard is not complete row authorization
122
+
123
+ After guard admission, a controller must consume the current typed scope. The common injection surface is `@Arg.rbacScopeCurrent()`; framework extraction obtains `app.bean.rbacScope.current()` for the exact request action.
124
+
125
+ | Operation | Required scope operation | Why |
126
+ | ------------------ | ------------------------ | --------------------------------------------------------------------- |
127
+ | List/select | `where(callerWhere)` | Preserve the caller filter while adding server scope |
128
+ | View/update/delete | `checkEntry(entry)` | Check the persisted target, not a client claim |
129
+ | Bulk mutation | `checkEntries(entries)` | Ensure every intended target is inside scope |
130
+ | Create | `ownerValues()` | Derive trusted owner/scope fields instead of accepting widening input |
131
+
132
+ A representative list shape is:
133
+
134
+ ```typescript
135
+ @Web.get()
136
+ @Passport.rbac({ dataScope: true })
137
+ async select(@Arg.rbacScopeCurrent() rbacScopeCurrent: IRbacScopeAccess) {
138
+ return await this.scope.service.record.select({
139
+ where: rbacScopeCurrent.where(this.ctx.request.query.where),
140
+ });
141
+ }
142
+ ```
143
+
144
+ The details vary by Resource contract, but the security rules do not:
145
+
146
+ - caller filters are structurally **ANDed** with the server predicate;
147
+ - permitted scope terms are **OR** alternatives;
148
+ - unrestricted access still keeps the ordinary caller filter; and
149
+ - missing, denied, malformed, or action-mismatched request decisions fail closed.
150
+
151
+ For create operations, never trust a browser-supplied owner or organizational field merely because the browser previously displayed a matching action. Use `ownerValues()` and validate the persisted result through the same server policy model.
152
+
153
+ **Representative shared sources**
154
+
155
+ - `vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/guard.rbac.ts`
156
+ - `vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacScope.ts`
157
+ - `vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/types/scope.ts`
158
+ - `vona/src/suite-vendor/a-vona/modules/a-web/src/lib/decorator/arguments.ts`
159
+
160
+ ## 4. Resource permission projection reuses real guards
161
+
162
+ The frontend does not receive a parallel grant database. `BeanPermission` builds a Resource permission snapshot by evaluating the real route guards for the current Passport.
163
+
164
+ The endpoint path is owned by the Home Base permission controller/service. For a Resource name, `BeanPermission`:
165
+
166
+ 1. maps the Resource to its controller and cached routes;
167
+ 2. omits configured ignored actions (the default excludes `select`);
168
+ 3. applies the Passport admission precheck: public/authenticated, account-active, and activation state;
169
+ 4. runs the registered `retrievePermissionAction` extension point;
170
+ 5. evaluates the actual composed guards for the target action; and
171
+ 6. returns an ordinary boolean or an RBAC projection.
172
+
173
+ ### Target-route guard simulation
174
+
175
+ To evaluate a target action without executing its controller, the permission bean temporarily:
176
+
177
+ - installs the target route as `ctx.route`;
178
+ - sets `ctx.innerAccess = false`;
179
+ - clears any existing request-local RBAC decision;
180
+ - calls the real `composeGuards(...)` chain;
181
+ - converts `false`, `401`, and `403` to a denied permission; and
182
+ - restores the previous route, inner-access value, and the prior decision's own-property state in `finally`.
183
+
184
+ Unexpected errors are not silently treated as authorization outcomes; they propagate. This isolation is essential: a permission query must not leak or corrupt a decision from the surrounding request action.
185
+
186
+ ### Boolean actions and RBAC actions
187
+
188
+ A permitted ordinary guarded action becomes `true`; a denied action becomes `false`. A permitted RBAC action asks the current typed scope for a browser projection:
189
+
190
+ ```text
191
+ boolean
192
+ or
193
+ {
194
+ key: string,
195
+ allowed: boolean,
196
+ matcher: { mode: 'all' }
197
+ | { mode: 'any', rules: [{ field, values }] }
198
+ }
199
+ ```
200
+
201
+ `permissionProjection()` returns `all` for unrestricted or non-data-scoped access. A restricted decision becomes normalized field/value rules, such as a trusted owner field or a configured organizational field. A restricted decision with no usable matcher rules fails with `403`; it does not become a permissive empty projection.
202
+
203
+ The contract deliberately excludes routes, guard options, raw grant expressions, and policy topology. It is still authorization-derived data: treat projected field names and values as a minimized UX contract, not as information that must remain secret from the current authorized browser.
204
+
205
+ **Representative shared sources**
206
+
207
+ - `vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts`
208
+ - `vona/src/suite-vendor/a-vona/modules/a-permission/src/dto/permissions.tsx`
209
+ - `vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/permissions.ts`
210
+ - `vona/src/suite-vendor/a-cabloy/modules/a-rbac/src/bean/bean.rbacScope.ts`
211
+
212
+ ## 5. Cache topology and server freshness
213
+
214
+ Permission projection has three distinct backend cache shapes:
215
+
216
+ | Result | Cache identity | Why |
217
+ | ----------------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
218
+ | Resource snapshot | Resource + current user | The returned action map can be user-specific |
219
+ | Ordinary action | Resource + action + sorted unique role IDs | Non-RBAC guard results can be shared by role set |
220
+ | RBAC action | Resource + action + current user + sorted unique role IDs | Resolved terms can include user-specific values, such as ownership |
221
+
222
+ `BeanPermission.clearAllCaches()` clears all three permission caches. Do not collapse an RBAC result to a role-only cache: two users can hold the same roles but receive different normalized owner/scope matcher values.
223
+
224
+ Server-side invalidation must follow durable policy mutation. The shared runtime provides `a-rbac:policyInvalidated`; Start's policy listener is one specimen that advances its policy revision and schedules permission-cache clearing after database commit. A Basic/custom policy implementation must provide equivalent resolver and invalidation wiring for its own mutable facts.
225
+
226
+ > [!NOTE]
227
+ > Server invalidation refreshes **future server permission evaluations**. It is not a push channel into already-open browsers.
228
+
229
+ ## 6. DTO, SDK, and Resource ownership
230
+
231
+ The permission DTO is emitted through the backend/OpenAPI contract. Generated Home API code owns the generated operation surface, while the generic Resource runtime fetches permissions through `ModelSdk.getPermissions(resource)` so every Resource can use the same infrastructure.
232
+
233
+ ```text
234
+ GET /permission/:resource
235
+ → IOpenapiPermissions
236
+ → ModelSdk.getPermissions(resource)
237
+ → ModelResource.permissions
238
+ → page/action render scope
239
+ ```
240
+
241
+ `ModelResource` is selector-scoped by Resource name and owns Resource bootstrap, schemas, query/mutation state, and permissions. Pages, tables, and entry toolbars consume this resource-owned reactive surface instead of creating unrelated permission requests.
242
+
243
+ Read [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern) for ModelResource ownership and reuse. This guide owns the projection producer, transport, matcher, and freshness semantics.
244
+
245
+ ## 7. Frontend permission decision order
246
+
247
+ `ModelPassport.checkPermission(...)` evaluates a permission hint and Resource snapshot in this order:
248
+
249
+ 1. `permissionHint.public === true` allows immediately.
250
+ 2. Resolve `permissionHint.actionInherit ?? actionName`.
251
+ 3. With no resolved action, permit the action-based check.
252
+ 4. Nil permissions or `permissions === false` deny; `permissions === true` allows.
253
+ 5. If the resolved action exists in `permissions.actions`, use that entry:
254
+ - boolean entries are final;
255
+ - RBAC objects go to `matchPermissionAction(...)`.
256
+ 6. Only when the selected action entry is absent, try legacy `roleIds` and `roleNames` fallback.
257
+ 7. Otherwise deny.
258
+
259
+ An explicit denied action therefore wins over legacy role fallback. A public/actionless hint is UI metadata, not a backend bypass: the eventual API route still runs its own Passport/RBAC guards.
260
+
261
+ ### Matcher semantics
262
+
263
+ `matchPermissionAction(...)` is deliberately fail closed:
264
+
265
+ | Projection/data condition | Frontend result |
266
+ | ------------------------------------------------------------------------- | ----------------------------------------- |
267
+ | `allowed !== true`, missing matcher, malformed rule, or empty `any` rules | Deny |
268
+ | `{ mode: 'all' }` | Allow |
269
+ | Valid `any` matcher, no `currentData` | Allow coarse capability/preflight |
270
+ | One record | At least one rule must match that record |
271
+ | Multiple records | Every record must match at least one rule |
272
+ | Empty record array | Deny |
273
+
274
+ Rules compare `String(record[field])` with string values. With no row data, a valid restricted projection means “some records may support this action”; it does **not** mean the current or later selected row is known to be authorized.
275
+
276
+ This explains a normal table pattern:
277
+
278
+ ```text
279
+ operations-column preflight → no row data → retain a possible action container
280
+ row render → actual row → hide actions outside the projected scope
281
+ ```
282
+
283
+ Bulk/action implementations that want selected-record semantics must pass those records to the matcher. Current generic UI surfaces may use a coarser no-row-data visibility decision; backend `checkEntries(...)` remains the final bulk authority.
284
+
285
+ **Representative shared sources**
286
+
287
+ - `zova/src/suite/a-home/modules/home-passport/src/model/passport.ts`
288
+ - `zova/src/suite/a-home/modules/home-passport/src/lib/permissionActionMatcher.ts`
289
+ - `zova/src/suite/a-home/modules/home-passport/test/lib/permissionActionMatcher.test.ts`
290
+
291
+ ## 8. SSR, hydration, and active-tab freshness
292
+
293
+ With cookie-backed server identity, Resource bootstrap can load permission state during SSR and transfer it through the query-cache hydration path. The client can start from that request-confirmed projection without an immediate duplicate server render decision.
294
+
295
+ When SSR cannot use cookies for identity, `ModelResource.permissions` intentionally remains unavailable on the server. Private action visibility must retain a neutral or fail-closed shell through hydration, then obtain the client-side authoritative projection at an explicit client boundary.
296
+
297
+ ### Current freshness limitation
298
+
299
+ `ModelSdk.getPermissions(resource)` uses `staleTime: Infinity`. The current architecture does not subscribe an already-open browser tab to server-side `a-rbac:policyInvalidated`, and it has no general targeted permission-query invalidation/refetch for an already-mounted page after an administrator changes policy.
300
+
301
+ Consequences:
302
+
303
+ - a removed permission can remain visible in an open tab until an explicit refresh boundary;
304
+ - a newly granted permission can remain hidden in an open tab until that boundary; and
305
+ - neither state bypasses the next backend guard or persisted-data scope check.
306
+
307
+ Use an application-appropriate refresh, reload, relogin, or explicit query invalidation/refetch boundary after live policy changes. Do not claim that clearing backend permission caches instantly updates a hydrated frontend query.
308
+
309
+ ## 9. Authoring and debugging checklist
310
+
311
+ When extending shared RBAC:
312
+
313
+ 1. Decorate only actions intended for dynamic RBAC.
314
+ 2. Keep catalog/policy inheritance valid and test every alias.
315
+ 3. Provide an `a-rbac:resolvePolicy` listener and scope adapter for the policy facts your application owns.
316
+ 4. Consume the typed scope in every protected controller operation; a passing guard alone is not enough for row security.
317
+ 5. Derive create-time ownership with `ownerValues()` instead of trusting client fields.
318
+ 6. Emit policy invalidation through durable mutations and clear dependent server caches after successful commit.
319
+ 7. Treat matcher fields/values as a minimized browser contract, and keep browser matcher semantics aligned with backend scope semantics.
320
+ 8. Test direct requests separately from menus, buttons, or permission projections.
321
+ 9. Test stale-tab behavior explicitly whenever a policy change must update a live interface.
322
+
323
+ ## Verification checklist
324
+
325
+ - Run the shared `a-rbac` catalog/guard and scope-current tests.
326
+ - Run permission tests that cover route substitution, request-decision restoration, cache isolation, and denied guards.
327
+ - Run frontend matcher tests for malformed projections, `all`, restricted one-row checks, and multi-row checks.
328
+ - Verify anonymous, inactive, granted, ungranted, and scoped direct API requests.
329
+ - Verify both cookie-backed and cookie-disabled SSR paths preserve a hydration-safe private-action shell.
330
+ - After a policy mutation, verify future server evaluation is fresh and explicitly verify the intended browser refresh boundary.
331
+
332
+ ## Read next
333
+
334
+ - [RBAC Authorization](/backend/rbac-authorization) — Start roles, grants, five data scopes, and operational policy management.
335
+ - [Controller AOP Guide](/backend/controller-aop-guide) — route guard authoring.
336
+ - [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern) — Resource-owned query and permission state.
337
+ - [Permission, formScene, and Action Visibility Guide](/frontend/permission-formscene-action-visibility-guide) — entry-page scene filtering and action rendering.
338
+ - [Table Action Visibility and Permission Flow Guide](/frontend/table-action-visibility-permission-flow-guide) — list-page action consumers.
339
+ - [Menu Authorization](/backend/menu-authorization) — navigation disclosure, a separate policy domain.
@@ -0,0 +1,92 @@
1
+ # User Management
2
+
3
+ User Management is the administrative façade for the canonical user records of an instance. It is for system operators who need to inspect and maintain accounts; it is not a second identity store, an authentication-provider guide, or a self-service registration flow.
4
+
5
+ > <Badge type="warning" text="Start specimen" /> This guide uses the Cabloy Start `admin-user` module to make the operational model concrete. An edition can compose the administration UI differently while keeping the same boundary between user facts, roles, departments, and backend authorization.
6
+
7
+ ## What User Management owns
8
+
9
+ A typical administrative User Resource lets a system administrator:
10
+
11
+ - list and search user accounts;
12
+ - view profile and account state;
13
+ - update permitted profile fields;
14
+ - activate accounts;
15
+ - change account status; and
16
+ - inspect the user's role and Department memberships.
17
+
18
+ The Start controller exposes this resource surface in `vona/src/suite/cabloy-admin/modules/admin-user/src/controller/user.ts`. Its management actions are individually protected with `@Passport.systemAdmin()`:
19
+
20
+ ```typescript
21
+ @Web.put('account-status/:id')
22
+ @Passport.systemAdmin()
23
+ async setAccountStatus(...) {
24
+ return await this.scope.service.user.setAccountStatus(...);
25
+ }
26
+ ```
27
+
28
+ This guard is the authority boundary. A visible Admin menu or UI action does not authorize a direct request by itself.
29
+
30
+ ## User facts, role memberships, and Department memberships
31
+
32
+ A user record is one kind of fact. Role membership and Department membership are related but separate facts:
33
+
34
+ | Fact | Answers | Owning administration flow |
35
+ | --------------------- | --------------------------------------------------- | ------------------------------------------------------- |
36
+ | User profile | Who is this account? | User Management |
37
+ | Role membership | Which access groups does the user hold? | [Role Management](/backend/role-management) |
38
+ | Department membership | Which organizational units does the user belong to? | [Department Management](/backend/department-management) |
39
+
40
+ The Start user-detail service deliberately projects all three so an operator can understand the account in one place. It does not make the User Resource the owner of every membership mutation.
41
+
42
+ A practical rule is: update account lifecycle in User Management; update an access grouping through Role Management; update organizational placement through Department Management.
43
+
44
+ ## Account lifecycle safeguards
45
+
46
+ Activation and account status are different concerns. Activation is generally part of bringing a registered account into service; account status controls whether an existing account can continue to use the system.
47
+
48
+ In the Start specimen, disabling an account has operational consequences beyond changing a field:
49
+
50
+ 1. the service refuses an unsafe system-administrator transition;
51
+ 2. permission state is cleared; and
52
+ 3. active Passport sessions for the disabled user are evicted.
53
+
54
+ This protects against a stale signed-in session continuing with an old permission projection. It also illustrates why account-status changes belong in the backend service, rather than in a frontend-only switch.
55
+
56
+ ## Recommended operator workflow
57
+
58
+ ```text
59
+ Find user
60
+ → inspect account, roles, and Departments
61
+ → update profile or account lifecycle when appropriate
62
+ → change role memberships in Role Management
63
+ → change organizational memberships in Department Management
64
+ → verify the affected user's refreshed access
65
+ ```
66
+
67
+ When an operator changes a current user's access-related state, the relevant client should reacquire authoritative Passport, menu, and permission data. That refresh improves UX; the backend guards still enforce every request independently.
68
+
69
+ ## Boundaries to keep clear
70
+
71
+ - User Management is not an authentication-provider implementation. Read [Auth Guide](/backend/auth-guide) for sign-in providers and authentication flows.
72
+ - A user having a role does not automatically grant every API action. Action and data authorization are configured through [RBAC Authorization](/backend/rbac-authorization).
73
+ - Seeing a menu does not authorize an API request. See [Menu Authorization](/backend/menu-authorization).
74
+ - User records are scoped to the active Vona instance; do not treat an administrative view as a cross-instance lookup surface.
75
+
76
+ ## Verification checklist
77
+
78
+ When implementing or adapting User Management, verify that:
79
+
80
+ 1. every account-management endpoint has an explicit backend guard;
81
+ 2. user detail can accurately distinguish account fields from role and Department memberships;
82
+ 3. disabling a user invalidates permission/session state according to the active edition's policy;
83
+ 4. protected administrator transitions cannot remove the final usable administrator; and
84
+ 5. role and Department changes are made through their respective owners rather than duplicated in a user form.
85
+
86
+ ## Read next
87
+
88
+ - [User Access Guide](/backend/user-access-guide)
89
+ - [Auth Guide](/backend/auth-guide)
90
+ - [Role Management](/backend/role-management)
91
+ - [Department Management](/backend/department-management)
92
+ - [RBAC Authorization](/backend/rbac-authorization)
@@ -65,6 +65,8 @@ It becomes the place that owns:
65
65
 
66
66
  This page explains that pattern explicitly.
67
67
 
68
+ `ModelResource` owns the reactive permission surface for its Resource. For the shared backend guard evaluation, permission DTO, matcher semantics, SSR transfer, and browser-freshness boundary behind that surface, read [Shared RBAC Architecture](/backend/shared-rbac-architecture).
69
+
68
70
  ## What “resource owner” means in Zova
69
71
 
70
72
  In this context, a resource-owner model is a model bean that becomes the reusable frontend boundary for one backend resource.
@@ -17,6 +17,7 @@ Several existing docs already explain nearby pieces:
17
17
  - [Form Scene to Page Meta Guide](/frontend/form-scene-to-page-meta-guide) explains how `formScene` becomes `formMeta`, then `pageMeta`
18
18
  - [Page Meta Guide](/frontend/page-meta-guide) explains shell/task presentation
19
19
  - [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern) and related resource-model docs explain permissions as part of resource ownership
20
+ - [Shared RBAC Architecture](/backend/shared-rbac-architecture) explains the shared backend permission projection, frontend matcher, and authority boundary
20
21
 
21
22
  What those pages do not isolate directly is the specific runtime rule for entry-page action visibility.
22
23
 
@@ -134,7 +135,7 @@ Only after the scene passes does the controller call:
134
135
  This is the critical source-confirmed distinction:
135
136
 
136
137
  - **scene filtering** decides whether this action should even be considered in the current page-entry scene
137
- - **permission checking** decides whether the user is allowed to perform it
138
+ - **frontend permission checking** decides whether the current UI should offer it; backend guards remain the authoritative execution decision
138
139
 
139
140
  ## 4. What `permissionHint.formScene` really means
140
141
 
@@ -311,6 +312,7 @@ Use these next steps depending on your question:
311
312
  - if you want the list-page row/bulk contrast, read [Table Action Visibility and Permission Flow Guide](/frontend/table-action-visibility-permission-flow-guide)
312
313
  - if you want shell/task presentation semantics, read [Page Meta Guide](/frontend/page-meta-guide)
313
314
  - if you want the underlying resource permission/model context, read [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
315
+ - if you want the shared backend projection, matcher, SSR, and freshness semantics, read [Shared RBAC Architecture](/backend/shared-rbac-architecture)
314
316
 
315
317
  ## Final takeaway
316
318
 
@@ -318,8 +320,8 @@ The most accurate rule for current Cabloy Basic entry-page action visibility is:
318
320
 
319
321
  - `permissionHint.formScene` is a scene-aware visibility prefilter
320
322
  - `$$pageEntry.formMeta.formScene` is the current scene source
321
- - `$passport.checkPermission(...)` is still the actual authorization check
322
- - the action renders only if both layers pass
323
+ - `$passport.checkPermission(...)` is the frontend permission-projection/visibility check; backend guards authorize execution
324
+ - the action renders only if both UI layers pass
323
325
  - bulk actions do not currently carry `formScene` in their permission hints
324
326
 
325
327
  That is the source-confirmed `permission / formScene / action visibility` path in the current Basic frontend architecture.
@@ -16,6 +16,7 @@ Several existing docs already explain nearby pieces:
16
16
 
17
17
  - [Table Guide](/frontend/table-guide) and [Zova Table Under the Hood](/frontend/zova-table-under-the-hood) explain table runtime and rendering
18
18
  - [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern) explains `ModelResource` as the owner of permissions and schema
19
+ - [Shared RBAC Architecture](/backend/shared-rbac-architecture) explains the shared backend projection, frontend matcher, and authority boundary
19
20
  - [Permission, formScene, and Action Visibility Guide](/frontend/permission-formscene-action-visibility-guide) explains the entry-page scene-aware action path
20
21
 
21
22
  What those pages do not isolate directly is the list-page row/bulk action visibility path.
@@ -247,6 +248,7 @@ Use these next steps depending on your question:
247
248
  - if you want the list-page runtime path that hosts these actions, read [Resource List Page Deep Dive](/frontend/resource-list-page-deep-dive)
248
249
  - if you want the entry-page scene-aware action path, read [Permission, formScene, and Action Visibility Guide](/frontend/permission-formscene-action-visibility-guide)
249
250
  - if you want the broader resource-owner permission surface, read [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
251
+ - if you want the shared projection producer, matcher, SSR, and freshness semantics, read [Shared RBAC Architecture](/backend/shared-rbac-architecture)
250
252
  - if you want table runtime internals, read [Zova Table Under the Hood](/frontend/zova-table-under-the-hood)
251
253
 
252
254
  ## Final takeaway