@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,10 +1,85 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Authorization Reference
|
|
3
|
+
description: Full option tables, binding keys, constants, enforcer internals, adapters, policy builders, and controller integration for the Authorization component
|
|
4
|
+
difficulty: advanced
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Authorization Reference
|
|
8
|
+
|
|
9
|
+
Every option, binding key, class, and method the Authorization component exposes. See the [Overview](./) for the guided introduction and [Usage](./usage) for task-oriented examples.
|
|
10
|
+
|
|
11
|
+
**Files:**
|
|
12
|
+
|
|
13
|
+
- [`packages/core/src/components/auth/authorize/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize) - component, providers, enforcers, adapters, models, middleware
|
|
14
|
+
- [`packages/core/src/components/auth/base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts) - `AbstractAuthRegistry` (shared with Authentication)
|
|
15
|
+
- [`packages/core/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts) - `@model` populating `AUTHORIZATION_SUBJECT`
|
|
16
|
+
- [`packages/core/src/helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts) - `MetadataRegistry` authorize-settings queries
|
|
17
|
+
|
|
18
|
+
## Import paths
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {
|
|
22
|
+
// Component & middleware
|
|
23
|
+
AuthorizeComponent, AuthorizationProvider, authorize,
|
|
24
|
+
|
|
25
|
+
// Registry
|
|
26
|
+
AuthorizationEnforcerRegistry,
|
|
27
|
+
|
|
28
|
+
// Enforcers
|
|
29
|
+
CasbinAuthorizationEnforcer,
|
|
30
|
+
|
|
31
|
+
// Adapters
|
|
32
|
+
BaseFilteredAdapter, ScopedCasbinAdapter,
|
|
33
|
+
|
|
34
|
+
// Scoped RBAC model
|
|
35
|
+
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
36
|
+
|
|
37
|
+
// Models
|
|
38
|
+
AuthorizationRole,
|
|
39
|
+
|
|
40
|
+
// Policy / permission catalog builders
|
|
41
|
+
AuthorizationPolicyBuilder, AuthorizationPermissionBuilder,
|
|
42
|
+
|
|
43
|
+
// Resource-hierarchy matcher (register on a custom Casbin model)
|
|
44
|
+
objectMatch,
|
|
45
|
+
|
|
46
|
+
// Constants
|
|
47
|
+
Authorization, AuthorizationActions, AuthorizationDecisions, AuthorizationDomainScopes,
|
|
48
|
+
AuthorizationPolicyVariants, AuthorizationRoles, AuthorizationEnforcerTypes,
|
|
49
|
+
CasbinEnforcerModelDrivers, CasbinEnforcerCachedDrivers, CasbinRuleVariants,
|
|
50
|
+
CasbinDomainMatchingFunctions,
|
|
51
|
+
|
|
52
|
+
// Binding keys
|
|
53
|
+
AuthorizeBindingKeys,
|
|
54
|
+
} from '@venizia/ignis';
|
|
55
|
+
|
|
56
|
+
import type {
|
|
57
|
+
// Core interfaces
|
|
58
|
+
IAuthorizeOptions, IAuthorizationEnforcer, IAuthorizationSpec, IAuthorizationRequest,
|
|
59
|
+
IAuthorizationRole, IAuthorizationDomainSource, TAuthorizationDomainResolver,
|
|
60
|
+
|
|
61
|
+
// Casbin options
|
|
62
|
+
ICasbinEnforcerOptions, ICasbinEnforcerCachedRedis,
|
|
63
|
+
|
|
64
|
+
// Adapter types
|
|
65
|
+
ICasbinPolicyFilter, ICasbinPolicySource, IScopedCasbinEntities, IScopedCasbinPolicyFilter,
|
|
66
|
+
|
|
67
|
+
// Function & utility types
|
|
68
|
+
TAuthorizeFn, TAuthorizationVoter, TAuthorizationConditions, TRegistryDescriptor,
|
|
69
|
+
|
|
70
|
+
// Model-based authorization metadata
|
|
71
|
+
IModelAuthorizeSettings,
|
|
72
|
+
|
|
73
|
+
// Value types (from TConstValue)
|
|
74
|
+
TAuthorizationAction, TAuthorizationDecision, TAuthorizationEnforcerType,
|
|
75
|
+
TCasbinEnforcerCachedDriver, TCasbinEnforcerModelDriver, TCasbinRuleVariant,
|
|
76
|
+
TCasbinDomainMatchingFunction, TAuthorizationPolicyVariant, TAuthorizationDomainScope,
|
|
77
|
+
} from '@venizia/ignis';
|
|
78
|
+
```
|
|
4
79
|
|
|
5
80
|
## Architecture
|
|
6
81
|
|
|
7
|
-
### System
|
|
82
|
+
### System overview
|
|
8
83
|
|
|
9
84
|
```mermaid
|
|
10
85
|
graph TB
|
|
@@ -22,22 +97,10 @@ graph TB
|
|
|
22
97
|
Registry --> Casbin["CasbinAuthorizationEnforcer<br/>+ FilteredAdapter"]
|
|
23
98
|
Registry --> Custom["Custom Enforcer"]
|
|
24
99
|
|
|
25
|
-
Provider --> Pipeline["
|
|
26
|
-
|
|
27
|
-
subgraph Pipeline["7-Step Middleware Pipeline"]
|
|
28
|
-
direction TB
|
|
29
|
-
S1["1. Skip check"]
|
|
30
|
-
S2["2. User check"]
|
|
31
|
-
S3["3. Role shortcuts"]
|
|
32
|
-
S4["4. Voters"]
|
|
33
|
-
S5["5. Resolve enforcer"]
|
|
34
|
-
S6["6. Build rules"]
|
|
35
|
-
S7["7. Evaluate"]
|
|
36
|
-
S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7
|
|
37
|
-
end
|
|
100
|
+
Provider --> Pipeline["7-Step Middleware Pipeline"]
|
|
38
101
|
```
|
|
39
102
|
|
|
40
|
-
### Middleware
|
|
103
|
+
### Middleware pipeline
|
|
41
104
|
|
|
42
105
|
```mermaid
|
|
43
106
|
flowchart TD
|
|
@@ -54,7 +117,8 @@ flowchart TD
|
|
|
54
117
|
Voters -->|ABSTAIN / none| HasEnforcers{Enforcers registered?}
|
|
55
118
|
HasEnforcers -->|No| Next6([next - skip, no enforcers])
|
|
56
119
|
HasEnforcers -->|Yes| Resolve[Resolve enforcer by name]
|
|
57
|
-
Resolve -->
|
|
120
|
+
Resolve --> ResolveDomain["Resolve domain (if spec.domain or domainResolver)"]
|
|
121
|
+
ResolveDomain --> Cache{Rules cached?}
|
|
58
122
|
Cache -->|Yes| Evaluate
|
|
59
123
|
Cache -->|No| PType{principalType?}
|
|
60
124
|
PType -->|Missing| E400[/400 principalType required/]
|
|
@@ -69,15 +133,11 @@ flowchart TD
|
|
|
69
133
|
Default -->|DENY| E403b
|
|
70
134
|
```
|
|
71
135
|
|
|
72
|
-
### Class
|
|
136
|
+
### Class hierarchy
|
|
73
137
|
|
|
74
138
|
```mermaid
|
|
75
139
|
classDiagram
|
|
76
|
-
class BaseHelper {
|
|
77
|
-
+logger
|
|
78
|
-
+scope
|
|
79
|
-
}
|
|
80
|
-
|
|
140
|
+
class BaseHelper { +logger +scope }
|
|
81
141
|
class AbstractAuthRegistry~TItem~ {
|
|
82
142
|
#descriptors: Map
|
|
83
143
|
#getBindingPrefix()* string
|
|
@@ -87,7 +147,6 @@ classDiagram
|
|
|
87
147
|
#registerDescriptor(opts) void
|
|
88
148
|
#resolveDescriptor(opts) TItem
|
|
89
149
|
}
|
|
90
|
-
|
|
91
150
|
class AuthorizationEnforcerRegistry {
|
|
92
151
|
-instance$ AuthorizationEnforcerRegistry
|
|
93
152
|
-configuredEnforcers: Set
|
|
@@ -97,7 +156,6 @@ classDiagram
|
|
|
97
156
|
+resolveEnforcer(opts) Promise
|
|
98
157
|
+resolveOptions() IAuthorizeOptions
|
|
99
158
|
}
|
|
100
|
-
|
|
101
159
|
class IAuthorizationEnforcer {
|
|
102
160
|
<<interface>>
|
|
103
161
|
+name: string
|
|
@@ -105,7 +163,6 @@ classDiagram
|
|
|
105
163
|
+buildRules(opts) TRules
|
|
106
164
|
+evaluate(opts) TAuthorizationDecision
|
|
107
165
|
}
|
|
108
|
-
|
|
109
166
|
class CasbinAuthorizationEnforcer {
|
|
110
167
|
-pool: BasePoolHelper~Enforcer~
|
|
111
168
|
-pendingLineFetches: Map
|
|
@@ -113,10 +170,9 @@ classDiagram
|
|
|
113
170
|
+destroy() void
|
|
114
171
|
+buildRules(opts) ICasbinRules
|
|
115
172
|
+evaluate(opts) TAuthorizationDecision
|
|
116
|
-
+invalidateUserCache(opts)?
|
|
173
|
+
+invalidateUserCache(opts)?
|
|
117
174
|
+rebuildUserCache(opts)?
|
|
118
175
|
}
|
|
119
|
-
|
|
120
176
|
class BaseFilteredAdapter~TFilter~ {
|
|
121
177
|
<<abstract>>
|
|
122
178
|
#dataSource: ICasbinPolicySource
|
|
@@ -125,17 +181,10 @@ classDiagram
|
|
|
125
181
|
+isFiltered() boolean
|
|
126
182
|
#loadLines(opts) void
|
|
127
183
|
}
|
|
128
|
-
|
|
129
184
|
class ScopedCasbinAdapter {
|
|
130
185
|
#entities: IScopedCasbinEntities
|
|
131
186
|
+loadFilteredPolicy(model, filter) void
|
|
132
|
-
#queryRoleAssignments(opts) lines + roleIds
|
|
133
|
-
#queryMemberships(opts) string[]
|
|
134
|
-
#queryGrants(opts) string[]
|
|
135
|
-
#loadStructuralTrees() string[]
|
|
136
|
-
#expandRoleClosure(opts) IdType[]
|
|
137
187
|
}
|
|
138
|
-
|
|
139
188
|
BaseHelper <|-- AbstractAuthRegistry
|
|
140
189
|
AbstractAuthRegistry <|-- AuthorizationEnforcerRegistry
|
|
141
190
|
IAuthorizationEnforcer <|.. CasbinAuthorizationEnforcer
|
|
@@ -144,155 +193,397 @@ classDiagram
|
|
|
144
193
|
BaseFilteredAdapter <|-- ScopedCasbinAdapter
|
|
145
194
|
```
|
|
146
195
|
|
|
147
|
-
### Module
|
|
196
|
+
### Module file layout
|
|
148
197
|
|
|
149
198
|
```
|
|
150
|
-
auth/authorize/
|
|
199
|
+
components/auth/authorize/
|
|
151
200
|
├── adapters/
|
|
152
201
|
│ ├── base-filtered.ts # BaseFilteredAdapter (thin abstract) + ICasbinPolicyFilter
|
|
153
202
|
│ ├── scoped-casbin.adapter.ts # ScopedCasbinAdapter (generic edge-table reader)
|
|
154
|
-
│ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable
|
|
203
|
+
│ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable, ICasbinPolicySource
|
|
155
204
|
├── common/
|
|
156
|
-
│ ├── constants.ts
|
|
157
|
-
│
|
|
158
|
-
│
|
|
159
|
-
│
|
|
160
|
-
│
|
|
161
|
-
│ ├──
|
|
162
|
-
│
|
|
163
|
-
│ ├── types.ts # IAuthorizeOptions, IAuthorizationEnforcer,
|
|
164
|
-
│ │ # IAuthorizationSpec, ICasbinEnforcerOptions, etc.
|
|
165
|
-
│ └── index.ts # Barrel export
|
|
205
|
+
│ ├── constants.ts # Authorization, Actions, Decisions, PolicyVariants, Roles, ...
|
|
206
|
+
│ ├── keys.ts # AuthorizeBindingKeys
|
|
207
|
+
│ ├── object-match.ts # objectMatch resource-hierarchy matcher
|
|
208
|
+
│ ├── permission-builder.ts # AuthorizationPermissionBuilder
|
|
209
|
+
│ ├── policy-builder.ts # AuthorizationPolicyBuilder
|
|
210
|
+
│ ├── resolve-request-domain.ts # resolveRequestDomain
|
|
211
|
+
│ └── types.ts # IAuthorizeOptions, IAuthorizationEnforcer, ICasbinEnforcerOptions, ...
|
|
166
212
|
├── enforcers/
|
|
167
213
|
│ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
|
|
168
214
|
│ ├── enforcer-registry.ts # AuthorizationEnforcerRegistry (singleton)
|
|
169
|
-
│
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
│ └── authorize.middleware.ts # authorize() standalone function
|
|
175
|
-
├── models/
|
|
176
|
-
│ ├── authorization-role.model.ts # AuthorizationRole
|
|
177
|
-
│ └── index.ts
|
|
178
|
-
├── providers/
|
|
179
|
-
│ └── authorization.provider.ts # AuthorizationProvider
|
|
180
|
-
├── component.ts # AuthorizeComponent
|
|
181
|
-
└── index.ts # Barrel export (all submodules)
|
|
215
|
+
│ └── models/rbac-domain.model.ts # CASBIN_RBAC_DOMAIN_SCOPED_MODEL
|
|
216
|
+
├── middlewares/authorize.middleware.ts # authorize() standalone function
|
|
217
|
+
├── models/authorization-role.model.ts # AuthorizationRole
|
|
218
|
+
├── providers/authorization.provider.ts # AuthorizationProvider
|
|
219
|
+
└── component.ts # AuthorizeComponent
|
|
182
220
|
```
|
|
183
221
|
|
|
184
|
-
###
|
|
185
|
-
|
|
186
|
-
| Technology | Purpose |
|
|
187
|
-
|------------|---------|
|
|
188
|
-
| **Hono middleware** | Route-level authorization via `createMiddleware` from `hono/factory` |
|
|
189
|
-
| **`casbin`** (optional) | External policy engine for Casbin enforcer. Peer dependency -- not bundled. |
|
|
190
|
-
| **`@venizia/ignis-helpers`** | `BaseHelper` base class, `getError` for error creation, `HTTP` result codes |
|
|
191
|
-
| **`@venizia/ignis-inversion`** | `IProvider` interface, `BindingScopes` for singleton registration |
|
|
192
|
-
|
|
193
|
-
### Design Decisions
|
|
222
|
+
### Design decisions
|
|
194
223
|
|
|
195
224
|
| Decision | Rationale |
|
|
196
225
|
|----------|-----------|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
226
|
+
| Enforcer-based | Pluggable architecture - swap Casbin for a custom enforcer without changing route configs |
|
|
227
|
+
| Registry + co-located options | Enforcer class, name, type, and options are registered together - no split configuration |
|
|
228
|
+
| Type-discriminated enforcers | `type: 'casbin' \| 'custom'` in registry constrains `options` (`ICasbinEnforcerOptions` vs `unknown`) |
|
|
229
|
+
| Voter pattern | Custom logic that short-circuits before the enforcer |
|
|
230
|
+
| Rules caching | Built rules cached on the Hono context per-request - avoids rebuilding for multi-spec routes |
|
|
231
|
+
| Registry singleton | Mirrors `AuthenticationStrategyRegistry` - shares `AbstractAuthRegistry<T>` |
|
|
232
|
+
| Filtered adapter pattern | `BaseFilteredAdapter` is a thin read-only base; subclasses implement only `loadFilteredPolicy` |
|
|
233
|
+
| No-enforcer fallback | No enforcers registered -> the middleware skips authorization and calls `next()` instead of throwing |
|
|
234
|
+
| Single edge table (scoped model) | `ScopedCasbinAdapter` reads one `PolicyDefinition` table for every edge type - no per-relation tables |
|
|
206
235
|
|
|
207
|
-
##
|
|
236
|
+
## AuthorizeComponent
|
|
208
237
|
|
|
209
|
-
|
|
238
|
+
`AuthorizeComponent extends BaseComponent`. Its `binding()` runs at application startup:
|
|
210
239
|
|
|
211
240
|
| Step | Action | Failure |
|
|
212
241
|
|------|--------|---------|
|
|
213
|
-
| 1 | Resolve `IAuthorizeOptions` from container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
|
|
214
|
-
| 2 |
|
|
242
|
+
| 1 | Resolve `IAuthorizeOptions` from the container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
|
|
243
|
+
| 2 | `bindAlwaysAllowRoles()` - binds `alwaysAllowRoles` to `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` if present | Skipped if no roles configured |
|
|
215
244
|
|
|
216
245
|
```typescript
|
|
217
246
|
class AuthorizeComponent extends BaseComponent {
|
|
218
|
-
constructor(
|
|
219
|
-
@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
|
|
220
|
-
) { ... }
|
|
221
|
-
|
|
247
|
+
constructor(@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication);
|
|
222
248
|
override binding(): ValueOrPromise<void>;
|
|
223
249
|
private bindAlwaysAllowRoles(opts: { options: IAuthorizeOptions }): void;
|
|
224
250
|
}
|
|
225
251
|
```
|
|
226
252
|
|
|
227
253
|
> [!NOTE]
|
|
228
|
-
>
|
|
254
|
+
> Enforcer registration is separate - `AuthorizeComponent` only validates global options. Register enforcers via `AuthorizationEnforcerRegistry.register()`.
|
|
229
255
|
|
|
230
|
-
|
|
256
|
+
Source -> [`component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/component.ts)
|
|
231
257
|
|
|
232
|
-
|
|
258
|
+
## Binding keys
|
|
233
259
|
|
|
234
|
-
|
|
260
|
+
| Key | Constant | Type | Description |
|
|
261
|
+
|-----|----------|------|-------------|
|
|
262
|
+
| `@app/authorize/options` | `AuthorizeBindingKeys.OPTIONS` | `IAuthorizeOptions` | Global authorization options |
|
|
263
|
+
| `@app/authorize/always-allow-roles` | `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` | `string[]` | Auto-bound by the component if present in options |
|
|
264
|
+
| `@app/authorize/enforcers/{name}/options` | `AuthorizeBindingKeys.enforcerOptions(name)` | `ICasbinEnforcerOptions \| unknown` | Per-enforcer options, auto-bound by the registry |
|
|
235
265
|
|
|
236
266
|
```typescript
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
267
|
+
class AuthorizeBindingKeys {
|
|
268
|
+
static readonly OPTIONS = '@app/authorize/options';
|
|
269
|
+
static readonly ALWAYS_ALLOW_ROLES = '@app/authorize/always-allow-roles';
|
|
270
|
+
static enforcerOptions(name: string): string {
|
|
271
|
+
return `@app/authorize/enforcers/${name}/options`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
241
274
|
```
|
|
242
275
|
|
|
243
|
-
|
|
276
|
+
`AuthorizeBindingKeys.enforcerOptions(name)` is called automatically by `AuthorizationEnforcerRegistry.register()` when `options` is provided; `CasbinAuthorizationEnforcer` injects its options from `AuthorizeBindingKeys.enforcerOptions('casbin')`.
|
|
277
|
+
|
|
278
|
+
Source -> [`common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/keys.ts)
|
|
279
|
+
|
|
280
|
+
## Option interfaces
|
|
281
|
+
|
|
282
|
+
### IAuthorizeOptions
|
|
283
|
+
|
|
284
|
+
Global settings, bound before registering `AuthorizeComponent`.
|
|
285
|
+
|
|
286
|
+
| Option | Type | Default | Description |
|
|
287
|
+
|--------|------|---------|-------------|
|
|
288
|
+
| `defaultDecision` | `TAuthorizationDecision` | - | **Required.** Decision applied when the enforcer returns `ABSTAIN` |
|
|
289
|
+
| `alwaysAllowRoles` | `string[]` | `[]` | Roles that bypass all authorization checks (global) |
|
|
290
|
+
| `domainResolver` | `TAuthorizationDomainResolver` | - | Fallback domain resolver used when a route's `spec.domain` is not set. Returns `{ type, id }` or `null` (-> `SYSTEM_WIDE`) |
|
|
244
291
|
|
|
245
292
|
```typescript
|
|
246
|
-
|
|
247
|
-
|
|
293
|
+
interface IAuthorizeOptions {
|
|
294
|
+
defaultDecision: TAuthorizationDecision;
|
|
295
|
+
alwaysAllowRoles?: string[];
|
|
296
|
+
domainResolver?: TAuthorizationDomainResolver;
|
|
297
|
+
}
|
|
298
|
+
```
|
|
248
299
|
|
|
249
|
-
|
|
300
|
+
### ICasbinEnforcerOptions
|
|
250
301
|
|
|
251
|
-
|
|
252
|
-
protected abstract getBindingPrefix(): string;
|
|
302
|
+
Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistry.register()`.
|
|
253
303
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
| Option | Type | Default | Description |
|
|
305
|
+
|--------|------|---------|-------------|
|
|
306
|
+
| `model` | `{ driver: 'file', definition } \| { driver: 'text', definition }` | - | **Required.** Casbin model (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
|
|
307
|
+
| `cached` | `{ use: false } \| (ICasbinEnforcerCachedRedis & { use: true })` | - | **Required.** Caching configuration (Redis-only) |
|
|
308
|
+
| `adapter` | `Adapter` | - | Casbin adapter instance (e.g. `ScopedCasbinAdapter`) |
|
|
309
|
+
| `isScoped` | `boolean` | `false` | Enables the scoped model: 4-token `(sub, dom, obj, act)` requests; auto-registers `keyMatch` on `g` + `objectMatch` on the resource relation |
|
|
310
|
+
| `poolSize` | `number` | `16` | Pooled enforcers (each request enforces on its own borrowed instance) |
|
|
311
|
+
| `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
|
|
312
|
+
| `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | - | Custom (non-scoped) payload normalizer, run before evaluation |
|
|
313
|
+
| `domainMatching` | `{ roleDefinition: string; fn: TCasbinDomainMatchingFunction }` | - | Opt-in domain matching function for the flat model. **Not needed when `isScoped: true`** |
|
|
258
314
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
315
|
+
```typescript
|
|
316
|
+
interface ICasbinEnforcerOptions<E extends Env = Env, TAction = string, TResource = string, TAdapter = Adapter> {
|
|
317
|
+
model: { driver: 'file'; definition: string } | { driver: 'text'; definition: string };
|
|
318
|
+
cached: { use: false } | (ICasbinEnforcerCachedRedis & { use: true });
|
|
319
|
+
adapter?: TAdapter;
|
|
320
|
+
isScoped?: boolean;
|
|
321
|
+
poolSize?: number;
|
|
322
|
+
poolAcquireTimeoutMs?: number;
|
|
323
|
+
normalizePayloadFn?(opts: { user: IAuthUser; action: TAction; resource: TResource; context: TContext<E, string> }): {
|
|
324
|
+
subject: string; resource: string; action: string; domain?: string;
|
|
325
|
+
};
|
|
326
|
+
domainMatching?: { roleDefinition: string; fn: TCasbinDomainMatchingFunction };
|
|
262
327
|
}
|
|
263
328
|
```
|
|
264
329
|
|
|
265
|
-
|
|
330
|
+
> [!NOTE]
|
|
331
|
+
> `cached.options.expiresIn` must be `>= 10_000` ms (`MIN_EXPIRES_IN`). Caching is **Redis-only** - the in-memory driver was removed.
|
|
266
332
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
333
|
+
**Cache configuration (discriminated union):**
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
interface { use: false } // every request rebuilds the user's policy from the datasource
|
|
337
|
+
|
|
338
|
+
interface ICasbinEnforcerCachedRedis {
|
|
339
|
+
driver: 'redis';
|
|
340
|
+
options: {
|
|
341
|
+
connection: IRedisHelper;
|
|
342
|
+
expiresIn: number;
|
|
343
|
+
keyFn: (opts: { user: IAuthorizationUser }) => ValueOrPromise<string>;
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### IAuthorizationSpec (route-level)
|
|
349
|
+
|
|
350
|
+
| Option | Type | Default | Description |
|
|
351
|
+
|--------|------|---------|-------------|
|
|
352
|
+
| `action` | `TAction` | - | **Required.** Action being performed (e.g. `'read'`, `'create'`) |
|
|
353
|
+
| `resource` | `TResource` | - | **Required.** Resource being accessed (e.g. `'Article'`) |
|
|
354
|
+
| `conditions` | `TAuthorizationConditions` | - | Key-value ABAC conditions. Plumbed into `request.conditions`; the built-in Casbin enforcer does not read it - only custom enforcers/voters can |
|
|
355
|
+
| `allowedRoles` | `string[]` | - | Roles that bypass the enforcer for this route |
|
|
356
|
+
| `voters` | `TAuthorizationVoter[]` | - | Custom voter functions for this route |
|
|
357
|
+
| `domain` | `IAuthorizationDomainSource \| TAuthorizationDomainResolver` | - | Per-route domain source for scoped RBAC. Omitted -> falls back to the global `domainResolver`, then `SYSTEM_WIDE` |
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
interface IAuthorizationSpec<E extends Env = Env, TAction = string, TResource = string> {
|
|
361
|
+
action: TAction;
|
|
362
|
+
resource: TResource;
|
|
363
|
+
conditions?: TAuthorizationConditions;
|
|
364
|
+
allowedRoles?: string[];
|
|
365
|
+
voters?: TAuthorizationVoter<E, TAction, TResource>[];
|
|
366
|
+
domain?: IAuthorizationDomainSource | TAuthorizationDomainResolver<E>;
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### IAuthorizationDomainSource / TAuthorizationDomainResolver
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
interface IAuthorizationDomainSource {
|
|
374
|
+
from: 'param' | 'header' | 'query' | 'context';
|
|
375
|
+
key: string;
|
|
376
|
+
type: string; // domain type, e.g. 'Merchant'
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
type TAuthorizationDomainResolver<E extends Env = Env> = (opts: {
|
|
380
|
+
context: TContext<E, string>;
|
|
381
|
+
}) => ValueOrPromise<TNullable<{ type: string; id: IdType }>>;
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
`resolveRequestDomain()` (`common/resolve-request-domain.ts`) turns either shape into a casbin domain string with this precedence: `spec.domain` (resolver, then declarative `readDeclarative()`) -> `IAuthorizeOptions.domainResolver` -> `AuthorizationDomainScopes.SYSTEM_WIDE`. `readDeclarative()` reads `context.req.param/header/query()` for `'param'|'header'|'query'`, or `context.get(key)` for `'context'`.
|
|
385
|
+
|
|
386
|
+
### TAuthorizationConditions / TAuthorizationVoter / TAuthorizeFn
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
type TAuthorizationConditions<KeyType extends string | symbol = string | symbol, ValueType = string | number | boolean | null> =
|
|
390
|
+
Record<KeyType, ValueType>;
|
|
391
|
+
|
|
392
|
+
type TAuthorizationVoter<E extends Env = Env, TAction = string, TResource = string> = (opts: {
|
|
393
|
+
user: IAuthUser; action: TAction; resource: TResource; context: TContext<E, string>;
|
|
394
|
+
}) => ValueOrPromise<TAuthorizationDecision>;
|
|
395
|
+
|
|
396
|
+
type TAuthorizeFn<E extends Env = Env, TAction = string, TResource = string> = (opts: {
|
|
397
|
+
spec: IAuthorizationSpec<E, TAction, TResource>;
|
|
398
|
+
enforcerName?: string;
|
|
399
|
+
}) => MiddlewareHandler;
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### IAuthorizationRequest
|
|
403
|
+
|
|
404
|
+
The request object built by the provider and passed to `evaluate()`.
|
|
274
405
|
|
|
275
|
-
|
|
406
|
+
```typescript
|
|
407
|
+
interface IAuthorizationRequest<TAction = string, TResource = string> {
|
|
408
|
+
action: TAction;
|
|
409
|
+
resource: TResource;
|
|
410
|
+
conditions?: TAuthorizationConditions;
|
|
411
|
+
/** Resolved domain scope: `"<DomainType>_<id>"` (e.g. `"Merchant_7"`) or the `"SYSTEM_WIDE"` sentinel. */
|
|
412
|
+
domain?: string;
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Source -> [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/types.ts)
|
|
417
|
+
|
|
418
|
+
## Constants
|
|
419
|
+
|
|
420
|
+
All constant classes follow the same pattern: static readonly values + `SCHEME_SET: Set<string>` + `isValid(input): boolean`, plus a companion type alias via `TConstValue<typeof ClassName>`.
|
|
421
|
+
|
|
422
|
+
**`Authorization`** - context keys.
|
|
423
|
+
|
|
424
|
+
| Constant | Value | Description |
|
|
425
|
+
|----------|-------|-------------|
|
|
426
|
+
| `Authorization.RULES` | `'authorization.rules'` | Context key for cached rules |
|
|
427
|
+
| `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | Context key to dynamically skip authorization |
|
|
428
|
+
| `Authorization.ENFORCER` | `'authorization.enforcer'` | Binding key prefix for enforcers |
|
|
429
|
+
| `Authorization.DOMAIN` | `'authorization.domain'` | Context key for the resolved request domain scope |
|
|
430
|
+
|
|
431
|
+
**`AuthorizationActions`** - `CREATE` `UPDATE` `DELETE` `EXECUTE` `READ` `WRITE` `MANAGE`. `AuthorizationActions.LATTICE` declares the standard action hierarchy consumed by `AuthorizationPolicyBuilder.actionLattice()`:
|
|
432
|
+
|
|
433
|
+
| `child` | `parent` |
|
|
434
|
+
|---|---|
|
|
435
|
+
| `READ`, `WRITE`, `EXECUTE` | `MANAGE` |
|
|
436
|
+
| `CREATE`, `UPDATE`, `DELETE` | `WRITE` |
|
|
437
|
+
|
|
438
|
+
**`AuthorizationDecisions`** - `ALLOW` `DENY` `ABSTAIN`.
|
|
439
|
+
|
|
440
|
+
| Method | String check | Number check |
|
|
441
|
+
|--------|-------------|--------------|
|
|
442
|
+
| `isAllow(input)` | `input.toLowerCase() === 'allow'` | `input > 0` |
|
|
443
|
+
| `isDeny(input)` | `input.toLowerCase() === 'deny'` | `input < 0` |
|
|
444
|
+
| `isAbstain(input)` | `input.toLowerCase() === 'abstain'` | `input === 0` |
|
|
445
|
+
|
|
446
|
+
**`AuthorizationEnforcerTypes`** - `CASBIN` (`'casbin'`), `CUSTOM` (`'custom'`).
|
|
447
|
+
|
|
448
|
+
**`CasbinEnforcerModelDrivers`** - `FILE` (`'file'`, load from a `.conf` path), `TEXT` (`'text'`, inline string).
|
|
449
|
+
|
|
450
|
+
**`CasbinEnforcerCachedDrivers`** - `REDIS` (`'redis'`) is the only driver; the in-memory driver was removed.
|
|
276
451
|
|
|
277
|
-
|
|
278
|
-
|----------|------------------------------|
|
|
279
|
-
| `AuthenticationStrategyRegistry` | `Authentication.STRATEGY` |
|
|
280
|
-
| `AuthorizationEnforcerRegistry` | `Authorization.ENFORCER` (`'authorization.enforcer'`) |
|
|
452
|
+
**`CasbinDomainMatchingFunctions`** - selectable for `ICasbinEnforcerOptions.domainMatching.fn`, each mapping 1:1 to a Casbin `Util.*Func`, applied to the **domain slot** of a role definition (e.g. `g`).
|
|
281
453
|
|
|
282
|
-
|
|
454
|
+
| Constant | Value | Description |
|
|
455
|
+
|----------|-------|-------------|
|
|
456
|
+
| `KEY_MATCH` | `'keyMatch'` | `*` is the only wildcard; exact compare otherwise (recommended for `Merchant_<uuid>`-style domains) |
|
|
457
|
+
| `KEY_MATCH_2` | `'keyMatch2'` | Adds URL-path `:param` segment matching |
|
|
458
|
+
| `KEY_MATCH_3` | `'keyMatch3'` | Adds `{param}` segment matching |
|
|
459
|
+
| `KEY_MATCH_4` | `'keyMatch4'` | `{param}` with repeated-name equality checks |
|
|
460
|
+
| `REGEX_MATCH` | `'regexMatch'` | Treats the stored/policy value as a full regular expression |
|
|
283
461
|
|
|
284
|
-
|
|
462
|
+
> [!IMPORTANT]
|
|
463
|
+
> Applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side. `keyMatch("Merchant_X", "*") === true`, `keyMatch("Merchant_X", "Merchant_X") === true`, `keyMatch("Merchant_X", "Merchant_Y") === false`.
|
|
285
464
|
|
|
286
|
-
|
|
465
|
+
**`CasbinRuleVariants`** - the Casbin line prefixes declared by the scoped model, numbered in request-tuple order (`sub -> dom -> obj -> act`).
|
|
287
466
|
|
|
467
|
+
| Constant | Value | Relation |
|
|
468
|
+
|----------|-------|----------|
|
|
469
|
+
| `P` | `'p'` | Permission policy line |
|
|
470
|
+
| `G` | `'g'` | Role membership + role inheritance (the `sub` axis) |
|
|
471
|
+
| `G2` | `'g2'` | User -> domain membership (the `dom` axis) |
|
|
472
|
+
| `G3` | `'g3'` | Domain hierarchy (the `dom` axis) |
|
|
473
|
+
| `G4` | `'g4'` | Resource hierarchy (the `obj` axis, via `objectMatch`) |
|
|
474
|
+
| `G5` | `'g5'` | Action hierarchy (the `act` axis) |
|
|
475
|
+
|
|
476
|
+
**`AuthorizationPolicyVariants`** - the DB `variant` discriminator stored on each `PolicyDefinition` row (the kind of "edge"). Each entry carries `action` (the DB value) and `rule` (the Casbin prefix `ScopedCasbinAdapter` emits for it).
|
|
477
|
+
|
|
478
|
+
| Variant | `action` (DB) | `rule` | Meaning |
|
|
479
|
+
|---------|---------------|--------|---------|
|
|
480
|
+
| `GRANT` | `'grant'` | `p` | Give a permission to a User or Role |
|
|
481
|
+
| `ASSIGN_ROLE` | `'assign_role'` | `g` | Give a User a Role (optionally domain-scoped) |
|
|
482
|
+
| `ROLE_INHERITS` | `'role_inherits'` | `g` | Role inherits another Role |
|
|
483
|
+
| `JOIN_DOMAIN` | `'join_domain'` | `g2` | User is a member of a Domain |
|
|
484
|
+
| `DOMAIN_INHERITS` | `'domain_inherits'` | `g3` | Domain nested under a parent Domain |
|
|
485
|
+
| `RESOURCE_INHERITS` | `'resource_inherits'` | `g4` | Resource nested under a broader Resource |
|
|
486
|
+
| `ACTION_INHERITS` | `'action_inherits'` | `g5` | Action implied by a broader Action |
|
|
487
|
+
|
|
488
|
+
`isValidAction(input)` / `isValidRule(input)` check membership; `ACTION_SCHEME_SET` / `RULE_SCHEME_SET` hold the sets.
|
|
489
|
+
|
|
490
|
+
**`AuthorizationDomainScopes`** - sentinel domain values on `grant` rows.
|
|
491
|
+
|
|
492
|
+
| Constant | Value | Meaning |
|
|
493
|
+
|----------|-------|---------|
|
|
494
|
+
| `ANY_MEMBER` | `'ANY_MEMBER'` | Applies in every domain the subject joined (checked via `g2`) |
|
|
495
|
+
| `SYSTEM_WIDE` | `'SYSTEM_WIDE'` | Applies system-wide, bypassing membership (super-admin) |
|
|
496
|
+
|
|
497
|
+
**`AuthorizationRoles`** - built-in role identifiers (see [AuthorizationRole](#authorizationrole)).
|
|
498
|
+
|
|
499
|
+
| Constant | Identifier | Priority |
|
|
500
|
+
|----------|------------|----------|
|
|
501
|
+
| `SUPER_ADMIN` | `'999_super-admin'` | 999 |
|
|
502
|
+
| `ADMIN` | `'900_admin'` | 900 |
|
|
503
|
+
| `USER` | `'010_user'` | 10 |
|
|
504
|
+
| `GUEST` | `'001_guest'` | 1 |
|
|
505
|
+
| `UNKNOWN_USER` | `'000_unknown-user'` | 0 |
|
|
506
|
+
|
|
507
|
+
Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/constants.ts)
|
|
508
|
+
|
|
509
|
+
## CASBIN_RBAC_DOMAIN_SCOPED_MODEL
|
|
510
|
+
|
|
511
|
+
The exported `.conf` text for the scoped model - pass it to `ICasbinEnforcerOptions.model` with `driver: CasbinEnforcerModelDrivers.TEXT` and `isScoped: true`.
|
|
512
|
+
|
|
513
|
+
```ini
|
|
514
|
+
[request_definition]
|
|
515
|
+
r = sub, dom, obj, act
|
|
516
|
+
|
|
517
|
+
[policy_definition]
|
|
518
|
+
p = sub, dom, obj, act, eft
|
|
519
|
+
|
|
520
|
+
[role_definition]
|
|
521
|
+
g = _, _, _
|
|
522
|
+
g2 = _, _
|
|
523
|
+
g3 = _, _
|
|
524
|
+
g4 = _, _
|
|
525
|
+
g5 = _, _
|
|
526
|
+
|
|
527
|
+
[policy_effect]
|
|
528
|
+
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
|
|
529
|
+
|
|
530
|
+
[matchers]
|
|
531
|
+
m = g(r.sub, p.sub, r.dom) && (p.dom == "SYSTEM_WIDE" || (p.dom == "ANY_MEMBER" && g2(r.sub, r.dom)) || g3(r.dom, p.dom)) && (objectMatch(r.obj, p.obj) || g4(r.obj, p.obj)) && g5(r.act, p.act)
|
|
288
532
|
```
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
533
|
+
|
|
534
|
+
| Relation | Axis | Meaning |
|
|
535
|
+
|---|---|---|
|
|
536
|
+
| `g` | `sub` | `assign_role` (user -> role) + `role_inherits` (role -> role), domain-aware. Registered with `keyMatch` so a `*` domain on a link matches any request domain |
|
|
537
|
+
| `g2` | `dom` (membership) | `join_domain` - powers the `ANY_MEMBER` grant scope |
|
|
538
|
+
| `g3` | `dom` (nesting) | `domain_inherits`, plus a self-link so an exact domain always matches itself |
|
|
539
|
+
| `g4` | `obj` | `resource_inherits` - explicit non-standard nesting edges; registered with `objectMatch` |
|
|
540
|
+
| `g5` | `act` | `action_inherits`, plus a self-link |
|
|
541
|
+
|
|
542
|
+
**Effect** is casbin's `allow-and-deny` effector: a request needs a matching `allow` AND no matching `deny` - default-DENY, and an explicit `deny` always overrides an `allow`. This is deliberately NOT casbin's `deny-override` effector (`!some(where (p.eft == deny))`), which would be default-ALLOW.
|
|
543
|
+
|
|
544
|
+
**Domain clause**, matched by `p.dom`: `SYSTEM_WIDE` matches every domain (bypasses membership, super-admin); `ANY_MEMBER` matches every domain the subject joined (via `g2`); `<Type>_<id>` matches that domain, or a nested child via `g3`.
|
|
545
|
+
|
|
546
|
+
> [!NOTE]
|
|
547
|
+
> Relies on the default `DefaultRoleManager`'s self-link behavior (`hasLink(name, name) === true`) for `g3`/`g4`/`g5` - a custom role manager must preserve self-links.
|
|
548
|
+
|
|
549
|
+
Source -> [`enforcers/models/rbac-domain.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/models/rbac-domain.model.ts)
|
|
550
|
+
|
|
551
|
+
## AbstractAuthRegistry
|
|
552
|
+
|
|
553
|
+
Shared base for the authentication strategy registry and `AuthorizationEnforcerRegistry`. Provides descriptor storage, binding-key generation, and DI resolution.
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
abstract class AbstractAuthRegistry<TItem> extends BaseHelper {
|
|
557
|
+
protected descriptors: Map<string, TRegistryDescriptor<TItem>>;
|
|
558
|
+
constructor(opts: { scope: string });
|
|
559
|
+
protected abstract getBindingPrefix(): string;
|
|
560
|
+
|
|
561
|
+
getKey(opts: { name: string }): string; // `${prefix}.${name}`
|
|
562
|
+
getDefaultName(): string; // first registered descriptor (Map insertion order)
|
|
563
|
+
reset(): void; // clears the Map
|
|
564
|
+
|
|
565
|
+
protected registerDescriptor(opts: { container: Container; target: TClass<TItem>; name: string }): void;
|
|
566
|
+
protected resolveDescriptor(opts: { name: string }): TItem;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
type TRegistryDescriptor<TItem> = { container: Container; targetClass: TClass<TItem> };
|
|
293
570
|
```
|
|
294
571
|
|
|
295
|
-
|
|
572
|
+
| Method | Description | Throws |
|
|
573
|
+
|--------|-------------|--------|
|
|
574
|
+
| `getKey({ name })` | Builds the binding key | `[getKey] Invalid name` if empty |
|
|
575
|
+
| `getDefaultName()` | First registered descriptor's name | `[ClassName] No items registered` if none |
|
|
576
|
+
| `registerDescriptor(opts)` | Stores the descriptor + binds the class `SINGLETON` in DI | - |
|
|
577
|
+
| `resolveDescriptor({ name })` | Resolves the instance from the DI container | `Descriptor not found: {name}` or `Failed to resolve: {name}` |
|
|
578
|
+
| `reset()` | Clears all descriptors | - |
|
|
579
|
+
|
|
580
|
+
`AuthorizationEnforcerRegistry.getBindingPrefix()` returns `Authorization.ENFORCER` (`'authorization.enforcer'`).
|
|
581
|
+
|
|
582
|
+
Source -> [`base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts)
|
|
583
|
+
|
|
584
|
+
## AuthorizationEnforcerRegistry
|
|
585
|
+
|
|
586
|
+
Singleton, extends `AbstractAuthRegistry<IAuthorizationEnforcer>`.
|
|
296
587
|
|
|
297
588
|
```typescript
|
|
298
589
|
class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationEnforcer> {
|
|
@@ -300,199 +591,115 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
|
|
|
300
591
|
private configuredEnforcers: Set<string>;
|
|
301
592
|
|
|
302
593
|
static getInstance(): AuthorizationEnforcerRegistry;
|
|
303
|
-
override reset(): void;
|
|
594
|
+
override reset(): void; // clears descriptors + configuredEnforcers
|
|
304
595
|
|
|
305
|
-
|
|
596
|
+
register(opts: {
|
|
597
|
+
container: Container;
|
|
598
|
+
enforcers: Array<
|
|
599
|
+
| { enforcer: TClass<IAuthorizationEnforcer>; name: string; type: 'casbin'; options?: ICasbinEnforcerOptions }
|
|
600
|
+
| { enforcer: TClass<IAuthorizationEnforcer>; name: string; type: 'custom'; options?: unknown }
|
|
601
|
+
>;
|
|
602
|
+
}): this;
|
|
306
603
|
|
|
307
|
-
register(opts: { ... }): this;
|
|
308
604
|
hasEnforcers(): boolean;
|
|
309
605
|
getDefaultEnforcerName(): string;
|
|
310
606
|
resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer>;
|
|
311
607
|
resolveOptions(): IAuthorizeOptions | undefined;
|
|
608
|
+
invalidateUserCache(opts: { user: IAuthorizationUser; enforcerName?: string }): Promise<{ invalidatedKeys: number }>;
|
|
609
|
+
rebuildUserCache(opts: { user; enforcerName? }): Promise<{ cacheKey: string; lineCount: number }>;
|
|
312
610
|
}
|
|
313
611
|
```
|
|
314
612
|
|
|
315
|
-
### API
|
|
316
|
-
|
|
317
613
|
| Method | Returns | Description |
|
|
318
614
|
|--------|---------|-------------|
|
|
319
|
-
| `getInstance()` | `AuthorizationEnforcerRegistry` |
|
|
320
|
-
| `register(opts)` | `this` | Registers enforcers with type-safe options
|
|
321
|
-
| `hasEnforcers()` | `boolean` |
|
|
322
|
-
| `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()`
|
|
323
|
-
| `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves
|
|
615
|
+
| `getInstance()` | `AuthorizationEnforcerRegistry` | Singleton instance (created on first call) |
|
|
616
|
+
| `register(opts)` | `this` | Registers enforcers with type-safe options (chainable) |
|
|
617
|
+
| `hasEnforcers()` | `boolean` | `descriptors.size > 0` - used by the middleware to skip authorization when no enforcers exist |
|
|
618
|
+
| `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` |
|
|
619
|
+
| `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves + auto-configures once (`configuredEnforcers` Set) |
|
|
324
620
|
| `resolveOptions()` | `IAuthorizeOptions \| undefined` | Iterates all registered containers looking for `AuthorizeBindingKeys.OPTIONS` |
|
|
325
|
-
| `
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
The `register` method accepts a discriminated union of enforcer descriptors:
|
|
330
|
-
|
|
331
|
-
```typescript
|
|
332
|
-
register(opts: {
|
|
333
|
-
container: Container;
|
|
334
|
-
enforcers: Array<
|
|
335
|
-
| {
|
|
336
|
-
enforcer: TClass<IAuthorizationEnforcer>;
|
|
337
|
-
name: string;
|
|
338
|
-
type: 'casbin';
|
|
339
|
-
options?: ICasbinEnforcerOptions;
|
|
340
|
-
}
|
|
341
|
-
| {
|
|
342
|
-
enforcer: TClass<IAuthorizationEnforcer>;
|
|
343
|
-
name: string;
|
|
344
|
-
type: 'custom';
|
|
345
|
-
options?: unknown;
|
|
346
|
-
}
|
|
347
|
-
>;
|
|
348
|
-
}) => this
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
**Behavior:**
|
|
352
|
-
1. Validates no duplicate names in the batch (across all `enforcers` in this call)
|
|
353
|
-
2. Validates each name is not already registered (against previously registered enforcers)
|
|
354
|
-
3. Calls `registerDescriptor()` -- binds each enforcer class as singleton: `authorization.enforcer.{name}`
|
|
355
|
-
4. If `options` is provided, binds it to `AuthorizeBindingKeys.enforcerOptions(name)` (`@app/authorize/enforcers/{name}/options`)
|
|
621
|
+
| `invalidateUserCache(opts)` | `Promise<{ invalidatedKeys }>` | Drops a user's cached policies - throws if the resolved enforcer lacks the optional method |
|
|
622
|
+
| `rebuildUserCache(opts)` | `Promise<{ cacheKey, lineCount }>` | Drops then immediately re-extracts + re-caches |
|
|
623
|
+
| `reset()` | `void` | Clears descriptors AND `configuredEnforcers` |
|
|
356
624
|
|
|
357
|
-
|
|
358
|
-
> `register()` returns `this`, enabling method chaining. The `type` field provides TypeScript-level type safety for the `options` field -- `type: 'casbin'` constrains `options` to `ICasbinEnforcerOptions`, while `type: 'custom'` allows `unknown`.
|
|
625
|
+
**`register()` behavior:** validates no duplicate names within the call, validates each name is not already registered, binds each class as a singleton at `authorization.enforcer.{name}`, and - if `options` is given - binds it to `AuthorizeBindingKeys.enforcerOptions(name)`.
|
|
359
626
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
The `resolveEnforcer()` method tracks which enforcers have been configured via the `configuredEnforcers: Set<string>`:
|
|
627
|
+
**Configure-once pattern:**
|
|
363
628
|
|
|
364
629
|
```typescript
|
|
365
630
|
async resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer> {
|
|
366
|
-
const enforcer = this.resolveDescriptor(opts);
|
|
367
|
-
|
|
631
|
+
const enforcer = this.resolveDescriptor(opts);
|
|
368
632
|
if (!this.configuredEnforcers.has(opts.name)) {
|
|
369
633
|
await enforcer.configure();
|
|
370
634
|
this.configuredEnforcers.add(opts.name);
|
|
371
635
|
}
|
|
372
|
-
|
|
373
636
|
return enforcer;
|
|
374
637
|
}
|
|
375
638
|
```
|
|
376
639
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
## IAuthorizationEnforcer Interface
|
|
640
|
+
Source -> [`enforcers/enforcer-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/enforcer-registry.ts)
|
|
380
641
|
|
|
381
|
-
|
|
642
|
+
## IAuthorizationEnforcer interface
|
|
382
643
|
|
|
383
644
|
```typescript
|
|
384
645
|
interface IAuthorizationEnforcer<
|
|
385
|
-
E extends Env = Env,
|
|
386
|
-
|
|
387
|
-
TResource = string,
|
|
388
|
-
TRules = unknown,
|
|
389
|
-
TBuildRulesReturn = ValueOrPromise<TRules>,
|
|
390
|
-
TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
|
|
646
|
+
E extends Env = Env, TAction = string, TResource = string, TRules = unknown,
|
|
647
|
+
TBuildRulesReturn = ValueOrPromise<TRules>, TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
|
|
391
648
|
> {
|
|
392
649
|
name: string;
|
|
393
|
-
|
|
394
650
|
configure(): ValueOrPromise<void>;
|
|
651
|
+
buildRules(opts: { user: IAuthorizationUser; context: TContext<E, string> }): TBuildRulesReturn;
|
|
652
|
+
evaluate(opts: { rules: TRules; request: IAuthorizationRequest<TAction, TResource>; context: TContext<E, string> }): TEvaluateReturn;
|
|
395
653
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}): TBuildRulesReturn;
|
|
400
|
-
|
|
401
|
-
evaluate(opts: {
|
|
402
|
-
rules: TRules;
|
|
403
|
-
request: IAuthorizationRequest<TAction, TResource>;
|
|
404
|
-
context: TContext<E, string>;
|
|
405
|
-
}): TEvaluateReturn;
|
|
654
|
+
/** Optional - implemented only by caching enforcers. */
|
|
655
|
+
invalidateUserCache?(opts: { user: IAuthorizationUser }): Promise<{ invalidatedKeys: number }>;
|
|
656
|
+
rebuildUserCache?(opts: { user: IAuthorizationUser }): Promise<{ cacheKey: string; lineCount: number }>;
|
|
406
657
|
}
|
|
407
658
|
```
|
|
408
659
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
| Parameter | Default | Description |
|
|
412
|
-
|-----------|---------|-------------|
|
|
660
|
+
| Generic | Default | Description |
|
|
661
|
+
|---------|---------|--------------|
|
|
413
662
|
| `E` | `Env` | Hono `Env` type for typed context access |
|
|
414
|
-
| `TAction` | `string` | Action type
|
|
415
|
-
| `
|
|
416
|
-
| `TRules` | `unknown` | Rules type produced by `buildRules` and consumed by `evaluate` |
|
|
663
|
+
| `TAction` / `TResource` | `string` | Action / resource type |
|
|
664
|
+
| `TRules` | `unknown` | Rules type produced by `buildRules`, consumed by `evaluate` |
|
|
417
665
|
| `TBuildRulesReturn` | `ValueOrPromise<TRules>` | Return type of `buildRules` |
|
|
418
666
|
| `TEvaluateReturn` | `ValueOrPromise<TAuthorizationDecision>` | Return type of `evaluate` |
|
|
419
667
|
|
|
420
|
-
### TRules per Enforcer
|
|
421
|
-
|
|
422
|
-
| Enforcer | TRules | Description |
|
|
423
|
-
|----------|--------|-------------|
|
|
424
|
-
| `CasbinAuthorizationEnforcer` | `ICasbinRules` | `{ user, lines }` - the user plus their resolved Casbin policy lines (loaded into a pooled enforcer at evaluate time) |
|
|
425
|
-
| Custom | Any type | Your custom rules structure |
|
|
426
|
-
|
|
427
|
-
### Method Contracts
|
|
428
|
-
|
|
429
668
|
| Method | Input | Returns | Called by |
|
|
430
|
-
|
|
431
|
-
| `configure()` |
|
|
432
|
-
| `buildRules` | `{ user, context }` | `TRules` | Provider
|
|
433
|
-
| `evaluate` | `{ rules, request, context }` | `TAuthorizationDecision` | Provider
|
|
434
|
-
|
|
435
|
-
## IAuthorizationRequest Interface
|
|
436
|
-
|
|
437
|
-
The request object passed to `evaluate()`:
|
|
669
|
+
|--------|-------|---------|-----------|
|
|
670
|
+
| `configure()` | - | `void` | Registry, on first `resolveEnforcer()` |
|
|
671
|
+
| `buildRules` | `{ user, context }` | `TRules` | Provider, pipeline step 6 |
|
|
672
|
+
| `evaluate` | `{ rules, request, context }` | `TAuthorizationDecision` | Provider, pipeline step 7 |
|
|
438
673
|
|
|
439
|
-
|
|
440
|
-
interface IAuthorizationRequest<TAction = string, TResource = string> {
|
|
441
|
-
action: TAction;
|
|
442
|
-
resource: TResource;
|
|
443
|
-
conditions?: TAuthorizationConditions;
|
|
444
|
-
/** Resolved domain scope: `"<DomainType>_<id>"` (e.g. `"Merchant_7"`) or the `"SYSTEM_WIDE"` sentinel. */
|
|
445
|
-
domain?: string;
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
| Field | Type | Description |
|
|
450
|
-
|-------|------|-------------|
|
|
451
|
-
| `action` | `TAction` | Action being checked (e.g., `'read'`, `'create'`) |
|
|
452
|
-
| `resource` | `TResource` | Resource being accessed (e.g., `'Article'`) |
|
|
453
|
-
| `conditions` | `TAuthorizationConditions` | Optional key-value conditions for ABAC |
|
|
454
|
-
|
|
455
|
-
## Casbin Enforcer
|
|
674
|
+
`invalidateUserCache`/`rebuildUserCache` are feature-detected at runtime (`typeof enforcer.invalidateUserCache === 'function'`) - only `CasbinAuthorizationEnforcer` with a Redis cache implements them.
|
|
456
675
|
|
|
457
|
-
|
|
676
|
+
## CasbinAuthorizationEnforcer
|
|
458
677
|
|
|
459
|
-
|
|
678
|
+
Wraps the `casbin` library (optional peer dependency). Each request evaluates on its **own** enforcer borrowed from a `BasePoolHelper<Enforcer>` - the adapter (DB load) only runs on a throwaway enforcer to build a user's lines (cached in Redis if configured); every request then enforces on a pooled enforcer freshly loaded with those lines. This isolates concurrency and keeps the DB out of the hot path.
|
|
460
679
|
|
|
461
680
|
```typescript
|
|
462
|
-
class CasbinAuthorizationEnforcer<
|
|
463
|
-
E extends Env = Env,
|
|
464
|
-
TAction extends string = string,
|
|
465
|
-
TResource extends string = string,
|
|
466
|
-
>
|
|
681
|
+
class CasbinAuthorizationEnforcer<E extends Env = Env, TAction extends string = string, TResource extends string = string>
|
|
467
682
|
extends BaseHelper
|
|
468
683
|
implements IAuthorizationEnforcer<E, TAction, TResource, ICasbinRules>
|
|
469
684
|
{
|
|
470
685
|
name = 'CasbinAuthorizationEnforcer';
|
|
471
|
-
|
|
472
686
|
private readonly MIN_EXPIRES_IN = 10_000;
|
|
473
|
-
private pool: TNullable<BasePoolHelper<CasbinEnforcerType>>;
|
|
474
|
-
private helper: TNullable<typeof CasbinHelper>;
|
|
687
|
+
private pool: TNullable<BasePoolHelper<CasbinEnforcerType>>;
|
|
688
|
+
private helper: TNullable<typeof CasbinHelper>; // casbin.Helper (loadPolicyLine)
|
|
475
689
|
private readonly pendingLineFetches = new Map<string, Promise<string[]>>(); // single-flight
|
|
476
|
-
private resolvedPayloadFn: TNullable<TNormalizePayloadFn>;
|
|
690
|
+
private resolvedPayloadFn: TNullable<TNormalizePayloadFn>; // memoized in configure()
|
|
477
691
|
|
|
478
|
-
constructor(
|
|
479
|
-
@inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') })
|
|
480
|
-
private options: ICasbinEnforcerOptions<E, TAction, TResource>,
|
|
481
|
-
);
|
|
692
|
+
constructor(@inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') }) private options: ICasbinEnforcerOptions<E, TAction, TResource>);
|
|
482
693
|
|
|
483
|
-
// Lifecycle
|
|
484
694
|
async configure(): Promise<void>;
|
|
485
695
|
destroy(): void;
|
|
486
696
|
|
|
487
|
-
//
|
|
488
|
-
async buildRules(opts: { user; context }): Promise<ICasbinRules>; // { user, lines }
|
|
697
|
+
async buildRules(opts: { user; context }): Promise<ICasbinRules>; // { user, lines }
|
|
489
698
|
async evaluate(opts: { rules; request; context }): Promise<TAuthorizationDecision>;
|
|
490
699
|
|
|
491
|
-
// Optional cache management (Redis only)
|
|
492
700
|
async invalidateUserCache(opts: { user }): Promise<{ invalidatedKeys: number }>;
|
|
493
701
|
async rebuildUserCache(opts: { user }): Promise<{ cacheKey: string; lineCount: number }>;
|
|
494
702
|
|
|
495
|
-
// Protected internals
|
|
496
703
|
protected async registerMatchers(opts: { enforcer; casbin }): Promise<void>;
|
|
497
704
|
protected assertMatcherCompilesSync(opts: { enforcer }): void;
|
|
498
705
|
protected resolveModel(opts): Model;
|
|
@@ -505,205 +712,83 @@ class CasbinAuthorizationEnforcer<
|
|
|
505
712
|
}
|
|
506
713
|
```
|
|
507
714
|
|
|
508
|
-
> **Architecture in one line:** the adapter (DB load) runs only on a *throwaway* enforcer to build a
|
|
509
|
-
> user's lines (cached in Redis); every request then enforces on a *pooled* enforcer freshly loaded
|
|
510
|
-
> with those lines. This isolates concurrency and keeps the DB out of the hot path.
|
|
511
|
-
|
|
512
|
-
### Constructor
|
|
513
|
-
|
|
514
|
-
Injects `ICasbinEnforcerOptions` from the DI container using the binding key `AuthorizeBindingKeys.enforcerOptions('casbin')`.
|
|
515
|
-
|
|
516
715
|
### configure()
|
|
517
716
|
|
|
518
|
-
Called once by the registry on first use
|
|
717
|
+
Called once by the registry on first use:
|
|
519
718
|
|
|
520
|
-
1. Dynamically imports `casbin` - throws
|
|
521
|
-
2. Validates `options.model`
|
|
522
|
-
3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`).
|
|
719
|
+
1. Dynamically imports `casbin` - throws if not installed.
|
|
720
|
+
2. Validates `options.model` is present.
|
|
721
|
+
3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`; the latter is `undefined` unless `isScoped`).
|
|
523
722
|
4. If `cached.use`, validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms).
|
|
524
|
-
5. Builds a
|
|
723
|
+
5. Builds a `BasePoolHelper<Enforcer>` (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (no DB load at warmup), then `registerMatchers()` and `assertMatcherCompilesSync()` run on it.
|
|
525
724
|
6. `await pool.warmup()` - pre-creates the enforcers.
|
|
526
725
|
|
|
527
|
-
`registerMatchers()` - when `isScoped`, registers `keyMatch` as the domain matching func on `g`, adds `objectMatch` as a function, and registers it as the matching func on the resource relation (`g4`). When `domainMatching` is set (
|
|
528
|
-
|
|
529
|
-
`assertMatcherCompilesSync()` - a boot-time smoke test: forces casbin's lazy matcher compile by running one dummy `enforceSync` (4 args when scoped/`normalizePayloadFn`, else 3), so a malformed matcher, an unregistered function, or an arity mismatch fails at warmup instead of on the first real request.
|
|
726
|
+
`registerMatchers()` - when `isScoped`, registers `keyMatch` as the domain matching func on `g`, adds `objectMatch` as a function, and registers it as the matching func on the resource relation (`g4`). When `domainMatching` is set (flat model), registers the chosen `Util.*Func` on the named role definition. Always finishes with `buildRoleLinks()`.
|
|
530
727
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
`this.pool?.destroy()` - drains and disposes the pooled enforcers.
|
|
728
|
+
`assertMatcherCompilesSync()` - a boot-time smoke test: forces casbin's lazy matcher compile with one dummy `enforceSync` (4 args when scoped/`normalizePayloadFn`, else 3), so a malformed matcher, an unregistered function, or an arity mismatch fails at warmup instead of on the first real request.
|
|
534
729
|
|
|
535
730
|
### buildRules()
|
|
536
731
|
|
|
537
|
-
Returns `ICasbinRules
|
|
538
|
-
|
|
539
|
-
```mermaid
|
|
540
|
-
flowchart TD
|
|
541
|
-
Start([buildRules]) --> Check{cached.use?}
|
|
542
|
-
Check -->|false| Extract["extractUserLines(user)"]
|
|
543
|
-
Check -->|true| Redis["fetchLinesWithRedisCache(user, cached)"]
|
|
544
|
-
Redis --> Hit{Redis hit?}
|
|
545
|
-
Hit -->|Yes| Lines([lines])
|
|
546
|
-
Hit -->|No| SF["single-flight → extractUserLines + SET PX"]
|
|
547
|
-
SF --> Lines
|
|
548
|
-
Extract --> Lines
|
|
549
|
-
Lines --> Return(["return { user, lines }"])
|
|
550
|
-
```
|
|
732
|
+
Returns `ICasbinRules = { user, lines }` - the user's complete Casbin policy lines.
|
|
551
733
|
|
|
552
|
-
-
|
|
553
|
-
|
|
554
|
-
p-type and g-type rule back into lines. This throwaway enforcer never serves a request - that is the
|
|
555
|
-
anti-poisoning guarantee.
|
|
556
|
-
- **`fetchLinesWithRedisCache`** returns cached lines on hit (Redis owns expiry via `PX`). On miss it
|
|
557
|
-
dedups concurrent misses through `pendingLineFetches` (single-flight), extracts once, and writes the
|
|
558
|
-
lines back to Redis. A corrupt entry is logged and discarded (refetch), never a 500.
|
|
734
|
+
- `extractUserLines(user)` builds a fresh, isolated enforcer *with the adapter*, calls `adapter.loadFilteredPolicy({ principal: { type, id } })`, then `extractLinesFrom()` serializes every p-type and g-type rule (not just `p`/`g` - every `p*`/`g*` the model declares, so the scoped model's `g2`-`g5` hierarchies are included) back into lines.
|
|
735
|
+
- `fetchLinesWithRedisCache` returns cached lines on a hit (Redis owns expiry via `PX`). On a miss it dedups concurrent misses through `pendingLineFetches` (single-flight), extracts once, and writes the lines back to Redis. A corrupt entry is logged and discarded (refetch), never a 500.
|
|
559
736
|
|
|
560
737
|
### evaluate()
|
|
561
738
|
|
|
562
|
-
Borrows an enforcer from the pool and evaluates
|
|
739
|
+
Borrows an enforcer from the pool and evaluates atomically inside `pool.use`:
|
|
563
740
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
Norm --> Dom["domain = normalized.domain ?? request.domain ?? (isScoped ? SYSTEM_WIDE : undefined)"]
|
|
570
|
-
Dom --> Enf["enforceWithExplain(vals)"]
|
|
571
|
-
Enf --> Dec{allowed?}
|
|
572
|
-
Dec -->|Yes| Allow([ALLOW])
|
|
573
|
-
Dec -->|No| Deny([DENY])
|
|
574
|
-
```
|
|
741
|
+
1. `loadPolicyLinesIntoModel(enforcer, rules.lines)` - `clearPolicy()` + `loadPolicyLine()` per line + `buildRoleLinks()`.
|
|
742
|
+
2. `normalizePayloadFn(user, action, resource, context)` normalizes the payload.
|
|
743
|
+
3. `domain = normalized.domain ?? request.domain ?? (isScoped ? SYSTEM_WIDE : undefined)`.
|
|
744
|
+
4. `vals` is `[subject, domain, resource, action]` when a domain is present, else `[subject, resource, action]`.
|
|
745
|
+
5. `enforceWithExplain(vals)` runs `enforceExSync` and logs the deciding policy on a DENY.
|
|
575
746
|
|
|
576
|
-
|
|
577
|
-
- On any error inside `pool.use`, the pool **destroys** the borrowed enforcer (fail-closed); a fresh one is created on demand.
|
|
578
|
-
- `enforceWithExplain` uses `enforceExSync` to also log the deciding policy on a DENY.
|
|
747
|
+
On any error inside `pool.use`, the pool **destroys** the borrowed enforcer (fail-closed); a fresh one is created on demand.
|
|
579
748
|
|
|
580
749
|
### invalidateUserCache() / rebuildUserCache()
|
|
581
750
|
|
|
582
|
-
Redis-only (throw if caching is disabled). `invalidateUserCache` deletes the user's shared Redis key
|
|
583
|
-
(next request rebuilds lazily). `rebuildUserCache` deletes then immediately re-extracts (on a throwaway
|
|
584
|
-
enforcer) and re-caches. Because the key is shared in Redis, a single call is correct across instances.
|
|
585
|
-
|
|
586
|
-
### Protected Methods
|
|
587
|
-
|
|
588
|
-
| Method | Output | Description |
|
|
589
|
-
|--------|--------|-------------|
|
|
590
|
-
| `registerMatchers` | `void` | Registers domain/resource matching funcs (+ `buildRoleLinks`); scoped vs `domainMatching` |
|
|
591
|
-
| `assertMatcherCompilesSync` | `void` | Boot-time matcher smoke test (forces lazy compile) |
|
|
592
|
-
| `resolveModel` | `Model` | Resolves casbin model from `file` or `text` driver |
|
|
593
|
-
| `validateExpiresIn` | `void` | Throws if `expiresIn < MIN_EXPIRES_IN` |
|
|
594
|
-
| `fetchLinesWithRedisCache` | `string[]` | Redis read → single-flight extract+write on miss |
|
|
595
|
-
| `extractUserLines` | `string[]` | Throwaway enforcer + adapter `loadFilteredPolicy` → `extractLinesFrom` |
|
|
596
|
-
| `extractLinesFrom` | `string[]` | Serializes every p-type and g-type rule into lines |
|
|
597
|
-
| `loadPolicyLinesIntoModel` | `void` | `clearPolicy` + `loadPolicyLine` per line + `buildRoleLinks` |
|
|
598
|
-
| `enforceWithExplain` | `boolean` | `enforceExSync`; logs the deciding rule on DENY |
|
|
599
|
-
|
|
600
|
-
#### extractLinesFrom()
|
|
751
|
+
Redis-only (throw if caching is disabled). `invalidateUserCache` deletes the user's shared Redis key (next request rebuilds lazily). `rebuildUserCache` deletes then immediately re-extracts (on a throwaway enforcer) and re-caches. Because the key is shared in Redis, a single call is correct across instances.
|
|
601
752
|
|
|
602
|
-
|
|
603
|
-
for the scoped model (`g2`…`g5`):
|
|
604
|
-
|
|
605
|
-
```typescript
|
|
606
|
-
const model = enforcer.getModel();
|
|
607
|
-
const lines: string[] = [];
|
|
608
|
-
|
|
609
|
-
for (const ptype of model.model.get(CasbinRuleVariants.P)?.keys() ?? []) {
|
|
610
|
-
for (const rule of await enforcer.getNamedPolicy(ptype)) lines.push([ptype, ...rule].join(', '));
|
|
611
|
-
}
|
|
612
|
-
for (const gtype of model.model.get(CasbinRuleVariants.G)?.keys() ?? []) {
|
|
613
|
-
for (const rule of await enforcer.getNamedGroupingPolicy(gtype)) lines.push([gtype, ...rule].join(', '));
|
|
614
|
-
}
|
|
615
|
-
return lines;
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
#### loadPolicyLinesIntoModel()
|
|
619
|
-
|
|
620
|
-
Atomically resets a borrowed enforcer's model to exactly `lines`:
|
|
621
|
-
|
|
622
|
-
```typescript
|
|
623
|
-
const model = opts.enforcer.getModel();
|
|
624
|
-
model.clearPolicy();
|
|
625
|
-
for (const line of opts.lines) {
|
|
626
|
-
this.helper.loadPolicyLine(line, model);
|
|
627
|
-
}
|
|
628
|
-
await opts.enforcer.buildRoleLinks();
|
|
629
|
-
```
|
|
753
|
+
Source -> [`enforcers/casbin.enforcer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/casbin.enforcer.ts)
|
|
630
754
|
|
|
631
755
|
## BaseFilteredAdapter
|
|
632
756
|
|
|
633
|
-
Thin read-only base for casbin `FilteredAdapter`s backed by a datasource.
|
|
634
|
-
every filtered adapter repeats - datasource/connector plumbing, the `isFiltered() === true` flag, the
|
|
635
|
-
no-op write methods, and a `loadLines` helper. A subclass implements only `loadFilteredPolicy`: query
|
|
636
|
-
the store for ONE principal's policies and turn them into casbin lines.
|
|
637
|
-
|
|
638
|
-
### Class
|
|
757
|
+
Thin read-only base for casbin `FilteredAdapter`s backed by a datasource. Owns the boilerplate every filtered adapter repeats; a subclass implements only `loadFilteredPolicy`.
|
|
639
758
|
|
|
640
759
|
```typescript
|
|
641
|
-
abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter>
|
|
642
|
-
extends BaseHelper
|
|
643
|
-
implements FilteredAdapter
|
|
644
|
-
{
|
|
760
|
+
abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter> extends BaseHelper implements FilteredAdapter {
|
|
645
761
|
protected readonly dataSource: ICasbinPolicySource;
|
|
646
762
|
protected get connector(): TCasbinPolicyConnector;
|
|
647
|
-
|
|
648
763
|
constructor(opts: { scope: string; dataSource: ICasbinPolicySource });
|
|
649
764
|
|
|
650
|
-
// Subclasses implement ONLY this:
|
|
651
765
|
abstract loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
|
|
652
|
-
|
|
653
766
|
isFiltered(): boolean; // always true
|
|
654
767
|
|
|
655
|
-
//
|
|
768
|
+
// Read-only adapter - no-op write methods
|
|
656
769
|
async loadPolicy(): Promise<void>;
|
|
657
|
-
async savePolicy(): Promise<boolean>;
|
|
770
|
+
async savePolicy(): Promise<boolean>; // returns true
|
|
658
771
|
async addPolicy(): Promise<void>;
|
|
659
772
|
async removePolicy(): Promise<void>;
|
|
660
773
|
async removeFilteredPolicy(): Promise<void>;
|
|
661
774
|
|
|
662
|
-
|
|
775
|
+
protected async query<TRow>(opts: { statement: SQL }): Promise<TRow[]>;
|
|
663
776
|
protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void>;
|
|
664
777
|
}
|
|
665
778
|
```
|
|
666
779
|
|
|
667
|
-
### Generic Parameters
|
|
668
|
-
|
|
669
|
-
| Parameter | Default | Description |
|
|
670
|
-
|-----------|---------|-------------|
|
|
671
|
-
| `TFilter` | `ICasbinPolicyFilter` | Filter shape passed to `loadFilteredPolicy`. Subclasses may narrow it (e.g. `IScopedCasbinPolicyFilter`) |
|
|
672
|
-
|
|
673
|
-
### ICasbinPolicyFilter
|
|
674
|
-
|
|
675
|
-
The default filter: which principal's policies to load. Subclasses may narrow it.
|
|
676
|
-
|
|
677
780
|
```typescript
|
|
678
|
-
interface ICasbinPolicyFilter {
|
|
679
|
-
principal: { type: string; id: IdType };
|
|
680
|
-
}
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
### ICasbinPolicySource
|
|
781
|
+
interface ICasbinPolicyFilter { principal: { type: string; id: IdType }; }
|
|
684
782
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
```typescript
|
|
690
|
-
interface ICasbinPolicySource {
|
|
691
|
-
connector: TCasbinPolicyConnector;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
type TCasbinPolicyConnector = ReturnType<
|
|
695
|
-
typeof drizzle<Record<string, AnyType>, NodePgClient>
|
|
696
|
-
>;
|
|
783
|
+
/** Minimal contract - NOT the framework's general IDataSource. Any Drizzle-backed datasource satisfies it. */
|
|
784
|
+
interface ICasbinPolicySource { connector: TCasbinPolicyConnector; }
|
|
785
|
+
type TCasbinPolicyConnector = PgDatabase<PgQueryResultHKT, Record<string, AnyType>>;
|
|
697
786
|
```
|
|
698
787
|
|
|
699
788
|
> [!NOTE]
|
|
700
|
-
> Components that only need query execution depend on this minimal local contract rather than a
|
|
701
|
-
> connector class -- keeps the casbin adapters decoupled from the full datasource surface.
|
|
702
|
-
|
|
703
|
-
### loadLines()
|
|
789
|
+
> Components that only need query execution depend on this minimal local contract (`ICasbinPolicySource`) rather than a connector class - `src/components/**` never imports `@/connectors/postgres` for this.
|
|
704
790
|
|
|
705
|
-
|
|
706
|
-
their casbin lines:
|
|
791
|
+
`query()` runs a raw `SQL` statement and normalizes the result to a row array - Drizzle's `execute()` shape differs per driver (node-postgres yields `{ rows }`, postgres-js yields the row list itself), so subclasses must call `query()` rather than read `.rows` directly. `loadLines()` is the other orchestration helper - subclasses call it after assembling their own casbin lines:
|
|
707
792
|
|
|
708
793
|
```typescript
|
|
709
794
|
protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void> {
|
|
@@ -714,42 +799,31 @@ protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void
|
|
|
714
799
|
}
|
|
715
800
|
```
|
|
716
801
|
|
|
717
|
-
|
|
718
|
-
own queries and line construction (see `ScopedCasbinAdapter` below for the reference implementation).
|
|
802
|
+
Source -> [`adapters/base-filtered.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/base-filtered.ts), [`adapters/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/types.ts)
|
|
719
803
|
|
|
720
804
|
## ScopedCasbinAdapter
|
|
721
805
|
|
|
722
|
-
The generic, read-only `FilteredAdapter` for the scoped RBAC model.
|
|
723
|
-
plus the **shared structural hierarchy** from a single `PolicyDefinition` edge table (joined to
|
|
724
|
-
`Permission` for codes) and emits casbin lines. No subclassing - configure it with `IScopedCasbinEntities`.
|
|
725
|
-
|
|
726
|
-
### Class
|
|
806
|
+
The generic, read-only `FilteredAdapter` for the scoped RBAC model. Reads **one principal's edges** plus the **shared structural hierarchy** from a single `PolicyDefinition` table (joined to `Permission` for codes) and emits casbin lines. No subclassing - configure it with `IScopedCasbinEntities`.
|
|
727
807
|
|
|
728
808
|
```typescript
|
|
729
809
|
class ScopedCasbinAdapter extends BaseFilteredAdapter<IScopedCasbinPolicyFilter> {
|
|
730
810
|
protected readonly entities: IScopedCasbinEntities;
|
|
731
|
-
|
|
732
811
|
constructor(opts: { dataSource: ICasbinPolicySource; entities: IScopedCasbinEntities });
|
|
733
812
|
|
|
734
813
|
async loadFilteredPolicy(model: Model, filter: IScopedCasbinPolicyFilter): Promise<void>;
|
|
735
814
|
|
|
736
|
-
//
|
|
737
|
-
protected
|
|
738
|
-
protected
|
|
739
|
-
protected
|
|
740
|
-
//
|
|
741
|
-
protected
|
|
742
|
-
protected
|
|
743
|
-
protected
|
|
744
|
-
protected
|
|
745
|
-
protected queryActionInherits(): Promise<string[]>; // → g5
|
|
746
|
-
// Role closure (BFS over role_inherits)
|
|
747
|
-
protected expandRoleClosure(opts: { role: { ids: IdType[]; edges: string[] } }): IdType[];
|
|
815
|
+
protected queryRoleAssignments(opts): Promise<{ lines: string[]; roleIds: IdType[] }>; // -> g
|
|
816
|
+
protected queryMemberships(opts): Promise<string[]>; // -> g2
|
|
817
|
+
protected queryGrants(opts): Promise<string[]>; // -> p
|
|
818
|
+
protected loadStructuralTrees(): Promise<string[]>; // role(g)/domain(g3)/resource(g4)/action(g5)
|
|
819
|
+
protected queryRoleInherits(): Promise<string[]>; // -> g
|
|
820
|
+
protected queryDomainInherits(): Promise<string[]>; // -> g3
|
|
821
|
+
protected queryResourceInherits(): Promise<string[]>; // -> g4
|
|
822
|
+
protected queryActionInherits(): Promise<string[]>; // -> g5
|
|
823
|
+
protected expandRoleClosure(opts: { role: { ids: IdType[]; edges: string[] } }): IdType[]; // BFS over role_inherits
|
|
748
824
|
}
|
|
749
825
|
```
|
|
750
826
|
|
|
751
|
-
### IScopedCasbinEntities
|
|
752
|
-
|
|
753
827
|
```typescript
|
|
754
828
|
interface IScopedCasbinTable { tableName: string; schemaName?: string; }
|
|
755
829
|
|
|
@@ -760,45 +834,18 @@ interface IScopedCasbinEntities {
|
|
|
760
834
|
domainTypes: string[]; // e.g. ['Merchant', 'Organizer']
|
|
761
835
|
softDelete?: { use: false } | { use: true; columnName: string };
|
|
762
836
|
}
|
|
763
|
-
```
|
|
764
|
-
|
|
765
|
-
### IScopedCasbinPolicyFilter
|
|
766
837
|
|
|
767
|
-
|
|
768
|
-
interface IScopedCasbinPolicyFilter {
|
|
769
|
-
principal: { type: string; id: IdType };
|
|
770
|
-
}
|
|
838
|
+
interface IScopedCasbinPolicyFilter { principal: { type: string; id: IdType }; }
|
|
771
839
|
```
|
|
772
840
|
|
|
773
|
-
|
|
841
|
+
**`loadFilteredPolicy()` - two waves:**
|
|
774
842
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
W1 --> Closure["expandRoleClosure(assigned roleIds, role_inherits edges)"]
|
|
780
|
-
Closure --> W2["Wave 2: queryGrants[roleClosure] (p)"]
|
|
781
|
-
W2 --> Load["loadLines(model, all lines)"]
|
|
782
|
-
```
|
|
783
|
-
|
|
784
|
-
1. **Wave 1 (parallel):** the principal's own edges - role assignments (`g`), domain memberships
|
|
785
|
-
(`g2`), direct grants (`p`) - plus the shared structural trees (`role_inherits` → `g`,
|
|
786
|
-
`domain_inherits` → `g3`, `resource_inherits` → `g4`, `action_inherits` → `g5`).
|
|
787
|
-
2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to
|
|
788
|
-
collect the assigned roles + all transitive parents.
|
|
789
|
-
3. **Wave 2:** fetch the grants (`p`) of every role in the closure, so a user inherits the permissions
|
|
790
|
-
of parent roles.
|
|
791
|
-
4. All lines are loaded via `loadLines`.
|
|
792
|
-
|
|
793
|
-
### SQL notes
|
|
843
|
+
1. **Wave 1 (parallel):** the principal's own edges - role assignments (`g`), domain memberships (`g2`), direct grants (`p`) - plus the shared structural trees (`role_inherits` -> `g`, `domain_inherits` -> `g3`, `resource_inherits` -> `g4`, `action_inherits` -> `g5`).
|
|
844
|
+
2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to collect the assigned roles plus all transitive parents.
|
|
845
|
+
3. **Wave 2:** fetches the grants (`p`) of every role in the closure, so a user inherits parent-role permissions.
|
|
846
|
+
4. All lines load via `loadLines`.
|
|
794
847
|
|
|
795
|
-
All queries use the `sql` template tag from `drizzle-orm
|
|
796
|
-
`sql.identifier` (injection-safe); interpolated values (the `variant` discriminator from
|
|
797
|
-
`AuthorizationPolicyVariants.*.action`, ids, types) are bound parameters. The soft-delete clause
|
|
798
|
-
(`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true. `queryGrants`
|
|
799
|
-
short-circuits to `[]` when given no subject ids (no DB round-trip).
|
|
800
|
-
|
|
801
|
-
### Usage Example
|
|
848
|
+
All queries use the `sql` template tag from `drizzle-orm`; tables are schema-qualified via `sql.identifier` (injection-safe), interpolated values are bound parameters. The soft-delete clause (`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true. `queryGrants` short-circuits to `[]` when given no subject ids (no DB round-trip).
|
|
802
849
|
|
|
803
850
|
```typescript
|
|
804
851
|
import { ScopedCasbinAdapter } from '@venizia/ignis';
|
|
@@ -815,294 +862,349 @@ const adapter = new ScopedCasbinAdapter({
|
|
|
815
862
|
});
|
|
816
863
|
```
|
|
817
864
|
|
|
818
|
-
|
|
865
|
+
Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
|
|
819
866
|
|
|
820
|
-
|
|
867
|
+
## objectMatch
|
|
821
868
|
|
|
822
|
-
|
|
869
|
+
Resource-hierarchy matcher registered by the scoped model (both as a function `objectMatch(r.obj, p.obj)` in the matcher expression AND as the `g4` matching func). Decides whether a requested resource falls under a granted one, WITHOUT needing a stored edge for the "standard" case (dotted nesting is derived from the code itself):
|
|
823
870
|
|
|
824
871
|
```typescript
|
|
825
|
-
|
|
826
|
-
|
|
872
|
+
const objectMatch = (requested: string, granted: string): boolean => {
|
|
873
|
+
if (granted === '*') return true;
|
|
874
|
+
if (requested === granted) return true;
|
|
875
|
+
return requested.startsWith(`${granted}.`);
|
|
876
|
+
};
|
|
877
|
+
```
|
|
827
878
|
|
|
828
|
-
|
|
879
|
+
`objectMatch('Activation.findById', 'Activation')` -> `true` (dotted nesting - endpoint under subject). `objectMatch('OrderItem', 'Order')` -> `false` unless a `resource_inherits` (`g4`) edge links them - non-standard nesting always needs an explicit edge.
|
|
880
|
+
|
|
881
|
+
Source -> [`common/object-match.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/object-match.ts)
|
|
882
|
+
|
|
883
|
+
## AuthorizationProvider
|
|
829
884
|
|
|
830
|
-
|
|
831
|
-
spec: IAuthorizationSpec;
|
|
832
|
-
enforcerName?: string;
|
|
833
|
-
}): MiddlewareHandler;
|
|
885
|
+
Implements `IProvider<TAuthorizeFn>` and produces the middleware factory.
|
|
834
886
|
|
|
887
|
+
```typescript
|
|
888
|
+
class AuthorizationProvider extends BaseHelper implements IProvider<TAuthorizeFn> {
|
|
889
|
+
constructor();
|
|
890
|
+
value(): TAuthorizeFn;
|
|
891
|
+
private createAuthorizeMiddleware(opts: { spec: IAuthorizationSpec; enforcerName?: string }): MiddlewareHandler;
|
|
835
892
|
private extractUserRoles(opts: { user: IAuthUser }): string[];
|
|
836
893
|
}
|
|
837
894
|
```
|
|
838
895
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
The `createAuthorizeMiddleware` method creates a Hono middleware with this evaluation order:
|
|
896
|
+
**The 7-step pipeline** (`createAuthorizeMiddleware`):
|
|
842
897
|
|
|
843
898
|
```typescript
|
|
844
|
-
//
|
|
845
|
-
|
|
846
|
-
if (isSkipAuthorize) → next()
|
|
899
|
+
// 1. Skip check
|
|
900
|
+
if (context.get(Authorization.SKIP_AUTHORIZATION)) return next();
|
|
847
901
|
|
|
848
|
-
//
|
|
902
|
+
// 2. User check
|
|
849
903
|
const user = context.get(Authentication.CURRENT_USER);
|
|
850
|
-
if (!user)
|
|
904
|
+
if (!user) throw 401 'No authenticated user found';
|
|
851
905
|
|
|
852
|
-
//
|
|
853
|
-
const needsRoleCheck = options?.alwaysAllowRoles?.length || spec.allowedRoles?.length;
|
|
906
|
+
// 3. Role shortcuts (alwaysAllowRoles + allowedRoles, merged; userRoles extracted once)
|
|
854
907
|
if (needsRoleCheck) {
|
|
855
|
-
const userRoles = extractUserRoles({ user });
|
|
856
|
-
if (alwaysAllowRoles match)
|
|
857
|
-
if (allowedRoles match)
|
|
908
|
+
const userRoles = extractUserRoles({ user });
|
|
909
|
+
if (alwaysAllowRoles match) return next();
|
|
910
|
+
if (allowedRoles match) return next();
|
|
858
911
|
}
|
|
859
912
|
|
|
860
|
-
//
|
|
861
|
-
for (voter of spec.voters) {
|
|
862
|
-
|
|
863
|
-
if (
|
|
864
|
-
|
|
913
|
+
// 4. Voters (from spec.voters)
|
|
914
|
+
for (const voter of spec.voters ?? []) {
|
|
915
|
+
const decision = await voter({ user, action, resource, context });
|
|
916
|
+
if (decision === DENY) throw 403 'Authorization denied by voter';
|
|
917
|
+
if (decision === ALLOW) return next();
|
|
918
|
+
// ABSTAIN -> next voter
|
|
865
919
|
}
|
|
866
920
|
|
|
867
|
-
//
|
|
868
|
-
if (!registry.hasEnforcers())
|
|
869
|
-
const
|
|
870
|
-
const enforcer = await registry.resolveEnforcer({ name: resolvedName });
|
|
921
|
+
// 5. Resolve enforcer (no-enforcer fallback)
|
|
922
|
+
if (!registry.hasEnforcers()) return next();
|
|
923
|
+
const enforcer = await registry.resolveEnforcer({ name: enforcerName ?? registry.getDefaultEnforcerName() });
|
|
871
924
|
|
|
872
|
-
//
|
|
925
|
+
// 5b. Resolve request domain - only when spec.domain or a global domainResolver is in play
|
|
873
926
|
if (spec.domain || options?.domainResolver) {
|
|
874
|
-
|
|
875
|
-
context.set(Authorization.DOMAIN, domainScope); // the enforcer reads this for request.domain
|
|
927
|
+
context.set(Authorization.DOMAIN, await resolveRequestDomain({ spec, context, options }));
|
|
876
928
|
}
|
|
877
929
|
|
|
878
|
-
//
|
|
930
|
+
// 6. Build/cache rules
|
|
879
931
|
let rules = context.get(Authorization.RULES);
|
|
880
932
|
if (!rules) {
|
|
881
|
-
if (!user.principalType)
|
|
933
|
+
if (!user.principalType) throw 400 'user.principalType is required for enforcer-based authorization';
|
|
882
934
|
rules = await enforcer.buildRules({ user, context });
|
|
883
|
-
context.set(Authorization.RULES, rules);
|
|
935
|
+
context.set(Authorization.RULES, rules);
|
|
884
936
|
}
|
|
885
937
|
|
|
886
|
-
//
|
|
887
|
-
let decision = await enforcer.evaluate({ rules, request, context });
|
|
888
|
-
if (decision === ABSTAIN)
|
|
889
|
-
if (decision !== ALLOW)
|
|
938
|
+
// 7. Evaluate
|
|
939
|
+
let decision = await enforcer.evaluate({ rules, request: { action, resource, conditions, domain: context.get(Authorization.DOMAIN) }, context });
|
|
940
|
+
if (decision === ABSTAIN) decision = options?.defaultDecision ?? DENY;
|
|
941
|
+
if (decision !== ALLOW) throw 403 'Authorization denied';
|
|
890
942
|
|
|
891
|
-
// All checks passed
|
|
892
943
|
await next();
|
|
893
944
|
```
|
|
894
945
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
The `extractUserRoles` method handles multiple role formats from the user object:
|
|
946
|
+
**`extractUserRoles()`** - normalizes `user.roles` to `string[]`, priority `identifier` > `name` > `String(id)`:
|
|
898
947
|
|
|
899
948
|
```typescript
|
|
900
|
-
|
|
901
|
-
const roles = user.roles; // via index signature
|
|
902
|
-
|
|
903
|
-
if (!Array.isArray(roles)) {
|
|
904
|
-
return [];
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return roles.map((r: string | { identifier?: string; name?: string; id?: unknown }) => {
|
|
908
|
-
if (typeof r === 'string') return r;
|
|
909
|
-
return r.identifier ?? r.name ?? String(r.id ?? '');
|
|
910
|
-
});
|
|
911
|
-
}
|
|
949
|
+
roles.map(r => typeof r === 'string' ? r : (r.identifier ?? r.name ?? String(r.id ?? '')));
|
|
912
950
|
```
|
|
913
951
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
Supports these formats:
|
|
917
|
-
```typescript
|
|
918
|
-
// String array
|
|
919
|
-
roles: ['admin', 'user']
|
|
920
|
-
|
|
921
|
-
// Object array with identifier (preferred - matches AuthorizationRole.identifier)
|
|
922
|
-
roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
|
|
952
|
+
Source -> [`providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/providers/authorization.provider.ts)
|
|
923
953
|
|
|
924
|
-
|
|
925
|
-
roles: [{ id: 1, name: 'admin' }]
|
|
926
|
-
|
|
927
|
-
// Object array with id-only fallback
|
|
928
|
-
roles: [{ id: 1 }]
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
## Standalone `authorize()` Function
|
|
954
|
+
## Standalone authorize() function
|
|
932
955
|
|
|
933
956
|
```typescript
|
|
934
|
-
// authorize.middleware.ts
|
|
935
957
|
const authorizationProvider = new AuthorizationProvider();
|
|
936
958
|
const authorizeFn = authorizationProvider.value();
|
|
937
959
|
|
|
938
|
-
export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) =>
|
|
939
|
-
return authorizeFn(opts);
|
|
940
|
-
};
|
|
960
|
+
export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) => authorizeFn(opts);
|
|
941
961
|
```
|
|
942
962
|
|
|
943
|
-
|
|
963
|
+
A module-level singleton `AuthorizationProvider`; the returned handler is a standard Hono `MiddlewareHandler`.
|
|
944
964
|
|
|
945
|
-
|
|
965
|
+
Source -> [`middlewares/authorize.middleware.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/middlewares/authorize.middleware.ts)
|
|
946
966
|
|
|
947
|
-
|
|
967
|
+
## AuthorizationRole
|
|
948
968
|
|
|
949
|
-
|
|
969
|
+
Value object for priority-based role comparison.
|
|
950
970
|
|
|
951
971
|
```typescript
|
|
952
972
|
class AuthorizationRole implements IAuthorizationRole {
|
|
953
973
|
readonly name: string;
|
|
954
974
|
readonly priority: number;
|
|
955
|
-
readonly delimiter: string;
|
|
975
|
+
readonly delimiter: string; // default '_'
|
|
956
976
|
|
|
957
977
|
static build(opts: { name: string; priority: number; delimiter?: string }): AuthorizationRole;
|
|
958
978
|
constructor(opts: { name: string; priority: number; delimiter?: string });
|
|
959
979
|
|
|
960
|
-
get identifier(): string;
|
|
980
|
+
get identifier(): string; // `${String(priority).padStart(3, '0')}${delimiter}${name}`
|
|
961
981
|
|
|
962
|
-
compare(opts: { target: IAuthorizationRole }): number;
|
|
982
|
+
compare(opts: { target: IAuthorizationRole }): number; // this.priority - target.priority
|
|
963
983
|
isHigherThan(opts: { target: IAuthorizationRole }): boolean;
|
|
964
984
|
isLowerThan(opts: { target: IAuthorizationRole }): boolean;
|
|
965
985
|
isEqualTo(opts: { target: IAuthorizationRole }): boolean;
|
|
966
986
|
}
|
|
987
|
+
|
|
988
|
+
interface IAuthorizationRole { readonly name: string; readonly priority: number; readonly identifier: string; }
|
|
967
989
|
```
|
|
968
990
|
|
|
969
|
-
|
|
991
|
+
Source -> [`models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/models/authorization-role.model.ts)
|
|
992
|
+
|
|
993
|
+
## Policy and permission builders
|
|
994
|
+
|
|
995
|
+
Framework-owned row shapes for seeding a `PolicyDefinition`/`Permission` store that `ScopedCasbinAdapter` reads. Neither builder touches the database - both return plain objects for your own repository/insert calls.
|
|
996
|
+
|
|
997
|
+
### AuthorizationPolicyBuilder
|
|
998
|
+
|
|
999
|
+
One static method per `PolicyDefinition` edge type (see [Authorization Policy Variants](#constants)). All accept a `TPolicyDomainInput` (a scope literal string or `{ type, id }`), serialized via `[type, id].join('_')`.
|
|
970
1000
|
|
|
971
1001
|
```typescript
|
|
972
|
-
|
|
973
|
-
readonly
|
|
974
|
-
|
|
975
|
-
|
|
1002
|
+
class AuthorizationPolicyBuilder {
|
|
1003
|
+
static readonly ACTION_PRINCIPAL = 'Action';
|
|
1004
|
+
|
|
1005
|
+
static grant(opts: { subject: { type; id }; permission: { type; id }; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
|
|
1006
|
+
static assignRole(opts: { user: { type; id }; role: { type; id }; domain?: TNullable<TPolicyDomainInput> }): PolicyDefinitionRow;
|
|
1007
|
+
static joinDomain(opts: { user: { type; id }; domain: { type; id } }): PolicyDefinitionRow;
|
|
1008
|
+
static roleInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
|
|
1009
|
+
static resourceInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow; // Permission ids
|
|
1010
|
+
static actionInherits(opts: { child: TAuthorizationAction; parent: TAuthorizationAction }): PolicyDefinitionRow;
|
|
1011
|
+
static domainInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
|
|
1012
|
+
|
|
1013
|
+
/** All action_inherits rows for AuthorizationActions.LATTICE. Seed once, idempotently. */
|
|
1014
|
+
static actionLattice(): PolicyDefinitionRow[];
|
|
1015
|
+
|
|
1016
|
+
/** A role's coarse grant rows from resolved permission codes -> ids. */
|
|
1017
|
+
static roleGrants(opts: {
|
|
1018
|
+
role: { type; id };
|
|
1019
|
+
permission: { type: string; idByCode: ReadonlyMap<string, string> };
|
|
1020
|
+
grants: ReadonlyArray<{ resourceCode: string; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }>;
|
|
1021
|
+
}): PolicyDefinitionRow[]; // unresolved resourceCodes are skipped
|
|
976
1022
|
}
|
|
977
1023
|
```
|
|
978
1024
|
|
|
979
|
-
|
|
1025
|
+
`domain` defaults: `grant` -> `null` maps to `ANY_MEMBER` (the adapter's default); `assignRole` -> `null` maps to `*` (every domain).
|
|
1026
|
+
|
|
1027
|
+
### AuthorizationPermissionBuilder
|
|
980
1028
|
|
|
981
|
-
|
|
1029
|
+
Builds `Permission` catalog rows (the `obj` axis the scoped matcher resolves). Generic over the name/description type, so i18n and plain-text apps both fit - the framework only owns the `code`/`method`/`action` shape.
|
|
982
1030
|
|
|
983
1031
|
```typescript
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1032
|
+
class AuthorizationPermissionBuilder {
|
|
1033
|
+
static readonly RESOURCE_NODE_METHOD = '*'; // sentinel method for a coarse resource node
|
|
1034
|
+
|
|
1035
|
+
/** Standard repository method -> base action. Unlisted methods resolve to `execute`. */
|
|
1036
|
+
static readonly METHOD_ACTIONS: Record<string, TAuthorizationAction>; // find/findById/findOne/count -> read, create -> create, updateById/updateBy -> update, deleteById/deleteBy -> delete
|
|
1037
|
+
static readonly DEFAULT_CRUD_METHODS: string[]; // the methods `crud()` generates by default
|
|
1038
|
+
|
|
1039
|
+
static actionForMethod(method: string): TAuthorizationAction;
|
|
1040
|
+
|
|
1041
|
+
/** One operation-level permission, code = `<subject>.<method>`. */
|
|
1042
|
+
static operation<TName>(opts: { subject: string; method: string; scope: string; name: TName; description?: TNullable<TName>; action?: TAuthorizationAction; parentId?: TNullable<IdType> }): PermissionRow;
|
|
1043
|
+
|
|
1044
|
+
/** A coarse resource node (module or subject) used as a grant target, e.g. `Sale`. code has no dotted method; action defaults to `manage`. */
|
|
1045
|
+
static resourceNode<TName>(opts: { code: string; subject?: string; scope: string; name: TName; description?: TNullable<TName>; action?: TAuthorizationAction; parentId?: TNullable<IdType> }): PermissionRow;
|
|
1046
|
+
|
|
1047
|
+
/** The CRUD permission set for a subject (find/findById/findOne/count/create/updateById/updateBy/deleteById/deleteBy by default). */
|
|
1048
|
+
static crud<TName>(opts: {
|
|
1049
|
+
subject: string; scope: string;
|
|
1050
|
+
name: (ctx: { subject: string; method: string; action: TAuthorizationAction }) => TName;
|
|
1051
|
+
description?: (ctx) => TNullable<TName>;
|
|
1052
|
+
methods?: ReadonlyArray<string>;
|
|
1053
|
+
}): PermissionRow[];
|
|
1054
|
+
}
|
|
988
1055
|
```
|
|
989
1056
|
|
|
990
|
-
|
|
1057
|
+
Source -> [`common/policy-builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/policy-builder.ts), [`common/permission-builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/permission-builder.ts)
|
|
991
1058
|
|
|
992
|
-
|
|
1059
|
+
## Model-based authorization metadata
|
|
993
1060
|
|
|
994
|
-
|
|
1061
|
+
`@model({ settings: { authorize: { principal } } })` (see [Persistent Models](/guides/core-concepts/persistent/models#authorization-settings)) drives two things:
|
|
1062
|
+
|
|
1063
|
+
**1. `AUTHORIZATION_SUBJECT` static.** The `@model` decorator (`base/metadata/persistents.ts`) copies `settings.authorize.principal` onto the class as `AUTHORIZATION_SUBJECT`, unless the class already declares its own:
|
|
995
1064
|
|
|
996
1065
|
```typescript
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1066
|
+
const principal = metadata.settings?.authorize?.principal;
|
|
1067
|
+
if (principal && !Object.hasOwn(target, 'AUTHORIZATION_SUBJECT')) {
|
|
1068
|
+
target.AUTHORIZATION_SUBJECT = principal;
|
|
1069
|
+
}
|
|
1000
1070
|
```
|
|
1001
1071
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
### How Authorization Middleware is Injected
|
|
1072
|
+
Declared on both entity bases: `BasePostgresEntity.AUTHORIZATION_SUBJECT?: string` and `BaseSearchEntity.AUTHORIZATION_SUBJECT?: string`.
|
|
1005
1073
|
|
|
1006
|
-
|
|
1074
|
+
**2. `IModelAuthorizeSettings`.**
|
|
1007
1075
|
|
|
1008
|
-
|
|
1076
|
+
```typescript
|
|
1077
|
+
interface IModelAuthorizeSettings {
|
|
1078
|
+
principal: string;
|
|
1079
|
+
[extra: string | symbol]: any; // extensible - consumers can add extra authorization metadata
|
|
1080
|
+
}
|
|
1081
|
+
```
|
|
1009
1082
|
|
|
1010
|
-
|
|
1083
|
+
**3. `MetadataRegistry` queries** (mixed in by `ModelMetadataMixin`) - retrieve every model's authorization principal at runtime, e.g. to seed Casbin `Permission` rows:
|
|
1011
1084
|
|
|
1012
1085
|
```typescript
|
|
1013
|
-
|
|
1014
|
-
const { authenticate = {}, authorize, ...restConfig } = configs;
|
|
1015
|
-
const mws = [];
|
|
1086
|
+
getModelAuthorizeSettings(opts: { name: string }): IModelAuthorizeSettings | undefined;
|
|
1016
1087
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
mws.push(authenticateFn({ strategies, mode }));
|
|
1020
|
-
}
|
|
1088
|
+
getAuthorizeModelPrincipals(opts: { format: 'array' }): string[];
|
|
1089
|
+
getAuthorizeModelPrincipals(opts: { format: 'record' }): Record<string, string>; // modelName -> principal
|
|
1021
1090
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1091
|
+
getAuthorizeModelSettings(opts: { format: 'array' }): Array<{ name: string; authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
|
|
1092
|
+
getAuthorizeModelSettings(opts: { format: 'record' }): Record<string, { authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
Source -> [`base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts), [`helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts)
|
|
1029
1096
|
|
|
1030
|
-
|
|
1031
|
-
if (restConfig.middleware) { ... }
|
|
1097
|
+
## Controller integration
|
|
1032
1098
|
|
|
1099
|
+
Authorization is supported in both **REST** and **gRPC** controllers, injected right after authentication.
|
|
1100
|
+
|
|
1101
|
+
### REST controllers
|
|
1102
|
+
|
|
1103
|
+
`AbstractRestController.buildRouteMiddlewares()` builds the middleware array; `getRouteConfigs()` calls it internally.
|
|
1104
|
+
|
|
1105
|
+
```typescript
|
|
1106
|
+
buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
|
|
1107
|
+
const { authenticate = {}, authorize, ...restConfig } = opts.configs;
|
|
1108
|
+
const mws = [];
|
|
1109
|
+
if (strategies.length > 0) mws.push(authenticateFn({ strategies, mode })); // 1. authenticate
|
|
1110
|
+
if (authorize) { // 2. authorize (single or array)
|
|
1111
|
+
for (const spec of Array.isArray(authorize) ? authorize : [authorize]) mws.push(authorizeFn({ spec }));
|
|
1112
|
+
}
|
|
1113
|
+
if (restConfig.middleware) { /* 3. custom middleware, last */ }
|
|
1033
1114
|
return { restConfig, security, mws };
|
|
1034
1115
|
}
|
|
1035
1116
|
```
|
|
1036
1117
|
|
|
1037
|
-
|
|
1118
|
+
```typescript
|
|
1119
|
+
interface IAuthRouteConfig extends HonoRouteConfig {
|
|
1120
|
+
authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
|
|
1121
|
+
authorize?: IAuthorizationSpec | IAuthorizationSpec[];
|
|
1122
|
+
}
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
An array `authorize` creates one middleware per spec - all must pass.
|
|
1038
1126
|
|
|
1039
|
-
|
|
1127
|
+
### gRPC controllers
|
|
1128
|
+
|
|
1129
|
+
`AbstractGrpcController.buildRpcMiddlewares()` injects middleware in the same order, reading `authorize` from `IRpcMetadata`:
|
|
1040
1130
|
|
|
1041
1131
|
```typescript
|
|
1042
1132
|
buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
|
|
1043
|
-
const { configs } = opts;
|
|
1044
1133
|
const mws = [];
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
// 2. Authorize middleware (second) - same pattern as REST
|
|
1050
|
-
if (configs.authorize) {
|
|
1051
|
-
const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
|
|
1052
|
-
for (const spec of specs) {
|
|
1134
|
+
if (configs.authenticate) { /* 1. authenticate */ }
|
|
1135
|
+
if (configs.authorize) { // 2. authorize
|
|
1136
|
+
for (const spec of Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize]) {
|
|
1053
1137
|
const authzMw = authorizeFn({ spec });
|
|
1054
1138
|
mws.push((context, next) => authzMw(context, next));
|
|
1055
1139
|
}
|
|
1056
1140
|
}
|
|
1057
|
-
|
|
1058
1141
|
return mws;
|
|
1059
1142
|
}
|
|
1060
1143
|
```
|
|
1061
1144
|
|
|
1062
|
-
### IAuthRouteConfig
|
|
1063
|
-
|
|
1064
|
-
Extended route config that supports both authentication and authorization (REST):
|
|
1065
|
-
|
|
1066
1145
|
```typescript
|
|
1067
|
-
interface
|
|
1146
|
+
interface IRpcMetadata {
|
|
1147
|
+
name: string; // proto method name
|
|
1148
|
+
method: TGrpcMethod;
|
|
1068
1149
|
authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
|
|
1069
1150
|
authorize?: IAuthorizationSpec | IAuthorizationSpec[];
|
|
1070
1151
|
}
|
|
1071
1152
|
```
|
|
1072
1153
|
|
|
1073
|
-
|
|
1154
|
+
### CRUD factory authorization
|
|
1074
1155
|
|
|
1075
|
-
|
|
1156
|
+
`defineControllerRouteConfigs` (`base/controllers/factory/definition.ts`) resolves each generated route's `authorize` via `resolveRouteAuthorize(routeKey)`:
|
|
1157
|
+
|
|
1158
|
+
1. Endpoint `authenticate: { skip: true }` -> `undefined` (skips both authentication and authorization).
|
|
1159
|
+
2. Endpoint `authorize: { skip: true }` -> `undefined` (authorization only; authentication still runs).
|
|
1160
|
+
3. Endpoint `authorize` (single spec or array) -> used as-is.
|
|
1161
|
+
4. No endpoint override -> falls back to the controller-level `authorize`.
|
|
1076
1162
|
|
|
1077
1163
|
```typescript
|
|
1078
|
-
/** Per-route authorization config: { skip: true }, single spec, or array of specs. */
|
|
1079
1164
|
type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
|
|
1165
|
+
type TRouteAuthConfig = { authenticate?: TRouteAuthenticateConfig; authorize?: TRouteAuthorizeConfig };
|
|
1166
|
+
```
|
|
1080
1167
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1168
|
+
Applied identically to `count`, `find`, `findById`, `findOne`, `create`, `updateById`, `updateBy`, `deleteById`, `deleteBy`.
|
|
1169
|
+
|
|
1170
|
+
Source -> [`base/controllers/rest/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/rest/abstract.ts), [`base/controllers/grpc/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/grpc/abstract.ts), [`base/controllers/factory/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/factory/definition.ts)
|
|
1171
|
+
|
|
1172
|
+
## Context variables
|
|
1173
|
+
|
|
1174
|
+
The auth module augments Hono's `ContextVariableMap` (`auth/context-variables.ts`), covering both authentication and authorization:
|
|
1175
|
+
|
|
1176
|
+
```typescript
|
|
1177
|
+
declare module 'hono' {
|
|
1178
|
+
interface ContextVariableMap {
|
|
1179
|
+
[Authentication.CURRENT_USER]: IAuthUser;
|
|
1180
|
+
[Authentication.AUDIT_USER_ID]: IdType;
|
|
1181
|
+
[Authentication.SKIP_AUTHENTICATION]: boolean;
|
|
1182
|
+
|
|
1183
|
+
[Authorization.RULES]: unknown;
|
|
1184
|
+
[Authorization.SKIP_AUTHORIZATION]: boolean;
|
|
1185
|
+
[Authorization.DOMAIN]: string;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1086
1188
|
```
|
|
1087
1189
|
|
|
1088
|
-
|
|
1190
|
+
| Key | Constant | Type | Description |
|
|
1191
|
+
|-----|----------|------|-------------|
|
|
1192
|
+
| `'authorization.rules'` | `Authorization.RULES` | `unknown` | Cached rules built by the enforcer - shape depends on the enforcer |
|
|
1193
|
+
| `'authorization.skip'` | `Authorization.SKIP_AUTHORIZATION` | `boolean` | Set `true` to dynamically skip authorization for this request |
|
|
1194
|
+
| `'authorization.domain'` | `Authorization.DOMAIN` | `string` | Resolved request domain scope (`"<Type>_<id>"` or `SYSTEM_WIDE`); read by the enforcer at step 7 |
|
|
1195
|
+
| `'authentication.currentUser'` | `Authentication.CURRENT_USER` | `IAuthUser` | Read at step 2 to get the authenticated user |
|
|
1196
|
+
| `'authentication.auditUserId'` | `Authentication.AUDIT_USER_ID` | `IdType` | Available for audit logging |
|
|
1089
1197
|
|
|
1090
|
-
|
|
1198
|
+
## IAuthUser / IJWTTokenPayload
|
|
1091
1199
|
|
|
1092
1200
|
```typescript
|
|
1093
1201
|
interface IAuthUser {
|
|
1094
|
-
userId: IdType;
|
|
1202
|
+
userId: IdType; // number | string | bigint
|
|
1095
1203
|
[extra: string | symbol]: any;
|
|
1096
1204
|
}
|
|
1097
1205
|
```
|
|
1098
1206
|
|
|
1099
|
-
|
|
1100
|
-
- `user.roles` -- used by `extractUserRoles()` for role-based shortcuts
|
|
1101
|
-
- `user.principalType` -- required by `buildRules()` for enforcer-based evaluation
|
|
1102
|
-
|
|
1103
|
-
## IJWTTokenPayload Interface
|
|
1104
|
-
|
|
1105
|
-
Full JWT token payload shape (extends `IAuthUser`):
|
|
1207
|
+
Accessed by the authorization module via the index signature: `user.roles` (role-based shortcuts), `user.principalType` (required for enforcer-based evaluation).
|
|
1106
1208
|
|
|
1107
1209
|
```typescript
|
|
1108
1210
|
interface IJWTTokenPayload extends JWTPayload, IAuthUser {
|
|
@@ -1116,30 +1218,8 @@ interface IJWTTokenPayload extends JWTPayload, IAuthUser {
|
|
|
1116
1218
|
}
|
|
1117
1219
|
```
|
|
1118
1220
|
|
|
1119
|
-
##
|
|
1120
|
-
|
|
1121
|
-
The auth module augments Hono's `ContextVariableMap` in `auth/context-variables.ts`:
|
|
1122
|
-
|
|
1123
|
-
```typescript
|
|
1124
|
-
declare module 'hono' {
|
|
1125
|
-
interface ContextVariableMap {
|
|
1126
|
-
// Authentication
|
|
1127
|
-
[Authentication.CURRENT_USER]: IAuthUser; // 'authentication.currentUser'
|
|
1128
|
-
[Authentication.AUDIT_USER_ID]: IdType; // 'authentication.auditUserId'
|
|
1129
|
-
[Authentication.SKIP_AUTHENTICATION]: boolean; // 'authentication.skip'
|
|
1130
|
-
|
|
1131
|
-
// Authorization
|
|
1132
|
-
[Authorization.RULES]: unknown; // 'authorization.rules'
|
|
1133
|
-
[Authorization.SKIP_AUTHORIZATION]: boolean; // 'authorization.skip'
|
|
1134
|
-
[Authorization.DOMAIN]: string; // 'authorization.domain'
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
```
|
|
1138
|
-
|
|
1139
|
-
This enables type-safe `context.get()` and `context.set()` across all auth middleware.
|
|
1140
|
-
|
|
1141
|
-
## See Also
|
|
1221
|
+
## See also
|
|
1142
1222
|
|
|
1143
|
-
- [Setup & Configuration](./)
|
|
1144
|
-
- [Usage & Examples](./usage)
|
|
1145
|
-
- [Error Reference](./errors)
|
|
1223
|
+
- [Setup & Configuration](./) - binding keys, options interfaces, and initial setup
|
|
1224
|
+
- [Usage & Examples](./usage) - securing routes, voters, patterns, and CRUD integration
|
|
1225
|
+
- [Error Reference](./errors) - error messages and troubleshooting
|