@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.
- package/CHANGELOG.md +109 -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 +19 -10
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,114 @@
|
|
|
1
1
|
# @dudousxd/nestjs-authz
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Build on NestJS's first-party `@nestjs/authorization` instead of a parallel engine. Requires
|
|
8
|
+
NestJS 12, `@nestjs/authorization` ^0.1.3 and Node 22+.
|
|
9
|
+
|
|
10
|
+
**Use the official API** for policies (`@Policy()`), route checks (`@Can(Policy, 'ability',
|
|
11
|
+
args?)`, `Can.Anyone()`), the global deny-by-default `AuthorizationGuard`, the current user
|
|
12
|
+
(`getUser`) and `AuthorizationService`. Import `AuthorizationModule.forRoot()` next to
|
|
13
|
+
`AuthzModule.forRoot()`.
|
|
14
|
+
|
|
15
|
+
**Added on top:**
|
|
16
|
+
|
|
17
|
+
- `RolePolicy` (`has`, `hasAll`) and `PermissionPolicy` (`has`, `hasAll`, wildcards) as
|
|
18
|
+
official policies over the RBAC seams; `GatePolicy` (`allows`) for `gate.define()` gates.
|
|
19
|
+
- `@Can()` helpers: `byId(Resource, { param? })` (loads the record with the `ResourceResolver`;
|
|
20
|
+
`authorizedResource(req, Resource)` reads it back), `roles()`, `permission()` /
|
|
21
|
+
`permissions()`, `ability()`, `staticCanArgs()`. Static resolvers carry their args on
|
|
22
|
+
`Symbol.for('@dudousxd/static-can-args')`.
|
|
23
|
+
- `@Roles(...)` and `@Permission(...)` sugar that writes official `@Can` metadata.
|
|
24
|
+
- `@PolicyFor(Resource)`: links a resource to its official policy (and applies `@Policy()`).
|
|
25
|
+
- `AuthzPolicy` base class, `superAdminBefore()` and `decisionProviderBefore()`: the
|
|
26
|
+
`superAdmin` hook and the `DECISION_PROVIDER` (e.g. the Cerbos adapter at
|
|
27
|
+
`@dudousxd/nestjs-authz/cerbos`) for official policies. `superAdmin` receives the resource.
|
|
28
|
+
`RolePolicy` / `PermissionPolicy` ask only `superAdmin`.
|
|
29
|
+
- `Gate.allows/denies/authorize(Policy, ability, ...args)` (delegates to `AuthorizationService`)
|
|
30
|
+
next to the string form; `allowsMany` also takes `{ policy, ability, args }`.
|
|
31
|
+
- The `authz:decision` channel now also carries the official guard's decisions (allowed and
|
|
32
|
+
denied, `reason: 'route'`, with `policy` and `handler`).
|
|
33
|
+
- `PolicyIndex`, `routeRequirements()`, `readCanRequirements()`, `isOfficialPolicy()`,
|
|
34
|
+
`currentUserOf()`.
|
|
35
|
+
- The per-request cache (nestjs-context store) now spans the official guard and the handler:
|
|
36
|
+
`RolePolicy` / `PermissionPolicy` and `Gate` calls share one `getPermissions`, one
|
|
37
|
+
`getRoles` and one `resolveUser` per user per request.
|
|
38
|
+
- `Gate` calls without `forUser()` read `@nestjs/authentication`'s `AuthenticationContext.user`
|
|
39
|
+
(optional peer, resolved through `ModuleRef`). Inside its scope (a call it handles, or
|
|
40
|
+
`AuthenticationContext.run()`) its answer is final, so an anonymous call is a guest;
|
|
41
|
+
nestjs-context's user ref is read only when there is no authentication scope at all.
|
|
42
|
+
- A migration mode, `AuthzModule.forRoot({ unannotated: 'allow' })` (`forRootAsync`: next to
|
|
43
|
+
`useFactory`), for apps that cannot annotate every route at once. With
|
|
44
|
+
`AuthorizationModule.forRoot({ globalGuard: false })`, it registers a global `AuthzGuard` that
|
|
45
|
+
hands every route with `@Can()` requirements to the official `AuthorizationGuard` (same user,
|
|
46
|
+
401/403 and `denied` event) and lets unannotated routes through. The startup fails if the
|
|
47
|
+
official global guard is still on. Default `'deny'`: nothing is registered. `AuthzGuard` is
|
|
48
|
+
exported for `@UseGuards()`.
|
|
49
|
+
- `decorators?: ClassDecorator[]`: class decorators applied to every controller the module
|
|
50
|
+
mounts (the `POST /authz/can` endpoint), which are open to the official guard by default
|
|
51
|
+
(`authorization:can-anyone`). `canEndpoint` and `decorators` are static options.
|
|
52
|
+
|
|
53
|
+
**Removed, with no compatibility shim** (move each to the official form): this package's
|
|
54
|
+
`Can` and `Policy` exports and `CanOptions`. The string `@Can('update', Post)` →
|
|
55
|
+
`@Can(PostPolicy, 'update', byId(Post))` (`Can` from `@nestjs/authorization`);
|
|
56
|
+
`@Can('create', Post, { classLevel: true })` → `@Can(PostPolicy, 'create')`;
|
|
57
|
+
`@Can('access-admin')` → `@Can(GatePolicy, 'allows', ability('access-admin'))`;
|
|
58
|
+
`@Policy(Post)` → official `@Policy()` + `@PolicyFor(Post)` (or `@PolicyFor(Post)` alone).
|
|
59
|
+
|
|
60
|
+
**Also removed:** `CanGuard`, `RolesGuard`, `PolicyRegistry` (use `PolicyIndex`), `CAN_METADATA`,
|
|
61
|
+
`ROLES_METADATA`, the `policies` and `after` options, deny messages on policy results
|
|
62
|
+
(`PolicyResponse` / `PolicyResult` remain only as `DecisionProvider` verdicts),
|
|
63
|
+
`PolicyNotDecoratedException`, `ResourceResolverMissingException`, `AfterHook`,
|
|
64
|
+
`PolicyMethod`, `PolicyBeforeHook`, `PolicyInstance`.
|
|
65
|
+
|
|
66
|
+
**The policy has the final word:** string abilities now ask `superAdmin` →
|
|
67
|
+
`DECISION_PROVIDER` → the linked policy → the persisted permissions → the ad-hoc gate. A
|
|
68
|
+
persisted permission named like a policy ability (`update`) no longer bypasses the policy, in
|
|
69
|
+
checks and in `gate.scope()`; only `superAdmin` does. Permissions still grant named abilities
|
|
70
|
+
no policy defines (`posts.publish`).
|
|
71
|
+
|
|
72
|
+
**Behavior changes from the official guard:** routes with no `@Can()` are denied (add
|
|
73
|
+
`@Can.Anyone()`, or migrate with `unannotated: 'allow'`); class- and method-level requirements add up; guests reach policies as
|
|
74
|
+
`null` and are answered 401; only a literal `true` allows; `superAdmin` applies to policies
|
|
75
|
+
that extend `AuthzPolicy` (and to the built-in ones, string `Gate` checks and `gate.scope`).
|
|
76
|
+
`gate.hasRole()` stays a plain fact (no `superAdmin`); `RolePolicy` honors it. A
|
|
77
|
+
`DecisionProvider` and `superAdmin` see a guest as `null` (was `undefined`).
|
|
78
|
+
|
|
79
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `@PolicyFor(Resource)` now throws when `Resource` is already linked to a different policy,
|
|
80
|
+
instead of silently replacing the process-wide link. Linking the same pair again stays a
|
|
81
|
+
no-op, and a re-declared class of the same name (a test helper declaring its policy per
|
|
82
|
+
call, a hot reload) still replaces the link.
|
|
83
|
+
|
|
84
|
+
### Patch Changes
|
|
85
|
+
|
|
86
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Fix packages that could be published without files their `exports` point to. `tsc` builds no longer use a `.tsbuildinfo` that can outlive `dist/` and skip emitting files. A new `check:exports` step (also `prepack`) fails the release if any `exports`/`main`/`types` path is missing.
|
|
87
|
+
|
|
88
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Document that a `DecisionProvider` deny `message` no longer reaches the 403 in 0.9. The official
|
|
89
|
+
guard answers `{ statusCode: 403, message: 'Forbidden' }`, and `@nestjs/authorization` has no
|
|
90
|
+
deny-with-reason. Added to the 0.8 → 0.9 migration table, with the workaround and what it costs.
|
|
91
|
+
`PolicyResponse`'s doc comment no longer claims the message is published on the decision event.
|
|
92
|
+
|
|
93
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - A `Gate` string check that reaches a linked `AuthzPolicy` now asks `superAdmin` and the
|
|
94
|
+
`DECISION_PROVIDER` once, not again in the policy's `before()`. Same decision order and verdicts.
|
|
95
|
+
|
|
96
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Importing `AuthzModule` without `@nestjs/authorization`'s `AuthorizationModule` now fails the boot
|
|
97
|
+
with an error naming the missing `AuthorizationModule.forRoot()` import, instead of Nest's
|
|
98
|
+
unresolvable-dependency error on `Gate` / `AuthzGuard`.
|
|
99
|
+
|
|
100
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `PolicyIndex` no longer freezes its first discovery scan: it rescans when a module or provider
|
|
101
|
+
is added (a lazy module) or a policy was not instantiated yet, so `resourcePolicies()`,
|
|
102
|
+
`classAbilities()` (the Inertia ability map) and `gate.scope()` see the same policies.
|
|
103
|
+
|
|
104
|
+
- [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `unannotated: 'allow'`: explain the official boot error. `@nestjs/authorization` 0.1.3 recognizes only its
|
|
105
|
+
own `AuthorizationGuard` when it checks at startup that each `@Can()` is enforced, so with
|
|
106
|
+
`globalGuard: false` it logs "`@Can() is not enforced on …`" for the routes the global `AuthzGuard`
|
|
107
|
+
enforces. `AuthzModule` now logs a line right after it that names the same handlers and says the
|
|
108
|
+
error is expected in this mode. `AuthzGuard` keeps no official brand on purpose: with it the
|
|
109
|
+
official check would report every route this mode lets through as denied. Documented in the
|
|
110
|
+
README's migration section.
|
|
111
|
+
|
|
3
112
|
## 0.8.1
|
|
4
113
|
|
|
5
114
|
### Patch Changes
|
package/README.md
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://davidecarvalho.github.io/aviary/docs/authz">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/DavideCarvalho/nestjs-authz/main/.github/banner.svg" alt="@dudousxd/nestjs-authz — an Aviary library. Call sign: Falcon.">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b><a href="https://davidecarvalho.github.io/aviary/docs/authz">📖 Read the documentation</a></b>
|
|
9
|
+
· part of the <a href="https://davidecarvalho.github.io/aviary/"><b>Aviary</b></a> ecosystem for NestJS
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# `@dudousxd/nestjs-authz`
|
|
15
|
+
|
|
16
|
+
Extras on top of NestJS's first-party [`@nestjs/authorization`](https://docs.nestjs.com/security/authorization).
|
|
17
|
+
Policies, `@Can()`, the global deny-by-default guard and `AuthorizationService` come from the
|
|
18
|
+
official package: read its docs first. This package adds what it does not have:
|
|
19
|
+
|
|
20
|
+
- **Persisted RBAC as official policies**: `RolePolicy` and `PermissionPolicy` (wildcards,
|
|
21
|
+
tenant-scoped roles, direct permissions, role sources), backed by the TypeORM / MikroORM /
|
|
22
|
+
Prisma / Drizzle adapters, fetched once per request.
|
|
23
|
+
- **Helpers for the official `@Can()`**: `byId(Post)`, `roles(...)`, `permission(...)`,
|
|
24
|
+
`ability(...)`, `staticCanArgs(...)`; sugar `@Roles(...)` / `@Permission(...)`.
|
|
25
|
+
- **A global super-admin** and an **external policy engine** (Cerbos, OPA, …): extend
|
|
26
|
+
`AuthzPolicy`.
|
|
27
|
+
- **A `Gate`** for services: string abilities, batches (`allowsMany`), ad-hoc gates
|
|
28
|
+
(`define`), roles, query scopes (`scope` + the adapters' `applyScope`).
|
|
29
|
+
- **Frontend and tooling**: Inertia props, a typed `can()` from codegen, client/React
|
|
30
|
+
helpers, test helpers, and an allow + deny decision event for Telescope.
|
|
31
|
+
|
|
32
|
+
See [`DESIGN.md`](https://github.com/DavideCarvalho/nestjs-authz/blob/main/DESIGN.md) for the design and the trade-offs.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pnpm add @nestjs/authorization @dudousxd/nestjs-authz
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires NestJS 12 and Node 22+. Two optional peers: `@nestjs/authentication` gives the `Gate`
|
|
41
|
+
its ambient user (`AuthenticationContext.user`); `@dudousxd/nestjs-context` gives it one too
|
|
42
|
+
(outside an authentication scope), plus the per-request permission cache and
|
|
43
|
+
tenant-scoped roles.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
@Module({
|
|
47
|
+
imports: [
|
|
48
|
+
AuthorizationModule.forRoot(), // official: guard, @Can, AuthorizationService
|
|
49
|
+
AuthzModule.forRoot({ superAdmin: (user) => user?.isRoot === true }),
|
|
50
|
+
],
|
|
51
|
+
providers: [PostPolicy],
|
|
52
|
+
})
|
|
53
|
+
export class AppModule {}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Policies (official) + `@PolicyFor`
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { Can, Policy } from '@nestjs/authorization';
|
|
60
|
+
import { AuthzPolicy, PolicyFor, authorizedResource, byId } from '@dudousxd/nestjs-authz';
|
|
61
|
+
|
|
62
|
+
@Policy()
|
|
63
|
+
@PolicyFor(Post) // optional: links Post → PostPolicy for the extras
|
|
64
|
+
export class PostPolicy extends AuthzPolicy { // optional: honors `superAdmin`
|
|
65
|
+
update(user: User | null, post: Post) {
|
|
66
|
+
return post.authorId === user?.id;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Controller('posts')
|
|
71
|
+
export class PostController {
|
|
72
|
+
@Patch(':id')
|
|
73
|
+
@Can(PostPolicy, 'update', byId(Post)) // byId loads the Post named by :id
|
|
74
|
+
update(@Req() req: Request) {
|
|
75
|
+
const post = authorizedResource(req, Post); // the record byId loaded, no second query
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`byId` uses the `ResourceResolver` given to `AuthzModule.forRoot({ resourceResolver })`
|
|
81
|
+
(default: an `{ id }` object from the route param; `idParam` renames it, `byId(Post, { param })`
|
|
82
|
+
per route). A record that is not found is a 403 before the policy runs.
|
|
83
|
+
|
|
84
|
+
`@PolicyFor(Post)` is what lets the extras go from a resource to its policy:
|
|
85
|
+
`gate.allows('update', post)`, `gate.scope(Post)`, the client ability map. The official API
|
|
86
|
+
never needs it, and `@PolicyFor` applies the official `@Policy()` itself.
|
|
87
|
+
|
|
88
|
+
## Roles and permissions
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
@Roles('admin', 'editor') // any of
|
|
92
|
+
@Can(RolePolicy, 'hasAll', roles('admin', 'editor')) // all of
|
|
93
|
+
@Permission('posts.publish') // any of; posts.* grants it
|
|
94
|
+
@Can(PermissionPolicy, 'hasAll', permission('posts.publish', 'posts.delete'))
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Roles come from `user.roles` / `user.role` (or `resolveRoles`) and from the RBAC adapter's role
|
|
98
|
+
store; permissions from the adapter:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
AuthzRbacModule.forRootAsync({
|
|
102
|
+
inject: [DataSource],
|
|
103
|
+
useFactory: (ds: DataSource) => ({ store: new TypeOrmAuthzStore(ds) }),
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Within one request (a nestjs-context store), a user's permissions and the adapter's roles are
|
|
108
|
+
fetched once, however many checks run: the official guard's `RolePolicy` / `PermissionPolicy`
|
|
109
|
+
and the handler's `Gate` calls share the fetch. The entry is keyed by the row the user names
|
|
110
|
+
(its class or `type`, plus `id`); a plain object without `type` is keyed by identity. A grant
|
|
111
|
+
made mid-request applies from the next request. Adapters set `exhaustivePermissions`, so a
|
|
112
|
+
permission the set lacks is answered without another query.
|
|
113
|
+
|
|
114
|
+
`roles()`, `permission()`, `ability()` and `staticCanArgs()` return resolvers whose arguments do
|
|
115
|
+
not depend on the call. They carry them on a non-enumerable `Symbol.for('@dudousxd/static-can-args')`
|
|
116
|
+
property, so tools such as the Aviary agent can evaluate a requirement without a request.
|
|
117
|
+
|
|
118
|
+
## Super-admin
|
|
119
|
+
|
|
120
|
+
The official guard runs only each policy's own `before()`. Extend `AuthzPolicy` (or call
|
|
121
|
+
`superAdminBefore(user, ability, resource)` from your `before()`) and the `superAdmin` hook
|
|
122
|
+
grants every ability of that policy. The hook receives the resource (the ability's first
|
|
123
|
+
argument), so it can scope the bypass. `RolePolicy` and `PermissionPolicy` extend it.
|
|
124
|
+
|
|
125
|
+
## The `Gate`
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
constructor(private readonly gate: Gate) {}
|
|
129
|
+
|
|
130
|
+
await this.gate.forUser(user).authorize(PostPolicy, 'update', post); // = AuthorizationService.authorize
|
|
131
|
+
await this.gate.forUser(user).allows('update', post); // the policy linked to Post
|
|
132
|
+
await this.gate.forUser(user).allows('posts.publish'); // a persisted permission
|
|
133
|
+
|
|
134
|
+
this.gate.define('access-admin', (user) => user.isStaff);
|
|
135
|
+
// on a route: @Can(GatePolicy, 'allows', ability('access-admin'))
|
|
136
|
+
|
|
137
|
+
const results = await this.gate.forUser(user).allowsMany([
|
|
138
|
+
{ ability: 'update', resource: post },
|
|
139
|
+
{ policy: ReportPolicy, ability: 'export', args: ['csv'] },
|
|
140
|
+
]); // one permission fetch
|
|
141
|
+
|
|
142
|
+
const where = await this.gate.forUser(user).scope(Post); // from PostPolicy.scope(user)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
String abilities ask `superAdmin`, then the external decision point (below), then the policy
|
|
146
|
+
linked to the resource, then the persisted permissions, then the ad-hoc gate. **The policy has
|
|
147
|
+
the final word**: holding a permission named like one of its abilities (`update`) does not
|
|
148
|
+
bypass it; only `superAdmin` does. Permissions grant the named abilities no policy defines
|
|
149
|
+
(`posts.publish`). The same goes for `scope()`: a permission scopes an entity only when no
|
|
150
|
+
policy method can.
|
|
151
|
+
|
|
152
|
+
Policy decisions are always the official service's. `authorize()` throws the official
|
|
153
|
+
`AuthorizationError`, which the official module turns into a 401/403.
|
|
154
|
+
|
|
155
|
+
Without `forUser`, the user is, in order:
|
|
156
|
+
|
|
157
|
+
1. `@nestjs/authentication`'s `AuthenticationContext.user`, when that optional peer is
|
|
158
|
+
installed and its module imported (found through Nest's `ModuleRef`), and its scope is active:
|
|
159
|
+
a call it handles, or `AuthenticationContext.run()`. Its answer is final there: `null` is a
|
|
160
|
+
guest, and nestjs-context is not asked. It is `AsyncLocalStorage`-backed, correct in handlers
|
|
161
|
+
and everything they await;
|
|
162
|
+
2. outside any authentication scope (a queue worker, a cron job, or without the package),
|
|
163
|
+
nestjs-context's user ref, hydrated by `resolveUser` (once per request) when configured;
|
|
164
|
+
3. a guest (`null`).
|
|
165
|
+
|
|
166
|
+
## Decisions
|
|
167
|
+
|
|
168
|
+
Every `Gate` check and every requirement the official guard enforced is published, allowed or
|
|
169
|
+
denied, on the `aviary:authz:decision` diagnostics channel (`authzDecisionChannel`). The official
|
|
170
|
+
package publishes denials only, on `nestjs:authorization:denied`.
|
|
171
|
+
|
|
172
|
+
## Migrating from 0.8
|
|
173
|
+
|
|
174
|
+
0.8 and earlier had their own engine. 0.9 runs on `@nestjs/authorization`, and the old
|
|
175
|
+
decorators are gone: move each one to its official form.
|
|
176
|
+
|
|
177
|
+
| 0.8 | 0.9 |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| `@Policy(Post)` | `@Policy()` from `@nestjs/authorization` + `@PolicyFor(Post)` (or `@PolicyFor(Post)` alone) |
|
|
180
|
+
| `@Can('update', Post)` | `@Can(PostPolicy, 'update', byId(Post))`, `@Can` from `@nestjs/authorization` |
|
|
181
|
+
| `@Can('create', Post, { classLevel: true })` | `@Can(PostPolicy, 'create')` |
|
|
182
|
+
| `@Can('access-admin')` | `@Can(GatePolicy, 'allows', ability('access-admin'))` |
|
|
183
|
+
| `@Roles('admin')` + `RolesGuard` | `@Roles('admin')`, enforced by the official guard |
|
|
184
|
+
| `CanGuard`, `RolesGuard`, `PolicyRegistry` | removed: the official `AuthorizationGuard`; `PolicyIndex` |
|
|
185
|
+
| `AuthzModule.forRoot({ policies })` | register policies as providers (or `AuthorizationModule.forRoot({ policies })`) |
|
|
186
|
+
| `superAdmin` applied everywhere | applied by `AuthzPolicy` subclasses, the built-in policies and the `Gate` |
|
|
187
|
+
| `after` hook, `{ allowed, message }` results | removed: only a literal `true` allows |
|
|
188
|
+
| current user from nestjs-context on routes | the official `getUser` (default `request.user`) |
|
|
189
|
+
| `gate.allows()` user from nestjs-context | `AuthenticationContext.user` inside its scope (`null` there is a guest), nestjs-context outside it |
|
|
190
|
+
| a persisted permission named like a policy ability grants it | the policy decides (only `superAdmin` bypasses it), in checks and in `scope()` |
|
|
191
|
+
| `DecisionProvider` / `superAdmin` see a guest as `undefined` | `null` |
|
|
192
|
+
| `DecisionProvider` asked by the Gate on every check | asked by `AuthzPolicy.before()` on routes, and by `Gate` string abilities |
|
|
193
|
+
| `import … from '@dudousxd/nestjs-authz-inertia/client'` | `import … from '@dudousxd/nestjs-authz-client'` (the subpath is removed) |
|
|
194
|
+
|
|
195
|
+
Behavior the official guard brings:
|
|
196
|
+
|
|
197
|
+
- **Deny by default.** A handler with no `@Can()` is denied; add `@Can.Anyone()` (or
|
|
198
|
+
`@Public()` from `@nestjs/authentication`) where anyone may call it.
|
|
199
|
+
- **Class and method requirements add up.** A class-level `@Roles` no longer gives way to the
|
|
200
|
+
method's.
|
|
201
|
+
- **Guests reach the policy as `null`.** Type the user `User | null`; a guest denial is a 401.
|
|
202
|
+
|
|
203
|
+
### Migrating one controller at a time: `unannotated: 'allow'`
|
|
204
|
+
|
|
205
|
+
The official guard denies every route that declares no `@Can()`: the app's public routes, and
|
|
206
|
+
every controller another library mounts. 0.8 let them through. Turning authorization on for a
|
|
207
|
+
large app in one go is rarely possible, so this package has a migration mode (the official guard
|
|
208
|
+
has no such option):
|
|
209
|
+
|
|
210
|
+
1. **Turn the official global guard off**, and switch the migration mode on:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
imports: [
|
|
214
|
+
AuthenticationModule.forRoot({ ... }), // or your Passport guard, first
|
|
215
|
+
AuthorizationModule.forRoot({ globalGuard: false }),
|
|
216
|
+
AuthzModule.forRoot({ unannotated: 'allow' }), // forRootAsync: next to useFactory
|
|
217
|
+
]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`AuthzModule` registers its own global `AuthzGuard`. A route with official `@Can()`
|
|
221
|
+
requirements (class or method, `@Roles`, `@Permission`) is handed
|
|
222
|
+
to the official `AuthorizationGuard` as is: same `getUser`, same policies, same 401 for a guest
|
|
223
|
+
and 403 for a user, same `denied` event. A route with no requirement is let through. The app
|
|
224
|
+
fails to start if the official global guard is still on, because it would keep denying them.
|
|
225
|
+
|
|
226
|
+
2. **Drop the 0.8 guard and move the old decorators.** Remove `RolesGuard` / `CanGuard` from
|
|
227
|
+
`APP_GUARD` and `@UseGuards()`, and rewrite each string `@Can` / `@Policy(Post)` with the table
|
|
228
|
+
above (`@Can('page.documents')` → `@Can(GatePolicy, 'allows', ability('page.documents'))`).
|
|
229
|
+
`@Roles('admin')` and `@Permission(...)` are unchanged. All of them are enforced by `AuthzGuard`.
|
|
230
|
+
|
|
231
|
+
3. **Annotate the open routes**, one controller at a time: `@Can.Anyone()` where anyone may call
|
|
232
|
+
it, `@Public()` (from `@nestjs/authentication`) where no sign-in is needed, a `@Can(...)`
|
|
233
|
+
everywhere else.
|
|
234
|
+
|
|
235
|
+
4. **Switch to deny by default** when every route declares something: remove `unannotated` (or set
|
|
236
|
+
`'deny'`) and `globalGuard: false`. The official guard is global again, and a forgotten
|
|
237
|
+
`@Can()` is a 403 instead of an open route.
|
|
238
|
+
|
|
239
|
+
A route that slipped through in step 3 is open in this mode, so keep it for the migration only.
|
|
240
|
+
|
|
241
|
+
## External policy engines (Cerbos, OPA, …)
|
|
242
|
+
|
|
243
|
+
Register a `DecisionProvider` under `DECISION_PROVIDER` to hand decisions to an external policy
|
|
244
|
+
decision point. Unlike the grant-only RBAC seam, it can **allow or deny** a resource-aware check.
|
|
245
|
+
Returning `undefined` abstains. The provider can also batch `gate.allowsMany`, and it can map the
|
|
246
|
+
engine's query plan onto `gate.scope()`. It runs right after the `superAdmin` hook:
|
|
247
|
+
|
|
248
|
+
- on routes, in the `before()` of every policy that extends `AuthzPolicy`, with the ability and
|
|
249
|
+
its first argument (`byId(Post)`'s record). `RolePolicy` and `PermissionPolicy` do not ask it:
|
|
250
|
+
their abilities name roles and permissions, not resources. Call `decisionProviderBefore()`
|
|
251
|
+
from your own `before()` when a policy cannot extend `AuthzPolicy`;
|
|
252
|
+
- in the `Gate`, before the policy, the permissions and the ad-hoc gate of a string ability.
|
|
253
|
+
`allowsMany` asks `decideMany` once for the batch.
|
|
254
|
+
|
|
255
|
+
A guest reaches it as `null`. A Cerbos adapter ships in the box as a subpath with no extra
|
|
256
|
+
dependency; bring your own `@cerbos/http` or `@cerbos/grpc` client:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { DECISION_PROVIDER } from '@dudousxd/nestjs-authz';
|
|
260
|
+
import { CerbosDecisionProvider } from '@dudousxd/nestjs-authz/cerbos';
|
|
261
|
+
import { HTTP } from '@cerbos/http';
|
|
262
|
+
|
|
263
|
+
providers: [{
|
|
264
|
+
provide: DECISION_PROVIDER,
|
|
265
|
+
useValue: new CerbosDecisionProvider({
|
|
266
|
+
client: new HTTP(process.env.CERBOS_URL!), // or (user) => per-tenant client | undefined
|
|
267
|
+
principal: (u) => u && { id: u.id, roles: u.roles, attr: { tenantId: u.tenantId } },
|
|
268
|
+
resource: (ability, r) => (r instanceof Post ? { kind: 'app:post', id: r.id, attr: { ownerId: r.ownerId } } : undefined),
|
|
269
|
+
scope: { resource: (entity) => (entity === Post ? { kind: 'app:post' } : undefined) },
|
|
270
|
+
}),
|
|
271
|
+
}]
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The adapter fails **closed** by default. An unreachable PDP denies, and an unsupported query plan
|
|
275
|
+
scopes to no rows. Set `onFailure: 'abstain'` to fall back to your own policies instead.
|
|
276
|
+
|
|
277
|
+
## License
|
|
278
|
+
|
|
279
|
+
MIT © Davi Carvalho
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AuthorizationGuard } from '@nestjs/authorization';
|
|
2
|
+
import { type CanActivate, type ExecutionContext, type OnApplicationBootstrap, type OnModuleInit } from '@nestjs/common';
|
|
3
|
+
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
|
|
4
|
+
/**
|
|
5
|
+
* The migration guard `AuthzModule.forRoot({ unannotated: 'allow' })` registers globally.
|
|
6
|
+
*
|
|
7
|
+
* A route with official `@Can()` requirements (on the class or the method, including
|
|
8
|
+
* `@Roles` and `@Permission`, which write the same metadata) is
|
|
9
|
+
* handed to the official `AuthorizationGuard` as is: the same evaluator, the same `getUser`,
|
|
10
|
+
* the same 401/403, the same denied event. A route that declares no requirement is let
|
|
11
|
+
* through, where the official guard would deny it. That lets an app turn authorization on
|
|
12
|
+
* one controller at a time.
|
|
13
|
+
*
|
|
14
|
+
* It needs `AuthorizationModule.forRoot({ globalGuard: false })`: the official global guard
|
|
15
|
+
* would still deny the unannotated routes. The application fails to start when both run.
|
|
16
|
+
*
|
|
17
|
+
* It deliberately does not carry the official guard's brand. `@nestjs/authorization` 0.1.3
|
|
18
|
+
* recognizes a guard that enforces `@Can()` only by that brand, and with it every route this
|
|
19
|
+
* guard lets through would be logged as "AuthorizationGuard denies every call to …". Without it,
|
|
20
|
+
* the official module logs "@Can() is not enforced on …" for the routes this guard does enforce;
|
|
21
|
+
* {@link AuthzGuardStartupCheck} explains that line right after it.
|
|
22
|
+
*/
|
|
23
|
+
export declare class AuthzGuard implements CanActivate {
|
|
24
|
+
private readonly official;
|
|
25
|
+
constructor(official?: AuthorizationGuard);
|
|
26
|
+
canActivate(context: ExecutionContext): Promise<boolean> | boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* With `unannotated: 'allow'`:
|
|
30
|
+
*
|
|
31
|
+
* - fails the startup when the official module still registers its own global guard: that guard
|
|
32
|
+
* runs too, and denies every unannotated route, so the option would do nothing;
|
|
33
|
+
* - after the official module's startup check, explains its "@Can() is not enforced on …" error.
|
|
34
|
+
* That check sees only `AuthorizationGuard` (by its brand), not the global `AuthzGuard` that
|
|
35
|
+
* enforces those routes, so it reports them at every boot in this mode.
|
|
36
|
+
*/
|
|
37
|
+
export declare class AuthzGuardStartupCheck implements OnModuleInit, OnApplicationBootstrap {
|
|
38
|
+
private readonly discovery;
|
|
39
|
+
private readonly metadataScanner;
|
|
40
|
+
private readonly logger;
|
|
41
|
+
constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner);
|
|
42
|
+
/** Runs after every `onModuleInit()`, so after the official error it explains. */
|
|
43
|
+
onApplicationBootstrap(): void;
|
|
44
|
+
/** Handlers with a `@Can()` requirement and no official guard of their own: AuthzGuard's alone. */
|
|
45
|
+
private routesOnlyAuthzGuardEnforces;
|
|
46
|
+
onModuleInit(): void;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=authz.guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authz.guard.d.ts","sourceRoot":"","sources":["../src/authz.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAIrB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EAGlB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAMjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,UAAW,YAAW,WAAW;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAEM,QAAQ,CAAC,EAAE,kBAAkB;IAIjF,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO;CAInE;AAuBD;;;;;;;;GAQG;AACH,qBACa,sBAAuB,YAAW,YAAY,EAAE,sBAAsB;IAI/E,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAJlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;gBAGjC,SAAS,EAAE,gBAAgB,EAC3B,eAAe,EAAE,eAAe;IAGnD,kFAAkF;IAClF,sBAAsB,IAAI,IAAI;IAc9B,mGAAmG;IACnG,OAAO,CAAC,4BAA4B;IAuBpC,YAAY,IAAI,IAAI;CAgBrB"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
|
+
};
|
|
10
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
|
+
};
|
|
13
|
+
import { AuthorizationGuard } from '@nestjs/authorization';
|
|
14
|
+
import { Inject, Injectable, Logger, Optional, } from '@nestjs/common';
|
|
15
|
+
import { GUARDS_METADATA } from '@nestjs/common/constants.js';
|
|
16
|
+
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
|
|
17
|
+
import { requireOfficial, routeRequirements } from './official.js';
|
|
18
|
+
/** The static brand `@nestjs/authorization` puts on `AuthorizationGuard` (and its subclasses). */
|
|
19
|
+
const OFFICIAL_GUARD_BRAND = Symbol.for('@nestjs/authorization:guard');
|
|
20
|
+
/**
|
|
21
|
+
* The migration guard `AuthzModule.forRoot({ unannotated: 'allow' })` registers globally.
|
|
22
|
+
*
|
|
23
|
+
* A route with official `@Can()` requirements (on the class or the method, including
|
|
24
|
+
* `@Roles` and `@Permission`, which write the same metadata) is
|
|
25
|
+
* handed to the official `AuthorizationGuard` as is: the same evaluator, the same `getUser`,
|
|
26
|
+
* the same 401/403, the same denied event. A route that declares no requirement is let
|
|
27
|
+
* through, where the official guard would deny it. That lets an app turn authorization on
|
|
28
|
+
* one controller at a time.
|
|
29
|
+
*
|
|
30
|
+
* It needs `AuthorizationModule.forRoot({ globalGuard: false })`: the official global guard
|
|
31
|
+
* would still deny the unannotated routes. The application fails to start when both run.
|
|
32
|
+
*
|
|
33
|
+
* It deliberately does not carry the official guard's brand. `@nestjs/authorization` 0.1.3
|
|
34
|
+
* recognizes a guard that enforces `@Can()` only by that brand, and with it every route this
|
|
35
|
+
* guard lets through would be logged as "AuthorizationGuard denies every call to …". Without it,
|
|
36
|
+
* the official module logs "@Can() is not enforced on …" for the routes this guard does enforce;
|
|
37
|
+
* {@link AuthzGuardStartupCheck} explains that line right after it.
|
|
38
|
+
*/
|
|
39
|
+
let AuthzGuard = class AuthzGuard {
|
|
40
|
+
official;
|
|
41
|
+
constructor(official) {
|
|
42
|
+
this.official = requireOfficial(official, 'AuthorizationGuard', 'AuthzGuard');
|
|
43
|
+
}
|
|
44
|
+
canActivate(context) {
|
|
45
|
+
if (routeRequirements(context.getClass(), context.getHandler()).length === 0)
|
|
46
|
+
return true;
|
|
47
|
+
return this.official.canActivate(context);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
AuthzGuard = __decorate([
|
|
51
|
+
Injectable(),
|
|
52
|
+
__param(0, Optional()),
|
|
53
|
+
__param(0, Inject(AuthorizationGuard)),
|
|
54
|
+
__metadata("design:paramtypes", [AuthorizationGuard])
|
|
55
|
+
], AuthzGuard);
|
|
56
|
+
export { AuthzGuard };
|
|
57
|
+
/** True when `value` is the official `AuthorizationGuard` (or a subclass of it). */
|
|
58
|
+
function isOfficialGuard(value) {
|
|
59
|
+
const type = value?.constructor;
|
|
60
|
+
return typeof type === 'function' && type[OFFICIAL_GUARD_BRAND] === true;
|
|
61
|
+
}
|
|
62
|
+
/** True when `guard` (a class from `@UseGuards()`, or an instance) carries the official brand. */
|
|
63
|
+
function isBrandedGuard(guard) {
|
|
64
|
+
const type = typeof guard === 'function' ? guard : guard?.constructor;
|
|
65
|
+
return (typeof type === 'function' &&
|
|
66
|
+
Boolean(type[OFFICIAL_GUARD_BRAND]));
|
|
67
|
+
}
|
|
68
|
+
const list = (wheres) => wheres.length > 3
|
|
69
|
+
? `${wheres.slice(0, 3).join(', ')} and ${wheres.length - 3} more`
|
|
70
|
+
: wheres.join(', ');
|
|
71
|
+
/**
|
|
72
|
+
* With `unannotated: 'allow'`:
|
|
73
|
+
*
|
|
74
|
+
* - fails the startup when the official module still registers its own global guard: that guard
|
|
75
|
+
* runs too, and denies every unannotated route, so the option would do nothing;
|
|
76
|
+
* - after the official module's startup check, explains its "@Can() is not enforced on …" error.
|
|
77
|
+
* That check sees only `AuthorizationGuard` (by its brand), not the global `AuthzGuard` that
|
|
78
|
+
* enforces those routes, so it reports them at every boot in this mode.
|
|
79
|
+
*/
|
|
80
|
+
let AuthzGuardStartupCheck = class AuthzGuardStartupCheck {
|
|
81
|
+
discovery;
|
|
82
|
+
metadataScanner;
|
|
83
|
+
logger = new Logger('AuthzModule');
|
|
84
|
+
constructor(discovery, metadataScanner) {
|
|
85
|
+
this.discovery = discovery;
|
|
86
|
+
this.metadataScanner = metadataScanner;
|
|
87
|
+
}
|
|
88
|
+
/** Runs after every `onModuleInit()`, so after the official error it explains. */
|
|
89
|
+
onApplicationBootstrap() {
|
|
90
|
+
const enforced = this.routesOnlyAuthzGuardEnforces();
|
|
91
|
+
if (enforced.length === 0)
|
|
92
|
+
return;
|
|
93
|
+
const official = '"@Can() is not enforced on …"';
|
|
94
|
+
this.logger.log([
|
|
95
|
+
`unannotated: 'allow': the global AuthzGuard enforces @Can() on ${list(enforced)}.`,
|
|
96
|
+
`@nestjs/authorization logs ${official} for these handlers because it only recognizes`,
|
|
97
|
+
'its own AuthorizationGuard; in this mode that error is expected. It goes away when you',
|
|
98
|
+
"remove `unannotated: 'allow'` and turn the official global guard back on.",
|
|
99
|
+
].join(' '));
|
|
100
|
+
}
|
|
101
|
+
/** Handlers with a `@Can()` requirement and no official guard of their own: AuthzGuard's alone. */
|
|
102
|
+
routesOnlyAuthzGuardEnforces() {
|
|
103
|
+
const wheres = [];
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const wrapper of [...this.discovery.getControllers(), ...this.discovery.getProviders()]) {
|
|
106
|
+
const type = wrapper.metatype;
|
|
107
|
+
if (typeof type !== 'function' || wrapper.isFactory || !type.prototype || seen.has(type)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
seen.add(type);
|
|
111
|
+
const classGuards = Reflect.getMetadata(GUARDS_METADATA, type) ?? [];
|
|
112
|
+
for (const key of this.metadataScanner.getAllMethodNames(type.prototype)) {
|
|
113
|
+
const method = type.prototype[key];
|
|
114
|
+
// An undecorated method under a class-level @Can() is a helper, not a handler.
|
|
115
|
+
if (typeof method !== 'function' || Reflect.getOwnMetadataKeys(method).length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
if (routeRequirements(type, method).length === 0)
|
|
118
|
+
continue;
|
|
119
|
+
const guards = [...classGuards, ...(Reflect.getMetadata(GUARDS_METADATA, method) ?? [])];
|
|
120
|
+
if (!guards.some(isBrandedGuard))
|
|
121
|
+
wheres.push(`${type.name}.${key}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return wheres;
|
|
125
|
+
}
|
|
126
|
+
onModuleInit() {
|
|
127
|
+
const officialGlobal = this.discovery
|
|
128
|
+
.getProviders()
|
|
129
|
+
.some((wrapper) => typeof wrapper.token === 'string' &&
|
|
130
|
+
wrapper.token.startsWith('APP_GUARD') &&
|
|
131
|
+
isOfficialGuard(wrapper.instance));
|
|
132
|
+
if (officialGlobal) {
|
|
133
|
+
throw new Error("AuthzModule: `unannotated: 'allow'` needs AuthorizationModule.forRoot({ globalGuard: false }). " +
|
|
134
|
+
"The official global guard is registered too, and it denies every route that declares no @Can(), so 'allow' would have no effect.");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
AuthzGuardStartupCheck = __decorate([
|
|
139
|
+
Injectable(),
|
|
140
|
+
__metadata("design:paramtypes", [DiscoveryService,
|
|
141
|
+
MetadataScanner])
|
|
142
|
+
], AuthzGuardStartupCheck);
|
|
143
|
+
export { AuthzGuardStartupCheck };
|
|
144
|
+
//# sourceMappingURL=authz.guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authz.guard.js","sourceRoot":"","sources":["../src/authz.guard.ts"],"names":[],"mappings":"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"}
|
|
@@ -30,25 +30,6 @@ export interface CanBatchResultItem {
|
|
|
30
30
|
} | null;
|
|
31
31
|
allowed: boolean;
|
|
32
32
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Build the opt-in fallback controller class, mounted at `path`. Kept as a
|
|
35
|
-
* factory so the route path is configurable via `AuthzModule.forRoot({ canEndpoint })`.
|
|
36
|
-
*
|
|
37
|
-
* The endpoint runs `gate.allows(ability, resource?)` for the CURRENT (context)
|
|
38
|
-
* user.
|
|
39
|
-
*
|
|
40
|
-
* RESOURCE REHYDRATION — how per-instance decisions resolve:
|
|
41
|
-
* the client sends a `{ type, id }` shim whose constructor is `Object`, which never
|
|
42
|
-
* matches a registered instance `@Policy` (those are keyed by the entity constructor).
|
|
43
|
-
* To bridge that, register `resourceLoaders` in `AuthzModule.forRoot({ resourceLoaders })`
|
|
44
|
-
* (keyed by the `type` name): when a loader exists for `resource.type`, the endpoint
|
|
45
|
-
* `await`s `loader(id)` and authorizes the REAL entity, so the instance `@Policy`
|
|
46
|
-
* matches and its method runs. A loader returning nullish → `{ allowed: false }` (not
|
|
47
|
-
* found). With NO loader for the type the raw shim is forwarded as before, so only
|
|
48
|
-
* class-level abilities and ad-hoc gates resolve and a resource-bound ability denies
|
|
49
|
-
* (a deliberate, documented fall-through, not a silent dead-end). An unresolved ability
|
|
50
|
-
* is likewise treated as a deny (never a 500).
|
|
51
|
-
*/
|
|
52
33
|
export declare function createCanController(path: string): Type<unknown>;
|
|
53
34
|
/** Default mount path for the fallback endpoint (matches codegen's `/authz/can`). */
|
|
54
35
|
export declare const DEFAULT_CAN_ENDPOINT_PATH = "authz/can";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"can-endpoint.controller.d.ts","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"can-endpoint.controller.d.ts","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,IAAI,EAEV,MAAM,gBAAgB,CAAC;AAOxB;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1D;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,cAAc,EAAE,CAAC;AAEnD,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,OAAO,EAAE,OAAO,CAAC;CAClB;AAwBD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAyH/D;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,cAAc,CAAC"}
|