@dudousxd/nestjs-authz 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/cerbos/cerbos-decision.provider.d.ts +128 -0
  3. package/dist/cerbos/cerbos-decision.provider.d.ts.map +1 -0
  4. package/dist/cerbos/cerbos-decision.provider.js +134 -0
  5. package/dist/cerbos/cerbos-decision.provider.js.map +1 -0
  6. package/dist/cerbos/index.d.ts +3 -0
  7. package/dist/cerbos/index.d.ts.map +1 -0
  8. package/dist/cerbos/index.js +5 -0
  9. package/dist/cerbos/index.js.map +1 -0
  10. package/dist/cerbos/plan.d.ts +41 -0
  11. package/dist/cerbos/plan.d.ts.map +1 -0
  12. package/dist/cerbos/plan.js +158 -0
  13. package/dist/cerbos/plan.js.map +1 -0
  14. package/dist/decision-provider.d.ts +58 -0
  15. package/dist/decision-provider.d.ts.map +1 -0
  16. package/dist/decision-provider.js +2 -0
  17. package/dist/decision-provider.js.map +1 -0
  18. package/dist/diagnostics.d.ts +1 -1
  19. package/dist/diagnostics.d.ts.map +1 -1
  20. package/dist/diagnostics.js.map +1 -1
  21. package/dist/gate.d.ts +17 -1
  22. package/dist/gate.d.ts.map +1 -1
  23. package/dist/gate.js +83 -13
  24. package/dist/gate.js.map +1 -1
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/index.js.map +1 -1
  29. package/dist/store-kit.d.ts +17 -0
  30. package/dist/store-kit.d.ts.map +1 -1
  31. package/dist/store-kit.js +5 -0
  32. package/dist/store-kit.js.map +1 -1
  33. package/dist/tokens.d.ts +7 -0
  34. package/dist/tokens.d.ts.map +1 -1
  35. package/dist/tokens.js +7 -0
  36. package/dist/tokens.js.map +1 -1
  37. package/dist/types.d.ts +6 -1
  38. package/dist/types.d.ts.map +1 -1
  39. package/package.json +10 -3
  40. package/skills/authz-rbac-seams/SKILL.md +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @dudousxd/nestjs-authz
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#57](https://github.com/DavideCarvalho/nestjs-authz/pull/57) [`6de64a9`](https://github.com/DavideCarvalho/nestjs-authz/commit/6de64a94280187b28f55c4ab5ae5aabb2ef34836) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - External policy engines: a new `DecisionProvider` seam (`DECISION_PROVIDER` token) and a Cerbos adapter at `@dudousxd/nestjs-authz/cerbos`.
8
+
9
+ - `DecisionProvider.decide(user, ability, resource)` can **allow, deny or abstain**. It runs right after the `superAdmin` hook and before the permission provider, the policy `before` hook, policies and gates. `authorize()` surfaces denials with reason `decision-provider`. It is also consulted for anonymous callers.
10
+ - Optional `decideMany` batches `gate.allowsMany` into one engine round-trip. If the batch call fails, each item falls back to its own `decide` call.
11
+ - Optional `planScope(user, Entity, ability)` feeds `gate.scope()` right after `superAdmin`. Returning `undefined` abstains and falls back to the permission provider or policy scope.
12
+ - The `superAdmin` hook now receives the resource as a third argument: the checked resource, or the entity class for `gate.scope()`. This is additive.
13
+ - `@dudousxd/nestjs-authz/cerbos`:
14
+ - `CerbosDecisionProvider` supports per-user/per-tenant clients, batching with de-duplication and chunking, and fails closed by default.
15
+ - `cerbosPlanToScope` maps Cerbos `PlanResources` onto the scope AST: and/or, `not` via De Morgan, eq/ne/lt/gt/le/ge/in, and null checks. Anything else is rejected, which fails closed.
16
+ - Client types are structural, so the package has no runtime dependency on the Cerbos SDKs.
17
+
18
+ - [#58](https://github.com/DavideCarvalho/nestjs-authz/pull/58) [`8b65088`](https://github.com/DavideCarvalho/nestjs-authz/commit/8b6508804216d46384ec3a734a55b9c0ac1e48b6) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Per-source role assignments (manual vs SSO/SCIM/…).
19
+
20
+ Each role assignment now records a `source`, default `'manual'`. Every store adapter gains:
21
+
22
+ - `setUserRoles(user, roleNames, { source?, tenantId? })` replaces only that source's assignments, so an SSO sync never removes a role that was granted by hand. It creates missing roles and runs in a transaction.
23
+ - `assignRole(user, role, { source })` and `removeRole(user, role, { source })`. Without `source`, `removeRole` removes the role from every source.
24
+ - `getRoleAssignments(user, scope?)` returns `{ role, source, tenantId }[]`.
25
+ - `getRolesForUser` and the Gate seams still return distinct role names.
26
+
27
+ Core's `store-kit` exports `DEFAULT_ROLE_SOURCE` and `RoleAssignment`. The adapters now require `@dudousxd/nestjs-authz >= 0.7.0`.
28
+
29
+ **Migration**
30
+
31
+ - **TypeORM:** `ensureAuthzSchema` / `autoCreateSchema` adds `source varchar(64) NOT NULL DEFAULT 'manual'` to `authz_user_role`, so existing rows become manual assignments. It never rewrites an existing primary key. Until you widen the key, a role can come from only one source per user, and a second-source insert is silently ignored. Widen it once:
32
+
33
+ - PostgreSQL: `ALTER TABLE authz_user_role DROP CONSTRAINT <pk_name>, ADD PRIMARY KEY ("userType","userId","roleId","tenantId","source");`
34
+ - MySQL (utf8mb4 needs the shorter key columns): `ALTER TABLE authz_user_role MODIFY roleId varchar(64) NOT NULL, MODIFY source varchar(64) NOT NULL DEFAULT 'manual', DROP PRIMARY KEY, ADD PRIMARY KEY (userType, userId, roleId, tenantId, source);`
35
+ - SQLite: rebuild the table.
36
+
37
+ New tables are created with the 5-column key, and `roleId` is now `varchar(64)` so the key fits MySQL's 3072-byte limit.
38
+
39
+ - **MikroORM:** the column self-heals the same way. The PK does not. See the adapter README for the SQL.
40
+ - **Prisma:** opt-in with `roleSources: true` after adding `source String @default("manual")` to `UserRole` and including it in `@@id`. See the adapter README. Without the option the adapter behaves exactly as before.
41
+ - **Drizzle:** `createAuthzTables()` now includes the column and the widened key, so regenerate with drizzle-kit. `ensureSchema()` adds the column. See the README for the key SQL.
42
+
3
43
  ## 0.6.7
4
44
 
5
45
  ### Patch Changes
@@ -0,0 +1,128 @@
1
+ import type { Type } from '@nestjs/common';
2
+ import type { DecisionProvider, DecisionRequest, DecisionVerdict } from '../decision-provider.js';
3
+ import { type ScopeConstraint } from '../scope.js';
4
+ import type { Resource, User } from '../types.js';
5
+ import { type CerbosFieldMapper, type CerbosPlanLike } from './plan.js';
6
+ /** A Cerbos attribute value (JSON). */
7
+ export type CerbosValue = string | number | boolean | null | CerbosValue[] | {
8
+ [key: string]: CerbosValue;
9
+ };
10
+ /** The principal sent to Cerbos. */
11
+ export interface CerbosPrincipal {
12
+ id: string;
13
+ roles: string[];
14
+ attr?: Record<string, CerbosValue>;
15
+ policyVersion?: string;
16
+ scope?: string;
17
+ }
18
+ /** A resource sent to Cerbos `CheckResources`. */
19
+ export interface CerbosResource {
20
+ kind: string;
21
+ id: string;
22
+ attr?: Record<string, CerbosValue>;
23
+ policyVersion?: string;
24
+ scope?: string;
25
+ }
26
+ /** The resource query sent to Cerbos `PlanResources` (a resource without an id). */
27
+ export type CerbosResourceQuery = Omit<CerbosResource, 'id'>;
28
+ /**
29
+ * The slice of a Cerbos SDK client the adapter uses. `@cerbos/http`'s `HTTP` and `@cerbos/grpc`'s
30
+ * `GRPC` satisfy it structurally, so this package does not depend on either.
31
+ */
32
+ export interface CerbosClientLike {
33
+ checkResources(request: {
34
+ principal: CerbosPrincipal;
35
+ resources: Array<{
36
+ resource: CerbosResource;
37
+ actions: string[];
38
+ }>;
39
+ }): Promise<{
40
+ isAllowed(check: {
41
+ resource: {
42
+ kind: string;
43
+ id: string;
44
+ policyVersion?: string;
45
+ scope?: string;
46
+ };
47
+ action: string;
48
+ }): boolean | undefined;
49
+ }>;
50
+ planResources?(request: {
51
+ principal: CerbosPrincipal;
52
+ resource: CerbosResourceQuery;
53
+ action: string;
54
+ }): Promise<CerbosPlanLike>;
55
+ }
56
+ type Awaitable<T> = T | Promise<T>;
57
+ export interface CerbosDecisionProviderOptions {
58
+ /**
59
+ * The Cerbos client, or a per-user resolver (e.g. per-tenant PDPs). Resolving `undefined`
60
+ * ABSTAINS — the Gate falls back to its own rules (RBAC/policies) for that user.
61
+ */
62
+ client: CerbosClientLike | ((user: User) => Awaitable<CerbosClientLike | undefined>);
63
+ /** Build the Cerbos principal. `undefined` (e.g. anonymous) abstains. */
64
+ principal: (user: User) => Awaitable<CerbosPrincipal | undefined>;
65
+ /**
66
+ * Map a Gate check to a Cerbos resource. `undefined` abstains — use it for abilities/resources
67
+ * Cerbos does not govern (model-less abilities, other resource types).
68
+ */
69
+ resource: (ability: string, resource: Resource | undefined) => CerbosResource | undefined;
70
+ /** Map an ability to a Cerbos action. Default: the ability name itself. */
71
+ action?: (ability: string) => string;
72
+ /**
73
+ * Query-plan support for `gate.scope(Entity, ability)`. `resource` names the Cerbos resource
74
+ * kind (and optional attrs/scope) for an entity — `undefined` abstains. `field` maps a resource
75
+ * attribute (`ownerId`, or `id`) to the column name the ORM adapter resolves (default: same
76
+ * name; return `undefined` to reject an attribute).
77
+ */
78
+ scope?: {
79
+ resource: (entity: Type<unknown>, ability: string) => CerbosResourceQuery | undefined;
80
+ field?: CerbosFieldMapper;
81
+ };
82
+ /**
83
+ * What an engine failure (network error, timeout, unsupported query plan) resolves to:
84
+ * `'deny'` (default — fail CLOSED: an outage must not widen access; scopes become deny-all) or
85
+ * `'abstain'` (fall back to the Gate's own rules).
86
+ */
87
+ onFailure?: 'deny' | 'abstain';
88
+ /** Observe engine failures (logging/metrics). Never affects the verdict. */
89
+ onError?: (error: unknown, context: {
90
+ phase: 'check' | 'plan';
91
+ ability: string;
92
+ }) => void;
93
+ /** Max resources per `CheckResources` call (Cerbos' default server limit is 50). */
94
+ maxBatchSize?: number;
95
+ }
96
+ /**
97
+ * A {@link DecisionProvider} backed by a [Cerbos](https://cerbos.dev) policy decision point:
98
+ * `gate.allows/authorize` → `CheckResources` (batched for `gate.allowsMany`), `gate.scope` →
99
+ * `PlanResources` mapped onto the scope AST (see {@link cerbosPlanToScope}).
100
+ *
101
+ * ```ts
102
+ * import { DECISION_PROVIDER } from '@dudousxd/nestjs-authz';
103
+ * import { CerbosDecisionProvider } from '@dudousxd/nestjs-authz/cerbos';
104
+ * import { HTTP } from '@cerbos/http';
105
+ *
106
+ * { provide: DECISION_PROVIDER, useValue: new CerbosDecisionProvider({
107
+ * client: new HTTP('http://cerbos:3592'),
108
+ * principal: (u: User) => u && { id: u.id, roles: u.roles, attr: { tenantId: u.tenantId } },
109
+ * resource: (ability, r) => r instanceof Post
110
+ * ? { kind: 'app:post', id: r.id, attr: { ownerId: r.ownerId } } : undefined,
111
+ * scope: { resource: (entity) => (entity === Post ? { kind: 'app:post' } : undefined) },
112
+ * }) }
113
+ * ```
114
+ */
115
+ export declare class CerbosDecisionProvider implements DecisionProvider {
116
+ private readonly options;
117
+ private readonly failureVerdict;
118
+ private readonly maxBatchSize;
119
+ constructor(options: CerbosDecisionProviderOptions);
120
+ private action;
121
+ private client;
122
+ private fail;
123
+ decide(user: User, ability: string, resource?: Resource): Promise<DecisionVerdict>;
124
+ decideMany(user: User, requests: DecisionRequest[]): Promise<DecisionVerdict[]>;
125
+ planScope(user: User, entity: Type<unknown>, ability: string): Promise<ScopeConstraint | undefined>;
126
+ }
127
+ export {};
128
+ //# sourceMappingURL=cerbos-decision.provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cerbos-decision.provider.d.ts","sourceRoot":"","sources":["../../src/cerbos/cerbos-decision.provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,134 @@
1
+ import { scopeNone } from '../scope.js';
2
+ import { cerbosPlanToScope } from './plan.js';
3
+ const resourceKey = (r) => JSON.stringify([r.kind, r.id, r.policyVersion ?? null, r.scope ?? null]);
4
+ /**
5
+ * A {@link DecisionProvider} backed by a [Cerbos](https://cerbos.dev) policy decision point:
6
+ * `gate.allows/authorize` → `CheckResources` (batched for `gate.allowsMany`), `gate.scope` →
7
+ * `PlanResources` mapped onto the scope AST (see {@link cerbosPlanToScope}).
8
+ *
9
+ * ```ts
10
+ * import { DECISION_PROVIDER } from '@dudousxd/nestjs-authz';
11
+ * import { CerbosDecisionProvider } from '@dudousxd/nestjs-authz/cerbos';
12
+ * import { HTTP } from '@cerbos/http';
13
+ *
14
+ * { provide: DECISION_PROVIDER, useValue: new CerbosDecisionProvider({
15
+ * client: new HTTP('http://cerbos:3592'),
16
+ * principal: (u: User) => u && { id: u.id, roles: u.roles, attr: { tenantId: u.tenantId } },
17
+ * resource: (ability, r) => r instanceof Post
18
+ * ? { kind: 'app:post', id: r.id, attr: { ownerId: r.ownerId } } : undefined,
19
+ * scope: { resource: (entity) => (entity === Post ? { kind: 'app:post' } : undefined) },
20
+ * }) }
21
+ * ```
22
+ */
23
+ export class CerbosDecisionProvider {
24
+ options;
25
+ failureVerdict;
26
+ maxBatchSize;
27
+ constructor(options) {
28
+ this.options = options;
29
+ this.failureVerdict = options.onFailure === 'abstain' ? undefined : false;
30
+ this.maxBatchSize = Math.max(1, options.maxBatchSize ?? 50);
31
+ }
32
+ action(ability) {
33
+ return this.options.action ? this.options.action(ability) : ability;
34
+ }
35
+ async client(user) {
36
+ const { client } = this.options;
37
+ return typeof client === 'function' ? client(user) : client;
38
+ }
39
+ fail(error, phase, ability) {
40
+ try {
41
+ this.options.onError?.(error, { phase, ability });
42
+ }
43
+ catch {
44
+ // An observer must never change the verdict.
45
+ }
46
+ }
47
+ async decide(user, ability, resource) {
48
+ const [verdict] = await this.decideMany(user, resource === undefined ? [{ ability }] : [{ ability, resource }]);
49
+ return verdict;
50
+ }
51
+ async decideMany(user, requests) {
52
+ const verdicts = requests.map(() => undefined);
53
+ const mapped = requests.map((r) => {
54
+ const resource = this.options.resource(r.ability, r.resource);
55
+ return resource ? { resource, action: this.action(r.ability) } : undefined;
56
+ });
57
+ if (mapped.every((m) => m === undefined))
58
+ return verdicts;
59
+ const client = await this.client(user);
60
+ if (!client)
61
+ return verdicts;
62
+ const principal = await this.options.principal(user);
63
+ if (!principal)
64
+ return verdicts;
65
+ // One ResourceCheck per distinct resource, carrying every action asked about it.
66
+ const checks = new Map();
67
+ for (const m of mapped) {
68
+ if (!m)
69
+ continue;
70
+ const key = resourceKey(m.resource);
71
+ const entry = checks.get(key) ?? { resource: m.resource, actions: new Set() };
72
+ entry.actions.add(m.action);
73
+ checks.set(key, entry);
74
+ }
75
+ const all = [...checks.values()];
76
+ const answers = new Map();
77
+ for (let i = 0; i < all.length; i += this.maxBatchSize) {
78
+ const chunk = all.slice(i, i + this.maxBatchSize);
79
+ try {
80
+ const response = await client.checkResources({
81
+ principal,
82
+ resources: chunk.map((c) => ({ resource: c.resource, actions: [...c.actions] })),
83
+ });
84
+ for (const c of chunk) {
85
+ for (const action of c.actions) {
86
+ answers.set(`${resourceKey(c.resource)}|${action}`, response.isAllowed({ resource: c.resource, action }));
87
+ }
88
+ }
89
+ }
90
+ catch (error) {
91
+ this.fail(error, 'check', requests[0]?.ability ?? '');
92
+ for (const c of chunk) {
93
+ for (const action of c.actions)
94
+ answers.set(`${resourceKey(c.resource)}|${action}`, undefined);
95
+ }
96
+ }
97
+ }
98
+ for (const [index, m] of mapped.entries()) {
99
+ if (!m)
100
+ continue;
101
+ const answer = answers.get(`${resourceKey(m.resource)}|${m.action}`);
102
+ // No answer for a resource we asked about = engine failure → the failure mode.
103
+ verdicts[index] = answer === undefined ? this.failureVerdict : answer;
104
+ }
105
+ return verdicts;
106
+ }
107
+ async planScope(user, entity, ability) {
108
+ const scope = this.options.scope;
109
+ if (!scope)
110
+ return undefined;
111
+ const resource = scope.resource(entity, ability);
112
+ if (!resource)
113
+ return undefined;
114
+ const client = await this.client(user);
115
+ if (!client || typeof client.planResources !== 'function')
116
+ return undefined;
117
+ const principal = await this.options.principal(user);
118
+ if (!principal)
119
+ return undefined;
120
+ try {
121
+ const plan = await client.planResources({
122
+ principal,
123
+ resource,
124
+ action: this.action(ability),
125
+ });
126
+ return cerbosPlanToScope(plan, scope.field);
127
+ }
128
+ catch (error) {
129
+ this.fail(error, 'plan', ability);
130
+ return this.failureVerdict === undefined ? undefined : scopeNone;
131
+ }
132
+ }
133
+ }
134
+ //# sourceMappingURL=cerbos-decision.provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cerbos-decision.provider.js","sourceRoot":"","sources":["../../src/cerbos/cerbos-decision.provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { CerbosDecisionProvider, type CerbosClientLike, type CerbosDecisionProviderOptions, type CerbosPrincipal, type CerbosResource, type CerbosResourceQuery, type CerbosValue, } from './cerbos-decision.provider.js';
2
+ export { CerbosPlanUnsupportedError, cerbosPlanToScope, type CerbosFieldMapper, type CerbosPlanLike, type CerbosPlanOperand, } from './plan.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cerbos/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,6BAA6B,EAClC,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACxB,KAAK,WAAW,GACjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EACjB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,iBAAiB,GACvB,MAAM,WAAW,CAAC"}
@@ -0,0 +1,5 @@
1
+ // `@dudousxd/nestjs-authz/cerbos` — a DecisionProvider backed by a Cerbos PDP. Structural client
2
+ // types only: bring your own `@cerbos/http` / `@cerbos/grpc` client (no runtime dependency here).
3
+ export { CerbosDecisionProvider, } from './cerbos-decision.provider.js';
4
+ export { CerbosPlanUnsupportedError, cerbosPlanToScope, } from './plan.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cerbos/index.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,kGAAkG;AAClG,OAAO,EACL,sBAAsB,GAOvB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,GAIlB,MAAM,WAAW,CAAC"}
@@ -0,0 +1,41 @@
1
+ import { type ScopeConstraint } from '../scope.js';
2
+ /** A Cerbos query-plan operand (structural mirror of `@cerbos/core`'s `PlanExpressionOperand`). */
3
+ export type CerbosPlanOperand = {
4
+ operator: string;
5
+ operands: CerbosPlanOperand[];
6
+ } | {
7
+ value: unknown;
8
+ } | {
9
+ name: string;
10
+ };
11
+ /** A Cerbos `PlanResources` response (structural: `kind` + optional `condition`). */
12
+ export interface CerbosPlanLike {
13
+ kind: string;
14
+ condition?: CerbosPlanOperand;
15
+ }
16
+ /**
17
+ * Maps a Cerbos resource attribute (the part after `request.resource.attr.`, or `id` for
18
+ * `request.resource.id`) to a scope `field` (a column the ORM adapter resolves). Return
19
+ * `undefined` to reject the attribute — the plan is then unsupported (fail closed).
20
+ */
21
+ export type CerbosFieldMapper = (attribute: string) => string | undefined;
22
+ /** Thrown when a Cerbos query plan uses something the scope AST cannot express. */
23
+ export declare class CerbosPlanUnsupportedError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ /**
27
+ * Translate a Cerbos `PlanResources` result into the ORM-neutral {@link ScopeConstraint} that
28
+ * `gate.scope()` returns (and every ORM adapter compiles to a WHERE):
29
+ *
30
+ * - `KIND_ALWAYS_ALLOWED` → `allow-all`, `KIND_ALWAYS_DENIED` → `deny-all`;
31
+ * - `KIND_CONDITIONAL` → the condition tree: `and`/`or` → groups, `not` pushed down to the leaves
32
+ * (De Morgan; `eq`↔`ne`, `lt`↔`gte`, `in`↔`nin`, …), comparisons `eq ne lt gt le ge in` against
33
+ * a resource attribute → leaf conditions (`eq null` → `isNull`). Operands may appear in either
34
+ * order (`value < attr` is flipped).
35
+ *
36
+ * Anything else — arithmetic, `exists`/`all`/lambdas, collection membership on an attribute
37
+ * (`"x" in R.attr.tags`), principal variables, unmapped attributes — throws
38
+ * {@link CerbosPlanUnsupportedError}; callers should fail closed.
39
+ */
40
+ export declare function cerbosPlanToScope(plan: CerbosPlanLike, field?: CerbosFieldMapper): ScopeConstraint;
41
+ //# sourceMappingURL=plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/cerbos/plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EAKrB,MAAM,aAAa,CAAC;AAErB,mGAAmG;AACnG,MAAM,MAAM,iBAAiB,GACzB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,EAAE,CAAA;CAAE,GACnD;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,qFAAqF;AACrF,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,iBAAiB,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AAE1E,mFAAmF;AACnF,qBAAa,0BAA2B,SAAQ,KAAK;gBACvC,OAAO,EAAE,MAAM;CAI5B;AA8DD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,cAAc,EACpB,KAAK,GAAE,iBAA4C,GAClD,eAAe,CAOjB"}
@@ -0,0 +1,158 @@
1
+ import { scopeAll, scopeNone, } from '../scope.js';
2
+ /** Thrown when a Cerbos query plan uses something the scope AST cannot express. */
3
+ export class CerbosPlanUnsupportedError extends Error {
4
+ constructor(message) {
5
+ super(`Unsupported Cerbos query plan: ${message}`);
6
+ this.name = 'CerbosPlanUnsupportedError';
7
+ }
8
+ }
9
+ const ALWAYS_ALLOWED = 'KIND_ALWAYS_ALLOWED';
10
+ const ALWAYS_DENIED = 'KIND_ALWAYS_DENIED';
11
+ const CONDITIONAL = 'KIND_CONDITIONAL';
12
+ /** Cerbos comparison operator → scope operator. */
13
+ const COMPARISON = {
14
+ eq: 'eq',
15
+ ne: 'ne',
16
+ lt: 'lt',
17
+ gt: 'gt',
18
+ le: 'lte',
19
+ ge: 'gte',
20
+ in: 'in',
21
+ };
22
+ /** Logical negation of a scope operator (used to push `not` down to the leaves). */
23
+ const NEGATED = {
24
+ eq: 'ne',
25
+ ne: 'eq',
26
+ lt: 'gte',
27
+ gte: 'lt',
28
+ gt: 'lte',
29
+ lte: 'gt',
30
+ in: 'nin',
31
+ nin: 'in',
32
+ isNull: 'isNotNull',
33
+ isNotNull: 'isNull',
34
+ };
35
+ /** Swap sides of a comparison (`value < attr` ⇔ `attr > value`). */
36
+ const FLIPPED = {
37
+ eq: 'eq',
38
+ ne: 'ne',
39
+ lt: 'gt',
40
+ gt: 'lt',
41
+ lte: 'gte',
42
+ gte: 'lte',
43
+ };
44
+ const isExpression = (o) => typeof o.operator === 'string';
45
+ const isVariable = (o) => typeof o.name === 'string';
46
+ const isValue = (o) => 'value' in o;
47
+ const RESOURCE_ATTR_PREFIXES = ['request.resource.attr.', 'R.attr.'];
48
+ const RESOURCE_ID = new Set(['request.resource.id', 'R.id']);
49
+ function attributeOf(name) {
50
+ if (RESOURCE_ID.has(name))
51
+ return 'id';
52
+ for (const prefix of RESOURCE_ATTR_PREFIXES) {
53
+ if (name.startsWith(prefix))
54
+ return name.slice(prefix.length);
55
+ }
56
+ return undefined;
57
+ }
58
+ const always = (allowed) => ({ kind: allowed ? 'and' : 'or', nodes: [] });
59
+ /**
60
+ * Translate a Cerbos `PlanResources` result into the ORM-neutral {@link ScopeConstraint} that
61
+ * `gate.scope()` returns (and every ORM adapter compiles to a WHERE):
62
+ *
63
+ * - `KIND_ALWAYS_ALLOWED` → `allow-all`, `KIND_ALWAYS_DENIED` → `deny-all`;
64
+ * - `KIND_CONDITIONAL` → the condition tree: `and`/`or` → groups, `not` pushed down to the leaves
65
+ * (De Morgan; `eq`↔`ne`, `lt`↔`gte`, `in`↔`nin`, …), comparisons `eq ne lt gt le ge in` against
66
+ * a resource attribute → leaf conditions (`eq null` → `isNull`). Operands may appear in either
67
+ * order (`value < attr` is flipped).
68
+ *
69
+ * Anything else — arithmetic, `exists`/`all`/lambdas, collection membership on an attribute
70
+ * (`"x" in R.attr.tags`), principal variables, unmapped attributes — throws
71
+ * {@link CerbosPlanUnsupportedError}; callers should fail closed.
72
+ */
73
+ export function cerbosPlanToScope(plan, field = (attribute) => attribute) {
74
+ if (plan.kind === ALWAYS_ALLOWED)
75
+ return scopeAll;
76
+ if (plan.kind === ALWAYS_DENIED)
77
+ return scopeNone;
78
+ if (plan.kind !== CONDITIONAL || !plan.condition) {
79
+ throw new CerbosPlanUnsupportedError(`unknown plan kind ${JSON.stringify(plan.kind)}`);
80
+ }
81
+ return toNode(plan.condition, false, field);
82
+ }
83
+ function toNode(operand, negate, field) {
84
+ if (isValue(operand)) {
85
+ if (typeof operand.value !== 'boolean') {
86
+ throw new CerbosPlanUnsupportedError('a non-boolean constant used as a condition');
87
+ }
88
+ return always(operand.value !== negate);
89
+ }
90
+ if (isVariable(operand)) {
91
+ // A bare boolean attribute (`R.attr.public`).
92
+ return leaf(operand.name, 'eq', !negate, field);
93
+ }
94
+ if (!isExpression(operand))
95
+ throw new CerbosPlanUnsupportedError('malformed operand');
96
+ const { operator, operands } = operand;
97
+ if (operator === 'not') {
98
+ const [inner] = operands;
99
+ if (operands.length !== 1 || !inner)
100
+ throw new CerbosPlanUnsupportedError('`not` arity');
101
+ return toNode(inner, !negate, field);
102
+ }
103
+ if (operator === 'and' || operator === 'or') {
104
+ // De Morgan: ¬(a ∧ b) = ¬a ∨ ¬b.
105
+ const isAnd = (operator === 'and') !== negate;
106
+ return {
107
+ kind: isAnd ? 'and' : 'or',
108
+ nodes: operands.map((child) => toNode(child, negate, field)),
109
+ };
110
+ }
111
+ const op = COMPARISON[operator];
112
+ if (!op)
113
+ throw new CerbosPlanUnsupportedError(`operator "${operator}"`);
114
+ const [left, right] = operands;
115
+ if (operands.length !== 2 || !left || !right) {
116
+ throw new CerbosPlanUnsupportedError(`\`${operator}\` arity`);
117
+ }
118
+ let variable;
119
+ let value;
120
+ let effective = op;
121
+ if (isVariable(left) && isValue(right)) {
122
+ variable = left;
123
+ value = right.value;
124
+ }
125
+ else if (isValue(left) && isVariable(right)) {
126
+ const flipped = FLIPPED[op];
127
+ if (!flipped) {
128
+ throw new CerbosPlanUnsupportedError(`"${operator}" with the attribute on the right (collection membership)`);
129
+ }
130
+ variable = right;
131
+ value = left.value;
132
+ effective = flipped;
133
+ }
134
+ else {
135
+ throw new CerbosPlanUnsupportedError(`"${operator}" must compare one attribute with one value`);
136
+ }
137
+ if (value === null && (effective === 'eq' || effective === 'ne')) {
138
+ effective = effective === 'eq' ? 'isNull' : 'isNotNull';
139
+ }
140
+ if (effective === 'in' && !Array.isArray(value)) {
141
+ throw new CerbosPlanUnsupportedError('`in` needs a list value');
142
+ }
143
+ return leaf(variable.name, negate ? NEGATED[effective] : effective, value, field);
144
+ }
145
+ function leaf(variable, op, value, field) {
146
+ const attribute = attributeOf(variable);
147
+ if (attribute === undefined) {
148
+ throw new CerbosPlanUnsupportedError(`variable "${variable}" is not a resource attribute`);
149
+ }
150
+ const mapped = field(attribute);
151
+ if (mapped === undefined) {
152
+ throw new CerbosPlanUnsupportedError(`resource attribute "${attribute}" is not mapped`);
153
+ }
154
+ return op === 'isNull' || op === 'isNotNull'
155
+ ? { kind: 'condition', field: mapped, op }
156
+ : { kind: 'condition', field: mapped, op, value };
157
+ }
158
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../../src/cerbos/plan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import type { Type } from '@nestjs/common';
2
+ import type { ScopeConstraint } from './scope.js';
3
+ import type { PolicyResult, Resource, User } from './types.js';
4
+ /**
5
+ * What a {@link DecisionProvider} answers for one `(user, ability, resource)`:
6
+ *
7
+ * - `true` / `{ allowed: true }` → **allow** (short-circuits the Gate);
8
+ * - `false` / `{ allowed: false, message? }` → **deny** (short-circuits the Gate — unlike the
9
+ * grant-only {@link PermissionProvider}, a decision provider CAN deny);
10
+ * - `undefined` / `null` → **abstain**: the provider has no opinion (e.g. the resource kind or
11
+ * the tenant isn't governed by the external engine) and the Gate continues with its normal
12
+ * resolution (permission provider → policy → ad-hoc gate).
13
+ */
14
+ export type DecisionVerdict = PolicyResult | undefined;
15
+ /** One `(ability, resource)` pair in a {@link DecisionProvider.decideMany} batch. */
16
+ export interface DecisionRequest {
17
+ ability: string;
18
+ resource?: Resource;
19
+ }
20
+ /**
21
+ * Optional seam for an **external policy decision point** (Cerbos, OPA, OpenFGA, a remote
22
+ * authz service, …) — the authoritative, resource-aware counterpart of the grant-only
23
+ * {@link PermissionProvider}. Register it under the {@link DECISION_PROVIDER} token.
24
+ *
25
+ * ## Precedence (single decision)
26
+ *
27
+ * 1. global `superAdmin` hook (receives the resource as 3rd argument) — allow/deny short-circuit;
28
+ * 2. **decision provider** `decide` — allow/deny short-circuit, `undefined` abstains;
29
+ * 3. permission provider grant (RBAC, grant-only);
30
+ * 4. policy `before` → policy method, else ad-hoc gate;
31
+ * 5. global `after` hook (only when the path above had no opinion).
32
+ *
33
+ * The provider is also consulted for anonymous callers (`user` is `undefined`) so an engine can
34
+ * allow public access; abstain to keep the Gate's default anonymous-deny.
35
+ *
36
+ * ## Batching
37
+ *
38
+ * `gate.allowsMany(...)` calls {@link decideMany} ONCE for the whole batch when implemented (a
39
+ * list page with N cards costs one engine round-trip instead of N); otherwise `decide` per item.
40
+ *
41
+ * ## Query scopes
42
+ *
43
+ * `gate.scope(Entity, ability)` calls {@link planScope} right after the `superAdmin` hook. A
44
+ * returned {@link ScopeConstraint} is used as-is (e.g. an engine's query plan mapped onto the scope
45
+ * AST — see `@dudousxd/nestjs-authz/cerbos`); `undefined` abstains and the Gate falls back to the
46
+ * permission-provider grant / the policy's `scope` method.
47
+ *
48
+ * Errors thrown by the provider propagate (the check fails loudly). Providers that talk to a
49
+ * remote engine should decide their own failure mode — the Cerbos adapter fails CLOSED (deny).
50
+ */
51
+ export interface DecisionProvider {
52
+ decide(user: User, ability: string, resource?: Resource): DecisionVerdict | Promise<DecisionVerdict>;
53
+ /** Optional batch form; must return one verdict per request, in order. */
54
+ decideMany?(user: User, requests: DecisionRequest[]): Promise<DecisionVerdict[]>;
55
+ /** Optional query-plan → scope mapping. `undefined` = abstain. */
56
+ planScope?(user: User, entity: Type<unknown>, ability: string): ScopeConstraint | undefined | Promise<ScopeConstraint | undefined>;
57
+ }
58
+ //# sourceMappingURL=decision-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision-provider.d.ts","sourceRoot":"","sources":["../src/decision-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAE/D;;;;;;;;;GASG;AACH,MAAM,MAAM,eAAe,GAAG,YAAY,GAAG,SAAS,CAAC;AAEvD,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CACJ,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,QAAQ,GAClB,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IAC9C,0EAA0E;IAC1E,UAAU,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACjF,kEAAkE;IAClE,SAAS,CAAC,CACR,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,EACrB,OAAO,EAAE,MAAM,GACd,eAAe,GAAG,SAAS,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC,CAAC;CACvE"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=decision-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision-provider.js","sourceRoot":"","sources":["../src/decision-provider.ts"],"names":[],"mappings":""}
@@ -30,7 +30,7 @@ export interface AuthzDecisionDiagnostic {
30
30
  resourceId: string | number | null;
31
31
  }
32
32
  /** The resolution path that produced a decision — the "why" behind a 403 (or a grant). */
33
- export type AuthzDecisionReason = 'super-admin' | 'permission-provider' | 'policy-before' | 'policy' | 'gate' | 'after' | 'anonymous';
33
+ export type AuthzDecisionReason = 'super-admin' | 'decision-provider' | 'permission-provider' | 'policy-before' | 'policy' | 'gate' | 'after' | 'anonymous';
34
34
  /**
35
35
  * Publish a decision to the channel, but only when something is listening. Never
36
36
  * throws — building the payload or notifying a subscriber must not break a check.