@venizia/ignis-docs 0.2.0 → 0.2.1-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/content/best-practices/api-usage-examples.md +39 -19
- package/content/best-practices/architectural-patterns.md +24 -13
- package/content/best-practices/architecture-decisions.md +29 -16
- package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
- package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
- package/content/best-practices/code-style-standards/control-flow.md +33 -1
- package/content/best-practices/code-style-standards/documentation.md +28 -8
- package/content/best-practices/code-style-standards/function-patterns.md +15 -4
- package/content/best-practices/code-style-standards/index.md +7 -2
- package/content/best-practices/code-style-standards/naming-conventions.md +27 -3
- package/content/best-practices/code-style-standards/route-definitions.md +9 -5
- package/content/best-practices/code-style-standards/tooling.md +14 -1
- package/content/best-practices/code-style-standards/type-safety.md +39 -6
- package/content/best-practices/common-pitfalls.md +59 -35
- package/content/best-practices/contribution-workflow.md +8 -4
- package/content/best-practices/data-modeling.md +78 -62
- package/content/best-practices/deployment-strategies.md +56 -19
- package/content/best-practices/error-handling.md +247 -153
- package/content/best-practices/index.md +6 -0
- package/content/best-practices/performance-optimization.md +26 -13
- package/content/best-practices/security-guidelines.md +35 -9
- package/content/best-practices/testing-strategies.md +7 -2
- package/content/best-practices/troubleshooting-tips.md +27 -17
- package/content/extensions/components/api-reference.md +109 -323
- package/content/extensions/components/authentication/api.md +489 -602
- package/content/extensions/components/authentication/errors.md +135 -498
- package/content/extensions/components/authentication/index.md +89 -801
- package/content/extensions/components/authentication/usage.md +231 -955
- package/content/extensions/components/authorization/api.md +991 -644
- package/content/extensions/components/authorization/errors.md +204 -208
- package/content/extensions/components/authorization/getting-started.md +227 -0
- package/content/extensions/components/authorization/index.md +88 -795
- package/content/extensions/components/authorization/usage.md +219 -528
- package/content/extensions/components/health-check.md +77 -243
- package/content/extensions/components/index.md +24 -90
- package/content/extensions/components/mail/api.md +553 -285
- package/content/extensions/components/mail/errors.md +76 -62
- package/content/extensions/components/mail/index.md +111 -463
- package/content/extensions/components/mail/usage.md +139 -173
- package/content/extensions/components/request-tracker.md +70 -173
- package/content/extensions/components/socket-io/api.md +462 -785
- package/content/extensions/components/socket-io/errors.md +49 -51
- package/content/extensions/components/socket-io/index.md +69 -372
- package/content/extensions/components/socket-io/usage.md +212 -105
- package/content/extensions/components/static-asset/api.md +461 -141
- package/content/extensions/components/static-asset/errors.md +121 -53
- package/content/extensions/components/static-asset/index.md +84 -606
- package/content/extensions/components/static-asset/usage.md +184 -299
- package/content/extensions/components/template/index.md +3 -3
- package/content/extensions/components/websocket/api.md +311 -404
- package/content/extensions/components/websocket/errors.md +47 -56
- package/content/extensions/components/websocket/index.md +75 -407
- package/content/extensions/components/websocket/usage.md +120 -338
- package/content/extensions/helpers/cron/index.md +52 -160
- package/content/extensions/helpers/crypto/index.md +67 -480
- package/content/extensions/helpers/crypto/reference.md +528 -0
- package/content/extensions/helpers/env/index.md +64 -178
- package/content/extensions/helpers/error/index.md +286 -196
- package/content/extensions/helpers/index.md +61 -47
- package/content/extensions/helpers/inversion/index.md +76 -550
- package/content/extensions/helpers/inversion/reference.md +530 -0
- package/content/extensions/helpers/kafka/admin.md +23 -3
- package/content/extensions/helpers/kafka/compile-binary.md +83 -52
- package/content/extensions/helpers/kafka/consumer.md +65 -28
- package/content/extensions/helpers/kafka/examples.md +46 -253
- package/content/extensions/helpers/kafka/index.md +55 -617
- package/content/extensions/helpers/kafka/producer.md +156 -22
- package/content/extensions/helpers/kafka/schema-registry.md +59 -79
- package/content/extensions/helpers/logger/hf-logger.md +220 -0
- package/content/extensions/helpers/logger/index.md +78 -552
- package/content/extensions/helpers/logger/pino.md +105 -0
- package/content/extensions/helpers/logger/reference.md +937 -0
- package/content/extensions/helpers/network/api.md +276 -195
- package/content/extensions/helpers/network/index.md +87 -524
- package/content/extensions/helpers/queue/index.md +80 -897
- package/content/extensions/helpers/queue/reference.md +494 -0
- package/content/extensions/helpers/redis/index.md +86 -640
- package/content/extensions/helpers/redis/reference.md +784 -0
- package/content/extensions/helpers/secrets/index.md +136 -0
- package/content/extensions/helpers/socket-io/api.md +325 -204
- package/content/extensions/helpers/socket-io/index.md +79 -429
- package/content/extensions/helpers/storage/api.md +584 -464
- package/content/extensions/helpers/storage/index.md +80 -573
- package/content/extensions/helpers/types/index.md +75 -495
- package/content/extensions/helpers/types/reference.md +689 -0
- package/content/extensions/helpers/uid/index.md +176 -189
- package/content/extensions/helpers/websocket/api.md +366 -214
- package/content/extensions/helpers/websocket/index.md +68 -503
- package/content/extensions/helpers/worker-thread/index.md +62 -396
- package/content/extensions/helpers/worker-thread/reference.md +428 -0
- package/content/extensions/index.md +38 -39
- package/content/extensions/src-details/mcp-server.md +96 -548
- package/content/guides/core-concepts/persistent/datasources.md +2 -2
- package/content/guides/core-concepts/persistent/index.md +5 -1
- package/content/guides/core-concepts/persistent/models.md +1 -1
- package/content/guides/core-concepts/persistent/pglite.md +218 -0
- package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
- package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
- package/content/guides/core-concepts/persistent/search-typesense.md +28 -16
- package/content/guides/core-concepts/persistent/sqlite.md +296 -0
- package/content/guides/core-concepts/persistent/transactions.md +12 -11
- package/content/guides/core-concepts/secrets-vault.md +177 -0
- package/content/guides/core-concepts/services.md +1 -1
- package/content/guides/get-started/5-minute-quickstart.md +59 -206
- package/content/guides/get-started/philosophy.md +135 -670
- package/content/guides/get-started/setup.md +53 -74
- package/content/guides/migrations/redis-helpers-migration.md +5 -4
- package/content/guides/migrations/scoped-rbac-migration.md +5 -5
- package/content/guides/migrations/unified-connectors-migration.md +8 -7
- package/content/guides/tutorials/ecommerce-api.md +3 -8
- package/content/guides/tutorials/realtime-chat.md +1 -1
- package/content/references/base/application.md +64 -22
- package/content/references/base/components.md +3 -3
- package/content/references/base/connectors.md +79 -136
- package/content/references/base/controllers.md +12 -12
- package/content/references/base/datasources-reference.md +600 -0
- package/content/references/base/datasources.md +84 -444
- package/content/references/base/dependency-injection.md +25 -46
- package/content/references/base/filter-system/application-usage.md +95 -123
- package/content/references/base/filter-system/array-operators.md +33 -43
- package/content/references/base/filter-system/comparison-operators.md +55 -63
- package/content/references/base/filter-system/default-filter.md +144 -350
- package/content/references/base/filter-system/fields-order-pagination.md +116 -148
- package/content/references/base/filter-system/index.md +114 -253
- package/content/references/base/filter-system/json-filtering.md +52 -181
- package/content/references/base/filter-system/list-operators.md +28 -44
- package/content/references/base/filter-system/logical-operators.md +69 -114
- package/content/references/base/filter-system/null-operators.md +41 -98
- package/content/references/base/filter-system/pattern-matching.md +48 -51
- package/content/references/base/filter-system/quick-reference.md +93 -196
- package/content/references/base/filter-system/range-operators.md +22 -38
- package/content/references/base/filter-system/tips.md +70 -133
- package/content/references/base/filter-system/use-cases.md +173 -232
- package/content/references/base/grpc-controllers.md +53 -17
- package/content/references/base/index.md +5 -3
- package/content/references/base/middlewares.md +43 -28
- package/content/references/base/models-reference.md +886 -0
- package/content/references/base/models.md +81 -1452
- package/content/references/base/providers.md +8 -8
- package/content/references/base/repositories/advanced.md +281 -419
- package/content/references/base/repositories/index.md +84 -644
- package/content/references/base/repositories/mixins.md +25 -21
- package/content/references/base/repositories/relations.md +194 -375
- package/content/references/base/repositories/soft-deletable.md +67 -55
- package/content/references/base/secrets.md +267 -0
- package/content/references/base/services.md +8 -6
- package/content/references/configuration/environment-variables.md +73 -21
- package/content/references/configuration/index.md +51 -31
- package/content/references/index.md +1 -1
- package/content/references/quick-reference.md +3 -16
- package/content/references/utilities/crypto.md +35 -76
- package/content/references/utilities/date.md +33 -73
- package/content/references/utilities/duration.md +85 -0
- package/content/references/utilities/index.md +6 -2
- package/content/references/utilities/jsx-reference.md +298 -0
- package/content/references/utilities/jsx.md +82 -525
- package/content/references/utilities/module.md +81 -61
- package/content/references/utilities/parse.md +34 -64
- package/content/references/utilities/performance.md +33 -58
- package/content/references/utilities/promise.md +28 -62
- package/content/references/utilities/request.md +58 -218
- package/content/references/utilities/retry.md +139 -0
- package/content/references/utilities/schema.md +43 -137
- package/content/references/utilities/statuses-reference.md +361 -0
- package/content/references/utilities/statuses.md +63 -667
- package/dist/mcp-server/index.js +0 -0
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
- package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
- package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
- package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
- package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
- package/package.json +24 -23
|
@@ -1,10 +1,103 @@
|
|
|
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
|
+
---
|
|
6
|
+
|
|
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-server/src/components/auth/authorize/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize) - component, providers, enforcers, adapters, models, middleware
|
|
14
|
+
- [`packages/core-server/src/components/auth/base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/base/abstract-auth-registry.ts) - `AbstractAuthRegistry` (shared with Authentication)
|
|
15
|
+
- [`packages/core-server/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/metadata/persistents.ts) - `@model` populating `AUTHORIZATION_SUBJECT`
|
|
16
|
+
- [`packages/core-server/src/helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/helpers/inversion/mixins/model.mixin.ts) - `MetadataRegistry` authorize-settings queries
|
|
17
|
+
|
|
18
|
+
## Find what you need
|
|
19
|
+
|
|
20
|
+
| You want to | Go to |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Bind global options and per-enforcer options | [Binding keys](#binding-keys) |
|
|
23
|
+
| Configure `AuthorizeComponent` at startup | [IAuthorizeOptions](#iauthorizeoptions) |
|
|
24
|
+
| Configure the Casbin enforcer (model, cache, pool) | [ICasbinEnforcerOptions](#icasbinenforceroptions) |
|
|
25
|
+
| Write a route's `authorize` spec | [IAuthorizationSpec (route-level)](#iauthorizationspec-route-level) |
|
|
26
|
+
| Resolve a request's domain scope | [IAuthorizationDomainSource / TAuthorizationDomainResolver](#iauthorizationdomainsource-tauthorizationdomainresolver) |
|
|
27
|
+
| Look up an action, decision, or role constant | [Constants](#constants) |
|
|
28
|
+
| Read the scoped RBAC `.conf` model | [CASBIN_RBAC_DOMAIN_SCOPED_MODEL](#casbin_rbac_domain_scoped_model) |
|
|
29
|
+
| Register or resolve enforcers | [AuthorizationEnforcerRegistry](#authorizationenforcerregistry) |
|
|
30
|
+
| Understand how the Casbin enforcer builds and evaluates rules | [CasbinAuthorizationEnforcer](#casbinauthorizationenforcer) |
|
|
31
|
+
| Use the ready-made Postgres adapter | [ScopedCasbinAdapter](#scopedcasbinadapter) |
|
|
32
|
+
| Write a custom adapter | [BaseFilteredAdapter](#basefilteredadapter) |
|
|
33
|
+
| Grant a subset of a subject's operations | [Subset grants (custom rows)](#subset-grants-custom-rows) and [GrantBuilder.planGrant](#grantbuilderplangrant) |
|
|
34
|
+
| Seed `PolicyDefinition` / `Permission` rows | [Policy and permission builders](#policy-and-permission-builders) |
|
|
35
|
+
| Wire `authorize` into a REST or gRPC controller | [Controller integration](#controller-integration) |
|
|
36
|
+
| Read the context keys the middleware sets | [Context variables](#context-variables) |
|
|
37
|
+
|
|
38
|
+
## Import paths
|
|
2
39
|
|
|
3
|
-
|
|
40
|
+
```typescript
|
|
41
|
+
import {
|
|
42
|
+
// Component & middleware
|
|
43
|
+
AuthorizeComponent, AuthorizationProvider, authorize,
|
|
44
|
+
|
|
45
|
+
// Registry
|
|
46
|
+
AuthorizationEnforcerRegistry,
|
|
47
|
+
|
|
48
|
+
// Enforcers
|
|
49
|
+
CasbinAuthorizationEnforcer,
|
|
50
|
+
|
|
51
|
+
// Adapters
|
|
52
|
+
BaseFilteredAdapter, ScopedCasbinAdapter, PrincipalPolicyEdges,
|
|
53
|
+
|
|
54
|
+
// Scoped RBAC model
|
|
55
|
+
CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
|
|
56
|
+
|
|
57
|
+
// Models
|
|
58
|
+
AuthorizationRole,
|
|
59
|
+
|
|
60
|
+
// Policy / permission catalog builders - AuthorizationPermissionBuilder.objectMatch is the
|
|
61
|
+
// resource-hierarchy matcher (register on a custom Casbin model)
|
|
62
|
+
AuthorizationPolicyBuilder, AuthorizationPermissionBuilder, GrantBuilder,
|
|
63
|
+
|
|
64
|
+
// Constants
|
|
65
|
+
Authorization, AuthorizationActions, AuthorizationDecisions, AuthorizationDomainScopes,
|
|
66
|
+
AuthorizationPolicyVariants, AuthorizationRoles, AuthorizationEnforcerTypes,
|
|
67
|
+
CasbinEnforcerModelDrivers, CasbinEnforcerCachedDrivers, CasbinRuleVariants,
|
|
68
|
+
CasbinDomainMatchingFunctions,
|
|
69
|
+
|
|
70
|
+
// Binding keys
|
|
71
|
+
AuthorizeBindingKeys,
|
|
72
|
+
} from '@venizia/ignis';
|
|
73
|
+
|
|
74
|
+
import type {
|
|
75
|
+
// Core interfaces
|
|
76
|
+
IAuthorizeOptions, IAuthorizationEnforcer, IAuthorizationSpec, IAuthorizationRequest,
|
|
77
|
+
IAuthorizationRole, IAuthorizationDomainSource, TAuthorizationDomainResolver,
|
|
78
|
+
|
|
79
|
+
// Casbin options
|
|
80
|
+
ICasbinEnforcerOptions, ICasbinEnforcerCachedRedis,
|
|
81
|
+
|
|
82
|
+
// Adapter types
|
|
83
|
+
ICasbinPolicyFilter, ICasbinPolicySource, IScopedCasbinEntities, IScopedCasbinPolicyFilter,
|
|
84
|
+
|
|
85
|
+
// Function & utility types
|
|
86
|
+
TAuthorizeFn, TAuthorizationVoter, TAuthorizationConditions, TRegistryDescriptor,
|
|
87
|
+
|
|
88
|
+
// Model-based authorization metadata
|
|
89
|
+
IModelAuthorizeSettings,
|
|
90
|
+
|
|
91
|
+
// Value types (from TConstValue)
|
|
92
|
+
TAuthorizationAction, TAuthorizationDecision, TAuthorizationEnforcerType,
|
|
93
|
+
TCasbinEnforcerCachedDriver, TCasbinEnforcerModelDriver, TCasbinRuleVariant,
|
|
94
|
+
TCasbinDomainMatchingFunction, TAuthorizationPolicyVariant, TAuthorizationDomainScope,
|
|
95
|
+
} from '@venizia/ignis';
|
|
96
|
+
```
|
|
4
97
|
|
|
5
98
|
## Architecture
|
|
6
99
|
|
|
7
|
-
### System
|
|
100
|
+
### System overview
|
|
8
101
|
|
|
9
102
|
```mermaid
|
|
10
103
|
graph TB
|
|
@@ -22,22 +115,10 @@ graph TB
|
|
|
22
115
|
Registry --> Casbin["CasbinAuthorizationEnforcer<br/>+ FilteredAdapter"]
|
|
23
116
|
Registry --> Custom["Custom Enforcer"]
|
|
24
117
|
|
|
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
|
|
118
|
+
Provider --> Pipeline["7-Step Middleware Pipeline"]
|
|
38
119
|
```
|
|
39
120
|
|
|
40
|
-
### Middleware
|
|
121
|
+
### Middleware pipeline
|
|
41
122
|
|
|
42
123
|
```mermaid
|
|
43
124
|
flowchart TD
|
|
@@ -52,9 +133,11 @@ flowchart TD
|
|
|
52
133
|
Voters -->|DENY| E403a[/403 Denied by voter/]
|
|
53
134
|
Voters -->|ALLOW| Next4([next - voter allow])
|
|
54
135
|
Voters -->|ABSTAIN / none| HasEnforcers{Enforcers registered?}
|
|
55
|
-
HasEnforcers -->|No| Next6([next -
|
|
136
|
+
HasEnforcers -->|No, defaultDecision: allow| Next6([next - allow, warning logged])
|
|
137
|
+
HasEnforcers -->|No, defaultDecision: deny or unset| E403c[/403 no enforcer registered/]
|
|
56
138
|
HasEnforcers -->|Yes| Resolve[Resolve enforcer by name]
|
|
57
|
-
Resolve -->
|
|
139
|
+
Resolve --> ResolveDomain["Resolve domain (if spec.domain or domainResolver)"]
|
|
140
|
+
ResolveDomain --> Cache{Rules cached?}
|
|
58
141
|
Cache -->|Yes| Evaluate
|
|
59
142
|
Cache -->|No| PType{principalType?}
|
|
60
143
|
PType -->|Missing| E400[/400 principalType required/]
|
|
@@ -69,15 +152,11 @@ flowchart TD
|
|
|
69
152
|
Default -->|DENY| E403b
|
|
70
153
|
```
|
|
71
154
|
|
|
72
|
-
### Class
|
|
155
|
+
### Class hierarchy
|
|
73
156
|
|
|
74
157
|
```mermaid
|
|
75
158
|
classDiagram
|
|
76
|
-
class BaseHelper {
|
|
77
|
-
+logger
|
|
78
|
-
+scope
|
|
79
|
-
}
|
|
80
|
-
|
|
159
|
+
class BaseHelper { +logger +scope }
|
|
81
160
|
class AbstractAuthRegistry~TItem~ {
|
|
82
161
|
#descriptors: Map
|
|
83
162
|
#getBindingPrefix()* string
|
|
@@ -87,7 +166,6 @@ classDiagram
|
|
|
87
166
|
#registerDescriptor(opts) void
|
|
88
167
|
#resolveDescriptor(opts) TItem
|
|
89
168
|
}
|
|
90
|
-
|
|
91
169
|
class AuthorizationEnforcerRegistry {
|
|
92
170
|
-instance$ AuthorizationEnforcerRegistry
|
|
93
171
|
-configuredEnforcers: Set
|
|
@@ -97,7 +175,6 @@ classDiagram
|
|
|
97
175
|
+resolveEnforcer(opts) Promise
|
|
98
176
|
+resolveOptions() IAuthorizeOptions
|
|
99
177
|
}
|
|
100
|
-
|
|
101
178
|
class IAuthorizationEnforcer {
|
|
102
179
|
<<interface>>
|
|
103
180
|
+name: string
|
|
@@ -105,7 +182,6 @@ classDiagram
|
|
|
105
182
|
+buildRules(opts) TRules
|
|
106
183
|
+evaluate(opts) TAuthorizationDecision
|
|
107
184
|
}
|
|
108
|
-
|
|
109
185
|
class CasbinAuthorizationEnforcer {
|
|
110
186
|
-pool: BasePoolHelper~Enforcer~
|
|
111
187
|
-pendingLineFetches: Map
|
|
@@ -113,10 +189,9 @@ classDiagram
|
|
|
113
189
|
+destroy() void
|
|
114
190
|
+buildRules(opts) ICasbinRules
|
|
115
191
|
+evaluate(opts) TAuthorizationDecision
|
|
116
|
-
+invalidateUserCache(opts)?
|
|
192
|
+
+invalidateUserCache(opts)?
|
|
117
193
|
+rebuildUserCache(opts)?
|
|
118
194
|
}
|
|
119
|
-
|
|
120
195
|
class BaseFilteredAdapter~TFilter~ {
|
|
121
196
|
<<abstract>>
|
|
122
197
|
#dataSource: ICasbinPolicySource
|
|
@@ -125,17 +200,10 @@ classDiagram
|
|
|
125
200
|
+isFiltered() boolean
|
|
126
201
|
#loadLines(opts) void
|
|
127
202
|
}
|
|
128
|
-
|
|
129
203
|
class ScopedCasbinAdapter {
|
|
130
204
|
#entities: IScopedCasbinEntities
|
|
131
205
|
+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
206
|
}
|
|
138
|
-
|
|
139
207
|
BaseHelper <|-- AbstractAuthRegistry
|
|
140
208
|
AbstractAuthRegistry <|-- AuthorizationEnforcerRegistry
|
|
141
209
|
IAuthorizationEnforcer <|.. CasbinAuthorizationEnforcer
|
|
@@ -144,155 +212,423 @@ classDiagram
|
|
|
144
212
|
BaseFilteredAdapter <|-- ScopedCasbinAdapter
|
|
145
213
|
```
|
|
146
214
|
|
|
147
|
-
### Module
|
|
215
|
+
### Module file layout
|
|
148
216
|
|
|
149
217
|
```
|
|
150
|
-
auth/authorize/
|
|
218
|
+
components/auth/authorize/
|
|
151
219
|
├── adapters/
|
|
152
220
|
│ ├── base-filtered.ts # BaseFilteredAdapter (thin abstract) + ICasbinPolicyFilter
|
|
153
221
|
│ ├── scoped-casbin.adapter.ts # ScopedCasbinAdapter (generic edge-table reader)
|
|
154
|
-
│ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable
|
|
222
|
+
│ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable, ICasbinPolicySource
|
|
223
|
+
├── builders/
|
|
224
|
+
│ ├── grant.builder.ts # GrantBuilder
|
|
225
|
+
│ ├── permission.builder.ts # AuthorizationPermissionBuilder + static objectMatch
|
|
226
|
+
│ └── policy.builder.ts # AuthorizationPolicyBuilder
|
|
155
227
|
├── common/
|
|
156
|
-
│ ├── constants.ts
|
|
157
|
-
│
|
|
158
|
-
│ │ # AuthorizationRoles, AuthorizationEnforcerTypes,
|
|
159
|
-
│ │ # CasbinEnforcerCachedDrivers, CasbinEnforcerModelDrivers,
|
|
160
|
-
│ │ # CasbinRuleVariants
|
|
161
|
-
│ ├── object-match.ts # objectMatch resource-hierarchy matcher
|
|
162
|
-
│ ├── keys.ts # AuthorizeBindingKeys
|
|
163
|
-
│ ├── types.ts # IAuthorizeOptions, IAuthorizationEnforcer,
|
|
164
|
-
│ │ # IAuthorizationSpec, ICasbinEnforcerOptions, etc.
|
|
165
|
-
│ └── index.ts # Barrel export
|
|
228
|
+
│ ├── constants.ts # Authorization, Actions, Decisions, PolicyVariants, Roles, AuthorizeBindingKeys, ...
|
|
229
|
+
│ └── types.ts # IAuthorizeOptions, IAuthorizationEnforcer, ICasbinEnforcerOptions, ...
|
|
166
230
|
├── enforcers/
|
|
167
231
|
│ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
|
|
168
232
|
│ ├── enforcer-registry.ts # AuthorizationEnforcerRegistry (singleton)
|
|
169
|
-
│
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
│ └── index.ts # Barrel export
|
|
173
|
-
├── middlewares/
|
|
174
|
-
│ └── authorize.middleware.ts # authorize() standalone function
|
|
175
|
-
├── models/
|
|
176
|
-
│ ├── authorization-role.model.ts # AuthorizationRole
|
|
177
|
-
│ └── index.ts
|
|
233
|
+
│ └── models/rbac-domain.model.ts # CASBIN_RBAC_DOMAIN_SCOPED_MODEL
|
|
234
|
+
├── middlewares/authorize.middleware.ts # authorize() standalone function
|
|
235
|
+
├── models/authorization-role.model.ts # AuthorizationRole
|
|
178
236
|
├── providers/
|
|
179
|
-
│
|
|
180
|
-
|
|
181
|
-
└──
|
|
237
|
+
│ ├── authorization.provider.ts # AuthorizationProvider
|
|
238
|
+
│ └── request-domain.ts # resolveRequestDomain, readDeclarative
|
|
239
|
+
└── component.ts # AuthorizeComponent
|
|
182
240
|
```
|
|
183
241
|
|
|
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
|
|
242
|
+
### Design decisions
|
|
194
243
|
|
|
195
244
|
| Decision | Rationale |
|
|
196
245
|
|----------|-----------|
|
|
197
|
-
|
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
246
|
+
| Enforcer-based | Pluggable architecture - swap Casbin for a custom enforcer without changing route configs |
|
|
247
|
+
| Registry + co-located options | Enforcer class, name, type, and options are registered together - no split configuration |
|
|
248
|
+
| Type-discriminated enforcers | `type: 'casbin' \| 'custom'` in registry constrains `options` (`ICasbinEnforcerOptions` vs `unknown`) |
|
|
249
|
+
| Voter pattern | Custom logic that short-circuits before the enforcer |
|
|
250
|
+
| Rules caching | Built rules cached on the Hono context per-request - avoids rebuilding for multi-spec routes |
|
|
251
|
+
| Registry singleton | Mirrors `AuthenticationStrategyRegistry` - shares `AbstractAuthRegistry<T>` |
|
|
252
|
+
| Filtered adapter pattern | `BaseFilteredAdapter` is a thin read-only base; subclasses implement only `loadFilteredPolicy` |
|
|
253
|
+
| No-enforcer fallback | No enforcers registered -> the middleware honors `defaultDecision`: `deny` (default) throws a named 403, `allow` proceeds and logs a warning |
|
|
254
|
+
| Single edge table (scoped model) | `ScopedCasbinAdapter` reads one `PolicyDefinition` table for every edge type - no per-relation tables |
|
|
206
255
|
|
|
207
|
-
##
|
|
256
|
+
## AuthorizeComponent
|
|
208
257
|
|
|
209
|
-
|
|
258
|
+
`AuthorizeComponent extends BaseComponent`. Its `binding()` runs at application startup:
|
|
210
259
|
|
|
211
260
|
| Step | Action | Failure |
|
|
212
261
|
|------|--------|---------|
|
|
213
|
-
| 1 | Resolve `IAuthorizeOptions` from container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
|
|
214
|
-
| 2 |
|
|
262
|
+
| 1 | Resolve `IAuthorizeOptions` from the container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
|
|
263
|
+
| 2 | `bindAlwaysAllowRoles()` - binds `alwaysAllowRoles` to `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` if present | Skipped if no roles configured |
|
|
215
264
|
|
|
216
265
|
```typescript
|
|
217
266
|
class AuthorizeComponent extends BaseComponent {
|
|
218
|
-
constructor(
|
|
219
|
-
@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
|
|
220
|
-
) { ... }
|
|
221
|
-
|
|
267
|
+
constructor(@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication);
|
|
222
268
|
override binding(): ValueOrPromise<void>;
|
|
223
269
|
private bindAlwaysAllowRoles(opts: { options: IAuthorizeOptions }): void;
|
|
224
270
|
}
|
|
225
271
|
```
|
|
226
272
|
|
|
227
273
|
> [!NOTE]
|
|
228
|
-
>
|
|
274
|
+
> Enforcer registration is separate - `AuthorizeComponent` only validates global options. Register enforcers via `AuthorizationEnforcerRegistry.register()`.
|
|
229
275
|
|
|
230
|
-
|
|
276
|
+
Source -> [`component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/component.ts)
|
|
231
277
|
|
|
232
|
-
|
|
278
|
+
## Binding keys
|
|
233
279
|
|
|
234
|
-
|
|
280
|
+
| Key | Constant | Type | Description |
|
|
281
|
+
|-----|----------|------|-------------|
|
|
282
|
+
| `@app/authorize/options` | `AuthorizeBindingKeys.OPTIONS` | `IAuthorizeOptions` | Global authorization options |
|
|
283
|
+
| `@app/authorize/always-allow-roles` | `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` | `string[]` | Auto-bound by the component if present in options |
|
|
284
|
+
| `@app/authorize/enforcers/{name}/options` | `AuthorizeBindingKeys.enforcerOptions(name)` | `ICasbinEnforcerOptions \| unknown` | Per-enforcer options, auto-bound by the registry |
|
|
235
285
|
|
|
236
286
|
```typescript
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
287
|
+
class AuthorizeBindingKeys {
|
|
288
|
+
static readonly OPTIONS = '@app/authorize/options';
|
|
289
|
+
static readonly ALWAYS_ALLOW_ROLES = '@app/authorize/always-allow-roles';
|
|
290
|
+
static enforcerOptions(name: string): string {
|
|
291
|
+
return `@app/authorize/enforcers/${name}/options`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
241
294
|
```
|
|
242
295
|
|
|
243
|
-
|
|
296
|
+
`AuthorizeBindingKeys.enforcerOptions(name)` is called automatically by `AuthorizationEnforcerRegistry.register()` when `options` is provided; `CasbinAuthorizationEnforcer` injects its options from `AuthorizeBindingKeys.enforcerOptions('casbin')`.
|
|
297
|
+
|
|
298
|
+
Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/constants.ts)
|
|
299
|
+
|
|
300
|
+
## Option interfaces
|
|
301
|
+
|
|
302
|
+
### IAuthorizeOptions
|
|
303
|
+
|
|
304
|
+
Global settings, bound before registering `AuthorizeComponent`.
|
|
305
|
+
|
|
306
|
+
| Option | Type | Default | Description |
|
|
307
|
+
|--------|------|---------|-------------|
|
|
308
|
+
| `defaultDecision` | `TAuthorizationDecision` | - | **Required.** Decision applied when the enforcer returns `ABSTAIN` |
|
|
309
|
+
| `alwaysAllowRoles` | `string[]` | `[]` | Roles that bypass all authorization checks (global) |
|
|
310
|
+
| `domainResolver` | `TAuthorizationDomainResolver` | - | Fallback domain resolver used when a route's `spec.domain` is not set. Returns `{ type, id }` or `null` (-> `SYSTEM_WIDE`) |
|
|
244
311
|
|
|
245
312
|
```typescript
|
|
246
|
-
|
|
247
|
-
|
|
313
|
+
interface IAuthorizeOptions {
|
|
314
|
+
defaultDecision: TAuthorizationDecision;
|
|
315
|
+
alwaysAllowRoles?: string[];
|
|
316
|
+
domainResolver?: TAuthorizationDomainResolver;
|
|
317
|
+
}
|
|
318
|
+
```
|
|
248
319
|
|
|
249
|
-
|
|
320
|
+
### ICasbinEnforcerOptions
|
|
250
321
|
|
|
251
|
-
|
|
252
|
-
protected abstract getBindingPrefix(): string;
|
|
322
|
+
Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistry.register()`.
|
|
253
323
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
324
|
+
| Option | Type | Default | Description |
|
|
325
|
+
|--------|------|---------|-------------|
|
|
326
|
+
| `model` | `{ driver: 'file', definition } \| { driver: 'text', definition }` | - | **Required.** Casbin model (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
|
|
327
|
+
| `cached` | `{ use: false } \| (ICasbinEnforcerCachedRedis & { use: true })` | - | **Required.** Caching configuration (Redis-only) |
|
|
328
|
+
| `adapter` | `Adapter` | - | Casbin adapter instance (e.g. `ScopedCasbinAdapter`) |
|
|
329
|
+
| `isScoped` | `boolean` | `false` | Enables the scoped model: 4-token `(sub, dom, obj, act)` requests. Auto-registers `keyMatch`, `objectMatch`, and `ResourceRoleManager` - see [`CasbinAuthorizationEnforcer`](#casbinauthorizationenforcer) |
|
|
330
|
+
| `poolSize` | `number` | `16` | Pooled enforcers (each request enforces on its own borrowed instance) |
|
|
331
|
+
| `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
|
|
332
|
+
| `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | - | Custom (non-scoped) payload normalizer, run before evaluation |
|
|
333
|
+
| `domainMatching` | `{ roleDefinition: string; fn: TCasbinDomainMatchingFunction }` | - | Opt-in domain matching function for the flat model. **Not needed when `isScoped: true`** |
|
|
258
334
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
335
|
+
```typescript
|
|
336
|
+
interface ICasbinEnforcerOptions<E extends Env = Env, TAction = string, TResource = string, TAdapter = Adapter> {
|
|
337
|
+
model: { driver: 'file'; definition: string } | { driver: 'text'; definition: string };
|
|
338
|
+
cached: { use: false } | (ICasbinEnforcerCachedRedis & { use: true });
|
|
339
|
+
adapter?: TAdapter;
|
|
340
|
+
isScoped?: boolean;
|
|
341
|
+
poolSize?: number;
|
|
342
|
+
poolAcquireTimeoutMs?: number;
|
|
343
|
+
normalizePayloadFn?(opts: { user: IAuthUser; action: TAction; resource: TResource; context: TContext<E, string> }): {
|
|
344
|
+
subject: string; resource: string; action: string; domain?: string;
|
|
345
|
+
};
|
|
346
|
+
domainMatching?: { roleDefinition: string; fn: TCasbinDomainMatchingFunction };
|
|
262
347
|
}
|
|
263
348
|
```
|
|
264
349
|
|
|
265
|
-
|
|
350
|
+
> [!NOTE]
|
|
351
|
+
> `cached.options.expiresIn` must be `>= 10_000` ms (`MIN_EXPIRES_IN`). Caching is **Redis-only** - the in-memory driver was removed.
|
|
266
352
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
353
|
+
### Domain hierarchy edges (`g3`)
|
|
354
|
+
|
|
355
|
+
There is no enforcer-level option for domain hierarchy - a role assignment (`g`), a grant (`g3`), or a domain membership (`g2`) declared at a parent domain reaching its children is driven entirely by `g3` policy lines already present in a principal's own line set, whenever `isScoped: true`. Two sources produce those lines - see `resolveDomainEdges` on [`ScopedCasbinAdapter`](#scopedcasbinadapter) below for the second one:
|
|
356
|
+
|
|
357
|
+
- `domain_inherits` rows reachable from the principal's domain closure (`ScopedCasbinAdapter`'s `DOMAIN_EDGE` branch, always on).
|
|
358
|
+
- `ScopedCasbinAdapter`'s `resolveDomainEdges` constructor hook, for a hierarchy the app already owns on a business table.
|
|
359
|
+
|
|
360
|
+
`registerMatchers()` (see [`configure()`](#configure) below) wires this unconditionally for every scoped model, sharing one overlay across the three role managers. Freshness is whatever the per-user policy-line cache already guarantees; there is no separate TTL or invalidation call to reason about.
|
|
361
|
+
|
|
362
|
+
**Cache configuration (discriminated union):**
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
interface { use: false } // every request rebuilds the user's policy from the datasource
|
|
366
|
+
|
|
367
|
+
interface ICasbinEnforcerCachedRedis {
|
|
368
|
+
driver: 'redis';
|
|
369
|
+
options: {
|
|
370
|
+
connection: IRedisHelper;
|
|
371
|
+
expiresIn: number;
|
|
372
|
+
keyFn: (opts: { user: IAuthorizationUser }) => ValueOrPromise<string>;
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### IAuthorizationSpec (route-level)
|
|
378
|
+
|
|
379
|
+
| Option | Type | Default | Description |
|
|
380
|
+
|--------|------|---------|-------------|
|
|
381
|
+
| `action` | `TAction` | - | **Required.** Action being performed (e.g. `'read'`, `'create'`) |
|
|
382
|
+
| `resource` | `TResource` | - | **Required.** Resource being accessed (e.g. `'Article'`) |
|
|
383
|
+
| `conditions` | `TAuthorizationConditions` | - | Key-value ABAC conditions. Plumbed into `request.conditions`; the built-in Casbin enforcer does not read it - only custom enforcers/voters can |
|
|
384
|
+
| `allowedRoles` | `string[]` | - | Roles that bypass the enforcer for this route |
|
|
385
|
+
| `voters` | `TAuthorizationVoter[]` | - | Custom voter functions for this route |
|
|
386
|
+
| `domain` | `IAuthorizationDomainSource \| TAuthorizationDomainResolver` | - | Per-route domain source for scoped RBAC. Omitted -> falls back to the global `domainResolver`, then `SYSTEM_WIDE` |
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
interface IAuthorizationSpec<E extends Env = Env, TAction = string, TResource = string> {
|
|
390
|
+
action: TAction;
|
|
391
|
+
resource: TResource;
|
|
392
|
+
conditions?: TAuthorizationConditions;
|
|
393
|
+
allowedRoles?: string[];
|
|
394
|
+
voters?: TAuthorizationVoter<E, TAction, TResource>[];
|
|
395
|
+
domain?: IAuthorizationDomainSource | TAuthorizationDomainResolver<E>;
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### IAuthorizationDomainSource / TAuthorizationDomainResolver
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
interface IAuthorizationDomainSource {
|
|
403
|
+
from: 'param' | 'header' | 'query' | 'context';
|
|
404
|
+
key: string;
|
|
405
|
+
type: string; // domain type, e.g. 'Merchant'
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
type TAuthorizationDomainResolver<E extends Env = Env> = (opts: {
|
|
409
|
+
context: TContext<E, string>;
|
|
410
|
+
}) => ValueOrPromise<TNullable<{ type: string; id: IdType }>>;
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
`resolveRequestDomain()` (`providers/request-domain.ts`) turns either shape into a casbin domain string. Precedence: `spec.domain` (resolver, then declarative `readDeclarative()`) -> `IAuthorizeOptions.domainResolver` -> `AuthorizationDomainScopes.SYSTEM_WIDE`.
|
|
414
|
+
|
|
415
|
+
`readDeclarative()` reads `context.req.param/header/query()` for `'param'|'header'|'query'`, or `context.get(key)` for `'context'`.
|
|
416
|
+
|
|
417
|
+
### TAuthorizationConditions / TAuthorizationVoter / TAuthorizeFn
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
type TAuthorizationConditions<KeyType extends string | symbol = string | symbol, ValueType = string | number | boolean | null> =
|
|
421
|
+
Record<KeyType, ValueType>;
|
|
422
|
+
|
|
423
|
+
type TAuthorizationVoter<E extends Env = Env, TAction = string, TResource = string> = (opts: {
|
|
424
|
+
user: IAuthUser; action: TAction; resource: TResource; context: TContext<E, string>;
|
|
425
|
+
}) => ValueOrPromise<TAuthorizationDecision>;
|
|
426
|
+
|
|
427
|
+
type TAuthorizeFn<E extends Env = Env, TAction = string, TResource = string> = (opts: {
|
|
428
|
+
spec: IAuthorizationSpec<E, TAction, TResource>;
|
|
429
|
+
enforcerName?: string;
|
|
430
|
+
}) => MiddlewareHandler;
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### IAuthorizationRequest
|
|
434
|
+
|
|
435
|
+
The request object built by the provider and passed to `evaluate()`.
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
interface IAuthorizationRequest<TAction = string, TResource = string> {
|
|
439
|
+
action: TAction;
|
|
440
|
+
resource: TResource;
|
|
441
|
+
conditions?: TAuthorizationConditions;
|
|
442
|
+
/** Resolved domain scope: `"<DomainType>_<id>"` (e.g. `"Merchant_7"`) or the `"SYSTEM_WIDE"` sentinel. */
|
|
443
|
+
domain?: string;
|
|
444
|
+
}
|
|
445
|
+
```
|
|
274
446
|
|
|
275
|
-
|
|
447
|
+
Source -> [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/types.ts)
|
|
276
448
|
|
|
277
|
-
|
|
278
|
-
|----------|------------------------------|
|
|
279
|
-
| `AuthenticationStrategyRegistry` | `Authentication.STRATEGY` |
|
|
280
|
-
| `AuthorizationEnforcerRegistry` | `Authorization.ENFORCER` (`'authorization.enforcer'`) |
|
|
449
|
+
## Constants
|
|
281
450
|
|
|
282
|
-
|
|
451
|
+
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>`.
|
|
283
452
|
|
|
284
|
-
|
|
453
|
+
**`Authorization`** - context keys.
|
|
285
454
|
|
|
286
|
-
|
|
455
|
+
| Constant | Value | Description |
|
|
456
|
+
|----------|-------|-------------|
|
|
457
|
+
| `Authorization.RULES` | `'authorization.rules'` | Context key for cached rules |
|
|
458
|
+
| `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | Context key to dynamically skip authorization |
|
|
459
|
+
| `Authorization.ENFORCER` | `'authorization.enforcer'` | Binding key prefix for enforcers |
|
|
460
|
+
| `Authorization.DOMAIN` | `'authorization.domain'` | Context key for the resolved request domain scope |
|
|
287
461
|
|
|
462
|
+
**`AuthorizationActions`** - `CREATE` `UPDATE` `DELETE` `EXECUTE` `READ` `WRITE` `MANAGE` `CUSTOM`. `AuthorizationActions.LATTICE` declares the standard action hierarchy consumed by `AuthorizationPolicyBuilder.actionLattice()`:
|
|
463
|
+
|
|
464
|
+
| `child` | `parent` |
|
|
465
|
+
|---|---|
|
|
466
|
+
| `READ`, `WRITE`, `EXECUTE` | `MANAGE` |
|
|
467
|
+
| `CREATE`, `UPDATE`, `DELETE` | `WRITE` |
|
|
468
|
+
|
|
469
|
+
`CUSTOM` (`'custom'`) is a grant-mode marker for a subset grant carrying `metadata.ops` - see [Subset grants](#subset-grants-custom-rows). It is deliberately absent from `LATTICE`: it names an encoding, not a position in the action hierarchy.
|
|
470
|
+
|
|
471
|
+
**`AuthorizationDecisions`** - `ALLOW` `DENY` `ABSTAIN`.
|
|
472
|
+
|
|
473
|
+
| Method | String check | Number check |
|
|
474
|
+
|--------|-------------|--------------|
|
|
475
|
+
| `isAllow(input)` | `input.toLowerCase() === 'allow'` | `input > 0` |
|
|
476
|
+
| `isDeny(input)` | `input.toLowerCase() === 'deny'` | `input < 0` |
|
|
477
|
+
| `isAbstain(input)` | `input.toLowerCase() === 'abstain'` | `input === 0` |
|
|
478
|
+
|
|
479
|
+
**`AuthorizationEnforcerTypes`** - `CASBIN` (`'casbin'`), `CUSTOM` (`'custom'`).
|
|
480
|
+
|
|
481
|
+
**`CasbinEnforcerModelDrivers`** - `FILE` (`'file'`, load from a `.conf` path), `TEXT` (`'text'`, inline string).
|
|
482
|
+
|
|
483
|
+
**`CasbinEnforcerCachedDrivers`** - `REDIS` (`'redis'`) is the only driver; the in-memory driver was removed.
|
|
484
|
+
|
|
485
|
+
**`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`).
|
|
486
|
+
|
|
487
|
+
| Constant | Value | Description |
|
|
488
|
+
|----------|-------|-------------|
|
|
489
|
+
| `KEY_MATCH` | `'keyMatch'` | `*` is the only wildcard; exact compare otherwise (recommended for `Merchant_<uuid>`-style domains) |
|
|
490
|
+
| `KEY_MATCH_2` | `'keyMatch2'` | Adds URL-path `:param` segment matching |
|
|
491
|
+
| `KEY_MATCH_3` | `'keyMatch3'` | Adds `{param}` segment matching |
|
|
492
|
+
| `KEY_MATCH_4` | `'keyMatch4'` | `{param}` with repeated-name equality checks |
|
|
493
|
+
| `REGEX_MATCH` | `'regexMatch'` | Treats the stored/policy value as a full regular expression |
|
|
494
|
+
|
|
495
|
+
> [!IMPORTANT]
|
|
496
|
+
> Applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side.
|
|
497
|
+
>
|
|
498
|
+
> | Call | Result |
|
|
499
|
+
> |---|---|
|
|
500
|
+
> | `keyMatch("Merchant_X", "*")` | `true` |
|
|
501
|
+
> | `keyMatch("Merchant_X", "Merchant_X")` | `true` |
|
|
502
|
+
> | `keyMatch("Merchant_X", "Merchant_Y")` | `false` |
|
|
503
|
+
|
|
504
|
+
**`CasbinRuleVariants`** - the Casbin line prefixes declared by the scoped model, numbered in request-tuple order (`sub -> dom -> obj -> act`).
|
|
505
|
+
|
|
506
|
+
| Constant | Value | Relation |
|
|
507
|
+
|----------|-------|----------|
|
|
508
|
+
| `P` | `'p'` | Permission policy line |
|
|
509
|
+
| `G` | `'g'` | Role membership + role inheritance (the `sub` axis) |
|
|
510
|
+
| `G2` | `'g2'` | User -> domain membership (the `dom` axis) |
|
|
511
|
+
| `G3` | `'g3'` | Domain hierarchy (the `dom` axis) |
|
|
512
|
+
| `G4` | `'g4'` | Resource hierarchy (the `obj` axis, served by `ResourceRoleManager`) |
|
|
513
|
+
| `G5` | `'g5'` | Action hierarchy (the `act` axis) |
|
|
514
|
+
|
|
515
|
+
**`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).
|
|
516
|
+
|
|
517
|
+
| Variant | `action` (DB) | `rule` | Meaning |
|
|
518
|
+
|---------|---------------|--------|---------|
|
|
519
|
+
| `GRANT` | `'grant'` | `p` | Give a permission to a User or Role |
|
|
520
|
+
| `ASSIGN_ROLE` | `'assign_role'` | `g` | Give a User a Role (optionally domain-scoped) |
|
|
521
|
+
| `ROLE_INHERITS` | `'role_inherits'` | `g` | Role inherits another Role |
|
|
522
|
+
| `JOIN_DOMAIN` | `'join_domain'` | `g2` | User is a member of a Domain |
|
|
523
|
+
| `DOMAIN_INHERITS` | `'domain_inherits'` | `g3` | Domain nested under a parent Domain |
|
|
524
|
+
| `RESOURCE_INHERITS` | `'resource_inherits'` | `g4` | Resource nested under a broader Resource |
|
|
525
|
+
| `ACTION_INHERITS` | `'action_inherits'` | `g5` | Action implied by a broader Action |
|
|
526
|
+
|
|
527
|
+
`isValidAction(input)` / `isValidRule(input)` check membership; `ACTION_SCHEME_SET` / `RULE_SCHEME_SET` hold the sets.
|
|
528
|
+
|
|
529
|
+
**`AuthorizationDomainScopes`** - sentinel domain values on `grant` rows.
|
|
530
|
+
|
|
531
|
+
| Constant | Value | Meaning |
|
|
532
|
+
|----------|-------|---------|
|
|
533
|
+
| `ANY_MEMBER` | `'ANY_MEMBER'` | Applies in every domain the subject joined (checked via `g2`) |
|
|
534
|
+
| `SYSTEM_WIDE` | `'SYSTEM_WIDE'` | Applies system-wide, bypassing membership (super-admin) |
|
|
535
|
+
|
|
536
|
+
**`AuthorizationRoles`** - built-in role identifiers (see [AuthorizationRole](#authorizationrole)).
|
|
537
|
+
|
|
538
|
+
| Constant | Identifier | Priority |
|
|
539
|
+
|----------|------------|----------|
|
|
540
|
+
| `SUPER_ADMIN` | `'999_super-admin'` | 999 |
|
|
541
|
+
| `ADMIN` | `'900_admin'` | 900 |
|
|
542
|
+
| `USER` | `'010_user'` | 10 |
|
|
543
|
+
| `GUEST` | `'001_guest'` | 1 |
|
|
544
|
+
| `UNKNOWN_USER` | `'000_unknown-user'` | 0 |
|
|
545
|
+
|
|
546
|
+
Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/constants.ts)
|
|
547
|
+
|
|
548
|
+
## CASBIN_RBAC_DOMAIN_SCOPED_MODEL
|
|
549
|
+
|
|
550
|
+
The exported `.conf` text for the scoped model - pass it to `ICasbinEnforcerOptions.model` with `driver: CasbinEnforcerModelDrivers.TEXT` and `isScoped: true`.
|
|
551
|
+
|
|
552
|
+
```ini
|
|
553
|
+
[request_definition]
|
|
554
|
+
r = sub, dom, obj, act
|
|
555
|
+
|
|
556
|
+
[policy_definition]
|
|
557
|
+
p = sub, dom, obj, act, eft
|
|
558
|
+
|
|
559
|
+
[role_definition]
|
|
560
|
+
g = _, _, _
|
|
561
|
+
g2 = _, _
|
|
562
|
+
g3 = _, _
|
|
563
|
+
g4 = _, _
|
|
564
|
+
g5 = _, _
|
|
565
|
+
|
|
566
|
+
[policy_effect]
|
|
567
|
+
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
|
|
568
|
+
|
|
569
|
+
[matchers]
|
|
570
|
+
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
571
|
```
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
572
|
+
|
|
573
|
+
| Relation | Axis | Meaning |
|
|
574
|
+
|---|---|---|
|
|
575
|
+
| `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 |
|
|
576
|
+
| `g2` | `dom` (membership) | `join_domain` - powers the `ANY_MEMBER` grant scope |
|
|
577
|
+
| `g3` | `dom` (nesting) | `domain_inherits`, plus a self-link so an exact domain always matches itself |
|
|
578
|
+
| `g4` | `obj` | `resource_inherits` - explicit non-standard nesting edges; served by `ResourceRoleManager`, not a matching function |
|
|
579
|
+
| `g5` | `act` | `action_inherits`, plus a self-link |
|
|
580
|
+
|
|
581
|
+
**Effect** is casbin's `allow-and-deny` effector. A request needs a matching `allow` AND no matching `deny` - default-DENY. 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.
|
|
582
|
+
|
|
583
|
+
**Domain clause**, matched by `p.dom`:
|
|
584
|
+
|
|
585
|
+
| `p.dom` value | Matches |
|
|
586
|
+
|---|---|
|
|
587
|
+
| `SYSTEM_WIDE` | Every domain - bypasses membership, super-admin |
|
|
588
|
+
| `ANY_MEMBER` | Every domain the subject joined, via `g2` |
|
|
589
|
+
| `<Type>_<id>` | That domain, or a nested child via `g3` |
|
|
590
|
+
|
|
591
|
+
> [!NOTE]
|
|
592
|
+
> 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.
|
|
593
|
+
|
|
594
|
+
Source -> [`enforcers/models/rbac-domain.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/models/rbac-domain.model.ts)
|
|
595
|
+
|
|
596
|
+
## AbstractAuthRegistry
|
|
597
|
+
|
|
598
|
+
Shared base for the authentication strategy registry and `AuthorizationEnforcerRegistry`. Provides descriptor storage, binding-key generation, and DI resolution.
|
|
599
|
+
|
|
600
|
+
```typescript
|
|
601
|
+
abstract class AbstractAuthRegistry<TItem> extends BaseHelper {
|
|
602
|
+
protected descriptors: Map<string, TRegistryDescriptor<TItem>>;
|
|
603
|
+
constructor(opts: { scope: string });
|
|
604
|
+
protected abstract getBindingPrefix(): string;
|
|
605
|
+
|
|
606
|
+
getKey(opts: { name: string }): string; // `${prefix}.${name}`
|
|
607
|
+
getDefaultName(): string; // first registered descriptor (Map insertion order)
|
|
608
|
+
reset(): void; // clears the Map
|
|
609
|
+
|
|
610
|
+
protected registerDescriptor(opts: { container: Container; target: TClass<TItem>; name: string }): void;
|
|
611
|
+
protected resolveDescriptor(opts: { name: string }): TItem;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
type TRegistryDescriptor<TItem> = { container: Container; targetClass: TClass<TItem> };
|
|
293
615
|
```
|
|
294
616
|
|
|
295
|
-
|
|
617
|
+
| Method | Description | Throws |
|
|
618
|
+
|--------|-------------|--------|
|
|
619
|
+
| `getKey({ name })` | Builds the binding key | `[getKey] Invalid name` if empty |
|
|
620
|
+
| `getDefaultName()` | First registered descriptor's name | `[ClassName] No items registered` if none |
|
|
621
|
+
| `registerDescriptor(opts)` | Stores the descriptor + binds the class `SINGLETON` in DI | - |
|
|
622
|
+
| `resolveDescriptor({ name })` | Resolves the instance from the DI container | `Descriptor not found: {name}` or `Failed to resolve: {name}` |
|
|
623
|
+
| `reset()` | Clears all descriptors | - |
|
|
624
|
+
|
|
625
|
+
`AuthorizationEnforcerRegistry.getBindingPrefix()` returns `Authorization.ENFORCER` (`'authorization.enforcer'`).
|
|
626
|
+
|
|
627
|
+
Source -> [`base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/base/abstract-auth-registry.ts)
|
|
628
|
+
|
|
629
|
+
## AuthorizationEnforcerRegistry
|
|
630
|
+
|
|
631
|
+
Singleton, extends `AbstractAuthRegistry<IAuthorizationEnforcer>`.
|
|
296
632
|
|
|
297
633
|
```typescript
|
|
298
634
|
class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationEnforcer> {
|
|
@@ -300,199 +636,120 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
|
|
|
300
636
|
private configuredEnforcers: Set<string>;
|
|
301
637
|
|
|
302
638
|
static getInstance(): AuthorizationEnforcerRegistry;
|
|
303
|
-
override reset(): void;
|
|
639
|
+
override reset(): void; // clears descriptors + configuredEnforcers
|
|
304
640
|
|
|
305
|
-
|
|
641
|
+
register(opts: {
|
|
642
|
+
container: Container;
|
|
643
|
+
enforcers: Array<
|
|
644
|
+
| { enforcer: TClass<IAuthorizationEnforcer>; name: string; type: 'casbin'; options?: ICasbinEnforcerOptions }
|
|
645
|
+
| { enforcer: TClass<IAuthorizationEnforcer>; name: string; type: 'custom'; options?: unknown }
|
|
646
|
+
>;
|
|
647
|
+
}): this;
|
|
306
648
|
|
|
307
|
-
register(opts: { ... }): this;
|
|
308
649
|
hasEnforcers(): boolean;
|
|
309
650
|
getDefaultEnforcerName(): string;
|
|
310
651
|
resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer>;
|
|
311
652
|
resolveOptions(): IAuthorizeOptions | undefined;
|
|
653
|
+
invalidateUserCache(opts: { user: IAuthorizationUser; enforcerName?: string }): Promise<{ invalidatedKeys: number }>;
|
|
654
|
+
rebuildUserCache(opts: { user; enforcerName? }): Promise<{ cacheKey: string; lineCount: number }>;
|
|
312
655
|
}
|
|
313
656
|
```
|
|
314
657
|
|
|
315
|
-
### API
|
|
316
|
-
|
|
317
658
|
| Method | Returns | Description |
|
|
318
659
|
|--------|---------|-------------|
|
|
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
|
|
660
|
+
| `getInstance()` | `AuthorizationEnforcerRegistry` | Singleton instance (created on first call) |
|
|
661
|
+
| `register(opts)` | `this` | Registers enforcers with type-safe options (chainable) |
|
|
662
|
+
| `hasEnforcers()` | `boolean` | `descriptors.size > 0` - used by the middleware to decide whether to honor `defaultDecision` instead of resolving an enforcer |
|
|
663
|
+
| `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` |
|
|
664
|
+
| `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves + auto-configures once (`configuredEnforcers` Set) |
|
|
324
665
|
| `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`)
|
|
666
|
+
| `invalidateUserCache(opts)` | `Promise<{ invalidatedKeys }>` | Drops a user's cached policies - throws if the resolved enforcer lacks the optional method |
|
|
667
|
+
| `rebuildUserCache(opts)` | `Promise<{ cacheKey, lineCount }>` | Drops then immediately re-extracts + re-caches |
|
|
668
|
+
| `reset()` | `void` | Clears descriptors AND `configuredEnforcers` |
|
|
356
669
|
|
|
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`.
|
|
670
|
+
**`register()` behavior:**
|
|
359
671
|
|
|
360
|
-
|
|
672
|
+
- Validates no duplicate names within the call.
|
|
673
|
+
- Validates each name is not already registered.
|
|
674
|
+
- Binds each class as a singleton at `authorization.enforcer.{name}`.
|
|
675
|
+
- If `options` is given, binds it to `AuthorizeBindingKeys.enforcerOptions(name)`.
|
|
361
676
|
|
|
362
|
-
|
|
677
|
+
**Configure-once pattern:**
|
|
363
678
|
|
|
364
679
|
```typescript
|
|
365
680
|
async resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer> {
|
|
366
|
-
const enforcer = this.resolveDescriptor(opts);
|
|
367
|
-
|
|
681
|
+
const enforcer = this.resolveDescriptor(opts);
|
|
368
682
|
if (!this.configuredEnforcers.has(opts.name)) {
|
|
369
683
|
await enforcer.configure();
|
|
370
684
|
this.configuredEnforcers.add(opts.name);
|
|
371
685
|
}
|
|
372
|
-
|
|
373
686
|
return enforcer;
|
|
374
687
|
}
|
|
375
688
|
```
|
|
376
689
|
|
|
377
|
-
|
|
690
|
+
Source -> [`enforcers/enforcer-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/enforcer-registry.ts)
|
|
378
691
|
|
|
379
|
-
## IAuthorizationEnforcer
|
|
380
|
-
|
|
381
|
-
The core enforcer contract. All enforcers (Casbin, custom) must implement this interface.
|
|
692
|
+
## IAuthorizationEnforcer interface
|
|
382
693
|
|
|
383
694
|
```typescript
|
|
384
695
|
interface IAuthorizationEnforcer<
|
|
385
|
-
E extends Env = Env,
|
|
386
|
-
|
|
387
|
-
TResource = string,
|
|
388
|
-
TRules = unknown,
|
|
389
|
-
TBuildRulesReturn = ValueOrPromise<TRules>,
|
|
390
|
-
TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
|
|
696
|
+
E extends Env = Env, TAction = string, TResource = string, TRules = unknown,
|
|
697
|
+
TBuildRulesReturn = ValueOrPromise<TRules>, TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
|
|
391
698
|
> {
|
|
392
699
|
name: string;
|
|
393
|
-
|
|
394
700
|
configure(): ValueOrPromise<void>;
|
|
701
|
+
buildRules(opts: { user: IAuthorizationUser; context: TContext<E, string> }): TBuildRulesReturn;
|
|
702
|
+
evaluate(opts: { rules: TRules; request: IAuthorizationRequest<TAction, TResource>; context: TContext<E, string> }): TEvaluateReturn;
|
|
395
703
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}): TBuildRulesReturn;
|
|
400
|
-
|
|
401
|
-
evaluate(opts: {
|
|
402
|
-
rules: TRules;
|
|
403
|
-
request: IAuthorizationRequest<TAction, TResource>;
|
|
404
|
-
context: TContext<E, string>;
|
|
405
|
-
}): TEvaluateReturn;
|
|
704
|
+
/** Optional - implemented only by caching enforcers. */
|
|
705
|
+
invalidateUserCache?(opts: { user: IAuthorizationUser }): Promise<{ invalidatedKeys: number }>;
|
|
706
|
+
rebuildUserCache?(opts: { user: IAuthorizationUser }): Promise<{ cacheKey: string; lineCount: number }>;
|
|
406
707
|
}
|
|
407
708
|
```
|
|
408
709
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
| Parameter | Default | Description |
|
|
412
|
-
|-----------|---------|-------------|
|
|
710
|
+
| Generic | Default | Description |
|
|
711
|
+
|---------|---------|--------------|
|
|
413
712
|
| `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` |
|
|
713
|
+
| `TAction` / `TResource` | `string` | Action / resource type |
|
|
714
|
+
| `TRules` | `unknown` | Rules type produced by `buildRules`, consumed by `evaluate` |
|
|
417
715
|
| `TBuildRulesReturn` | `ValueOrPromise<TRules>` | Return type of `buildRules` |
|
|
418
716
|
| `TEvaluateReturn` | `ValueOrPromise<TAuthorizationDecision>` | Return type of `evaluate` |
|
|
419
717
|
|
|
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
718
|
| 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()`:
|
|
438
|
-
|
|
439
|
-
```typescript
|
|
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 |
|
|
719
|
+
|--------|-------|---------|-----------|
|
|
720
|
+
| `configure()` | - | `void` | Registry, on first `resolveEnforcer()` |
|
|
721
|
+
| `buildRules` | `{ user, context }` | `TRules` | Provider, pipeline step 6 |
|
|
722
|
+
| `evaluate` | `{ rules, request, context }` | `TAuthorizationDecision` | Provider, pipeline step 7 |
|
|
454
723
|
|
|
455
|
-
|
|
724
|
+
`invalidateUserCache`/`rebuildUserCache` are feature-detected at runtime (`typeof enforcer.invalidateUserCache === 'function'`) - only `CasbinAuthorizationEnforcer` with a Redis cache implements them.
|
|
456
725
|
|
|
457
|
-
|
|
726
|
+
## CasbinAuthorizationEnforcer
|
|
458
727
|
|
|
459
|
-
|
|
728
|
+
Wraps the `casbin` library (optional peer dependency). The adapter only loads from the database on a throwaway enforcer, to build one user's policy lines (cached in Redis if configured). Every request then evaluates on its own enforcer, borrowed from a `BasePoolHelper<Enforcer>` and freshly loaded with those lines. This isolates concurrency and keeps the database out of the hot path.
|
|
460
729
|
|
|
461
730
|
```typescript
|
|
462
|
-
class CasbinAuthorizationEnforcer<
|
|
463
|
-
E extends Env = Env,
|
|
464
|
-
TAction extends string = string,
|
|
465
|
-
TResource extends string = string,
|
|
466
|
-
>
|
|
731
|
+
class CasbinAuthorizationEnforcer<E extends Env = Env, TAction extends string = string, TResource extends string = string>
|
|
467
732
|
extends BaseHelper
|
|
468
733
|
implements IAuthorizationEnforcer<E, TAction, TResource, ICasbinRules>
|
|
469
734
|
{
|
|
470
735
|
name = 'CasbinAuthorizationEnforcer';
|
|
471
|
-
|
|
472
736
|
private readonly MIN_EXPIRES_IN = 10_000;
|
|
473
|
-
private pool: TNullable<BasePoolHelper<CasbinEnforcerType>>;
|
|
474
|
-
private helper: TNullable<typeof CasbinHelper>;
|
|
737
|
+
private pool: TNullable<BasePoolHelper<CasbinEnforcerType>>;
|
|
738
|
+
private helper: TNullable<typeof CasbinHelper>; // casbin.Helper (loadPolicyLine)
|
|
475
739
|
private readonly pendingLineFetches = new Map<string, Promise<string[]>>(); // single-flight
|
|
476
|
-
private resolvedPayloadFn: TNullable<TNormalizePayloadFn>;
|
|
740
|
+
private resolvedPayloadFn: TNullable<TNormalizePayloadFn>; // memoized in configure()
|
|
477
741
|
|
|
478
|
-
constructor(
|
|
479
|
-
@inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') })
|
|
480
|
-
private options: ICasbinEnforcerOptions<E, TAction, TResource>,
|
|
481
|
-
);
|
|
742
|
+
constructor(@inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') }) private options: ICasbinEnforcerOptions<E, TAction, TResource>);
|
|
482
743
|
|
|
483
|
-
// Lifecycle
|
|
484
744
|
async configure(): Promise<void>;
|
|
485
745
|
destroy(): void;
|
|
486
746
|
|
|
487
|
-
//
|
|
488
|
-
async buildRules(opts: { user; context }): Promise<ICasbinRules>; // { user, lines }
|
|
747
|
+
async buildRules(opts: { user; context }): Promise<ICasbinRules>; // { user, lines }
|
|
489
748
|
async evaluate(opts: { rules; request; context }): Promise<TAuthorizationDecision>;
|
|
490
749
|
|
|
491
|
-
// Optional cache management (Redis only)
|
|
492
750
|
async invalidateUserCache(opts: { user }): Promise<{ invalidatedKeys: number }>;
|
|
493
751
|
async rebuildUserCache(opts: { user }): Promise<{ cacheKey: string; lineCount: number }>;
|
|
494
752
|
|
|
495
|
-
// Protected internals
|
|
496
753
|
protected async registerMatchers(opts: { enforcer; casbin }): Promise<void>;
|
|
497
754
|
protected assertMatcherCompilesSync(opts: { enforcer }): void;
|
|
498
755
|
protected resolveModel(opts): Model;
|
|
@@ -505,205 +762,113 @@ class CasbinAuthorizationEnforcer<
|
|
|
505
762
|
}
|
|
506
763
|
```
|
|
507
764
|
|
|
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
765
|
### configure()
|
|
517
766
|
|
|
518
|
-
Called once by the registry on first use
|
|
767
|
+
Called once by the registry on first use:
|
|
519
768
|
|
|
520
|
-
1. Dynamically imports `casbin` - throws
|
|
521
|
-
2. Validates `options.model`
|
|
522
|
-
3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`).
|
|
769
|
+
1. Dynamically imports `casbin` - throws if not installed.
|
|
770
|
+
2. Validates `options.model` is present.
|
|
771
|
+
3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`; the latter is `undefined` unless `isScoped`).
|
|
523
772
|
4. If `cached.use`, validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms).
|
|
524
|
-
5. Builds a
|
|
773
|
+
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
774
|
6. `await pool.warmup()` - pre-creates the enforcers.
|
|
526
775
|
|
|
527
|
-
|
|
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.
|
|
776
|
+
**`registerMatchers()`** - when `isScoped`, registers three things:
|
|
530
777
|
|
|
531
|
-
|
|
778
|
+
| Registers | On |
|
|
779
|
+
|---|---|
|
|
780
|
+
| `keyMatch` | Domain matching func on `g` |
|
|
781
|
+
| `objectMatch` | Matcher-expression function via `addFunction` - called directly in the model's matcher string, not as a relation's matching func |
|
|
782
|
+
| `ResourceRoleManager` | Named role manager for `g4` |
|
|
532
783
|
|
|
533
|
-
`
|
|
784
|
+
`g4` skips `addNamedMatchingFunc` on purpose. It sets casbin's `hasPattern`, which disables `DefaultRoleManager`'s fast path on every link check, not just `g4` lookups.
|
|
534
785
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
Returns `ICasbinRules` = `{ user, lines }`. The `lines` are the user's complete Casbin policy lines.
|
|
786
|
+
For every scoped model, three more role managers are wired unconditionally - not behind a separate option:
|
|
538
787
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
```
|
|
788
|
+
| Registers | On | Notes |
|
|
789
|
+
|---|---|---|
|
|
790
|
+
| `MembershipRoleManager` | `g2` | Joining a parent domain membership makes the request domain's ancestors match too |
|
|
791
|
+
| `DomainHierarchyRoleManager` | `g3` | Grant domain nesting, request-domain-first |
|
|
792
|
+
| `DomainHierarchyRoleManager` (`reversed: true`) | `g`, via casbin's own `DefaultRoleManager.addDomainHierarchy()` | Role-assignment domain, stored-domain-first - the opposite argument order from `g3` |
|
|
551
793
|
|
|
552
|
-
|
|
553
|
-
`adapter.loadFilteredPolicy({ principal: { type, id } })`, then `extractLinesFrom()` serializes every
|
|
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.
|
|
794
|
+
The `g3` instance, the reversed `g` instance and `MembershipRoleManager` on `g2` are handed the same overlay `Map<child, Set<parent>>`. Casbin's `buildRoleLinks()` feeds every `g3` policy line to the `g3` instance via `addLink`, which writes into that shared overlay - the reversed `g` instance and `MembershipRoleManager` only ever read it, since casbin never puts the `g`-axis manager in its own `rmMap` and so never calls `addLink` on it directly. See [Domain hierarchy edges (`g3`)](#domain-hierarchy-edges-g3) above for where those `g3` lines come from.
|
|
559
795
|
|
|
560
|
-
|
|
796
|
+
When `domainMatching` is set (flat model), `registerMatchers()` registers the chosen `Util.*Func` on the named role definition instead, and always finishes with `buildRoleLinks()`.
|
|
561
797
|
|
|
562
|
-
|
|
798
|
+
**`assertMatcherCompilesSync()`** is a boot-time smoke test. It forces casbin's lazy matcher compile with one dummy `enforceSync` call (4 args when scoped/`normalizePayloadFn`, else 3). A malformed matcher, an unregistered function, or an arity mismatch fails at warmup, not on the first real request.
|
|
563
799
|
|
|
564
|
-
|
|
565
|
-
flowchart TD
|
|
566
|
-
Start([evaluate]) --> Use["pool.use(enforcer =>"]
|
|
567
|
-
Use --> Load["loadPolicyLinesIntoModel(enforcer, rules.lines)<br/>clearPolicy + loadPolicyLine* + buildRoleLinks"]
|
|
568
|
-
Load --> Norm["normalizePayloadFn(user, action, resource, context)"]
|
|
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
|
-
```
|
|
575
|
-
|
|
576
|
-
- `vals` is `[subject, domain, resource, action]` when a domain is present (scoped), else `[subject, resource, action]`.
|
|
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.
|
|
579
|
-
|
|
580
|
-
### invalidateUserCache() / rebuildUserCache()
|
|
800
|
+
### buildRules()
|
|
581
801
|
|
|
582
|
-
|
|
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.
|
|
802
|
+
Returns `ICasbinRules = { user, lines }` - the user's complete Casbin policy lines.
|
|
585
803
|
|
|
586
|
-
|
|
804
|
+
| Function | Behavior |
|
|
805
|
+
|---|---|
|
|
806
|
+
| `extractUserLines(user)` | Builds a fresh, isolated enforcer *with the adapter* and calls `adapter.loadFilteredPolicy({ principal: { type, id } })` |
|
|
807
|
+
| `extractLinesFrom()` | Serializes every `p*`/`g*` rule the model declares back into lines, not just `p`/`g` - including the scoped model's `g2`-`g5` hierarchies |
|
|
808
|
+
| `fetchLinesWithRedisCache` | Returns cached lines on a hit (Redis owns expiry via `PX`); on a miss, dedups concurrent misses via `pendingLineFetches` (single-flight), extracts once, and writes the lines back to Redis |
|
|
587
809
|
|
|
588
|
-
|
|
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 |
|
|
810
|
+
A corrupt cache entry is logged and discarded, then refetched - never surfaced as a `500`.
|
|
599
811
|
|
|
600
|
-
|
|
812
|
+
### evaluate()
|
|
601
813
|
|
|
602
|
-
|
|
603
|
-
for the scoped model (`g2`…`g5`):
|
|
814
|
+
Borrows an enforcer from the pool and evaluates atomically inside `pool.use`:
|
|
604
815
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
816
|
+
1. `loadPolicyLinesIntoModel(enforcer, rules.lines)` - `clearPolicy()` + `loadPolicyLine()` per line + `buildRoleLinks()`.
|
|
817
|
+
2. `normalizePayloadFn(user, action, resource, context)` normalizes the payload.
|
|
818
|
+
3. `domain = normalized.domain ?? request.domain ?? (isScoped ? SYSTEM_WIDE : undefined)`.
|
|
819
|
+
4. `vals` is `[subject, domain, resource, action]` when a domain is present, else `[subject, resource, action]`.
|
|
820
|
+
5. `enforceWithExplain(vals)` runs `enforceExSync` and logs the deciding policy on a DENY.
|
|
608
821
|
|
|
609
|
-
|
|
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
|
-
```
|
|
822
|
+
On any error inside `pool.use`, the pool **destroys** the borrowed enforcer (fail-closed); a fresh one is created on demand.
|
|
617
823
|
|
|
618
|
-
|
|
824
|
+
### invalidateUserCache() / rebuildUserCache()
|
|
619
825
|
|
|
620
|
-
|
|
826
|
+
Redis-only - both throw if caching is disabled. `invalidateUserCache` deletes the user's shared Redis key; the next request rebuilds lazily. `rebuildUserCache` deletes, then immediately re-extracts (on a throwaway enforcer) and re-caches. The key is shared in Redis, so one call is correct across every instance.
|
|
621
827
|
|
|
622
|
-
|
|
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
|
-
```
|
|
828
|
+
Source -> [`enforcers/casbin.enforcer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/casbin.enforcer.ts)
|
|
630
829
|
|
|
631
830
|
## BaseFilteredAdapter
|
|
632
831
|
|
|
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
|
|
832
|
+
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
833
|
|
|
640
834
|
```typescript
|
|
641
|
-
abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter>
|
|
642
|
-
extends BaseHelper
|
|
643
|
-
implements FilteredAdapter
|
|
644
|
-
{
|
|
835
|
+
abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter> extends BaseHelper implements FilteredAdapter {
|
|
645
836
|
protected readonly dataSource: ICasbinPolicySource;
|
|
646
837
|
protected get connector(): TCasbinPolicyConnector;
|
|
647
|
-
|
|
648
838
|
constructor(opts: { scope: string; dataSource: ICasbinPolicySource });
|
|
649
839
|
|
|
650
|
-
// Subclasses implement ONLY this:
|
|
651
840
|
abstract loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
|
|
652
|
-
|
|
653
841
|
isFiltered(): boolean; // always true
|
|
654
842
|
|
|
655
|
-
//
|
|
843
|
+
// Read-only adapter - no-op write methods
|
|
656
844
|
async loadPolicy(): Promise<void>;
|
|
657
|
-
async savePolicy(): Promise<boolean>;
|
|
845
|
+
async savePolicy(): Promise<boolean>; // returns true
|
|
658
846
|
async addPolicy(): Promise<void>;
|
|
659
847
|
async removePolicy(): Promise<void>;
|
|
660
848
|
async removeFilteredPolicy(): Promise<void>;
|
|
661
849
|
|
|
662
|
-
|
|
850
|
+
protected async query<TRow>(opts: { statement: SQL }): Promise<TRow[]>;
|
|
663
851
|
protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void>;
|
|
664
852
|
}
|
|
665
853
|
```
|
|
666
854
|
|
|
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
855
|
```typescript
|
|
678
|
-
interface ICasbinPolicyFilter {
|
|
679
|
-
principal: { type: string; id: IdType };
|
|
680
|
-
}
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
### ICasbinPolicySource
|
|
856
|
+
interface ICasbinPolicyFilter { principal: { type: string; id: IdType }; }
|
|
684
857
|
|
|
685
|
-
|
|
686
|
-
general `IDataSource` interface. Any drizzle-backed datasource (e.g. `BasePostgresDataSource`) satisfies
|
|
687
|
-
it structurally; the adapter only ever needs the connector to run policy queries.
|
|
688
|
-
|
|
689
|
-
```typescript
|
|
858
|
+
/** Minimal contract - NOT the framework's general IDataSource. Any Drizzle-backed datasource satisfies it. */
|
|
690
859
|
interface ICasbinPolicySource {
|
|
691
|
-
|
|
860
|
+
getConnector?(): TCasbinPolicyConnector; // preferred: lazily wires the driver on first read, survives pool rotation
|
|
861
|
+
connector?: TCasbinPolicyConnector; // back-compat: a pre-wired connector
|
|
692
862
|
}
|
|
693
|
-
|
|
694
|
-
type TCasbinPolicyConnector = ReturnType<
|
|
695
|
-
typeof drizzle<Record<string, AnyType>, NodePgClient>
|
|
696
|
-
>;
|
|
863
|
+
type TCasbinPolicyConnector = PgDatabase<PgQueryResultHKT, Record<string, AnyType>>;
|
|
697
864
|
```
|
|
698
865
|
|
|
699
866
|
> [!NOTE]
|
|
700
|
-
>
|
|
701
|
-
> connector class -- keeps the casbin adapters decoupled from the full datasource surface.
|
|
867
|
+
> `ICasbinPolicySource` is a minimal local contract, not the framework's general `IDataSource` - `src/components/**` never imports `@/connectors/postgres` for this. The `connector` getter resolves `getConnector?.() ?? connector`. When a datasource exposes neither, it throws a clear `[BaseFilteredAdapter]` error - never a bare `TypeError`.
|
|
702
868
|
|
|
703
|
-
|
|
869
|
+
`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. Call `query()` rather than read `.rows` directly.
|
|
704
870
|
|
|
705
|
-
|
|
706
|
-
their casbin lines:
|
|
871
|
+
`loadLines()` is the other orchestration helper. Call it after assembling your own casbin lines:
|
|
707
872
|
|
|
708
873
|
```typescript
|
|
709
874
|
protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void> {
|
|
@@ -714,91 +879,135 @@ protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void
|
|
|
714
879
|
}
|
|
715
880
|
```
|
|
716
881
|
|
|
717
|
-
|
|
718
|
-
own queries and line construction (see `ScopedCasbinAdapter` below for the reference implementation).
|
|
882
|
+
Source -> [`adapters/base-filtered.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/base-filtered.ts), [`adapters/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/types.ts)
|
|
719
883
|
|
|
720
884
|
## ScopedCasbinAdapter
|
|
721
885
|
|
|
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
|
|
886
|
+
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
887
|
|
|
728
888
|
```typescript
|
|
729
889
|
class ScopedCasbinAdapter extends BaseFilteredAdapter<IScopedCasbinPolicyFilter> {
|
|
730
890
|
protected readonly entities: IScopedCasbinEntities;
|
|
731
|
-
|
|
732
|
-
constructor(opts: {
|
|
891
|
+
protected readonly resolveDomainEdges?: TResolveDomainEdgesFn;
|
|
892
|
+
constructor(opts: {
|
|
893
|
+
dataSource: ICasbinPolicySource;
|
|
894
|
+
entities: IScopedCasbinEntities;
|
|
895
|
+
resolveDomainEdges?: TResolveDomainEdgesFn;
|
|
896
|
+
});
|
|
733
897
|
|
|
734
898
|
async loadFilteredPolicy(model: Model, filter: IScopedCasbinPolicyFilter): Promise<void>;
|
|
735
899
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
protected
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
protected
|
|
746
|
-
//
|
|
747
|
-
|
|
900
|
+
protected async queryPrincipalPolicies(opts: {
|
|
901
|
+
principal: { type: string; id: IdType };
|
|
902
|
+
}): Promise<TPrincipalPolicyRow[]>; // one statement, two recursive CTEs (role_closure, domain_closure) - direct edges + reachable role_inherits + role-closure grants + reachable domain_inherits
|
|
903
|
+
protected collectDirectRow(opts: {
|
|
904
|
+
row: TPrincipalPolicyRow;
|
|
905
|
+
principal: { type: string; id: IdType };
|
|
906
|
+
lines: string[];
|
|
907
|
+
directGrants: TGrantRow[];
|
|
908
|
+
}): void; // routes one 'direct' row to its g/g2 line, or into the direct-grant batch
|
|
909
|
+
protected async buildGrantLines(opts: { subjectType: string; rows: TGrantRow[] }): Promise<string[]>; // -> p, shared by direct and role-closure grants
|
|
910
|
+
protected async queryEdgePolicies(): Promise<string[]>; // -> g4 (resource_inherits) + g5 (action_inherits), the two code-fixed structural trees
|
|
911
|
+
}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
```typescript
|
|
915
|
+
/** The `kind` discriminator values in `TPrincipalPolicyRow`, one per UNION ALL branch of `queryPrincipalPolicies`. */
|
|
916
|
+
class PrincipalPolicyEdges {
|
|
917
|
+
static readonly DIRECT = 'direct';
|
|
918
|
+
static readonly ROLE_EDGE = 'roleEdge';
|
|
919
|
+
static readonly ROLE_GRANT = 'roleGrant';
|
|
920
|
+
static readonly DOMAIN_EDGE = 'domainEdge';
|
|
748
921
|
}
|
|
922
|
+
|
|
923
|
+
/** A grant row as fetched, before it becomes casbin lines. Permission columns are null when the join misses. */
|
|
924
|
+
type TGrantRow = {
|
|
925
|
+
subjectId: IdType;
|
|
926
|
+
objectCode: TNullable<string>;
|
|
927
|
+
objectSubject: TNullable<string>;
|
|
928
|
+
objectMethod: TNullable<string>;
|
|
929
|
+
action: TNullable<string>;
|
|
930
|
+
effect: TNullable<string>;
|
|
931
|
+
domain: TNullable<string>;
|
|
932
|
+
metadata?: unknown;
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
/** A row from the single principal-policy statement; `kind` says which branch produced it. */
|
|
936
|
+
type TPrincipalPolicyRow = TGrantRow & {
|
|
937
|
+
kind: TConstValue<typeof PrincipalPolicyEdges>;
|
|
938
|
+
variant: string;
|
|
939
|
+
targetType: TNullable<string>;
|
|
940
|
+
targetId: IdType;
|
|
941
|
+
};
|
|
749
942
|
```
|
|
750
943
|
|
|
751
|
-
|
|
944
|
+
> [!NOTE]
|
|
945
|
+
> **There is no cache in the adapter.** Every `loadFilteredPolicy()` call re-runs both statements. If extraction cost becomes a measured problem, add the indexes below rather than a staleness window. The framework does not create these indexes. Your `PolicyDefinition` schema owns that, but the queries need them:
|
|
946
|
+
>
|
|
947
|
+
> | Index | Serves |
|
|
948
|
+
> |---|---|
|
|
949
|
+
> | `(variant, subject_type, subject_id)` | `queryPrincipalPolicies`' two CTE anchor terms (`role_closure`, `domain_closure`) and its `direct` branch |
|
|
950
|
+
> | `(variant, subject_id)` | `role_closure`'s recursive-term join and the role-grant branch |
|
|
951
|
+
> | `(variant, subject_type, subject_id)` again | `domain_closure`'s recursive term and the `domainEdge` branch. A domain node's identity is `(type, id)`, so both join on the pair, not `subject_id` alone |
|
|
952
|
+
> | `(variant)`, or per-variant partial indexes | `queryEdgePolicies`' two branches (`resource_inherits`, `action_inherits`) - each filters on `variant` alone |
|
|
953
|
+
>
|
|
954
|
+
> Without them, the recursive CTEs' anchor and direct-edge branches fall back to a sequential scan of the whole `PolicyDefinition` table. Measured with `EXPLAIN (ANALYZE, BUFFERS)` against a real Postgres database.
|
|
752
955
|
|
|
753
956
|
```typescript
|
|
754
957
|
interface IScopedCasbinTable { tableName: string; schemaName?: string; }
|
|
755
958
|
|
|
756
959
|
interface IScopedCasbinEntities {
|
|
757
|
-
policyDefinition: IScopedCasbinTable;
|
|
960
|
+
policyDefinition: IScopedCasbinTable & { metadata?: { columnName: string } }; // metadata.columnName opts into subset grants
|
|
758
961
|
permission: IScopedCasbinTable; // permission catalog (id, code, ...)
|
|
759
962
|
principals: { user: string; role: string }; // casbin name prefixes
|
|
760
963
|
domainTypes: string[]; // e.g. ['Merchant', 'Organizer']
|
|
761
964
|
softDelete?: { use: false } | { use: true; columnName: string };
|
|
762
965
|
}
|
|
966
|
+
|
|
967
|
+
interface IScopedCasbinPolicyFilter { principal: { type: string; id: IdType }; }
|
|
763
968
|
```
|
|
764
969
|
|
|
765
|
-
|
|
970
|
+
**`loadFilteredPolicy()` runs one wave of two independent statements:**
|
|
766
971
|
|
|
767
|
-
|
|
768
|
-
interface IScopedCasbinPolicyFilter {
|
|
769
|
-
principal: { type: string; id: IdType };
|
|
770
|
-
}
|
|
771
|
-
```
|
|
972
|
+
1. `Promise.all` of two statements, neither waiting on the other:
|
|
772
973
|
|
|
773
|
-
|
|
974
|
+
**`queryPrincipalPolicies`** covers everything scoped to the principal, tagged by `kind`:
|
|
774
975
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
976
|
+
| `kind` | Rows |
|
|
977
|
+
|---|---|
|
|
978
|
+
| `DIRECT` | The principal's own `assign_role` / `join_domain` / `grant` rows |
|
|
979
|
+
| `ROLE_EDGE` | `role_inherits` edges reachable from its roles |
|
|
980
|
+
| `ROLE_GRANT` | Grants of that role closure |
|
|
981
|
+
| `DOMAIN_EDGE` | `domain_inherits` edges reachable from its domains |
|
|
982
|
+
|
|
983
|
+
It resolves two `WITH RECURSIVE` CTEs in SQL. `role_closure` seeds from `assign_role` rows and walks `role_inherits`. `domain_closure` seeds from `join_domain` rows and walks `domain_inherits`. Each recursive term uses `UNION`, not `UNION ALL` - the de-duplication is what terminates a cyclic graph.
|
|
984
|
+
|
|
985
|
+
**`queryEdgePolicies`** covers the two code-fixed structural trees, `resource_inherits` (`g4`) and `action_inherits` (`g5`), merged into one statement with two `UNION ALL` branches. `domain_inherits` (`g3`) is not loaded here - see [why `g3` is scoped differently](#why-g3-is-scoped-and-g4-g5-are-not).
|
|
986
|
+
|
|
987
|
+
2. **Row routing:**
|
|
783
988
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
`
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
of parent roles.
|
|
791
|
-
4. All lines are loaded via `loadLines`.
|
|
989
|
+
| `kind` | Routed to |
|
|
990
|
+
|---|---|
|
|
991
|
+
| `direct` | `collectDirectRow` - `g` for `assign_role`, `g2` for `join_domain`, or the direct-grant batch for `grant` |
|
|
992
|
+
| `roleEdge` | `g` lines, inline |
|
|
993
|
+
| `domainEdge` | `g3` lines, inline |
|
|
994
|
+
| `roleGrant` | Batched separately |
|
|
792
995
|
|
|
793
|
-
|
|
996
|
+
Both grant batches expand through the shared `buildGrantLines`.
|
|
794
997
|
|
|
795
|
-
All
|
|
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).
|
|
998
|
+
3. All lines load via `loadLines`.
|
|
800
999
|
|
|
801
|
-
|
|
1000
|
+
Only reachable edges are emitted: `role_inherits` edges from the principal's roles, and `domain_inherits` edges from its domains - never the whole role/domain graph. An edge outside either closure could never be traversed by the matcher anyway. This is behavior-preserving, and it shrinks every user's payload.
|
|
1001
|
+
|
|
1002
|
+
### Why `g3` is scoped and `g4`/`g5` are not
|
|
1003
|
+
|
|
1004
|
+
`g4` (resource) and `g5` (action) are fixed by the codebase - a few hundred rows, constant regardless of tenant count. `queryEdgePolicies` loads them whole for every principal.
|
|
1005
|
+
|
|
1006
|
+
`g3` (domain) grows with the domain count - many merchants under few organizers. It is scoped to the principal's domain closure inside `queryPrincipalPolicies` instead.
|
|
1007
|
+
|
|
1008
|
+
**The permission join is a `LEFT JOIN`, not `INNER JOIN`.** A grant whose target does not resolve (missing or soft-deleted `Permission` row) is logged and skipped by `buildGrantLines`, not silently dropped from the result set.
|
|
1009
|
+
|
|
1010
|
+
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.
|
|
802
1011
|
|
|
803
1012
|
```typescript
|
|
804
1013
|
import { ScopedCasbinAdapter } from '@venizia/ignis';
|
|
@@ -815,294 +1024,454 @@ const adapter = new ScopedCasbinAdapter({
|
|
|
815
1024
|
});
|
|
816
1025
|
```
|
|
817
1026
|
|
|
818
|
-
|
|
1027
|
+
Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
|
|
819
1028
|
|
|
820
|
-
`
|
|
1029
|
+
### `resolveDomainEdges` - `g3` edges from business data
|
|
821
1030
|
|
|
822
|
-
|
|
1031
|
+
A second, opt-in source of `g3` edges, alongside the `DOMAIN_EDGE` branch above. Configured on the constructor, for a tenant hierarchy the app already owns as a plain foreign key on a business table rather than `domain_inherits` rows:
|
|
823
1032
|
|
|
824
1033
|
```typescript
|
|
825
|
-
|
|
826
|
-
|
|
1034
|
+
type TResolveDomainEdgesFn = (opts: {
|
|
1035
|
+
principal: { type: string; id: IdType };
|
|
1036
|
+
domains: string[];
|
|
1037
|
+
}) => Promise<Array<{ child: string; parent: string }>>;
|
|
827
1038
|
|
|
828
|
-
|
|
1039
|
+
new ScopedCasbinAdapter({
|
|
1040
|
+
dataSource,
|
|
1041
|
+
entities,
|
|
1042
|
+
resolveDomainEdges: async ({ principal, domains }) => [{ child, parent }, ...],
|
|
1043
|
+
});
|
|
1044
|
+
```
|
|
829
1045
|
|
|
830
|
-
|
|
831
|
-
spec: IAuthorizationSpec;
|
|
832
|
-
enforcerName?: string;
|
|
833
|
-
}): MiddlewareHandler;
|
|
1046
|
+
`domains` is the principal's own domain closure, reconstructed from rows `queryPrincipalPolicies` already fetched (the `join_domain` seed plus both ends of every `domainEdge` row) rather than a third query. The hook returns `{ child, parent }` pairs as already-formed `<Type>_<id>` tokens; `loadFilteredPolicy` turns each into a `g3, <child>, <parent>` line, the exact shape a real `domain_inherits` row produces - nothing downstream can tell which source produced a given edge. A hook edge duplicating a real `domain_inherits` row is harmless: `DomainHierarchyRoleManager.addLink` stores parents in a `Set`, so the duplicate `addLink` call is a no-op.
|
|
834
1047
|
|
|
835
|
-
|
|
1048
|
+
A throwing hook is caught, logged, and treated as no edges for that one load - the rows already gathered (direct grants, role assignments, table-sourced `g3` rows) still load normally. This is the fail-secure direction: a missing `g3` edge only narrows what `g`/`g2`/`g3` reach, it can never widen it. The hook cannot join `queryPrincipalPolicies`'s wave (it needs that query's rows to compute `domains`) but does not wait on the independent `queryEdgePolicies` either - both resolve concurrently once the closure is known.
|
|
1049
|
+
|
|
1050
|
+
### Subset grants (custom rows)
|
|
1051
|
+
|
|
1052
|
+
A grant row can express an arbitrary subset of a subject's operations instead of a full tier:
|
|
1053
|
+
|
|
1054
|
+
| Field | Value |
|
|
1055
|
+
|---|---|
|
|
1056
|
+
| `action` | `'custom'` |
|
|
1057
|
+
| Target | A subject-level resource node - `Permission.method` is `AuthorizationPermissionBuilder.RESOURCE_NODE_METHOD`, the `*` sentinel |
|
|
1058
|
+
| `metadata` | `{ ops: [...] }` |
|
|
1059
|
+
|
|
1060
|
+
`ops` holds **method names**, not full permission codes. The subject comes from the target node, so `ops: ['find']` against node `Order` resolves to `Order.find`.
|
|
1061
|
+
|
|
1062
|
+
`buildGrantLines` expands each custom row into one `p` line per operation, using that operation's catalogued action (never the `custom` sentinel). The emitted lines are byte-identical to what equivalent per-operation grant rows produce. Expansion runs one extra batched query (`queryOperationCatalog`) per extraction, and none when no custom rows are present.
|
|
1063
|
+
|
|
1064
|
+
Reading is **opt-in**: without `entities.policyDefinition.metadata.columnName` mapped, the adapter never selects the `metadata` column, and a custom row is logged and skipped.
|
|
1065
|
+
|
|
1066
|
+
**Rejection rules** (`rejectCustomRow`, checked in this order). Each produces one `error`-level log line naming the subject id and object code, so a skipped grant can be diagnosed from the log alone:
|
|
1067
|
+
|
|
1068
|
+
| Condition | Logged reason |
|
|
1069
|
+
|---|---|
|
|
1070
|
+
| `action = 'custom'` but `metadata.columnName` is not mapped | `metadata.columnName is not mapped, so metadata.ops cannot be read` |
|
|
1071
|
+
| `action = 'custom'` but `metadata.ops` is missing, empty, or not an array of non-empty strings | `metadata.ops is missing, empty, or not an array of non-empty strings` |
|
|
1072
|
+
| `metadata.ops` is present but `action` is not `'custom'` | `metadata.ops is present but action is not "custom", so the intent is ambiguous` |
|
|
1073
|
+
| The target's `Permission.method` is not the `*` resource-node sentinel | `the target must be a subject-level resource node` |
|
|
1074
|
+
|
|
1075
|
+
A row that passes all four checks can still drop an individual **unresolvable operation name** during expansion. `expandCustomGrants` logs it separately, naming the unknown operations. The row's other valid operations still expand and emit lines.
|
|
1076
|
+
|
|
1077
|
+
**Composing a grant:** use `planGrant` (below) rather than hand-building a custom row. It collapses an operation selection into tier grants wherever possible. What does not collapse falls back to a custom row, or a single per-operation row.
|
|
1078
|
+
|
|
1079
|
+
Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
|
|
1080
|
+
|
|
1081
|
+
## AuthorizationPermissionBuilder.objectMatch
|
|
1082
|
+
|
|
1083
|
+
Resource-hierarchy matcher registered by the scoped model **only** as a function - `objectMatch(r.obj, p.obj)`, called directly in the matcher expression via `addFunction`. It 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.
|
|
1084
|
+
|
|
1085
|
+
`objectMatch` is **not** registered as the `g4` matching func. `g4` (`resource_inherits`) is served by a dedicated `ResourceRoleManager` instead - see [ScopedCasbinAdapter](#scopedcasbinadapter). Registering it via `addNamedMatchingFunc` would set casbin's `hasPattern`. That disables `DefaultRoleManager`'s O(1) fast path on every link check, not only `g4` lookups.
|
|
1086
|
+
|
|
1087
|
+
It lives as a `static` method on `AuthorizationPermissionBuilder`, the class that owns the `code = <subject>.<method>` format it matches against. It must stay `static` with no `this` reference, since Casbin calls it by reference:
|
|
1088
|
+
|
|
1089
|
+
```typescript
|
|
1090
|
+
class AuthorizationPermissionBuilder {
|
|
1091
|
+
static objectMatch(requested: string, granted: string): boolean {
|
|
1092
|
+
if (granted === '*') return true;
|
|
1093
|
+
if (requested === granted) return true;
|
|
1094
|
+
return requested.startsWith(`${granted}.`);
|
|
1095
|
+
}
|
|
836
1096
|
}
|
|
1097
|
+
|
|
1098
|
+
enforcer.addFunction('objectMatch', AuthorizationPermissionBuilder.objectMatch);
|
|
837
1099
|
```
|
|
838
1100
|
|
|
839
|
-
|
|
1101
|
+
| Call | Result | Why |
|
|
1102
|
+
|---|---|---|
|
|
1103
|
+
| `objectMatch('Activation.findById', 'Activation')` | `true` | Dotted nesting - endpoint under subject |
|
|
1104
|
+
| `objectMatch('OrderItem', 'Order')` | `false`, unless a `resource_inherits` (`g4`) edge links them | Non-standard nesting always needs an explicit edge |
|
|
1105
|
+
|
|
1106
|
+
Source -> [`builders/permission.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/permission.builder.ts)
|
|
840
1107
|
|
|
841
|
-
|
|
1108
|
+
## AuthorizationProvider
|
|
1109
|
+
|
|
1110
|
+
Implements `IProvider<TAuthorizeFn>` and produces the middleware factory.
|
|
842
1111
|
|
|
843
1112
|
```typescript
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1113
|
+
class AuthorizationProvider extends BaseHelper implements IProvider<TAuthorizeFn> {
|
|
1114
|
+
constructor();
|
|
1115
|
+
value(): TAuthorizeFn;
|
|
1116
|
+
private createAuthorizeMiddleware(opts: { spec: IAuthorizationSpec; enforcerName?: string }): MiddlewareHandler;
|
|
1117
|
+
private extractUserRoles(opts: { user: IAuthUser }): string[];
|
|
1118
|
+
}
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
**The 7-step pipeline** (`createAuthorizeMiddleware`):
|
|
847
1122
|
|
|
848
|
-
|
|
1123
|
+
```typescript
|
|
1124
|
+
// 1. Skip check
|
|
1125
|
+
if (context.get(Authorization.SKIP_AUTHORIZATION)) return next();
|
|
1126
|
+
|
|
1127
|
+
// 2. User check
|
|
849
1128
|
const user = context.get(Authentication.CURRENT_USER);
|
|
850
|
-
if (!user)
|
|
1129
|
+
if (!user) throw 401 'No authenticated user found';
|
|
851
1130
|
|
|
852
|
-
//
|
|
853
|
-
const needsRoleCheck = options?.alwaysAllowRoles?.length || spec.allowedRoles?.length;
|
|
1131
|
+
// 3. Role shortcuts (alwaysAllowRoles + allowedRoles, merged; userRoles extracted once)
|
|
854
1132
|
if (needsRoleCheck) {
|
|
855
|
-
const userRoles = extractUserRoles({ user });
|
|
856
|
-
if (alwaysAllowRoles match)
|
|
857
|
-
if (allowedRoles match)
|
|
1133
|
+
const userRoles = extractUserRoles({ user });
|
|
1134
|
+
if (alwaysAllowRoles match) return next();
|
|
1135
|
+
if (allowedRoles match) return next();
|
|
858
1136
|
}
|
|
859
1137
|
|
|
860
|
-
//
|
|
861
|
-
for (voter of spec.voters) {
|
|
862
|
-
|
|
863
|
-
if (
|
|
864
|
-
|
|
1138
|
+
// 4. Voters (from spec.voters)
|
|
1139
|
+
for (const voter of spec.voters ?? []) {
|
|
1140
|
+
const decision = await voter({ user, action, resource, context });
|
|
1141
|
+
if (decision === DENY) throw 403 'Authorization denied by voter';
|
|
1142
|
+
if (decision === ALLOW) return next();
|
|
1143
|
+
// ABSTAIN -> next voter
|
|
865
1144
|
}
|
|
866
1145
|
|
|
867
|
-
//
|
|
868
|
-
if (!registry.hasEnforcers())
|
|
869
|
-
|
|
870
|
-
|
|
1146
|
+
// 5. Resolve enforcer (no-enforcer fallback honors defaultDecision - fails closed by default)
|
|
1147
|
+
if (!registry.hasEnforcers()) {
|
|
1148
|
+
if (options?.defaultDecision === 'allow') return next(); // logs a warning
|
|
1149
|
+
throw 403 'no enforcer registered'; // AuthorizationErrors.ENFORCER_NOT_REGISTERED
|
|
1150
|
+
}
|
|
1151
|
+
const enforcer = await registry.resolveEnforcer({ name: enforcerName ?? registry.getDefaultEnforcerName() });
|
|
871
1152
|
|
|
872
|
-
//
|
|
1153
|
+
// 5b. Resolve request domain - only when spec.domain or a global domainResolver is in play
|
|
873
1154
|
if (spec.domain || options?.domainResolver) {
|
|
874
|
-
|
|
875
|
-
context.set(Authorization.DOMAIN, domainScope); // the enforcer reads this for request.domain
|
|
1155
|
+
context.set(Authorization.DOMAIN, await resolveRequestDomain({ spec, context, options }));
|
|
876
1156
|
}
|
|
877
1157
|
|
|
878
|
-
//
|
|
1158
|
+
// 6. Build/cache rules
|
|
879
1159
|
let rules = context.get(Authorization.RULES);
|
|
880
1160
|
if (!rules) {
|
|
881
|
-
if (!user.principalType)
|
|
1161
|
+
if (!user.principalType) throw 400 'user.principalType is required for enforcer-based authorization';
|
|
882
1162
|
rules = await enforcer.buildRules({ user, context });
|
|
883
|
-
context.set(Authorization.RULES, rules);
|
|
1163
|
+
context.set(Authorization.RULES, rules);
|
|
884
1164
|
}
|
|
885
1165
|
|
|
886
|
-
//
|
|
887
|
-
let decision = await enforcer.evaluate({ rules, request, context });
|
|
888
|
-
if (decision === ABSTAIN)
|
|
889
|
-
if (decision !== ALLOW)
|
|
1166
|
+
// 7. Evaluate
|
|
1167
|
+
let decision = await enforcer.evaluate({ rules, request: { action, resource, conditions, domain: context.get(Authorization.DOMAIN) }, context });
|
|
1168
|
+
if (decision === ABSTAIN) decision = options?.defaultDecision ?? DENY;
|
|
1169
|
+
if (decision !== ALLOW) throw 403 'Authorization denied';
|
|
890
1170
|
|
|
891
|
-
// All checks passed
|
|
892
1171
|
await next();
|
|
893
1172
|
```
|
|
894
1173
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
The `extractUserRoles` method handles multiple role formats from the user object:
|
|
1174
|
+
**`extractUserRoles()`** - normalizes `user.roles` to `string[]`, priority `identifier` > `name` > `String(id)`:
|
|
898
1175
|
|
|
899
1176
|
```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
|
-
}
|
|
1177
|
+
roles.map(r => typeof r === 'string' ? r : (r.identifier ?? r.name ?? String(r.id ?? '')));
|
|
912
1178
|
```
|
|
913
1179
|
|
|
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 }]
|
|
1180
|
+
Source -> [`providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/providers/authorization.provider.ts)
|
|
923
1181
|
|
|
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
|
|
1182
|
+
## Standalone authorize() function
|
|
932
1183
|
|
|
933
1184
|
```typescript
|
|
934
|
-
// authorize.middleware.ts
|
|
935
1185
|
const authorizationProvider = new AuthorizationProvider();
|
|
936
1186
|
const authorizeFn = authorizationProvider.value();
|
|
937
1187
|
|
|
938
|
-
export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) =>
|
|
939
|
-
return authorizeFn(opts);
|
|
940
|
-
};
|
|
1188
|
+
export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) => authorizeFn(opts);
|
|
941
1189
|
```
|
|
942
1190
|
|
|
943
|
-
|
|
1191
|
+
A module-level singleton `AuthorizationProvider`; the returned handler is a standard Hono `MiddlewareHandler`.
|
|
944
1192
|
|
|
945
|
-
|
|
1193
|
+
Source -> [`middlewares/authorize.middleware.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/middlewares/authorize.middleware.ts)
|
|
946
1194
|
|
|
947
|
-
|
|
1195
|
+
## AuthorizationRole
|
|
948
1196
|
|
|
949
|
-
|
|
1197
|
+
Value object for priority-based role comparison.
|
|
950
1198
|
|
|
951
1199
|
```typescript
|
|
952
1200
|
class AuthorizationRole implements IAuthorizationRole {
|
|
953
1201
|
readonly name: string;
|
|
954
1202
|
readonly priority: number;
|
|
955
|
-
readonly delimiter: string;
|
|
1203
|
+
readonly delimiter: string; // default '_'
|
|
956
1204
|
|
|
957
1205
|
static build(opts: { name: string; priority: number; delimiter?: string }): AuthorizationRole;
|
|
958
1206
|
constructor(opts: { name: string; priority: number; delimiter?: string });
|
|
959
1207
|
|
|
960
|
-
get identifier(): string;
|
|
1208
|
+
get identifier(): string; // `${String(priority).padStart(3, '0')}${delimiter}${name}`
|
|
961
1209
|
|
|
962
|
-
compare(opts: { target: IAuthorizationRole }): number;
|
|
1210
|
+
compare(opts: { target: IAuthorizationRole }): number; // this.priority - target.priority
|
|
963
1211
|
isHigherThan(opts: { target: IAuthorizationRole }): boolean;
|
|
964
1212
|
isLowerThan(opts: { target: IAuthorizationRole }): boolean;
|
|
965
1213
|
isEqualTo(opts: { target: IAuthorizationRole }): boolean;
|
|
966
1214
|
}
|
|
1215
|
+
|
|
1216
|
+
interface IAuthorizationRole { readonly name: string; readonly priority: number; readonly identifier: string; }
|
|
967
1217
|
```
|
|
968
1218
|
|
|
969
|
-
|
|
1219
|
+
Source -> [`models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/models/authorization-role.model.ts)
|
|
1220
|
+
|
|
1221
|
+
## Policy and permission builders
|
|
1222
|
+
|
|
1223
|
+
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.
|
|
1224
|
+
|
|
1225
|
+
### AuthorizationPolicyBuilder
|
|
1226
|
+
|
|
1227
|
+
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
1228
|
|
|
971
1229
|
```typescript
|
|
972
|
-
|
|
973
|
-
readonly
|
|
974
|
-
|
|
975
|
-
|
|
1230
|
+
class AuthorizationPolicyBuilder {
|
|
1231
|
+
static readonly ACTION_PRINCIPAL = 'Action';
|
|
1232
|
+
|
|
1233
|
+
static grant(opts: { subject: { type; id }; permission: { type; id }; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
|
|
1234
|
+
/** A subset grant: subject -> resource node, granting only `ops` (method names). `action` is fixed to `AuthorizationActions.CUSTOM`; the adapter expands `ops` at read time (see Subset grants). */
|
|
1235
|
+
static customGrant(opts: { subject: { type; id }; permission: { type; id }; ops: string[]; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
|
|
1236
|
+
static assignRole(opts: { user: { type; id }; role: { type; id }; domain?: TNullable<TPolicyDomainInput> }): PolicyDefinitionRow;
|
|
1237
|
+
static joinDomain(opts: { user: { type; id }; domain: { type; id } }): PolicyDefinitionRow;
|
|
1238
|
+
static roleInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
|
|
1239
|
+
static resourceInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow; // Permission ids
|
|
1240
|
+
static actionInherits(opts: { child: TAuthorizationAction; parent: TAuthorizationAction }): PolicyDefinitionRow;
|
|
1241
|
+
static domainInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
|
|
1242
|
+
|
|
1243
|
+
/** All action_inherits rows for AuthorizationActions.LATTICE. Seed once, idempotently. */
|
|
1244
|
+
static actionLattice(): PolicyDefinitionRow[];
|
|
1245
|
+
|
|
1246
|
+
/** A role's coarse grant rows from resolved permission codes -> ids. */
|
|
1247
|
+
static roleGrants(opts: {
|
|
1248
|
+
role: { type; id };
|
|
1249
|
+
permission: { type: string; idByCode: ReadonlyMap<string, string> };
|
|
1250
|
+
grants: ReadonlyArray<{ resourceCode: string; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }>;
|
|
1251
|
+
}): PolicyDefinitionRow[]; // unresolved resourceCodes are skipped
|
|
976
1252
|
}
|
|
977
1253
|
```
|
|
978
1254
|
|
|
979
|
-
|
|
1255
|
+
`domain` defaults: `grant` -> `null` maps to `ANY_MEMBER` (the adapter's default); `assignRole` -> `null` maps to `*` (every domain).
|
|
1256
|
+
|
|
1257
|
+
### AuthorizationPermissionBuilder
|
|
980
1258
|
|
|
981
|
-
|
|
1259
|
+
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
1260
|
|
|
983
1261
|
```typescript
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1262
|
+
class AuthorizationPermissionBuilder {
|
|
1263
|
+
static readonly RESOURCE_NODE_METHOD = '*'; // sentinel method for a coarse resource node
|
|
1264
|
+
|
|
1265
|
+
/** Standard repository method -> base action. Unlisted methods resolve to `execute`. */
|
|
1266
|
+
static readonly METHOD_ACTIONS: Record<string, TAuthorizationAction>; // find/findById/findOne/count -> read, create -> create, updateById/updateBy -> update, deleteById/deleteBy -> delete
|
|
1267
|
+
static readonly DEFAULT_CRUD_METHODS: string[]; // the methods `crud()` generates by default
|
|
1268
|
+
|
|
1269
|
+
static actionForMethod(method: string): TAuthorizationAction;
|
|
1270
|
+
|
|
1271
|
+
/** One operation-level permission, code = `<subject>.<method>`. */
|
|
1272
|
+
static operation<TName>(opts: { subject: string; method: string; scope: string; name: TName; description?: TNullable<TName>; action?: TAuthorizationAction; parentId?: TNullable<IdType> }): PermissionRow;
|
|
1273
|
+
|
|
1274
|
+
/** A coarse resource node (module or subject) used as a grant target, e.g. `Sale`. code has no dotted method; action defaults to `manage`. */
|
|
1275
|
+
static resourceNode<TName>(opts: { code: string; subject?: string; scope: string; name: TName; description?: TNullable<TName>; action?: TAuthorizationAction; parentId?: TNullable<IdType> }): PermissionRow;
|
|
1276
|
+
|
|
1277
|
+
/** The CRUD permission set for a subject (find/findById/findOne/count/create/updateById/updateBy/deleteById/deleteBy by default). */
|
|
1278
|
+
static crud<TName>(opts: {
|
|
1279
|
+
subject: string; scope: string;
|
|
1280
|
+
name: (ctx: { subject: string; method: string; action: TAuthorizationAction }) => TName;
|
|
1281
|
+
description?: (ctx) => TNullable<TName>;
|
|
1282
|
+
methods?: ReadonlyArray<string>;
|
|
1283
|
+
}): PermissionRow[];
|
|
1284
|
+
}
|
|
988
1285
|
```
|
|
989
1286
|
|
|
990
|
-
|
|
1287
|
+
Source -> [`builders/policy.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/policy.builder.ts), [`builders/permission.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/permission.builder.ts)
|
|
991
1288
|
|
|
992
|
-
###
|
|
1289
|
+
### GrantBuilder.planGrant
|
|
993
1290
|
|
|
994
|
-
|
|
1291
|
+
The supported way to compose an operations-subset grant. Pure - resolves against a caller-supplied catalog and returns rows for the caller to persist. It never touches the database. Call it on the singleton (`GrantBuilder.getInstance().planGrant(...)`).
|
|
995
1292
|
|
|
996
1293
|
```typescript
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1294
|
+
type TGrantIntent = { tier: TAuthorizationAction } | { ops: string[] };
|
|
1295
|
+
|
|
1296
|
+
class GrantBuilder {
|
|
1297
|
+
planGrant(opts: {
|
|
1298
|
+
subject: { type: string; id: IdType };
|
|
1299
|
+
resource: { type: string; id: IdType; subject: string };
|
|
1300
|
+
intent: TGrantIntent;
|
|
1301
|
+
catalog: Array<{ subject: string; method: string; code: string; action: string }>;
|
|
1302
|
+
domain?: TNullable<TPolicyDomainInput>;
|
|
1303
|
+
effect?: TAuthorizationDecision; // default AuthorizationDecisions.ALLOW
|
|
1304
|
+
supportsCustomMetadata?: boolean; // default true - false forces per-operation rows instead of a custom row
|
|
1305
|
+
exact?: boolean; // default false - true skips collapsing into tier grants
|
|
1306
|
+
}): TPlannedGrantRow[];
|
|
1307
|
+
}
|
|
1000
1308
|
```
|
|
1001
1309
|
|
|
1002
|
-
|
|
1310
|
+
| `intent` | Behavior |
|
|
1311
|
+
|---|---|
|
|
1312
|
+
| `{ tier }` | One tier-grant row. Throws on a tier outside `read`/`write`/`execute`/`manage`. |
|
|
1313
|
+
| `{ ops }` collapsing into `manage` | Only when the subject has an operation in **each** of `read`, `write`, and `execute` - otherwise `manage` would silently pre-authorize a future operation in an empty tier. |
|
|
1314
|
+
| `{ ops }` collapsing into a narrow tier | Whenever the selection **completely covers** a `read`/`write`/`execute` tier, however few operations it holds. `ops: ['find']` collapses into `read` when `find` is the subject's only `read` operation. |
|
|
1315
|
+
| `{ ops }` with `exact: true` | Skips collapsing entirely - use for a selection that must never widen as new operations join a tier. |
|
|
1316
|
+
| Leftover after collapsing | One custom row (`AuthorizationPolicyBuilder.customGrant`) - or, when the leftover is a single operation or `supportsCustomMetadata: false`, one per-operation row per leftover operation. |
|
|
1317
|
+
|
|
1318
|
+
A per-operation row's `permission.id` is the operation's **code** (e.g. `Order.find`), not a database id. The planner is pure and has no DB access, so the caller resolves codes to ids when persisting.
|
|
1003
1319
|
|
|
1004
|
-
|
|
1320
|
+
Throws (`getError`) on an invalid tier, an empty `ops`, or an `ops` entry absent from the resource's catalog slice.
|
|
1005
1321
|
|
|
1006
|
-
|
|
1322
|
+
Mirrors `ScopedCasbinAdapter.buildGrantLines`'s expansion: a planned custom row and the equivalent per-operation rows expand to identical casbin lines - see [Subset grants](#subset-grants-custom-rows).
|
|
1007
1323
|
|
|
1008
|
-
|
|
1324
|
+
Source -> [`builders/grant.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/grant.builder.ts)
|
|
1009
1325
|
|
|
1010
|
-
|
|
1326
|
+
## Model-based authorization metadata
|
|
1327
|
+
|
|
1328
|
+
`@model({ settings: { authorize: { principal } } })` (see [Persistent Models](/guides/core-concepts/persistent/models#authorization-settings)) drives two things:
|
|
1329
|
+
|
|
1330
|
+
**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:
|
|
1011
1331
|
|
|
1012
1332
|
```typescript
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1333
|
+
const principal = metadata.settings?.authorize?.principal;
|
|
1334
|
+
if (principal && !Object.hasOwn(target, 'AUTHORIZATION_SUBJECT')) {
|
|
1335
|
+
target.AUTHORIZATION_SUBJECT = principal;
|
|
1336
|
+
}
|
|
1337
|
+
```
|
|
1016
1338
|
|
|
1017
|
-
|
|
1018
|
-
if (strategies.length > 0) {
|
|
1019
|
-
mws.push(authenticateFn({ strategies, mode }));
|
|
1020
|
-
}
|
|
1339
|
+
Declared on both entity bases: `BasePostgresEntity.AUTHORIZATION_SUBJECT?: string` and `BaseSearchEntity.AUTHORIZATION_SUBJECT?: string`.
|
|
1021
1340
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1341
|
+
**2. `IModelAuthorizeSettings`.**
|
|
1342
|
+
|
|
1343
|
+
```typescript
|
|
1344
|
+
interface IModelAuthorizeSettings {
|
|
1345
|
+
principal: string;
|
|
1346
|
+
[extra: string | symbol]: any; // extensible - consumers can add extra authorization metadata
|
|
1347
|
+
}
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
**3. `MetadataRegistry` queries** (mixed in by `ModelMetadataMixin`) - retrieve every model's authorization principal at runtime, e.g. to seed Casbin `Permission` rows:
|
|
1351
|
+
|
|
1352
|
+
```typescript
|
|
1353
|
+
getModelAuthorizeSettings(opts: { name: string }): IModelAuthorizeSettings | undefined;
|
|
1354
|
+
|
|
1355
|
+
getAuthorizeModelPrincipals(opts: { format: 'array' }): string[];
|
|
1356
|
+
getAuthorizeModelPrincipals(opts: { format: 'record' }): Record<string, string>; // modelName -> principal
|
|
1357
|
+
|
|
1358
|
+
getAuthorizeModelSettings(opts: { format: 'array' }): Array<{ name: string; authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
|
|
1359
|
+
getAuthorizeModelSettings(opts: { format: 'record' }): Record<string, { authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
Source -> [`base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/metadata/persistents.ts), [`helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/helpers/inversion/mixins/model.mixin.ts)
|
|
1029
1363
|
|
|
1030
|
-
|
|
1031
|
-
if (restConfig.middleware) { ... }
|
|
1364
|
+
## Controller integration
|
|
1032
1365
|
|
|
1366
|
+
Authorization is supported in both **REST** and **gRPC** controllers, injected right after authentication.
|
|
1367
|
+
|
|
1368
|
+
### REST controllers
|
|
1369
|
+
|
|
1370
|
+
`AbstractRestController.buildRouteMiddlewares()` builds the middleware array; `getRouteConfigs()` calls it internally.
|
|
1371
|
+
|
|
1372
|
+
```typescript
|
|
1373
|
+
buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
|
|
1374
|
+
const { authenticate = {}, authorize, ...restConfig } = opts.configs;
|
|
1375
|
+
const mws = [];
|
|
1376
|
+
if (strategies.length > 0) mws.push(authenticateFn({ strategies, mode })); // 1. authenticate
|
|
1377
|
+
if (authorize) { // 2. authorize (single or array)
|
|
1378
|
+
for (const spec of Array.isArray(authorize) ? authorize : [authorize]) mws.push(authorizeFn({ spec }));
|
|
1379
|
+
}
|
|
1380
|
+
if (restConfig.middleware) { /* 3. custom middleware, last */ }
|
|
1033
1381
|
return { restConfig, security, mws };
|
|
1034
1382
|
}
|
|
1035
1383
|
```
|
|
1036
1384
|
|
|
1037
|
-
|
|
1385
|
+
```typescript
|
|
1386
|
+
interface IAuthRouteConfig extends HonoRouteConfig {
|
|
1387
|
+
authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
|
|
1388
|
+
authorize?: IAuthorizationSpec | IAuthorizationSpec[];
|
|
1389
|
+
}
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
An array `authorize` creates one middleware per spec - all must pass.
|
|
1393
|
+
|
|
1394
|
+
### gRPC controllers
|
|
1038
1395
|
|
|
1039
|
-
|
|
1396
|
+
`AbstractGrpcController.buildRpcMiddlewares()` injects middleware in the same order, reading `authorize` from `IRpcMetadata`:
|
|
1040
1397
|
|
|
1041
1398
|
```typescript
|
|
1042
1399
|
buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
|
|
1043
|
-
const { configs } = opts;
|
|
1044
1400
|
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) {
|
|
1401
|
+
if (configs.authenticate) { /* 1. authenticate */ }
|
|
1402
|
+
if (configs.authorize) { // 2. authorize
|
|
1403
|
+
for (const spec of Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize]) {
|
|
1053
1404
|
const authzMw = authorizeFn({ spec });
|
|
1054
1405
|
mws.push((context, next) => authzMw(context, next));
|
|
1055
1406
|
}
|
|
1056
1407
|
}
|
|
1057
|
-
|
|
1058
1408
|
return mws;
|
|
1059
1409
|
}
|
|
1060
1410
|
```
|
|
1061
1411
|
|
|
1062
|
-
### IAuthRouteConfig
|
|
1063
|
-
|
|
1064
|
-
Extended route config that supports both authentication and authorization (REST):
|
|
1065
|
-
|
|
1066
1412
|
```typescript
|
|
1067
|
-
interface
|
|
1413
|
+
interface IRpcMetadata {
|
|
1414
|
+
name: string; // proto method name
|
|
1415
|
+
method: TGrpcMethod;
|
|
1068
1416
|
authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
|
|
1069
1417
|
authorize?: IAuthorizationSpec | IAuthorizationSpec[];
|
|
1070
1418
|
}
|
|
1071
1419
|
```
|
|
1072
1420
|
|
|
1073
|
-
|
|
1421
|
+
### CRUD factory authorization
|
|
1074
1422
|
|
|
1075
|
-
|
|
1423
|
+
`defineControllerRouteConfigs` (`base/controllers/factory/definition.ts`) resolves each generated route's `authorize` via `resolveRouteAuthorize(routeKey)`:
|
|
1424
|
+
|
|
1425
|
+
1. Endpoint `authenticate: { skip: true }` -> `undefined` (skips both authentication and authorization).
|
|
1426
|
+
2. Endpoint `authorize: { skip: true }` -> `undefined` (authorization only; authentication still runs).
|
|
1427
|
+
3. Endpoint `authorize` (single spec or array) -> used as-is.
|
|
1428
|
+
4. No endpoint override -> falls back to the controller-level `authorize`.
|
|
1076
1429
|
|
|
1077
1430
|
```typescript
|
|
1078
|
-
/** Per-route authorization config: { skip: true }, single spec, or array of specs. */
|
|
1079
1431
|
type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
|
|
1432
|
+
type TRouteAuthConfig = { authenticate?: TRouteAuthenticateConfig; authorize?: TRouteAuthorizeConfig };
|
|
1433
|
+
```
|
|
1080
1434
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1435
|
+
Applied identically to `count`, `find`, `findById`, `findOne`, `create`, `updateById`, `updateBy`, `deleteById`, `deleteBy`.
|
|
1436
|
+
|
|
1437
|
+
Source -> [`base/controllers/rest/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/rest/abstract.ts), [`base/controllers/grpc/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/grpc/abstract.ts), [`base/controllers/factory/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/factory/definition.ts)
|
|
1438
|
+
|
|
1439
|
+
## Context variables
|
|
1440
|
+
|
|
1441
|
+
The auth module augments Hono's `ContextVariableMap` (`auth/context-variables.ts`), covering both authentication and authorization:
|
|
1442
|
+
|
|
1443
|
+
```typescript
|
|
1444
|
+
declare module 'hono' {
|
|
1445
|
+
interface ContextVariableMap {
|
|
1446
|
+
[Authentication.CURRENT_USER]: IAuthUser;
|
|
1447
|
+
[Authentication.AUDIT_USER_ID]: IdType;
|
|
1448
|
+
[Authentication.SKIP_AUTHENTICATION]: boolean;
|
|
1449
|
+
|
|
1450
|
+
[Authorization.RULES]: Map<string, unknown>;
|
|
1451
|
+
[Authorization.SKIP_AUTHORIZATION]: boolean;
|
|
1452
|
+
[Authorization.DOMAIN]: string;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1086
1455
|
```
|
|
1087
1456
|
|
|
1088
|
-
|
|
1457
|
+
| Key | Constant | Type | Description |
|
|
1458
|
+
|-----|----------|------|-------------|
|
|
1459
|
+
| `'authorization.rules'` | `Authorization.RULES` | `unknown` | Cached rules built by the enforcer - shape depends on the enforcer |
|
|
1460
|
+
| `'authorization.skip'` | `Authorization.SKIP_AUTHORIZATION` | `boolean` | Set `true` to dynamically skip authorization for this request |
|
|
1461
|
+
| `'authorization.domain'` | `Authorization.DOMAIN` | `string` | Resolved request domain scope (`"<Type>_<id>"` or `SYSTEM_WIDE`); read by the enforcer at step 7 |
|
|
1462
|
+
| `'authentication.currentUser'` | `Authentication.CURRENT_USER` | `IAuthUser` | Read at step 2 to get the authenticated user |
|
|
1463
|
+
| `'authentication.auditUserId'` | `Authentication.AUDIT_USER_ID` | `IdType` | Available for audit logging |
|
|
1089
1464
|
|
|
1090
|
-
|
|
1465
|
+
## IAuthUser / IJWTTokenPayload
|
|
1091
1466
|
|
|
1092
1467
|
```typescript
|
|
1093
1468
|
interface IAuthUser {
|
|
1094
|
-
userId: IdType;
|
|
1469
|
+
userId: IdType; // number | string | bigint
|
|
1095
1470
|
[extra: string | symbol]: any;
|
|
1096
1471
|
}
|
|
1097
1472
|
```
|
|
1098
1473
|
|
|
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`):
|
|
1474
|
+
Accessed by the authorization module via the index signature: `user.roles` (role-based shortcuts), `user.principalType` (required for enforcer-based evaluation).
|
|
1106
1475
|
|
|
1107
1476
|
```typescript
|
|
1108
1477
|
interface IJWTTokenPayload extends JWTPayload, IAuthUser {
|
|
@@ -1116,30 +1485,8 @@ interface IJWTTokenPayload extends JWTPayload, IAuthUser {
|
|
|
1116
1485
|
}
|
|
1117
1486
|
```
|
|
1118
1487
|
|
|
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
|
|
1488
|
+
## See also
|
|
1142
1489
|
|
|
1143
|
-
- [Setup & Configuration](./)
|
|
1144
|
-
- [Usage & Examples](./usage)
|
|
1145
|
-
- [Error Reference](./errors)
|
|
1490
|
+
- [Setup & Configuration](./) - binding keys, options interfaces, and initial setup
|
|
1491
|
+
- [Usage & Examples](./usage) - securing routes, voters, patterns, and CRUD integration
|
|
1492
|
+
- [Error Reference](./errors) - error messages and troubleshooting
|