@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.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,437 +1,46 @@
1
- # Authorization -- Setup & Configuration
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
- > Enforcer-based authorization with RBAC, voters, and Casbin integration
7
+ # Authorization
4
8
 
5
- ## Quick Reference
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
- | Item | Value |
8
- |------|-------|
9
- | **Package** | `@venizia/ignis` |
10
- | **Class** | `AuthorizeComponent` |
11
- | **Runtimes** | Both |
11
+ ## In one example
12
12
 
13
- ### Key Components
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
- // 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
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
- async registerAuthorization() {
424
- // Step 1: Global options
23
+ preConfigure() {
24
+ // 1. Global options
425
25
  this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
426
- defaultDecision: 'deny',
26
+ defaultDecision: AuthorizationDecisions.DENY,
427
27
  alwaysAllowRoles: ['999_super-admin'],
428
28
  });
429
29
 
430
- // Step 2: Register component
30
+ // 2. Component (validates options, binds alwaysAllowRoles)
431
31
  this.component(AuthorizeComponent);
432
32
 
433
- // Step 3: Register enforcer(s) with co-located options
434
- const adapter = new ScopedCasbinAdapter({ dataSource, entities: { /* IScopedCasbinEntities */ } });
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
- interface IJWTTokenPayload extends JWTPayload, IAuthUser {
701
- userId: IdType;
702
- roles: { id: IdType; identifier: string; priority: number }[];
703
- clientId?: string;
704
- provider?: string;
705
- email?: string;
706
- name?: string;
707
- [extra: string | symbol]: any;
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
- > [!NOTE]
712
- > `IdType = number | string | bigint` is defined in `@/base/models/common/types`.
74
+ ## How it works
713
75
 
714
- ## Relationship with Authentication
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
- Authorization runs **after** authentication in the middleware chain. Both REST and gRPC controllers ensure the correct ordering:
82
+ **Pipeline (7 steps, short-circuits marked)**
717
83
 
718
- ```mermaid
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
- ```
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
- 1. Authentication middleware is injected first (from `authenticate` config)
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
- This means `Authentication.CURRENT_USER` is always available when the authorization middleware executes.
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
- // In AbstractRestController
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
- ### gRPC Controllers
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
- // In AbstractGrpcController
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
- ### IAuthRouteConfig
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
- interface IAuthRouteConfig extends HonoRouteConfig {
794
- authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
795
- authorize?: IAuthorizationSpec | IAuthorizationSpec[];
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
- When `authorize` is an array, each spec creates a separate middleware. All must pass for the handler to execute.
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
- ControllerFactory.defineCrudController({
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
- **Priority rules:**
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
- /** Per-route authorization config: { skip: true }, single spec, or array of specs. */
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 Also
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
- - **Related Components:**
854
- - [Authentication](../authentication/) -- Authentication system (runs before authorization)
855
- - [All Components](../index) -- Built-in components list
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
- - **References:**
858
- - [Controllers](/references/base/controllers) -- Route configuration with auth
859
- - [Middlewares](/references/base/middlewares) -- Custom middleware integration
139
+ **Files:**
860
140
 
861
- - **Best Practices:**
862
- - [Security Guidelines](/best-practices/security-guidelines) -- Authorization best practices
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`