@venizia/ignis-docs 0.2.0 → 0.2.1-0
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 +22 -11
- 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 +26 -2
- 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 +6 -2
- 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 +182 -93
- 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 +107 -322
- package/content/extensions/components/authentication/api.md +454 -603
- package/content/extensions/components/authentication/errors.md +121 -498
- package/content/extensions/components/authentication/index.md +88 -801
- package/content/extensions/components/authentication/usage.md +207 -956
- package/content/extensions/components/authorization/api.md +736 -656
- package/content/extensions/components/authorization/errors.md +168 -206
- package/content/extensions/components/authorization/index.md +82 -797
- package/content/extensions/components/authorization/usage.md +194 -527
- package/content/extensions/components/health-check.md +71 -243
- package/content/extensions/components/mail/api.md +504 -287
- package/content/extensions/components/mail/errors.md +73 -61
- package/content/extensions/components/mail/index.md +96 -467
- package/content/extensions/components/mail/usage.md +130 -172
- package/content/extensions/components/request-tracker.md +66 -173
- package/content/extensions/components/socket-io/api.md +195 -13
- package/content/extensions/components/socket-io/errors.md +3 -3
- package/content/extensions/components/socket-io/index.md +50 -337
- package/content/extensions/components/socket-io/usage.md +143 -26
- package/content/extensions/components/static-asset/api.md +410 -142
- package/content/extensions/components/static-asset/errors.md +110 -53
- package/content/extensions/components/static-asset/index.md +79 -608
- package/content/extensions/components/static-asset/usage.md +180 -300
- package/content/extensions/components/websocket/api.md +275 -399
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +74 -407
- package/content/extensions/components/websocket/usage.md +110 -341
- package/content/extensions/helpers/cron/index.md +51 -160
- package/content/extensions/helpers/crypto/index.md +62 -483
- package/content/extensions/helpers/crypto/reference.md +456 -0
- package/content/extensions/helpers/env/index.md +60 -178
- package/content/extensions/helpers/error/index.md +221 -207
- package/content/extensions/helpers/inversion/index.md +65 -556
- package/content/extensions/helpers/inversion/reference.md +522 -0
- package/content/extensions/helpers/kafka/admin.md +20 -1
- package/content/extensions/helpers/kafka/compile-binary.md +41 -35
- package/content/extensions/helpers/kafka/consumer.md +54 -20
- package/content/extensions/helpers/kafka/examples.md +21 -16
- package/content/extensions/helpers/kafka/index.md +80 -610
- package/content/extensions/helpers/kafka/producer.md +134 -5
- package/content/extensions/helpers/kafka/schema-registry.md +45 -70
- package/content/extensions/helpers/logger/hf-logger.md +193 -0
- package/content/extensions/helpers/logger/index.md +64 -563
- package/content/extensions/helpers/logger/pino.md +85 -0
- package/content/extensions/helpers/logger/reference.md +746 -0
- package/content/extensions/helpers/network/api.md +241 -195
- package/content/extensions/helpers/network/index.md +72 -530
- package/content/extensions/helpers/queue/index.md +72 -900
- package/content/extensions/helpers/queue/reference.md +467 -0
- package/content/extensions/helpers/redis/index.md +73 -645
- package/content/extensions/helpers/redis/reference.md +727 -0
- package/content/extensions/helpers/secrets/index.md +66 -0
- package/content/extensions/helpers/socket-io/api.md +305 -203
- package/content/extensions/helpers/socket-io/index.md +66 -432
- package/content/extensions/helpers/storage/api.md +564 -462
- package/content/extensions/helpers/storage/index.md +77 -573
- package/content/extensions/helpers/types/index.md +66 -499
- package/content/extensions/helpers/types/reference.md +650 -0
- package/content/extensions/helpers/uid/index.md +58 -227
- package/content/extensions/helpers/websocket/api.md +329 -216
- package/content/extensions/helpers/websocket/index.md +65 -503
- package/content/extensions/helpers/worker-thread/index.md +58 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
- package/content/guides/core-concepts/persistent/transactions.md +1 -1
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/migrations/redis-helpers-migration.md +1 -1
- package/content/guides/migrations/unified-connectors-migration.md +2 -2
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/references/base/application.md +1 -1
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/datasources-reference.md +599 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +17 -39
- package/content/references/base/filter-system/application-usage.md +69 -121
- package/content/references/base/filter-system/array-operators.md +12 -0
- package/content/references/base/filter-system/comparison-operators.md +12 -0
- package/content/references/base/filter-system/default-filter.md +136 -348
- package/content/references/base/filter-system/fields-order-pagination.md +38 -16
- package/content/references/base/filter-system/index.md +106 -257
- package/content/references/base/filter-system/json-filtering.md +12 -2
- package/content/references/base/filter-system/list-operators.md +16 -2
- package/content/references/base/filter-system/logical-operators.md +13 -0
- package/content/references/base/filter-system/null-operators.md +13 -0
- package/content/references/base/filter-system/pattern-matching.md +12 -0
- package/content/references/base/filter-system/quick-reference.md +11 -2
- package/content/references/base/filter-system/range-operators.md +12 -0
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +156 -233
- package/content/references/base/middlewares.md +35 -21
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +80 -1452
- package/content/references/base/repositories/advanced.md +156 -192
- package/content/references/base/repositories/index.md +77 -650
- package/content/references/base/repositories/mixins.md +22 -18
- package/content/references/base/repositories/relations.md +123 -171
- package/content/references/base/repositories/soft-deletable.md +58 -56
- package/content/references/base/secrets.md +263 -0
- package/content/references/base/services.md +2 -2
- package/content/references/configuration/environment-variables.md +48 -4
- package/content/references/configuration/index.md +49 -31
- 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/index.md +1 -1
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +29 -62
- 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 +57 -218
- 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/package.json +8 -8
|
@@ -1,437 +1,46 @@
|
|
|
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` wires up enforcer-based route authorization - RBAC through Casbin (with an optional multi-tenant domain-scoped model), voters, and role shortcuts - evaluated by the `authorize()` middleware after authentication.
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|------|-------|
|
|
9
|
-
| **Package** | `@venizia/ignis` |
|
|
10
|
-
| **Class** | `AuthorizeComponent` |
|
|
11
|
-
| **Runtimes** | Both |
|
|
11
|
+
## In one example
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
| Component | Purpose |
|
|
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
|
|
13
|
+
The recommended setup: scoped Casbin RBAC backed by one `PolicyDefinition` edge table, then a protected route.
|
|
210
14
|
|
|
211
15
|
```typescript
|
|
212
|
-
// Classes & functions
|
|
213
16
|
import {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
407
|
-
|
|
408
|
-
```typescript
|
|
409
|
-
import {
|
|
410
|
-
AuthorizeComponent,
|
|
411
|
-
AuthorizeBindingKeys,
|
|
412
|
-
AuthorizationEnforcerRegistry,
|
|
413
|
-
AuthorizationEnforcerTypes,
|
|
414
|
-
CasbinAuthorizationEnforcer,
|
|
415
|
-
CasbinEnforcerModelDrivers,
|
|
416
|
-
ScopedCasbinAdapter,
|
|
417
|
-
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
418
|
-
BaseApplication,
|
|
419
|
-
IAuthorizeOptions,
|
|
17
|
+
AuthorizeBindingKeys, AuthorizeComponent, AuthorizationDecisions, AuthorizationEnforcerRegistry,
|
|
18
|
+
AuthorizationEnforcerTypes, CasbinAuthorizationEnforcer, CasbinEnforcerModelDrivers,
|
|
19
|
+
ScopedCasbinAdapter, CASBIN_RBAC_DOMAIN_SCOPED_MODEL, BaseApplication, IAuthorizeOptions,
|
|
420
20
|
} from '@venizia/ignis';
|
|
421
21
|
|
|
422
22
|
export class Application extends BaseApplication {
|
|
423
|
-
|
|
424
|
-
//
|
|
23
|
+
preConfigure() {
|
|
24
|
+
// 1. Global options
|
|
425
25
|
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
426
|
-
defaultDecision:
|
|
26
|
+
defaultDecision: AuthorizationDecisions.DENY,
|
|
427
27
|
alwaysAllowRoles: ['999_super-admin'],
|
|
428
28
|
});
|
|
429
29
|
|
|
430
|
-
//
|
|
30
|
+
// 2. Component (validates options, binds alwaysAllowRoles)
|
|
431
31
|
this.component(AuthorizeComponent);
|
|
432
32
|
|
|
433
|
-
//
|
|
434
|
-
const adapter = new ScopedCasbinAdapter({
|
|
33
|
+
// 3. Enforcer, registered by class + name + type + co-located options
|
|
34
|
+
const adapter = new ScopedCasbinAdapter({
|
|
35
|
+
dataSource, // a BasePostgresDataSource instance
|
|
36
|
+
entities: {
|
|
37
|
+
policyDefinition: { tableName: 'PolicyDefinition', schemaName: 'identity' },
|
|
38
|
+
permission: { tableName: 'Permission', schemaName: 'identity' },
|
|
39
|
+
principals: { user: 'User', role: 'Role' },
|
|
40
|
+
domainTypes: ['Merchant'],
|
|
41
|
+
softDelete: { use: true, columnName: 'deleted_at' },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
435
44
|
|
|
436
45
|
AuthorizationEnforcerRegistry.getInstance().register({
|
|
437
46
|
container: this,
|
|
@@ -440,10 +49,7 @@ export class Application extends BaseApplication {
|
|
|
440
49
|
name: 'casbin',
|
|
441
50
|
type: AuthorizationEnforcerTypes.CASBIN,
|
|
442
51
|
options: {
|
|
443
|
-
model: {
|
|
444
|
-
driver: CasbinEnforcerModelDrivers.TEXT,
|
|
445
|
-
definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
446
|
-
},
|
|
52
|
+
model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL },
|
|
447
53
|
isScoped: true,
|
|
448
54
|
adapter,
|
|
449
55
|
cached: { use: false },
|
|
@@ -454,409 +60,88 @@ export class Application extends BaseApplication {
|
|
|
454
60
|
}
|
|
455
61
|
```
|
|
456
62
|
|
|
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
63
|
```typescript
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
64
|
+
// Inside a controller's binding() - a route protected by authentication + authorization
|
|
65
|
+
const DELETE_ARTICLE_CONFIG = {
|
|
66
|
+
path: '/articles/{id}',
|
|
67
|
+
method: 'delete',
|
|
68
|
+
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
69
|
+
authorize: { action: AuthorizationActions.DELETE, resource: 'Article' },
|
|
70
|
+
responses: jsonResponse({ description: 'Deleted article', schema: ArticleSchema }),
|
|
71
|
+
} as const;
|
|
709
72
|
```
|
|
710
73
|
|
|
711
|
-
|
|
712
|
-
> `IdType = number | string | bigint` is defined in `@/base/models/common/types`.
|
|
74
|
+
## How it works
|
|
713
75
|
|
|
714
|
-
|
|
76
|
+
- **Enforcer-based and pluggable.** `authorize({ spec })` returns Hono middleware built by `AuthorizationProvider`. It resolves an `IAuthorizationEnforcer` from `AuthorizationEnforcerRegistry` by name (default: the first registered) - swap `CasbinAuthorizationEnforcer` for a custom class without touching route configs.
|
|
77
|
+
- **Runs after authentication.** The middleware reads `Authentication.CURRENT_USER` from the Hono context, so `AuthenticateComponent` must run first and the route needs an `authenticate` config alongside `authorize`.
|
|
78
|
+
- **No enforcers registered = no-op.** If `AuthorizationEnforcerRegistry.hasEnforcers()` is `false`, the middleware calls `next()` and skips authorization entirely - useful during incremental rollout, dangerous if you forget to register an enforcer in production.
|
|
79
|
+
- **Casbin's scoped RBAC model is the recommended engine.** `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` + `isScoped: true` + `ScopedCasbinAdapter` reads one principal's role/permission/domain edges (plus the shared role/resource/action/domain hierarchy) from a single `PolicyDefinition` table, and supports multi-tenant grants scoped to `SYSTEM_WIDE`, `ANY_MEMBER`, or a specific `<Type>_<id>` domain.
|
|
80
|
+
- **Per-request enforcers, cached lines.** Each Casbin evaluation borrows an isolated enforcer from an internal pool, loads that user's policy lines into it, and evaluates - the datasource query only runs on cache miss (or always, if `cached.use: false`).
|
|
715
81
|
|
|
716
|
-
|
|
82
|
+
**Pipeline (7 steps, short-circuits marked)**
|
|
717
83
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
84
|
+
| # | Step | Short-circuits when |
|
|
85
|
+
|---|------|----------------------|
|
|
86
|
+
| 1 | `Authorization.SKIP_AUTHORIZATION` check | Set -> `next()` |
|
|
87
|
+
| 2 | Read `Authentication.CURRENT_USER` | Missing -> 401 |
|
|
88
|
+
| 3 | Role shortcuts (`alwaysAllowRoles` + `allowedRoles`) | Match -> `next()` |
|
|
89
|
+
| 4 | Voters (per-route) | `ALLOW`/`DENY` -> `next()` / 403 |
|
|
90
|
+
| 5 | Resolve enforcer | None registered -> `next()` |
|
|
91
|
+
| 6 | Build/cache rules (+ resolve domain, if any) | - |
|
|
92
|
+
| 7 | `enforcer.evaluate()` | `DENY`/`ABSTAIN`-as-deny -> 403 |
|
|
725
93
|
|
|
726
|
-
|
|
727
|
-
2. Authorization middleware is injected second (from `authorize` config)
|
|
728
|
-
3. Custom middleware is injected last (from `middleware` config -- REST only)
|
|
94
|
+
## Common tasks
|
|
729
95
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
### REST Controllers
|
|
733
|
-
|
|
734
|
-
The `AbstractRestController.buildRouteMiddlewares()` method builds the middleware array from route config. The `getRouteConfigs()` method calls `buildRouteMiddlewares()` internally and wraps it into a Hono route definition:
|
|
96
|
+
**Secure a route.** Add `authorize: { action, resource }` next to `authenticate` in a route config or `@get`/`@post` decorator.
|
|
735
97
|
|
|
736
98
|
```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
|
-
}
|
|
99
|
+
authorize: { action: AuthorizationActions.READ, resource: 'Article' }
|
|
760
100
|
```
|
|
761
101
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
The `AbstractGrpcController.buildRpcMiddlewares()` method provides symmetric authorization support for gRPC routes. Authorization specs are applied the same way as REST:
|
|
102
|
+
**Bypass the enforcer for trusted roles.** `alwaysAllowRoles` (global, in `IAuthorizeOptions`) or `allowedRoles` (per-route spec) skip straight to `next()` once a matching role is found.
|
|
765
103
|
|
|
766
104
|
```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
|
-
}
|
|
105
|
+
authorize: { action: AuthorizationActions.DELETE, resource: 'Article', allowedRoles: [AuthorizationRoles.ADMIN.identifier] }
|
|
786
106
|
```
|
|
787
107
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
Extended route config that supports both authentication and authorization:
|
|
108
|
+
**Add custom logic before the enforcer.** A voter returns `ALLOW`/`DENY`/`ABSTAIN`; the first non-`ABSTAIN` decision wins.
|
|
791
109
|
|
|
792
110
|
```typescript
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
111
|
+
const ownerVoter: TAuthorizationVoter = async ({ user, context }) =>
|
|
112
|
+
(await articleService.findById({ id: context.req.param('id') }))?.authorId === user.userId
|
|
113
|
+
? AuthorizationDecisions.ALLOW
|
|
114
|
+
: AuthorizationDecisions.ABSTAIN;
|
|
797
115
|
```
|
|
798
116
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
### Per-Route Configuration in CRUD Factory
|
|
802
|
-
|
|
803
|
-
CRUD factory routes support both authentication and authorization configuration:
|
|
117
|
+
**Scope a grant to a tenant.** Declare where the domain comes from per route (or globally via `IAuthorizeOptions.domainResolver`).
|
|
804
118
|
|
|
805
119
|
```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
|
-
});
|
|
120
|
+
authorize: { action: 'read', resource: 'Order', domain: { from: 'param', key: 'merchantId', type: 'Merchant' } }
|
|
826
121
|
```
|
|
827
122
|
|
|
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
|
|
123
|
+
**Reference a model instead of a hardcoded string.** `@model({ settings: { authorize: { principal } } } })` auto-populates `Model.AUTHORIZATION_SUBJECT`.
|
|
835
124
|
|
|
836
125
|
```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
|
-
};
|
|
126
|
+
authorize: { action: AuthorizationActions.READ, resource: Article.AUTHORIZATION_SUBJECT }
|
|
845
127
|
```
|
|
846
128
|
|
|
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
|
|
129
|
+
## See also
|
|
852
130
|
|
|
853
|
-
-
|
|
854
|
-
|
|
855
|
-
|
|
131
|
+
- [Usage & Examples](./usage) - securing routes, voters, CRUD factory integration, domain scoping in depth
|
|
132
|
+
- [API Reference](./api) - architecture, enforcer internals, provider pipeline, registry, adapters
|
|
133
|
+
- [Error Reference](./errors) - every error message and how to fix it
|
|
134
|
+
- [Authentication](../authentication/) - runs before authorization, populates `Authentication.CURRENT_USER`
|
|
135
|
+
- [Components Overview](/guides/core-concepts/components) - component system basics
|
|
136
|
+
- [Persistent Models](/guides/core-concepts/persistent/models#authorization-settings) - declaring `AUTHORIZATION_SUBJECT` on a model
|
|
137
|
+
- [Security Guidelines](/best-practices/security-guidelines) - authorization best practices
|
|
856
138
|
|
|
857
|
-
|
|
858
|
-
- [Controllers](/references/base/controllers) -- Route configuration with auth
|
|
859
|
-
- [Middlewares](/references/base/middlewares) -- Custom middleware integration
|
|
139
|
+
**Files:**
|
|
860
140
|
|
|
861
|
-
-
|
|
862
|
-
|
|
141
|
+
- [`packages/core/src/components/auth/authorize/component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/component.ts) - `AuthorizeComponent`
|
|
142
|
+
- [`packages/core/src/components/auth/authorize/common/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common) - constants, binding keys, types, policy/permission builders
|
|
143
|
+
- [`packages/core/src/components/auth/authorize/providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/providers/authorization.provider.ts) - `AuthorizationProvider` (the 7-step pipeline)
|
|
144
|
+
- [`packages/core/src/components/auth/authorize/enforcers/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers) - `CasbinAuthorizationEnforcer`, `AuthorizationEnforcerRegistry`, `CASBIN_RBAC_DOMAIN_SCOPED_MODEL`
|
|
145
|
+
- [`packages/core/src/components/auth/authorize/adapters/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters) - `BaseFilteredAdapter`, `ScopedCasbinAdapter`
|
|
146
|
+
- [`packages/core/src/components/auth/authorize/models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/models/authorization-role.model.ts) - `AuthorizationRole`
|
|
147
|
+
- [`packages/core/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts) - `@model` auto-populating `AUTHORIZATION_SUBJECT` from `settings.authorize.principal`
|