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.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +22 -0
- package/package.json +1 -1
- package/repo-docs/.vitepress/config.mjs +11 -0
- package/repo-docs/backend/department-management.md +98 -0
- package/repo-docs/backend/menu-authorization.md +124 -0
- package/repo-docs/backend/rbac-authorization.md +117 -0
- package/repo-docs/backend/resource-field-update.md +2 -2
- package/repo-docs/backend/role-management.md +92 -0
- package/repo-docs/backend/shared-rbac-architecture.md +339 -0
- package/repo-docs/backend/user-management.md +92 -0
- package/repo-docs/frontend/model-resource-owner-pattern.md +2 -0
- package/repo-docs/frontend/permission-formscene-action-visibility-guide.md +5 -3
- package/repo-docs/frontend/table-action-visibility-permission-flow-guide.md +2 -0
- package/repo-docs/frontend/table-guide.md +75 -22
- package/repo-docs/frontend/table-resource-crud-cookbook.md +25 -4
- package/repo-docs/frontend/zova-table-source-reading-map.md +54 -19
- package/repo-docs/frontend/zova-table-under-the-hood.md +70 -39
- package/repo-e2e/specs/cabloy-basic.spec.ts +16 -16
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/pnpm-lock.yaml +80 -84
- package/zova/pnpm-lock.yaml +16 -16
|
@@ -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
|
|
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
|
|
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
|