@dudousxd/nestjs-authz 0.8.1 → 0.9.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 (122) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +279 -0
  3. package/dist/authz.guard.d.ts +48 -0
  4. package/dist/authz.guard.d.ts.map +1 -0
  5. package/dist/authz.guard.js +144 -0
  6. package/dist/authz.guard.js.map +1 -0
  7. package/dist/can-endpoint.controller.d.ts +0 -19
  8. package/dist/can-endpoint.controller.d.ts.map +1 -1
  9. package/dist/can-endpoint.controller.js +18 -9
  10. package/dist/can-endpoint.controller.js.map +1 -1
  11. package/dist/decision-provider.d.ts +25 -16
  12. package/dist/decision-provider.d.ts.map +1 -1
  13. package/dist/decision-provider.js +11 -1
  14. package/dist/decision-provider.js.map +1 -1
  15. package/dist/decisions.d.ts +24 -0
  16. package/dist/decisions.d.ts.map +1 -0
  17. package/dist/decisions.js +87 -0
  18. package/dist/decisions.js.map +1 -0
  19. package/dist/decorator/roles.decorator.d.ts +6 -12
  20. package/dist/decorator/roles.decorator.d.ts.map +1 -1
  21. package/dist/decorator/roles.decorator.js +13 -15
  22. package/dist/decorator/roles.decorator.js.map +1 -1
  23. package/dist/diagnostics.d.ts +15 -5
  24. package/dist/diagnostics.d.ts.map +1 -1
  25. package/dist/diagnostics.js +3 -1
  26. package/dist/diagnostics.js.map +1 -1
  27. package/dist/errors/exceptions.d.ts +0 -8
  28. package/dist/errors/exceptions.d.ts.map +1 -1
  29. package/dist/errors/exceptions.js +0 -14
  30. package/dist/errors/exceptions.js.map +1 -1
  31. package/dist/gate.d.ts +83 -218
  32. package/dist/gate.d.ts.map +1 -1
  33. package/dist/gate.js +209 -611
  34. package/dist/gate.js.map +1 -1
  35. package/dist/helpers.d.ts +46 -0
  36. package/dist/helpers.d.ts.map +1 -0
  37. package/dist/helpers.js +89 -0
  38. package/dist/helpers.js.map +1 -0
  39. package/dist/index.d.ts +28 -21
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +30 -13
  42. package/dist/index.js.map +1 -1
  43. package/dist/links.d.ts +39 -0
  44. package/dist/links.d.ts.map +1 -0
  45. package/dist/links.js +57 -0
  46. package/dist/links.js.map +1 -0
  47. package/dist/module.d.ts +23 -22
  48. package/dist/module.d.ts.map +1 -1
  49. package/dist/module.js +129 -158
  50. package/dist/module.js.map +1 -1
  51. package/dist/official.d.ts +39 -0
  52. package/dist/official.d.ts.map +1 -0
  53. package/dist/official.js +44 -0
  54. package/dist/official.js.map +1 -0
  55. package/dist/policies/authz-policy.d.ts +42 -0
  56. package/dist/policies/authz-policy.d.ts.map +1 -0
  57. package/dist/policies/authz-policy.js +80 -0
  58. package/dist/policies/authz-policy.js.map +1 -0
  59. package/dist/policies/gate.policy.d.ts +18 -0
  60. package/dist/policies/gate.policy.d.ts.map +1 -0
  61. package/dist/policies/gate.policy.js +37 -0
  62. package/dist/policies/gate.policy.js.map +1 -0
  63. package/dist/policies/index.d.ts +5 -0
  64. package/dist/policies/index.d.ts.map +1 -0
  65. package/dist/policies/index.js +5 -0
  66. package/dist/policies/index.js.map +1 -0
  67. package/dist/policies/permission.policy.d.ts +27 -0
  68. package/dist/policies/permission.policy.d.ts.map +1 -0
  69. package/dist/policies/permission.policy.js +61 -0
  70. package/dist/policies/permission.policy.js.map +1 -0
  71. package/dist/policies/role.policy.d.ts +27 -0
  72. package/dist/policies/role.policy.d.ts.map +1 -0
  73. package/dist/policies/role.policy.js +51 -0
  74. package/dist/policies/role.policy.js.map +1 -0
  75. package/dist/policy-index.d.ts +51 -0
  76. package/dist/policy-index.d.ts.map +1 -0
  77. package/dist/policy-index.js +135 -0
  78. package/dist/policy-index.js.map +1 -0
  79. package/dist/rbac.d.ts +92 -0
  80. package/dist/rbac.d.ts.map +1 -0
  81. package/dist/rbac.js +293 -0
  82. package/dist/rbac.js.map +1 -0
  83. package/dist/resource-resolver.d.ts +15 -17
  84. package/dist/resource-resolver.d.ts.map +1 -1
  85. package/dist/resource-resolver.js +8 -8
  86. package/dist/resource-resolver.js.map +1 -1
  87. package/dist/runtime.d.ts +31 -0
  88. package/dist/runtime.d.ts.map +1 -0
  89. package/dist/runtime.js +40 -0
  90. package/dist/runtime.js.map +1 -0
  91. package/dist/tokens.d.ts +4 -8
  92. package/dist/tokens.d.ts.map +1 -1
  93. package/dist/tokens.js +4 -8
  94. package/dist/tokens.js.map +1 -1
  95. package/dist/types.d.ts +52 -171
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +19 -10
  98. package/skills/authz-enforcement/SKILL.md +67 -135
  99. package/skills/authz-policies/SKILL.md +43 -117
  100. package/skills/authz-query-scopes/SKILL.md +16 -9
  101. package/skills/authz-rbac-seams/SKILL.md +29 -12
  102. package/skills/authz-setup/SKILL.md +45 -106
  103. package/dist/decorator/can.decorator.d.ts +0 -29
  104. package/dist/decorator/can.decorator.d.ts.map +0 -1
  105. package/dist/decorator/can.decorator.js +0 -23
  106. package/dist/decorator/can.decorator.js.map +0 -1
  107. package/dist/decorator/policy.decorator.d.ts +0 -18
  108. package/dist/decorator/policy.decorator.d.ts.map +0 -1
  109. package/dist/decorator/policy.decorator.js +0 -28
  110. package/dist/decorator/policy.decorator.js.map +0 -1
  111. package/dist/guard/can.guard.d.ts +0 -31
  112. package/dist/guard/can.guard.d.ts.map +0 -1
  113. package/dist/guard/can.guard.js +0 -92
  114. package/dist/guard/can.guard.js.map +0 -1
  115. package/dist/guard/roles.guard.d.ts +0 -21
  116. package/dist/guard/roles.guard.d.ts.map +0 -1
  117. package/dist/guard/roles.guard.js +0 -50
  118. package/dist/guard/roles.guard.js.map +0 -1
  119. package/dist/policy-registry.d.ts +0 -46
  120. package/dist/policy-registry.d.ts.map +0 -1
  121. package/dist/policy-registry.js +0 -101
  122. package/dist/policy-registry.js.map +0 -1
package/dist/types.d.ts CHANGED
@@ -1,219 +1,100 @@
1
1
  import type { Type } from '@nestjs/common';
2
2
  import type { UserRef } from './context-accessor.js';
3
3
  import type { ResourceResolver } from './resource-resolver.js';
4
- /**
5
- * A resource instance passed to policy methods. `unknown` because authz never
6
- * inspects the resource itself — the policy method does.
7
- */
4
+ /** A resource passed to a policy ability. authz never inspects it; the policy does. */
8
5
  export type Resource = object;
9
6
  /**
10
- * The current user as seen by a policy/gate. authz does not model the user; it
11
- * is whatever the app's auth layer produced (or `undefined` when unauthenticated).
7
+ * The user a policy or gate sees: whatever authentication produced, `null` for a
8
+ * guest. authz does not model it.
12
9
  */
13
10
  export type User = unknown;
14
11
  /**
15
- * The richer result a policy method / ad-hoc gate may return instead of a bare
16
- * boolean: a verdict plus an optional human-readable `message` explaining a denial.
17
- * The message is surfaced on the thrown `ForbiddenException` (so a 403 is debuggable)
18
- * and published on the diagnostics decision. Returning a bare boolean is unchanged.
19
- *
20
- * Mirrors Laravel's `Response::allow()/deny('message')`.
12
+ * A verdict with an optional reason. What a {@link DecisionProvider} may answer instead of
13
+ * a bare boolean. Only `allowed` is used: `@nestjs/authorization` has no deny-with-reason, so a
14
+ * route denial is the official 403 and the `message` is dropped (README, "Migrating from 0.8").
21
15
  */
22
16
  export interface PolicyResponse {
23
17
  allowed: boolean;
24
18
  message?: string;
25
19
  }
26
- /**
27
- * What a policy method / gate / hook may return: a bare boolean (unchanged) or the
28
- * richer {@link PolicyResponse}. Hooks additionally allow `void`/`undefined` to mean
29
- * "no opinion — fall through".
30
- */
20
+ /** A bare boolean or a {@link PolicyResponse}. */
31
21
  export type PolicyResult = boolean | PolicyResponse;
22
+ /** An ad-hoc, model-less gate registered with `gate.define(name, fn)`. */
23
+ export type GateFn = (user: User, resource?: Resource) => boolean | Promise<boolean>;
32
24
  /**
33
- * A policy method: decides a single ability for a `(user, resource?)` pair.
34
- * Class-level abilities (e.g. `create`) omit the resource. May be async. May return
35
- * a bare boolean (unchanged) or a richer {@link PolicyResponse} with a deny message.
25
+ * The global super-admin hook. `true` grants any check; anything else falls through.
26
+ * Policies honor it when they extend `AuthzPolicy` (or call `superAdminBefore` from
27
+ * their `before()`); `RolePolicy` and `PermissionPolicy` do. The `Gate`'s string
28
+ * abilities and `gate.scope()` ask it first.
36
29
  */
37
- export type PolicyMethod = (user: User, resource?: Resource) => PolicyResult | Promise<PolicyResult>;
38
- /**
39
- * Optional per-policy `before` hook. Runs before the ability method.
40
- * - return `true` → allow (short-circuit)
41
- * - return `false` → deny (short-circuit)
42
- * - return `void`/`undefined` → fall through to the ability method
43
- */
44
- export type PolicyBeforeHook = (user: User, ability: string) => PolicyResult | undefined | Promise<PolicyResult | undefined>;
45
- /** A policy instance is an arbitrary object whose methods are abilities. */
46
- export type PolicyInstance = Record<string, unknown> & {
47
- before?: PolicyBeforeHook;
48
- };
49
- /**
50
- * An ad-hoc gate: a model-less ability resolved by name via `gate.define`.
51
- * May return a bare boolean (unchanged) or a richer {@link PolicyResponse}.
52
- */
53
- export type GateFn = (user: User, resource?: Resource) => PolicyResult | Promise<PolicyResult>;
54
- /**
55
- * Global `after` hook. Mirrors Laravel's `Gate::after()`: runs AFTER policy/gate
56
- * resolution and may observe or override the decision.
57
- *
58
- * `result` is the verdict produced by the policy/gate (`true`/`false`), or
59
- * `undefined` when the policy/gate returned a nullish value (it had "no opinion").
60
- *
61
- * Override semantics (Laravel-idiomatic): the hook's return value REPLACES the
62
- * decision ONLY when the policy/gate produced no explicit verdict (`result` is
63
- * `undefined`). When the policy/gate DID decide (returned a boolean), `after` is
64
- * purely an observer and its return value is ignored — so it can never silently
65
- * overturn an explicit allow/deny. Returning `void`/`undefined` always leaves the
66
- * decision untouched. Like a policy, it may return a {@link PolicyResponse} to
67
- * attach a deny message.
68
- */
69
- export type AfterHook = (user: User, ability: string, result: boolean | undefined, resource?: Resource) => PolicyResult | undefined | Promise<PolicyResult | undefined>;
30
+ export type SuperAdminHook = (user: User, ability: string,
70
31
  /**
71
- * A resource loader: turns an `id` into the REAL entity instance for a resource
72
- * `type`. Used by the opt-in `POST /authz/can` endpoint to rehydrate the client's
73
- * `{ type, id }` shim before authorizing, so an instance-bound `@Policy` matches by
74
- * constructor. Returning nullish signals "not found" (the endpoint denies). May be async.
32
+ * What the check targets: the resource of a single check (the policy's first
33
+ * argument), or the entity class for `gate.scope(...)`. `undefined` for a model-less
34
+ * ability. Lets the hook scope the bypass (e.g. to one tenant's resources).
75
35
  */
36
+ resource?: Resource) => boolean | null | undefined | Promise<boolean | null | undefined>;
37
+ /** Loads the real entity for a client `{ type, id }` reference (the `POST /authz/can` endpoint). */
76
38
  export type ResourceLoader = (id: string | number) => unknown | Promise<unknown>;
77
- /**
78
- * Resource loaders keyed by the resource `type` name as emitted by the client/codegen
79
- * (e.g. `'Post'`). See {@link AuthzModuleOptions.resourceLoaders}.
80
- */
39
+ /** Resource loaders keyed by the resource `type` name the client sends (e.g. `'Post'`). */
81
40
  export type ResourceLoaderMap = Record<string, ResourceLoader>;
82
41
  /**
83
- * Global super-admin before-hook. Runs before any policy/gate; truthy → allow.
84
- * `void`/`undefined`/`false` falls through to normal resolution.
42
+ * What happens to a route that declares no `@Can()` (nor `Can.Anyone()` / `@Public()`):
43
+ * - `'deny'` (default): nothing special; the official `AuthorizationGuard` denies it.
44
+ * - `'allow'`: the migration mode. `AuthzModule` registers its own global {@link AuthzGuard},
45
+ * which enforces the official requirements exactly as the official guard does and lets
46
+ * unannotated routes through. Needs `AuthorizationModule.forRoot({ globalGuard: false })`.
85
47
  */
86
- export type SuperAdminHook = (user: User, ability: string,
87
- /**
88
- * The dispatch target: the resource instance/class of a single check, or the scoped entity
89
- * class for `gate.scope(...)`. `undefined` for a model-less ability.
90
- */
91
- resource?: Resource) => PolicyResult | undefined | Promise<PolicyResult | undefined>;
92
- export interface AuthzModuleOptions {
48
+ export type UnannotatedRoutes = 'allow' | 'deny';
49
+ /** Options that decide what the module registers, so they are static (read before any factory runs). */
50
+ export interface AuthzModuleStaticOptions {
93
51
  /**
94
- * Policy classes to register explicitly. Each must be decorated with
95
- * `@Policy(Resource)`. Auto-discovery of `@Policy` providers also works, so
96
- * this is optional.
52
+ * Opt-in `POST /authz/can` endpoint for client checks that are not hydrated. `true`
53
+ * mounts it at `authz/can`; a string at that path. Off by default.
97
54
  */
98
- policies?: Array<Type<PolicyInstance>>;
55
+ canEndpoint?: boolean | string;
99
56
  /**
100
- * Global super-admin before-hook applied to every authorization check. Return
101
- * `true` to grant (e.g. super-admins). Returning `void`/`false` falls through.
102
- *
103
- * ⚠️ IMPORTANT — what `user` is: on the **context path** (no `forUser`), the
104
- * `user` passed here is the raw {@link UserRef} from nestjs-context — just
105
- * `{ type, id }`, NOT your hydrated user entity. So `superAdmin: u => u.isAdmin`
106
- * will NOT fire from a route/`gate.allows(...)` unless you also configure
107
- * {@link AuthzModuleOptions.resolveUser} to hydrate the full entity. On the
108
- * explicit `gate.forUser(entity)` path you receive exactly what you passed.
57
+ * Class decorators applied to every controller this module mounts (the `POST /authz/can`
58
+ * endpoint), after its own: `[UseGuards(JwtAuthGuard), ApiTags('authz')]`. The controllers
59
+ * are open to the official guard by default (`Can.Anyone()`); a `@Can()` here restricts them.
109
60
  */
61
+ decorators?: ClassDecorator[];
62
+ /** See {@link UnannotatedRoutes}. Default `'deny'`. */
63
+ unannotated?: UnannotatedRoutes;
64
+ }
65
+ export interface AuthzModuleOptions extends AuthzModuleStaticOptions {
66
+ /** The global super-admin hook. See {@link SuperAdminHook}. */
110
67
  superAdmin?: SuperAdminHook;
111
68
  /**
112
- * Global `after` hook (Laravel's `Gate::after()`). Runs AFTER policy/gate
113
- * resolution and may observe or override the decision. It can only OVERRIDE the
114
- * verdict when the policy/gate produced no explicit result (returned nullish);
115
- * when the policy/gate decided explicitly, `after` is observe-only and its return
116
- * value is ignored. Returning `void`/`undefined` always leaves the decision
117
- * untouched. See {@link AfterHook}.
118
- *
119
- * Receives the same (possibly un-hydrated) `user` as {@link superAdmin} on the
120
- * context path — apply {@link resolveUser} if you need the full entity.
121
- */
122
- after?: AfterHook;
123
- /**
124
- * Optional hook to hydrate the full user entity from the context's
125
- * {@link UserRef} (`{ type, id }`) before policies / `before` / `superAdmin`
126
- * run. WITHOUT this hook, the context path passes only `{ type, id }` to those
127
- * functions (it does not load your `User` row), so property checks such as
128
- * `superAdmin: u => u.isAdmin` or `post.authorId === user.id` against a real
129
- * column will not see hydrated fields. Provide `resolveUser` to load the entity
130
- * (e.g. from your ORM) keyed by the ref. Returning `undefined` is treated as
131
- * "no user" (deny path), identical to an anonymous request. Within one request
132
- * (nestjs-context store) the hook runs once per `type:id`; every later check
133
- * reuses that result.
134
- *
135
- * Only affects the context path; `gate.forUser(entity)` always uses the value
136
- * you pass directly and never invokes this hook.
69
+ * Hydrates the full user from nestjs-context's `{ type, id }` ref for `Gate` calls
70
+ * made without a user (`gate.allows(...)` rather than `gate.forUser(user)`), when
71
+ * `@nestjs/authentication`'s `AuthenticationContext` has no user to offer. Runs once
72
+ * per `type:id` per request (nestjs-context store). Routes never use it: the official
73
+ * guard reads the user with its own `getUser`.
137
74
  */
138
75
  resolveUser?: (ref: UserRef) => User | undefined | Promise<User | undefined>;
139
76
  /**
140
- * Override the default {@link RoleResolver} used by coarse role-checks
141
- * (`gate.hasRole('teacher')`, `@Roles('admin')`). The default reads `user.roles`
142
- * (`string[]`) OR `user.role` (`string | string[]`) off the user object, so
143
- * role-checks work with ZERO RBAC tables. Provide this to derive roles from a
144
- * different shape. When the optional `ROLE_PROVIDER` seam is ALSO registered, the
145
- * Gate unions both sources' roles.
146
- *
147
- * On the context path the resolver receives whatever the context produced (the
148
- * raw {@link UserRef} unless `resolveUser` hydrated the entity); on the explicit
149
- * `gate.forUser(entity)` path it receives exactly what you passed.
77
+ * Derives roles from the user object. Default: `user.roles` (`string[]`) or
78
+ * `user.role` (`string | string[]`). Unioned with the RBAC adapter's role store.
150
79
  */
151
80
  resolveRoles?: (user: User) => string[] | undefined | Promise<string[] | undefined>;
152
- /**
153
- * Override the default {@link ResourceResolver} used to load an instance for
154
- * `@Can(ability, Resource)` routes. Defaults to {@link IdParamResourceResolver}
155
- * (reads the route `:id` param), registered automatically by `forRoot`. Apps
156
- * needing real ORM entities provide their own here (or bind the
157
- * `RESOURCE_RESOLVER` token directly).
158
- */
81
+ /** Loads the resource for `byId(Resource)`. Default: {@link IdParamResourceResolver}. */
159
82
  resourceResolver?: ResourceResolver;
160
- /**
161
- * Route param name the default {@link IdParamResourceResolver} reads to build
162
- * the resource shim. Defaults to `'id'`. Ignored when {@link resourceResolver}
163
- * is supplied.
164
- */
83
+ /** Route param the default resolver reads. Default `'id'`. */
165
84
  idParam?: string;
166
- /**
167
- * Opt-in `POST /authz/can` fallback endpoint. **Off by default.** When enabled,
168
- * registers a controller that runs `gate.allows(ability, resource?)` for the
169
- * current (context) user and returns `{ allowed: boolean }`. This is the
170
- * last-resort path the codegen-emitted `can()` helper targets.
171
- *
172
- * - `true` → mount at the default path `authz/can`.
173
- * - `string` → mount at that path (e.g. `'api/authz/can'`).
174
- * - `false`/omitted → no endpoint is registered.
175
- *
176
- * Prefer the no-request paths (shared Inertia props, per-resource `can` maps)
177
- * — this endpoint exists only for abilities not already hydrated on the client.
178
- */
179
- canEndpoint?: boolean | string;
180
- /**
181
- * Resource loaders keyed by the resource `type` name the client/codegen emits
182
- * (e.g. `'Post'`). They close the per-instance gap in the {@link canEndpoint}
183
- * fallback: when the endpoint receives `{ ability, resource: { type, id } }` and a
184
- * loader is registered for `type`, it `await`s `loader(id)` and authorizes the REAL
185
- * entity — so an instance-bound `@Policy` matches by constructor and its method runs
186
- * with the loaded resource. A loader returning nullish is treated as "not found"
187
- * (the endpoint denies). Types WITHOUT a loader keep the prior behavior (class-level
188
- * / ad-hoc only; a resource-bound ability still denies). Opt-in: unset → unchanged.
189
- *
190
- * ```ts
191
- * resourceLoaders: { Post: (id) => postRepo.findOneBy({ id: Number(id) }) }
192
- * ```
193
- */
85
+ /** Loaders that turn the endpoint's `{ type, id }` into the real entity, keyed by type. */
194
86
  resourceLoaders?: ResourceLoaderMap;
195
87
  }
196
88
  export interface AuthzModuleOptionsFactory {
197
89
  createAuthzOptions(): Promise<AuthzModuleOptions> | AuthzModuleOptions;
198
90
  }
199
- export interface AuthzModuleAsyncOptions {
91
+ export interface AuthzModuleAsyncOptions extends AuthzModuleStaticOptions {
200
92
  imports?: unknown[];
201
93
  useExisting?: Type<AuthzModuleOptionsFactory>;
202
94
  useClass?: Type<AuthzModuleOptionsFactory>;
203
95
  useFactory?: (...args: unknown[]) => Promise<AuthzModuleOptions> | AuthzModuleOptions;
204
96
  inject?: unknown[];
205
- /**
206
- * Opt-in `POST /authz/can` fallback endpoint (see {@link AuthzModuleOptions.canEndpoint}).
207
- * Declared statically here because controllers are registered at module-definition
208
- * time, before the async options factory resolves. Off by default.
209
- */
210
- canEndpoint?: boolean | string;
211
- /**
212
- * Resource loaders for the {@link canEndpoint} fallback
213
- * (see {@link AuthzModuleOptions.resourceLoaders}). May also be returned from the
214
- * async options factory; either way the endpoint reads them from the resolved
215
- * options, so this declaration is for the rarer case of supplying them statically.
216
- */
97
+ /** See {@link AuthzModuleOptions.resourceLoaders}. */
217
98
  resourceLoaders?: ResourceLoaderMap;
218
99
  }
219
100
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-authz",
3
- "version": "0.8.1",
4
- "description": "NestJS authorization — Laravel-style Gates & Policies, @Can guard, resource resolver (zero DB).",
3
+ "version": "0.9.0",
4
+ "description": "Extras on top of @nestjs/authorization: persisted RBAC policies, @Can helpers (byId, roles, permission), a Gate with batch checks and ad-hoc gates, super-admin, query scoping.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -21,7 +21,8 @@
21
21
  "acl",
22
22
  "rbac",
23
23
  "tanstack-intent",
24
- "cerbos"
24
+ "cerbos",
25
+ "nestjs-authorization"
25
26
  ],
26
27
  "author": "Davi Carvalho <davi@goflip.ai>",
27
28
  "type": "module",
@@ -55,24 +56,32 @@
55
56
  },
56
57
  "peerDependencies": {
57
58
  "@dudousxd/nestjs-context": ">=0.1.0",
58
- "@nestjs/common": ">=10.0.0",
59
- "@nestjs/core": ">=10.0.0",
60
- "reflect-metadata": ">=0.1.13"
59
+ "@nestjs/authentication": "^0.1.5",
60
+ "@nestjs/authorization": "^0.1.3",
61
+ "@nestjs/common": "^12.0.0",
62
+ "@nestjs/core": "^12.0.0",
63
+ "reflect-metadata": ">=0.1.13",
64
+ "rxjs": "^7.1.0"
61
65
  },
62
66
  "peerDependenciesMeta": {
63
67
  "@dudousxd/nestjs-context": {
64
68
  "optional": true
69
+ },
70
+ "@nestjs/authentication": {
71
+ "optional": true
65
72
  }
66
73
  },
67
74
  "devDependencies": {
68
75
  "@cerbos/http": "0.29.0",
76
+ "@nestjs/authentication": "0.1.5",
77
+ "@nestjs/authorization": "0.1.3",
69
78
  "@nestjs/common": "12.0.1",
70
79
  "@nestjs/core": "12.0.1",
71
80
  "@nestjs/platform-express": "12.0.1",
72
81
  "@nestjs/testing": "12.0.1",
73
82
  "@tanstack/intent": "0.4.0",
74
83
  "@types/express": "4.17.25",
75
- "@types/node": "20.19.43",
84
+ "@types/node": "22.20.5",
76
85
  "@types/supertest": "6.0.3",
77
86
  "express": "4.22.2",
78
87
  "reflect-metadata": "0.2.2",
@@ -82,12 +91,12 @@
82
91
  "vitest": "3.2.7"
83
92
  },
84
93
  "engines": {
85
- "node": ">=20"
94
+ "node": ">=22"
86
95
  },
87
96
  "scripts": {
88
- "build": "tsc -p tsconfig.json",
97
+ "build": "tsc -p tsconfig.json --incremental false",
89
98
  "test": "vitest run --passWithNoTests",
90
99
  "test:watch": "vitest",
91
- "typecheck": "tsc -p tsconfig.json --noEmit"
100
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json"
92
101
  }
93
102
  }
@@ -1,171 +1,103 @@
1
1
  ---
2
2
  name: authz-enforcement
3
3
  description: >
4
- Enforce authorization in @dudousxd/nestjs-authz with the @Can and @Roles guards and the
5
- programmatic Gate. @Can('update', Post) resolves a Post instance via the ResourceResolver
6
- and runs PostPolicy.update; @Can('create', Post, { classLevel: true }) skips loading;
7
- @Can('access-admin') hits an ad-hoc gate. @Roles('admin','teacher') is the coarse role
8
- check. Both guards are auto-registered as APP_GUARD and are inert on un-annotated routes.
9
- Covers the ResourceResolver seam (default IdParamResourceResolver reads :id vs a real ORM
10
- resolver bound to RESOURCE_RESOLVER) and the Gate API: authorize/allows/denies/allowsMany,
11
- forUser(user) BoundGate, define(ability, fn), hasRole/hasAnyRole.
4
+ Enforce authorization with @dudousxd/nestjs-authz 0.9 on top of @nestjs/authorization.
5
+ Routes use the official @Can(Policy, 'ability', argsResolver) and its global guard; our
6
+ helpers go inside it: byId(Post) (loads the :id record with the ResourceResolver, kept for
7
+ the handler via authorizedResource), roles(), permission(), ability(), staticCanArgs()
8
+ (static, marked with Symbol.for('@dudousxd/static-can-args')), and the sugar @Roles /
9
+ @Permission. Services use the Gate: allows/denies/authorize with a policy class or a string
10
+ ability, allowsMany, forUser, define, hasRole. Covers migrating the 0.8 string @Can.
12
11
  metadata:
13
12
  type: core
14
13
  library: "@dudousxd/nestjs-authz"
15
- library_version: "0.6.3"
14
+ library_version: "0.9.0"
16
15
  framework: nestjs
17
16
  ---
18
17
 
19
18
  # Enforcing authorization
20
19
 
21
- Two paths: **declarative** guards (`@Can`, `@Roles`) and the **programmatic** `Gate`.
22
- Both guards are registered as `APP_GUARD` by `AuthzModule.forRoot`, and both are **inert**
23
- on routes without their decorator (no metadata → allow).
24
-
25
- ## Setup
26
-
27
- ```ts
28
- import { Controller, Param, Patch, Post as HttpPost } from '@nestjs/common';
29
- import { Can, Roles } from '@dudousxd/nestjs-authz';
30
- import { Post } from './post.entity';
31
-
32
- @Controller('posts')
33
- export class PostController {
34
- @Patch(':id')
35
- @Can('update', Post) // load Post by :id, run PostPolicy.update(currentUser, post)
36
- update(@Param('id') id: string) {}
37
-
38
- @HttpPost()
39
- @Can('create', Post, { classLevel: true }) // run PostPolicy.create(currentUser) — no instance
40
- create() {}
41
-
42
- @HttpPost('purge')
43
- @Roles('admin', 'teacher') // allow if the user holds ANY of these roles
44
- purge() {}
45
- }
46
- ```
47
-
48
- ## Core patterns
49
-
50
- ### Programmatic Gate
51
-
52
- Inject `Gate` for checks inside services. `authorize` throws `ForbiddenException` on deny;
53
- `allows`/`denies` return a boolean.
20
+ ## Routes
54
21
 
55
22
  ```ts
56
- import { Injectable } from '@nestjs/common';
57
- import { Gate } from '@dudousxd/nestjs-authz';
58
-
59
- @Injectable()
60
- export class PostService {
61
- constructor(private readonly gate: Gate) {}
23
+ import { Can } from '@nestjs/authorization';
24
+ import { GatePolicy, PermissionPolicy, Roles, ability, authorizedResource, byId, permission } from '@dudousxd/nestjs-authz';
62
25
 
63
- async update(post: Post) {
64
- await this.gate.authorize('update', post); // throws on deny
65
- if (await this.gate.allows('delete', post)) { /* ... */ }
66
- }
26
+ @Patch(':id')
27
+ @Can(PostPolicy, 'update', byId(Post)) // 403 if the Post is not found
28
+ update(@Req() req: Request) {
29
+ const post = authorizedResource(req, Post); // the record byId loaded
67
30
  }
68
- ```
69
31
 
70
- `forUser(user)` returns a `BoundGate` that bypasses the context accessor — use it when no
71
- nestjs-context is wired, or to check a user other than the current one:
32
+ @Post(':id/publish')
33
+ @Can(PermissionPolicy, 'has', permission('posts.publish')) // or @Permission('posts.publish')
34
+ publish() {}
72
35
 
73
- ```ts
74
- await this.gate.forUser(someUser).authorize('update', post);
75
- ```
36
+ @Get('admin')
37
+ @Roles('admin', 'editor') // any of; RolePolicy.hasAll for all of
38
+ admin() {}
76
39
 
77
- `allowsMany` batch-checks one user in a single pass (resolves the user once, shares a
78
- permission cache — kills the N+1 on a list page). Inside a nestjs-context request, plain
79
- `allows`/`authorize`/`hasRole` calls also share one `resolveUser`, one permission fetch and
80
- one role fetch per user for the whole request (keyed by the row the user names — its class
81
- or `type` plus `id`; a plain object without `type` is keyed by identity, so two users that
82
- only share an `id` never share an entry):
40
+ @Get('reports')
41
+ @Can(GatePolicy, 'allows', ability('access-admin')) // a gate.define() gate
42
+ reports() {}
83
43
 
84
- ```ts
85
- const results = await this.gate.allowsMany([
86
- { ability: 'update', resource: post },
87
- { ability: 'delete', resource: post },
88
- ]); // → [{ ability:'update', resource, allowed }, ...] (order preserved)
44
+ @Get('me')
45
+ @Can.Anyone() // the official way to open a route
46
+ me() {}
89
47
  ```
90
48
 
91
- ### Ad-hoc gates
49
+ Class-level and method-level requirements all apply (AND); a method's `Can.Anyone()` lifts the
50
+ class's. Argument resolvers run in the guard, before pipes, with no DI: `byId` reads the
51
+ `ResourceResolver` `AuthzModule` registered at boot.
92
52
 
93
- Register a model-less, named ability with `define`, then guard or check it:
53
+ ## Services
94
54
 
95
55
  ```ts
96
- this.gate.define('access-admin', (user) => (user as User).role === 'staff');
97
- await this.gate.allows('access-admin'); // programmatic
98
- // @Can('access-admin') // declarative (no resource class)
56
+ await gate.forUser(user).authorize(PostPolicy, 'update', post); // = AuthorizationService.authorize
57
+ await gate.forUser(user).allows('update', post); // policy linked with @PolicyFor(Post)
58
+ await gate.forUser(user).allows('posts.publish'); // persisted permission
59
+ await gate.forUser(user).allowsMany([{ ability: 'update', resource: post }]);
60
+ gate.define('access-admin', (user) => user.isStaff);
99
61
  ```
100
62
 
101
- ### Custom ResourceResolver
102
-
103
- `@Can('update', Post)` needs an instance. The default `IdParamResourceResolver` builds a
104
- `{ id }` shim from the route `:id`. For real entities, implement `ResourceResolver` and
105
- bind `RESOURCE_RESOLVER` (or pass `resourceResolver` to `forRoot`):
106
-
107
- ```ts
108
- import type { ResourceResolver } from '@dudousxd/nestjs-authz';
109
- import { RESOURCE_RESOLVER } from '@dudousxd/nestjs-authz';
110
- import type { ExecutionContext, Type } from '@nestjs/common';
111
-
112
- class OrmResourceResolver implements ResourceResolver {
113
- async resolve(resource: Type<unknown>, ctx: ExecutionContext) {
114
- const req = ctx.switchToHttp().getRequest<{ params: { id: string } }>();
115
- return this.repos.get(resource).findOneBy({ id: Number(req.params.id) });
116
- }
117
- }
118
- // provide: { provide: RESOURCE_RESOLVER, useClass: OrmResourceResolver }
119
- ```
63
+ String abilities ask, in order: `superAdmin` (it receives the resource too), the external
64
+ decision point (`DECISION_PROVIDER`, e.g. the Cerbos adapter — its allow or deny is final,
65
+ `undefined` abstains), the linked policy (official evaluation), the persisted permissions,
66
+ the ad-hoc gate; else `AbilityNotResolvedException`. The policy has the final word: a
67
+ persisted permission named like one of its abilities (`update`) does not bypass it — only
68
+ `superAdmin` short-circuits a policy. `authorize()` throws the official `AuthorizationError`.
69
+
70
+ Without `forUser()`, the user is `@nestjs/authentication`'s `AuthenticationContext.user`
71
+ when that package is installed and its scope is active for the call (its answer is final,
72
+ so an anonymous call there is a guest), else nestjs-context's user ref
73
+ (hydrated once per request by `resolveUser`), else a guest.
74
+
75
+ Inside a nestjs-context request, every check shares one `resolveUser`, one permission fetch
76
+ and one role fetch per user for the whole request — the official guard's `RolePolicy` /
77
+ `PermissionPolicy` and the handler's `Gate` calls alike (keyed by the row the user names —
78
+ its class or `type` plus `id`; a plain object without `type` is keyed by identity, so two
79
+ users that only share an `id` never share an entry). `allowsMany` shares one fetch per
80
+ batch even without nestjs-context, and asks a `DECISION_PROVIDER`'s `decideMany` once.
120
81
 
121
82
  ## Common mistakes
122
83
 
123
- ### 1. Class-level ability without `{ classLevel: true }`
84
+ ### 1. Writing the 0.8 string form
124
85
 
125
- ```ts
126
- // Wrong — the guard tries to LOAD a Post (by :id) for a creation route that has none,
127
- // so the resolver returns undefined and the request is denied.
128
- @HttpPost()
129
- @Can('create', Post)
130
- create() {}
131
-
132
- // Correct — flag it class-level so the guard skips resource loading.
133
- @HttpPost()
134
- @Can('create', Post, { classLevel: true })
135
- create() {}
136
- ```
86
+ 0.9 removed it: `@Can` comes from `@nestjs/authorization` and names the policy.
137
87
 
138
- Mechanism: with a `resource` and no `classLevel`, the guard loads an instance via the
139
- resolver; a `undefined` instance is treated as not-found → deny.
140
- Source: `packages/core/src/guard/can.guard.ts` (`canActivate`), `packages/core/src/decorator/can.decorator.ts`.
88
+ | 0.8 | 0.9 |
89
+ | --- | --- |
90
+ | `@Can('update', Post)` | `@Can(PostPolicy, 'update', byId(Post))` |
91
+ | `@Can('create', Post, { classLevel: true })` | `@Can(PostPolicy, 'create')` |
92
+ | `@Can('access-admin')` | `@Can(GatePolicy, 'allows', ability('access-admin'))` |
141
93
 
142
- ### 2. Using the default resolver and expecting real entity fields
94
+ ### 2. Loading the record again in the handler
143
95
 
144
- ```ts
145
- // Wrong — IdParamResourceResolver only sets `{ id }`; `post.authorId` is undefined,
146
- // so an ownership policy reading post.authorId can never match.
147
- AuthzModule.forRoot({ policies: [PostPolicy] });
96
+ Read it with `authorizedResource(req, Post)`.
148
97
 
149
- // Correct — register an ORM-backed resolver so the policy receives a hydrated row.
150
- AuthzModule.forRoot({ policies: [PostPolicy], resourceResolver: new OrmResourceResolver(...) });
151
- ```
152
-
153
- Mechanism: the default resolver produces a `Object.create(resource.prototype)` shim with
154
- only `id` set (enough for `instance.constructor` matching, not for column reads).
155
- Source: `packages/core/src/resource-resolver.ts` (`IdParamResourceResolver`).
156
-
157
- ### 3. Ambiguous class-level ability across multiple policies
98
+ ### 3. Writing an inline resolver for constant args
158
99
 
159
- ```ts
160
- // Wrong — both PostPolicy and CommentPolicy define create(); with no resource the Gate
161
- // cannot tell which to run and throws AmbiguousAbilityException.
162
- await this.gate.allows('create');
163
-
164
- // Correct — pass the resource class so the Gate selects the right policy.
165
- await this.gate.allows('create', Post); // programmatic
166
- // @Can('create', Post, { classLevel: true }) // declarative
167
- ```
100
+ `@Can(FlagPolicy, 'on', () => ['billing'])` works, but tools cannot read it without a call.
101
+ Use `staticCanArgs('billing')`.
168
102
 
169
- Mechanism: with no resource the Gate scans all policies for the method; >1 match is an
170
- ambiguous (arbitrary) choice, so it throws instead of guessing.
171
- Source: `packages/core/src/gate.ts` (`resolvePolicy`, `AmbiguousAbilityException`).
103
+ Source: `packages/core/src/helpers.ts`, `packages/core/src/gate.ts`, `packages/core/src/decorator/roles.decorator.ts`.