@dudousxd/nestjs-authz 0.8.0 → 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 +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 -209
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +209 -577
- 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/permission-cache.d.ts +18 -9
- package/dist/permission-cache.d.ts.map +1 -1
- package/dist/permission-cache.js +87 -24
- package/dist/permission-cache.js.map +1 -1
- package/dist/permission-matcher.d.ts +13 -0
- package/dist/permission-matcher.d.ts.map +1 -1
- package/dist/permission-matcher.js +38 -2
- package/dist/permission-matcher.js.map +1 -1
- package/dist/permission-provider.d.ts +11 -0
- package/dist/permission-provider.d.ts.map +1 -1
- 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 -169
- package/dist/types.d.ts.map +1 -1
- package/package.json +19 -10
- package/skills/authz-enforcement/SKILL.md +67 -131
- package/skills/authz-policies/SKILL.md +43 -117
- package/skills/authz-query-scopes/SKILL.md +16 -9
- package/skills/authz-rbac-seams/SKILL.md +39 -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
|
@@ -1,167 +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
|
-
|
|
40
|
+
@Get('reports')
|
|
41
|
+
@Can(GatePolicy, 'allows', ability('access-admin')) // a gate.define() gate
|
|
42
|
+
reports() {}
|
|
79
43
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{ ability: 'delete', resource: post },
|
|
84
|
-
]); // → [{ ability:'update', resource, allowed }, ...] (order preserved)
|
|
44
|
+
@Get('me')
|
|
45
|
+
@Can.Anyone() // the official way to open a route
|
|
46
|
+
me() {}
|
|
85
47
|
```
|
|
86
48
|
|
|
87
|
-
|
|
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.
|
|
88
52
|
|
|
89
|
-
|
|
53
|
+
## Services
|
|
90
54
|
|
|
91
55
|
```ts
|
|
92
|
-
|
|
93
|
-
await
|
|
94
|
-
|
|
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);
|
|
95
61
|
```
|
|
96
62
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```
|
|
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.
|
|
116
81
|
|
|
117
82
|
## Common mistakes
|
|
118
83
|
|
|
119
|
-
### 1.
|
|
84
|
+
### 1. Writing the 0.8 string form
|
|
120
85
|
|
|
121
|
-
|
|
122
|
-
// Wrong — the guard tries to LOAD a Post (by :id) for a creation route that has none,
|
|
123
|
-
// so the resolver returns undefined and the request is denied.
|
|
124
|
-
@HttpPost()
|
|
125
|
-
@Can('create', Post)
|
|
126
|
-
create() {}
|
|
127
|
-
|
|
128
|
-
// Correct — flag it class-level so the guard skips resource loading.
|
|
129
|
-
@HttpPost()
|
|
130
|
-
@Can('create', Post, { classLevel: true })
|
|
131
|
-
create() {}
|
|
132
|
-
```
|
|
86
|
+
0.9 removed it: `@Can` comes from `@nestjs/authorization` and names the policy.
|
|
133
87
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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'))` |
|
|
137
93
|
|
|
138
|
-
### 2.
|
|
94
|
+
### 2. Loading the record again in the handler
|
|
139
95
|
|
|
140
|
-
|
|
141
|
-
// Wrong — IdParamResourceResolver only sets `{ id }`; `post.authorId` is undefined,
|
|
142
|
-
// so an ownership policy reading post.authorId can never match.
|
|
143
|
-
AuthzModule.forRoot({ policies: [PostPolicy] });
|
|
96
|
+
Read it with `authorizedResource(req, Post)`.
|
|
144
97
|
|
|
145
|
-
|
|
146
|
-
AuthzModule.forRoot({ policies: [PostPolicy], resourceResolver: new OrmResourceResolver(...) });
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
Mechanism: the default resolver produces a `Object.create(resource.prototype)` shim with
|
|
150
|
-
only `id` set (enough for `instance.constructor` matching, not for column reads).
|
|
151
|
-
Source: `packages/core/src/resource-resolver.ts` (`IdParamResourceResolver`).
|
|
152
|
-
|
|
153
|
-
### 3. Ambiguous class-level ability across multiple policies
|
|
98
|
+
### 3. Writing an inline resolver for constant args
|
|
154
99
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// cannot tell which to run and throws AmbiguousAbilityException.
|
|
158
|
-
await this.gate.allows('create');
|
|
159
|
-
|
|
160
|
-
// Correct — pass the resource class so the Gate selects the right policy.
|
|
161
|
-
await this.gate.allows('create', Post); // programmatic
|
|
162
|
-
// @Can('create', Post, { classLevel: true }) // declarative
|
|
163
|
-
```
|
|
100
|
+
`@Can(FlagPolicy, 'on', () => ['billing'])` works, but tools cannot read it without a call.
|
|
101
|
+
Use `staticCanArgs('billing')`.
|
|
164
102
|
|
|
165
|
-
|
|
166
|
-
ambiguous (arbitrary) choice, so it throws instead of guessing.
|
|
167
|
-
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`.
|
|
@@ -1,150 +1,76 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: authz-policies
|
|
3
3
|
description: >
|
|
4
|
-
Write
|
|
5
|
-
@Policy(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
the global superAdmin / after hooks with their override semantics (after can only fill
|
|
11
|
-
in when the policy returned nullish; default-deny on no opinion).
|
|
4
|
+
Write policies for @dudousxd/nestjs-authz 0.9: they are official @nestjs/authorization
|
|
5
|
+
policies (@Policy(), abilities taking `user | null`, optional before(), only a literal true
|
|
6
|
+
allows). Our additions: @PolicyFor(Resource) to link a resource to its policy (for
|
|
7
|
+
gate.allows('update', post), gate.scope(Post), client ability maps), the AuthzPolicy base
|
|
8
|
+
class / superAdminBefore() that honor the global superAdmin hook, and a scope(user) method
|
|
9
|
+
for query scoping. Covers migrating the 0.8 @Policy(Resource).
|
|
12
10
|
metadata:
|
|
13
11
|
type: core
|
|
14
12
|
library: "@dudousxd/nestjs-authz"
|
|
15
|
-
library_version: "0.
|
|
13
|
+
library_version: "0.9.0"
|
|
16
14
|
framework: nestjs
|
|
17
15
|
---
|
|
18
16
|
|
|
19
17
|
# Writing policies
|
|
20
18
|
|
|
21
|
-
A **policy** is a class decorated with `@Policy(Resource)`. Its methods are **abilities**
|
|
22
|
-
dispatched by name: `update(user, post)` answers `gate.authorize('update', post)` and
|
|
23
|
-
`@Can('update', Post)`.
|
|
24
|
-
|
|
25
|
-
## Setup
|
|
26
|
-
|
|
27
19
|
```ts
|
|
28
|
-
import { Policy } from '@
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@
|
|
33
|
-
export class PostPolicy {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
before(user: User, ability: string) {
|
|
37
|
-
if (user.isAdmin) return true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
view(user: User, post: Post) {
|
|
41
|
-
return post.published || post.authorId === user.id;
|
|
20
|
+
import { Policy } from '@nestjs/authorization';
|
|
21
|
+
import { AuthzPolicy, PolicyFor, eq } from '@dudousxd/nestjs-authz';
|
|
22
|
+
|
|
23
|
+
@Policy()
|
|
24
|
+
@PolicyFor(Post) // optional link; also applies @Policy() itself
|
|
25
|
+
export class PostPolicy extends AuthzPolicy { // optional: superAdmin grants every ability
|
|
26
|
+
update(user: User | null, post: Post) {
|
|
27
|
+
return post.authorId === user?.id;
|
|
42
28
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return post.authorId === user.id;
|
|
29
|
+
create(user: User | null) {
|
|
30
|
+
return user?.verified === true; // no record: a class-level ability
|
|
46
31
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
create(user: User) {
|
|
50
|
-
return user.verified;
|
|
32
|
+
scope(user: User) {
|
|
33
|
+
return eq('authorId', user.id); // not an ability; used by gate.scope(Post)
|
|
51
34
|
}
|
|
52
35
|
}
|
|
53
36
|
```
|
|
54
37
|
|
|
55
|
-
`@Policy` also makes the class `@Injectable()`, so it is auto-discovered from the Nest
|
|
56
|
-
container (you do not have to list it in `policies: []`, though you may). Register it via
|
|
57
|
-
`AuthzModule.forRoot({ policies: [PostPolicy] })` or rely on auto-discovery.
|
|
58
|
-
|
|
59
38
|
## Core patterns
|
|
60
39
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Global superAdmin and after hooks
|
|
74
|
-
|
|
75
|
-
Configured on the module, not the policy. `superAdmin` runs **before** any policy/gate;
|
|
76
|
-
truthy grants. `after` (Laravel `Gate::after`) runs **after** resolution:
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
AuthzModule.forRoot({
|
|
80
|
-
superAdmin: (user, ability) => (user as User).role === 'root', // truthy → allow
|
|
81
|
-
after: (user, ability, result) => {
|
|
82
|
-
// result is the policy/gate verdict, or undefined when it had "no opinion".
|
|
83
|
-
if (result === undefined) return ability.startsWith('public.'); // only fills in a gap
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Inheritance
|
|
89
|
-
|
|
90
|
-
A subclass of a `@Policy`-decorated class still resolves the inherited resource
|
|
91
|
-
(`getPolicyResource` uses `getMetadata`, walking the prototype chain), so you can share a
|
|
92
|
-
base policy and override individual ability methods.
|
|
40
|
+
- Abilities are the official contract: `ability(user | null, ...args)`, `true` allows,
|
|
41
|
+
anything else denies. A guest denial is a 401, a signed-in one a 403.
|
|
42
|
+
- `AuthzPolicy.before()` asks `superAdmin(user, ability, resource)`, then the
|
|
43
|
+
`DECISION_PROVIDER` when one is registered (its allow/deny is final; `undefined` abstains).
|
|
44
|
+
Own `before()`: override and call `super.before(user, ability, ...args)` first to keep both.
|
|
45
|
+
Policies that cannot extend `AuthzPolicy` call `superAdminBefore(user, ability, resource)`
|
|
46
|
+
and `decisionProviderBefore(user, ability, resource)`.
|
|
47
|
+
- A persisted permission named like an ability does not bypass the policy: the policy has the
|
|
48
|
+
final word.
|
|
49
|
+
- Policies must be singletons (official rule).
|
|
93
50
|
|
|
94
51
|
## Common mistakes
|
|
95
52
|
|
|
96
|
-
### 1.
|
|
53
|
+
### 1. Typing the user as `User`
|
|
97
54
|
|
|
98
55
|
```ts
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
view(user: User, post: Post) {
|
|
102
|
-
if (post.authorId === user.id) return true;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Correct — return an explicit boolean for every path.
|
|
106
|
-
view(user: User, post: Post) {
|
|
107
|
-
return post.published || post.authorId === user.id;
|
|
108
|
-
}
|
|
56
|
+
update(user: User, post: Post) {} // Wrong — guests reach it as null; @Can rejects it
|
|
57
|
+
update(user: User | null, post: Post) {} // Correct
|
|
109
58
|
```
|
|
110
59
|
|
|
111
|
-
|
|
112
|
-
default-denies in `resolve` (after giving an `after` hook a chance).
|
|
113
|
-
Source: `packages/core/src/gate.ts` (`resolveBase` / `resolve`, `normalizeResult`).
|
|
60
|
+
### 2. Returning `{ allowed, message }` or a truthy value
|
|
114
61
|
|
|
115
|
-
|
|
62
|
+
0.x accepted `PolicyResponse`. The official evaluator allows only a literal `true`; a
|
|
63
|
+
truthy object denies. Return booleans.
|
|
116
64
|
|
|
117
|
-
|
|
118
|
-
// Wrong — `before` grants 'publish', but there is no publish() method, so the ability
|
|
119
|
-
// is unresolved and throws AbilityNotResolvedException instead of being allowed.
|
|
120
|
-
@Policy(Post)
|
|
121
|
-
class PostPolicy {
|
|
122
|
-
before(user: User, ability: string) { if (ability === 'publish') return true; }
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Correct — define the method; `before` may then short-circuit it.
|
|
126
|
-
@Policy(Post)
|
|
127
|
-
class PostPolicy {
|
|
128
|
-
before(user: User, ability: string) { if (ability === 'publish') return true; }
|
|
129
|
-
publish(user: User, post: Post) { return post.authorId === user.id; }
|
|
130
|
-
}
|
|
131
|
-
```
|
|
65
|
+
### 3. Expecting superAdmin everywhere
|
|
132
66
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Source: `packages/core/src/gate.ts` (`resolveBase`, "gate it on method existence FIRST").
|
|
67
|
+
The official guard runs only the policy's own `before()`. A policy that does not extend
|
|
68
|
+
`AuthzPolicy` (or call `superAdminBefore`) never sees superAdmin on routes.
|
|
136
69
|
|
|
137
|
-
###
|
|
70
|
+
### 4. Writing the 0.8 `@Policy(Post)`
|
|
138
71
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
after: (user, ability, result) => true;
|
|
143
|
-
|
|
144
|
-
// Correct — `after` can only supply a verdict when the policy/gate had no opinion.
|
|
145
|
-
after: (user, ability, result) => (result === undefined ? true : undefined);
|
|
146
|
-
```
|
|
72
|
+
0.9 removed it: this package no longer exports `Policy`. Use the official `@Policy()` from
|
|
73
|
+
`@nestjs/authorization` plus `@PolicyFor(Post)` (or `@PolicyFor(Post)` alone, which applies
|
|
74
|
+
`@Policy()` itself).
|
|
147
75
|
|
|
148
|
-
|
|
149
|
-
explicit verdict); when the policy decided, `after` is purely an observer.
|
|
150
|
-
Source: `packages/core/src/types.ts` (`AfterHook` docs), `packages/core/src/gate.ts` (`resolve`).
|
|
76
|
+
Source: `packages/core/src/links.ts`, `packages/core/src/policies/authz-policy.ts`.
|
|
@@ -8,12 +8,14 @@ description: >
|
|
|
8
8
|
helpers (operators eq, ne, gt, gte, lt, lte, in, nin, isNull, isNotNull). A @Policy scope(
|
|
9
9
|
user, Entity) method produces it (returning true=allow-all, false/nullish=deny-all sugar);
|
|
10
10
|
an ORM adapter such as @dudousxd/nestjs-authz-typeorm's applyScope / applyScopeConstraint
|
|
11
|
-
compiles it to a parameterized, identifier-safe WHERE. Mirrors the Gate's
|
|
12
|
-
super-admin
|
|
11
|
+
compiles it to a parameterized, identifier-safe WHERE. Mirrors the Gate's order:
|
|
12
|
+
super-admin → allow-all; a DECISION_PROVIDER planScope; the policy's scope (a persisted
|
|
13
|
+
permission never widens it); anonymous → deny-all; no policy method → a permission grant
|
|
14
|
+
allows all, else deny-all.
|
|
13
15
|
metadata:
|
|
14
16
|
type: core
|
|
15
17
|
library: "@dudousxd/nestjs-authz"
|
|
16
|
-
library_version: "0.
|
|
18
|
+
library_version: "0.9.0"
|
|
17
19
|
framework: nestjs
|
|
18
20
|
---
|
|
19
21
|
|
|
@@ -30,11 +32,13 @@ Add a `scope(user, Entity)` method to a `@Policy` class, returning a `ScopeConst
|
|
|
30
32
|
built from the core helpers:
|
|
31
33
|
|
|
32
34
|
```ts
|
|
33
|
-
import { Policy
|
|
35
|
+
import { Policy } from '@nestjs/authorization';
|
|
36
|
+
import { PolicyFor, eq, and, where, scopeAll } from '@dudousxd/nestjs-authz';
|
|
34
37
|
import { Post } from './post.entity';
|
|
35
38
|
import { User } from './user.entity';
|
|
36
39
|
|
|
37
|
-
@Policy(
|
|
40
|
+
@Policy()
|
|
41
|
+
@PolicyFor(Post)
|
|
38
42
|
export class PostPolicy {
|
|
39
43
|
scope(user: User, _entity: typeof Post) {
|
|
40
44
|
if (user.isAdmin) return scopeAll; // sugar: every row
|
|
@@ -75,10 +79,13 @@ await applyScope(qb, this.gate, Post);
|
|
|
75
79
|
|
|
76
80
|
### Grant order mirrors the single-resource Gate
|
|
77
81
|
|
|
78
|
-
`gate.scope` resolves consistently with `gate.allows`: a `superAdmin` grant
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
`gate.scope` resolves consistently with `gate.allows`: a `superAdmin` grant (it receives the
|
|
83
|
+
entity class) → `scopeAll`; a `DECISION_PROVIDER`'s `planScope` (e.g. Cerbos `PlanResources`),
|
|
84
|
+
when it does not abstain, is used as-is; a policy `before` returning `true`/`false` scopes to
|
|
85
|
+
allow-all/deny-all; an anonymous user → `scopeNone`; otherwise the policy's `scope` constraint.
|
|
86
|
+
The policy has the final word: a persisted `viewAny` permission does not widen it. Only when
|
|
87
|
+
there is no policy `scope`/`viewAny` method does a permission grant for the ability give
|
|
88
|
+
`scopeAll` (else `scopeNone`).
|
|
82
89
|
|
|
83
90
|
### Return-value sugar
|
|
84
91
|
|
|
@@ -3,10 +3,11 @@ name: authz-rbac-seams
|
|
|
3
3
|
description: >
|
|
4
4
|
Add persisted roles & permissions to @dudousxd/nestjs-authz through its optional,
|
|
5
5
|
grant-only seams. PERMISSION_PROVIDER (a PermissionProvider with hasPermission and an
|
|
6
|
-
optional getPermissions)
|
|
7
|
-
a
|
|
6
|
+
optional getPermissions) grants named abilities no policy defines (posts.publish) — it never
|
|
7
|
+
bypasses a policy that defines the ability (the policy has the final word; only superAdmin
|
|
8
|
+
short-circuits it); getPermissions enables
|
|
8
9
|
segment wildcard matching (granted posts.* satisfies posts.update, * satisfies anything).
|
|
9
|
-
ROLE_PROVIDER (a RoleProvider.getRoles) feeds coarse checks gate.hasRole / @Roles, unioned
|
|
10
|
+
ROLE_PROVIDER (a RoleProvider.getRoles) feeds coarse checks gate.hasRole / @Roles (RolePolicy), unioned
|
|
10
11
|
with the user-object defaultRoleResolver (reads user.roles / user.role). Both tokens are
|
|
11
12
|
capability symbols the ORM adapters (@dudousxd/nestjs-authz-typeorm / -prisma / -mikro-orm / -drizzle)
|
|
12
13
|
register via AuthzRbacModule; seams are grant-only — a false result never DENIES, it falls
|
|
@@ -14,7 +15,7 @@ description: >
|
|
|
14
15
|
metadata:
|
|
15
16
|
type: core
|
|
16
17
|
library: "@dudousxd/nestjs-authz"
|
|
17
|
-
library_version: "0.
|
|
18
|
+
library_version: "0.9.0"
|
|
18
19
|
framework: nestjs
|
|
19
20
|
---
|
|
20
21
|
|
|
@@ -23,7 +24,8 @@ metadata:
|
|
|
23
24
|
The core stays zero-DB. Persisted roles/permissions plug in through two **optional,
|
|
24
25
|
grant-only** seams the Gate consults via `@Optional()` capability tokens:
|
|
25
26
|
|
|
26
|
-
- `PERMISSION_PROVIDER` → a `PermissionProvider` (
|
|
27
|
+
- `PERMISSION_PROVIDER` → a `PermissionProvider` (feeds `PermissionPolicy` / `@Permission` and
|
|
28
|
+
the `Gate`'s named abilities that no policy defines).
|
|
27
29
|
- `ROLE_PROVIDER` → a `RoleProvider` (feeds `gate.hasRole` / `@Roles`).
|
|
28
30
|
|
|
29
31
|
When neither is registered, the Gate behaves exactly as before. **Grant-only** means a
|
|
@@ -32,11 +34,13 @@ provider returning `false`/nullish never DENIES — it falls through to normal r
|
|
|
32
34
|
## Setup
|
|
33
35
|
|
|
34
36
|
The simplest path is an ORM adapter's `AuthzRbacModule`, which builds a store and
|
|
35
|
-
registers both seams. Example with the Prisma adapter (
|
|
36
|
-
|
|
37
|
+
registers both seams. Example with the Prisma adapter (which takes `store` or `client`;
|
|
38
|
+
MikroORM takes `store`; the TypeORM and Drizzle siblings also accept `connectionName` /
|
|
39
|
+
`storeOptions` to build the store from the official connection instead of a prebuilt `store`):
|
|
37
40
|
|
|
38
41
|
```ts
|
|
39
42
|
import { Module } from '@nestjs/common';
|
|
43
|
+
import { AuthorizationModule } from '@nestjs/authorization';
|
|
40
44
|
import { AuthzModule } from '@dudousxd/nestjs-authz';
|
|
41
45
|
import { AuthzRbacModule } from '@dudousxd/nestjs-authz-prisma';
|
|
42
46
|
import { PrismaClient } from '@prisma/client';
|
|
@@ -45,7 +49,8 @@ const prisma = new PrismaClient();
|
|
|
45
49
|
|
|
46
50
|
@Module({
|
|
47
51
|
imports: [
|
|
48
|
-
|
|
52
|
+
AuthorizationModule.forRoot(),
|
|
53
|
+
AuthzModule.forRoot(),
|
|
49
54
|
AuthzRbacModule.forRoot({ client: prisma }), // builds the store, registers the seams
|
|
50
55
|
],
|
|
51
56
|
})
|
|
@@ -62,6 +67,17 @@ await gate.forUser(user).allows('posts.publish'); // true via PERMISSION_PROVIDE
|
|
|
62
67
|
await gate.forUser(user).hasRole('editor'); // true via ROLE_PROVIDER
|
|
63
68
|
```
|
|
64
69
|
|
|
70
|
+
On routes, the same seams answer through the official guard as the core's official policies:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
@Permission('posts.publish') // = @Can(PermissionPolicy, 'has', permission('posts.publish'))
|
|
74
|
+
@Roles('editor') // = @Can(RolePolicy, 'has', roles('editor'))
|
|
75
|
+
@Can(PermissionPolicy, 'hasAll', permission('posts.publish', 'posts.delete'))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Both extend `AuthzPolicy`, so `superAdmin` grants them; `gate.hasRole()` is a plain fact and
|
|
79
|
+
ignores it.
|
|
80
|
+
|
|
65
81
|
Admin-UI management lives on every adapter's store too (same semantics everywhere):
|
|
66
82
|
`syncRolePermissions(role, names)` replaces a role's permission set, `getRolePermissions(roles)`
|
|
67
83
|
returns `{ [role]: permissionNames }` (missing roles absent), `deleteRole(role)` removes the role
|
|
@@ -95,6 +111,16 @@ The core runs segment-based wildcard matching against `getPermissions`: a grante
|
|
|
95
111
|
`posts.*` satisfies `posts.update`; `*` satisfies anything. `hasPermission` is the
|
|
96
112
|
exact-match contract for providers that do not list grants.
|
|
97
113
|
|
|
114
|
+
Within one request (nestjs-context) the Gate fetches `getPermissions` and
|
|
115
|
+
`RoleProvider.getRoles` once per user and reuses them for every check (keyed by the
|
|
116
|
+
row the user names — its class or `type` plus `id`, so ORMs without an identity map
|
|
117
|
+
share entries; a plain object without `type` is keyed by identity, so two users that
|
|
118
|
+
only share an `id` never share an entry); a grant made
|
|
119
|
+
mid-request applies from the next request. When the set misses, the Gate still calls
|
|
120
|
+
`hasPermission` (it may grant on the resource). Set `readonly exhaustivePermissions = true`
|
|
121
|
+
when `getPermissions` already lists everything `hasPermission` can grant, so a miss is
|
|
122
|
+
answered without another lookup; the ORM adapters' providers do.
|
|
123
|
+
|
|
98
124
|
### Zero-table role checks (no provider)
|
|
99
125
|
|
|
100
126
|
`gate.hasRole` / `@Roles` work with **no** RBAC tables: the `defaultRoleResolver` reads
|
|
@@ -126,10 +152,11 @@ hasPermission(user: User, permission: string) {
|
|
|
126
152
|
}
|
|
127
153
|
```
|
|
128
154
|
|
|
129
|
-
Mechanism: the provider
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
`packages/core/src/
|
|
155
|
+
Mechanism: the provider only grants — `false`/`undefined` falls through to the ad-hoc gate and
|
|
156
|
+
never denies. It is asked after the policy: a string ability a linked policy defines is the
|
|
157
|
+
policy's to decide, whatever the user's permissions; only `superAdmin` (and a
|
|
158
|
+
`DECISION_PROVIDER`) come first. Source: `packages/core/src/permission-provider.ts` (grant
|
|
159
|
+
semantics), `packages/core/src/rbac.ts` (`grants`), `packages/core/src/gate.ts` (`decideString`).
|
|
133
160
|
|
|
134
161
|
### 2. Re-implementing wildcard matching in your adapter
|
|
135
162
|
|