@venizia/ignis-docs 0.0.8-1 → 0.0.8-2
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/package.json +13 -13
- package/wiki/extensions/components/authorization/api.md +239 -376
- package/wiki/extensions/components/authorization/errors.md +52 -43
- package/wiki/extensions/components/authorization/index.md +127 -65
- package/wiki/extensions/components/authorization/usage.md +198 -98
- package/wiki/guides/migrations/scoped-rbac-migration.md +300 -0
- package/wiki/references/base/filter-system/default-filter.md +6 -3
- package/wiki/references/base/filter-system/fields-order-pagination.md +26 -0
- package/wiki/references/base/models.md +6 -3
|
@@ -107,31 +107,33 @@ classDiagram
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
class CasbinAuthorizationEnforcer {
|
|
110
|
-
-
|
|
111
|
-
-
|
|
110
|
+
-pool: BasePoolHelper~Enforcer~
|
|
111
|
+
-pendingLineFetches: Map
|
|
112
112
|
+configure() void
|
|
113
113
|
+destroy() void
|
|
114
|
-
+buildRules(opts)
|
|
114
|
+
+buildRules(opts) ICasbinRules
|
|
115
115
|
+evaluate(opts) TAuthorizationDecision
|
|
116
|
+
+invalidateUserCache(opts)?
|
|
117
|
+
+rebuildUserCache(opts)?
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
class BaseFilteredAdapter~
|
|
120
|
+
class BaseFilteredAdapter~TFilter~ {
|
|
119
121
|
<<abstract>>
|
|
120
|
-
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
#
|
|
125
|
-
#formatDomain(domain) string
|
|
126
|
-
#toGroupLine(opts) string
|
|
127
|
-
#toPolicyLine(opts) string
|
|
122
|
+
#dataSource: IDataSource
|
|
123
|
+
#connector: TAnyConnector
|
|
124
|
+
+loadFilteredPolicy(model, filter)* void
|
|
125
|
+
+isFiltered() boolean
|
|
126
|
+
#loadLines(opts) void
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
class
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
#
|
|
134
|
-
#
|
|
129
|
+
class ScopedCasbinAdapter {
|
|
130
|
+
#entities: IScopedCasbinEntities
|
|
131
|
+
+loadFilteredPolicy(model, filter) void
|
|
132
|
+
#queryRoleAssignments(opts) lines + roleIds
|
|
133
|
+
#queryMemberships(opts) string[]
|
|
134
|
+
#queryGrants(opts) string[]
|
|
135
|
+
#loadStructuralTrees() string[]
|
|
136
|
+
#expandRoleClosure(opts) IdType[]
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
BaseHelper <|-- AbstractAuthRegistry
|
|
@@ -139,7 +141,7 @@ classDiagram
|
|
|
139
141
|
IAuthorizationEnforcer <|.. CasbinAuthorizationEnforcer
|
|
140
142
|
BaseHelper <|-- CasbinAuthorizationEnforcer
|
|
141
143
|
BaseHelper <|-- BaseFilteredAdapter
|
|
142
|
-
BaseFilteredAdapter <|--
|
|
144
|
+
BaseFilteredAdapter <|-- ScopedCasbinAdapter
|
|
143
145
|
```
|
|
144
146
|
|
|
145
147
|
### Module File Layout
|
|
@@ -147,13 +149,16 @@ classDiagram
|
|
|
147
149
|
```
|
|
148
150
|
auth/authorize/
|
|
149
151
|
├── adapters/
|
|
150
|
-
│ ├── base-filtered.ts # BaseFilteredAdapter (abstract
|
|
151
|
-
│
|
|
152
|
+
│ ├── base-filtered.ts # BaseFilteredAdapter (thin abstract) + ICasbinPolicyFilter
|
|
153
|
+
│ ├── scoped-casbin.adapter.ts # ScopedCasbinAdapter (generic edge-table reader)
|
|
154
|
+
│ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable
|
|
152
155
|
├── common/
|
|
153
156
|
│ ├── constants.ts # Authorization, AuthorizationActions, AuthorizationDecisions,
|
|
157
|
+
│ │ # AuthorizationDomainScopes, AuthorizationPolicyVariants,
|
|
154
158
|
│ │ # AuthorizationRoles, AuthorizationEnforcerTypes,
|
|
155
159
|
│ │ # CasbinEnforcerCachedDrivers, CasbinEnforcerModelDrivers,
|
|
156
160
|
│ │ # CasbinRuleVariants
|
|
161
|
+
│ ├── object-match.ts # objectMatch resource-hierarchy matcher
|
|
157
162
|
│ ├── keys.ts # AuthorizeBindingKeys
|
|
158
163
|
│ ├── types.ts # IAuthorizeOptions, IAuthorizationEnforcer,
|
|
159
164
|
│ │ # IAuthorizationSpec, ICasbinEnforcerOptions, etc.
|
|
@@ -161,14 +166,13 @@ auth/authorize/
|
|
|
161
166
|
├── enforcers/
|
|
162
167
|
│ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
|
|
163
168
|
│ ├── enforcer-registry.ts # AuthorizationEnforcerRegistry (singleton)
|
|
169
|
+
│ ├── models/
|
|
170
|
+
│ │ ├── rbac-domain.model.ts # CASBIN_RBAC_DOMAIN_SCOPED_MODEL (scoped model string)
|
|
171
|
+
│ │ └── index.ts
|
|
164
172
|
│ └── index.ts # Barrel export
|
|
165
173
|
├── middlewares/
|
|
166
174
|
│ └── authorize.middleware.ts # authorize() standalone function
|
|
167
175
|
├── models/
|
|
168
|
-
│ ├── abilities/
|
|
169
|
-
│ │ ├── string-action.model.ts # StringAuthorizationAction
|
|
170
|
-
│ │ ├── string-resource.model.ts # StringAuthorizationResource
|
|
171
|
-
│ │ └── index.ts
|
|
172
176
|
│ ├── authorization-role.model.ts # AuthorizationRole
|
|
173
177
|
│ └── index.ts
|
|
174
178
|
├── providers/
|
|
@@ -197,8 +201,7 @@ auth/authorize/
|
|
|
197
201
|
| **Rules caching** | Built rules cached on Hono context per-request -- avoids rebuilding for multi-spec routes |
|
|
198
202
|
| **Registry singleton** | Mirrors `AuthenticationStrategyRegistry` pattern -- consistent with the codebase |
|
|
199
203
|
| **Abstract base** | `AbstractAuthRegistry<T>` shared between authentication and authorization registries |
|
|
200
|
-
| **Filtered adapter pattern** | `BaseFilteredAdapter`
|
|
201
|
-
| **IAuthorizationComparable** | Generic comparison interface for custom action/resource types beyond plain strings |
|
|
204
|
+
| **Filtered adapter pattern** | `BaseFilteredAdapter` is a thin read-only base; subclasses implement `loadFilteredPolicy` for custom query backends |
|
|
202
205
|
| **No-enforcer fallback** | When no enforcers are registered, the middleware skips authorization and calls `next()` instead of throwing -- prevents hard failures during development or gradual rollout |
|
|
203
206
|
|
|
204
207
|
## Component Lifecycle
|
|
@@ -408,8 +411,8 @@ interface IAuthorizationEnforcer<
|
|
|
408
411
|
| Parameter | Default | Description |
|
|
409
412
|
|-----------|---------|-------------|
|
|
410
413
|
| `E` | `Env` | Hono `Env` type for typed context access |
|
|
411
|
-
| `TAction` | `string` | Action type
|
|
412
|
-
| `TResource` | `string` | Resource type
|
|
414
|
+
| `TAction` | `string` | Action type (string) |
|
|
415
|
+
| `TResource` | `string` | Resource type (string) |
|
|
413
416
|
| `TRules` | `unknown` | Rules type produced by `buildRules` and consumed by `evaluate` |
|
|
414
417
|
| `TBuildRulesReturn` | `ValueOrPromise<TRules>` | Return type of `buildRules` |
|
|
415
418
|
| `TEvaluateReturn` | `ValueOrPromise<TAuthorizationDecision>` | Return type of `evaluate` |
|
|
@@ -418,7 +421,7 @@ interface IAuthorizationEnforcer<
|
|
|
418
421
|
|
|
419
422
|
| Enforcer | TRules | Description |
|
|
420
423
|
|----------|--------|-------------|
|
|
421
|
-
| `CasbinAuthorizationEnforcer` | `
|
|
424
|
+
| `CasbinAuthorizationEnforcer` | `ICasbinRules` | `{ user, lines }` — the user plus their resolved Casbin policy lines (loaded into a pooled enforcer at evaluate time) |
|
|
422
425
|
| Custom | Any type | Your custom rules structure |
|
|
423
426
|
|
|
424
427
|
### Method Contracts
|
|
@@ -438,6 +441,8 @@ interface IAuthorizationRequest<TAction = string, TResource = string> {
|
|
|
438
441
|
action: TAction;
|
|
439
442
|
resource: TResource;
|
|
440
443
|
conditions?: TAuthorizationConditions;
|
|
444
|
+
/** Resolved domain scope: `"<DomainType>_<id>"` (e.g. `"Merchant_7"`) or the `"SYSTEM_WIDE"` sentinel. */
|
|
445
|
+
domain?: string;
|
|
441
446
|
}
|
|
442
447
|
```
|
|
443
448
|
|
|
@@ -447,87 +452,6 @@ interface IAuthorizationRequest<TAction = string, TResource = string> {
|
|
|
447
452
|
| `resource` | `TResource` | Resource being accessed (e.g., `'Article'`) |
|
|
448
453
|
| `conditions` | `TAuthorizationConditions` | Optional key-value conditions for ABAC |
|
|
449
454
|
|
|
450
|
-
## IAuthorizationComparable Interface
|
|
451
|
-
|
|
452
|
-
Generic comparison interface for custom action and resource types. Allows enforcers to work with objects that define their own comparison logic rather than plain strings.
|
|
453
|
-
|
|
454
|
-
```typescript
|
|
455
|
-
interface IAuthorizationComparable<TElement = string, TCompareResult = number> {
|
|
456
|
-
value: TElement;
|
|
457
|
-
compare(other: TElement): TCompareResult;
|
|
458
|
-
isEqual(other: TElement): boolean;
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
| Member | Type | Description |
|
|
463
|
-
|--------|------|-------------|
|
|
464
|
-
| `value` | `TElement` | The underlying value |
|
|
465
|
-
| `compare(other)` | `TCompareResult` | Compare with another value. Convention: `0` means equal. |
|
|
466
|
-
| `isEqual(other)` | `boolean` | Convenience check -- typically `compare(other) === 0` |
|
|
467
|
-
|
|
468
|
-
The `CasbinAuthorizationEnforcer` constrains its `TAction` and `TResource` generics to `string | IAuthorizationComparable`, allowing either plain strings or comparable objects.
|
|
469
|
-
|
|
470
|
-
## StringAuthorizationAction
|
|
471
|
-
|
|
472
|
-
`IAuthorizationComparable` implementation for string-based actions with wildcard support.
|
|
473
|
-
|
|
474
|
-
```typescript
|
|
475
|
-
class StringAuthorizationAction implements IAuthorizationComparable<string> {
|
|
476
|
-
static readonly WILDCARD = '*';
|
|
477
|
-
|
|
478
|
-
readonly value: string;
|
|
479
|
-
|
|
480
|
-
static build(opts: { value: string }): StringAuthorizationAction;
|
|
481
|
-
constructor(opts: { value: string });
|
|
482
|
-
|
|
483
|
-
compare(other: string): number;
|
|
484
|
-
isEqual(other: string): boolean;
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### Comparison Logic
|
|
489
|
-
|
|
490
|
-
- If `this.value === '*'` (WILDCARD), `compare()` returns `0` (matches everything)
|
|
491
|
-
- Otherwise, `this.value.localeCompare(other)` is used
|
|
492
|
-
|
|
493
|
-
```typescript
|
|
494
|
-
import { StringAuthorizationAction } from '@venizia/ignis';
|
|
495
|
-
|
|
496
|
-
const wildcard = StringAuthorizationAction.build({ value: '*' });
|
|
497
|
-
wildcard.isEqual('read'); // true — wildcard matches all
|
|
498
|
-
wildcard.isEqual('delete'); // true — wildcard matches all
|
|
499
|
-
|
|
500
|
-
const read = StringAuthorizationAction.build({ value: 'read' });
|
|
501
|
-
read.isEqual('read'); // true
|
|
502
|
-
read.isEqual('create'); // false
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
## StringAuthorizationResource
|
|
506
|
-
|
|
507
|
-
`IAuthorizationComparable` implementation for string-based resources using `localeCompare`.
|
|
508
|
-
|
|
509
|
-
```typescript
|
|
510
|
-
class StringAuthorizationResource implements IAuthorizationComparable<string> {
|
|
511
|
-
readonly value: string;
|
|
512
|
-
|
|
513
|
-
static build(opts: { value: string }): StringAuthorizationResource;
|
|
514
|
-
constructor(opts: { value: string });
|
|
515
|
-
|
|
516
|
-
compare(other: string): number; // this.value.localeCompare(other)
|
|
517
|
-
isEqual(other: string): boolean; // compare(other) === 0
|
|
518
|
-
}
|
|
519
|
-
```
|
|
520
|
-
|
|
521
|
-
Unlike `StringAuthorizationAction`, this class has no wildcard support -- comparison is always via `localeCompare`.
|
|
522
|
-
|
|
523
|
-
```typescript
|
|
524
|
-
import { StringAuthorizationResource } from '@venizia/ignis';
|
|
525
|
-
|
|
526
|
-
const article = StringAuthorizationResource.build({ value: 'Article' });
|
|
527
|
-
article.isEqual('Article'); // true
|
|
528
|
-
article.isEqual('User'); // false
|
|
529
|
-
```
|
|
530
|
-
|
|
531
455
|
## Casbin Enforcer
|
|
532
456
|
|
|
533
457
|
`CasbinAuthorizationEnforcer` wraps the `casbin` library (optional peer dependency).
|
|
@@ -537,17 +461,19 @@ article.isEqual('User'); // false
|
|
|
537
461
|
```typescript
|
|
538
462
|
class CasbinAuthorizationEnforcer<
|
|
539
463
|
E extends Env = Env,
|
|
540
|
-
TAction extends string
|
|
541
|
-
TResource extends string
|
|
464
|
+
TAction extends string = string,
|
|
465
|
+
TResource extends string = string,
|
|
542
466
|
>
|
|
543
467
|
extends BaseHelper
|
|
544
|
-
implements IAuthorizationEnforcer<E, TAction, TResource,
|
|
468
|
+
implements IAuthorizationEnforcer<E, TAction, TResource, ICasbinRules>
|
|
545
469
|
{
|
|
546
470
|
name = 'CasbinAuthorizationEnforcer';
|
|
547
471
|
|
|
548
472
|
private readonly MIN_EXPIRES_IN = 10_000;
|
|
549
|
-
private
|
|
550
|
-
private
|
|
473
|
+
private pool: TNullable<BasePoolHelper<CasbinEnforcerType>>; // per-request enforcers
|
|
474
|
+
private helper: TNullable<typeof CasbinHelper>; // casbin.Helper (loadPolicyLine)
|
|
475
|
+
private readonly pendingLineFetches = new Map<string, Promise<string[]>>(); // single-flight
|
|
476
|
+
private resolvedPayloadFn: TNullable<TNormalizePayloadFn>; // memoized in configure()
|
|
551
477
|
|
|
552
478
|
constructor(
|
|
553
479
|
@inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') })
|
|
@@ -559,382 +485,312 @@ class CasbinAuthorizationEnforcer<
|
|
|
559
485
|
destroy(): void;
|
|
560
486
|
|
|
561
487
|
// IAuthorizationEnforcer
|
|
562
|
-
async buildRules(opts: { user; context }): Promise<
|
|
488
|
+
async buildRules(opts: { user; context }): Promise<ICasbinRules>; // { user, lines }
|
|
563
489
|
async evaluate(opts: { rules; request; context }): Promise<TAuthorizationDecision>;
|
|
564
490
|
|
|
491
|
+
// Optional cache management (Redis only)
|
|
492
|
+
async invalidateUserCache(opts: { user }): Promise<{ invalidatedKeys: number }>;
|
|
493
|
+
async rebuildUserCache(opts: { user }): Promise<{ cacheKey: string; lineCount: number }>;
|
|
494
|
+
|
|
565
495
|
// Protected internals
|
|
566
|
-
protected async
|
|
496
|
+
protected async registerMatchers(opts: { enforcer; casbin }): Promise<void>;
|
|
497
|
+
protected assertMatcherCompilesSync(opts: { enforcer }): void;
|
|
567
498
|
protected resolveModel(opts): Model;
|
|
568
499
|
protected validateExpiresIn(opts: { expiresIn: number }): void;
|
|
569
|
-
protected async
|
|
570
|
-
protected async
|
|
571
|
-
protected async
|
|
572
|
-
protected async loadPolicyLinesIntoModel(opts: { lines
|
|
500
|
+
protected async fetchLinesWithRedisCache(opts: { user; cached }): Promise<string[]>;
|
|
501
|
+
protected async extractUserLines(opts: { user }): Promise<string[]>; // throwaway enforcer + adapter
|
|
502
|
+
protected async extractLinesFrom(enforcer): Promise<string[]>;
|
|
503
|
+
protected async loadPolicyLinesIntoModel(opts: { enforcer; lines }): Promise<void>;
|
|
504
|
+
protected enforceWithExplain(opts: { enforcer; vals: string[] }): boolean;
|
|
573
505
|
}
|
|
574
506
|
```
|
|
575
507
|
|
|
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
|
+
|
|
576
512
|
### Constructor
|
|
577
513
|
|
|
578
|
-
Injects `ICasbinEnforcerOptions` from the DI container using the binding key `AuthorizeBindingKeys.enforcerOptions('casbin')
|
|
514
|
+
Injects `ICasbinEnforcerOptions` from the DI container using the binding key `AuthorizeBindingKeys.enforcerOptions('casbin')`.
|
|
579
515
|
|
|
580
516
|
### configure()
|
|
581
517
|
|
|
582
518
|
Called once by the registry on first use. Performs:
|
|
583
519
|
|
|
584
|
-
1. Dynamically imports `casbin`
|
|
585
|
-
2. Validates `options.model`
|
|
586
|
-
3.
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
- `cached.use: false` -> `casbin.newEnforcer(model, adapter)`
|
|
591
|
-
- `cached.driver: 'in-memory'` -> `casbin.newCachedEnforcer(model, adapter)` + periodic invalidation timer (`setInterval`)
|
|
592
|
-
- `cached.driver: 'redis'` -> `casbin.newEnforcer(model, adapter)` (Redis handles caching externally)
|
|
593
|
-
5. Validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms) for both in-memory and redis cache drivers
|
|
520
|
+
1. Dynamically imports `casbin` — throws `"casbin" is not installed` if missing.
|
|
521
|
+
2. Validates `options.model` — throws `options.model is required.` if missing.
|
|
522
|
+
3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`).
|
|
523
|
+
4. If `cached.use`, validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms).
|
|
524
|
+
5. Builds a **`BasePoolHelper<Enforcer>`** (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (`newEnforcer(model)` — no DB load at warmup), then `registerMatchers()` and `assertMatcherCompilesSync()` run on it.
|
|
525
|
+
6. `await pool.warmup()` — pre-creates the enforcers.
|
|
594
526
|
|
|
595
|
-
|
|
527
|
+
`registerMatchers()` — when `isScoped`, registers `keyMatch` as the domain matching func on `g`, adds `objectMatch` as a function, and registers it as the matching func on the resource relation (`g4`). When `domainMatching` is set (non-scoped), registers the chosen `Util.*Func` on the named role definition. Always finishes with `buildRoleLinks()`.
|
|
596
528
|
|
|
597
|
-
|
|
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.
|
|
598
530
|
|
|
599
|
-
|
|
600
|
-
destroy() {
|
|
601
|
-
if (!this.inMemoryInvalidationTimer) {
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
clearInterval(this.inMemoryInvalidationTimer);
|
|
605
|
-
this.inMemoryInvalidationTimer = null;
|
|
606
|
-
}
|
|
607
|
-
```
|
|
531
|
+
### destroy()
|
|
608
532
|
|
|
609
|
-
|
|
533
|
+
`this.pool?.destroy()` — drains and disposes the pooled enforcers.
|
|
610
534
|
|
|
611
535
|
### buildRules()
|
|
612
536
|
|
|
613
|
-
|
|
537
|
+
Returns `ICasbinRules` = `{ user, lines }`. The `lines` are the user's complete Casbin policy lines.
|
|
614
538
|
|
|
615
539
|
```mermaid
|
|
616
540
|
flowchart TD
|
|
617
541
|
Start([buildRules]) --> Check{cached.use?}
|
|
618
|
-
Check -->|false|
|
|
619
|
-
Check -->|true|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
542
|
+
Check -->|false| Extract["extractUserLines(user)"]
|
|
543
|
+
Check -->|true| Redis["fetchLinesWithRedisCache(user, cached)"]
|
|
544
|
+
Redis --> Hit{Redis hit?}
|
|
545
|
+
Hit -->|Yes| Lines([lines])
|
|
546
|
+
Hit -->|No| SF["single-flight → extractUserLines + SET PX"]
|
|
547
|
+
SF --> Lines
|
|
548
|
+
Extract --> Lines
|
|
549
|
+
Lines --> Return(["return { user, lines }"])
|
|
626
550
|
```
|
|
627
551
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
552
|
+
- **`extractUserLines(user)`** builds a fresh, **isolated** enforcer *with the adapter*, calls
|
|
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.
|
|
635
559
|
|
|
636
560
|
### evaluate()
|
|
637
561
|
|
|
638
|
-
|
|
562
|
+
Borrows an enforcer from the pool and evaluates **atomically** inside `pool.use`:
|
|
639
563
|
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
564
|
+
```mermaid
|
|
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])
|
|
649
574
|
```
|
|
650
575
|
|
|
651
|
-
|
|
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.
|
|
652
579
|
|
|
653
|
-
###
|
|
580
|
+
### invalidateUserCache() / rebuildUserCache()
|
|
654
581
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
| `resolveModel` | `{ casbin, model }` | `Model` | Resolves casbin model from file or text via driver discriminant |
|
|
659
|
-
| `validateExpiresIn` | `{ expiresIn }` | `void` | Throws if `expiresIn < MIN_EXPIRES_IN` |
|
|
660
|
-
| `loadPoliciesFromAdapter` | `{ user }` | `void` | Calls `enforcer.loadFilteredPolicy({ principalType, principalValue })` |
|
|
661
|
-
| `loadPoliciesWithRedisCache` | `{ user, cached }` | `void` | Redis cache flow: check cache -> hit: load lines; miss: load from adapter + cache |
|
|
662
|
-
| `extractPolicyLines` | -- | `string[]` | Extracts `p` and `g` lines from enforcer model via `getPolicy()` and `getGroupingPolicy()` |
|
|
663
|
-
| `loadPolicyLinesIntoModel` | `{ lines }` | `void` | Clears model, loads lines via `Helper.loadPolicyLine()`, rebuilds role links |
|
|
582
|
+
Redis-only (throw if caching is disabled). `invalidateUserCache` deletes the user's shared Redis key
|
|
583
|
+
(next request rebuilds lazily). `rebuildUserCache` deletes then immediately re-extracts (on a throwaway
|
|
584
|
+
enforcer) and re-caches. Because the key is shared in Redis, a single call is correct across instances.
|
|
664
585
|
|
|
665
|
-
###
|
|
586
|
+
### Protected Methods
|
|
666
587
|
|
|
667
|
-
|
|
588
|
+
| Method | Output | Description |
|
|
589
|
+
|--------|--------|-------------|
|
|
590
|
+
| `registerMatchers` | `void` | Registers domain/resource matching funcs (+ `buildRoleLinks`); scoped vs `domainMatching` |
|
|
591
|
+
| `assertMatcherCompilesSync` | `void` | Boot-time matcher smoke test (forces lazy compile) |
|
|
592
|
+
| `resolveModel` | `Model` | Resolves casbin model from `file` or `text` driver |
|
|
593
|
+
| `validateExpiresIn` | `void` | Throws if `expiresIn < MIN_EXPIRES_IN` |
|
|
594
|
+
| `fetchLinesWithRedisCache` | `string[]` | Redis read → single-flight extract+write on miss |
|
|
595
|
+
| `extractUserLines` | `string[]` | Throwaway enforcer + adapter `loadFilteredPolicy` → `extractLinesFrom` |
|
|
596
|
+
| `extractLinesFrom` | `string[]` | Serializes every p-type and g-type rule into lines |
|
|
597
|
+
| `loadPolicyLinesIntoModel` | `void` | `clearPolicy` + `loadPolicyLine` per line + `buildRoleLinks` |
|
|
598
|
+
| `enforceWithExplain` | `boolean` | `enforceExSync`; logs the deciding rule on DENY |
|
|
668
599
|
|
|
669
|
-
|
|
670
|
-
flowchart TD
|
|
671
|
-
Start([buildRules with Redis]) --> Key["keyFn({ user }) → cacheKey"]
|
|
672
|
-
Key --> Valid{cacheKey truthy?}
|
|
673
|
-
Valid -->|No| E400[/400 Invalid cachedKey/]
|
|
674
|
-
Valid -->|Yes| Get["Redis GET cacheKey"]
|
|
675
|
-
Get --> Hit{Cache hit?}
|
|
676
|
-
Hit -->|Yes| Parse["JSON.parse(cachedData)"]
|
|
677
|
-
Parse --> LoadModel["loadPolicyLinesIntoModel(lines)"]
|
|
678
|
-
LoadModel --> LogHit["Log: Loaded CACHED Policies"]
|
|
679
|
-
Hit -->|No| Adapter["loadPoliciesFromAdapter(user)"]
|
|
680
|
-
Adapter --> Extract["extractPolicyLines()"]
|
|
681
|
-
Extract --> Set["Redis SET cacheKey, lines, PX expiresIn"]
|
|
682
|
-
Set --> LogMiss["Log: Loaded ADAPTER + CACHED Policies"]
|
|
683
|
-
LogHit --> Return([return user])
|
|
684
|
-
LogMiss --> Return
|
|
685
|
-
```
|
|
686
|
-
|
|
687
|
-
#### extractPolicyLines()
|
|
600
|
+
#### extractLinesFrom()
|
|
688
601
|
|
|
689
|
-
|
|
602
|
+
Serializes **all** policy + grouping rule types (not just `p`/`g`) so the cached payload is complete
|
|
603
|
+
for the scoped model (`g2`…`g5`):
|
|
690
604
|
|
|
691
605
|
```typescript
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
const ps = pRules.map(r => [CasbinRuleVariants.P, ...r].join(', '));
|
|
695
|
-
|
|
696
|
-
// Group rules: ["g, user_123, role_admin, org_1", ...]
|
|
697
|
-
const gRules = await this.enforcer.getGroupingPolicy();
|
|
698
|
-
const gs = gRules.map(r => [CasbinRuleVariants.G, ...r].join(', '));
|
|
606
|
+
const model = enforcer.getModel();
|
|
607
|
+
const lines: string[] = [];
|
|
699
608
|
|
|
700
|
-
|
|
609
|
+
for (const ptype of model.model.get(CasbinRuleVariants.P)?.keys() ?? []) {
|
|
610
|
+
for (const rule of await enforcer.getNamedPolicy(ptype)) lines.push([ptype, ...rule].join(', '));
|
|
611
|
+
}
|
|
612
|
+
for (const gtype of model.model.get(CasbinRuleVariants.G)?.keys() ?? []) {
|
|
613
|
+
for (const rule of await enforcer.getNamedGroupingPolicy(gtype)) lines.push([gtype, ...rule].join(', '));
|
|
614
|
+
}
|
|
615
|
+
return lines;
|
|
701
616
|
```
|
|
702
617
|
|
|
703
618
|
#### loadPolicyLinesIntoModel()
|
|
704
619
|
|
|
705
|
-
|
|
620
|
+
Atomically resets a borrowed enforcer's model to exactly `lines`:
|
|
706
621
|
|
|
707
622
|
```typescript
|
|
708
|
-
const
|
|
709
|
-
const model = this.enforcer.getModel();
|
|
623
|
+
const model = opts.enforcer.getModel();
|
|
710
624
|
model.clearPolicy();
|
|
711
|
-
|
|
712
625
|
for (const line of opts.lines) {
|
|
713
|
-
|
|
626
|
+
this.helper.loadPolicyLine(line, model);
|
|
714
627
|
}
|
|
715
|
-
|
|
716
|
-
await this.enforcer.buildRoleLinks();
|
|
628
|
+
await opts.enforcer.buildRoleLinks();
|
|
717
629
|
```
|
|
718
630
|
|
|
719
631
|
## BaseFilteredAdapter
|
|
720
632
|
|
|
721
|
-
|
|
633
|
+
Thin read-only base for casbin `FilteredAdapter`s backed by a datasource. It owns the boilerplate
|
|
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.
|
|
722
637
|
|
|
723
638
|
### Class
|
|
724
639
|
|
|
725
640
|
```typescript
|
|
726
|
-
abstract class BaseFilteredAdapter<
|
|
727
|
-
TEntities extends IBaseFilteredAdapterEntities = IBaseFilteredAdapterEntities,
|
|
728
|
-
TFilter = ICasbinPolicyFilter,
|
|
729
|
-
TPolicyRow extends TBasePolicyRow = TBasePolicyRow,
|
|
730
|
-
>
|
|
641
|
+
abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter>
|
|
731
642
|
extends BaseHelper
|
|
732
643
|
implements FilteredAdapter
|
|
733
644
|
{
|
|
734
|
-
protected readonly
|
|
645
|
+
protected readonly dataSource: IDataSource;
|
|
646
|
+
protected get connector(): TAnyConnector;
|
|
647
|
+
|
|
648
|
+
constructor(opts: { scope: string; dataSource: IDataSource });
|
|
735
649
|
|
|
736
|
-
|
|
650
|
+
// Subclasses implement ONLY this:
|
|
651
|
+
abstract loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
|
|
737
652
|
|
|
738
|
-
// FilteredAdapter — public API
|
|
739
|
-
async loadPolicy(): Promise<void>; // no-op
|
|
740
|
-
async loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
|
|
741
653
|
isFiltered(): boolean; // always true
|
|
742
654
|
|
|
743
655
|
// No-op write methods (read-only adapter)
|
|
744
|
-
async
|
|
745
|
-
async
|
|
746
|
-
async
|
|
747
|
-
async
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
protected
|
|
752
|
-
protected abstract buildRolePolicies(opts): ValueOrPromise<string[]>;
|
|
753
|
-
|
|
754
|
-
// Shared formatters
|
|
755
|
-
protected formatDomain(domain: string | null): string | null;
|
|
756
|
-
protected toGroupLine(opts): string;
|
|
757
|
-
protected toPolicyLine(opts): string | null;
|
|
656
|
+
async loadPolicy(): Promise<void>;
|
|
657
|
+
async savePolicy(): Promise<boolean>; // returns true
|
|
658
|
+
async addPolicy(): Promise<void>;
|
|
659
|
+
async removePolicy(): Promise<void>;
|
|
660
|
+
async removeFilteredPolicy(): Promise<void>;
|
|
661
|
+
|
|
662
|
+
// Helper: parse + load casbin lines into a model.
|
|
663
|
+
protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void>;
|
|
758
664
|
}
|
|
759
665
|
```
|
|
760
666
|
|
|
761
667
|
### Generic Parameters
|
|
762
668
|
|
|
763
|
-
| Parameter |
|
|
764
|
-
|
|
765
|
-
| `
|
|
766
|
-
| `TFilter` | -- | `ICasbinPolicyFilter` | Filter shape passed to `loadFilteredPolicy` |
|
|
767
|
-
| `TPolicyRow` | `TBasePolicyRow` | `TBasePolicyRow` | Policy row shape consumed by `toPolicyLine` |
|
|
768
|
-
|
|
769
|
-
### IBaseFilteredAdapterEntities
|
|
770
|
-
|
|
771
|
-
```typescript
|
|
772
|
-
interface IBaseFilteredAdapterEntities {
|
|
773
|
-
role: { principalType: string };
|
|
774
|
-
domain?: { principalType: string };
|
|
775
|
-
}
|
|
776
|
-
```
|
|
669
|
+
| Parameter | Default | Description |
|
|
670
|
+
|-----------|---------|-------------|
|
|
671
|
+
| `TFilter` | `ICasbinPolicyFilter` | Filter shape passed to `loadFilteredPolicy`. Subclasses may narrow it (e.g. `IScopedCasbinPolicyFilter`) |
|
|
777
672
|
|
|
778
673
|
### ICasbinPolicyFilter
|
|
779
674
|
|
|
675
|
+
The default filter: which principal's policies to load. Subclasses may narrow it.
|
|
676
|
+
|
|
780
677
|
```typescript
|
|
781
678
|
interface ICasbinPolicyFilter {
|
|
782
|
-
|
|
783
|
-
principalValue: string | number;
|
|
679
|
+
principal: { type: string; id: IdType };
|
|
784
680
|
}
|
|
785
681
|
```
|
|
786
682
|
|
|
787
|
-
###
|
|
683
|
+
### loadLines()
|
|
788
684
|
|
|
789
|
-
|
|
685
|
+
The base's only orchestration helper — subclasses call it from `loadFilteredPolicy` after assembling
|
|
686
|
+
their casbin lines:
|
|
790
687
|
|
|
791
688
|
```typescript
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
effect: string | null;
|
|
799
|
-
domain: string | null;
|
|
800
|
-
};
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
### loadFilteredPolicy()
|
|
804
|
-
|
|
805
|
-
Orchestrates three query phases using `casbin.Helper.loadPolicyLine()`:
|
|
806
|
-
|
|
807
|
-
```mermaid
|
|
808
|
-
flowchart LR
|
|
809
|
-
Start([loadFilteredPolicy]) --> D[buildDirectPolicies]
|
|
810
|
-
D -->|p lines| Load1[Load into model]
|
|
811
|
-
Load1 --> G[buildGroupPolicies]
|
|
812
|
-
G -->|g lines + roleIds| Load2[Load into model]
|
|
813
|
-
Load2 --> Check{roleIds empty?}
|
|
814
|
-
Check -->|No| R[buildRolePolicies]
|
|
815
|
-
R -->|p lines| Load3[Load into model]
|
|
816
|
-
Check -->|Yes| Done([Done])
|
|
817
|
-
Load3 --> Done
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
```
|
|
821
|
-
1. buildDirectPolicies({ filter, rolePrincipal })
|
|
822
|
-
→ Direct permissions assigned to the principal → casbin `p` lines
|
|
823
|
-
|
|
824
|
-
2. buildGroupPolicies({ filter })
|
|
825
|
-
→ Role assignments → casbin `g` lines + roleIds
|
|
826
|
-
|
|
827
|
-
3. buildRolePolicies({ roleIds, rolePrincipal })
|
|
828
|
-
→ Permissions inherited through roles → casbin `p` lines
|
|
829
|
-
(only if roleIds is non-empty)
|
|
689
|
+
protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void> {
|
|
690
|
+
const { Helper } = await import('casbin');
|
|
691
|
+
for (const line of opts.lines) {
|
|
692
|
+
Helper.loadPolicyLine(line, opts.model);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
830
695
|
```
|
|
831
696
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
| Hook | Input | Output | Description |
|
|
835
|
-
|------|-------|--------|-------------|
|
|
836
|
-
| `buildDirectPolicies` | `{ filter: TFilter, rolePrincipal: string }` | `string[]` | Direct permission `p` lines for the user |
|
|
837
|
-
| `buildGroupPolicies` | `{ filter: TFilter }` | `{ lines: string[], roleIds: (string \| number)[] }` | Role assignment `g` lines + role IDs |
|
|
838
|
-
| `buildRolePolicies` | `{ roleIds: (string \| number)[], rolePrincipal: string }` | `string[]` | Inherited permission `p` lines via roles |
|
|
697
|
+
There are no template-method query hooks or shared line formatters on the base — a subclass owns its
|
|
698
|
+
own queries and line construction (see `ScopedCasbinAdapter` below for the reference implementation).
|
|
839
699
|
|
|
840
|
-
|
|
700
|
+
## ScopedCasbinAdapter
|
|
841
701
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
| `toGroupLine(opts)` | `{ subject, role, domain }` | `string` | Formats: <code v-pre>g, <subject>, <role>[, <domain>]</code> |
|
|
846
|
-
| `toPolicyLine(opts)` | `{ row: TPolicyRow }` | `string \| null` | Formats: <code v-pre>p, <subject>, [<domain>,] <resource>, <action>, <effect></code>. Returns `null` if row has no action. Effect defaults to `'allow'`. |
|
|
847
|
-
|
|
848
|
-
## DrizzleCasbinAdapter
|
|
849
|
-
|
|
850
|
-
Concrete read-only `FilteredAdapter` using raw SQL queries via Drizzle's `connector.execute()`.
|
|
702
|
+
The generic, read-only `FilteredAdapter` for the scoped RBAC model. It reads **one principal's edges**
|
|
703
|
+
plus the **shared structural hierarchy** from a single `PolicyDefinition` edge table (joined to
|
|
704
|
+
`Permission` for codes) and emits casbin lines. No subclassing — configure it with `IScopedCasbinEntities`.
|
|
851
705
|
|
|
852
706
|
### Class
|
|
853
707
|
|
|
854
708
|
```typescript
|
|
855
|
-
class
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
constructor(opts:
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
709
|
+
class ScopedCasbinAdapter extends BaseFilteredAdapter<IScopedCasbinPolicyFilter> {
|
|
710
|
+
protected readonly entities: IScopedCasbinEntities;
|
|
711
|
+
|
|
712
|
+
constructor(opts: { dataSource: IDataSource; entities: IScopedCasbinEntities });
|
|
713
|
+
|
|
714
|
+
async loadFilteredPolicy(model: Model, filter: IScopedCasbinPolicyFilter): Promise<void>;
|
|
715
|
+
|
|
716
|
+
// Per-principal queries
|
|
717
|
+
protected queryRoleAssignments(opts): Promise<{ lines: string[]; roleIds: IdType[] }>; // → g
|
|
718
|
+
protected queryMemberships(opts): Promise<string[]>; // → g2
|
|
719
|
+
protected queryGrants(opts): Promise<string[]>; // → p
|
|
720
|
+
// Shared hierarchy
|
|
721
|
+
protected loadStructuralTrees(): Promise<string[]>; // role(g)/domain(g3)/resource(g4)/action(g5)
|
|
722
|
+
protected queryRoleInherits(): Promise<string[]>; // → g
|
|
723
|
+
protected queryDomainInherits(): Promise<string[]>; // → g3
|
|
724
|
+
protected queryResourceInherits(): Promise<string[]>; // → g4
|
|
725
|
+
protected queryActionInherits(): Promise<string[]>; // → g5
|
|
726
|
+
// Role closure (BFS over role_inherits)
|
|
727
|
+
protected expandRoleClosure(opts: { role: { ids: IdType[]; edges: string[] } }): IdType[];
|
|
863
728
|
}
|
|
864
729
|
```
|
|
865
730
|
|
|
866
|
-
###
|
|
731
|
+
### IScopedCasbinEntities
|
|
867
732
|
|
|
868
733
|
```typescript
|
|
869
|
-
interface
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
policyDefinition:
|
|
873
|
-
|
|
734
|
+
interface IScopedCasbinTable { tableName: string; schemaName?: string; }
|
|
735
|
+
|
|
736
|
+
interface IScopedCasbinEntities {
|
|
737
|
+
policyDefinition: IScopedCasbinTable; // the single edge table
|
|
738
|
+
permission: IScopedCasbinTable; // permission catalog (id, code, ...)
|
|
739
|
+
principals: { user: string; role: string }; // casbin name prefixes
|
|
740
|
+
domainTypes: string[]; // e.g. ['Merchant', 'Organizer']
|
|
741
|
+
softDelete?: { use: false } | { use: true; columnName: string };
|
|
874
742
|
}
|
|
875
743
|
```
|
|
876
744
|
|
|
877
|
-
###
|
|
745
|
+
### IScopedCasbinPolicyFilter
|
|
878
746
|
|
|
879
747
|
```typescript
|
|
880
|
-
interface
|
|
881
|
-
|
|
882
|
-
entities: IDrizzleCasbinEntities;
|
|
748
|
+
interface IScopedCasbinPolicyFilter {
|
|
749
|
+
principal: { type: string; id: IdType };
|
|
883
750
|
}
|
|
884
751
|
```
|
|
885
752
|
|
|
886
|
-
###
|
|
887
|
-
|
|
888
|
-
All queries use the `sql` template tag from `drizzle-orm` and filter by `variant` using `CasbinRuleVariants.POLICY` or `CasbinRuleVariants.GROUP` constants.
|
|
889
|
-
|
|
890
|
-
**buildDirectPolicies** -- direct permissions assigned to the user:
|
|
891
|
-
```sql
|
|
892
|
-
SELECT pd.variant, p.code, pd.action,
|
|
893
|
-
pd.subject_type AS "subjectType", pd.subject_id AS "subjectId",
|
|
894
|
-
pd.effect, pd.domain
|
|
895
|
-
FROM {policyDefinition.tableName} pd
|
|
896
|
-
INNER JOIN {permission.tableName} p ON pd.target_id = p.id
|
|
897
|
-
WHERE pd.variant = 'policy'
|
|
898
|
-
AND pd.subject_type = :principalType
|
|
899
|
-
AND pd.subject_id = :principalValue
|
|
900
|
-
AND pd.target_type = :permission.principalType
|
|
901
|
-
```
|
|
753
|
+
### loadFilteredPolicy() — two waves
|
|
902
754
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
AND pd.target_type = :role.principalType
|
|
755
|
+
```mermaid
|
|
756
|
+
flowchart TD
|
|
757
|
+
Start([loadFilteredPolicy]) --> W1["Wave 1 (parallel): queryRoleAssignments (g) ·
|
|
758
|
+
queryMemberships (g2) · queryGrants[user] (p) · loadStructuralTrees (g/g3/g4/g5)"]
|
|
759
|
+
W1 --> Closure["expandRoleClosure(assigned roleIds, role_inherits edges)"]
|
|
760
|
+
Closure --> W2["Wave 2: queryGrants[roleClosure] (p)"]
|
|
761
|
+
W2 --> Load["loadLines(model, all lines)"]
|
|
911
762
|
```
|
|
912
763
|
|
|
913
|
-
**
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
764
|
+
1. **Wave 1 (parallel):** the principal's own edges — role assignments (`g`), domain memberships
|
|
765
|
+
(`g2`), direct grants (`p`) — plus the shared structural trees (`role_inherits` → `g`,
|
|
766
|
+
`domain_inherits` → `g3`, `resource_inherits` → `g4`, `action_inherits` → `g5`).
|
|
767
|
+
2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to
|
|
768
|
+
collect the assigned roles + all transitive parents.
|
|
769
|
+
3. **Wave 2:** fetch the grants (`p`) of every role in the closure, so a user inherits the permissions
|
|
770
|
+
of parent roles.
|
|
771
|
+
4. All lines are loaded via `loadLines`.
|
|
772
|
+
|
|
773
|
+
### SQL notes
|
|
774
|
+
|
|
775
|
+
All queries use the `sql` template tag from `drizzle-orm`. Tables are schema-qualified via
|
|
776
|
+
`sql.identifier` (injection-safe); interpolated values (the `variant` discriminator from
|
|
777
|
+
`AuthorizationPolicyVariants.*.action`, ids, types) are bound parameters. The soft-delete clause
|
|
778
|
+
(`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true. `queryGrants`
|
|
779
|
+
short-circuits to `[]` when given no subject ids (no DB round-trip).
|
|
925
780
|
|
|
926
781
|
### Usage Example
|
|
927
782
|
|
|
928
783
|
```typescript
|
|
929
|
-
import {
|
|
784
|
+
import { ScopedCasbinAdapter } from '@venizia/ignis';
|
|
930
785
|
|
|
931
|
-
const adapter = new
|
|
786
|
+
const adapter = new ScopedCasbinAdapter({
|
|
932
787
|
dataSource: myPostgresDataSource,
|
|
933
788
|
entities: {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
789
|
+
policyDefinition: { tableName: 'PolicyDefinition', schemaName: 'identity' },
|
|
790
|
+
permission: { tableName: 'Permission', schemaName: 'identity' },
|
|
791
|
+
principals: { user: 'User', role: 'Role' },
|
|
792
|
+
domainTypes: ['Merchant', 'Organizer'],
|
|
793
|
+
softDelete: { use: true, columnName: 'deleted_at' },
|
|
938
794
|
},
|
|
939
795
|
});
|
|
940
796
|
```
|
|
@@ -993,6 +849,12 @@ if (!registry.hasEnforcers()) → next() // skip if no enforcers registered
|
|
|
993
849
|
const resolvedName = enforcerName ?? registry.getDefaultEnforcerName();
|
|
994
850
|
const enforcer = await registry.resolveEnforcer({ name: resolvedName });
|
|
995
851
|
|
|
852
|
+
// Step 5b: Resolve request domain scope (only when domain scoping is in play)
|
|
853
|
+
if (spec.domain || options?.domainResolver) {
|
|
854
|
+
const domainScope = await resolveRequestDomain({ spec, context, options }); // "<Type>_<id>" | SYSTEM_WIDE
|
|
855
|
+
context.set(Authorization.DOMAIN, domainScope); // the enforcer reads this for request.domain
|
|
856
|
+
}
|
|
857
|
+
|
|
996
858
|
// Step 6: Build/cache rules
|
|
997
859
|
let rules = context.get(Authorization.RULES);
|
|
998
860
|
if (!rules) {
|
|
@@ -1249,6 +1111,7 @@ declare module 'hono' {
|
|
|
1249
1111
|
// Authorization
|
|
1250
1112
|
[Authorization.RULES]: unknown; // 'authorization.rules'
|
|
1251
1113
|
[Authorization.SKIP_AUTHORIZATION]: boolean; // 'authorization.skip'
|
|
1114
|
+
[Authorization.DOMAIN]: string; // 'authorization.domain'
|
|
1252
1115
|
}
|
|
1253
1116
|
}
|
|
1254
1117
|
```
|