@venizia/ignis-docs 0.2.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/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +8 -4
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +247 -153
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- 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/models.md +1 -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 +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- 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 +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +58 -218
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- 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 +24 -23
|
@@ -1,437 +1,49 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Authorization
|
|
3
|
+
description: Enforcer-based RBAC/ABAC authorization with Casbin, a multi-tenant domain-scoped model, voters, and role shortcuts
|
|
4
|
+
difficulty: advanced
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Authorization
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
`AuthorizeComponent` decides whether an authenticated request is allowed to proceed. It evaluates role shortcuts, custom voters, and a Casbin RBAC enforcer, in that order, through the `authorize()` middleware. The middleware runs after authentication. Casbin's optional domain-scoped model adds multi-tenant grants on top.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
| **Package** | `@venizia/ignis` |
|
|
10
|
-
| **Class** | `AuthorizeComponent` |
|
|
11
|
-
| **Runtimes** | Both |
|
|
11
|
+
> [!TIP]
|
|
12
|
+
> New to this component? Start with [Getting Started](./getting-started) - it builds the mental model (a graph of edges, not a rule table), then seeds one grant and protects one route end to end before you read the reference material below.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
## In one example
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|-----------|---------|
|
|
17
|
-
| **AuthorizeComponent** | Main component validating authorization options and binding global config |
|
|
18
|
-
| **AuthorizationEnforcerRegistry** | Singleton managing registered enforcers (mirrors `AuthenticationStrategyRegistry`) |
|
|
19
|
-
| **CasbinAuthorizationEnforcer** | Casbin-backed enforcer (optional `casbin` peer dep) |
|
|
20
|
-
| **AuthorizationProvider** | IProvider producing the `authorize()` middleware factory |
|
|
21
|
-
| **authorize** | Standalone function wrapping `AuthorizationProvider.value()` |
|
|
22
|
-
| **AuthorizationRole** | Value object for role identity with priority-based comparison |
|
|
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 |
|
|
25
|
-
| **AbstractAuthRegistry** | Shared base class for authentication strategy registry and authorization enforcer registry |
|
|
26
|
-
|
|
27
|
-
### Authorization Flow (7 Steps)
|
|
28
|
-
|
|
29
|
-
```mermaid
|
|
30
|
-
flowchart TD
|
|
31
|
-
R([Request]) --> S1{"1. SKIP_AUTHORIZATION?"}
|
|
32
|
-
S1 -->|Yes| Pass1([next])
|
|
33
|
-
S1 -->|No| S2{"2. User on context?"}
|
|
34
|
-
S2 -->|No| E401[/401/]
|
|
35
|
-
S2 -->|Yes| S3{"3. Role shortcuts match?"}
|
|
36
|
-
S3 -->|alwaysAllowRoles| Pass2([next])
|
|
37
|
-
S3 -->|allowedRoles| Pass3([next])
|
|
38
|
-
S3 -->|No match| S4{"4. Voters?"}
|
|
39
|
-
S4 -->|DENY| E403a[/403/]
|
|
40
|
-
S4 -->|ALLOW| Pass4([next])
|
|
41
|
-
S4 -->|ABSTAIN / none| S5{"5. Enforcers registered?"}
|
|
42
|
-
S5 -->|No enforcers| Pass5([next - skip])
|
|
43
|
-
S5 -->|Yes| S5b["Resolve enforcer"]
|
|
44
|
-
S5b --> S6["6. Build/cache rules"]
|
|
45
|
-
S6 --> S7{"7. Evaluate"}
|
|
46
|
-
S7 -->|ALLOW| Pass6([next])
|
|
47
|
-
S7 -->|DENY/ABSTAIN| E403b[/403/]
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
| Step | Action | Short-circuits? |
|
|
51
|
-
|------|--------|-----------------|
|
|
52
|
-
| 1 | Check `Authorization.SKIP_AUTHORIZATION` flag | Yes -- skip all |
|
|
53
|
-
| 2 | Get authenticated user from context | Yes -- 401 if missing |
|
|
54
|
-
| 3 | Check role-based shortcuts (`alwaysAllowRoles` + `allowedRoles`) | Yes -- allow if matched |
|
|
55
|
-
| 4 | Execute `voters` (per-route) | Yes -- DENY/ALLOW short-circuits |
|
|
56
|
-
| 5 | Check if enforcers are registered; resolve enforcer (by name or default) | Yes -- skip all if no enforcers registered |
|
|
57
|
-
| 6 | Build or retrieve cached rules | No |
|
|
58
|
-
| 7 | Evaluate permission via enforcer | Yes -- 403 if denied |
|
|
59
|
-
|
|
60
|
-
> [!NOTE]
|
|
61
|
-
> Step 3 merges the global `alwaysAllowRoles` check and the per-route `allowedRoles` check into a single step. User roles are extracted once and checked against both lists.
|
|
62
|
-
|
|
63
|
-
> [!NOTE]
|
|
64
|
-
> Step 5 has a safety fallback: if no enforcers are registered in the `AuthorizationEnforcerRegistry`, the middleware skips authorization entirely and calls `next()`. This prevents hard failures when authorization is configured on routes but no enforcer has been registered yet.
|
|
65
|
-
|
|
66
|
-
### Authorization Constants
|
|
67
|
-
|
|
68
|
-
| Constant | Value | Description |
|
|
69
|
-
|----------|-------|-------------|
|
|
70
|
-
| `Authorization.RULES` | `'authorization.rules'` | Context key for cached rules |
|
|
71
|
-
| `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | Context key to dynamically skip authorization |
|
|
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) |
|
|
74
|
-
|
|
75
|
-
### Authorization Actions
|
|
76
|
-
|
|
77
|
-
| Constant | Value |
|
|
78
|
-
|----------|-------|
|
|
79
|
-
| `AuthorizationActions.CREATE` | `'create'` |
|
|
80
|
-
| `AuthorizationActions.READ` | `'read'` |
|
|
81
|
-
| `AuthorizationActions.UPDATE` | `'update'` |
|
|
82
|
-
| `AuthorizationActions.DELETE` | `'delete'` |
|
|
83
|
-
| `AuthorizationActions.EXECUTE` | `'execute'` |
|
|
84
|
-
|
|
85
|
-
`AuthorizationActions.SCHEME_SET` contains all valid actions. `AuthorizationActions.isValid(input)` checks membership.
|
|
86
|
-
|
|
87
|
-
### Authorization Decisions
|
|
88
|
-
|
|
89
|
-
| Constant | Value | Description |
|
|
90
|
-
|----------|-------|-------------|
|
|
91
|
-
| `AuthorizationDecisions.ALLOW` | `'allow'` | Grant access |
|
|
92
|
-
| `AuthorizationDecisions.DENY` | `'deny'` | Deny access |
|
|
93
|
-
| `AuthorizationDecisions.ABSTAIN` | `'abstain'` | No opinion -- fall through to next check |
|
|
94
|
-
|
|
95
|
-
`AuthorizationDecisions` also provides comparison helpers that accept both strings and numbers:
|
|
96
|
-
|
|
97
|
-
| Method | String check | Number check |
|
|
98
|
-
|--------|-------------|--------------|
|
|
99
|
-
| `isAllow(input)` | `input.toLowerCase() === 'allow'` | `input > 0` |
|
|
100
|
-
| `isDeny(input)` | `input.toLowerCase() === 'deny'` | `input < 0` |
|
|
101
|
-
| `isAbstain(input)` | `input.toLowerCase() === 'abstain'` | `input === 0` |
|
|
102
|
-
|
|
103
|
-
`AuthorizationDecisions.SCHEME_SET` contains all valid decisions. `AuthorizationDecisions.isValid(input)` checks membership.
|
|
104
|
-
|
|
105
|
-
### Authorization Enforcer Types
|
|
106
|
-
|
|
107
|
-
| Constant | Value | Description |
|
|
108
|
-
|----------|-------|-------------|
|
|
109
|
-
| `AuthorizationEnforcerTypes.CASBIN` | `'casbin'` | Casbin-backed enforcer |
|
|
110
|
-
| `AuthorizationEnforcerTypes.CUSTOM` | `'custom'` | Custom enforcer implementation |
|
|
111
|
-
|
|
112
|
-
`AuthorizationEnforcerTypes.SCHEME_SET` contains all valid types. `AuthorizationEnforcerTypes.isValid(input)` checks membership.
|
|
113
|
-
|
|
114
|
-
### Casbin Enforcer Model Drivers
|
|
115
|
-
|
|
116
|
-
| Constant | Value | Description |
|
|
117
|
-
|----------|-------|-------------|
|
|
118
|
-
| `CasbinEnforcerModelDrivers.FILE` | `'file'` | Load model from `.conf` file path |
|
|
119
|
-
| `CasbinEnforcerModelDrivers.TEXT` | `'text'` | Load model from inline string |
|
|
120
|
-
|
|
121
|
-
`CasbinEnforcerModelDrivers.SCHEME_SET` contains all valid drivers. `CasbinEnforcerModelDrivers.isValid(input)` checks membership.
|
|
122
|
-
|
|
123
|
-
### Casbin Enforcer Cached Drivers
|
|
124
|
-
|
|
125
|
-
| Constant | Value | Description |
|
|
126
|
-
|----------|-------|-------------|
|
|
127
|
-
| `CasbinEnforcerCachedDrivers.REDIS` | `'redis'` | Redis-backed per-user line cache with TTL (the only cache driver) |
|
|
128
|
-
|
|
129
|
-
`CasbinEnforcerCachedDrivers.SCHEME_SET` contains all valid drivers. `CasbinEnforcerCachedDrivers.isValid(input)` checks membership.
|
|
130
|
-
|
|
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`).
|
|
138
|
-
|
|
139
|
-
| Constant | Value | Description |
|
|
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 |
|
|
181
|
-
|
|
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) |
|
|
193
|
-
|
|
194
|
-
> [!NOTE]
|
|
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`).
|
|
196
|
-
|
|
197
|
-
### Built-in Roles
|
|
198
|
-
|
|
199
|
-
| Constant | Identifier | Priority | Description |
|
|
200
|
-
|----------|------------|----------|-------------|
|
|
201
|
-
| `AuthorizationRoles.SUPER_ADMIN` | `'999_super-admin'` | 999 | Highest privilege |
|
|
202
|
-
| `AuthorizationRoles.ADMIN` | `'900_admin'` | 900 | Administrator |
|
|
203
|
-
| `AuthorizationRoles.USER` | `'010_user'` | 10 | Regular user |
|
|
204
|
-
| `AuthorizationRoles.GUEST` | `'001_guest'` | 1 | Guest user |
|
|
205
|
-
| `AuthorizationRoles.UNKNOWN_USER` | `'000_unknown-user'` | 0 | Unauthenticated fallback |
|
|
206
|
-
|
|
207
|
-
Each built-in role is an `AuthorizationRole` instance. `AuthorizationRoles.SCHEME_SET` contains identifier strings. `AuthorizationRoles.isValid(input)` checks membership.
|
|
208
|
-
|
|
209
|
-
### Import Paths
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
// Classes & functions
|
|
213
|
-
import {
|
|
214
|
-
// Component & middleware
|
|
215
|
-
AuthorizeComponent,
|
|
216
|
-
AuthorizationProvider,
|
|
217
|
-
authorize,
|
|
218
|
-
|
|
219
|
-
// Registry
|
|
220
|
-
AuthorizationEnforcerRegistry,
|
|
221
|
-
|
|
222
|
-
// Enforcers
|
|
223
|
-
CasbinAuthorizationEnforcer,
|
|
224
|
-
|
|
225
|
-
// Adapters
|
|
226
|
-
BaseFilteredAdapter,
|
|
227
|
-
ScopedCasbinAdapter,
|
|
228
|
-
|
|
229
|
-
// Scoped RBAC model
|
|
230
|
-
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
231
|
-
|
|
232
|
-
// Models
|
|
233
|
-
AuthorizationRole,
|
|
234
|
-
|
|
235
|
-
// Constants
|
|
236
|
-
Authorization,
|
|
237
|
-
AuthorizationActions,
|
|
238
|
-
AuthorizationDecisions,
|
|
239
|
-
AuthorizationDomainScopes,
|
|
240
|
-
AuthorizationPolicyVariants,
|
|
241
|
-
AuthorizationRoles,
|
|
242
|
-
AuthorizationEnforcerTypes,
|
|
243
|
-
CasbinEnforcerModelDrivers,
|
|
244
|
-
CasbinEnforcerCachedDrivers,
|
|
245
|
-
CasbinRuleVariants,
|
|
246
|
-
CasbinDomainMatchingFunctions,
|
|
247
|
-
|
|
248
|
-
// Binding keys
|
|
249
|
-
AuthorizeBindingKeys,
|
|
250
|
-
} from '@venizia/ignis';
|
|
251
|
-
|
|
252
|
-
// Types & interfaces
|
|
253
|
-
import type {
|
|
254
|
-
// Core interfaces
|
|
255
|
-
IAuthorizeOptions,
|
|
256
|
-
IAuthorizationEnforcer,
|
|
257
|
-
IAuthorizationSpec,
|
|
258
|
-
IAuthorizationRequest,
|
|
259
|
-
IAuthorizationRole,
|
|
260
|
-
|
|
261
|
-
// Casbin options
|
|
262
|
-
ICasbinEnforcerOptions,
|
|
263
|
-
ICasbinEnforcerCachedRedis,
|
|
264
|
-
|
|
265
|
-
// Adapter types
|
|
266
|
-
ICasbinPolicyFilter,
|
|
267
|
-
ICasbinPolicySource,
|
|
268
|
-
IScopedCasbinEntities,
|
|
269
|
-
IScopedCasbinPolicyFilter,
|
|
270
|
-
|
|
271
|
-
// Function & utility types
|
|
272
|
-
TAuthorizeFn,
|
|
273
|
-
TAuthorizationVoter,
|
|
274
|
-
TAuthorizationConditions,
|
|
275
|
-
TRegistryDescriptor,
|
|
276
|
-
|
|
277
|
-
// Value types (from TConstValue)
|
|
278
|
-
TAuthorizationAction,
|
|
279
|
-
TAuthorizationDecision,
|
|
280
|
-
TAuthorizationEnforcerType,
|
|
281
|
-
TCasbinEnforcerCachedDriver,
|
|
282
|
-
TCasbinEnforcerModelDriver,
|
|
283
|
-
TCasbinRuleVariant,
|
|
284
|
-
TCasbinDomainMatchingFunction,
|
|
285
|
-
} from '@venizia/ignis';
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
## Setup
|
|
289
|
-
|
|
290
|
-
Authorization setup is a **three-step process**: bind global options, register the component, then register enforcers via the registry.
|
|
291
|
-
|
|
292
|
-
```mermaid
|
|
293
|
-
flowchart LR
|
|
294
|
-
subgraph Step1["Step 1"]
|
|
295
|
-
A["bind IAuthorizeOptions<br/>to AuthorizeBindingKeys.OPTIONS"]
|
|
296
|
-
end
|
|
297
|
-
subgraph Step2["Step 2"]
|
|
298
|
-
B["this.component(AuthorizeComponent)<br/>validates options + binds alwaysAllowRoles"]
|
|
299
|
-
end
|
|
300
|
-
subgraph Step3["Step 3"]
|
|
301
|
-
C["AuthorizationEnforcerRegistry.register()<br/>class + name + type + options"]
|
|
302
|
-
end
|
|
303
|
-
A --> B --> C
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
### Step 1: Bind Global Options
|
|
307
|
-
|
|
308
|
-
Bind `IAuthorizeOptions` to configure global authorization behavior. This interface is minimal -- it only contains global settings, not enforcer-specific configuration.
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
import {
|
|
312
|
-
AuthorizeBindingKeys,
|
|
313
|
-
AuthorizationDecisions,
|
|
314
|
-
IAuthorizeOptions,
|
|
315
|
-
} from '@venizia/ignis';
|
|
316
|
-
|
|
317
|
-
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
318
|
-
defaultDecision: AuthorizationDecisions.DENY,
|
|
319
|
-
alwaysAllowRoles: ['999_super-admin'],
|
|
320
|
-
});
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
### Step 2: Register the Component
|
|
324
|
-
|
|
325
|
-
```typescript
|
|
326
|
-
import { AuthorizeComponent } from '@venizia/ignis';
|
|
327
|
-
|
|
328
|
-
this.component(AuthorizeComponent);
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
The component validates that `IAuthorizeOptions` is bound and extracts `alwaysAllowRoles` into a separate binding (`AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES`) for downstream consumers.
|
|
332
|
-
|
|
333
|
-
### Step 3: Register Enforcers via Registry
|
|
334
|
-
|
|
335
|
-
Enforcer registration is separate from global options. Each enforcer is registered with its **class**, **name**, **type**, and **options** -- all co-located in one call.
|
|
336
|
-
|
|
337
|
-
#### Casbin Enforcer (Recommended)
|
|
338
|
-
|
|
339
|
-
```typescript
|
|
340
|
-
import {
|
|
341
|
-
AuthorizationEnforcerRegistry,
|
|
342
|
-
AuthorizationEnforcerTypes,
|
|
343
|
-
CasbinAuthorizationEnforcer,
|
|
344
|
-
CasbinEnforcerModelDrivers,
|
|
345
|
-
ScopedCasbinAdapter,
|
|
346
|
-
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
347
|
-
} from '@venizia/ignis';
|
|
348
|
-
|
|
349
|
-
// The generic scoped adapter - reads one principal's edges + the shared hierarchy from a single
|
|
350
|
-
// PolicyDefinition edge table. No subclassing; configure it with IScopedCasbinEntities.
|
|
351
|
-
const adapter = new ScopedCasbinAdapter({
|
|
352
|
-
dataSource,
|
|
353
|
-
entities: {
|
|
354
|
-
policyDefinition: { tableName: 'PolicyDefinition', schemaName: 'identity' },
|
|
355
|
-
permission: { tableName: 'Permission', schemaName: 'identity' },
|
|
356
|
-
principals: { user: 'User', role: 'Role' }, // casbin name prefixes
|
|
357
|
-
domainTypes: ['Merchant', 'Organizer'], // domain types you scope on
|
|
358
|
-
softDelete: { use: true, columnName: 'deleted_at' },
|
|
359
|
-
},
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
AuthorizationEnforcerRegistry.getInstance().register({
|
|
363
|
-
container: this,
|
|
364
|
-
enforcers: [{
|
|
365
|
-
enforcer: CasbinAuthorizationEnforcer,
|
|
366
|
-
name: 'casbin',
|
|
367
|
-
type: AuthorizationEnforcerTypes.CASBIN,
|
|
368
|
-
options: {
|
|
369
|
-
model: {
|
|
370
|
-
driver: CasbinEnforcerModelDrivers.TEXT,
|
|
371
|
-
definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
372
|
-
},
|
|
373
|
-
isScoped: true, // 4-token (sub, dom, obj, act); auto-registers keyMatch + objectMatch
|
|
374
|
-
adapter,
|
|
375
|
-
cached: {
|
|
376
|
-
use: true,
|
|
377
|
-
driver: 'redis',
|
|
378
|
-
options: {
|
|
379
|
-
connection: redisHelper,
|
|
380
|
-
expiresIn: 5 * 60 * 1000, // 5 minutes
|
|
381
|
-
keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
|
|
382
|
-
},
|
|
383
|
-
},
|
|
384
|
-
// poolSize / poolAcquireTimeoutMs are optional (defaults 16 / 5000ms).
|
|
385
|
-
// In scoped mode you do NOT pass domainMatching or normalizePayloadFn - the request domain
|
|
386
|
-
// is supplied by the provider's domain resolver (see "Domain scoping" in usage.md).
|
|
387
|
-
},
|
|
388
|
-
}],
|
|
389
|
-
});
|
|
390
|
-
```
|
|
391
|
-
|
|
392
|
-
#### Custom Enforcer
|
|
393
|
-
|
|
394
|
-
```typescript
|
|
395
|
-
AuthorizationEnforcerRegistry.getInstance().register({
|
|
396
|
-
container: this,
|
|
397
|
-
enforcers: [{
|
|
398
|
-
enforcer: MyCustomEnforcer,
|
|
399
|
-
name: 'my-custom',
|
|
400
|
-
type: AuthorizationEnforcerTypes.CUSTOM,
|
|
401
|
-
options: { /* your enforcer-specific options */ },
|
|
402
|
-
}],
|
|
403
|
-
});
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
### Full Setup Example
|
|
16
|
+
The recommended setup: scoped Casbin RBAC backed by one `PolicyDefinition` edge table, then a protected route.
|
|
407
17
|
|
|
408
18
|
```typescript
|
|
409
19
|
import {
|
|
410
|
-
AuthorizeComponent,
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
AuthorizationEnforcerTypes,
|
|
414
|
-
CasbinAuthorizationEnforcer,
|
|
415
|
-
CasbinEnforcerModelDrivers,
|
|
416
|
-
ScopedCasbinAdapter,
|
|
417
|
-
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
418
|
-
BaseApplication,
|
|
419
|
-
IAuthorizeOptions,
|
|
20
|
+
AuthorizeBindingKeys, AuthorizeComponent, AuthorizationDecisions, AuthorizationEnforcerRegistry,
|
|
21
|
+
AuthorizationEnforcerTypes, CasbinAuthorizationEnforcer, CasbinEnforcerModelDrivers,
|
|
22
|
+
ScopedCasbinAdapter, CASBIN_RBAC_DOMAIN_SCOPED_MODEL, BaseApplication, IAuthorizeOptions,
|
|
420
23
|
} from '@venizia/ignis';
|
|
421
24
|
|
|
422
25
|
export class Application extends BaseApplication {
|
|
423
|
-
|
|
424
|
-
//
|
|
26
|
+
preConfigure() {
|
|
27
|
+
// 1. Global options
|
|
425
28
|
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
426
|
-
defaultDecision:
|
|
29
|
+
defaultDecision: AuthorizationDecisions.DENY,
|
|
427
30
|
alwaysAllowRoles: ['999_super-admin'],
|
|
428
31
|
});
|
|
429
32
|
|
|
430
|
-
//
|
|
33
|
+
// 2. Component (validates options, binds alwaysAllowRoles)
|
|
431
34
|
this.component(AuthorizeComponent);
|
|
432
35
|
|
|
433
|
-
//
|
|
434
|
-
const adapter = new ScopedCasbinAdapter({
|
|
36
|
+
// 3. Enforcer, registered by class + name + type + co-located options
|
|
37
|
+
const adapter = new ScopedCasbinAdapter({
|
|
38
|
+
dataSource, // a BasePostgresDataSource instance
|
|
39
|
+
entities: {
|
|
40
|
+
policyDefinition: { tableName: 'PolicyDefinition', schemaName: 'identity' },
|
|
41
|
+
permission: { tableName: 'Permission', schemaName: 'identity' },
|
|
42
|
+
principals: { user: 'User', role: 'Role' },
|
|
43
|
+
domainTypes: ['Merchant'],
|
|
44
|
+
softDelete: { use: true, columnName: 'deleted_at' },
|
|
45
|
+
},
|
|
46
|
+
});
|
|
435
47
|
|
|
436
48
|
AuthorizationEnforcerRegistry.getInstance().register({
|
|
437
49
|
container: this,
|
|
@@ -440,10 +52,7 @@ export class Application extends BaseApplication {
|
|
|
440
52
|
name: 'casbin',
|
|
441
53
|
type: AuthorizationEnforcerTypes.CASBIN,
|
|
442
54
|
options: {
|
|
443
|
-
model: {
|
|
444
|
-
driver: CasbinEnforcerModelDrivers.TEXT,
|
|
445
|
-
definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
446
|
-
},
|
|
55
|
+
model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL },
|
|
447
56
|
isScoped: true,
|
|
448
57
|
adapter,
|
|
449
58
|
cached: { use: false },
|
|
@@ -454,409 +63,93 @@ export class Application extends BaseApplication {
|
|
|
454
63
|
}
|
|
455
64
|
```
|
|
456
65
|
|
|
457
|
-
> [!IMPORTANT]
|
|
458
|
-
> Authorization depends on authentication. Register `AuthenticateComponent` **before** `AuthorizeComponent` so that `Authentication.CURRENT_USER` is populated before authorization checks run.
|
|
459
|
-
|
|
460
|
-
> [!NOTE]
|
|
461
|
-
> Enforcer-specific options (model, adapter, cached, normalizePayloadFn) are co-located with the enforcer registration in `AuthorizationEnforcerRegistry.register()`, not inside `IAuthorizeOptions`. This keeps enforcer configuration next to the enforcer class.
|
|
462
|
-
|
|
463
|
-
## Configuration
|
|
464
|
-
|
|
465
|
-
### IAuthorizeOptions
|
|
466
|
-
|
|
467
|
-
Global authorization settings. Bound to the container before registering `AuthorizeComponent`.
|
|
468
|
-
|
|
469
|
-
| Option | Type | Default | Description |
|
|
470
|
-
|--------|------|---------|-------------|
|
|
471
|
-
| `defaultDecision` | `TAuthorizationDecision` | -- | **Required.** Decision when enforcer returns `ABSTAIN` (`'allow'`, `'deny'`, or `'abstain'`) |
|
|
472
|
-
| `alwaysAllowRoles` | `string[]` | `[]` | Roles that bypass all authorization checks (global) |
|
|
473
|
-
| `domainResolver` | `TAuthorizationDomainResolver` | -- | Fallback domain resolver used when a route's `spec.domain` is not set. Returns `{ type, id }` or `null` (→ `SYSTEM_WIDE`) |
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
interface IAuthorizeOptions {
|
|
477
|
-
defaultDecision: TAuthorizationDecision;
|
|
478
|
-
alwaysAllowRoles?: string[];
|
|
479
|
-
/** Fallback domain resolver used when a route's spec has no `domain`. */
|
|
480
|
-
domainResolver?: TAuthorizationDomainResolver;
|
|
481
|
-
}
|
|
482
|
-
```
|
|
483
|
-
|
|
484
|
-
### ICasbinEnforcerOptions
|
|
485
|
-
|
|
486
|
-
Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistry.register()`.
|
|
487
|
-
|
|
488
|
-
| Option | Type | Default | Description |
|
|
489
|
-
|--------|------|---------|-------------|
|
|
490
|
-
| `model` | `{ driver, definition }` | -- | **Required.** Casbin model definition (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
|
|
491
|
-
| `cached` | `{ use: false } \| { use: true, driver: 'redis', options }` | -- | **Required.** Caching configuration (Redis-only) |
|
|
492
|
-
| `adapter` | `Adapter` | -- | Casbin adapter instance (e.g., `ScopedCasbinAdapter`) |
|
|
493
|
-
| `isScoped` | `boolean` | `false` | Enable the scoped model: 4-token `(sub, dom, obj, act)` requests; auto-registers `keyMatch` on `g` + `objectMatch` on the resource relation |
|
|
494
|
-
| `poolSize` | `number` | `16` | Number of pooled enforcers (each request enforces on its own) |
|
|
495
|
-
| `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
|
|
496
|
-
| `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | -- | (Non-scoped/custom) normalize subject/resource/action before evaluation |
|
|
497
|
-
| `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 |
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
interface ICasbinEnforcerOptions<
|
|
501
|
-
E extends Env = Env,
|
|
502
|
-
TAction = string,
|
|
503
|
-
TResource = string,
|
|
504
|
-
TAdapter = Adapter,
|
|
505
|
-
> {
|
|
506
|
-
model:
|
|
507
|
-
| { driver: 'file'; definition: string }
|
|
508
|
-
| { driver: 'text'; definition: string };
|
|
509
|
-
|
|
510
|
-
cached:
|
|
511
|
-
| { use: false }
|
|
512
|
-
| (ICasbinEnforcerCachedRedis & { use: true });
|
|
513
|
-
|
|
514
|
-
adapter?: TAdapter;
|
|
515
|
-
|
|
516
|
-
// Enable the scoped RBAC model (4-token requests + auto-registered matchers).
|
|
517
|
-
isScoped?: boolean;
|
|
518
|
-
|
|
519
|
-
// Per-request enforcer pool (concurrency-safe; fail-closed on error).
|
|
520
|
-
poolSize?: number; // default 16
|
|
521
|
-
poolAcquireTimeoutMs?: number; // default 5000
|
|
522
|
-
|
|
523
|
-
normalizePayloadFn?(opts: {
|
|
524
|
-
user: IAuthUser;
|
|
525
|
-
action: TAction;
|
|
526
|
-
resource: TResource;
|
|
527
|
-
context: TContext<E, string>;
|
|
528
|
-
}): {
|
|
529
|
-
subject: string;
|
|
530
|
-
resource: string;
|
|
531
|
-
action: string;
|
|
532
|
-
domain?: string;
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
// Non-scoped only. Registers a Casbin domain matching function on the named role definition.
|
|
536
|
-
// When isScoped is true, the scoped model registers its own matchers - do not set this.
|
|
537
|
-
domainMatching?: {
|
|
538
|
-
roleDefinition: string; // e.g. 'g'
|
|
539
|
-
fn: TCasbinDomainMatchingFunction;
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
> [!NOTE]
|
|
545
|
-
> `cached.options.expiresIn` must be >= 10,000 ms (10 seconds). Values below this threshold cause a validation error (`MIN_EXPIRES_IN = 10_000`).
|
|
546
|
-
|
|
547
|
-
#### Cache Configuration Types
|
|
548
|
-
|
|
549
|
-
The `cached` field is a discriminated union. **Caching is Redis-only** (the in-memory driver was removed):
|
|
550
|
-
|
|
551
|
-
```typescript
|
|
552
|
-
// No caching - every request rebuilds the user's policy from the datasource.
|
|
553
|
-
interface { use: false }
|
|
554
|
-
|
|
555
|
-
// Redis cache (store/retrieve the user's policy lines from Redis, TTL via PX).
|
|
556
|
-
interface ICasbinEnforcerCachedRedis {
|
|
557
|
-
driver: 'redis';
|
|
558
|
-
options: {
|
|
559
|
-
connection: IRedisHelper;
|
|
560
|
-
expiresIn: number;
|
|
561
|
-
keyFn: (opts: { user: IAuthorizationUser }) => ValueOrPromise<string>;
|
|
562
|
-
};
|
|
563
|
-
}
|
|
564
|
-
```
|
|
565
|
-
|
|
566
|
-
### IAuthorizationSpec (Route-level)
|
|
567
|
-
|
|
568
|
-
| Option | Type | Default | Description |
|
|
569
|
-
|--------|------|---------|-------------|
|
|
570
|
-
| `action` | `TAction` | -- | **Required.** Action being performed (e.g., `'read'`, `'create'`) |
|
|
571
|
-
| `resource` | `TResource` | -- | **Required.** Resource being accessed (e.g., `'Article'`, `'User'`) |
|
|
572
|
-
| `conditions` | `TAuthorizationConditions` | -- | Key-value conditions for ABAC (strict equality) |
|
|
573
|
-
| `allowedRoles` | `string[]` | -- | Roles that bypass enforcer for this specific route |
|
|
574
|
-
| `voters` | `TAuthorizationVoter[]` | -- | Custom voter functions for this specific route |
|
|
575
|
-
|
|
576
|
-
```typescript
|
|
577
|
-
interface IAuthorizationSpec<E extends Env = Env, TAction = string, TResource = string> {
|
|
578
|
-
action: TAction;
|
|
579
|
-
resource: TResource;
|
|
580
|
-
conditions?: TAuthorizationConditions;
|
|
581
|
-
allowedRoles?: string[];
|
|
582
|
-
voters?: TAuthorizationVoter<E, TAction, TResource>[];
|
|
583
|
-
}
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
### TAuthorizationConditions
|
|
587
|
-
|
|
588
|
-
Key-value conditions for attribute-based access control. Values are compared with strict equality (`===`).
|
|
589
|
-
|
|
590
|
-
```typescript
|
|
591
|
-
type TAuthorizationConditions<
|
|
592
|
-
KeyType extends string | symbol = string | symbol,
|
|
593
|
-
ValueType = string | number | boolean | null,
|
|
594
|
-
> = Record<KeyType, ValueType>;
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
### TAuthorizationVoter
|
|
598
|
-
|
|
599
|
-
Function type for voter callbacks:
|
|
600
|
-
|
|
601
|
-
```typescript
|
|
602
|
-
type TAuthorizationVoter<
|
|
603
|
-
E extends Env = Env,
|
|
604
|
-
TAction = string,
|
|
605
|
-
TResource = string,
|
|
606
|
-
> = (opts: {
|
|
607
|
-
user: IAuthUser;
|
|
608
|
-
action: TAction;
|
|
609
|
-
resource: TResource;
|
|
610
|
-
context: TContext<E, string>;
|
|
611
|
-
}) => ValueOrPromise<TAuthorizationDecision>;
|
|
612
|
-
```
|
|
613
|
-
|
|
614
|
-
### TAuthorizeFn
|
|
615
|
-
|
|
616
|
-
Function type for the `authorize()` middleware factory:
|
|
617
|
-
|
|
618
|
-
```typescript
|
|
619
|
-
type TAuthorizeFn<E extends Env = Env, TAction = string, TResource = string> = (opts: {
|
|
620
|
-
spec: IAuthorizationSpec<E, TAction, TResource>;
|
|
621
|
-
enforcerName?: string;
|
|
622
|
-
}) => MiddlewareHandler;
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
## Binding Keys
|
|
626
|
-
|
|
627
|
-
| Key | Constant | Type | Description |
|
|
628
|
-
|-----|----------|------|-------------|
|
|
629
|
-
| `@app/authorize/options` | `AuthorizeBindingKeys.OPTIONS` | `IAuthorizeOptions` | Global authorization options |
|
|
630
|
-
| `@app/authorize/always-allow-roles` | `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` | `string[]` | Auto-bound by component if present in options |
|
|
631
|
-
| `@app/authorize/enforcers/{name}/options` | `AuthorizeBindingKeys.enforcerOptions(name)` | `ICasbinEnforcerOptions \| unknown` | Per-enforcer options, auto-bound by registry |
|
|
632
|
-
|
|
633
|
-
```typescript
|
|
634
|
-
class AuthorizeBindingKeys {
|
|
635
|
-
static readonly OPTIONS = '@app/authorize/options';
|
|
636
|
-
static readonly ALWAYS_ALLOW_ROLES = '@app/authorize/always-allow-roles';
|
|
637
|
-
|
|
638
|
-
static enforcerOptions(name: string): string {
|
|
639
|
-
return `@app/authorize/enforcers/${name}/options`;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
> [!NOTE]
|
|
645
|
-
> `AuthorizeBindingKeys.enforcerOptions(name)` is called automatically by `AuthorizationEnforcerRegistry.register()` when `options` is provided. The `CasbinAuthorizationEnforcer` injects its options from `AuthorizeBindingKeys.enforcerOptions('casbin')`.
|
|
646
|
-
|
|
647
|
-
## Context Variables
|
|
648
|
-
|
|
649
|
-
The authorization module extends Hono's `ContextVariableMap` for type-safe context access. The full augmentation is defined in `auth/context-variables.ts` and covers both authentication and authorization:
|
|
650
|
-
|
|
651
|
-
```typescript
|
|
652
|
-
declare module 'hono' {
|
|
653
|
-
interface ContextVariableMap {
|
|
654
|
-
// Authentication
|
|
655
|
-
[Authentication.CURRENT_USER]: IAuthUser;
|
|
656
|
-
[Authentication.AUDIT_USER_ID]: IdType;
|
|
657
|
-
[Authentication.SKIP_AUTHENTICATION]: boolean;
|
|
658
|
-
|
|
659
|
-
// Authorization
|
|
660
|
-
[Authorization.RULES]: unknown;
|
|
661
|
-
[Authorization.SKIP_AUTHORIZATION]: boolean;
|
|
662
|
-
[Authorization.DOMAIN]: string;
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
```
|
|
666
|
-
|
|
667
|
-
### Authorization-Specific Variables
|
|
668
|
-
|
|
669
|
-
| Key | Constant | Type | Description |
|
|
670
|
-
|-----|----------|------|-------------|
|
|
671
|
-
| `'authorization.rules'` | `Authorization.RULES` | `unknown` | Cached rules built by the enforcer. Type depends on enforcer implementation. |
|
|
672
|
-
| `'authorization.skip'` | `Authorization.SKIP_AUTHORIZATION` | `boolean` | Set to `true` to dynamically skip authorization for this request. |
|
|
673
|
-
| `'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. |
|
|
674
|
-
|
|
675
|
-
### Authentication Variables Used by Authorization
|
|
676
|
-
|
|
677
|
-
| Key | Constant | Type | Used for |
|
|
678
|
-
|-----|----------|------|----------|
|
|
679
|
-
| `'authentication.currentUser'` | `Authentication.CURRENT_USER` | `IAuthUser` | Read in step 2 to get authenticated user |
|
|
680
|
-
| `'authentication.auditUserId'` | `Authentication.AUDIT_USER_ID` | `IdType` | Available for audit logging |
|
|
681
|
-
|
|
682
|
-
### IAuthUser Interface
|
|
683
|
-
|
|
684
|
-
The `IAuthUser` interface (from the authenticate module) is the user object available during authorization:
|
|
685
|
-
|
|
686
|
-
```typescript
|
|
687
|
-
interface IAuthUser {
|
|
688
|
-
userId: IdType; // IdType = number | string | bigint
|
|
689
|
-
[extra: string | symbol]: any;
|
|
690
|
-
}
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
The authorization middleware accesses `user.roles` (for role extraction) and `user.principalType` (for enforcer-based evaluation) via the index signature.
|
|
694
|
-
|
|
695
|
-
### IJWTTokenPayload Interface
|
|
696
|
-
|
|
697
|
-
When using JWT authentication, the full token payload extends `IAuthUser`:
|
|
698
|
-
|
|
699
66
|
```typescript
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
67
|
+
// Inside a controller's binding() - a route protected by authentication + authorization
|
|
68
|
+
const DELETE_ARTICLE_CONFIG = {
|
|
69
|
+
path: '/articles/{id}',
|
|
70
|
+
method: 'delete',
|
|
71
|
+
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
72
|
+
authorize: { action: AuthorizationActions.DELETE, resource: 'Article' },
|
|
73
|
+
responses: jsonResponse({ description: 'Deleted article', schema: ArticleSchema }),
|
|
74
|
+
} as const;
|
|
709
75
|
```
|
|
710
76
|
|
|
711
|
-
|
|
712
|
-
> `IdType = number | string | bigint` is defined in `@/base/models/common/types`.
|
|
77
|
+
## How it works
|
|
713
78
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
flowchart LR
|
|
720
|
-
Req([Request]) --> Auth["1. authenticate()<br/>JWT / Basic"]
|
|
721
|
-
Auth --> Authz["2. authorize()<br/>enforcer-based"]
|
|
722
|
-
Authz --> Custom["3. Custom middleware"]
|
|
723
|
-
Custom --> Handler([Route Handler])
|
|
724
|
-
```
|
|
79
|
+
- **Enforcer-based and pluggable.** `authorize({ spec })` returns Hono middleware built by `AuthorizationProvider`, which resolves an `IAuthorizationEnforcer` from `AuthorizationEnforcerRegistry` by name (default: the first registered). Swap `CasbinAuthorizationEnforcer` for a custom class without touching route configs.
|
|
80
|
+
- **Runs after authentication.** The middleware reads `Authentication.CURRENT_USER` from the Hono context. `AuthenticateComponent` must run first, and the route needs an `authenticate` config alongside `authorize`.
|
|
81
|
+
- **No enforcers registered = deny, unless you opt into allow.** If `AuthorizationEnforcerRegistry.hasEnforcers()` is `false`, the middleware throws a 403 naming the missing enforcer. Set `defaultDecision: 'allow'` on `IAuthorizeOptions` to proceed instead - useful during incremental rollout - and the middleware logs a warning each time it does.
|
|
82
|
+
- **Casbin's scoped RBAC model is the recommended engine.** Combine `CASBIN_RBAC_DOMAIN_SCOPED_MODEL`, `isScoped: true`, and `ScopedCasbinAdapter` to read one principal's policy edges from a single `PolicyDefinition` table. See [RBAC with domains](./usage#rbac-with-domains-multi-tenant) for multi-tenant grant scoping.
|
|
83
|
+
- **Per-request enforcers, cached lines.** Each Casbin evaluation borrows an isolated enforcer from an internal pool, loads that user's policy lines into it, then evaluates. The datasource query runs only on a cache miss, or every time if `cached.use: false`.
|
|
725
84
|
|
|
726
|
-
|
|
727
|
-
2. Authorization middleware is injected second (from `authorize` config)
|
|
728
|
-
3. Custom middleware is injected last (from `middleware` config -- REST only)
|
|
85
|
+
**Pipeline (7 steps, short-circuits marked)**
|
|
729
86
|
|
|
730
|
-
|
|
87
|
+
| # | Step | Short-circuits when |
|
|
88
|
+
|---|------|----------------------|
|
|
89
|
+
| 1 | `Authorization.SKIP_AUTHORIZATION` check | Set -> `next()` |
|
|
90
|
+
| 2 | Read `Authentication.CURRENT_USER` | Missing -> 401 |
|
|
91
|
+
| 3 | Role shortcuts (`alwaysAllowRoles` + `allowedRoles`) | Match -> `next()` |
|
|
92
|
+
| 4 | Voters (per-route) | `ALLOW`/`DENY` -> `next()` / 403 |
|
|
93
|
+
| 5 | Resolve enforcer | None registered -> 403, or `next()` if `defaultDecision: 'allow'` |
|
|
94
|
+
| 6 | Build/cache rules (+ resolve domain, if any) | - |
|
|
95
|
+
| 7 | `enforcer.evaluate()` | `DENY`/`ABSTAIN`-as-deny -> 403 |
|
|
731
96
|
|
|
732
|
-
|
|
97
|
+
## Common tasks
|
|
733
98
|
|
|
734
|
-
|
|
99
|
+
**Secure a route.** Add `authorize: { action, resource }` next to `authenticate` in a route config or `@get`/`@post` decorator.
|
|
735
100
|
|
|
736
101
|
```typescript
|
|
737
|
-
|
|
738
|
-
buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
|
|
739
|
-
const { authenticate = {}, authorize, ...restConfig } = opts.configs;
|
|
740
|
-
const mws = [];
|
|
741
|
-
|
|
742
|
-
// 1. Authenticate middleware (first)
|
|
743
|
-
if (strategies.length > 0) {
|
|
744
|
-
mws.push(authenticateFn({ strategies, mode }));
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
// 2. Authorize middleware (second) - supports single or array
|
|
748
|
-
if (authorize) {
|
|
749
|
-
const specs = Array.isArray(authorize) ? authorize : [authorize];
|
|
750
|
-
for (const spec of specs) {
|
|
751
|
-
mws.push(authorizeFn({ spec }));
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// 3. Custom middleware (last)
|
|
756
|
-
if (restConfig.middleware) { ... }
|
|
757
|
-
|
|
758
|
-
return { restConfig, security, mws };
|
|
759
|
-
}
|
|
102
|
+
authorize: { action: AuthorizationActions.READ, resource: 'Article' }
|
|
760
103
|
```
|
|
761
104
|
|
|
762
|
-
|
|
105
|
+
**Bypass the enforcer for trusted roles.** Both skip straight to `next()` once a matching role is found.
|
|
763
106
|
|
|
764
|
-
|
|
107
|
+
| Option | Scope |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `alwaysAllowRoles` | Global, set in `IAuthorizeOptions` |
|
|
110
|
+
| `allowedRoles` | Per-route, set on the spec |
|
|
765
111
|
|
|
766
112
|
```typescript
|
|
767
|
-
|
|
768
|
-
buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
|
|
769
|
-
const { configs } = opts;
|
|
770
|
-
const mws = [];
|
|
771
|
-
|
|
772
|
-
// 1. Authenticate middleware
|
|
773
|
-
if (configs.authenticate) { ... }
|
|
774
|
-
|
|
775
|
-
// 2. Authorize middleware - same pattern as REST
|
|
776
|
-
if (configs.authorize) {
|
|
777
|
-
const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
|
|
778
|
-
for (const spec of specs) {
|
|
779
|
-
const authzMw = authorizeFn({ spec });
|
|
780
|
-
mws.push((context, next) => authzMw(context, next));
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
return mws;
|
|
785
|
-
}
|
|
113
|
+
authorize: { action: AuthorizationActions.DELETE, resource: 'Article', allowedRoles: [AuthorizationRoles.ADMIN.identifier] }
|
|
786
114
|
```
|
|
787
115
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
Extended route config that supports both authentication and authorization:
|
|
116
|
+
**Add custom logic before the enforcer.** A voter returns `ALLOW`/`DENY`/`ABSTAIN`; the first non-`ABSTAIN` decision wins.
|
|
791
117
|
|
|
792
118
|
```typescript
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
119
|
+
const ownerVoter: TAuthorizationVoter = async ({ user, context }) =>
|
|
120
|
+
(await articleService.findById({ id: context.req.param('id') }))?.authorId === user.userId
|
|
121
|
+
? AuthorizationDecisions.ALLOW
|
|
122
|
+
: AuthorizationDecisions.ABSTAIN;
|
|
797
123
|
```
|
|
798
124
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
### Per-Route Configuration in CRUD Factory
|
|
802
|
-
|
|
803
|
-
CRUD factory routes support both authentication and authorization configuration:
|
|
125
|
+
**Scope a grant to a tenant.** Declare where the domain comes from per route (or globally via `IAuthorizeOptions.domainResolver`).
|
|
804
126
|
|
|
805
127
|
```typescript
|
|
806
|
-
|
|
807
|
-
entity: Article,
|
|
808
|
-
repository: { name: 'ArticleRepository' },
|
|
809
|
-
controller: {
|
|
810
|
-
name: 'ArticleController',
|
|
811
|
-
basePath: '/articles',
|
|
812
|
-
},
|
|
813
|
-
authenticate: { strategies: [Authentication.STRATEGY_JWT], mode: AuthenticationModes.ANY },
|
|
814
|
-
authorize: { action: AuthorizationActions.READ, resource: 'Article' },
|
|
815
|
-
routes: {
|
|
816
|
-
// Skip both auth for public read
|
|
817
|
-
find: { authenticate: { skip: true } },
|
|
818
|
-
// Override authorization for delete
|
|
819
|
-
deleteById: {
|
|
820
|
-
authorize: { action: AuthorizationActions.DELETE, resource: 'Article' },
|
|
821
|
-
},
|
|
822
|
-
// Skip only authorization
|
|
823
|
-
count: { authorize: { skip: true } },
|
|
824
|
-
},
|
|
825
|
-
});
|
|
128
|
+
authorize: { action: 'read', resource: 'Order', domain: { from: 'param', key: 'merchantId', type: 'Merchant' } }
|
|
826
129
|
```
|
|
827
130
|
|
|
828
|
-
**
|
|
829
|
-
1. `authenticate: { skip: true }` -- skips both authentication AND authorization
|
|
830
|
-
2. `authorize: { skip: true }` -- skips only authorization (authentication still runs)
|
|
831
|
-
3. Per-route `authorize` overrides controller-level `authorize`
|
|
832
|
-
4. No per-route config -- inherits controller-level config
|
|
833
|
-
|
|
834
|
-
### Per-Route Auth Types
|
|
131
|
+
**Reference a model instead of a hardcoded string.** `@model({ settings: { authorize: { principal } } })` auto-populates `Model.AUTHORIZATION_SUBJECT`.
|
|
835
132
|
|
|
836
133
|
```typescript
|
|
837
|
-
|
|
838
|
-
type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
|
|
839
|
-
|
|
840
|
-
/** Per-route auth config. Endpoint config takes precedence over controller-level config. */
|
|
841
|
-
type TRouteAuthConfig = {
|
|
842
|
-
authenticate?: TRouteAuthenticateConfig;
|
|
843
|
-
authorize?: TRouteAuthorizeConfig;
|
|
844
|
-
};
|
|
134
|
+
authorize: { action: AuthorizationActions.READ, resource: Article.AUTHORIZATION_SUBJECT }
|
|
845
135
|
```
|
|
846
136
|
|
|
847
|
-
## See
|
|
848
|
-
|
|
849
|
-
- [Usage & Examples](./usage) -- Securing routes, voters, patterns, and CRUD integration
|
|
850
|
-
- [API Reference](./api) -- Architecture, enforcer internals, provider, registry, and adapters
|
|
851
|
-
- [Error Reference](./errors) -- Error messages and troubleshooting
|
|
137
|
+
## See also
|
|
852
138
|
|
|
853
|
-
-
|
|
854
|
-
|
|
855
|
-
|
|
139
|
+
- [Usage & Examples](./usage) - securing routes, voters, CRUD factory integration, domain scoping in depth
|
|
140
|
+
- [API Reference](./api) - architecture, enforcer internals, provider pipeline, registry, adapters
|
|
141
|
+
- [Error Reference](./errors) - every error message and how to fix it
|
|
142
|
+
- [Authentication](../authentication/) - runs before authorization, populates `Authentication.CURRENT_USER`
|
|
143
|
+
- [Components Overview](/guides/core-concepts/components) - component system basics
|
|
144
|
+
- [Persistent Models](/guides/core-concepts/persistent/models#authorization-settings) - declaring `AUTHORIZATION_SUBJECT` on a model
|
|
145
|
+
- [Security Guidelines](/best-practices/security-guidelines) - authorization best practices
|
|
856
146
|
|
|
857
|
-
|
|
858
|
-
- [Controllers](/references/base/controllers) -- Route configuration with auth
|
|
859
|
-
- [Middlewares](/references/base/middlewares) -- Custom middleware integration
|
|
147
|
+
**Files:**
|
|
860
148
|
|
|
861
|
-
-
|
|
862
|
-
|
|
149
|
+
- [`packages/core-server/src/components/auth/authorize/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/component.ts) - `AuthorizeComponent`
|
|
150
|
+
- [`packages/core-server/src/components/auth/authorize/common/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common) - constants, binding keys, types, policy/permission builders
|
|
151
|
+
- [`packages/core-server/src/components/auth/authorize/providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/providers/authorization.provider.ts) - `AuthorizationProvider` (the 7-step pipeline)
|
|
152
|
+
- [`packages/core-server/src/components/auth/authorize/enforcers/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers) - `CasbinAuthorizationEnforcer`, `AuthorizationEnforcerRegistry`, `CASBIN_RBAC_DOMAIN_SCOPED_MODEL`
|
|
153
|
+
- [`packages/core-server/src/components/auth/authorize/adapters/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters) - `BaseFilteredAdapter`, `ScopedCasbinAdapter`
|
|
154
|
+
- [`packages/core-server/src/components/auth/authorize/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) - `AuthorizationRole`
|
|
155
|
+
- [`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` auto-populating `AUTHORIZATION_SUBJECT` from `settings.authorize.principal`
|