@venizia/ignis-docs 0.2.1-0 → 0.2.1-1
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/content/best-practices/architectural-patterns.md +3 -3
- package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
- package/content/best-practices/contribution-workflow.md +2 -2
- package/content/best-practices/error-handling.md +94 -89
- package/content/best-practices/security-guidelines.md +5 -5
- package/content/extensions/components/api-reference.md +22 -21
- package/content/extensions/components/authentication/api.md +64 -28
- package/content/extensions/components/authentication/errors.md +19 -5
- package/content/extensions/components/authentication/index.md +19 -18
- package/content/extensions/components/authentication/usage.md +55 -30
- package/content/extensions/components/authorization/api.md +351 -84
- package/content/extensions/components/authorization/errors.md +51 -17
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +24 -16
- package/content/extensions/components/authorization/usage.md +45 -21
- package/content/extensions/components/health-check.md +15 -9
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +105 -54
- package/content/extensions/components/mail/errors.md +12 -10
- package/content/extensions/components/mail/index.md +32 -13
- package/content/extensions/components/mail/usage.md +26 -18
- package/content/extensions/components/request-tracker.md +18 -14
- package/content/extensions/components/socket-io/api.md +377 -882
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +72 -88
- package/content/extensions/components/socket-io/usage.md +107 -117
- package/content/extensions/components/static-asset/api.md +83 -31
- package/content/extensions/components/static-asset/errors.md +18 -7
- package/content/extensions/components/static-asset/index.md +18 -11
- package/content/extensions/components/static-asset/usage.md +11 -6
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +58 -27
- package/content/extensions/components/websocket/errors.md +3 -3
- package/content/extensions/components/websocket/index.md +8 -7
- package/content/extensions/components/websocket/usage.md +21 -8
- package/content/extensions/helpers/cron/index.md +8 -7
- package/content/extensions/helpers/crypto/index.md +16 -8
- package/content/extensions/helpers/crypto/reference.md +96 -24
- package/content/extensions/helpers/env/index.md +14 -10
- package/content/extensions/helpers/error/index.md +99 -23
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +23 -6
- package/content/extensions/helpers/inversion/reference.md +30 -22
- package/content/extensions/helpers/kafka/admin.md +3 -2
- package/content/extensions/helpers/kafka/compile-binary.md +69 -44
- package/content/extensions/helpers/kafka/consumer.md +24 -21
- package/content/extensions/helpers/kafka/examples.md +22 -234
- package/content/extensions/helpers/kafka/index.md +30 -62
- package/content/extensions/helpers/kafka/producer.md +31 -26
- package/content/extensions/helpers/kafka/schema-registry.md +19 -14
- package/content/extensions/helpers/logger/hf-logger.md +49 -22
- package/content/extensions/helpers/logger/index.md +37 -12
- package/content/extensions/helpers/logger/pino.md +31 -11
- package/content/extensions/helpers/logger/reference.md +243 -52
- package/content/extensions/helpers/network/api.md +65 -30
- package/content/extensions/helpers/network/index.md +32 -11
- package/content/extensions/helpers/queue/index.md +17 -6
- package/content/extensions/helpers/queue/reference.md +52 -25
- package/content/extensions/helpers/redis/index.md +29 -11
- package/content/extensions/helpers/redis/reference.md +85 -28
- package/content/extensions/helpers/secrets/index.md +82 -12
- package/content/extensions/helpers/socket-io/api.md +40 -21
- package/content/extensions/helpers/socket-io/index.md +26 -10
- package/content/extensions/helpers/storage/api.md +42 -24
- package/content/extensions/helpers/storage/index.md +10 -7
- package/content/extensions/helpers/types/index.md +20 -7
- package/content/extensions/helpers/types/reference.md +53 -14
- package/content/extensions/helpers/uid/index.md +166 -10
- package/content/extensions/helpers/websocket/api.md +52 -13
- package/content/extensions/helpers/websocket/index.md +7 -4
- package/content/extensions/helpers/worker-thread/index.md +9 -5
- package/content/extensions/helpers/worker-thread/reference.md +20 -20
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +4 -3
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +7 -6
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +63 -21
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +14 -14
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +32 -31
- package/content/references/base/datasources.md +6 -6
- package/content/references/base/dependency-injection.md +12 -11
- package/content/references/base/filter-system/application-usage.md +54 -30
- package/content/references/base/filter-system/array-operators.md +24 -46
- package/content/references/base/filter-system/comparison-operators.md +47 -67
- package/content/references/base/filter-system/default-filter.md +59 -53
- package/content/references/base/filter-system/fields-order-pagination.md +92 -146
- package/content/references/base/filter-system/index.md +25 -13
- package/content/references/base/filter-system/json-filtering.md +45 -184
- package/content/references/base/filter-system/list-operators.md +23 -53
- package/content/references/base/filter-system/logical-operators.md +63 -121
- package/content/references/base/filter-system/null-operators.md +34 -104
- package/content/references/base/filter-system/pattern-matching.md +40 -55
- package/content/references/base/filter-system/quick-reference.md +86 -198
- package/content/references/base/filter-system/range-operators.md +18 -46
- package/content/references/base/filter-system/tips.md +6 -6
- package/content/references/base/filter-system/use-cases.md +33 -15
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +11 -10
- package/content/references/base/models-reference.md +17 -17
- package/content/references/base/models.md +4 -3
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +224 -326
- package/content/references/base/repositories/index.md +19 -6
- package/content/references/base/repositories/mixins.md +5 -5
- package/content/references/base/repositories/relations.md +160 -293
- package/content/references/base/repositories/soft-deletable.md +16 -6
- package/content/references/base/secrets.md +17 -13
- package/content/references/base/services.md +6 -4
- package/content/references/configuration/environment-variables.md +31 -23
- package/content/references/configuration/index.md +6 -4
- package/content/references/index.md +1 -1
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +5 -1
- package/content/references/utilities/jsx-reference.md +11 -11
- package/content/references/utilities/jsx.md +2 -2
- package/content/references/utilities/module.md +78 -25
- package/content/references/utilities/request.md +2 -1
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +2 -2
- package/content/references/utilities/statuses-reference.md +4 -4
- package/content/references/utilities/statuses.md +5 -5
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +17 -16
|
@@ -10,10 +10,30 @@ Every option, binding key, class, and method the Authorization component exposes
|
|
|
10
10
|
|
|
11
11
|
**Files:**
|
|
12
12
|
|
|
13
|
-
- [`packages/core/src/components/auth/authorize/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize) - component, providers, enforcers, adapters, models, middleware
|
|
14
|
-
- [`packages/core/src/components/auth/base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts) - `AbstractAuthRegistry` (shared with Authentication)
|
|
15
|
-
- [`packages/core/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts) - `@model` populating `AUTHORIZATION_SUBJECT`
|
|
16
|
-
- [`packages/core/src/helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts) - `MetadataRegistry` authorize-settings queries
|
|
13
|
+
- [`packages/core-server/src/components/auth/authorize/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize) - component, providers, enforcers, adapters, models, middleware
|
|
14
|
+
- [`packages/core-server/src/components/auth/base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/base/abstract-auth-registry.ts) - `AbstractAuthRegistry` (shared with Authentication)
|
|
15
|
+
- [`packages/core-server/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/metadata/persistents.ts) - `@model` populating `AUTHORIZATION_SUBJECT`
|
|
16
|
+
- [`packages/core-server/src/helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/helpers/inversion/mixins/model.mixin.ts) - `MetadataRegistry` authorize-settings queries
|
|
17
|
+
|
|
18
|
+
## Find what you need
|
|
19
|
+
|
|
20
|
+
| You want to | Go to |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Bind global options and per-enforcer options | [Binding keys](#binding-keys) |
|
|
23
|
+
| Configure `AuthorizeComponent` at startup | [IAuthorizeOptions](#iauthorizeoptions) |
|
|
24
|
+
| Configure the Casbin enforcer (model, cache, pool) | [ICasbinEnforcerOptions](#icasbinenforceroptions) |
|
|
25
|
+
| Write a route's `authorize` spec | [IAuthorizationSpec (route-level)](#iauthorizationspec-route-level) |
|
|
26
|
+
| Resolve a request's domain scope | [IAuthorizationDomainSource / TAuthorizationDomainResolver](#iauthorizationdomainsource-tauthorizationdomainresolver) |
|
|
27
|
+
| Look up an action, decision, or role constant | [Constants](#constants) |
|
|
28
|
+
| Read the scoped RBAC `.conf` model | [CASBIN_RBAC_DOMAIN_SCOPED_MODEL](#casbin_rbac_domain_scoped_model) |
|
|
29
|
+
| Register or resolve enforcers | [AuthorizationEnforcerRegistry](#authorizationenforcerregistry) |
|
|
30
|
+
| Understand how the Casbin enforcer builds and evaluates rules | [CasbinAuthorizationEnforcer](#casbinauthorizationenforcer) |
|
|
31
|
+
| Use the ready-made Postgres adapter | [ScopedCasbinAdapter](#scopedcasbinadapter) |
|
|
32
|
+
| Write a custom adapter | [BaseFilteredAdapter](#basefilteredadapter) |
|
|
33
|
+
| Grant a subset of a subject's operations | [Subset grants (custom rows)](#subset-grants-custom-rows) and [GrantBuilder.planGrant](#grantbuilderplangrant) |
|
|
34
|
+
| Seed `PolicyDefinition` / `Permission` rows | [Policy and permission builders](#policy-and-permission-builders) |
|
|
35
|
+
| Wire `authorize` into a REST or gRPC controller | [Controller integration](#controller-integration) |
|
|
36
|
+
| Read the context keys the middleware sets | [Context variables](#context-variables) |
|
|
17
37
|
|
|
18
38
|
## Import paths
|
|
19
39
|
|
|
@@ -29,7 +49,7 @@ import {
|
|
|
29
49
|
CasbinAuthorizationEnforcer,
|
|
30
50
|
|
|
31
51
|
// Adapters
|
|
32
|
-
BaseFilteredAdapter, ScopedCasbinAdapter,
|
|
52
|
+
BaseFilteredAdapter, ScopedCasbinAdapter, PrincipalPolicyEdges,
|
|
33
53
|
|
|
34
54
|
// Scoped RBAC model
|
|
35
55
|
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
@@ -37,11 +57,9 @@ import {
|
|
|
37
57
|
// Models
|
|
38
58
|
AuthorizationRole,
|
|
39
59
|
|
|
40
|
-
// Policy / permission catalog builders
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Resource-hierarchy matcher (register on a custom Casbin model)
|
|
44
|
-
objectMatch,
|
|
60
|
+
// Policy / permission catalog builders - AuthorizationPermissionBuilder.objectMatch is the
|
|
61
|
+
// resource-hierarchy matcher (register on a custom Casbin model)
|
|
62
|
+
AuthorizationPolicyBuilder, AuthorizationPermissionBuilder, GrantBuilder,
|
|
45
63
|
|
|
46
64
|
// Constants
|
|
47
65
|
Authorization, AuthorizationActions, AuthorizationDecisions, AuthorizationDomainScopes,
|
|
@@ -115,7 +133,8 @@ flowchart TD
|
|
|
115
133
|
Voters -->|DENY| E403a[/403 Denied by voter/]
|
|
116
134
|
Voters -->|ALLOW| Next4([next - voter allow])
|
|
117
135
|
Voters -->|ABSTAIN / none| HasEnforcers{Enforcers registered?}
|
|
118
|
-
HasEnforcers -->|No| Next6([next -
|
|
136
|
+
HasEnforcers -->|No, defaultDecision: allow| Next6([next - allow, warning logged])
|
|
137
|
+
HasEnforcers -->|No, defaultDecision: deny or unset| E403c[/403 no enforcer registered/]
|
|
119
138
|
HasEnforcers -->|Yes| Resolve[Resolve enforcer by name]
|
|
120
139
|
Resolve --> ResolveDomain["Resolve domain (if spec.domain or domainResolver)"]
|
|
121
140
|
ResolveDomain --> Cache{Rules cached?}
|
|
@@ -201,13 +220,12 @@ components/auth/authorize/
|
|
|
201
220
|
│ ├── base-filtered.ts # BaseFilteredAdapter (thin abstract) + ICasbinPolicyFilter
|
|
202
221
|
│ ├── scoped-casbin.adapter.ts # ScopedCasbinAdapter (generic edge-table reader)
|
|
203
222
|
│ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable, ICasbinPolicySource
|
|
223
|
+
├── builders/
|
|
224
|
+
│ ├── grant.builder.ts # GrantBuilder
|
|
225
|
+
│ ├── permission.builder.ts # AuthorizationPermissionBuilder + static objectMatch
|
|
226
|
+
│ └── policy.builder.ts # AuthorizationPolicyBuilder
|
|
204
227
|
├── common/
|
|
205
|
-
│ ├── constants.ts # Authorization, Actions, Decisions, PolicyVariants, Roles, ...
|
|
206
|
-
│ ├── keys.ts # AuthorizeBindingKeys
|
|
207
|
-
│ ├── object-match.ts # objectMatch resource-hierarchy matcher
|
|
208
|
-
│ ├── permission-builder.ts # AuthorizationPermissionBuilder
|
|
209
|
-
│ ├── policy-builder.ts # AuthorizationPolicyBuilder
|
|
210
|
-
│ ├── resolve-request-domain.ts # resolveRequestDomain
|
|
228
|
+
│ ├── constants.ts # Authorization, Actions, Decisions, PolicyVariants, Roles, AuthorizeBindingKeys, ...
|
|
211
229
|
│ └── types.ts # IAuthorizeOptions, IAuthorizationEnforcer, ICasbinEnforcerOptions, ...
|
|
212
230
|
├── enforcers/
|
|
213
231
|
│ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
|
|
@@ -215,7 +233,9 @@ components/auth/authorize/
|
|
|
215
233
|
│ └── models/rbac-domain.model.ts # CASBIN_RBAC_DOMAIN_SCOPED_MODEL
|
|
216
234
|
├── middlewares/authorize.middleware.ts # authorize() standalone function
|
|
217
235
|
├── models/authorization-role.model.ts # AuthorizationRole
|
|
218
|
-
├── providers/
|
|
236
|
+
├── providers/
|
|
237
|
+
│ ├── authorization.provider.ts # AuthorizationProvider
|
|
238
|
+
│ └── request-domain.ts # resolveRequestDomain, readDeclarative
|
|
219
239
|
└── component.ts # AuthorizeComponent
|
|
220
240
|
```
|
|
221
241
|
|
|
@@ -230,7 +250,7 @@ components/auth/authorize/
|
|
|
230
250
|
| Rules caching | Built rules cached on the Hono context per-request - avoids rebuilding for multi-spec routes |
|
|
231
251
|
| Registry singleton | Mirrors `AuthenticationStrategyRegistry` - shares `AbstractAuthRegistry<T>` |
|
|
232
252
|
| Filtered adapter pattern | `BaseFilteredAdapter` is a thin read-only base; subclasses implement only `loadFilteredPolicy` |
|
|
233
|
-
| No-enforcer fallback | No enforcers registered -> the middleware
|
|
253
|
+
| No-enforcer fallback | No enforcers registered -> the middleware honors `defaultDecision`: `deny` (default) throws a named 403, `allow` proceeds and logs a warning |
|
|
234
254
|
| Single edge table (scoped model) | `ScopedCasbinAdapter` reads one `PolicyDefinition` table for every edge type - no per-relation tables |
|
|
235
255
|
|
|
236
256
|
## AuthorizeComponent
|
|
@@ -253,7 +273,7 @@ class AuthorizeComponent extends BaseComponent {
|
|
|
253
273
|
> [!NOTE]
|
|
254
274
|
> Enforcer registration is separate - `AuthorizeComponent` only validates global options. Register enforcers via `AuthorizationEnforcerRegistry.register()`.
|
|
255
275
|
|
|
256
|
-
Source -> [`component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/component.ts)
|
|
276
|
+
Source -> [`component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/component.ts)
|
|
257
277
|
|
|
258
278
|
## Binding keys
|
|
259
279
|
|
|
@@ -275,7 +295,7 @@ class AuthorizeBindingKeys {
|
|
|
275
295
|
|
|
276
296
|
`AuthorizeBindingKeys.enforcerOptions(name)` is called automatically by `AuthorizationEnforcerRegistry.register()` when `options` is provided; `CasbinAuthorizationEnforcer` injects its options from `AuthorizeBindingKeys.enforcerOptions('casbin')`.
|
|
277
297
|
|
|
278
|
-
Source -> [`common/
|
|
298
|
+
Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/constants.ts)
|
|
279
299
|
|
|
280
300
|
## Option interfaces
|
|
281
301
|
|
|
@@ -306,7 +326,7 @@ Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistr
|
|
|
306
326
|
| `model` | `{ driver: 'file', definition } \| { driver: 'text', definition }` | - | **Required.** Casbin model (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
|
|
307
327
|
| `cached` | `{ use: false } \| (ICasbinEnforcerCachedRedis & { use: true })` | - | **Required.** Caching configuration (Redis-only) |
|
|
308
328
|
| `adapter` | `Adapter` | - | Casbin adapter instance (e.g. `ScopedCasbinAdapter`) |
|
|
309
|
-
| `isScoped` | `boolean` | `false` | Enables the scoped model: 4-token `(sub, dom, obj, act)` requests
|
|
329
|
+
| `isScoped` | `boolean` | `false` | Enables the scoped model: 4-token `(sub, dom, obj, act)` requests. Auto-registers `keyMatch`, `objectMatch`, and `ResourceRoleManager` - see [`CasbinAuthorizationEnforcer`](#casbinauthorizationenforcer) |
|
|
310
330
|
| `poolSize` | `number` | `16` | Pooled enforcers (each request enforces on its own borrowed instance) |
|
|
311
331
|
| `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
|
|
312
332
|
| `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | - | Custom (non-scoped) payload normalizer, run before evaluation |
|
|
@@ -330,6 +350,15 @@ interface ICasbinEnforcerOptions<E extends Env = Env, TAction = string, TResourc
|
|
|
330
350
|
> [!NOTE]
|
|
331
351
|
> `cached.options.expiresIn` must be `>= 10_000` ms (`MIN_EXPIRES_IN`). Caching is **Redis-only** - the in-memory driver was removed.
|
|
332
352
|
|
|
353
|
+
### Domain hierarchy edges (`g3`)
|
|
354
|
+
|
|
355
|
+
There is no enforcer-level option for domain hierarchy - a role assignment (`g`), a grant (`g3`), or a domain membership (`g2`) declared at a parent domain reaching its children is driven entirely by `g3` policy lines already present in a principal's own line set, whenever `isScoped: true`. Two sources produce those lines - see `resolveDomainEdges` on [`ScopedCasbinAdapter`](#scopedcasbinadapter) below for the second one:
|
|
356
|
+
|
|
357
|
+
- `domain_inherits` rows reachable from the principal's domain closure (`ScopedCasbinAdapter`'s `DOMAIN_EDGE` branch, always on).
|
|
358
|
+
- `ScopedCasbinAdapter`'s `resolveDomainEdges` constructor hook, for a hierarchy the app already owns on a business table.
|
|
359
|
+
|
|
360
|
+
`registerMatchers()` (see [`configure()`](#configure) below) wires this unconditionally for every scoped model, sharing one overlay across the three role managers. Freshness is whatever the per-user policy-line cache already guarantees; there is no separate TTL or invalidation call to reason about.
|
|
361
|
+
|
|
333
362
|
**Cache configuration (discriminated union):**
|
|
334
363
|
|
|
335
364
|
```typescript
|
|
@@ -381,7 +410,9 @@ type TAuthorizationDomainResolver<E extends Env = Env> = (opts: {
|
|
|
381
410
|
}) => ValueOrPromise<TNullable<{ type: string; id: IdType }>>;
|
|
382
411
|
```
|
|
383
412
|
|
|
384
|
-
`resolveRequestDomain()` (`
|
|
413
|
+
`resolveRequestDomain()` (`providers/request-domain.ts`) turns either shape into a casbin domain string. Precedence: `spec.domain` (resolver, then declarative `readDeclarative()`) -> `IAuthorizeOptions.domainResolver` -> `AuthorizationDomainScopes.SYSTEM_WIDE`.
|
|
414
|
+
|
|
415
|
+
`readDeclarative()` reads `context.req.param/header/query()` for `'param'|'header'|'query'`, or `context.get(key)` for `'context'`.
|
|
385
416
|
|
|
386
417
|
### TAuthorizationConditions / TAuthorizationVoter / TAuthorizeFn
|
|
387
418
|
|
|
@@ -413,7 +444,7 @@ interface IAuthorizationRequest<TAction = string, TResource = string> {
|
|
|
413
444
|
}
|
|
414
445
|
```
|
|
415
446
|
|
|
416
|
-
Source -> [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/types.ts)
|
|
447
|
+
Source -> [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/types.ts)
|
|
417
448
|
|
|
418
449
|
## Constants
|
|
419
450
|
|
|
@@ -428,13 +459,15 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
|
|
|
428
459
|
| `Authorization.ENFORCER` | `'authorization.enforcer'` | Binding key prefix for enforcers |
|
|
429
460
|
| `Authorization.DOMAIN` | `'authorization.domain'` | Context key for the resolved request domain scope |
|
|
430
461
|
|
|
431
|
-
**`AuthorizationActions`** - `CREATE` `UPDATE` `DELETE` `EXECUTE` `READ` `WRITE` `MANAGE`. `AuthorizationActions.LATTICE` declares the standard action hierarchy consumed by `AuthorizationPolicyBuilder.actionLattice()`:
|
|
462
|
+
**`AuthorizationActions`** - `CREATE` `UPDATE` `DELETE` `EXECUTE` `READ` `WRITE` `MANAGE` `CUSTOM`. `AuthorizationActions.LATTICE` declares the standard action hierarchy consumed by `AuthorizationPolicyBuilder.actionLattice()`:
|
|
432
463
|
|
|
433
464
|
| `child` | `parent` |
|
|
434
465
|
|---|---|
|
|
435
466
|
| `READ`, `WRITE`, `EXECUTE` | `MANAGE` |
|
|
436
467
|
| `CREATE`, `UPDATE`, `DELETE` | `WRITE` |
|
|
437
468
|
|
|
469
|
+
`CUSTOM` (`'custom'`) is a grant-mode marker for a subset grant carrying `metadata.ops` - see [Subset grants](#subset-grants-custom-rows). It is deliberately absent from `LATTICE`: it names an encoding, not a position in the action hierarchy.
|
|
470
|
+
|
|
438
471
|
**`AuthorizationDecisions`** - `ALLOW` `DENY` `ABSTAIN`.
|
|
439
472
|
|
|
440
473
|
| Method | String check | Number check |
|
|
@@ -460,7 +493,13 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
|
|
|
460
493
|
| `REGEX_MATCH` | `'regexMatch'` | Treats the stored/policy value as a full regular expression |
|
|
461
494
|
|
|
462
495
|
> [!IMPORTANT]
|
|
463
|
-
> Applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side.
|
|
496
|
+
> Applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side.
|
|
497
|
+
>
|
|
498
|
+
> | Call | Result |
|
|
499
|
+
> |---|---|
|
|
500
|
+
> | `keyMatch("Merchant_X", "*")` | `true` |
|
|
501
|
+
> | `keyMatch("Merchant_X", "Merchant_X")` | `true` |
|
|
502
|
+
> | `keyMatch("Merchant_X", "Merchant_Y")` | `false` |
|
|
464
503
|
|
|
465
504
|
**`CasbinRuleVariants`** - the Casbin line prefixes declared by the scoped model, numbered in request-tuple order (`sub -> dom -> obj -> act`).
|
|
466
505
|
|
|
@@ -470,7 +509,7 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
|
|
|
470
509
|
| `G` | `'g'` | Role membership + role inheritance (the `sub` axis) |
|
|
471
510
|
| `G2` | `'g2'` | User -> domain membership (the `dom` axis) |
|
|
472
511
|
| `G3` | `'g3'` | Domain hierarchy (the `dom` axis) |
|
|
473
|
-
| `G4` | `'g4'` | Resource hierarchy (the `obj` axis,
|
|
512
|
+
| `G4` | `'g4'` | Resource hierarchy (the `obj` axis, served by `ResourceRoleManager`) |
|
|
474
513
|
| `G5` | `'g5'` | Action hierarchy (the `act` axis) |
|
|
475
514
|
|
|
476
515
|
**`AuthorizationPolicyVariants`** - the DB `variant` discriminator stored on each `PolicyDefinition` row (the kind of "edge"). Each entry carries `action` (the DB value) and `rule` (the Casbin prefix `ScopedCasbinAdapter` emits for it).
|
|
@@ -504,7 +543,7 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
|
|
|
504
543
|
| `GUEST` | `'001_guest'` | 1 |
|
|
505
544
|
| `UNKNOWN_USER` | `'000_unknown-user'` | 0 |
|
|
506
545
|
|
|
507
|
-
Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/constants.ts)
|
|
546
|
+
Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/constants.ts)
|
|
508
547
|
|
|
509
548
|
## CASBIN_RBAC_DOMAIN_SCOPED_MODEL
|
|
510
549
|
|
|
@@ -536,17 +575,23 @@ m = g(r.sub, p.sub, r.dom) && (p.dom == "SYSTEM_WIDE" || (p.dom == "ANY_MEMBER"
|
|
|
536
575
|
| `g` | `sub` | `assign_role` (user -> role) + `role_inherits` (role -> role), domain-aware. Registered with `keyMatch` so a `*` domain on a link matches any request domain |
|
|
537
576
|
| `g2` | `dom` (membership) | `join_domain` - powers the `ANY_MEMBER` grant scope |
|
|
538
577
|
| `g3` | `dom` (nesting) | `domain_inherits`, plus a self-link so an exact domain always matches itself |
|
|
539
|
-
| `g4` | `obj` | `resource_inherits` - explicit non-standard nesting edges;
|
|
578
|
+
| `g4` | `obj` | `resource_inherits` - explicit non-standard nesting edges; served by `ResourceRoleManager`, not a matching function |
|
|
540
579
|
| `g5` | `act` | `action_inherits`, plus a self-link |
|
|
541
580
|
|
|
542
|
-
**Effect** is casbin's `allow-and-deny` effector
|
|
581
|
+
**Effect** is casbin's `allow-and-deny` effector. A request needs a matching `allow` AND no matching `deny` - default-DENY. An explicit `deny` always overrides an `allow`. This is deliberately NOT casbin's `deny-override` effector (`!some(where (p.eft == deny))`), which would be default-ALLOW.
|
|
582
|
+
|
|
583
|
+
**Domain clause**, matched by `p.dom`:
|
|
543
584
|
|
|
544
|
-
|
|
585
|
+
| `p.dom` value | Matches |
|
|
586
|
+
|---|---|
|
|
587
|
+
| `SYSTEM_WIDE` | Every domain - bypasses membership, super-admin |
|
|
588
|
+
| `ANY_MEMBER` | Every domain the subject joined, via `g2` |
|
|
589
|
+
| `<Type>_<id>` | That domain, or a nested child via `g3` |
|
|
545
590
|
|
|
546
591
|
> [!NOTE]
|
|
547
592
|
> Relies on the default `DefaultRoleManager`'s self-link behavior (`hasLink(name, name) === true`) for `g3`/`g4`/`g5` - a custom role manager must preserve self-links.
|
|
548
593
|
|
|
549
|
-
Source -> [`enforcers/models/rbac-domain.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/models/rbac-domain.model.ts)
|
|
594
|
+
Source -> [`enforcers/models/rbac-domain.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/models/rbac-domain.model.ts)
|
|
550
595
|
|
|
551
596
|
## AbstractAuthRegistry
|
|
552
597
|
|
|
@@ -579,7 +624,7 @@ type TRegistryDescriptor<TItem> = { container: Container; targetClass: TClass<TI
|
|
|
579
624
|
|
|
580
625
|
`AuthorizationEnforcerRegistry.getBindingPrefix()` returns `Authorization.ENFORCER` (`'authorization.enforcer'`).
|
|
581
626
|
|
|
582
|
-
Source -> [`base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts)
|
|
627
|
+
Source -> [`base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/base/abstract-auth-registry.ts)
|
|
583
628
|
|
|
584
629
|
## AuthorizationEnforcerRegistry
|
|
585
630
|
|
|
@@ -614,7 +659,7 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
|
|
|
614
659
|
|--------|---------|-------------|
|
|
615
660
|
| `getInstance()` | `AuthorizationEnforcerRegistry` | Singleton instance (created on first call) |
|
|
616
661
|
| `register(opts)` | `this` | Registers enforcers with type-safe options (chainable) |
|
|
617
|
-
| `hasEnforcers()` | `boolean` | `descriptors.size > 0` - used by the middleware to
|
|
662
|
+
| `hasEnforcers()` | `boolean` | `descriptors.size > 0` - used by the middleware to decide whether to honor `defaultDecision` instead of resolving an enforcer |
|
|
618
663
|
| `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` |
|
|
619
664
|
| `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves + auto-configures once (`configuredEnforcers` Set) |
|
|
620
665
|
| `resolveOptions()` | `IAuthorizeOptions \| undefined` | Iterates all registered containers looking for `AuthorizeBindingKeys.OPTIONS` |
|
|
@@ -622,7 +667,12 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
|
|
|
622
667
|
| `rebuildUserCache(opts)` | `Promise<{ cacheKey, lineCount }>` | Drops then immediately re-extracts + re-caches |
|
|
623
668
|
| `reset()` | `void` | Clears descriptors AND `configuredEnforcers` |
|
|
624
669
|
|
|
625
|
-
**`register()` behavior:**
|
|
670
|
+
**`register()` behavior:**
|
|
671
|
+
|
|
672
|
+
- Validates no duplicate names within the call.
|
|
673
|
+
- Validates each name is not already registered.
|
|
674
|
+
- Binds each class as a singleton at `authorization.enforcer.{name}`.
|
|
675
|
+
- If `options` is given, binds it to `AuthorizeBindingKeys.enforcerOptions(name)`.
|
|
626
676
|
|
|
627
677
|
**Configure-once pattern:**
|
|
628
678
|
|
|
@@ -637,7 +687,7 @@ async resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer> {
|
|
|
637
687
|
}
|
|
638
688
|
```
|
|
639
689
|
|
|
640
|
-
Source -> [`enforcers/enforcer-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/enforcer-registry.ts)
|
|
690
|
+
Source -> [`enforcers/enforcer-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/enforcer-registry.ts)
|
|
641
691
|
|
|
642
692
|
## IAuthorizationEnforcer interface
|
|
643
693
|
|
|
@@ -675,7 +725,7 @@ interface IAuthorizationEnforcer<
|
|
|
675
725
|
|
|
676
726
|
## CasbinAuthorizationEnforcer
|
|
677
727
|
|
|
678
|
-
Wraps the `casbin` library (optional peer dependency).
|
|
728
|
+
Wraps the `casbin` library (optional peer dependency). The adapter only loads from the database on a throwaway enforcer, to build one user's policy lines (cached in Redis if configured). Every request then evaluates on its own enforcer, borrowed from a `BasePoolHelper<Enforcer>` and freshly loaded with those lines. This isolates concurrency and keeps the database out of the hot path.
|
|
679
729
|
|
|
680
730
|
```typescript
|
|
681
731
|
class CasbinAuthorizationEnforcer<E extends Env = Env, TAction extends string = string, TResource extends string = string>
|
|
@@ -723,16 +773,41 @@ Called once by the registry on first use:
|
|
|
723
773
|
5. Builds a `BasePoolHelper<Enforcer>` (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (no DB load at warmup), then `registerMatchers()` and `assertMatcherCompilesSync()` run on it.
|
|
724
774
|
6. `await pool.warmup()` - pre-creates the enforcers.
|
|
725
775
|
|
|
726
|
-
|
|
776
|
+
**`registerMatchers()`** - when `isScoped`, registers three things:
|
|
777
|
+
|
|
778
|
+
| Registers | On |
|
|
779
|
+
|---|---|
|
|
780
|
+
| `keyMatch` | Domain matching func on `g` |
|
|
781
|
+
| `objectMatch` | Matcher-expression function via `addFunction` - called directly in the model's matcher string, not as a relation's matching func |
|
|
782
|
+
| `ResourceRoleManager` | Named role manager for `g4` |
|
|
783
|
+
|
|
784
|
+
`g4` skips `addNamedMatchingFunc` on purpose. It sets casbin's `hasPattern`, which disables `DefaultRoleManager`'s fast path on every link check, not just `g4` lookups.
|
|
785
|
+
|
|
786
|
+
For every scoped model, three more role managers are wired unconditionally - not behind a separate option:
|
|
727
787
|
|
|
728
|
-
|
|
788
|
+
| Registers | On | Notes |
|
|
789
|
+
|---|---|---|
|
|
790
|
+
| `MembershipRoleManager` | `g2` | Joining a parent domain membership makes the request domain's ancestors match too |
|
|
791
|
+
| `DomainHierarchyRoleManager` | `g3` | Grant domain nesting, request-domain-first |
|
|
792
|
+
| `DomainHierarchyRoleManager` (`reversed: true`) | `g`, via casbin's own `DefaultRoleManager.addDomainHierarchy()` | Role-assignment domain, stored-domain-first - the opposite argument order from `g3` |
|
|
793
|
+
|
|
794
|
+
The `g3` instance, the reversed `g` instance and `MembershipRoleManager` on `g2` are handed the same overlay `Map<child, Set<parent>>`. Casbin's `buildRoleLinks()` feeds every `g3` policy line to the `g3` instance via `addLink`, which writes into that shared overlay - the reversed `g` instance and `MembershipRoleManager` only ever read it, since casbin never puts the `g`-axis manager in its own `rmMap` and so never calls `addLink` on it directly. See [Domain hierarchy edges (`g3`)](#domain-hierarchy-edges-g3) above for where those `g3` lines come from.
|
|
795
|
+
|
|
796
|
+
When `domainMatching` is set (flat model), `registerMatchers()` registers the chosen `Util.*Func` on the named role definition instead, and always finishes with `buildRoleLinks()`.
|
|
797
|
+
|
|
798
|
+
**`assertMatcherCompilesSync()`** is a boot-time smoke test. It forces casbin's lazy matcher compile with one dummy `enforceSync` call (4 args when scoped/`normalizePayloadFn`, else 3). A malformed matcher, an unregistered function, or an arity mismatch fails at warmup, not on the first real request.
|
|
729
799
|
|
|
730
800
|
### buildRules()
|
|
731
801
|
|
|
732
802
|
Returns `ICasbinRules = { user, lines }` - the user's complete Casbin policy lines.
|
|
733
803
|
|
|
734
|
-
|
|
735
|
-
|
|
804
|
+
| Function | Behavior |
|
|
805
|
+
|---|---|
|
|
806
|
+
| `extractUserLines(user)` | Builds a fresh, isolated enforcer *with the adapter* and calls `adapter.loadFilteredPolicy({ principal: { type, id } })` |
|
|
807
|
+
| `extractLinesFrom()` | Serializes every `p*`/`g*` rule the model declares back into lines, not just `p`/`g` - including the scoped model's `g2`-`g5` hierarchies |
|
|
808
|
+
| `fetchLinesWithRedisCache` | Returns cached lines on a hit (Redis owns expiry via `PX`); on a miss, dedups concurrent misses via `pendingLineFetches` (single-flight), extracts once, and writes the lines back to Redis |
|
|
809
|
+
|
|
810
|
+
A corrupt cache entry is logged and discarded, then refetched - never surfaced as a `500`.
|
|
736
811
|
|
|
737
812
|
### evaluate()
|
|
738
813
|
|
|
@@ -748,9 +823,9 @@ On any error inside `pool.use`, the pool **destroys** the borrowed enforcer (fai
|
|
|
748
823
|
|
|
749
824
|
### invalidateUserCache() / rebuildUserCache()
|
|
750
825
|
|
|
751
|
-
Redis-only
|
|
826
|
+
Redis-only - both throw if caching is disabled. `invalidateUserCache` deletes the user's shared Redis key; the next request rebuilds lazily. `rebuildUserCache` deletes, then immediately re-extracts (on a throwaway enforcer) and re-caches. The key is shared in Redis, so one call is correct across every instance.
|
|
752
827
|
|
|
753
|
-
Source -> [`enforcers/casbin.enforcer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/casbin.enforcer.ts)
|
|
828
|
+
Source -> [`enforcers/casbin.enforcer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/casbin.enforcer.ts)
|
|
754
829
|
|
|
755
830
|
## BaseFilteredAdapter
|
|
756
831
|
|
|
@@ -781,14 +856,19 @@ abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter> extends BaseHe
|
|
|
781
856
|
interface ICasbinPolicyFilter { principal: { type: string; id: IdType }; }
|
|
782
857
|
|
|
783
858
|
/** Minimal contract - NOT the framework's general IDataSource. Any Drizzle-backed datasource satisfies it. */
|
|
784
|
-
interface ICasbinPolicySource {
|
|
859
|
+
interface ICasbinPolicySource {
|
|
860
|
+
getConnector?(): TCasbinPolicyConnector; // preferred: lazily wires the driver on first read, survives pool rotation
|
|
861
|
+
connector?: TCasbinPolicyConnector; // back-compat: a pre-wired connector
|
|
862
|
+
}
|
|
785
863
|
type TCasbinPolicyConnector = PgDatabase<PgQueryResultHKT, Record<string, AnyType>>;
|
|
786
864
|
```
|
|
787
865
|
|
|
788
866
|
> [!NOTE]
|
|
789
|
-
>
|
|
867
|
+
> `ICasbinPolicySource` is a minimal local contract, not the framework's general `IDataSource` - `src/components/**` never imports `@/connectors/postgres` for this. The `connector` getter resolves `getConnector?.() ?? connector`. When a datasource exposes neither, it throws a clear `[BaseFilteredAdapter]` error - never a bare `TypeError`.
|
|
868
|
+
|
|
869
|
+
`query()` runs a raw `SQL` statement and normalizes the result to a row array. Drizzle's `execute()` shape differs per driver: node-postgres yields `{ rows }`, postgres-js yields the row list itself. Call `query()` rather than read `.rows` directly.
|
|
790
870
|
|
|
791
|
-
`
|
|
871
|
+
`loadLines()` is the other orchestration helper. Call it after assembling your own casbin lines:
|
|
792
872
|
|
|
793
873
|
```typescript
|
|
794
874
|
protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void> {
|
|
@@ -799,7 +879,7 @@ protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void
|
|
|
799
879
|
}
|
|
800
880
|
```
|
|
801
881
|
|
|
802
|
-
Source -> [`adapters/base-filtered.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/base-filtered.ts), [`adapters/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/types.ts)
|
|
882
|
+
Source -> [`adapters/base-filtered.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/base-filtered.ts), [`adapters/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/types.ts)
|
|
803
883
|
|
|
804
884
|
## ScopedCasbinAdapter
|
|
805
885
|
|
|
@@ -808,27 +888,76 @@ The generic, read-only `FilteredAdapter` for the scoped RBAC model. Reads **one
|
|
|
808
888
|
```typescript
|
|
809
889
|
class ScopedCasbinAdapter extends BaseFilteredAdapter<IScopedCasbinPolicyFilter> {
|
|
810
890
|
protected readonly entities: IScopedCasbinEntities;
|
|
811
|
-
|
|
891
|
+
protected readonly resolveDomainEdges?: TResolveDomainEdgesFn;
|
|
892
|
+
constructor(opts: {
|
|
893
|
+
dataSource: ICasbinPolicySource;
|
|
894
|
+
entities: IScopedCasbinEntities;
|
|
895
|
+
resolveDomainEdges?: TResolveDomainEdgesFn;
|
|
896
|
+
});
|
|
812
897
|
|
|
813
898
|
async loadFilteredPolicy(model: Model, filter: IScopedCasbinPolicyFilter): Promise<void>;
|
|
814
899
|
|
|
815
|
-
protected
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
protected
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
900
|
+
protected async queryPrincipalPolicies(opts: {
|
|
901
|
+
principal: { type: string; id: IdType };
|
|
902
|
+
}): Promise<TPrincipalPolicyRow[]>; // one statement, two recursive CTEs (role_closure, domain_closure) - direct edges + reachable role_inherits + role-closure grants + reachable domain_inherits
|
|
903
|
+
protected collectDirectRow(opts: {
|
|
904
|
+
row: TPrincipalPolicyRow;
|
|
905
|
+
principal: { type: string; id: IdType };
|
|
906
|
+
lines: string[];
|
|
907
|
+
directGrants: TGrantRow[];
|
|
908
|
+
}): void; // routes one 'direct' row to its g/g2 line, or into the direct-grant batch
|
|
909
|
+
protected async buildGrantLines(opts: { subjectType: string; rows: TGrantRow[] }): Promise<string[]>; // -> p, shared by direct and role-closure grants
|
|
910
|
+
protected async queryEdgePolicies(): Promise<string[]>; // -> g4 (resource_inherits) + g5 (action_inherits), the two code-fixed structural trees
|
|
824
911
|
}
|
|
825
912
|
```
|
|
826
913
|
|
|
914
|
+
```typescript
|
|
915
|
+
/** The `kind` discriminator values in `TPrincipalPolicyRow`, one per UNION ALL branch of `queryPrincipalPolicies`. */
|
|
916
|
+
class PrincipalPolicyEdges {
|
|
917
|
+
static readonly DIRECT = 'direct';
|
|
918
|
+
static readonly ROLE_EDGE = 'roleEdge';
|
|
919
|
+
static readonly ROLE_GRANT = 'roleGrant';
|
|
920
|
+
static readonly DOMAIN_EDGE = 'domainEdge';
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** A grant row as fetched, before it becomes casbin lines. Permission columns are null when the join misses. */
|
|
924
|
+
type TGrantRow = {
|
|
925
|
+
subjectId: IdType;
|
|
926
|
+
objectCode: TNullable<string>;
|
|
927
|
+
objectSubject: TNullable<string>;
|
|
928
|
+
objectMethod: TNullable<string>;
|
|
929
|
+
action: TNullable<string>;
|
|
930
|
+
effect: TNullable<string>;
|
|
931
|
+
domain: TNullable<string>;
|
|
932
|
+
metadata?: unknown;
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
/** A row from the single principal-policy statement; `kind` says which branch produced it. */
|
|
936
|
+
type TPrincipalPolicyRow = TGrantRow & {
|
|
937
|
+
kind: TConstValue<typeof PrincipalPolicyEdges>;
|
|
938
|
+
variant: string;
|
|
939
|
+
targetType: TNullable<string>;
|
|
940
|
+
targetId: IdType;
|
|
941
|
+
};
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
> [!NOTE]
|
|
945
|
+
> **There is no cache in the adapter.** Every `loadFilteredPolicy()` call re-runs both statements. If extraction cost becomes a measured problem, add the indexes below rather than a staleness window. The framework does not create these indexes. Your `PolicyDefinition` schema owns that, but the queries need them:
|
|
946
|
+
>
|
|
947
|
+
> | Index | Serves |
|
|
948
|
+
> |---|---|
|
|
949
|
+
> | `(variant, subject_type, subject_id)` | `queryPrincipalPolicies`' two CTE anchor terms (`role_closure`, `domain_closure`) and its `direct` branch |
|
|
950
|
+
> | `(variant, subject_id)` | `role_closure`'s recursive-term join and the role-grant branch |
|
|
951
|
+
> | `(variant, subject_type, subject_id)` again | `domain_closure`'s recursive term and the `domainEdge` branch. A domain node's identity is `(type, id)`, so both join on the pair, not `subject_id` alone |
|
|
952
|
+
> | `(variant)`, or per-variant partial indexes | `queryEdgePolicies`' two branches (`resource_inherits`, `action_inherits`) - each filters on `variant` alone |
|
|
953
|
+
>
|
|
954
|
+
> Without them, the recursive CTEs' anchor and direct-edge branches fall back to a sequential scan of the whole `PolicyDefinition` table. Measured with `EXPLAIN (ANALYZE, BUFFERS)` against a real Postgres database.
|
|
955
|
+
|
|
827
956
|
```typescript
|
|
828
957
|
interface IScopedCasbinTable { tableName: string; schemaName?: string; }
|
|
829
958
|
|
|
830
959
|
interface IScopedCasbinEntities {
|
|
831
|
-
policyDefinition: IScopedCasbinTable;
|
|
960
|
+
policyDefinition: IScopedCasbinTable & { metadata?: { columnName: string } }; // metadata.columnName opts into subset grants
|
|
832
961
|
permission: IScopedCasbinTable; // permission catalog (id, code, ...)
|
|
833
962
|
principals: { user: string; role: string }; // casbin name prefixes
|
|
834
963
|
domainTypes: string[]; // e.g. ['Merchant', 'Organizer']
|
|
@@ -838,14 +967,47 @@ interface IScopedCasbinEntities {
|
|
|
838
967
|
interface IScopedCasbinPolicyFilter { principal: { type: string; id: IdType }; }
|
|
839
968
|
```
|
|
840
969
|
|
|
841
|
-
**`loadFilteredPolicy()`
|
|
970
|
+
**`loadFilteredPolicy()` runs one wave of two independent statements:**
|
|
971
|
+
|
|
972
|
+
1. `Promise.all` of two statements, neither waiting on the other:
|
|
973
|
+
|
|
974
|
+
**`queryPrincipalPolicies`** covers everything scoped to the principal, tagged by `kind`:
|
|
975
|
+
|
|
976
|
+
| `kind` | Rows |
|
|
977
|
+
|---|---|
|
|
978
|
+
| `DIRECT` | The principal's own `assign_role` / `join_domain` / `grant` rows |
|
|
979
|
+
| `ROLE_EDGE` | `role_inherits` edges reachable from its roles |
|
|
980
|
+
| `ROLE_GRANT` | Grants of that role closure |
|
|
981
|
+
| `DOMAIN_EDGE` | `domain_inherits` edges reachable from its domains |
|
|
982
|
+
|
|
983
|
+
It resolves two `WITH RECURSIVE` CTEs in SQL. `role_closure` seeds from `assign_role` rows and walks `role_inherits`. `domain_closure` seeds from `join_domain` rows and walks `domain_inherits`. Each recursive term uses `UNION`, not `UNION ALL` - the de-duplication is what terminates a cyclic graph.
|
|
984
|
+
|
|
985
|
+
**`queryEdgePolicies`** covers the two code-fixed structural trees, `resource_inherits` (`g4`) and `action_inherits` (`g5`), merged into one statement with two `UNION ALL` branches. `domain_inherits` (`g3`) is not loaded here - see [why `g3` is scoped differently](#why-g3-is-scoped-and-g4-g5-are-not).
|
|
986
|
+
|
|
987
|
+
2. **Row routing:**
|
|
988
|
+
|
|
989
|
+
| `kind` | Routed to |
|
|
990
|
+
|---|---|
|
|
991
|
+
| `direct` | `collectDirectRow` - `g` for `assign_role`, `g2` for `join_domain`, or the direct-grant batch for `grant` |
|
|
992
|
+
| `roleEdge` | `g` lines, inline |
|
|
993
|
+
| `domainEdge` | `g3` lines, inline |
|
|
994
|
+
| `roleGrant` | Batched separately |
|
|
995
|
+
|
|
996
|
+
Both grant batches expand through the shared `buildGrantLines`.
|
|
997
|
+
|
|
998
|
+
3. All lines load via `loadLines`.
|
|
999
|
+
|
|
1000
|
+
Only reachable edges are emitted: `role_inherits` edges from the principal's roles, and `domain_inherits` edges from its domains - never the whole role/domain graph. An edge outside either closure could never be traversed by the matcher anyway. This is behavior-preserving, and it shrinks every user's payload.
|
|
842
1001
|
|
|
843
|
-
|
|
844
|
-
2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to collect the assigned roles plus all transitive parents.
|
|
845
|
-
3. **Wave 2:** fetches the grants (`p`) of every role in the closure, so a user inherits parent-role permissions.
|
|
846
|
-
4. All lines load via `loadLines`.
|
|
1002
|
+
### Why `g3` is scoped and `g4`/`g5` are not
|
|
847
1003
|
|
|
848
|
-
|
|
1004
|
+
`g4` (resource) and `g5` (action) are fixed by the codebase - a few hundred rows, constant regardless of tenant count. `queryEdgePolicies` loads them whole for every principal.
|
|
1005
|
+
|
|
1006
|
+
`g3` (domain) grows with the domain count - many merchants under few organizers. It is scoped to the principal's domain closure inside `queryPrincipalPolicies` instead.
|
|
1007
|
+
|
|
1008
|
+
**The permission join is a `LEFT JOIN`, not `INNER JOIN`.** A grant whose target does not resolve (missing or soft-deleted `Permission` row) is logged and skipped by `buildGrantLines`, not silently dropped from the result set.
|
|
1009
|
+
|
|
1010
|
+
All queries use the `sql` template tag from `drizzle-orm`. Tables are schema-qualified via `sql.identifier` (injection-safe); interpolated values are bound parameters. The soft-delete clause (`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true.
|
|
849
1011
|
|
|
850
1012
|
```typescript
|
|
851
1013
|
import { ScopedCasbinAdapter } from '@venizia/ignis';
|
|
@@ -862,23 +1024,86 @@ const adapter = new ScopedCasbinAdapter({
|
|
|
862
1024
|
});
|
|
863
1025
|
```
|
|
864
1026
|
|
|
865
|
-
Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
|
|
1027
|
+
Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
|
|
866
1028
|
|
|
867
|
-
|
|
1029
|
+
### `resolveDomainEdges` - `g3` edges from business data
|
|
868
1030
|
|
|
869
|
-
|
|
1031
|
+
A second, opt-in source of `g3` edges, alongside the `DOMAIN_EDGE` branch above. Configured on the constructor, for a tenant hierarchy the app already owns as a plain foreign key on a business table rather than `domain_inherits` rows:
|
|
870
1032
|
|
|
871
1033
|
```typescript
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1034
|
+
type TResolveDomainEdgesFn = (opts: {
|
|
1035
|
+
principal: { type: string; id: IdType };
|
|
1036
|
+
domains: string[];
|
|
1037
|
+
}) => Promise<Array<{ child: string; parent: string }>>;
|
|
1038
|
+
|
|
1039
|
+
new ScopedCasbinAdapter({
|
|
1040
|
+
dataSource,
|
|
1041
|
+
entities,
|
|
1042
|
+
resolveDomainEdges: async ({ principal, domains }) => [{ child, parent }, ...],
|
|
1043
|
+
});
|
|
877
1044
|
```
|
|
878
1045
|
|
|
879
|
-
`
|
|
1046
|
+
`domains` is the principal's own domain closure, reconstructed from rows `queryPrincipalPolicies` already fetched (the `join_domain` seed plus both ends of every `domainEdge` row) rather than a third query. The hook returns `{ child, parent }` pairs as already-formed `<Type>_<id>` tokens; `loadFilteredPolicy` turns each into a `g3, <child>, <parent>` line, the exact shape a real `domain_inherits` row produces - nothing downstream can tell which source produced a given edge. A hook edge duplicating a real `domain_inherits` row is harmless: `DomainHierarchyRoleManager.addLink` stores parents in a `Set`, so the duplicate `addLink` call is a no-op.
|
|
1047
|
+
|
|
1048
|
+
A throwing hook is caught, logged, and treated as no edges for that one load - the rows already gathered (direct grants, role assignments, table-sourced `g3` rows) still load normally. This is the fail-secure direction: a missing `g3` edge only narrows what `g`/`g2`/`g3` reach, it can never widen it. The hook cannot join `queryPrincipalPolicies`'s wave (it needs that query's rows to compute `domains`) but does not wait on the independent `queryEdgePolicies` either - both resolve concurrently once the closure is known.
|
|
1049
|
+
|
|
1050
|
+
### Subset grants (custom rows)
|
|
1051
|
+
|
|
1052
|
+
A grant row can express an arbitrary subset of a subject's operations instead of a full tier:
|
|
1053
|
+
|
|
1054
|
+
| Field | Value |
|
|
1055
|
+
|---|---|
|
|
1056
|
+
| `action` | `'custom'` |
|
|
1057
|
+
| Target | A subject-level resource node - `Permission.method` is `AuthorizationPermissionBuilder.RESOURCE_NODE_METHOD`, the `*` sentinel |
|
|
1058
|
+
| `metadata` | `{ ops: [...] }` |
|
|
1059
|
+
|
|
1060
|
+
`ops` holds **method names**, not full permission codes. The subject comes from the target node, so `ops: ['find']` against node `Order` resolves to `Order.find`.
|
|
1061
|
+
|
|
1062
|
+
`buildGrantLines` expands each custom row into one `p` line per operation, using that operation's catalogued action (never the `custom` sentinel). The emitted lines are byte-identical to what equivalent per-operation grant rows produce. Expansion runs one extra batched query (`queryOperationCatalog`) per extraction, and none when no custom rows are present.
|
|
880
1063
|
|
|
881
|
-
|
|
1064
|
+
Reading is **opt-in**: without `entities.policyDefinition.metadata.columnName` mapped, the adapter never selects the `metadata` column, and a custom row is logged and skipped.
|
|
1065
|
+
|
|
1066
|
+
**Rejection rules** (`rejectCustomRow`, checked in this order). Each produces one `error`-level log line naming the subject id and object code, so a skipped grant can be diagnosed from the log alone:
|
|
1067
|
+
|
|
1068
|
+
| Condition | Logged reason |
|
|
1069
|
+
|---|---|
|
|
1070
|
+
| `action = 'custom'` but `metadata.columnName` is not mapped | `metadata.columnName is not mapped, so metadata.ops cannot be read` |
|
|
1071
|
+
| `action = 'custom'` but `metadata.ops` is missing, empty, or not an array of non-empty strings | `metadata.ops is missing, empty, or not an array of non-empty strings` |
|
|
1072
|
+
| `metadata.ops` is present but `action` is not `'custom'` | `metadata.ops is present but action is not "custom", so the intent is ambiguous` |
|
|
1073
|
+
| The target's `Permission.method` is not the `*` resource-node sentinel | `the target must be a subject-level resource node` |
|
|
1074
|
+
|
|
1075
|
+
A row that passes all four checks can still drop an individual **unresolvable operation name** during expansion. `expandCustomGrants` logs it separately, naming the unknown operations. The row's other valid operations still expand and emit lines.
|
|
1076
|
+
|
|
1077
|
+
**Composing a grant:** use `planGrant` (below) rather than hand-building a custom row. It collapses an operation selection into tier grants wherever possible. What does not collapse falls back to a custom row, or a single per-operation row.
|
|
1078
|
+
|
|
1079
|
+
Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
|
|
1080
|
+
|
|
1081
|
+
## AuthorizationPermissionBuilder.objectMatch
|
|
1082
|
+
|
|
1083
|
+
Resource-hierarchy matcher registered by the scoped model **only** as a function - `objectMatch(r.obj, p.obj)`, called directly in the matcher expression via `addFunction`. It decides whether a requested resource falls under a granted one, without needing a stored edge for the standard case. Dotted nesting is derived from the code itself.
|
|
1084
|
+
|
|
1085
|
+
`objectMatch` is **not** registered as the `g4` matching func. `g4` (`resource_inherits`) is served by a dedicated `ResourceRoleManager` instead - see [ScopedCasbinAdapter](#scopedcasbinadapter). Registering it via `addNamedMatchingFunc` would set casbin's `hasPattern`. That disables `DefaultRoleManager`'s O(1) fast path on every link check, not only `g4` lookups.
|
|
1086
|
+
|
|
1087
|
+
It lives as a `static` method on `AuthorizationPermissionBuilder`, the class that owns the `code = <subject>.<method>` format it matches against. It must stay `static` with no `this` reference, since Casbin calls it by reference:
|
|
1088
|
+
|
|
1089
|
+
```typescript
|
|
1090
|
+
class AuthorizationPermissionBuilder {
|
|
1091
|
+
static objectMatch(requested: string, granted: string): boolean {
|
|
1092
|
+
if (granted === '*') return true;
|
|
1093
|
+
if (requested === granted) return true;
|
|
1094
|
+
return requested.startsWith(`${granted}.`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
enforcer.addFunction('objectMatch', AuthorizationPermissionBuilder.objectMatch);
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
| Call | Result | Why |
|
|
1102
|
+
|---|---|---|
|
|
1103
|
+
| `objectMatch('Activation.findById', 'Activation')` | `true` | Dotted nesting - endpoint under subject |
|
|
1104
|
+
| `objectMatch('OrderItem', 'Order')` | `false`, unless a `resource_inherits` (`g4`) edge links them | Non-standard nesting always needs an explicit edge |
|
|
1105
|
+
|
|
1106
|
+
Source -> [`builders/permission.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/permission.builder.ts)
|
|
882
1107
|
|
|
883
1108
|
## AuthorizationProvider
|
|
884
1109
|
|
|
@@ -918,8 +1143,11 @@ for (const voter of spec.voters ?? []) {
|
|
|
918
1143
|
// ABSTAIN -> next voter
|
|
919
1144
|
}
|
|
920
1145
|
|
|
921
|
-
// 5. Resolve enforcer (no-enforcer fallback)
|
|
922
|
-
if (!registry.hasEnforcers())
|
|
1146
|
+
// 5. Resolve enforcer (no-enforcer fallback honors defaultDecision - fails closed by default)
|
|
1147
|
+
if (!registry.hasEnforcers()) {
|
|
1148
|
+
if (options?.defaultDecision === 'allow') return next(); // logs a warning
|
|
1149
|
+
throw 403 'no enforcer registered'; // AuthorizationErrors.ENFORCER_NOT_REGISTERED
|
|
1150
|
+
}
|
|
923
1151
|
const enforcer = await registry.resolveEnforcer({ name: enforcerName ?? registry.getDefaultEnforcerName() });
|
|
924
1152
|
|
|
925
1153
|
// 5b. Resolve request domain - only when spec.domain or a global domainResolver is in play
|
|
@@ -949,7 +1177,7 @@ await next();
|
|
|
949
1177
|
roles.map(r => typeof r === 'string' ? r : (r.identifier ?? r.name ?? String(r.id ?? '')));
|
|
950
1178
|
```
|
|
951
1179
|
|
|
952
|
-
Source -> [`providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/providers/authorization.provider.ts)
|
|
1180
|
+
Source -> [`providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/providers/authorization.provider.ts)
|
|
953
1181
|
|
|
954
1182
|
## Standalone authorize() function
|
|
955
1183
|
|
|
@@ -962,7 +1190,7 @@ export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: strin
|
|
|
962
1190
|
|
|
963
1191
|
A module-level singleton `AuthorizationProvider`; the returned handler is a standard Hono `MiddlewareHandler`.
|
|
964
1192
|
|
|
965
|
-
Source -> [`middlewares/authorize.middleware.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/middlewares/authorize.middleware.ts)
|
|
1193
|
+
Source -> [`middlewares/authorize.middleware.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/middlewares/authorize.middleware.ts)
|
|
966
1194
|
|
|
967
1195
|
## AuthorizationRole
|
|
968
1196
|
|
|
@@ -988,7 +1216,7 @@ class AuthorizationRole implements IAuthorizationRole {
|
|
|
988
1216
|
interface IAuthorizationRole { readonly name: string; readonly priority: number; readonly identifier: string; }
|
|
989
1217
|
```
|
|
990
1218
|
|
|
991
|
-
Source -> [`models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/models/authorization-role.model.ts)
|
|
1219
|
+
Source -> [`models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/models/authorization-role.model.ts)
|
|
992
1220
|
|
|
993
1221
|
## Policy and permission builders
|
|
994
1222
|
|
|
@@ -1003,6 +1231,8 @@ class AuthorizationPolicyBuilder {
|
|
|
1003
1231
|
static readonly ACTION_PRINCIPAL = 'Action';
|
|
1004
1232
|
|
|
1005
1233
|
static grant(opts: { subject: { type; id }; permission: { type; id }; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
|
|
1234
|
+
/** A subset grant: subject -> resource node, granting only `ops` (method names). `action` is fixed to `AuthorizationActions.CUSTOM`; the adapter expands `ops` at read time (see Subset grants). */
|
|
1235
|
+
static customGrant(opts: { subject: { type; id }; permission: { type; id }; ops: string[]; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
|
|
1006
1236
|
static assignRole(opts: { user: { type; id }; role: { type; id }; domain?: TNullable<TPolicyDomainInput> }): PolicyDefinitionRow;
|
|
1007
1237
|
static joinDomain(opts: { user: { type; id }; domain: { type; id } }): PolicyDefinitionRow;
|
|
1008
1238
|
static roleInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
|
|
@@ -1026,7 +1256,7 @@ class AuthorizationPolicyBuilder {
|
|
|
1026
1256
|
|
|
1027
1257
|
### AuthorizationPermissionBuilder
|
|
1028
1258
|
|
|
1029
|
-
Builds `Permission` catalog rows (the `obj` axis the scoped matcher resolves). Generic over the name/description type, so i18n and plain-text apps both fit
|
|
1259
|
+
Builds `Permission` catalog rows (the `obj` axis the scoped matcher resolves). Generic over the name/description type, so i18n and plain-text apps both fit. The framework only owns the `code`/`method`/`action` shape.
|
|
1030
1260
|
|
|
1031
1261
|
```typescript
|
|
1032
1262
|
class AuthorizationPermissionBuilder {
|
|
@@ -1054,7 +1284,44 @@ class AuthorizationPermissionBuilder {
|
|
|
1054
1284
|
}
|
|
1055
1285
|
```
|
|
1056
1286
|
|
|
1057
|
-
Source -> [`
|
|
1287
|
+
Source -> [`builders/policy.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/policy.builder.ts), [`builders/permission.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/permission.builder.ts)
|
|
1288
|
+
|
|
1289
|
+
### GrantBuilder.planGrant
|
|
1290
|
+
|
|
1291
|
+
The supported way to compose an operations-subset grant. Pure - resolves against a caller-supplied catalog and returns rows for the caller to persist. It never touches the database. Call it on the singleton (`GrantBuilder.getInstance().planGrant(...)`).
|
|
1292
|
+
|
|
1293
|
+
```typescript
|
|
1294
|
+
type TGrantIntent = { tier: TAuthorizationAction } | { ops: string[] };
|
|
1295
|
+
|
|
1296
|
+
class GrantBuilder {
|
|
1297
|
+
planGrant(opts: {
|
|
1298
|
+
subject: { type: string; id: IdType };
|
|
1299
|
+
resource: { type: string; id: IdType; subject: string };
|
|
1300
|
+
intent: TGrantIntent;
|
|
1301
|
+
catalog: Array<{ subject: string; method: string; code: string; action: string }>;
|
|
1302
|
+
domain?: TNullable<TPolicyDomainInput>;
|
|
1303
|
+
effect?: TAuthorizationDecision; // default AuthorizationDecisions.ALLOW
|
|
1304
|
+
supportsCustomMetadata?: boolean; // default true - false forces per-operation rows instead of a custom row
|
|
1305
|
+
exact?: boolean; // default false - true skips collapsing into tier grants
|
|
1306
|
+
}): TPlannedGrantRow[];
|
|
1307
|
+
}
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
| `intent` | Behavior |
|
|
1311
|
+
|---|---|
|
|
1312
|
+
| `{ tier }` | One tier-grant row. Throws on a tier outside `read`/`write`/`execute`/`manage`. |
|
|
1313
|
+
| `{ ops }` collapsing into `manage` | Only when the subject has an operation in **each** of `read`, `write`, and `execute` - otherwise `manage` would silently pre-authorize a future operation in an empty tier. |
|
|
1314
|
+
| `{ ops }` collapsing into a narrow tier | Whenever the selection **completely covers** a `read`/`write`/`execute` tier, however few operations it holds. `ops: ['find']` collapses into `read` when `find` is the subject's only `read` operation. |
|
|
1315
|
+
| `{ ops }` with `exact: true` | Skips collapsing entirely - use for a selection that must never widen as new operations join a tier. |
|
|
1316
|
+
| Leftover after collapsing | One custom row (`AuthorizationPolicyBuilder.customGrant`) - or, when the leftover is a single operation or `supportsCustomMetadata: false`, one per-operation row per leftover operation. |
|
|
1317
|
+
|
|
1318
|
+
A per-operation row's `permission.id` is the operation's **code** (e.g. `Order.find`), not a database id. The planner is pure and has no DB access, so the caller resolves codes to ids when persisting.
|
|
1319
|
+
|
|
1320
|
+
Throws (`getError`) on an invalid tier, an empty `ops`, or an `ops` entry absent from the resource's catalog slice.
|
|
1321
|
+
|
|
1322
|
+
Mirrors `ScopedCasbinAdapter.buildGrantLines`'s expansion: a planned custom row and the equivalent per-operation rows expand to identical casbin lines - see [Subset grants](#subset-grants-custom-rows).
|
|
1323
|
+
|
|
1324
|
+
Source -> [`builders/grant.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/grant.builder.ts)
|
|
1058
1325
|
|
|
1059
1326
|
## Model-based authorization metadata
|
|
1060
1327
|
|
|
@@ -1092,7 +1359,7 @@ getAuthorizeModelSettings(opts: { format: 'array' }): Array<{ name: string; auth
|
|
|
1092
1359
|
getAuthorizeModelSettings(opts: { format: 'record' }): Record<string, { authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
|
|
1093
1360
|
```
|
|
1094
1361
|
|
|
1095
|
-
Source -> [`base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts), [`helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts)
|
|
1362
|
+
Source -> [`base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/metadata/persistents.ts), [`helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/helpers/inversion/mixins/model.mixin.ts)
|
|
1096
1363
|
|
|
1097
1364
|
## Controller integration
|
|
1098
1365
|
|
|
@@ -1167,7 +1434,7 @@ type TRouteAuthConfig = { authenticate?: TRouteAuthenticateConfig; authorize?: T
|
|
|
1167
1434
|
|
|
1168
1435
|
Applied identically to `count`, `find`, `findById`, `findOne`, `create`, `updateById`, `updateBy`, `deleteById`, `deleteBy`.
|
|
1169
1436
|
|
|
1170
|
-
Source -> [`base/controllers/rest/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/rest/abstract.ts), [`base/controllers/grpc/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/grpc/abstract.ts), [`base/controllers/factory/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/factory/definition.ts)
|
|
1437
|
+
Source -> [`base/controllers/rest/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/rest/abstract.ts), [`base/controllers/grpc/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/grpc/abstract.ts), [`base/controllers/factory/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/factory/definition.ts)
|
|
1171
1438
|
|
|
1172
1439
|
## Context variables
|
|
1173
1440
|
|
|
@@ -1180,7 +1447,7 @@ declare module 'hono' {
|
|
|
1180
1447
|
[Authentication.AUDIT_USER_ID]: IdType;
|
|
1181
1448
|
[Authentication.SKIP_AUTHENTICATION]: boolean;
|
|
1182
1449
|
|
|
1183
|
-
[Authorization.RULES]: unknown
|
|
1450
|
+
[Authorization.RULES]: Map<string, unknown>;
|
|
1184
1451
|
[Authorization.SKIP_AUTHORIZATION]: boolean;
|
|
1185
1452
|
[Authorization.DOMAIN]: string;
|
|
1186
1453
|
}
|