@venizia/ignis-docs 0.0.6-2 → 0.0.7-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 +125 -388
- package/dist/mcp-server/common/config.d.ts +0 -21
- package/dist/mcp-server/common/config.d.ts.map +1 -1
- package/dist/mcp-server/common/config.js +1 -36
- package/dist/mcp-server/common/config.js.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.d.ts +0 -24
- package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/docs.helper.js +0 -25
- package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
- package/dist/mcp-server/helpers/github.helper.d.ts +0 -13
- package/dist/mcp-server/helpers/github.helper.d.ts.map +1 -1
- package/dist/mcp-server/helpers/github.helper.js +3 -20
- package/dist/mcp-server/helpers/github.helper.js.map +1 -1
- package/dist/mcp-server/index.js +1 -20
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/mcp-server/tools/base.tool.d.ts +4 -85
- package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/base.tool.js +1 -38
- package/dist/mcp-server/tools/base.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts +8 -2
- package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-content.tool.js +1 -10
- package/dist/mcp-server/tools/docs/get-document-content.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts +13 -2
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +1 -10
- package/dist/mcp-server/tools/docs/get-document-metadata.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +16 -8
- package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/get-package-overview.tool.js +2 -25
- package/dist/mcp-server/tools/docs/get-package-overview.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/list-categories.tool.d.ts +5 -2
- package/dist/mcp-server/tools/docs/list-categories.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/list-categories.tool.js +1 -10
- package/dist/mcp-server/tools/docs/list-categories.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/list-documents.tool.d.ts +11 -2
- package/dist/mcp-server/tools/docs/list-documents.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/list-documents.tool.js +1 -10
- package/dist/mcp-server/tools/docs/list-documents.tool.js.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +13 -2
- package/dist/mcp-server/tools/docs/search-documents.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/docs/search-documents.tool.js +1 -10
- package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +9 -2
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +1 -10
- package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.d.ts +16 -2
- package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/search-code.tool.js +2 -14
- package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts +19 -6
- package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +2 -19
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.d.ts +8 -2
- package/dist/mcp-server/tools/github/view-source-file.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -10
- package/dist/mcp-server/tools/github/view-source-file.tool.js.map +1 -1
- package/dist/mcp-server/tools/index.d.ts.map +1 -1
- package/dist/mcp-server/tools/index.js +0 -2
- package/dist/mcp-server/tools/index.js.map +1 -1
- package/package.json +68 -54
- package/wiki/best-practices/api-usage-examples.md +7 -5
- package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
- package/wiki/best-practices/code-style-standards/constants-configuration.md +1 -1
- package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
- package/wiki/best-practices/code-style-standards/function-patterns.md +1 -1
- package/wiki/best-practices/common-pitfalls.md +1 -1
- package/wiki/best-practices/data-modeling.md +33 -1
- package/wiki/best-practices/error-handling.md +7 -4
- package/wiki/best-practices/performance-optimization.md +1 -1
- package/wiki/best-practices/security-guidelines.md +5 -4
- package/wiki/guides/core-concepts/components-guide.md +1 -1
- package/wiki/guides/core-concepts/controllers.md +14 -8
- package/wiki/guides/core-concepts/persistent/models.md +32 -0
- package/wiki/guides/core-concepts/services.md +2 -1
- package/wiki/guides/get-started/5-minute-quickstart.md +1 -1
- package/wiki/guides/reference/mcp-docs-server.md +0 -134
- package/wiki/guides/tutorials/building-a-crud-api.md +2 -1
- package/wiki/guides/tutorials/complete-installation.md +2 -2
- package/wiki/guides/tutorials/ecommerce-api.md +3 -3
- package/wiki/guides/tutorials/realtime-chat.md +7 -6
- package/wiki/index.md +2 -1
- package/wiki/references/base/components.md +2 -1
- package/wiki/references/base/controllers.md +19 -12
- package/wiki/references/base/middlewares.md +2 -1
- package/wiki/references/base/models.md +11 -2
- package/wiki/references/base/services.md +2 -1
- package/wiki/references/components/authentication/api.md +525 -205
- package/wiki/references/components/authentication/errors.md +502 -105
- package/wiki/references/components/authentication/index.md +388 -75
- package/wiki/references/components/authentication/usage.md +428 -266
- package/wiki/references/components/authorization/api.md +1213 -0
- package/wiki/references/components/authorization/errors.md +387 -0
- package/wiki/references/components/authorization/index.md +712 -0
- package/wiki/references/components/authorization/usage.md +758 -0
- package/wiki/references/components/health-check.md +2 -1
- package/wiki/references/components/index.md +2 -0
- package/wiki/references/components/socket-io/index.md +9 -4
- package/wiki/references/components/socket-io/usage.md +1 -1
- package/wiki/references/components/static-asset/index.md +3 -5
- package/wiki/references/components/swagger.md +2 -1
- package/wiki/references/configuration/environment-variables.md +2 -1
- package/wiki/references/configuration/index.md +2 -1
- package/wiki/references/helpers/error/index.md +1 -1
- package/wiki/references/helpers/index.md +1 -0
- package/wiki/references/helpers/inversion/index.md +1 -1
- package/wiki/references/helpers/kafka/index.md +305 -0
- package/wiki/references/helpers/redis/index.md +2 -9
- package/wiki/references/quick-reference.md +3 -5
- package/wiki/references/utilities/crypto.md +2 -2
- package/wiki/references/utilities/date.md +5 -5
- package/wiki/references/utilities/index.md +3 -11
- package/wiki/references/utilities/jsx.md +4 -2
- package/wiki/references/utilities/module.md +1 -1
- package/wiki/references/utilities/parse.md +4 -4
- package/wiki/references/utilities/performance.md +2 -2
- package/wiki/references/utilities/promise.md +4 -4
- package/wiki/references/utilities/request.md +2 -2
|
@@ -0,0 +1,1213 @@
|
|
|
1
|
+
# Authorization -- API Reference
|
|
2
|
+
|
|
3
|
+
> Architecture, enforcer internals, provider, registry, adapters, models, and middleware pipeline. See [Setup & Configuration](./) for initial setup.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
### System Overview
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
graph TB
|
|
11
|
+
subgraph Application["Application Setup"]
|
|
12
|
+
A1["1. bind IAuthorizeOptions"]
|
|
13
|
+
A2["2. this.component(AuthorizeComponent)"]
|
|
14
|
+
A3["3. AuthorizationEnforcerRegistry.register(...)"]
|
|
15
|
+
A1 --> A2 --> A3
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
A3 --> Registry["Enforcer Registry<br/>(singleton)"]
|
|
19
|
+
A3 --> Provider["AuthorizationProvider<br/>(IProvider)"]
|
|
20
|
+
A3 --> MW["authorize() Middleware"]
|
|
21
|
+
|
|
22
|
+
Registry --> Casbin["CasbinAuthorizationEnforcer<br/>+ FilteredAdapter"]
|
|
23
|
+
Registry --> Custom["Custom Enforcer"]
|
|
24
|
+
|
|
25
|
+
Provider --> Pipeline["Request 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
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Middleware Pipeline Flowchart
|
|
41
|
+
|
|
42
|
+
```mermaid
|
|
43
|
+
flowchart TD
|
|
44
|
+
Start([Request]) --> Skip{SKIP_AUTHORIZATION?}
|
|
45
|
+
Skip -->|Yes| Next([next - skip all])
|
|
46
|
+
Skip -->|No| User{User on context?}
|
|
47
|
+
User -->|No| E401[/401 Unauthorized/]
|
|
48
|
+
User -->|Yes| Roles{Any role shortcuts?}
|
|
49
|
+
Roles -->|alwaysAllowRoles match| Next2([next - role bypass])
|
|
50
|
+
Roles -->|allowedRoles match| Next3([next - route role bypass])
|
|
51
|
+
Roles -->|No match| Voters{Has voters?}
|
|
52
|
+
Voters -->|DENY| E403a[/403 Denied by voter/]
|
|
53
|
+
Voters -->|ALLOW| Next4([next - voter allow])
|
|
54
|
+
Voters -->|ABSTAIN / none| Resolve[Resolve enforcer by name]
|
|
55
|
+
Resolve --> Cache{Rules cached?}
|
|
56
|
+
Cache -->|Yes| Evaluate
|
|
57
|
+
Cache -->|No| PType{principalType?}
|
|
58
|
+
PType -->|Missing| E400[/400 principalType required/]
|
|
59
|
+
PType -->|Present| Build[enforcer.buildRules]
|
|
60
|
+
Build --> CacheSet[Cache rules on context]
|
|
61
|
+
CacheSet --> Evaluate[enforcer.evaluate]
|
|
62
|
+
Evaluate --> Decision{Decision?}
|
|
63
|
+
Decision -->|ALLOW| Next5([next - authorized])
|
|
64
|
+
Decision -->|DENY| E403b[/403 Denied/]
|
|
65
|
+
Decision -->|ABSTAIN| Default{defaultDecision}
|
|
66
|
+
Default -->|ALLOW| Next5
|
|
67
|
+
Default -->|DENY| E403b
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Class Hierarchy
|
|
71
|
+
|
|
72
|
+
```mermaid
|
|
73
|
+
classDiagram
|
|
74
|
+
class BaseHelper {
|
|
75
|
+
+logger
|
|
76
|
+
+scope
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class AbstractAuthRegistry~TItem~ {
|
|
80
|
+
#descriptors: Map
|
|
81
|
+
#getBindingPrefix()* string
|
|
82
|
+
+getKey(opts) string
|
|
83
|
+
+getDefaultName() string
|
|
84
|
+
+reset() void
|
|
85
|
+
#registerDescriptor(opts) void
|
|
86
|
+
#resolveDescriptor(opts) TItem
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class AuthorizationEnforcerRegistry {
|
|
90
|
+
-instance$ AuthorizationEnforcerRegistry
|
|
91
|
+
-configuredEnforcers: Set
|
|
92
|
+
+getInstance()$ AuthorizationEnforcerRegistry
|
|
93
|
+
+register(opts) this
|
|
94
|
+
+resolveEnforcer(opts) Promise
|
|
95
|
+
+resolveOptions() IAuthorizeOptions
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
class IAuthorizationEnforcer {
|
|
99
|
+
<<interface>>
|
|
100
|
+
+name: string
|
|
101
|
+
+configure() void
|
|
102
|
+
+buildRules(opts) TRules
|
|
103
|
+
+evaluate(opts) TAuthorizationDecision
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class CasbinAuthorizationEnforcer {
|
|
107
|
+
-enforcer: CasbinEnforcer
|
|
108
|
+
-inMemoryInvalidationTimer
|
|
109
|
+
+configure() void
|
|
110
|
+
+destroy() void
|
|
111
|
+
+buildRules(opts) IAuthUser
|
|
112
|
+
+evaluate(opts) TAuthorizationDecision
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class BaseFilteredAdapter~TEntities~ {
|
|
116
|
+
<<abstract>>
|
|
117
|
+
#entities: TEntities
|
|
118
|
+
+loadFilteredPolicy(model, filter) void
|
|
119
|
+
#buildDirectPolicies(opts)* string[]
|
|
120
|
+
#buildGroupPolicies(opts)* lines + roleIds
|
|
121
|
+
#buildRolePolicies(opts)* string[]
|
|
122
|
+
#formatDomain(domain) string
|
|
123
|
+
#toGroupLine(opts) string
|
|
124
|
+
#toPolicyLine(opts) string
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class DrizzleCasbinAdapter {
|
|
128
|
+
-connector: TAnyConnector
|
|
129
|
+
#buildDirectPolicies(opts) string[]
|
|
130
|
+
#buildGroupPolicies(opts) lines + roleIds
|
|
131
|
+
#buildRolePolicies(opts) string[]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
BaseHelper <|-- AbstractAuthRegistry
|
|
135
|
+
AbstractAuthRegistry <|-- AuthorizationEnforcerRegistry
|
|
136
|
+
IAuthorizationEnforcer <|.. CasbinAuthorizationEnforcer
|
|
137
|
+
BaseHelper <|-- CasbinAuthorizationEnforcer
|
|
138
|
+
BaseHelper <|-- BaseFilteredAdapter
|
|
139
|
+
BaseFilteredAdapter <|-- DrizzleCasbinAdapter
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Module File Layout
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
auth/authorize/
|
|
146
|
+
├── adapters/
|
|
147
|
+
│ ├── base-filtered.ts # BaseFilteredAdapter (abstract template)
|
|
148
|
+
│ └── drizzle-casbin.ts # DrizzleCasbinAdapter (concrete SQL)
|
|
149
|
+
├── common/
|
|
150
|
+
│ ├── constants.ts # Authorization, AuthorizationActions, AuthorizationDecisions,
|
|
151
|
+
│ │ # AuthorizationRoles, AuthorizationEnforcerTypes,
|
|
152
|
+
│ │ # CasbinEnforcerCachedDrivers, CasbinEnforcerModelDrivers,
|
|
153
|
+
│ │ # CasbinRuleVariants
|
|
154
|
+
│ ├── keys.ts # AuthorizeBindingKeys
|
|
155
|
+
│ ├── types.ts # IAuthorizeOptions, IAuthorizationEnforcer,
|
|
156
|
+
│ │ # IAuthorizationSpec, ICasbinEnforcerOptions, etc.
|
|
157
|
+
│ └── index.ts # Barrel export
|
|
158
|
+
├── enforcers/
|
|
159
|
+
│ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
|
|
160
|
+
│ ├── enforcer-registry.ts # AuthorizationEnforcerRegistry (singleton)
|
|
161
|
+
│ └── index.ts # Barrel export
|
|
162
|
+
├── middlewares/
|
|
163
|
+
│ └── authorize.middleware.ts # authorize() standalone function
|
|
164
|
+
├── models/
|
|
165
|
+
│ ├── abilities/
|
|
166
|
+
│ │ ├── string-action.model.ts # StringAuthorizationAction
|
|
167
|
+
│ │ ├── string-resource.model.ts # StringAuthorizationResource
|
|
168
|
+
│ │ └── index.ts
|
|
169
|
+
│ ├── authorization-role.model.ts # AuthorizationRole
|
|
170
|
+
│ └── index.ts
|
|
171
|
+
├── providers/
|
|
172
|
+
│ └── authorization.provider.ts # AuthorizationProvider
|
|
173
|
+
├── component.ts # AuthorizeComponent
|
|
174
|
+
└── index.ts # Barrel export (all submodules)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Tech Stack
|
|
178
|
+
|
|
179
|
+
| Technology | Purpose |
|
|
180
|
+
|------------|---------|
|
|
181
|
+
| **Hono middleware** | Route-level authorization via `createMiddleware` from `hono/factory` |
|
|
182
|
+
| **`casbin`** (optional) | External policy engine for Casbin enforcer. Peer dependency -- not bundled. |
|
|
183
|
+
| **`@venizia/ignis-helpers`** | `BaseHelper` base class, `getError` for error creation, `HTTP` result codes |
|
|
184
|
+
| **`@venizia/ignis-inversion`** | `IProvider` interface, `BindingScopes` for singleton registration |
|
|
185
|
+
|
|
186
|
+
### Design Decisions
|
|
187
|
+
|
|
188
|
+
| Decision | Rationale |
|
|
189
|
+
|----------|-----------|
|
|
190
|
+
| **Enforcer-based** | Pluggable architecture -- swap between Casbin and custom enforcers without changing route configs |
|
|
191
|
+
| **Registry + co-located options** | Enforcer class, name, type, and options are registered together -- no split configuration across two binding sites |
|
|
192
|
+
| **Type-discriminated enforcers** | `type: 'casbin' \| 'custom'` in registry for type-safe options (`ICasbinEnforcerOptions` vs `unknown`) |
|
|
193
|
+
| **Voter pattern** | Custom logic that short-circuits before the enforcer (Spring Security inspiration) |
|
|
194
|
+
| **Rules caching** | Built rules cached on Hono context per-request -- avoids rebuilding for multi-spec routes |
|
|
195
|
+
| **Registry singleton** | Mirrors `AuthenticationStrategyRegistry` pattern -- consistent with the codebase |
|
|
196
|
+
| **Abstract base** | `AbstractAuthRegistry<T>` shared between authentication and authorization registries |
|
|
197
|
+
| **Filtered adapter pattern** | `BaseFilteredAdapter` template method pattern allows custom query backends while sharing formatting logic |
|
|
198
|
+
| **IAuthorizationComparable** | Generic comparison interface for custom action/resource types beyond plain strings |
|
|
199
|
+
|
|
200
|
+
## Component Lifecycle
|
|
201
|
+
|
|
202
|
+
The `AuthorizeComponent` extends `BaseComponent` and executes during its `binding()` method:
|
|
203
|
+
|
|
204
|
+
| Step | Action | Failure |
|
|
205
|
+
|------|--------|---------|
|
|
206
|
+
| 1 | Resolve `IAuthorizeOptions` from container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
|
|
207
|
+
| 2 | Call `bindAlwaysAllowRoles()` -- binds `alwaysAllowRoles` to `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` if present | -- (skipped if no roles) |
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
class AuthorizeComponent extends BaseComponent {
|
|
211
|
+
constructor(
|
|
212
|
+
@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
|
|
213
|
+
) { ... }
|
|
214
|
+
|
|
215
|
+
override binding(): ValueOrPromise<void>;
|
|
216
|
+
private bindAlwaysAllowRoles(opts: { options: IAuthorizeOptions }): void;
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
> [!NOTE]
|
|
221
|
+
> The component's role is minimal -- it validates that global options exist and binds `alwaysAllowRoles` for consumer access. Enforcer registration happens separately via `AuthorizationEnforcerRegistry.register()`.
|
|
222
|
+
|
|
223
|
+
## AbstractAuthRegistry
|
|
224
|
+
|
|
225
|
+
Shared base class for both authentication and authorization registries. Provides descriptor storage, binding key generation, and DI resolution.
|
|
226
|
+
|
|
227
|
+
### TRegistryDescriptor
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
type TRegistryDescriptor<TItem> = {
|
|
231
|
+
container: Container;
|
|
232
|
+
targetClass: TClass<TItem>;
|
|
233
|
+
};
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Class
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
abstract class AbstractAuthRegistry<TItem> extends BaseHelper {
|
|
240
|
+
protected descriptors: Map<string, TRegistryDescriptor<TItem>>;
|
|
241
|
+
|
|
242
|
+
constructor(opts: { scope: string });
|
|
243
|
+
|
|
244
|
+
// Abstract -- subclass provides the binding key prefix
|
|
245
|
+
protected abstract getBindingPrefix(): string;
|
|
246
|
+
|
|
247
|
+
// Public API
|
|
248
|
+
getKey(opts: { name: string }): string;
|
|
249
|
+
getDefaultName(): string;
|
|
250
|
+
reset(): void;
|
|
251
|
+
|
|
252
|
+
// Protected internals
|
|
253
|
+
protected registerDescriptor(opts: { container: Container; target: TClass<TItem>; name: string }): void;
|
|
254
|
+
protected resolveDescriptor(opts: { name: string }): TItem;
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Methods
|
|
259
|
+
|
|
260
|
+
| Method | Description | Throws |
|
|
261
|
+
|--------|-------------|--------|
|
|
262
|
+
| `getKey({ name })` | Builds binding key as `{prefix}.{name}` | `[getKey] Invalid name` if name is empty |
|
|
263
|
+
| `getDefaultName()` | Returns the first registered descriptor's name (Map insertion order) | `[ClassName] No items registered` if none |
|
|
264
|
+
| `registerDescriptor(opts)` | Stores `TRegistryDescriptor` in Map + binds class as `SINGLETON` in DI container | -- |
|
|
265
|
+
| `resolveDescriptor({ name })` | Resolves instance from DI container by key | `Descriptor not found: {name}` or `Failed to resolve: {name}` |
|
|
266
|
+
| `reset()` | Clears all descriptors from the Map | -- |
|
|
267
|
+
|
|
268
|
+
### Subclass Binding Prefixes
|
|
269
|
+
|
|
270
|
+
| Registry | `getBindingPrefix()` returns |
|
|
271
|
+
|----------|------------------------------|
|
|
272
|
+
| `AuthenticationStrategyRegistry` | `Authentication.STRATEGY` |
|
|
273
|
+
| `AuthorizationEnforcerRegistry` | `Authorization.ENFORCER` (`'authorization.enforcer'`) |
|
|
274
|
+
|
|
275
|
+
## Enforcer Registry
|
|
276
|
+
|
|
277
|
+
<code v-pre>AuthorizationEnforcerRegistry</code> is a **singleton** that manages registered enforcers. It extends `AbstractAuthRegistry<IAuthorizationEnforcer>`.
|
|
278
|
+
|
|
279
|
+
### Class Hierarchy
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
BaseHelper
|
|
283
|
+
└── AbstractAuthRegistry<TItem>
|
|
284
|
+
├── AuthenticationStrategyRegistry (authenticate)
|
|
285
|
+
└── AuthorizationEnforcerRegistry (authorize)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Class
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationEnforcer> {
|
|
292
|
+
private static instance: AuthorizationEnforcerRegistry;
|
|
293
|
+
private configuredEnforcers: Set<string>;
|
|
294
|
+
|
|
295
|
+
static getInstance(): AuthorizationEnforcerRegistry;
|
|
296
|
+
override reset(): void; // clears descriptors + configuredEnforcers
|
|
297
|
+
|
|
298
|
+
protected getBindingPrefix(): string; // returns Authorization.ENFORCER
|
|
299
|
+
|
|
300
|
+
register(opts: { ... }): this;
|
|
301
|
+
getDefaultEnforcerName(): string;
|
|
302
|
+
resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer>;
|
|
303
|
+
resolveOptions(): IAuthorizeOptions | undefined;
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### API
|
|
308
|
+
|
|
309
|
+
| Method | Returns | Description |
|
|
310
|
+
|--------|---------|-------------|
|
|
311
|
+
| `getInstance()` | `AuthorizationEnforcerRegistry` | Returns the singleton instance (creates on first call) |
|
|
312
|
+
| `register(opts)` | `this` | Registers enforcers with type-safe options. See below. |
|
|
313
|
+
| `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` -- returns the first registered enforcer's name |
|
|
314
|
+
| `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves and auto-configures an enforcer (configure-once pattern via `configuredEnforcers` Set) |
|
|
315
|
+
| `resolveOptions()` | `IAuthorizeOptions \| undefined` | Iterates all registered containers looking for `AuthorizeBindingKeys.OPTIONS` |
|
|
316
|
+
| `reset()` | `void` | Clears all descriptors AND the `configuredEnforcers` set |
|
|
317
|
+
|
|
318
|
+
### register()
|
|
319
|
+
|
|
320
|
+
The `register` method accepts a discriminated union of enforcer descriptors:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
register(opts: {
|
|
324
|
+
container: Container;
|
|
325
|
+
enforcers: Array<
|
|
326
|
+
| {
|
|
327
|
+
enforcer: TClass<IAuthorizationEnforcer>;
|
|
328
|
+
name: string;
|
|
329
|
+
type: 'casbin';
|
|
330
|
+
options?: ICasbinEnforcerOptions;
|
|
331
|
+
}
|
|
332
|
+
| {
|
|
333
|
+
enforcer: TClass<IAuthorizationEnforcer>;
|
|
334
|
+
name: string;
|
|
335
|
+
type: 'custom';
|
|
336
|
+
options?: unknown;
|
|
337
|
+
}
|
|
338
|
+
>;
|
|
339
|
+
}) => this
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Behavior:**
|
|
343
|
+
1. Validates no duplicate names in the batch (across all `enforcers` in this call)
|
|
344
|
+
2. Validates each name is not already registered (against previously registered enforcers)
|
|
345
|
+
3. Calls `registerDescriptor()` -- binds each enforcer class as singleton: `authorization.enforcer.{name}`
|
|
346
|
+
4. If `options` is provided, binds it to `AuthorizeBindingKeys.enforcerOptions(name)` (`@app/authorize/enforcers/{name}/options`)
|
|
347
|
+
|
|
348
|
+
> [!NOTE]
|
|
349
|
+
> `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`.
|
|
350
|
+
|
|
351
|
+
### Configure-Once Pattern
|
|
352
|
+
|
|
353
|
+
The `resolveEnforcer()` method tracks which enforcers have been configured via the `configuredEnforcers: Set<string>`:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
async resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer> {
|
|
357
|
+
const enforcer = this.resolveDescriptor(opts); // from AbstractAuthRegistry
|
|
358
|
+
|
|
359
|
+
if (!this.configuredEnforcers.has(opts.name)) {
|
|
360
|
+
await enforcer.configure();
|
|
361
|
+
this.configuredEnforcers.add(opts.name);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return enforcer;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
First call: resolves + calls `configure()`. Subsequent calls: resolves only.
|
|
369
|
+
|
|
370
|
+
## IAuthorizationEnforcer Interface
|
|
371
|
+
|
|
372
|
+
The core enforcer contract. All enforcers (Casbin, custom) must implement this interface.
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
interface IAuthorizationEnforcer<
|
|
376
|
+
E extends Env = Env,
|
|
377
|
+
TAction = string,
|
|
378
|
+
TResource = string,
|
|
379
|
+
TRules = unknown,
|
|
380
|
+
TBuildRulesReturn = ValueOrPromise<TRules>,
|
|
381
|
+
TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
|
|
382
|
+
> {
|
|
383
|
+
name: string;
|
|
384
|
+
|
|
385
|
+
configure(): ValueOrPromise<void>;
|
|
386
|
+
|
|
387
|
+
buildRules(opts: {
|
|
388
|
+
user: { principalType: string } & IAuthUser;
|
|
389
|
+
context: TContext<E, string>;
|
|
390
|
+
}): TBuildRulesReturn;
|
|
391
|
+
|
|
392
|
+
evaluate(opts: {
|
|
393
|
+
rules: TRules;
|
|
394
|
+
request: IAuthorizationRequest<TAction, TResource>;
|
|
395
|
+
context: TContext<E, string>;
|
|
396
|
+
}): TEvaluateReturn;
|
|
397
|
+
}
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Generic Parameters
|
|
401
|
+
|
|
402
|
+
| Parameter | Default | Description |
|
|
403
|
+
|-----------|---------|-------------|
|
|
404
|
+
| `E` | `Env` | Hono `Env` type for typed context access |
|
|
405
|
+
| `TAction` | `string` | Action type. Can be `string` or `IAuthorizationComparable` for custom comparison |
|
|
406
|
+
| `TResource` | `string` | Resource type. Can be `string` or `IAuthorizationComparable` for custom comparison |
|
|
407
|
+
| `TRules` | `unknown` | Rules type produced by `buildRules` and consumed by `evaluate` |
|
|
408
|
+
| `TBuildRulesReturn` | `ValueOrPromise<TRules>` | Return type of `buildRules` |
|
|
409
|
+
| `TEvaluateReturn` | `ValueOrPromise<TAuthorizationDecision>` | Return type of `evaluate` |
|
|
410
|
+
|
|
411
|
+
### TRules per Enforcer
|
|
412
|
+
|
|
413
|
+
| Enforcer | TRules | Description |
|
|
414
|
+
|----------|--------|-------------|
|
|
415
|
+
| `CasbinAuthorizationEnforcer` | `IAuthUser` | User object (Casbin evaluates internally from loaded model) |
|
|
416
|
+
| Custom | Any type | Your custom rules structure |
|
|
417
|
+
|
|
418
|
+
### Method Contracts
|
|
419
|
+
|
|
420
|
+
| Method | Input | Returns | Called by |
|
|
421
|
+
|--------|-------|---------|----------|
|
|
422
|
+
| `configure()` | None | `void` | Registry on first `resolveEnforcer()` |
|
|
423
|
+
| `buildRules` | `{ user, context }` | `TRules` | Provider at step 6 |
|
|
424
|
+
| `evaluate` | `{ rules, request, context }` | `TAuthorizationDecision` | Provider at step 7 |
|
|
425
|
+
|
|
426
|
+
## IAuthorizationRequest Interface
|
|
427
|
+
|
|
428
|
+
The request object passed to `evaluate()`:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
interface IAuthorizationRequest<TAction = string, TResource = string> {
|
|
432
|
+
action: TAction;
|
|
433
|
+
resource: TResource;
|
|
434
|
+
conditions?: TAuthorizationConditions;
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
| Field | Type | Description |
|
|
439
|
+
|-------|------|-------------|
|
|
440
|
+
| `action` | `TAction` | Action being checked (e.g., `'read'`, `'create'`) |
|
|
441
|
+
| `resource` | `TResource` | Resource being accessed (e.g., `'Article'`) |
|
|
442
|
+
| `conditions` | `TAuthorizationConditions` | Optional key-value conditions for ABAC |
|
|
443
|
+
|
|
444
|
+
## IAuthorizationComparable Interface
|
|
445
|
+
|
|
446
|
+
Generic comparison interface for custom action and resource types. Allows enforcers to work with objects that define their own comparison logic rather than plain strings.
|
|
447
|
+
|
|
448
|
+
```typescript
|
|
449
|
+
interface IAuthorizationComparable<TElement = string, TCompareResult = number> {
|
|
450
|
+
value: TElement;
|
|
451
|
+
compare(other: TElement): TCompareResult;
|
|
452
|
+
isEqual(other: TElement): boolean;
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
| Member | Type | Description |
|
|
457
|
+
|--------|------|-------------|
|
|
458
|
+
| `value` | `TElement` | The underlying value |
|
|
459
|
+
| `compare(other)` | `TCompareResult` | Compare with another value. Convention: `0` means equal. |
|
|
460
|
+
| `isEqual(other)` | `boolean` | Convenience check -- typically `compare(other) === 0` |
|
|
461
|
+
|
|
462
|
+
The `CasbinAuthorizationEnforcer` constrains its `TAction` and `TResource` generics to `string | IAuthorizationComparable`, allowing either plain strings or comparable objects.
|
|
463
|
+
|
|
464
|
+
## StringAuthorizationAction
|
|
465
|
+
|
|
466
|
+
`IAuthorizationComparable` implementation for string-based actions with wildcard support.
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
class StringAuthorizationAction implements IAuthorizationComparable<string> {
|
|
470
|
+
static readonly WILDCARD = '*';
|
|
471
|
+
|
|
472
|
+
readonly value: string;
|
|
473
|
+
|
|
474
|
+
static build(opts: { value: string }): StringAuthorizationAction;
|
|
475
|
+
constructor(opts: { value: string });
|
|
476
|
+
|
|
477
|
+
compare(other: string): number;
|
|
478
|
+
isEqual(other: string): boolean;
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Comparison Logic
|
|
483
|
+
|
|
484
|
+
- If `this.value === '*'` (WILDCARD), `compare()` returns `0` (matches everything)
|
|
485
|
+
- Otherwise, `this.value.localeCompare(other)` is used
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { StringAuthorizationAction } from '@venizia/ignis';
|
|
489
|
+
|
|
490
|
+
const wildcard = StringAuthorizationAction.build({ value: '*' });
|
|
491
|
+
wildcard.isEqual('read'); // true — wildcard matches all
|
|
492
|
+
wildcard.isEqual('delete'); // true — wildcard matches all
|
|
493
|
+
|
|
494
|
+
const read = StringAuthorizationAction.build({ value: 'read' });
|
|
495
|
+
read.isEqual('read'); // true
|
|
496
|
+
read.isEqual('create'); // false
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## StringAuthorizationResource
|
|
500
|
+
|
|
501
|
+
`IAuthorizationComparable` implementation for string-based resources using `localeCompare`.
|
|
502
|
+
|
|
503
|
+
```typescript
|
|
504
|
+
class StringAuthorizationResource implements IAuthorizationComparable<string> {
|
|
505
|
+
readonly value: string;
|
|
506
|
+
|
|
507
|
+
static build(opts: { value: string }): StringAuthorizationResource;
|
|
508
|
+
constructor(opts: { value: string });
|
|
509
|
+
|
|
510
|
+
compare(other: string): number; // this.value.localeCompare(other)
|
|
511
|
+
isEqual(other: string): boolean; // compare(other) === 0
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Unlike `StringAuthorizationAction`, this class has no wildcard support -- comparison is always via `localeCompare`.
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
import { StringAuthorizationResource } from '@venizia/ignis';
|
|
519
|
+
|
|
520
|
+
const article = StringAuthorizationResource.build({ value: 'Article' });
|
|
521
|
+
article.isEqual('Article'); // true
|
|
522
|
+
article.isEqual('User'); // false
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Casbin Enforcer
|
|
526
|
+
|
|
527
|
+
`CasbinAuthorizationEnforcer` wraps the `casbin` library (optional peer dependency).
|
|
528
|
+
|
|
529
|
+
### Class
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
class CasbinAuthorizationEnforcer<
|
|
533
|
+
E extends Env = Env,
|
|
534
|
+
TAction extends string | IAuthorizationComparable = string,
|
|
535
|
+
TResource extends string | IAuthorizationComparable = string,
|
|
536
|
+
>
|
|
537
|
+
extends BaseHelper
|
|
538
|
+
implements IAuthorizationEnforcer<E, TAction, TResource, IAuthUser>
|
|
539
|
+
{
|
|
540
|
+
name = 'CasbinAuthorizationEnforcer';
|
|
541
|
+
|
|
542
|
+
private readonly MIN_EXPIRES_IN = 10_000;
|
|
543
|
+
private enforcer: TNullable<CasbinEnforcerType | CasbinCachedEnforcerType>;
|
|
544
|
+
private inMemoryInvalidationTimer: TNullable<NodeJS.Timeout>;
|
|
545
|
+
|
|
546
|
+
constructor(
|
|
547
|
+
@inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') })
|
|
548
|
+
private options: ICasbinEnforcerOptions<E, TAction, TResource>,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Lifecycle
|
|
552
|
+
async configure(): Promise<void>;
|
|
553
|
+
destroy(): void;
|
|
554
|
+
|
|
555
|
+
// IAuthorizationEnforcer
|
|
556
|
+
async buildRules(opts: { user; context }): Promise<IAuthUser>;
|
|
557
|
+
async evaluate(opts: { rules; request; context }): Promise<TAuthorizationDecision>;
|
|
558
|
+
|
|
559
|
+
// Protected internals
|
|
560
|
+
protected async resolveCasbinEnforcer(opts): Promise<CasbinEnforcerType | CasbinCachedEnforcerType>;
|
|
561
|
+
protected resolveModel(opts): Model;
|
|
562
|
+
protected validateExpiresIn(opts: { expiresIn: number }): void;
|
|
563
|
+
protected async loadPoliciesFromAdapter(opts): Promise<void>;
|
|
564
|
+
protected async loadPoliciesWithRedisCache(opts): Promise<void>;
|
|
565
|
+
protected async extractPolicyLines(): Promise<string[]>;
|
|
566
|
+
protected async loadPolicyLinesIntoModel(opts: { lines: string[] }): Promise<void>;
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Constructor
|
|
571
|
+
|
|
572
|
+
Injects `ICasbinEnforcerOptions` from the DI container using the binding key `AuthorizeBindingKeys.enforcerOptions('casbin')` (which resolves to `@app/authorize/enforcers/casbin/options`).
|
|
573
|
+
|
|
574
|
+
### configure()
|
|
575
|
+
|
|
576
|
+
Called once by the registry on first use. Performs:
|
|
577
|
+
|
|
578
|
+
1. Dynamically imports `casbin` -- throws `"casbin" is not installed` if missing
|
|
579
|
+
2. Validates `options.model` -- throws `options.model is required.` if missing
|
|
580
|
+
3. Resolves model via driver:
|
|
581
|
+
- `'file'` → `casbin.newModelFromFile(definition)`
|
|
582
|
+
- `'text'` → `casbin.newModelFromString(definition)`
|
|
583
|
+
4. Creates enforcer based on cache config:
|
|
584
|
+
- `cached.use: false` → `casbin.newEnforcer(model, adapter)`
|
|
585
|
+
- `cached.driver: 'in-memory'` → `casbin.newCachedEnforcer(model, adapter)` + periodic invalidation timer (`setInterval`)
|
|
586
|
+
- `cached.driver: 'redis'` → `casbin.newEnforcer(model, adapter)` (Redis handles caching externally)
|
|
587
|
+
5. Validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms) for both in-memory and redis cache drivers
|
|
588
|
+
|
|
589
|
+
### destroy()
|
|
590
|
+
|
|
591
|
+
Cleans up the in-memory invalidation timer:
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
destroy() {
|
|
595
|
+
if (!this.inMemoryInvalidationTimer) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
clearInterval(this.inMemoryInvalidationTimer);
|
|
599
|
+
this.inMemoryInvalidationTimer = null;
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
Call this when shutting down the application to prevent timer leaks. Only relevant when using the `'in-memory'` cache driver.
|
|
604
|
+
|
|
605
|
+
### buildRules()
|
|
606
|
+
|
|
607
|
+
Loads policies into the casbin enforcer model. Always uses `loadFilteredPolicy` -- the adapter must implement the `FilteredAdapter` interface.
|
|
608
|
+
|
|
609
|
+
```mermaid
|
|
610
|
+
flowchart TD
|
|
611
|
+
Start([buildRules]) --> Check{cached.use?}
|
|
612
|
+
Check -->|false| Direct["loadPoliciesFromAdapter(user)"]
|
|
613
|
+
Check -->|true| Driver{cached.driver?}
|
|
614
|
+
Driver -->|in-memory| InMem["loadPoliciesFromAdapter(user)<br/>(CachedEnforcer handles invalidation)"]
|
|
615
|
+
Driver -->|redis| Redis["loadPoliciesWithRedisCache(user, cached)"]
|
|
616
|
+
Driver -->|other| Error[/500 Invalid cached.driver/]
|
|
617
|
+
Direct --> Return([return user])
|
|
618
|
+
InMem --> Return
|
|
619
|
+
Redis --> Return
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
| Cache Driver | Behavior |
|
|
623
|
+
|-------------|----------|
|
|
624
|
+
| `use: false` | Load policies from adapter directly via `loadPoliciesFromAdapter()` |
|
|
625
|
+
| `'in-memory'` | Load policies from adapter (periodic invalidation handles cache refresh) |
|
|
626
|
+
| `'redis'` | Check Redis cache → hit: load lines into model via `loadPolicyLinesIntoModel()`; miss: load from adapter, extract lines via `extractPolicyLines()`, cache in Redis with TTL |
|
|
627
|
+
|
|
628
|
+
Returns the `IAuthUser` directly (Casbin evaluates policies from its internal model, not from the returned value).
|
|
629
|
+
|
|
630
|
+
### evaluate()
|
|
631
|
+
|
|
632
|
+
Delegates to Casbin's synchronous `enforceSync()`:
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
// Without normalizePayloadFn:
|
|
636
|
+
// subject = `${user.principalType}_${user.userId}`
|
|
637
|
+
// enforceSync(subject, resource, action)
|
|
638
|
+
|
|
639
|
+
// With normalizePayloadFn:
|
|
640
|
+
// const { subject, domain, resource, action } = normalizePayloadFn({ user, ... })
|
|
641
|
+
// Domain-aware: enforceSync(subject, domain, resource, action)
|
|
642
|
+
// No domain: enforceSync(subject, resource, action)
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
Returns `AuthorizationDecisions.ALLOW` or `AuthorizationDecisions.DENY`.
|
|
646
|
+
|
|
647
|
+
### Protected Methods
|
|
648
|
+
|
|
649
|
+
| Method | Input | Output | Description |
|
|
650
|
+
|--------|-------|--------|-------------|
|
|
651
|
+
| `resolveCasbinEnforcer` | `{ casbin, model, adapter, cached }` | `Enforcer \| CachedEnforcer` | Creates the casbin enforcer instance based on cache config |
|
|
652
|
+
| `resolveModel` | `{ casbin, model }` | `Model` | Resolves casbin model from file or text via driver discriminant |
|
|
653
|
+
| `validateExpiresIn` | `{ expiresIn }` | `void` | Throws if `expiresIn < MIN_EXPIRES_IN` |
|
|
654
|
+
| `loadPoliciesFromAdapter` | `{ user }` | `void` | Calls `enforcer.loadFilteredPolicy({ principalType, principalValue })` |
|
|
655
|
+
| `loadPoliciesWithRedisCache` | `{ user, cached }` | `void` | Redis cache flow: check cache → hit: load lines; miss: load from adapter + cache |
|
|
656
|
+
| `extractPolicyLines` | -- | `string[]` | Extracts `p` and `g` lines from enforcer model via `getPolicy()` and `getGroupingPolicy()` |
|
|
657
|
+
| `loadPolicyLinesIntoModel` | `{ lines }` | `void` | Clears model, loads lines via `Helper.loadPolicyLine()`, rebuilds role links |
|
|
658
|
+
|
|
659
|
+
### Policy Loading Internals
|
|
660
|
+
|
|
661
|
+
#### Redis Cache Flow
|
|
662
|
+
|
|
663
|
+
```mermaid
|
|
664
|
+
flowchart TD
|
|
665
|
+
Start([buildRules with Redis]) --> Key["keyFn({ user }) → cacheKey"]
|
|
666
|
+
Key --> Valid{cacheKey truthy?}
|
|
667
|
+
Valid -->|No| E400[/400 Invalid cachedKey/]
|
|
668
|
+
Valid -->|Yes| Get["Redis GET cacheKey"]
|
|
669
|
+
Get --> Hit{Cache hit?}
|
|
670
|
+
Hit -->|Yes| Parse["JSON.parse(cachedData)"]
|
|
671
|
+
Parse --> LoadModel["loadPolicyLinesIntoModel(lines)"]
|
|
672
|
+
LoadModel --> LogHit["Log: Loaded CACHED Policies"]
|
|
673
|
+
Hit -->|No| Adapter["loadPoliciesFromAdapter(user)"]
|
|
674
|
+
Adapter --> Extract["extractPolicyLines()"]
|
|
675
|
+
Extract --> Set["Redis SET cacheKey, lines, PX expiresIn"]
|
|
676
|
+
Set --> LogMiss["Log: Loaded ADAPTER + CACHED Policies"]
|
|
677
|
+
LogHit --> Return([return user])
|
|
678
|
+
LogMiss --> Return
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
#### extractPolicyLines()
|
|
682
|
+
|
|
683
|
+
Extracts all loaded policies from the enforcer model as casbin-format strings:
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// Policy rules: ["p, user_123, Article, read, allow", ...]
|
|
687
|
+
const pRules = await this.enforcer.getPolicy();
|
|
688
|
+
const ps = pRules.map(r => [CasbinRuleVariants.P, ...r].join(', '));
|
|
689
|
+
|
|
690
|
+
// Group rules: ["g, user_123, role_admin, org_1", ...]
|
|
691
|
+
const gRules = await this.enforcer.getGroupingPolicy();
|
|
692
|
+
const gs = gRules.map(r => [CasbinRuleVariants.G, ...r].join(', '));
|
|
693
|
+
|
|
694
|
+
return [...ps, ...gs];
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
#### loadPolicyLinesIntoModel()
|
|
698
|
+
|
|
699
|
+
Clears the model and reloads from cached string lines:
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
const { Helper } = await import('casbin');
|
|
703
|
+
const model = this.enforcer.getModel();
|
|
704
|
+
model.clearPolicy();
|
|
705
|
+
|
|
706
|
+
for (const line of opts.lines) {
|
|
707
|
+
Helper.loadPolicyLine(line, model);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
await this.enforcer.buildRoleLinks();
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
## BaseFilteredAdapter
|
|
714
|
+
|
|
715
|
+
Abstract read-only casbin `FilteredAdapter` using a template method pattern. Subclasses provide query hooks; the base orchestrates loading and provides shared formatters.
|
|
716
|
+
|
|
717
|
+
### Class
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
abstract class BaseFilteredAdapter<
|
|
721
|
+
TEntities extends IBaseFilteredAdapterEntities = IBaseFilteredAdapterEntities,
|
|
722
|
+
TFilter = ICasbinPolicyFilter,
|
|
723
|
+
TPolicyRow extends TBasePolicyRow = TBasePolicyRow,
|
|
724
|
+
>
|
|
725
|
+
extends BaseHelper
|
|
726
|
+
implements FilteredAdapter
|
|
727
|
+
{
|
|
728
|
+
protected readonly entities: TEntities;
|
|
729
|
+
|
|
730
|
+
constructor(opts: { scope: string; entities: TEntities });
|
|
731
|
+
|
|
732
|
+
// FilteredAdapter — public API
|
|
733
|
+
async loadPolicy(): Promise<void>; // no-op
|
|
734
|
+
async loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
|
|
735
|
+
isFiltered(): boolean; // always true
|
|
736
|
+
|
|
737
|
+
// No-op write methods (read-only adapter)
|
|
738
|
+
async savePolicy(): Promise<boolean>; // returns true
|
|
739
|
+
async addPolicy(): Promise<void>; // no-op
|
|
740
|
+
async removePolicy(): Promise<void>; // no-op
|
|
741
|
+
async removeFilteredPolicy(): Promise<void>; // no-op
|
|
742
|
+
|
|
743
|
+
// Abstract hooks — subclasses provide the data queries
|
|
744
|
+
protected abstract buildDirectPolicies(opts): ValueOrPromise<string[]>;
|
|
745
|
+
protected abstract buildGroupPolicies(opts): ValueOrPromise<{ lines: string[]; roleIds }>;
|
|
746
|
+
protected abstract buildRolePolicies(opts): ValueOrPromise<string[]>;
|
|
747
|
+
|
|
748
|
+
// Shared formatters
|
|
749
|
+
protected formatDomain(domain: string | null): string | null;
|
|
750
|
+
protected toGroupLine(opts): string;
|
|
751
|
+
protected toPolicyLine(opts): string | null;
|
|
752
|
+
}
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Generic Parameters
|
|
756
|
+
|
|
757
|
+
| Parameter | Extends | Default | Description |
|
|
758
|
+
|-----------|---------|---------|-------------|
|
|
759
|
+
| `TEntities` | `IBaseFilteredAdapterEntities` | `IBaseFilteredAdapterEntities` | Entity configuration (subclass adds fields like `tableName`) |
|
|
760
|
+
| `TFilter` | -- | `ICasbinPolicyFilter` | Filter shape passed to `loadFilteredPolicy` |
|
|
761
|
+
| `TPolicyRow` | `TBasePolicyRow` | `TBasePolicyRow` | Policy row shape consumed by `toPolicyLine` |
|
|
762
|
+
|
|
763
|
+
### IBaseFilteredAdapterEntities
|
|
764
|
+
|
|
765
|
+
```typescript
|
|
766
|
+
interface IBaseFilteredAdapterEntities {
|
|
767
|
+
role: { principalType: string };
|
|
768
|
+
domain?: { principalType: string };
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### ICasbinPolicyFilter
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
interface ICasbinPolicyFilter {
|
|
776
|
+
principalType: string;
|
|
777
|
+
principalValue: string | number;
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### TBasePolicyRow
|
|
782
|
+
|
|
783
|
+
Declared as `type` (not `interface`) for Drizzle compatibility -- carries an implicit index signature required by `connector.execute<T>()`.
|
|
784
|
+
|
|
785
|
+
```typescript
|
|
786
|
+
type TBasePolicyRow = {
|
|
787
|
+
variant: string; // 'policy' or 'group'
|
|
788
|
+
code: string; // permission/resource code
|
|
789
|
+
action: string | null;
|
|
790
|
+
subjectType: string;
|
|
791
|
+
subjectId: string | number;
|
|
792
|
+
effect: string | null;
|
|
793
|
+
domain: string | null;
|
|
794
|
+
};
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
### loadFilteredPolicy()
|
|
798
|
+
|
|
799
|
+
Orchestrates three query phases using `casbin.Helper.loadPolicyLine()`:
|
|
800
|
+
|
|
801
|
+
```mermaid
|
|
802
|
+
flowchart LR
|
|
803
|
+
Start([loadFilteredPolicy]) --> D[buildDirectPolicies]
|
|
804
|
+
D -->|p lines| Load1[Load into model]
|
|
805
|
+
Load1 --> G[buildGroupPolicies]
|
|
806
|
+
G -->|g lines + roleIds| Load2[Load into model]
|
|
807
|
+
Load2 --> Check{roleIds empty?}
|
|
808
|
+
Check -->|No| R[buildRolePolicies]
|
|
809
|
+
R -->|p lines| Load3[Load into model]
|
|
810
|
+
Check -->|Yes| Done([Done])
|
|
811
|
+
Load3 --> Done
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
```
|
|
815
|
+
1. buildDirectPolicies({ filter, rolePrincipal })
|
|
816
|
+
→ Direct permissions assigned to the principal → casbin `p` lines
|
|
817
|
+
|
|
818
|
+
2. buildGroupPolicies({ filter })
|
|
819
|
+
→ Role assignments → casbin `g` lines + roleIds
|
|
820
|
+
|
|
821
|
+
3. buildRolePolicies({ roleIds, rolePrincipal })
|
|
822
|
+
→ Permissions inherited through roles → casbin `p` lines
|
|
823
|
+
(only if roleIds is non-empty)
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Abstract Query Hooks
|
|
827
|
+
|
|
828
|
+
| Hook | Input | Output | Description |
|
|
829
|
+
|------|-------|--------|-------------|
|
|
830
|
+
| `buildDirectPolicies` | `{ filter: TFilter, rolePrincipal: string }` | `string[]` | Direct permission `p` lines for the user |
|
|
831
|
+
| `buildGroupPolicies` | `{ filter: TFilter }` | `{ lines: string[], roleIds: (string \| number)[] }` | Role assignment `g` lines + role IDs |
|
|
832
|
+
| `buildRolePolicies` | `{ roleIds: (string \| number)[], rolePrincipal: string }` | `string[]` | Inherited permission `p` lines via roles |
|
|
833
|
+
|
|
834
|
+
### Shared Formatters
|
|
835
|
+
|
|
836
|
+
| Method | Input | Output | Description |
|
|
837
|
+
|--------|-------|--------|-------------|
|
|
838
|
+
| `formatDomain(domain)` | `string \| null` | `string \| null` | Prepends `entities.domain.principalType` prefix if configured (e.g., `"Organization_<uuid>"`). Returns `null` if input is null. |
|
|
839
|
+
| `toGroupLine(opts)` | `{ subject, role, domain }` | `string` | Formats: <code v-pre>g, <subject>, <role>[, <domain>]</code> |
|
|
840
|
+
| `toPolicyLine(opts)` | `{ row: TPolicyRow }` | `string \| null` | Formats: <code v-pre>p, <subject>, [<domain>,] <resource>, <action>, <effect></code>. Returns `null` if row has no action. Effect defaults to `'allow'`. |
|
|
841
|
+
|
|
842
|
+
## DrizzleCasbinAdapter
|
|
843
|
+
|
|
844
|
+
Concrete read-only `FilteredAdapter` using raw SQL queries via Drizzle's `connector.execute()`.
|
|
845
|
+
|
|
846
|
+
### Class
|
|
847
|
+
|
|
848
|
+
```typescript
|
|
849
|
+
class DrizzleCasbinAdapter extends BaseFilteredAdapter<IDrizzleCasbinEntities> {
|
|
850
|
+
private connector: TAnyConnector;
|
|
851
|
+
|
|
852
|
+
constructor(opts: IDrizzleCasbinAdapterOptions);
|
|
853
|
+
|
|
854
|
+
protected async buildDirectPolicies(opts): Promise<string[]>;
|
|
855
|
+
protected async buildGroupPolicies(opts): Promise<{ lines; roleIds }>;
|
|
856
|
+
protected async buildRolePolicies(opts): Promise<string[]>;
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### IDrizzleCasbinEntities
|
|
861
|
+
|
|
862
|
+
```typescript
|
|
863
|
+
interface IDrizzleCasbinEntities extends IBaseFilteredAdapterEntities {
|
|
864
|
+
permission: { tableName: string; principalType: string };
|
|
865
|
+
role: { tableName: string; principalType: string };
|
|
866
|
+
policyDefinition: { tableName: string; principalType: string };
|
|
867
|
+
domain?: { principalType: string };
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### IDrizzleCasbinAdapterOptions
|
|
872
|
+
|
|
873
|
+
```typescript
|
|
874
|
+
interface IDrizzleCasbinAdapterOptions {
|
|
875
|
+
dataSource: IDataSource;
|
|
876
|
+
entities: IDrizzleCasbinEntities;
|
|
877
|
+
}
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
### SQL Queries
|
|
881
|
+
|
|
882
|
+
All queries use the `sql` template tag from `drizzle-orm` and filter by `variant` using `CasbinRuleVariants.POLICY` or `CasbinRuleVariants.GROUP` constants.
|
|
883
|
+
|
|
884
|
+
**buildDirectPolicies** -- direct permissions assigned to the user:
|
|
885
|
+
```sql
|
|
886
|
+
SELECT pd.variant, p.code, pd.action,
|
|
887
|
+
pd.subject_type AS "subjectType", pd.subject_id AS "subjectId",
|
|
888
|
+
pd.effect, pd.domain
|
|
889
|
+
FROM {policyDefinition.tableName} pd
|
|
890
|
+
INNER JOIN {permission.tableName} p ON pd.target_id = p.id
|
|
891
|
+
WHERE pd.variant = 'policy'
|
|
892
|
+
AND pd.subject_type = :principalType
|
|
893
|
+
AND pd.subject_id = :principalValue
|
|
894
|
+
AND pd.target_type = :permission.principalType
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
**buildGroupPolicies** -- role assignments for the user:
|
|
898
|
+
```sql
|
|
899
|
+
SELECT pd.target_id AS "targetId", pd.domain
|
|
900
|
+
FROM {policyDefinition.tableName} pd
|
|
901
|
+
WHERE pd.variant = 'group'
|
|
902
|
+
AND pd.subject_type = :principalType
|
|
903
|
+
AND pd.subject_id = :principalValue
|
|
904
|
+
AND pd.target_type = :role.principalType
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
**buildRolePolicies** -- permissions inherited through assigned roles:
|
|
908
|
+
```sql
|
|
909
|
+
SELECT pd.variant, p.code, pd.action,
|
|
910
|
+
pd.subject_type AS "subjectType", pd.subject_id AS "subjectId",
|
|
911
|
+
pd.effect, pd.domain
|
|
912
|
+
FROM {policyDefinition.tableName} pd
|
|
913
|
+
INNER JOIN {permission.tableName} p ON pd.target_id = p.id
|
|
914
|
+
WHERE pd.variant = 'policy'
|
|
915
|
+
AND pd.subject_type = :role.principalType
|
|
916
|
+
AND pd.subject_id IN (:roleIds)
|
|
917
|
+
AND pd.target_type = :permission.principalType
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
### Usage Example
|
|
921
|
+
|
|
922
|
+
```typescript
|
|
923
|
+
import { DrizzleCasbinAdapter } from '@venizia/ignis';
|
|
924
|
+
|
|
925
|
+
const adapter = new DrizzleCasbinAdapter({
|
|
926
|
+
dataSource: myPostgresDataSource,
|
|
927
|
+
entities: {
|
|
928
|
+
permission: { tableName: 'Permission', principalType: 'Permission' },
|
|
929
|
+
role: { tableName: 'Role', principalType: 'Role' },
|
|
930
|
+
policyDefinition: { tableName: 'PolicyDefinition', principalType: 'PolicyDefinition' },
|
|
931
|
+
domain: { principalType: 'Organization' },
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
## Authorization Provider
|
|
937
|
+
|
|
938
|
+
`AuthorizationProvider` implements `IProvider<TAuthorizeFn>` and produces the middleware factory.
|
|
939
|
+
|
|
940
|
+
### Class
|
|
941
|
+
|
|
942
|
+
```typescript
|
|
943
|
+
class AuthorizationProvider extends BaseHelper implements IProvider<TAuthorizeFn> {
|
|
944
|
+
constructor();
|
|
945
|
+
|
|
946
|
+
value(): TAuthorizeFn;
|
|
947
|
+
|
|
948
|
+
private createAuthorizeMiddleware(opts: {
|
|
949
|
+
spec: IAuthorizationSpec;
|
|
950
|
+
enforcerName?: string;
|
|
951
|
+
}): MiddlewareHandler;
|
|
952
|
+
|
|
953
|
+
private extractUserRoles(opts: { user: IAuthUser }): string[];
|
|
954
|
+
}
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
### Middleware Pipeline (7 Steps)
|
|
958
|
+
|
|
959
|
+
The `createAuthorizeMiddleware` method creates a Hono middleware with this evaluation order:
|
|
960
|
+
|
|
961
|
+
```typescript
|
|
962
|
+
// Step 1: Skip check
|
|
963
|
+
const isSkipAuthorize = context.get(Authorization.SKIP_AUTHORIZATION);
|
|
964
|
+
if (isSkipAuthorize) → next()
|
|
965
|
+
|
|
966
|
+
// Step 2: User check
|
|
967
|
+
const user = context.get(Authentication.CURRENT_USER);
|
|
968
|
+
if (!user) → throw 401 "No authenticated user found"
|
|
969
|
+
|
|
970
|
+
// Step 3: Role-based shortcuts (alwaysAllowRoles + allowedRoles merged)
|
|
971
|
+
const needsRoleCheck = options?.alwaysAllowRoles?.length || spec.allowedRoles?.length;
|
|
972
|
+
if (needsRoleCheck) {
|
|
973
|
+
const userRoles = extractUserRoles({ user }); // called once
|
|
974
|
+
if (alwaysAllowRoles match) → next() // logs "User has always-allow role"
|
|
975
|
+
if (allowedRoles match) → next() // logs "User has allowed role for route"
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Step 4: Voters (from IAuthorizationSpec)
|
|
979
|
+
for (voter of spec.voters) {
|
|
980
|
+
if (DENY) → throw 403 "Authorization denied by voter"
|
|
981
|
+
if (ALLOW) → next()
|
|
982
|
+
// ABSTAIN → continue to next voter
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Step 5: Resolve enforcer
|
|
986
|
+
const resolvedName = enforcerName ?? registry.getDefaultEnforcerName();
|
|
987
|
+
const enforcer = await registry.resolveEnforcer({ name: resolvedName });
|
|
988
|
+
|
|
989
|
+
// Step 6: Build/cache rules
|
|
990
|
+
let rules = context.get(Authorization.RULES);
|
|
991
|
+
if (!rules) {
|
|
992
|
+
if (!user.principalType) → throw 400 "principalType is required"
|
|
993
|
+
rules = await enforcer.buildRules({ user, context });
|
|
994
|
+
context.set(Authorization.RULES, rules); // cache on context
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Step 7: Evaluate
|
|
998
|
+
let decision = await enforcer.evaluate({ rules, request, context });
|
|
999
|
+
if (decision === ABSTAIN) → decision = options?.defaultDecision ?? DENY;
|
|
1000
|
+
if (decision !== ALLOW) → throw 403 "Authorization denied"
|
|
1001
|
+
|
|
1002
|
+
// All checks passed
|
|
1003
|
+
await next();
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
### Role Extraction
|
|
1007
|
+
|
|
1008
|
+
The `extractUserRoles` method handles multiple role formats from the user object:
|
|
1009
|
+
|
|
1010
|
+
```typescript
|
|
1011
|
+
private extractUserRoles(opts: { user: IAuthUser }): string[] {
|
|
1012
|
+
const roles = user.roles; // via index signature
|
|
1013
|
+
|
|
1014
|
+
if (!Array.isArray(roles)) {
|
|
1015
|
+
return [];
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
return roles.map((r: string | { identifier?: string; name?: string; id?: unknown }) => {
|
|
1019
|
+
if (typeof r === 'string') return r;
|
|
1020
|
+
return r.identifier ?? r.name ?? String(r.id ?? '');
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
**Extraction priority:** `identifier` > `name` > `String(id)`.
|
|
1026
|
+
|
|
1027
|
+
Supports these formats:
|
|
1028
|
+
```typescript
|
|
1029
|
+
// String array
|
|
1030
|
+
roles: ['admin', 'user']
|
|
1031
|
+
|
|
1032
|
+
// Object array with identifier (preferred — matches AuthorizationRole.identifier)
|
|
1033
|
+
roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
|
|
1034
|
+
|
|
1035
|
+
// Object array with name fallback
|
|
1036
|
+
roles: [{ id: 1, name: 'admin' }]
|
|
1037
|
+
|
|
1038
|
+
// Object array with id-only fallback
|
|
1039
|
+
roles: [{ id: 1 }]
|
|
1040
|
+
```
|
|
1041
|
+
|
|
1042
|
+
## Standalone `authorize()` Function
|
|
1043
|
+
|
|
1044
|
+
```typescript
|
|
1045
|
+
// authorize.middleware.ts
|
|
1046
|
+
const authorizationProvider = new AuthorizationProvider();
|
|
1047
|
+
const authorizeFn = authorizationProvider.value();
|
|
1048
|
+
|
|
1049
|
+
export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) => {
|
|
1050
|
+
return authorizeFn(opts);
|
|
1051
|
+
};
|
|
1052
|
+
```
|
|
1053
|
+
|
|
1054
|
+
This is the primary export for creating authorization middleware. It creates a singleton `AuthorizationProvider` instance at module load time. The returned middleware handler is a standard Hono `MiddlewareHandler`.
|
|
1055
|
+
|
|
1056
|
+
## AuthorizationRole Model
|
|
1057
|
+
|
|
1058
|
+
Value object representing a role with priority-based comparison.
|
|
1059
|
+
|
|
1060
|
+
### Class
|
|
1061
|
+
|
|
1062
|
+
```typescript
|
|
1063
|
+
class AuthorizationRole implements IAuthorizationRole {
|
|
1064
|
+
readonly name: string;
|
|
1065
|
+
readonly priority: number;
|
|
1066
|
+
readonly delimiter: string; // default '_'
|
|
1067
|
+
|
|
1068
|
+
static build(opts: { name: string; priority: number; delimiter?: string }): AuthorizationRole;
|
|
1069
|
+
constructor(opts: { name: string; priority: number; delimiter?: string });
|
|
1070
|
+
|
|
1071
|
+
get identifier(): string;
|
|
1072
|
+
|
|
1073
|
+
compare(opts: { target: IAuthorizationRole }): number;
|
|
1074
|
+
isHigherThan(opts: { target: IAuthorizationRole }): boolean;
|
|
1075
|
+
isLowerThan(opts: { target: IAuthorizationRole }): boolean;
|
|
1076
|
+
isEqualTo(opts: { target: IAuthorizationRole }): boolean;
|
|
1077
|
+
}
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
### IAuthorizationRole Interface
|
|
1081
|
+
|
|
1082
|
+
```typescript
|
|
1083
|
+
interface IAuthorizationRole {
|
|
1084
|
+
readonly name: string;
|
|
1085
|
+
readonly priority: number;
|
|
1086
|
+
readonly identifier: string;
|
|
1087
|
+
}
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### Identifier Format
|
|
1091
|
+
|
|
1092
|
+
The identifier is generated as `{paddedPriority}{delimiter}{name}`. Priority is zero-padded to 3 digits:
|
|
1093
|
+
|
|
1094
|
+
```typescript
|
|
1095
|
+
// Priority 999, name 'super-admin', delimiter '_' → '999_super-admin'
|
|
1096
|
+
// Priority 10, name 'user', delimiter '_' → '010_user'
|
|
1097
|
+
// Priority 1, name 'guest', delimiter '_' → '001_guest'
|
|
1098
|
+
// Priority 0, name 'unknown-user', delimiter '_' → '000_unknown-user'
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
Implementation: `[String(this.priority).padStart(3, '0'), this.name].join(this.delimiter)`
|
|
1102
|
+
|
|
1103
|
+
### Comparison
|
|
1104
|
+
|
|
1105
|
+
Roles are compared by priority (higher number = higher privilege). `compare()` returns `this.priority - target.priority`:
|
|
1106
|
+
|
|
1107
|
+
```typescript
|
|
1108
|
+
AuthorizationRoles.SUPER_ADMIN.isHigherThan({ target: AuthorizationRoles.ADMIN }); // true (999 > 900)
|
|
1109
|
+
AuthorizationRoles.GUEST.isLowerThan({ target: AuthorizationRoles.USER }); // true (1 < 10)
|
|
1110
|
+
AuthorizationRoles.ADMIN.isEqualTo({ target: AuthorizationRoles.ADMIN }); // true (900 === 900)
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
## Controller Integration
|
|
1114
|
+
|
|
1115
|
+
### How Authorization Middleware is Injected
|
|
1116
|
+
|
|
1117
|
+
The `AbstractController.getRouteConfigs()` method handles middleware injection order:
|
|
1118
|
+
|
|
1119
|
+
```typescript
|
|
1120
|
+
getRouteConfigs<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
|
|
1121
|
+
const { authenticate = {}, authorize, ...restConfig } = configs;
|
|
1122
|
+
const mws = [];
|
|
1123
|
+
|
|
1124
|
+
// 1. Authenticate middleware (first)
|
|
1125
|
+
if (strategies.length > 0) {
|
|
1126
|
+
mws.push(authenticateFn({ strategies, mode }));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// 2. Authorize middleware (second) — supports single or array
|
|
1130
|
+
if (authorize) {
|
|
1131
|
+
const specs = Array.isArray(authorize) ? authorize : [authorize];
|
|
1132
|
+
for (const spec of specs) {
|
|
1133
|
+
mws.push(authorizeFn({ spec }));
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// 3. Custom middleware (last)
|
|
1138
|
+
if (restConfig.middleware) { ... }
|
|
1139
|
+
|
|
1140
|
+
return createRoute({ ...restConfig, middleware: mws, ... });
|
|
1141
|
+
}
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
### IAuthRouteConfig
|
|
1145
|
+
|
|
1146
|
+
Extended route config that supports both authentication and authorization:
|
|
1147
|
+
|
|
1148
|
+
```typescript
|
|
1149
|
+
interface IAuthRouteConfig extends HonoRouteConfig {
|
|
1150
|
+
authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
|
|
1151
|
+
authorize?: IAuthorizationSpec | IAuthorizationSpec[];
|
|
1152
|
+
}
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
When `authorize` is an array, each spec creates a separate middleware. All must pass for the handler to execute.
|
|
1156
|
+
|
|
1157
|
+
## IAuthUser Interface
|
|
1158
|
+
|
|
1159
|
+
The user object available during authorization. Defined in `authenticate/common/types.ts`:
|
|
1160
|
+
|
|
1161
|
+
```typescript
|
|
1162
|
+
interface IAuthUser {
|
|
1163
|
+
userId: IdType; // IdType = number | string | bigint
|
|
1164
|
+
[extra: string | symbol]: any;
|
|
1165
|
+
}
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
Key properties accessed by the authorization module via the index signature:
|
|
1169
|
+
- `user.roles` -- used by `extractUserRoles()` for role-based shortcuts
|
|
1170
|
+
- `user.principalType` -- required by `buildRules()` for enforcer-based evaluation
|
|
1171
|
+
|
|
1172
|
+
## IJWTTokenPayload Interface
|
|
1173
|
+
|
|
1174
|
+
Full JWT token payload shape (extends `IAuthUser`):
|
|
1175
|
+
|
|
1176
|
+
```typescript
|
|
1177
|
+
interface IJWTTokenPayload extends JWTPayload, IAuthUser {
|
|
1178
|
+
userId: IdType;
|
|
1179
|
+
roles: { id: IdType; identifier: string; priority: number }[];
|
|
1180
|
+
clientId?: string;
|
|
1181
|
+
provider?: string;
|
|
1182
|
+
email?: string;
|
|
1183
|
+
name?: string;
|
|
1184
|
+
[extra: string | symbol]: any;
|
|
1185
|
+
}
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
## Hono Context Variables (ContextVariableMap)
|
|
1189
|
+
|
|
1190
|
+
The auth module augments Hono's `ContextVariableMap` in `auth/context-variables.ts`:
|
|
1191
|
+
|
|
1192
|
+
```typescript
|
|
1193
|
+
declare module 'hono' {
|
|
1194
|
+
interface ContextVariableMap {
|
|
1195
|
+
// Authentication
|
|
1196
|
+
[Authentication.CURRENT_USER]: IAuthUser; // 'authentication.currentUser'
|
|
1197
|
+
[Authentication.AUDIT_USER_ID]: IdType; // 'authentication.auditUserId'
|
|
1198
|
+
[Authentication.SKIP_AUTHENTICATION]: boolean; // 'authentication.skip'
|
|
1199
|
+
|
|
1200
|
+
// Authorization
|
|
1201
|
+
[Authorization.RULES]: unknown; // 'authorization.rules'
|
|
1202
|
+
[Authorization.SKIP_AUTHORIZATION]: boolean; // 'authorization.skip'
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
This enables type-safe `context.get()` and `context.set()` across all auth middleware.
|
|
1208
|
+
|
|
1209
|
+
## See Also
|
|
1210
|
+
|
|
1211
|
+
- [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
|
|
1212
|
+
- [Usage & Examples](./usage) -- Securing routes, voters, patterns, and CRUD integration
|
|
1213
|
+
- [Error Reference](./errors) -- Error messages and troubleshooting
|