@venizia/ignis-docs 0.0.8-1 → 0.0.8-3
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/package.json +13 -13
- package/wiki/best-practices/error-handling.md +19 -4
- package/wiki/extensions/components/authorization/api.md +239 -376
- package/wiki/extensions/components/authorization/errors.md +52 -43
- package/wiki/extensions/components/authorization/index.md +127 -65
- package/wiki/extensions/components/authorization/usage.md +198 -98
- package/wiki/guides/migrations/scoped-rbac-migration.md +300 -0
- package/wiki/references/base/filter-system/default-filter.md +6 -3
- package/wiki/references/base/filter-system/fields-order-pagination.md +26 -0
- package/wiki/references/base/middlewares.md +36 -27
- package/wiki/references/base/models.md +6 -3
|
@@ -22,11 +22,9 @@ flowchart TD
|
|
|
22
22
|
S5b -->|DI fails| E500c[/"500: Failed to resolve"/]
|
|
23
23
|
S5b -->|OK| S6{"Step 6: Build rules"}
|
|
24
24
|
|
|
25
|
-
S6 -->|
|
|
26
|
-
S6 -->|Not initialized| E500d[/"500: Enforcer not initialized"/]
|
|
25
|
+
S6 -->|Not configured| E500d[/"500: Not configured. Call configure() first."/]
|
|
27
26
|
S6 -->|No FilteredAdapter| E500e[/"500: Adapter does not support loadFilteredPolicy"/]
|
|
28
|
-
S6 -->|
|
|
29
|
-
S6 -->|Empty Redis key| E400b[/"400: Invalid cachedKey"/]
|
|
27
|
+
S6 -->|Empty Redis key| E400b[/"400: keyFn returned an empty cache key"/]
|
|
30
28
|
S6 -->|OK| S7{"Step 7: Evaluate"}
|
|
31
29
|
|
|
32
30
|
S7 -->|No action/resource| E500g[/"500: request.action and resource required"/]
|
|
@@ -73,22 +71,38 @@ All error messages from the authorization module, organized by source:
|
|
|
73
71
|
|---------------|--------|--------|
|
|
74
72
|
| `[CasbinAuthorizationEnforcer] "casbin" is not installed` | 500 | `configure` |
|
|
75
73
|
| `[CasbinAuthorizationEnforcer] options.model is required.` | 500 | `configure` |
|
|
76
|
-
| `[CasbinAuthorizationEnforcer]
|
|
77
|
-
| `[
|
|
74
|
+
| `[CasbinAuthorizationEnforcer] Not configured. Call configure() first.` | 500 | `evaluate`, `loadPolicyLinesIntoModel` |
|
|
75
|
+
| `[extractUserLines] Adapter does not support loadFilteredPolicy.` | 500 | `buildRules` (via `extractUserLines`) |
|
|
78
76
|
| `[CasbinAuthorizationEnforcer] request.action and request.resource are required.` | 500 | `evaluate` |
|
|
77
|
+
| `[CasbinAuthorizationEnforcer] keyFn returned an empty cache key.` | 400 | `buildRules`/cache management (via `resolveCacheKey`) |
|
|
79
78
|
| <code v-pre>[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000 (ms) | Received: {{value}}</code> | 500 | `configure` (via `validateExpiresIn`) |
|
|
80
|
-
| <code v-pre>[
|
|
81
|
-
| <code v-pre>[
|
|
79
|
+
| <code v-pre>[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup — ...</code> | 500 | `configure` (via `assertMatcherCompilesSync`) |
|
|
80
|
+
| <code v-pre>[resolveDomainMatchingFn] Unsupported func: {{name}} | Valids: [...]</code> | 500 | `configure` (via `registerMatchers`) |
|
|
81
|
+
| <code v-pre>[registerMatchers] Role definition "{{name}}" is not declared in the Casbin model. ...</code> | 500 | `configure` (via `registerMatchers`, only when `domainMatching` is set) |
|
|
82
82
|
| <code v-pre>[resolveModel] Invalid model.driver | Valids: [file, text]</code> | 500 | `configure` (via `resolveModel`) |
|
|
83
|
+
| `[CasbinAuthorizationEnforcer] Cache management requires the redis cache driver, but caching is disabled.` | 500 | `invalidateUserCache`/`rebuildUserCache` (via `requireRedisCache`) |
|
|
84
|
+
|
|
85
|
+
> The `Invalid cached.driver` errors were removed — `cached` is now a typed union
|
|
86
|
+
> (`{ use: false } | { use: true, driver: 'redis', ... }`), so an invalid driver is a compile-time
|
|
87
|
+
> error, not a runtime one.
|
|
83
88
|
|
|
84
89
|
### Policy Loading Errors (CasbinAuthorizationEnforcer internals)
|
|
85
90
|
|
|
86
91
|
| Error Message | Status | Method |
|
|
87
92
|
|---------------|--------|--------|
|
|
88
|
-
| `[
|
|
89
|
-
| `[
|
|
90
|
-
| `[
|
|
91
|
-
| `[
|
|
93
|
+
| `[CasbinAuthorizationEnforcer] keyFn returned an empty cache key.` | 400 | `resolveCacheKey` (read + cache-management paths) |
|
|
94
|
+
| `[extractUserLines] Adapter does not support loadFilteredPolicy.` | 500 | `extractUserLines` |
|
|
95
|
+
| `[loadPolicyLinesIntoModel] Not configured. Call configure() first.` | 500 | `loadPolicyLinesIntoModel` |
|
|
96
|
+
| `[CasbinAuthorizationEnforcer] Cached payload is not an array of policy lines.` | — (logged, not thrown) | `parseCachedPolicyLines` — corrupt entry is discarded + refetched |
|
|
97
|
+
|
|
98
|
+
> A corrupted Redis cache entry does **not** raise an error — it is logged and discarded, and the
|
|
99
|
+
> lines are refetched from the adapter (the request never 500s on cache corruption).
|
|
100
|
+
|
|
101
|
+
### Registry Errors (AuthorizationEnforcerRegistry)
|
|
102
|
+
|
|
103
|
+
| Error Message | Status | Method |
|
|
104
|
+
|---------------|--------|--------|
|
|
105
|
+
| `[AuthorizationEnforcerRegistry] Enforcer "{{name}}" does not support cache invalidation` | 500 | `invalidateUserCache` / `rebuildUserCache` (the resolved enforcer lacks the optional method) |
|
|
92
106
|
|
|
93
107
|
## Troubleshooting
|
|
94
108
|
|
|
@@ -264,18 +278,18 @@ AuthorizationEnforcerRegistry.getInstance().register({
|
|
|
264
278
|
});
|
|
265
279
|
```
|
|
266
280
|
|
|
267
|
-
### "[
|
|
281
|
+
### "[extractUserLines] Adapter does not support loadFilteredPolicy"
|
|
268
282
|
|
|
269
283
|
**Cause:** The adapter provided to the Casbin enforcer does not implement the `loadFilteredPolicy` method from casbin's `FilteredAdapter` interface. The authorization system always uses filtered policy loading.
|
|
270
284
|
|
|
271
|
-
**Fix:** Use an adapter that implements `FilteredAdapter`, such as `
|
|
285
|
+
**Fix:** Use an adapter that implements `FilteredAdapter`, such as `ScopedCasbinAdapter` or a custom adapter extending `BaseFilteredAdapter`:
|
|
272
286
|
|
|
273
287
|
```typescript
|
|
274
|
-
import {
|
|
288
|
+
import { ScopedCasbinAdapter } from '@venizia/ignis';
|
|
275
289
|
|
|
276
|
-
const adapter = new
|
|
290
|
+
const adapter = new ScopedCasbinAdapter({
|
|
277
291
|
dataSource,
|
|
278
|
-
entities: {
|
|
292
|
+
entities: { /* IScopedCasbinEntities */ },
|
|
279
293
|
});
|
|
280
294
|
```
|
|
281
295
|
|
|
@@ -297,9 +311,9 @@ cached: {
|
|
|
297
311
|
},
|
|
298
312
|
```
|
|
299
313
|
|
|
300
|
-
### "[CasbinAuthorizationEnforcer]
|
|
314
|
+
### "[CasbinAuthorizationEnforcer] Not configured. Call configure() first."
|
|
301
315
|
|
|
302
|
-
**Cause:** The Casbin enforcer's `evaluate()
|
|
316
|
+
**Cause:** The Casbin enforcer's `evaluate()` (or `loadPolicyLinesIntoModel()`) was called before `configure()` built the enforcer pool. This should not happen when using `resolveEnforcer()` (which auto-configures), but can occur if the enforcer is resolved manually via the DI container.
|
|
303
317
|
|
|
304
318
|
**Fix:** Always resolve enforcers through the registry's `resolveEnforcer()` method, which handles configure-once automatically:
|
|
305
319
|
|
|
@@ -323,36 +337,31 @@ keyFn: ({ user }) => {
|
|
|
323
337
|
},
|
|
324
338
|
```
|
|
325
339
|
|
|
326
|
-
### "[
|
|
327
|
-
|
|
328
|
-
**Cause:** `loadPoliciesFromAdapter()` was called but the internal casbin enforcer is null. This is an internal state error.
|
|
329
|
-
|
|
330
|
-
**Fix:** This should not occur in normal usage. If it does, ensure `configure()` completed successfully before any policy loading operations.
|
|
340
|
+
### "[CasbinAuthorizationEnforcer] Not configured. Call configure() first."
|
|
331
341
|
|
|
332
|
-
|
|
342
|
+
**Cause:** `evaluate()` or `loadPolicyLinesIntoModel()` ran before the enforcer pool was built.
|
|
333
343
|
|
|
334
|
-
**
|
|
344
|
+
**Fix:** Ensure `configure()` completed successfully (the registry calls it on first use) before any
|
|
345
|
+
evaluation. The enforcer pool is created in `configure()`.
|
|
335
346
|
|
|
336
|
-
|
|
347
|
+
### "[CasbinAuthorizationEnforcer] keyFn returned an empty cache key."
|
|
337
348
|
|
|
338
|
-
|
|
349
|
+
**Cause:** The Redis `cached.options.keyFn` returned an empty string for a user.
|
|
339
350
|
|
|
340
|
-
**
|
|
351
|
+
**Fix:** Return a stable, non-empty key (commonly derived from `user.principalType` + `user.userId`):
|
|
341
352
|
|
|
342
|
-
|
|
353
|
+
```typescript
|
|
354
|
+
keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
|
|
355
|
+
```
|
|
343
356
|
|
|
344
|
-
### Invalid driver
|
|
357
|
+
### "[resolveModel] Invalid model.driver | Valids: [file, text]"
|
|
345
358
|
|
|
346
|
-
**
|
|
347
|
-
- `[buildRules] Invalid cached.driver | Valids: [in-memory, redis]`
|
|
348
|
-
- `[resolveCasbinEnforcer] Invalid cached.driver | Valids: [in-memory, redis]`
|
|
349
|
-
- `[resolveModel] Invalid model.driver | Valids: [file, text]`
|
|
359
|
+
**Cause:** An unsupported `model.driver` string was passed.
|
|
350
360
|
|
|
351
|
-
**
|
|
361
|
+
**Fix:** Use `CasbinEnforcerModelDrivers.FILE` (`'file'`) or `CasbinEnforcerModelDrivers.TEXT` (`'text'`).
|
|
352
362
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
- Model drivers: `CasbinEnforcerModelDrivers.FILE` (`'file'`) or `CasbinEnforcerModelDrivers.TEXT` (`'text'`)
|
|
363
|
+
> There is no longer an `Invalid cached.driver` runtime error — `cached` is a typed union, so an
|
|
364
|
+
> unsupported cache driver is caught at compile time. Caching is **Redis-only**.
|
|
356
365
|
|
|
357
366
|
## Common Patterns
|
|
358
367
|
|
|
@@ -373,10 +382,10 @@ Check that rules are being cached correctly. The middleware caches on `Authoriza
|
|
|
373
382
|
|
|
374
383
|
### Casbin Policies Not Loading
|
|
375
384
|
|
|
376
|
-
1. **Check adapter entities** -- ensure `
|
|
377
|
-
2. **Check variant column** --
|
|
378
|
-
3. **Check subject/target types** -- the SQL queries filter by `subject_type` and `
|
|
379
|
-
4. **Check the model
|
|
385
|
+
1. **Check adapter entities** -- ensure `IScopedCasbinEntities` (`policyDefinition`/`permission` table names + `schemaName`, `principals`, `domainTypes`) match your database schema
|
|
386
|
+
2. **Check the variant column** -- `PolicyDefinition.variant` must use the `AuthorizationPolicyVariants.*.action` values: `grant`, `assign_role`, `join_domain`, `role_inherits`, `resource_inherits`, `action_inherits`, `domain_inherits`
|
|
387
|
+
3. **Check subject/target types** -- the SQL queries filter by `subject_type`/`target_type` against `principals` and `domainTypes`
|
|
388
|
+
4. **Check the model** -- for scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` with `isScoped: true`
|
|
380
389
|
|
|
381
390
|
### Redis Cache Not Working
|
|
382
391
|
|
|
@@ -20,10 +20,8 @@
|
|
|
20
20
|
| **AuthorizationProvider** | IProvider producing the `authorize()` middleware factory |
|
|
21
21
|
| **authorize** | Standalone function wrapping `AuthorizationProvider.value()` |
|
|
22
22
|
| **AuthorizationRole** | Value object for role identity with priority-based comparison |
|
|
23
|
-
| **BaseFilteredAdapter** |
|
|
24
|
-
| **
|
|
25
|
-
| **StringAuthorizationAction** | `IAuthorizationComparable` implementation for string actions (includes `WILDCARD = '*'`) |
|
|
26
|
-
| **StringAuthorizationResource** | `IAuthorizationComparable` implementation for string resources |
|
|
23
|
+
| **BaseFilteredAdapter** | Thin abstract casbin `FilteredAdapter` (datasource plumbing + `loadLines`); subclasses implement only `loadFilteredPolicy` |
|
|
24
|
+
| **ScopedCasbinAdapter** | Generic read-only `FilteredAdapter` for the scoped RBAC model — reads one principal's edges + the shared hierarchy from a single `PolicyDefinition` table |
|
|
27
25
|
| **AbstractAuthRegistry** | Shared base class for authentication strategy registry and authorization enforcer registry |
|
|
28
26
|
|
|
29
27
|
### Authorization Flow (7 Steps)
|
|
@@ -72,6 +70,7 @@ flowchart TD
|
|
|
72
70
|
| `Authorization.RULES` | `'authorization.rules'` | Context key for cached rules |
|
|
73
71
|
| `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | Context key to dynamically skip authorization |
|
|
74
72
|
| `Authorization.ENFORCER` | `'authorization.enforcer'` | Binding key prefix for enforcers |
|
|
73
|
+
| `Authorization.DOMAIN` | `'authorization.domain'` | Context key for the resolved request domain scope (set by the provider when domain scoping is in play) |
|
|
75
74
|
|
|
76
75
|
### Authorization Actions
|
|
77
76
|
|
|
@@ -125,21 +124,72 @@ flowchart TD
|
|
|
125
124
|
|
|
126
125
|
| Constant | Value | Description |
|
|
127
126
|
|----------|-------|-------------|
|
|
128
|
-
| `CasbinEnforcerCachedDrivers.
|
|
129
|
-
| `CasbinEnforcerCachedDrivers.REDIS` | `'redis'` | Redis-backed cache with TTL |
|
|
127
|
+
| `CasbinEnforcerCachedDrivers.REDIS` | `'redis'` | Redis-backed per-user line cache with TTL (the only cache driver) |
|
|
130
128
|
|
|
131
129
|
`CasbinEnforcerCachedDrivers.SCHEME_SET` contains all valid drivers. `CasbinEnforcerCachedDrivers.isValid(input)` checks membership.
|
|
132
130
|
|
|
133
|
-
|
|
131
|
+
> The in-memory cache driver was removed. Caching is **Redis-only**: set `cached` to
|
|
132
|
+
> `{ use: true, driver: 'redis', options: { connection, expiresIn, keyFn } }`, or `{ use: false }`
|
|
133
|
+
> to disable caching (every request rebuilds the user's policy from the datasource).
|
|
134
|
+
|
|
135
|
+
### Casbin Domain Matching Functions
|
|
136
|
+
|
|
137
|
+
Built-in Casbin matching functions selectable for `ICasbinEnforcerOptions.domainMatching.fn`. Each value maps 1:1 to a Casbin `Util.*Func` export and is applied to the **domain slot** of a role definition (e.g. `g`).
|
|
134
138
|
|
|
135
139
|
| Constant | Value | Description |
|
|
136
140
|
|----------|-------|-------------|
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH` | `'keyMatch'` | `*` is the only wildcard; exact compare otherwise. Recommended for `Merchant_<uuid>`-style domains |
|
|
142
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH_2` | `'keyMatch2'` | Adds URL-path `:param` segment matching |
|
|
143
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH_3` | `'keyMatch3'` | Adds `{param}` segment matching |
|
|
144
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH_4` | `'keyMatch4'` | `{param}` with repeated-name equality checks |
|
|
145
|
+
| `CasbinDomainMatchingFunctions.REGEX_MATCH` | `'regexMatch'` | Treats the stored/policy value as a full regular expression |
|
|
146
|
+
|
|
147
|
+
`CasbinDomainMatchingFunctions.SCHEME_SET` contains all valid values. `CasbinDomainMatchingFunctions.isValid(input)` checks membership. Companion type: `TCasbinDomainMatchingFunction`.
|
|
148
|
+
|
|
149
|
+
> [!IMPORTANT]
|
|
150
|
+
> The function is applied as `fn(requestDomain, policyDomain)` — the wildcard must live on the **stored/policy** side. With `keyMatch`: `keyMatch("Merchant_X", "*") === true`, `keyMatch("Merchant_X", "Merchant_X") === true`, `keyMatch("Merchant_X", "Merchant_Y") === false`. Store only `*` or exact domain values (never partial patterns like `Merchant_*`) to keep tenant isolation guaranteed.
|
|
151
|
+
|
|
152
|
+
### Casbin Rule Variants
|
|
153
|
+
|
|
154
|
+
`CasbinRuleVariants` holds **only the Casbin line prefixes** declared by the model, numbered in
|
|
155
|
+
request-tuple order (`sub → dom → obj → act`):
|
|
156
|
+
|
|
157
|
+
| Constant | Value | Relation |
|
|
158
|
+
|----------|-------|----------|
|
|
159
|
+
| `CasbinRuleVariants.P` | `'p'` | Permission policy line |
|
|
160
|
+
| `CasbinRuleVariants.G` | `'g'` | Role membership + role inheritance (the `sub` axis) |
|
|
161
|
+
| `CasbinRuleVariants.G2` | `'g2'` | User→domain membership (the `dom` axis) |
|
|
162
|
+
| `CasbinRuleVariants.G3` | `'g3'` | Domain hierarchy (the `dom` axis) |
|
|
163
|
+
| `CasbinRuleVariants.G4` | `'g4'` | Resource hierarchy (the `obj` axis, via `objectMatch`) |
|
|
164
|
+
| `CasbinRuleVariants.G5` | `'g5'` | Action hierarchy (the `act` axis) |
|
|
165
|
+
|
|
166
|
+
### Authorization Policy Variants
|
|
167
|
+
|
|
168
|
+
The DB `variant` discriminator (the kind of "edge" stored in `PolicyDefinition`) lives on
|
|
169
|
+
`AuthorizationPolicyVariants`. Each entry carries `action` (the DB value) and `rule` (the Casbin prefix
|
|
170
|
+
the adapter emits for that edge):
|
|
171
|
+
|
|
172
|
+
| Variant | `action` (DB) | `rule` | Meaning |
|
|
173
|
+
|---------|---------------|--------|---------|
|
|
174
|
+
| `GRANT` | `'grant'` | `p` | Give a permission to a User or Role |
|
|
175
|
+
| `ASSIGN_ROLE` | `'assign_role'` | `g` | Give a User a Role (optionally domain-scoped) |
|
|
176
|
+
| `ROLE_INHERITS` | `'role_inherits'` | `g` | Role inherits another Role |
|
|
177
|
+
| `JOIN_DOMAIN` | `'join_domain'` | `g2` | User is a member of a Domain |
|
|
178
|
+
| `DOMAIN_INHERITS` | `'domain_inherits'` | `g3` | Domain nested under a parent Domain |
|
|
179
|
+
| `RESOURCE_INHERITS` | `'resource_inherits'` | `g4` | Resource nested under a broader Resource |
|
|
180
|
+
| `ACTION_INHERITS` | `'action_inherits'` | `g5` | Action implied by a broader Action |
|
|
141
181
|
|
|
142
|
-
`
|
|
182
|
+
`AuthorizationPolicyVariants.isValidAction(input)` / `isValidRule(input)` check membership;
|
|
183
|
+
`ACTION_SCHEME_SET` / `RULE_SCHEME_SET` hold the sets.
|
|
184
|
+
|
|
185
|
+
### Authorization Domain Scopes
|
|
186
|
+
|
|
187
|
+
Sentinel domain values used on `grant` rows:
|
|
188
|
+
|
|
189
|
+
| Constant | Value | Meaning |
|
|
190
|
+
|----------|-------|---------|
|
|
191
|
+
| `AuthorizationDomainScopes.ANY_MEMBER` | `'ANY_MEMBER'` | Applies in every domain the subject joined (checked via `g2`) |
|
|
192
|
+
| `AuthorizationDomainScopes.SYSTEM_WIDE` | `'SYSTEM_WIDE'` | Applies system-wide, bypassing membership (super-admin) |
|
|
143
193
|
|
|
144
194
|
> [!NOTE]
|
|
145
195
|
> All constant classes follow the same pattern: static readonly values + `SCHEME_SET: Set<string>` + `isValid(input): boolean`. Each class also has a companion type alias generated via `TConstValue<typeof ClassName>` (e.g., `TAuthorizationAction`, `TAuthorizationDecision`, `TCasbinRuleVariant`).
|
|
@@ -174,22 +224,26 @@ import {
|
|
|
174
224
|
|
|
175
225
|
// Adapters
|
|
176
226
|
BaseFilteredAdapter,
|
|
177
|
-
|
|
227
|
+
ScopedCasbinAdapter,
|
|
228
|
+
|
|
229
|
+
// Scoped RBAC model
|
|
230
|
+
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
178
231
|
|
|
179
232
|
// Models
|
|
180
233
|
AuthorizationRole,
|
|
181
|
-
StringAuthorizationAction,
|
|
182
|
-
StringAuthorizationResource,
|
|
183
234
|
|
|
184
235
|
// Constants
|
|
185
236
|
Authorization,
|
|
186
237
|
AuthorizationActions,
|
|
187
238
|
AuthorizationDecisions,
|
|
239
|
+
AuthorizationDomainScopes,
|
|
240
|
+
AuthorizationPolicyVariants,
|
|
188
241
|
AuthorizationRoles,
|
|
189
242
|
AuthorizationEnforcerTypes,
|
|
190
243
|
CasbinEnforcerModelDrivers,
|
|
191
244
|
CasbinEnforcerCachedDrivers,
|
|
192
245
|
CasbinRuleVariants,
|
|
246
|
+
CasbinDomainMatchingFunctions,
|
|
193
247
|
|
|
194
248
|
// Binding keys
|
|
195
249
|
AuthorizeBindingKeys,
|
|
@@ -203,19 +257,15 @@ import type {
|
|
|
203
257
|
IAuthorizationSpec,
|
|
204
258
|
IAuthorizationRequest,
|
|
205
259
|
IAuthorizationRole,
|
|
206
|
-
IAuthorizationComparable,
|
|
207
260
|
|
|
208
261
|
// Casbin options
|
|
209
262
|
ICasbinEnforcerOptions,
|
|
210
|
-
ICasbinEnforcerCachedMemory,
|
|
211
263
|
ICasbinEnforcerCachedRedis,
|
|
212
264
|
|
|
213
265
|
// Adapter types
|
|
214
|
-
IBaseFilteredAdapterEntities,
|
|
215
266
|
ICasbinPolicyFilter,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
IDrizzleCasbinAdapterOptions,
|
|
267
|
+
IScopedCasbinEntities,
|
|
268
|
+
IScopedCasbinPolicyFilter,
|
|
219
269
|
|
|
220
270
|
// Function & utility types
|
|
221
271
|
TAuthorizeFn,
|
|
@@ -230,6 +280,7 @@ import type {
|
|
|
230
280
|
TCasbinEnforcerCachedDriver,
|
|
231
281
|
TCasbinEnforcerModelDriver,
|
|
232
282
|
TCasbinRuleVariant,
|
|
283
|
+
TCasbinDomainMatchingFunction,
|
|
233
284
|
} from '@venizia/ignis';
|
|
234
285
|
```
|
|
235
286
|
|
|
@@ -290,18 +341,20 @@ import {
|
|
|
290
341
|
AuthorizationEnforcerTypes,
|
|
291
342
|
CasbinAuthorizationEnforcer,
|
|
292
343
|
CasbinEnforcerModelDrivers,
|
|
293
|
-
|
|
344
|
+
ScopedCasbinAdapter,
|
|
345
|
+
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
294
346
|
} from '@venizia/ignis';
|
|
295
|
-
import path from 'node:path';
|
|
296
347
|
|
|
297
|
-
//
|
|
298
|
-
|
|
348
|
+
// The generic scoped adapter — reads one principal's edges + the shared hierarchy from a single
|
|
349
|
+
// PolicyDefinition edge table. No subclassing; configure it with IScopedCasbinEntities.
|
|
350
|
+
const adapter = new ScopedCasbinAdapter({
|
|
299
351
|
dataSource,
|
|
300
352
|
entities: {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
353
|
+
policyDefinition: { tableName: 'PolicyDefinition', schemaName: 'identity' },
|
|
354
|
+
permission: { tableName: 'Permission', schemaName: 'identity' },
|
|
355
|
+
principals: { user: 'User', role: 'Role' }, // casbin name prefixes
|
|
356
|
+
domainTypes: ['Merchant', 'Organizer'], // domain types you scope on
|
|
357
|
+
softDelete: { use: true, columnName: 'deleted_at' },
|
|
305
358
|
},
|
|
306
359
|
});
|
|
307
360
|
|
|
@@ -313,9 +366,10 @@ AuthorizationEnforcerRegistry.getInstance().register({
|
|
|
313
366
|
type: AuthorizationEnforcerTypes.CASBIN,
|
|
314
367
|
options: {
|
|
315
368
|
model: {
|
|
316
|
-
driver: CasbinEnforcerModelDrivers.
|
|
317
|
-
definition:
|
|
369
|
+
driver: CasbinEnforcerModelDrivers.TEXT,
|
|
370
|
+
definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
318
371
|
},
|
|
372
|
+
isScoped: true, // 4-token (sub, dom, obj, act); auto-registers keyMatch + objectMatch
|
|
319
373
|
adapter,
|
|
320
374
|
cached: {
|
|
321
375
|
use: true,
|
|
@@ -323,15 +377,12 @@ AuthorizationEnforcerRegistry.getInstance().register({
|
|
|
323
377
|
options: {
|
|
324
378
|
connection: redisHelper,
|
|
325
379
|
expiresIn: 5 * 60 * 1000, // 5 minutes
|
|
326
|
-
keyFn: ({ user }) => `authz:policies:${user.userId}`,
|
|
380
|
+
keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
|
|
327
381
|
},
|
|
328
382
|
},
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
resource,
|
|
333
|
-
action,
|
|
334
|
-
}),
|
|
383
|
+
// poolSize / poolAcquireTimeoutMs are optional (defaults 16 / 5000ms).
|
|
384
|
+
// In scoped mode you do NOT pass domainMatching or normalizePayloadFn — the request domain
|
|
385
|
+
// is supplied by the provider's domain resolver (see "Domain scoping" in usage.md).
|
|
335
386
|
},
|
|
336
387
|
}],
|
|
337
388
|
});
|
|
@@ -361,7 +412,8 @@ import {
|
|
|
361
412
|
AuthorizationEnforcerTypes,
|
|
362
413
|
CasbinAuthorizationEnforcer,
|
|
363
414
|
CasbinEnforcerModelDrivers,
|
|
364
|
-
|
|
415
|
+
ScopedCasbinAdapter,
|
|
416
|
+
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
365
417
|
BaseApplication,
|
|
366
418
|
IAuthorizeOptions,
|
|
367
419
|
} from '@venizia/ignis';
|
|
@@ -378,7 +430,7 @@ export class Application extends BaseApplication {
|
|
|
378
430
|
this.component(AuthorizeComponent);
|
|
379
431
|
|
|
380
432
|
// Step 3: Register enforcer(s) with co-located options
|
|
381
|
-
const adapter = new
|
|
433
|
+
const adapter = new ScopedCasbinAdapter({ dataSource, entities: { /* IScopedCasbinEntities */ } });
|
|
382
434
|
|
|
383
435
|
AuthorizationEnforcerRegistry.getInstance().register({
|
|
384
436
|
container: this,
|
|
@@ -388,9 +440,10 @@ export class Application extends BaseApplication {
|
|
|
388
440
|
type: AuthorizationEnforcerTypes.CASBIN,
|
|
389
441
|
options: {
|
|
390
442
|
model: {
|
|
391
|
-
driver: CasbinEnforcerModelDrivers.
|
|
392
|
-
definition:
|
|
443
|
+
driver: CasbinEnforcerModelDrivers.TEXT,
|
|
444
|
+
definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
393
445
|
},
|
|
446
|
+
isScoped: true,
|
|
394
447
|
adapter,
|
|
395
448
|
cached: { use: false },
|
|
396
449
|
},
|
|
@@ -416,11 +469,14 @@ Global authorization settings. Bound to the container before registering `Author
|
|
|
416
469
|
|--------|------|---------|-------------|
|
|
417
470
|
| `defaultDecision` | `TAuthorizationDecision` | -- | **Required.** Decision when enforcer returns `ABSTAIN` (`'allow'`, `'deny'`, or `'abstain'`) |
|
|
418
471
|
| `alwaysAllowRoles` | `string[]` | `[]` | Roles that bypass all authorization checks (global) |
|
|
472
|
+
| `domainResolver` | `TAuthorizationDomainResolver` | -- | Fallback domain resolver used when a route's `spec.domain` is not set. Returns `{ type, id }` or `null` (→ `SYSTEM_WIDE`) |
|
|
419
473
|
|
|
420
474
|
```typescript
|
|
421
475
|
interface IAuthorizeOptions {
|
|
422
476
|
defaultDecision: TAuthorizationDecision;
|
|
423
477
|
alwaysAllowRoles?: string[];
|
|
478
|
+
/** Fallback domain resolver used when a route's spec has no `domain`. */
|
|
479
|
+
domainResolver?: TAuthorizationDomainResolver;
|
|
424
480
|
}
|
|
425
481
|
```
|
|
426
482
|
|
|
@@ -430,10 +486,14 @@ Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistr
|
|
|
430
486
|
|
|
431
487
|
| Option | Type | Default | Description |
|
|
432
488
|
|--------|------|---------|-------------|
|
|
433
|
-
| `model` | `{ driver, definition }` | -- | **Required.** Casbin model definition (file path or inline text) |
|
|
434
|
-
| `cached` | `{ use: false } \| { use: true, driver, options }` | -- | **Required.** Caching configuration |
|
|
435
|
-
| `adapter` | `Adapter` | -- | Casbin adapter instance (e.g., `
|
|
436
|
-
| `
|
|
489
|
+
| `model` | `{ driver, definition }` | -- | **Required.** Casbin model definition (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
|
|
490
|
+
| `cached` | `{ use: false } \| { use: true, driver: 'redis', options }` | -- | **Required.** Caching configuration (Redis-only) |
|
|
491
|
+
| `adapter` | `Adapter` | -- | Casbin adapter instance (e.g., `ScopedCasbinAdapter`) |
|
|
492
|
+
| `isScoped` | `boolean` | `false` | Enable the scoped model: 4-token `(sub, dom, obj, act)` requests; auto-registers `keyMatch` on `g` + `objectMatch` on the resource relation |
|
|
493
|
+
| `poolSize` | `number` | `16` | Number of pooled enforcers (each request enforces on its own) |
|
|
494
|
+
| `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
|
|
495
|
+
| `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | -- | (Non-scoped/custom) normalize subject/resource/action before evaluation |
|
|
496
|
+
| `domainMatching` | `{ roleDefinition: string; fn: TCasbinDomainMatchingFunction }` | -- | (Non-scoped) opt-in domain matching function on a role definition. **Not needed when `isScoped: true`** — the scoped model registers its matchers automatically |
|
|
437
497
|
|
|
438
498
|
```typescript
|
|
439
499
|
interface ICasbinEnforcerOptions<
|
|
@@ -448,15 +508,17 @@ interface ICasbinEnforcerOptions<
|
|
|
448
508
|
|
|
449
509
|
cached:
|
|
450
510
|
| { use: false }
|
|
451
|
-
| { use: true
|
|
452
|
-
| { use: true; driver: 'redis'; options: {
|
|
453
|
-
connection: DefaultRedisHelper;
|
|
454
|
-
expiresIn: number;
|
|
455
|
-
keyFn: (opts: { user: { principalType: string } & IAuthUser }) => ValueOrPromise<string>;
|
|
456
|
-
} };
|
|
511
|
+
| (ICasbinEnforcerCachedRedis & { use: true });
|
|
457
512
|
|
|
458
513
|
adapter?: TAdapter;
|
|
459
514
|
|
|
515
|
+
// Enable the scoped RBAC model (4-token requests + auto-registered matchers).
|
|
516
|
+
isScoped?: boolean;
|
|
517
|
+
|
|
518
|
+
// Per-request enforcer pool (concurrency-safe; fail-closed on error).
|
|
519
|
+
poolSize?: number; // default 16
|
|
520
|
+
poolAcquireTimeoutMs?: number; // default 5000
|
|
521
|
+
|
|
460
522
|
normalizePayloadFn?(opts: {
|
|
461
523
|
user: IAuthUser;
|
|
462
524
|
action: TAction;
|
|
@@ -468,6 +530,13 @@ interface ICasbinEnforcerOptions<
|
|
|
468
530
|
action: string;
|
|
469
531
|
domain?: string;
|
|
470
532
|
};
|
|
533
|
+
|
|
534
|
+
// Non-scoped only. Registers a Casbin domain matching function on the named role definition.
|
|
535
|
+
// When isScoped is true, the scoped model registers its own matchers — do not set this.
|
|
536
|
+
domainMatching?: {
|
|
537
|
+
roleDefinition: string; // e.g. 'g'
|
|
538
|
+
fn: TCasbinDomainMatchingFunction;
|
|
539
|
+
};
|
|
471
540
|
}
|
|
472
541
|
```
|
|
473
542
|
|
|
@@ -476,25 +545,19 @@ interface ICasbinEnforcerOptions<
|
|
|
476
545
|
|
|
477
546
|
#### Cache Configuration Types
|
|
478
547
|
|
|
479
|
-
The `cached` field is a discriminated union:
|
|
548
|
+
The `cached` field is a discriminated union. **Caching is Redis-only** (the in-memory driver was removed):
|
|
480
549
|
|
|
481
550
|
```typescript
|
|
482
|
-
// No caching
|
|
551
|
+
// No caching — every request rebuilds the user's policy from the datasource.
|
|
483
552
|
interface { use: false }
|
|
484
553
|
|
|
485
|
-
//
|
|
486
|
-
interface ICasbinEnforcerCachedMemory {
|
|
487
|
-
driver: 'in-memory';
|
|
488
|
-
options: { expiresIn: number };
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Redis cache (store/retrieve policy lines from Redis)
|
|
554
|
+
// Redis cache (store/retrieve the user's policy lines from Redis, TTL via PX).
|
|
492
555
|
interface ICasbinEnforcerCachedRedis {
|
|
493
556
|
driver: 'redis';
|
|
494
557
|
options: {
|
|
495
558
|
connection: DefaultRedisHelper;
|
|
496
559
|
expiresIn: number;
|
|
497
|
-
keyFn: (opts: { user:
|
|
560
|
+
keyFn: (opts: { user: IAuthorizationUser }) => ValueOrPromise<string>;
|
|
498
561
|
};
|
|
499
562
|
}
|
|
500
563
|
```
|
|
@@ -519,9 +582,6 @@ interface IAuthorizationSpec<E extends Env = Env, TAction = string, TResource =
|
|
|
519
582
|
}
|
|
520
583
|
```
|
|
521
584
|
|
|
522
|
-
> [!NOTE]
|
|
523
|
-
> `TAction` and `TResource` default to `string` but can accept `IAuthorizationComparable` implementations (e.g., `StringAuthorizationAction`) for custom comparison logic.
|
|
524
|
-
|
|
525
585
|
### TAuthorizationConditions
|
|
526
586
|
|
|
527
587
|
Key-value conditions for attribute-based access control. Values are compared with strict equality (`===`).
|
|
@@ -598,6 +658,7 @@ declare module 'hono' {
|
|
|
598
658
|
// Authorization
|
|
599
659
|
[Authorization.RULES]: unknown;
|
|
600
660
|
[Authorization.SKIP_AUTHORIZATION]: boolean;
|
|
661
|
+
[Authorization.DOMAIN]: string;
|
|
601
662
|
}
|
|
602
663
|
}
|
|
603
664
|
```
|
|
@@ -608,6 +669,7 @@ declare module 'hono' {
|
|
|
608
669
|
|-----|----------|------|-------------|
|
|
609
670
|
| `'authorization.rules'` | `Authorization.RULES` | `unknown` | Cached rules built by the enforcer. Type depends on enforcer implementation. |
|
|
610
671
|
| `'authorization.skip'` | `Authorization.SKIP_AUTHORIZATION` | `boolean` | Set to `true` to dynamically skip authorization for this request. |
|
|
672
|
+
| `'authorization.domain'` | `Authorization.DOMAIN` | `string` | Resolved request domain scope (`"<Type>_<id>"` or `SYSTEM_WIDE`); set by the provider when `spec.domain` or a global `domainResolver` is in play, and read by the enforcer. |
|
|
611
673
|
|
|
612
674
|
### Authentication Variables Used by Authorization
|
|
613
675
|
|