@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,44 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Authorization Usage
|
|
3
|
+
description: Securing REST and gRPC routes, voters, role shortcuts, CRUD factory integration, custom enforcers, and multi-tenant domain scoping
|
|
4
|
+
difficulty: advanced
|
|
5
|
+
---
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
# Authorization Usage
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
Task-oriented examples for the Authorization component. See the [Overview](./) for initial setup and the [API Reference](./api) for every option and class.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## Find what you need
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
| You want to | Go to |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Add `authorize` to a REST or gRPC route | [Securing routes](#securing-routes) |
|
|
16
|
+
| Call `authorize()` outside a route config | [Using the standalone authorize() function](#using-the-standalone-authorize-function) |
|
|
17
|
+
| Add custom allow/deny logic before the enforcer | [Voters](#voters) |
|
|
18
|
+
| Bypass the enforcer for trusted roles | [Role-based shortcuts](#role-based-shortcuts) |
|
|
19
|
+
| Wire authorization into generated CRUD routes | [CRUD factory integration](#crud-factory-integration) |
|
|
20
|
+
| Skip authorization for one request at runtime | [Dynamic skip authorization](#dynamic-skip-authorization) |
|
|
21
|
+
| Read the resolved user, rules, or domain in a handler | [Accessing context variables](#accessing-context-variables) |
|
|
22
|
+
| Replace Casbin with your own enforcer | [Custom enforcer](#custom-enforcer) |
|
|
23
|
+
| Store policies in a schema `ScopedCasbinAdapter` doesn't fit | [Custom filtered adapter](#custom-filtered-adapter) |
|
|
24
|
+
| Compare roles by priority | [AuthorizationRole comparison](#authorizationrole-comparison) |
|
|
25
|
+
| Reference a model instead of a hardcoded resource string | [Model-Based Resource References](#model-based-resource-references) |
|
|
26
|
+
| Scope grants to a tenant (multi-tenant apps) | [RBAC with domains](#rbac-with-domains-multi-tenant) |
|
|
27
|
+
|
|
28
|
+
## Securing routes
|
|
29
|
+
|
|
30
|
+
**Imperative route.** Add `authorize` to `defineRoute()`'s `configs`, next to `authenticate`.
|
|
10
31
|
|
|
11
32
|
```typescript
|
|
12
|
-
import {
|
|
13
|
-
BaseRestController,
|
|
14
|
-
Authentication,
|
|
15
|
-
AuthorizationActions,
|
|
16
|
-
} from '@venizia/ignis';
|
|
33
|
+
import { BaseRestController, Authentication, AuthorizationActions } from '@venizia/ignis';
|
|
17
34
|
|
|
18
35
|
class ArticleController extends BaseRestController {
|
|
19
36
|
binding() {
|
|
20
|
-
// Read requires 'read' action on 'Article' resource
|
|
21
|
-
this.defineRoute({
|
|
22
|
-
configs: {
|
|
23
|
-
path: '/',
|
|
24
|
-
method: 'get',
|
|
25
|
-
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
26
|
-
authorize: {
|
|
27
|
-
action: AuthorizationActions.READ,
|
|
28
|
-
resource: 'Article',
|
|
29
|
-
},
|
|
30
|
-
responses: jsonResponse({
|
|
31
|
-
description: 'List of articles',
|
|
32
|
-
schema: z.array(ArticleSchema),
|
|
33
|
-
}),
|
|
34
|
-
},
|
|
35
|
-
handler: async (context) => {
|
|
36
|
-
const articles = await this.articleService.findAll();
|
|
37
|
-
return context.json(articles);
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
// Delete requires 'delete' action with conditions
|
|
42
37
|
this.defineRoute({
|
|
43
38
|
configs: {
|
|
44
39
|
path: '/{id}',
|
|
@@ -47,74 +42,26 @@ class ArticleController extends BaseRestController {
|
|
|
47
42
|
authorize: {
|
|
48
43
|
action: AuthorizationActions.DELETE,
|
|
49
44
|
resource: 'Article',
|
|
50
|
-
conditions: { ownerId: 'currentUser' },
|
|
45
|
+
conditions: { ownerId: 'currentUser' }, // available to voters/custom enforcers - the built-in Casbin enforcer ignores it
|
|
51
46
|
},
|
|
52
|
-
responses: jsonResponse({
|
|
53
|
-
description: 'Deleted article',
|
|
54
|
-
schema: ArticleSchema,
|
|
55
|
-
}),
|
|
47
|
+
responses: jsonResponse({ description: 'Deleted article', schema: ArticleSchema }),
|
|
56
48
|
},
|
|
57
|
-
handler: async
|
|
49
|
+
handler: async context => {
|
|
58
50
|
const { id } = context.req.valid('param');
|
|
59
|
-
|
|
60
|
-
return context.json(result);
|
|
51
|
+
return context.json(await this.articleService.deleteById({ id }));
|
|
61
52
|
},
|
|
62
53
|
});
|
|
63
54
|
}
|
|
64
55
|
}
|
|
65
56
|
```
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
Pass an array of `IAuthorizationSpec` to require **all** specs to pass. Each spec creates a separate middleware -- all must succeed for the handler to execute:
|
|
70
|
-
|
|
71
|
-
```typescript
|
|
72
|
-
import { Authentication, AuthorizationActions } from '@venizia/ignis';
|
|
73
|
-
|
|
74
|
-
this.defineRoute({
|
|
75
|
-
configs: {
|
|
76
|
-
path: '/admin/users/{id}',
|
|
77
|
-
method: 'patch',
|
|
78
|
-
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
79
|
-
authorize: [
|
|
80
|
-
{ action: AuthorizationActions.UPDATE, resource: 'User' },
|
|
81
|
-
{ action: AuthorizationActions.UPDATE, resource: 'Admin' },
|
|
82
|
-
],
|
|
83
|
-
responses: jsonResponse({
|
|
84
|
-
description: 'Updated user',
|
|
85
|
-
schema: UserSchema,
|
|
86
|
-
}),
|
|
87
|
-
},
|
|
88
|
-
handler: async (context) => {
|
|
89
|
-
// Both 'update:User' AND 'update:Admin' must pass
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
> [!NOTE]
|
|
95
|
-
> When multiple specs are evaluated on the same route, rules are built once and cached on the context (`Authorization.RULES`). The second spec reuses the cached rules without rebuilding.
|
|
96
|
-
|
|
97
|
-
### Decorator-Based Route
|
|
98
|
-
|
|
99
|
-
Use the `authorize` field alongside `authenticate` in decorator configs:
|
|
58
|
+
**Decorator-based route.** Same `authorize` field inside `@get`/`@post`/etc. `configs`.
|
|
100
59
|
|
|
101
60
|
```typescript
|
|
102
61
|
import { controller, get, post, AuthorizationActions, AuthorizationRoles } from '@venizia/ignis';
|
|
103
62
|
|
|
104
63
|
@controller({ path: '/articles' })
|
|
105
64
|
class ArticleController extends BaseRestController {
|
|
106
|
-
@get({
|
|
107
|
-
configs: {
|
|
108
|
-
path: '/',
|
|
109
|
-
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
110
|
-
authorize: { action: AuthorizationActions.READ, resource: 'Article' },
|
|
111
|
-
responses: jsonResponse({ description: 'Articles', schema: z.array(ArticleSchema) }),
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
async findAll(opts: { context: TRouteContext }) {
|
|
115
|
-
// Handler runs only if authorized
|
|
116
|
-
}
|
|
117
|
-
|
|
118
65
|
@post({
|
|
119
66
|
configs: {
|
|
120
67
|
path: '/',
|
|
@@ -129,232 +76,127 @@ class ArticleController extends BaseRestController {
|
|
|
129
76
|
},
|
|
130
77
|
})
|
|
131
78
|
async create(opts: { context: TRouteContext }) {
|
|
132
|
-
//
|
|
79
|
+
// Runs if the user has 'create:Article' permission OR the 'editor'/'900_admin' role
|
|
133
80
|
}
|
|
134
81
|
}
|
|
135
82
|
```
|
|
136
83
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Authorization works the same way in gRPC controllers. Use the `authorize` field in RPC metadata:
|
|
84
|
+
**Require multiple specs.** Pass an array - each spec becomes its own middleware, and all must pass.
|
|
140
85
|
|
|
141
86
|
```typescript
|
|
142
|
-
|
|
87
|
+
authorize: [
|
|
88
|
+
{ action: AuthorizationActions.UPDATE, resource: 'User' },
|
|
89
|
+
{ action: AuthorizationActions.UPDATE, resource: 'Admin' },
|
|
90
|
+
]
|
|
91
|
+
```
|
|
143
92
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
93
|
+
> [!NOTE]
|
|
94
|
+
> When several specs run on the same route, rules are built once per ENFORCER, via `enforcer.buildRules()`, and cached on `Authorization.RULES` - a `Map` keyed by enforcer name - for the rest of the request. A second spec on the same enforcer reuses them; a spec on a different enforcer builds its own. The cache used to be a single slot shared by every enforcer, so one enforcer's rules could answer another's check.
|
|
95
|
+
|
|
96
|
+
**gRPC route.** Same `authorize` field, inside RPC metadata. `AbstractGrpcController.buildRpcMiddlewares()` injects it in the same order as REST: authenticate, then authorize.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
@unary({
|
|
100
|
+
configs: {
|
|
101
|
+
name: 'deleteUser',
|
|
102
|
+
authenticate: { strategies: ['jwt'] },
|
|
103
|
+
authorize: { action: AuthorizationActions.DELETE, resource: 'user' },
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
async deleteUser(opts: { request: DeleteUserRequest; context: TRouteContext }) { ... }
|
|
161
107
|
```
|
|
162
108
|
|
|
163
|
-
|
|
109
|
+
See [gRPC Controllers](/guides/core-concepts/grpc-controllers) for the full decorator/`defineRoute` gRPC surface.
|
|
164
110
|
|
|
165
|
-
## Using the `authorize()`
|
|
111
|
+
## Using the standalone `authorize()` function
|
|
166
112
|
|
|
167
|
-
|
|
113
|
+
`authorize({ spec, enforcerName? })` returns a plain Hono `MiddlewareHandler`. Use it outside route configs - for example, when you wire a Hono sub-app directly.
|
|
168
114
|
|
|
169
115
|
```typescript
|
|
170
116
|
import { authorize, authenticate, Authentication, AuthorizationActions } from '@venizia/ignis';
|
|
171
117
|
|
|
172
|
-
// Use as Hono middleware directly
|
|
173
118
|
app.delete(
|
|
174
119
|
'/articles/:id',
|
|
175
120
|
authenticate({ strategies: [Authentication.STRATEGY_JWT] }),
|
|
176
121
|
authorize({ spec: { action: AuthorizationActions.DELETE, resource: 'Article' } }),
|
|
177
|
-
|
|
178
|
-
const user = c.get(Authentication.CURRENT_USER);
|
|
179
|
-
return c.json({ deleted: true });
|
|
180
|
-
},
|
|
122
|
+
c => c.json({ deleted: true }),
|
|
181
123
|
);
|
|
182
124
|
```
|
|
183
125
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
If multiple enforcers are registered, specify which one to use:
|
|
126
|
+
**Target a specific enforcer.** Omit `enforcerName` to use the first one registered.
|
|
187
127
|
|
|
188
128
|
```typescript
|
|
189
129
|
authorize({
|
|
190
130
|
spec: { action: AuthorizationActions.READ, resource: 'Report' },
|
|
191
|
-
enforcerName: 'my-custom',
|
|
131
|
+
enforcerName: 'my-custom',
|
|
192
132
|
});
|
|
193
133
|
```
|
|
194
134
|
|
|
195
135
|
## Voters
|
|
196
136
|
|
|
197
|
-
Voters
|
|
198
|
-
|
|
199
|
-
```mermaid
|
|
200
|
-
flowchart TD
|
|
201
|
-
Start([Step 4: Voters]) --> HasVoters{Has voters?}
|
|
202
|
-
HasVoters -->|No| Enforcer([Continue to enforcer])
|
|
203
|
-
HasVoters -->|Yes| V1["Call voter 1"]
|
|
204
|
-
V1 --> D1{Decision?}
|
|
205
|
-
D1 -->|DENY| E403[/403 Forbidden/]
|
|
206
|
-
D1 -->|ALLOW| Next([next - authorized])
|
|
207
|
-
D1 -->|ABSTAIN| V2["Call voter 2"]
|
|
208
|
-
V2 --> D2{Decision?}
|
|
209
|
-
D2 -->|DENY| E403
|
|
210
|
-
D2 -->|ALLOW| Next
|
|
211
|
-
D2 -->|ABSTAIN| VN["... voter N"]
|
|
212
|
-
VN -->|All ABSTAIN| Enforcer
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
Each voter returns one of three decisions:
|
|
137
|
+
Voters run **before** the enforcer (pipeline step 4) and are evaluated in order - the first non-`ABSTAIN` decision wins.
|
|
216
138
|
|
|
217
139
|
| Decision | Effect |
|
|
218
140
|
|----------|--------|
|
|
219
|
-
| `AuthorizationDecisions.ALLOW` |
|
|
220
|
-
| `AuthorizationDecisions.DENY` |
|
|
221
|
-
| `AuthorizationDecisions.ABSTAIN` | No opinion
|
|
222
|
-
|
|
223
|
-
### Basic Voter Example
|
|
141
|
+
| `AuthorizationDecisions.ALLOW` | Grants access immediately - skips remaining voters and the enforcer |
|
|
142
|
+
| `AuthorizationDecisions.DENY` | Denies access immediately - throws 403 |
|
|
143
|
+
| `AuthorizationDecisions.ABSTAIN` | No opinion - falls through to the next voter, then the enforcer |
|
|
224
144
|
|
|
225
145
|
```typescript
|
|
226
|
-
import {
|
|
227
|
-
AuthorizationActions,
|
|
228
|
-
AuthorizationDecisions,
|
|
229
|
-
TAuthorizationVoter,
|
|
230
|
-
} from '@venizia/ignis';
|
|
146
|
+
import { AuthorizationActions, AuthorizationDecisions, TAuthorizationVoter } from '@venizia/ignis';
|
|
231
147
|
|
|
232
148
|
const ownerVoter: TAuthorizationVoter = async ({ user, action, resource, context }) => {
|
|
233
149
|
if (action !== AuthorizationActions.UPDATE && action !== AuthorizationActions.DELETE) {
|
|
234
150
|
return AuthorizationDecisions.ABSTAIN;
|
|
235
151
|
}
|
|
236
152
|
|
|
237
|
-
const
|
|
238
|
-
const article = await articleService.findById({ id: articleId });
|
|
239
|
-
|
|
153
|
+
const article = await articleService.findById({ id: context.req.param('id') });
|
|
240
154
|
if (!article) {
|
|
241
155
|
return AuthorizationDecisions.ABSTAIN;
|
|
242
156
|
}
|
|
243
157
|
|
|
244
|
-
|
|
245
|
-
return AuthorizationDecisions.ALLOW;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
return AuthorizationDecisions.ABSTAIN; // Let enforcer decide
|
|
158
|
+
return article.authorId === user.userId ? AuthorizationDecisions.ALLOW : AuthorizationDecisions.ABSTAIN;
|
|
249
159
|
};
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
### Using Voters in Routes
|
|
253
160
|
|
|
254
|
-
```typescript
|
|
255
|
-
this.defineRoute({
|
|
256
|
-
configs: {
|
|
257
|
-
path: '/{id}',
|
|
258
|
-
method: 'patch',
|
|
259
|
-
authenticate: { strategies: [Authentication.STRATEGY_JWT] },
|
|
260
|
-
authorize: {
|
|
261
|
-
action: AuthorizationActions.UPDATE,
|
|
262
|
-
resource: 'Article',
|
|
263
|
-
voters: [ownerVoter],
|
|
264
|
-
},
|
|
265
|
-
// ...
|
|
266
|
-
},
|
|
267
|
-
handler: async (context) => {
|
|
268
|
-
// Runs if: owner (voter ALLOW) OR enforcer permits
|
|
269
|
-
},
|
|
270
|
-
});
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### Multiple Voters
|
|
274
|
-
|
|
275
|
-
Voters are evaluated sequentially. The first non-ABSTAIN decision wins:
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
161
|
authorize: {
|
|
279
162
|
action: AuthorizationActions.UPDATE,
|
|
280
163
|
resource: 'Article',
|
|
281
|
-
voters: [ownerVoter, adminOverrideVoter,
|
|
164
|
+
voters: [ownerVoter, adminOverrideVoter], // sequential; first non-ABSTAIN wins
|
|
282
165
|
}
|
|
283
166
|
```
|
|
284
167
|
|
|
285
|
-
**Evaluation flow:**
|
|
286
|
-
1. `ownerVoter` returns `ABSTAIN` -- continue
|
|
287
|
-
2. `adminOverrideVoter` returns `ALLOW` -- **access granted** (skips remaining voters and enforcer)
|
|
288
|
-
|
|
289
168
|
> [!TIP]
|
|
290
|
-
>
|
|
169
|
+
> Default to `ABSTAIN` when a voter has no strong opinion. Only return `DENY` when the request should be blocked regardless of any other check. It short-circuits everything, including a later `ALLOW` voter.
|
|
291
170
|
|
|
292
|
-
## Role-
|
|
171
|
+
## Role-based shortcuts
|
|
293
172
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
Roles listed in `alwaysAllowRoles` bypass **all** authorization checks globally (step 3 in the pipeline):
|
|
173
|
+
**Global bypass.** Roles in `alwaysAllowRoles` (bound via `IAuthorizeOptions`) skip all authorization checks on every route.
|
|
297
174
|
|
|
298
175
|
```typescript
|
|
299
|
-
import { AuthorizationRoles } from '@venizia/ignis';
|
|
300
|
-
|
|
301
176
|
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
302
177
|
defaultDecision: 'deny',
|
|
303
178
|
alwaysAllowRoles: [AuthorizationRoles.SUPER_ADMIN.identifier, 'system'],
|
|
304
179
|
});
|
|
305
180
|
```
|
|
306
181
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
Roles listed in `allowedRoles` on a specific `IAuthorizationSpec` bypass the enforcer for that route only (still evaluated at step 3):
|
|
182
|
+
**Per-route bypass.** Roles in a spec's `allowedRoles` skip the enforcer for that route only.
|
|
310
183
|
|
|
311
184
|
```typescript
|
|
312
|
-
authorize: {
|
|
313
|
-
action: AuthorizationActions.DELETE,
|
|
314
|
-
resource: 'Article',
|
|
315
|
-
allowedRoles: [AuthorizationRoles.ADMIN.identifier, 'moderator'],
|
|
316
|
-
}
|
|
185
|
+
authorize: { action: AuthorizationActions.DELETE, resource: 'Article', allowedRoles: [AuthorizationRoles.ADMIN.identifier, 'moderator'] }
|
|
317
186
|
```
|
|
318
187
|
|
|
319
|
-
|
|
188
|
+
**Role formats accepted.** `extractUserRoles()` reads `user.roles` and normalizes every entry to a string, trying `identifier` first, then `name`, then `String(id)`.
|
|
320
189
|
|
|
321
|
-
|
|
190
|
+
| `user.roles` shape | Extracted as |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `['admin', 'user']` | Used as-is |
|
|
193
|
+
| `[{ id: 1, identifier: '900_admin', priority: 900 }]` | `'900_admin'` (preferred - matches `AuthorizationRole.identifier`) |
|
|
194
|
+
| `[{ id: 1, name: 'admin' }]` | `'admin'` |
|
|
195
|
+
| `[{ id: 1 }]` | `'1'` |
|
|
322
196
|
|
|
323
|
-
|
|
324
|
-
flowchart TD
|
|
325
|
-
Input["user.roles"] --> IsArray{Array?}
|
|
326
|
-
IsArray -->|No| Empty(["return []"])
|
|
327
|
-
IsArray -->|Yes| Map["Map each role"]
|
|
328
|
-
Map --> Type{Type?}
|
|
329
|
-
Type -->|string| AsIs["Use as-is"]
|
|
330
|
-
Type -->|object| Prio["r.identifier"]
|
|
331
|
-
Prio -->|undefined| Name["r.name"]
|
|
332
|
-
Name -->|undefined| Id["String(r.id)"]
|
|
333
|
-
```
|
|
197
|
+
## CRUD factory integration
|
|
334
198
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
```typescript
|
|
338
|
-
// String array
|
|
339
|
-
roles: ['admin', 'user']
|
|
340
|
-
|
|
341
|
-
// Object array with identifier (preferred - matches AuthorizationRole.identifier)
|
|
342
|
-
roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
|
|
343
|
-
|
|
344
|
-
// Object array with name fallback
|
|
345
|
-
roles: [{ id: 1, name: 'admin' }]
|
|
346
|
-
|
|
347
|
-
// Object array with id-only fallback
|
|
348
|
-
roles: [{ id: 1 }]
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
Extraction priority: `identifier` > `name` > `String(id)`
|
|
352
|
-
|
|
353
|
-
## CRUD Factory Integration
|
|
354
|
-
|
|
355
|
-
### Controller-Level Authorization
|
|
356
|
-
|
|
357
|
-
Apply authorization to all CRUD routes:
|
|
199
|
+
**Apply to every generated route.** Set `authorize` at the controller level in `defineCrudController`.
|
|
358
200
|
|
|
359
201
|
```typescript
|
|
360
202
|
import { AuthorizationActions } from '@venizia/ignis';
|
|
@@ -368,89 +210,41 @@ ControllerFactory.defineCrudController({
|
|
|
368
210
|
});
|
|
369
211
|
```
|
|
370
212
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
Override authorization per CRUD endpoint:
|
|
213
|
+
**Override per endpoint.** `routes.<endpoint>.authorize` takes precedence over the controller-level spec.
|
|
374
214
|
|
|
375
215
|
```typescript
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// Public read -- skip both auth
|
|
386
|
-
find: { authenticate: { skip: true } },
|
|
387
|
-
count: { authenticate: { skip: true } },
|
|
388
|
-
|
|
389
|
-
// Custom authorization for write operations
|
|
390
|
-
create: {
|
|
391
|
-
authorize: { action: AuthorizationActions.CREATE, resource: 'Article' },
|
|
392
|
-
},
|
|
393
|
-
updateById: {
|
|
394
|
-
authorize: { action: AuthorizationActions.UPDATE, resource: 'Article' },
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
// Skip only authorization (still requires auth)
|
|
398
|
-
findOne: { authorize: { skip: true } },
|
|
399
|
-
|
|
400
|
-
// Strict delete with custom roles
|
|
401
|
-
deleteById: {
|
|
402
|
-
authorize: {
|
|
403
|
-
action: AuthorizationActions.DELETE,
|
|
404
|
-
resource: 'Article',
|
|
405
|
-
allowedRoles: [AuthorizationRoles.ADMIN.identifier],
|
|
406
|
-
},
|
|
216
|
+
routes: {
|
|
217
|
+
find: { authenticate: { skip: true } }, // public read - skips BOTH checks
|
|
218
|
+
count: { authorize: { skip: true } }, // authenticated, no authorization
|
|
219
|
+
create: { authorize: { action: AuthorizationActions.CREATE, resource: 'Article' } },
|
|
220
|
+
deleteById: {
|
|
221
|
+
authorize: {
|
|
222
|
+
action: AuthorizationActions.DELETE,
|
|
223
|
+
resource: 'Article',
|
|
224
|
+
allowedRoles: [AuthorizationRoles.ADMIN.identifier],
|
|
407
225
|
},
|
|
408
226
|
},
|
|
409
|
-
}
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
### Priority Resolution (Factory Routes)
|
|
413
|
-
|
|
414
|
-
The `resolveRouteAuthorize` function in `defineControllerRouteConfigs` resolves authorization with this priority:
|
|
415
|
-
|
|
416
|
-
```mermaid
|
|
417
|
-
flowchart TD
|
|
418
|
-
Route([Route config]) --> AuthSkip{"authenticate:<br/>{ skip: true }?"}
|
|
419
|
-
AuthSkip -->|Yes| NoAuth([Skip BOTH<br/>auth + authz])
|
|
420
|
-
AuthSkip -->|No| AuthzSkip{"authorize:<br/>{ skip: true }?"}
|
|
421
|
-
AuthzSkip -->|Yes| NoAuthz([Skip authz only])
|
|
422
|
-
AuthzSkip -->|No| PerRoute{"Per-route<br/>authorize spec?"}
|
|
423
|
-
PerRoute -->|Yes| UseRoute([Use per-route spec])
|
|
424
|
-
PerRoute -->|No| Controller{"Controller-level<br/>authorize?"}
|
|
425
|
-
Controller -->|Yes| UseCtrl([Use controller spec])
|
|
426
|
-
Controller -->|No| NoAuthz2([No authorization])
|
|
227
|
+
}
|
|
427
228
|
```
|
|
428
229
|
|
|
429
|
-
|
|
430
|
-
2. **`authorize: { skip: true }`** -- skips authorization only
|
|
431
|
-
3. **Per-route `authorize` spec** -- overrides controller-level
|
|
432
|
-
4. **Controller-level `authorize`** -- default for all routes
|
|
433
|
-
|
|
434
|
-
### Per-Route Auth Type
|
|
230
|
+
**Priority, in order:**
|
|
435
231
|
|
|
436
|
-
|
|
232
|
+
1. `authenticate: { skip: true }` - skips both authentication AND authorization
|
|
233
|
+
2. `authorize: { skip: true }` - skips authorization only (authentication still runs)
|
|
234
|
+
3. Per-route `authorize` spec - overrides the controller-level spec
|
|
235
|
+
4. No per-route config - inherits the controller-level `authorize`
|
|
437
236
|
|
|
438
|
-
|
|
439
|
-
type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
|
|
440
|
-
```
|
|
441
|
-
|
|
442
|
-
This means each route can either skip authorization entirely, provide a single spec, or provide an array of specs that all must pass.
|
|
237
|
+
Per-route config is typed `TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[]`. See the [API Reference](./api#crud-factory-authorization) for the full resolver signature.
|
|
443
238
|
|
|
444
|
-
## Dynamic
|
|
239
|
+
## Dynamic skip authorization
|
|
445
240
|
|
|
446
|
-
|
|
241
|
+
Set `Authorization.SKIP_AUTHORIZATION` from an earlier middleware to bypass authorization for that one request (pipeline step 1). This is useful for trusted service-to-service calls.
|
|
447
242
|
|
|
448
243
|
```typescript
|
|
449
244
|
import { Authorization } from '@venizia/ignis';
|
|
450
245
|
import { createMiddleware } from 'hono/factory';
|
|
451
246
|
|
|
452
247
|
const conditionalAuthzMiddleware = createMiddleware(async (c, next) => {
|
|
453
|
-
// Skip authorization for internal service-to-service calls
|
|
454
248
|
if (c.req.header('X-Internal-Service') === 'trusted-key') {
|
|
455
249
|
c.set(Authorization.SKIP_AUTHORIZATION, true);
|
|
456
250
|
}
|
|
@@ -458,64 +252,38 @@ const conditionalAuthzMiddleware = createMiddleware(async (c, next) => {
|
|
|
458
252
|
});
|
|
459
253
|
```
|
|
460
254
|
|
|
461
|
-
##
|
|
462
|
-
|
|
463
|
-
The authorization middleware caches rules on the Hono context to avoid rebuilding them on every authorization spec evaluation. This is especially useful when multiple authorization specs are applied to the same route:
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
// First spec triggers buildRules() → result cached on context
|
|
467
|
-
authorize: [
|
|
468
|
-
{ action: AuthorizationActions.READ, resource: 'Article' },
|
|
469
|
-
{ action: AuthorizationActions.READ, resource: 'Comment' },
|
|
470
|
-
]
|
|
471
|
-
// Second spec reuses cached rules → no rebuild
|
|
472
|
-
```
|
|
473
|
-
|
|
474
|
-
> [!TIP]
|
|
475
|
-
> Rules caching happens per-request. Each new HTTP request starts with an empty cache. If you need to invalidate cached rules mid-request (e.g., after role change), set `context.set(Authorization.RULES, null)`.
|
|
476
|
-
|
|
477
|
-
## Accessing Context Variables
|
|
478
|
-
|
|
479
|
-
The authorization module provides type-safe access to auth data on the Hono context:
|
|
255
|
+
## Accessing context variables
|
|
480
256
|
|
|
481
257
|
```typescript
|
|
482
258
|
import { Authorization, Authentication } from '@venizia/ignis';
|
|
483
259
|
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
const domain = c.get(Authorization.DOMAIN); // string ("<Type>_<id>" | "SYSTEM_WIDE"), set when domain scoping is in play
|
|
260
|
+
const user = c.get(Authentication.CURRENT_USER); // IAuthUser
|
|
261
|
+
const rules = c.get(Authorization.RULES); // unknown - shape depends on the enforcer
|
|
262
|
+
const isSkipped = c.get(Authorization.SKIP_AUTHORIZATION); // boolean
|
|
263
|
+
const domain = c.get(Authorization.DOMAIN); // "<Type>_<id>" | "SYSTEM_WIDE" - set only when domain scoping is in play
|
|
489
264
|
|
|
490
|
-
// Set skip dynamically
|
|
491
265
|
c.set(Authorization.SKIP_AUTHORIZATION, true);
|
|
492
|
-
|
|
493
|
-
// Invalidate cached rules
|
|
494
|
-
c.set(Authorization.RULES, null);
|
|
266
|
+
c.set(Authorization.RULES, null); // force a rebuild later in the SAME request
|
|
495
267
|
```
|
|
496
268
|
|
|
497
|
-
|
|
269
|
+
> [!TIP]
|
|
270
|
+
> Rules are cached per-request only - every new HTTP request starts with an empty cache. `c.set(Authorization.RULES, null)` only matters if you need to force a rebuild mid-request, for example after mutating the user's roles inline.
|
|
271
|
+
|
|
272
|
+
## Custom enforcer
|
|
498
273
|
|
|
499
|
-
|
|
274
|
+
Implement `IAuthorizationEnforcer` to replace or supplement Casbin.
|
|
500
275
|
|
|
501
276
|
```typescript
|
|
502
277
|
import {
|
|
503
|
-
IAuthorizationEnforcer,
|
|
504
|
-
|
|
505
|
-
IAuthUser,
|
|
506
|
-
TAuthorizationDecision,
|
|
507
|
-
AuthorizationDecisions,
|
|
508
|
-
TContext,
|
|
278
|
+
IAuthorizationEnforcer, IAuthorizationRequest, IAuthUser, TAuthorizationDecision,
|
|
279
|
+
AuthorizationDecisions, TContext,
|
|
509
280
|
} from '@venizia/ignis';
|
|
510
281
|
import { BaseHelper, ValueOrPromise } from '@venizia/ignis-helpers';
|
|
511
282
|
import { Env } from 'hono';
|
|
512
283
|
|
|
513
284
|
type MyRules = Map<string, Set<string>>;
|
|
514
285
|
|
|
515
|
-
class MyCustomEnforcer
|
|
516
|
-
extends BaseHelper
|
|
517
|
-
implements IAuthorizationEnforcer<Env, string, string, MyRules>
|
|
518
|
-
{
|
|
286
|
+
class MyCustomEnforcer extends BaseHelper implements IAuthorizationEnforcer<Env, string, string, MyRules> {
|
|
519
287
|
name = 'my-custom';
|
|
520
288
|
|
|
521
289
|
constructor() {
|
|
@@ -523,85 +291,54 @@ class MyCustomEnforcer
|
|
|
523
291
|
}
|
|
524
292
|
|
|
525
293
|
async configure(): Promise<void> {
|
|
526
|
-
// One-time initialization
|
|
294
|
+
// One-time initialization - called by the registry on first resolveEnforcer()
|
|
527
295
|
}
|
|
528
296
|
|
|
529
|
-
async buildRules(opts: {
|
|
530
|
-
|
|
531
|
-
context: TContext;
|
|
532
|
-
}): Promise<MyRules> {
|
|
533
|
-
const rules = new Map<string, Set<string>>();
|
|
534
|
-
// Build your rules map from DB, config, etc.
|
|
535
|
-
return rules;
|
|
297
|
+
async buildRules(opts: { user: { principalType: string } & IAuthUser; context: TContext }): Promise<MyRules> {
|
|
298
|
+
return new Map(); // build from DB, config, etc.
|
|
536
299
|
}
|
|
537
300
|
|
|
538
|
-
async evaluate(opts: {
|
|
539
|
-
rules
|
|
540
|
-
request:
|
|
541
|
-
context: TContext;
|
|
542
|
-
}): Promise<TAuthorizationDecision> {
|
|
543
|
-
const { rules, request } = opts;
|
|
544
|
-
const resourceActions = rules.get(request.resource);
|
|
545
|
-
if (resourceActions?.has(request.action)) {
|
|
546
|
-
return AuthorizationDecisions.ALLOW;
|
|
547
|
-
}
|
|
548
|
-
return AuthorizationDecisions.DENY;
|
|
301
|
+
async evaluate(opts: { rules: MyRules; request: IAuthorizationRequest; context: TContext }): Promise<TAuthorizationDecision> {
|
|
302
|
+
const actions = opts.rules.get(opts.request.resource);
|
|
303
|
+
return actions?.has(opts.request.action) ? AuthorizationDecisions.ALLOW : AuthorizationDecisions.DENY;
|
|
549
304
|
}
|
|
550
305
|
}
|
|
551
306
|
```
|
|
552
307
|
|
|
553
|
-
|
|
308
|
+
Register it the same way as the Casbin enforcer, with `type: AuthorizationEnforcerTypes.CUSTOM`:
|
|
554
309
|
|
|
555
310
|
```typescript
|
|
556
|
-
import {
|
|
557
|
-
AuthorizationEnforcerRegistry,
|
|
558
|
-
AuthorizationEnforcerTypes,
|
|
559
|
-
AuthorizeBindingKeys,
|
|
560
|
-
AuthorizeComponent,
|
|
561
|
-
IAuthorizeOptions,
|
|
562
|
-
} from '@venizia/ignis';
|
|
563
|
-
|
|
564
|
-
// Step 1: Global options
|
|
565
|
-
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
566
|
-
defaultDecision: 'deny',
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
// Step 2: Component
|
|
570
|
-
this.component(AuthorizeComponent);
|
|
571
|
-
|
|
572
|
-
// Step 3: Register custom enforcer
|
|
573
311
|
AuthorizationEnforcerRegistry.getInstance().register({
|
|
574
312
|
container: this,
|
|
575
313
|
enforcers: [{
|
|
576
314
|
enforcer: MyCustomEnforcer,
|
|
577
315
|
name: 'my-custom',
|
|
578
316
|
type: AuthorizationEnforcerTypes.CUSTOM,
|
|
579
|
-
options: { /* your enforcer-specific options if
|
|
317
|
+
options: { /* your enforcer-specific options, if any */ },
|
|
580
318
|
}],
|
|
581
319
|
});
|
|
582
320
|
```
|
|
583
321
|
|
|
584
322
|
> [!NOTE]
|
|
585
|
-
>
|
|
323
|
+
> A custom enforcer can inject its own options via `@inject({ key: AuthorizeBindingKeys.enforcerOptions('my-custom') })` in the constructor, exactly like `CasbinAuthorizationEnforcer` does.
|
|
324
|
+
|
|
325
|
+
## Custom filtered adapter
|
|
326
|
+
|
|
327
|
+
Use the ready-made [`ScopedCasbinAdapter`](./api#scopedcasbinadapter) for most apps - it reads one edge table and needs no subclassing. Write a custom adapter only when your storage model differs.
|
|
586
328
|
|
|
587
|
-
|
|
329
|
+
`BaseFilteredAdapter` provides:
|
|
588
330
|
|
|
589
|
-
|
|
590
|
-
|
|
331
|
+
- Datasource/connector plumbing
|
|
332
|
+
- `isFiltered()`
|
|
333
|
+
- The no-op write methods
|
|
334
|
+
- A `loadLines` helper
|
|
591
335
|
|
|
592
|
-
`
|
|
593
|
-
flag, no-op write methods, and a `loadLines` helper. A subclass implements **only** `loadFilteredPolicy`
|
|
594
|
-
- query your store for ONE principal's policies and turn them into casbin lines.
|
|
336
|
+
A subclass implements only `loadFilteredPolicy`: query your store for ONE principal's policies and turn them into casbin lines.
|
|
595
337
|
|
|
596
338
|
```typescript
|
|
597
|
-
import {
|
|
598
|
-
BaseFilteredAdapter,
|
|
599
|
-
ICasbinPolicyFilter,
|
|
600
|
-
type ICasbinPolicySource,
|
|
601
|
-
} from '@venizia/ignis';
|
|
339
|
+
import { BaseFilteredAdapter, ICasbinPolicyFilter, type ICasbinPolicySource } from '@venizia/ignis';
|
|
602
340
|
import type { Model } from 'casbin';
|
|
603
341
|
|
|
604
|
-
// Narrow the filter if you like, or use the default ICasbinPolicyFilter ({ principal: { type, id } }).
|
|
605
342
|
class MyCustomAdapter extends BaseFilteredAdapter<ICasbinPolicyFilter> {
|
|
606
343
|
constructor(opts: { dataSource: ICasbinPolicySource }) {
|
|
607
344
|
super({ scope: MyCustomAdapter.name, dataSource: opts.dataSource });
|
|
@@ -611,9 +348,7 @@ class MyCustomAdapter extends BaseFilteredAdapter<ICasbinPolicyFilter> {
|
|
|
611
348
|
const { type, id } = filter.principal;
|
|
612
349
|
|
|
613
350
|
// 1. Query your store for THIS principal's policies (this.connector is provided by the base).
|
|
614
|
-
// 2. Build casbin lines as plain strings, e.g
|
|
615
|
-
// `p, User_${id}, Order, read, allow`
|
|
616
|
-
// `g, User_${id}, Role_42, *`
|
|
351
|
+
// 2. Build casbin lines as plain strings, e.g. `p, User_${id}, Order, read, allow` or `g, User_${id}, Role_42, *`.
|
|
617
352
|
const lines: string[] = await this.buildLinesFor({ type, id });
|
|
618
353
|
|
|
619
354
|
// 3. Load them into the model with the base helper.
|
|
@@ -621,58 +356,46 @@ class MyCustomAdapter extends BaseFilteredAdapter<ICasbinPolicyFilter> {
|
|
|
621
356
|
}
|
|
622
357
|
|
|
623
358
|
private async buildLinesFor(principal: { type: string; id: unknown }): Promise<string[]> {
|
|
624
|
-
// ...your queries via this.connector...
|
|
625
|
-
return [];
|
|
359
|
+
return []; // ...your queries via this.connector...
|
|
626
360
|
}
|
|
627
361
|
}
|
|
628
362
|
```
|
|
629
363
|
|
|
630
|
-
The base no
|
|
631
|
-
formatters - you own line construction. See `ScopedCasbinAdapter` for a full reference implementation
|
|
632
|
-
(role closure, structural trees, soft-delete, schema-qualified SQL).
|
|
364
|
+
The base has no template-method query hooks or line formatters - a subclass owns its own queries and line construction. See `ScopedCasbinAdapter` for a full reference implementation (role closure, structural trees, soft-delete, schema-qualified SQL).
|
|
633
365
|
|
|
634
|
-
## AuthorizationRole
|
|
366
|
+
## AuthorizationRole comparison
|
|
635
367
|
|
|
636
|
-
|
|
368
|
+
`AuthorizationRole` gives priority-based role comparison; identifiers are `{paddedPriority}{delimiter}{name}`.
|
|
637
369
|
|
|
638
370
|
```typescript
|
|
639
371
|
import { AuthorizationRole, AuthorizationRoles } from '@venizia/ignis';
|
|
640
372
|
|
|
641
|
-
// Built-in roles
|
|
642
373
|
AuthorizationRoles.SUPER_ADMIN.identifier; // '999_super-admin'
|
|
643
|
-
AuthorizationRoles.ADMIN.identifier;
|
|
644
|
-
AuthorizationRoles.USER.identifier; // '010_user'
|
|
374
|
+
AuthorizationRoles.ADMIN.identifier; // '900_admin'
|
|
645
375
|
|
|
646
|
-
//
|
|
647
|
-
AuthorizationRoles.
|
|
648
|
-
AuthorizationRoles.GUEST.isLowerThan({ target: AuthorizationRoles.USER }); // true
|
|
376
|
+
AuthorizationRoles.SUPER_ADMIN.isHigherThan({ target: AuthorizationRoles.ADMIN }); // true (999 > 900)
|
|
377
|
+
AuthorizationRoles.GUEST.isLowerThan({ target: AuthorizationRoles.USER }); // true (1 < 10)
|
|
649
378
|
|
|
650
|
-
// Custom roles
|
|
651
379
|
const moderator = AuthorizationRole.build({ name: 'moderator', priority: 500 });
|
|
652
|
-
moderator.identifier;
|
|
653
|
-
moderator.isHigherThan({ target: AuthorizationRoles.USER }); // true (500 > 10)
|
|
654
|
-
moderator.isLowerThan({ target: AuthorizationRoles.ADMIN }); // true (500 < 900)
|
|
380
|
+
moderator.identifier; // '500_moderator'
|
|
655
381
|
|
|
656
|
-
// Custom delimiter
|
|
657
382
|
const customRole = AuthorizationRole.build({ name: 'editor', priority: 100, delimiter: '-' });
|
|
658
|
-
customRole.identifier;
|
|
383
|
+
customRole.identifier; // '100-editor'
|
|
659
384
|
```
|
|
660
385
|
|
|
661
386
|
## Model-Based Resource References
|
|
662
387
|
|
|
663
|
-
Instead of hardcoding resource strings,
|
|
388
|
+
Instead of hardcoding resource strings, declare `authorize.principal` on the model - `@model` auto-populates the static `AUTHORIZATION_SUBJECT` from it.
|
|
664
389
|
|
|
665
390
|
```typescript
|
|
666
|
-
import {
|
|
391
|
+
import { BasePostgresEntity, model, generateIdColumnDefs } from '@venizia/ignis';
|
|
667
392
|
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
668
393
|
|
|
669
394
|
@model({
|
|
670
395
|
type: 'entity',
|
|
671
|
-
settings: {
|
|
672
|
-
authorize: { principal: 'article' },
|
|
673
|
-
},
|
|
396
|
+
settings: { authorize: { principal: 'article' } },
|
|
674
397
|
})
|
|
675
|
-
export class Article extends
|
|
398
|
+
export class Article extends BasePostgresEntity<typeof Article.schema> {
|
|
676
399
|
static override schema = pgTable('Article', {
|
|
677
400
|
...generateIdColumnDefs({ id: { dataType: 'string' } }),
|
|
678
401
|
title: text('title').notNull(),
|
|
@@ -682,79 +405,60 @@ export class Article extends BaseEntity<typeof Article.schema> {
|
|
|
682
405
|
// Article.AUTHORIZATION_SUBJECT === 'article'
|
|
683
406
|
```
|
|
684
407
|
|
|
685
|
-
Use it in route configs for type-safe, refactor-friendly resource references:
|
|
686
|
-
|
|
687
408
|
```typescript
|
|
688
409
|
import { AuthorizationActions } from '@venizia/ignis';
|
|
689
410
|
import { Article } from '../models/entities/article.model';
|
|
690
411
|
|
|
691
|
-
// Instead of: resource: 'article'
|
|
692
412
|
authorize: {
|
|
693
413
|
action: AuthorizationActions.READ,
|
|
694
|
-
resource: Article.AUTHORIZATION_SUBJECT,
|
|
414
|
+
resource: Article.AUTHORIZATION_SUBJECT, // instead of the string literal 'article'
|
|
695
415
|
}
|
|
696
416
|
```
|
|
697
417
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
Use `MetadataRegistry` to retrieve all registered authorization principals at runtime:
|
|
418
|
+
**Query every declared principal at runtime** - for example, to seed Casbin policies.
|
|
701
419
|
|
|
702
420
|
```typescript
|
|
703
421
|
import { MetadataRegistry } from '@venizia/ignis';
|
|
704
422
|
|
|
705
423
|
const registry = MetadataRegistry.getInstance();
|
|
706
424
|
|
|
707
|
-
|
|
708
|
-
const principals = registry.getAuthorizeModelPrincipals({ format: 'array' });
|
|
425
|
+
registry.getAuthorizeModelPrincipals({ format: 'array' });
|
|
709
426
|
// ['article', 'user', 'configuration']
|
|
710
427
|
|
|
711
|
-
|
|
712
|
-
const principalMap = registry.getAuthorizeModelPrincipals({ format: 'record' });
|
|
428
|
+
registry.getAuthorizeModelPrincipals({ format: 'record' });
|
|
713
429
|
// { Article: 'article', User: 'user', Configuration: 'configuration' }
|
|
714
430
|
|
|
715
|
-
|
|
716
|
-
const settings = registry.getAuthorizeModelSettings({ format: 'array' });
|
|
431
|
+
registry.getAuthorizeModelSettings({ format: 'array' });
|
|
717
432
|
// [{ name: 'Article', authorize: { principal: 'article' }, entry: IModelRegistryEntry }]
|
|
718
433
|
```
|
|
719
434
|
|
|
720
435
|
> [!TIP]
|
|
721
|
-
>
|
|
436
|
+
> Declaring `authorize.principal` on the model makes it the single source of truth for the authorization subject. Route configs and policy setup no longer duplicate the string. See the [Persistent Models guide](/guides/core-concepts/persistent/models#authorization-settings) for the full `@model` settings surface.
|
|
722
437
|
|
|
723
|
-
## RBAC with
|
|
438
|
+
## RBAC with domains (multi-tenant)
|
|
724
439
|
|
|
725
|
-
|
|
440
|
+
Some apps hand out roles **scoped to a specific tenant**. A user might be `owner` in Merchant A, only `viewer` in Merchant B, and hold a global role everywhere too. Casbin's [RBAC with domains](https://casbin.apache.org/docs/rbac-with-domains/) pattern keeps that policy **linear**: `memberships + permissions`, not a `permissions x tenants` cross-product. At 30 tenants and 700 permissions, that's roughly 730 lines instead of 21,000.
|
|
726
441
|
|
|
727
|
-
|
|
728
|
-
> Why this matters: putting the tenant on the membership (`g`) and keeping permissions wildcard (`p.dom = "*"`) keeps a user's materialized policy count **linear** (`memberships + permissions`) instead of the `permissions × tenants` cross-product. For a user with 30 tenants and 700 permissions that is ~730 lines instead of ~21,000.
|
|
729
|
-
|
|
730
|
-
There are two ways to do domain scoping:
|
|
442
|
+
You have two ways to get there:
|
|
731
443
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
- **Manual flat model (lower-level)** - keep a flat `g + p` model and register `domainMatching` +
|
|
737
|
-
`normalizePayloadFn` yourself. Documented below under [The model](#the-model).
|
|
444
|
+
| Approach | Setup | Matchers |
|
|
445
|
+
|---|---|---|
|
|
446
|
+
| **Scoped model (recommended)** | `isScoped: true` + `ScopedCasbinAdapter` + `CASBIN_RBAC_DOMAIN_SCOPED_MODEL`. Supply the domain per route via `spec.domain`, or globally via `domainResolver`. | Registered for you |
|
|
447
|
+
| **Manual flat model (lower-level)** | Keep a flat `g + p` model. See [The flat model](#the-flat-model). | Register `domainMatching` and `normalizePayloadFn` yourself |
|
|
738
448
|
|
|
739
449
|
### Scoped model + per-route domain (recommended)
|
|
740
450
|
|
|
741
|
-
Register the
|
|
742
|
-
then tell each route where to read its domain from. `IAuthorizationSpec.domain` accepts either a
|
|
743
|
-
**declarative source** or a **resolver function**:
|
|
451
|
+
Register the enforcer with `isScoped: true` (see the [Overview](./#in-one-example)), then tell each route where to read its domain from. `IAuthorizationSpec.domain` accepts a **declarative source** or a **resolver function**.
|
|
744
452
|
|
|
745
453
|
```typescript
|
|
746
454
|
import type { IAuthorizationDomainSource, TAuthorizationDomainResolver } from '@venizia/ignis';
|
|
747
455
|
|
|
748
456
|
// (a) Declarative - read the domain id from a request param/header/query/context var:
|
|
749
457
|
authorize({
|
|
750
|
-
spec: {
|
|
751
|
-
action: 'read',
|
|
752
|
-
resource: 'Order',
|
|
753
|
-
domain: { from: 'param', key: 'merchantId', type: 'Merchant' }, // → "Merchant_<param>"
|
|
754
|
-
},
|
|
458
|
+
spec: { action: 'read', resource: 'Order', domain: { from: 'param', key: 'merchantId', type: 'Merchant' } }, // -> "Merchant_<param>"
|
|
755
459
|
});
|
|
756
460
|
|
|
757
|
-
// (b) Resolver - compute { type, id } yourself (return null
|
|
461
|
+
// (b) Resolver - compute { type, id } yourself (return null -> SYSTEM_WIDE):
|
|
758
462
|
authorize({
|
|
759
463
|
spec: {
|
|
760
464
|
action: 'read',
|
|
@@ -767,12 +471,11 @@ authorize({
|
|
|
767
471
|
});
|
|
768
472
|
```
|
|
769
473
|
|
|
770
|
-
Precedence (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
enforces `SYSTEM_WIDE` (super-admin scope) in scoped mode.
|
|
474
|
+
- **Precedence** (`resolveRequestDomain`): `spec.domain` (resolver, then declarative) -> the global `IAuthorizeOptions.domainResolver` -> `SYSTEM_WIDE`.
|
|
475
|
+
- **Where it lands.** The resolved value is stashed on `Authorization.DOMAIN` and passed to the enforcer as `request.domain`.
|
|
476
|
+
- **No domain = super-admin scope.** A route with no domain source at all enforces `SYSTEM_WIDE` in scoped mode.
|
|
774
477
|
|
|
775
|
-
|
|
478
|
+
**Global fallback** - apply the same resolver to every route that doesn't set `spec.domain`:
|
|
776
479
|
|
|
777
480
|
```typescript
|
|
778
481
|
this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
@@ -784,7 +487,7 @@ this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
|
|
|
784
487
|
});
|
|
785
488
|
```
|
|
786
489
|
|
|
787
|
-
### The model
|
|
490
|
+
### The flat model
|
|
788
491
|
|
|
789
492
|
```ini
|
|
790
493
|
[request_definition]
|
|
@@ -804,93 +507,81 @@ m = g(r.sub, p.sub, r.dom) && keyMatch(r.dom, p.dom) && r.obj == p.obj && r.act
|
|
|
804
507
|
```
|
|
805
508
|
|
|
806
509
|
- `g = _, _, _` - the membership relation is **domain-aware** (subject, role, domain).
|
|
807
|
-
- `g(r.sub, p.sub, r.dom)` - the registered domain matching function decides whether the request domain matches the stored membership domain
|
|
808
|
-
- `keyMatch(r.dom, p.dom)` -
|
|
809
|
-
|
|
810
|
-
### Registering the domain matching function
|
|
510
|
+
- `g(r.sub, p.sub, r.dom)` - the registered domain matching function decides whether the request domain matches the stored membership domain. This is what makes `*` a wildcard.
|
|
511
|
+
- `keyMatch(r.dom, p.dom)` - a **built-in** matcher (no registration needed) that lets a permission with `p.dom = "*"` match any request domain.
|
|
811
512
|
|
|
812
|
-
Pass `domainMatching` in the enforcer options
|
|
513
|
+
**Register the domain matching function.** Pass `domainMatching` in the enforcer options - it registers once during `configure()`.
|
|
813
514
|
|
|
814
515
|
```typescript
|
|
815
516
|
import {
|
|
816
|
-
AuthorizationEnforcerRegistry,
|
|
817
|
-
|
|
818
|
-
CasbinAuthorizationEnforcer,
|
|
819
|
-
CasbinDomainMatchingFunctions,
|
|
820
|
-
CasbinEnforcerModelDrivers,
|
|
821
|
-
type ICasbinEnforcerOptions,
|
|
517
|
+
AuthorizationEnforcerRegistry, AuthorizationEnforcerTypes, CasbinAuthorizationEnforcer,
|
|
518
|
+
CasbinDomainMatchingFunctions, CasbinEnforcerModelDrivers, type ICasbinEnforcerOptions,
|
|
822
519
|
} from '@venizia/ignis';
|
|
823
520
|
|
|
824
521
|
AuthorizationEnforcerRegistry.getInstance().register({
|
|
825
522
|
container: this,
|
|
826
|
-
enforcers: [
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
} satisfies ICasbinEnforcerOptions,
|
|
845
|
-
},
|
|
846
|
-
],
|
|
523
|
+
enforcers: [{
|
|
524
|
+
enforcer: CasbinAuthorizationEnforcer,
|
|
525
|
+
name: 'casbin',
|
|
526
|
+
type: AuthorizationEnforcerTypes.CASBIN,
|
|
527
|
+
options: {
|
|
528
|
+
model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: theFlatModelConfAbove }, // your own .conf text - not a framework export
|
|
529
|
+
adapter,
|
|
530
|
+
cached,
|
|
531
|
+
// For a domain model, normalizePayloadFn MUST always return a `domain`.
|
|
532
|
+
normalizePayloadFn: ({ user, action, resource, context }) => ({
|
|
533
|
+
subject: `User_${user.userId}`,
|
|
534
|
+
domain: `Merchant_${resolveActiveMerchant({ context })}`,
|
|
535
|
+
resource,
|
|
536
|
+
action,
|
|
537
|
+
}),
|
|
538
|
+
domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.KEY_MATCH },
|
|
539
|
+
} satisfies ICasbinEnforcerOptions,
|
|
540
|
+
}],
|
|
847
541
|
});
|
|
848
542
|
```
|
|
849
543
|
|
|
850
544
|
> [!NOTE]
|
|
851
|
-
> `domainMatching` is opt-in. When omitted, domains are compared as exact strings and behavior is unchanged. The enforcer calls Casbin's `addNamedDomainMatchingFunc(roleDefinition, Util.keyMatchFunc)` internally -
|
|
545
|
+
> `domainMatching` is opt-in. When omitted, domains are compared as exact strings and behavior is unchanged. The enforcer calls Casbin's `addNamedDomainMatchingFunc(roleDefinition, Util.keyMatchFunc)` internally - never call it directly.
|
|
852
546
|
|
|
853
|
-
|
|
547
|
+
**Choosing the matching function.** `keyMatch` is the safe default for opaque domain identifiers like `Merchant_<uuid>`. It treats only `*` as special and never splits on `/` or `:`, so it cannot accidentally match one tenant against another. Use the others only for structured path-style domains.
|
|
854
548
|
|
|
855
|
-
|
|
549
|
+
| Function | Adds |
|
|
550
|
+
|---|---|
|
|
551
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH` | Nothing beyond `*` (recommended default) |
|
|
552
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH_2` | URL-path `:param` segments |
|
|
553
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH_3` | `{param}` segments |
|
|
554
|
+
| `CasbinDomainMatchingFunctions.KEY_MATCH_4` | `{param}` segments with repeated-name equality checks |
|
|
555
|
+
| `CasbinDomainMatchingFunctions.REGEX_MATCH` | Full regular-expression matching on the stored value |
|
|
856
556
|
|
|
857
|
-
|
|
858
|
-
domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.KEY_MATCH }; // * wildcard (recommended)
|
|
859
|
-
domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.KEY_MATCH_2 }; // /tenants/:id
|
|
860
|
-
domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.KEY_MATCH_3 }; // /tenants/{id}
|
|
861
|
-
domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.REGEX_MATCH }; // ^Merchant_.*$
|
|
862
|
-
```
|
|
863
|
-
|
|
864
|
-
### All cases - policy lines and outcomes
|
|
865
|
-
|
|
866
|
-
Given the model above with `keyMatch` registered on `g`, the request is `enforceSync(subject, domain, resource, action)`:
|
|
557
|
+
**Policy lines and outcomes**, using the flat model above with `keyMatch` registered on `g`. Each row calls `enforceSync(subject, domain, resource, action)`:
|
|
867
558
|
|
|
868
559
|
| Case | Policy lines | Request | Outcome |
|
|
869
560
|
|------|--------------|---------|---------|
|
|
870
|
-
|
|
|
871
|
-
|
|
|
872
|
-
|
|
|
873
|
-
|
|
|
874
|
-
|
|
|
875
|
-
|
|
|
876
|
-
|
|
|
561
|
+
| Scoped role (owner in a tenant) | `g, User_u, Role_owner, Merchant_A`<br/>`p, Role_owner, *, Material.find, read, allow` | `(User_u, Merchant_A, Material.find, read)` | allow |
|
|
562
|
+
| Scoped role - isolation | (same as above) | `(User_u, Merchant_B, Material.find, read)` | deny (`g` domain doesn't match) |
|
|
563
|
+
| Multi-tenant role | `g, User_u, Role_owner, Merchant_A`<br/>`g, User_u, Role_owner, Merchant_B`<br/>`p, Role_owner, *, Material.find, read, allow` | `(User_u, Merchant_B, Material.find, read)` | allow (one `g` line per tenant; a single `p` line) |
|
|
564
|
+
| Global role (for example, guest/onboarding) | `g, User_u, Role_guest, *`<br/>`p, Role_guest, *, Organizer.onBoarding, create, allow` | `(User_u, Merchant_anything, Organizer.onBoarding, create)` | allow (wildcard `g` domain) |
|
|
565
|
+
| Direct user permission (scoped) | `p, User_u, Merchant_A, Report.read, read, allow` | `(User_u, Merchant_A, Report.read, read)` | allow (reflexive `g(u,u,dom)` + `keyMatch`) |
|
|
566
|
+
| Direct user permission - isolation | (same as above) | `(User_u, Merchant_B, Report.read, read)` | deny |
|
|
567
|
+
| Deny override | `p, Role_x, *, Secret.read, read, deny`<br/>`p, Role_y, *, Secret.read, read, allow` | any domain where the user has both roles | deny (`!some(p.eft == deny)`) |
|
|
877
568
|
|
|
878
569
|
> [!IMPORTANT]
|
|
879
|
-
> The function is applied as `fn(requestDomain, policyDomain)` - the wildcard belongs on the **stored** side. Store only `*` or exact domain values (never `Merchant_*`) to keep isolation guaranteed.
|
|
880
|
-
|
|
881
|
-
### Misconfiguration is caught early
|
|
570
|
+
> The matching function is applied as `fn(requestDomain, policyDomain)` - the wildcard belongs on the **stored** side. Store only `*` or exact domain values (never `Merchant_*`) to keep tenant isolation guaranteed.
|
|
882
571
|
|
|
883
|
-
If `roleDefinition` is not declared under `[role_definition]` in the model, `configure()` throws
|
|
572
|
+
**Misconfiguration is caught early.** If `roleDefinition` is not declared under `[role_definition]` in the model, `configure()` throws at boot. Without that check, Casbin would silently register the function as a no-op, leaving wildcard domains permanently unmatched - global roles would be silently denied.
|
|
884
573
|
|
|
885
574
|
```typescript
|
|
886
575
|
// model declares `g` only
|
|
887
576
|
domainMatching: { roleDefinition: 'g2', fn: CasbinDomainMatchingFunctions.KEY_MATCH };
|
|
888
|
-
//
|
|
889
|
-
//
|
|
577
|
+
// throws: Role definition "g2" is not declared in the Casbin model. Declare it under
|
|
578
|
+
// [role_definition] (e.g. `g = _, _, _`) before enabling domainMatching.
|
|
890
579
|
```
|
|
891
580
|
|
|
892
|
-
## See
|
|
581
|
+
## See also
|
|
893
582
|
|
|
894
|
-
- [Setup & Configuration](./)
|
|
895
|
-
- [API Reference](./api)
|
|
896
|
-
- [Error Reference](./errors)
|
|
583
|
+
- [Setup & Configuration](./) - binding keys, options interfaces, and initial setup
|
|
584
|
+
- [API Reference](./api) - architecture, enforcer internals, provider, registry, and adapters
|
|
585
|
+
- [Error Reference](./errors) - error messages and troubleshooting
|
|
586
|
+
- [Persistent Models](/guides/core-concepts/persistent/models#authorization-settings) - `@model` authorization settings
|
|
587
|
+
- [gRPC Controllers](/guides/core-concepts/grpc-controllers) - full gRPC decorator/route surface
|