@dudousxd/nestjs-authz 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +279 -0
- package/dist/authz.guard.d.ts +48 -0
- package/dist/authz.guard.d.ts.map +1 -0
- package/dist/authz.guard.js +144 -0
- package/dist/authz.guard.js.map +1 -0
- package/dist/can-endpoint.controller.d.ts +0 -19
- package/dist/can-endpoint.controller.d.ts.map +1 -1
- package/dist/can-endpoint.controller.js +18 -9
- package/dist/can-endpoint.controller.js.map +1 -1
- package/dist/decision-provider.d.ts +25 -16
- package/dist/decision-provider.d.ts.map +1 -1
- package/dist/decision-provider.js +11 -1
- package/dist/decision-provider.js.map +1 -1
- package/dist/decisions.d.ts +24 -0
- package/dist/decisions.d.ts.map +1 -0
- package/dist/decisions.js +87 -0
- package/dist/decisions.js.map +1 -0
- package/dist/decorator/roles.decorator.d.ts +6 -12
- package/dist/decorator/roles.decorator.d.ts.map +1 -1
- package/dist/decorator/roles.decorator.js +13 -15
- package/dist/decorator/roles.decorator.js.map +1 -1
- package/dist/diagnostics.d.ts +15 -5
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +3 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/errors/exceptions.d.ts +0 -8
- package/dist/errors/exceptions.d.ts.map +1 -1
- package/dist/errors/exceptions.js +0 -14
- package/dist/errors/exceptions.js.map +1 -1
- package/dist/gate.d.ts +83 -218
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +209 -611
- package/dist/gate.js.map +1 -1
- package/dist/helpers.d.ts +46 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +89 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +28 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -13
- package/dist/index.js.map +1 -1
- package/dist/links.d.ts +39 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/links.js +57 -0
- package/dist/links.js.map +1 -0
- package/dist/module.d.ts +23 -22
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +129 -158
- package/dist/module.js.map +1 -1
- package/dist/official.d.ts +39 -0
- package/dist/official.d.ts.map +1 -0
- package/dist/official.js +44 -0
- package/dist/official.js.map +1 -0
- package/dist/policies/authz-policy.d.ts +42 -0
- package/dist/policies/authz-policy.d.ts.map +1 -0
- package/dist/policies/authz-policy.js +80 -0
- package/dist/policies/authz-policy.js.map +1 -0
- package/dist/policies/gate.policy.d.ts +18 -0
- package/dist/policies/gate.policy.d.ts.map +1 -0
- package/dist/policies/gate.policy.js +37 -0
- package/dist/policies/gate.policy.js.map +1 -0
- package/dist/policies/index.d.ts +5 -0
- package/dist/policies/index.d.ts.map +1 -0
- package/dist/policies/index.js +5 -0
- package/dist/policies/index.js.map +1 -0
- package/dist/policies/permission.policy.d.ts +27 -0
- package/dist/policies/permission.policy.d.ts.map +1 -0
- package/dist/policies/permission.policy.js +61 -0
- package/dist/policies/permission.policy.js.map +1 -0
- package/dist/policies/role.policy.d.ts +27 -0
- package/dist/policies/role.policy.d.ts.map +1 -0
- package/dist/policies/role.policy.js +51 -0
- package/dist/policies/role.policy.js.map +1 -0
- package/dist/policy-index.d.ts +51 -0
- package/dist/policy-index.d.ts.map +1 -0
- package/dist/policy-index.js +135 -0
- package/dist/policy-index.js.map +1 -0
- package/dist/rbac.d.ts +92 -0
- package/dist/rbac.d.ts.map +1 -0
- package/dist/rbac.js +293 -0
- package/dist/rbac.js.map +1 -0
- package/dist/resource-resolver.d.ts +15 -17
- package/dist/resource-resolver.d.ts.map +1 -1
- package/dist/resource-resolver.js +8 -8
- package/dist/resource-resolver.js.map +1 -1
- package/dist/runtime.d.ts +31 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +40 -0
- package/dist/runtime.js.map +1 -0
- package/dist/tokens.d.ts +4 -8
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +4 -8
- package/dist/tokens.js.map +1 -1
- package/dist/types.d.ts +52 -171
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -11
- package/skills/authz-enforcement/SKILL.md +67 -135
- package/skills/authz-policies/SKILL.md +43 -117
- package/skills/authz-query-scopes/SKILL.md +16 -9
- package/skills/authz-rbac-seams/SKILL.md +29 -12
- package/skills/authz-setup/SKILL.md +45 -106
- package/dist/decorator/can.decorator.d.ts +0 -29
- package/dist/decorator/can.decorator.d.ts.map +0 -1
- package/dist/decorator/can.decorator.js +0 -23
- package/dist/decorator/can.decorator.js.map +0 -1
- package/dist/decorator/policy.decorator.d.ts +0 -18
- package/dist/decorator/policy.decorator.d.ts.map +0 -1
- package/dist/decorator/policy.decorator.js +0 -28
- package/dist/decorator/policy.decorator.js.map +0 -1
- package/dist/guard/can.guard.d.ts +0 -31
- package/dist/guard/can.guard.d.ts.map +0 -1
- package/dist/guard/can.guard.js +0 -92
- package/dist/guard/can.guard.js.map +0 -1
- package/dist/guard/roles.guard.d.ts +0 -21
- package/dist/guard/roles.guard.d.ts.map +0 -1
- package/dist/guard/roles.guard.js +0 -50
- package/dist/guard/roles.guard.js.map +0 -1
- package/dist/policy-registry.d.ts +0 -46
- package/dist/policy-registry.d.ts.map +0 -1
- package/dist/policy-registry.js +0 -101
- 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
|
|
11
|
-
*
|
|
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
|
-
*
|
|
16
|
-
* boolean
|
|
17
|
-
*
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
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
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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
|
-
*
|
|
84
|
-
* `
|
|
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
|
|
87
|
-
/**
|
|
88
|
-
|
|
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
|
-
*
|
|
95
|
-
*
|
|
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
|
-
|
|
55
|
+
canEndpoint?: boolean | string;
|
|
99
56
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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
|
-
*
|
|
141
|
-
*
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D
|
|
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.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
|
@@ -51,28 +52,36 @@
|
|
|
51
52
|
"skills"
|
|
52
53
|
],
|
|
53
54
|
"dependencies": {
|
|
54
|
-
"@dudousxd/nestjs-diagnostics": "0.7.1"
|
|
55
|
+
"@dudousxd/nestjs-diagnostics": ">=0.7.0 <1.0.0"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"@dudousxd/nestjs-context": ">=0.1.0",
|
|
58
|
-
"@nestjs/
|
|
59
|
-
"@nestjs/
|
|
60
|
-
"
|
|
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.
|
|
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": ">=
|
|
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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
14
|
+
library_version: "0.9.0"
|
|
16
15
|
framework: nestjs
|
|
17
16
|
---
|
|
18
17
|
|
|
19
18
|
# Enforcing authorization
|
|
20
19
|
|
|
21
|
-
|
|
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 {
|
|
57
|
-
import {
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
32
|
+
@Post(':id/publish')
|
|
33
|
+
@Can(PermissionPolicy, 'has', permission('posts.publish')) // or @Permission('posts.publish')
|
|
34
|
+
publish() {}
|
|
72
35
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
36
|
+
@Get('admin')
|
|
37
|
+
@Roles('admin', 'editor') // any of; RolePolicy.hasAll for all of
|
|
38
|
+
admin() {}
|
|
76
39
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
## Services
|
|
94
54
|
|
|
95
55
|
```ts
|
|
96
|
-
|
|
97
|
-
await
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
84
|
+
### 1. Writing the 0.8 string form
|
|
124
85
|
|
|
125
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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.
|
|
94
|
+
### 2. Loading the record again in the handler
|
|
143
95
|
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
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`.
|