@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
|
@@ -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
|
|
@@ -136,10 +152,11 @@ hasPermission(user: User, permission: string) {
|
|
|
136
152
|
}
|
|
137
153
|
```
|
|
138
154
|
|
|
139
|
-
Mechanism: the provider
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`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`).
|
|
143
160
|
|
|
144
161
|
### 2. Re-implementing wildcard matching in your adapter
|
|
145
162
|
|
|
@@ -1,153 +1,92 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: authz-setup
|
|
3
3
|
description: >
|
|
4
|
-
Set up @dudousxd/nestjs-authz
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Set up @dudousxd/nestjs-authz 0.9, the extras on top of NestJS's first-party
|
|
5
|
+
@nestjs/authorization. Import AuthorizationModule.forRoot() (official: global
|
|
6
|
+
deny-by-default AuthorizationGuard, @Can, AuthorizationService, getUser) next to
|
|
7
|
+
AuthzModule.forRoot / forRootAsync (ours: Gate, RolePolicy / PermissionPolicy /
|
|
8
|
+
GatePolicy, the ResourceResolver byId() uses, superAdmin, the allow+deny decision event,
|
|
9
|
+
the opt-in POST /authz/can endpoint). Covers the optional peers: @nestjs/authentication
|
|
10
|
+
(AuthenticationContext.user is the ambient Gate user) and @dudousxd/nestjs-context
|
|
11
|
+
(fallback ambient user via resolveUser, per-request permission cache, tenant).
|
|
11
12
|
metadata:
|
|
12
13
|
type: core
|
|
13
14
|
library: "@dudousxd/nestjs-authz"
|
|
14
|
-
library_version: "0.
|
|
15
|
+
library_version: "0.9.0"
|
|
15
16
|
framework: nestjs
|
|
16
17
|
---
|
|
17
18
|
|
|
18
19
|
# Setting up @dudousxd/nestjs-authz
|
|
19
20
|
|
|
20
|
-
`@
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Authorization decisions belong to [`@nestjs/authorization`](https://docs.nestjs.com/security/authorization).
|
|
22
|
+
This package adds RBAC, `@Can` helpers, a service-layer `Gate`, super-admin, query scopes and
|
|
23
|
+
frontend glue. It never authenticates.
|
|
23
24
|
|
|
24
25
|
## Setup
|
|
25
26
|
|
|
26
27
|
```sh
|
|
27
|
-
pnpm add @dudousxd/nestjs-authz
|
|
28
|
+
pnpm add @nestjs/authorization @dudousxd/nestjs-authz
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
current user for free; when absent you pass the user explicitly via `gate.forUser(...)`.
|
|
33
|
-
|
|
34
|
-
Register the module once (it is `global: true`, so every module sees the `Gate` and the
|
|
35
|
-
guards):
|
|
31
|
+
NestJS 12, Node 22+. Peers: `@nestjs/common`, `@nestjs/core`, `@nestjs/authorization`,
|
|
32
|
+
`reflect-metadata`, `rxjs`; optional `@nestjs/authentication` and `@dudousxd/nestjs-context`.
|
|
36
33
|
|
|
37
34
|
```ts
|
|
38
|
-
import {
|
|
35
|
+
import { AuthorizationModule } from '@nestjs/authorization';
|
|
39
36
|
import { AuthzModule } from '@dudousxd/nestjs-authz';
|
|
40
|
-
import { PostPolicy } from './post.policy';
|
|
41
37
|
|
|
42
38
|
@Module({
|
|
43
39
|
imports: [
|
|
40
|
+
AuthorizationModule.forRoot({ getUser: (ctx) => ctx.switchToHttp().getRequest().user }),
|
|
44
41
|
AuthzModule.forRoot({
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
superAdmin: (user) => user?.isRoot === true,
|
|
43
|
+
resourceResolver: new OrmResourceResolver(), // for byId(); default builds { id } from :id
|
|
47
44
|
}),
|
|
48
45
|
],
|
|
46
|
+
providers: [PostPolicy], // official policies are discovered wherever they are providers
|
|
49
47
|
})
|
|
50
48
|
export class AppModule {}
|
|
51
49
|
```
|
|
52
50
|
|
|
53
|
-
`
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
init from `policies: []` **and** auto-discovered `@Policy` providers.
|
|
51
|
+
`forRootAsync({ imports, inject, useFactory | useClass | useExisting, canEndpoint, decorators, unannotated })` takes the
|
|
52
|
+
same options. `canEndpoint`, `decorators` and `unannotated` are static (read from the top
|
|
53
|
+
level next to `useFactory`, never from the factory's return — `decorators` and `unannotated` in the factory's return throw).
|
|
57
54
|
|
|
58
55
|
## Core patterns
|
|
59
56
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
imports: [ConfigModule],
|
|
68
|
-
inject: [ConfigService],
|
|
69
|
-
useFactory: (config: ConfigService) => ({
|
|
70
|
-
policies: [PostPolicy],
|
|
71
|
-
superAdmin: (user) => (user as { role?: string }).role === config.get('SUPER_ROLE'),
|
|
72
|
-
}),
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
`canEndpoint` and `resourceLoaders` may be declared **statically** on the async options
|
|
77
|
-
too (controllers are registered at module-definition time, before the factory resolves).
|
|
78
|
-
|
|
79
|
-
### Wiring the current user via nestjs-context
|
|
80
|
-
|
|
81
|
-
The Gate resolves the current user structurally through the well-known capability token
|
|
82
|
-
`CONTEXT_ACCESSOR` (`capability('context','accessor')` ===
|
|
83
|
-
`Symbol.for('@dudousxd/nestjs-context:accessor')`). Install and configure
|
|
84
|
-
`@dudousxd/nestjs-context` so requests carry a user; the Gate then needs no wiring. With
|
|
85
|
-
**no** accessor and no explicit user, checks treat the request as anonymous and **deny**.
|
|
86
|
-
|
|
87
|
-
### resolveUser — hydrate the real entity (the critical hook)
|
|
88
|
-
|
|
89
|
-
On the **context path**, the value handed to `superAdmin`, `after`, `resolveRoles`, and
|
|
90
|
-
your policy methods is the raw **`UserRef`** (`{ type, id }`) from nestjs-context — **not**
|
|
91
|
-
your hydrated user row. Provide `resolveUser` to load the entity before those run:
|
|
92
|
-
|
|
93
|
-
```ts
|
|
94
|
-
AuthzModule.forRoot({
|
|
95
|
-
resolveUser: (ref) => userRepo.findOneBy({ id: Number(ref.id) }), // ref: { type, id }
|
|
96
|
-
superAdmin: (user) => (user as User).isAdmin, // now sees a real column
|
|
97
|
-
});
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
`resolveUser` returning nullish is treated as "no user" (deny). It is **only** applied on
|
|
101
|
-
the context path — `gate.forUser(entity)` always uses the value you pass verbatim.
|
|
57
|
+
- The official module must stay global (its default): the `Gate` injects `AuthorizationService`.
|
|
58
|
+
- `getUser` is the only user on routes. The `Gate`'s ambient user (calls without `forUser`) is
|
|
59
|
+
`@nestjs/authentication`'s `AuthenticationContext.user` when that package is installed and its
|
|
60
|
+
scope is active (its answer is final, so an anonymous call there is a guest), else nestjs-context's
|
|
61
|
+
`{ type, id }` ref, hydrated by `resolveUser` once per request.
|
|
62
|
+
- Every route needs a declaration: the official guard denies a handler with no `@Can()`,
|
|
63
|
+
`@Can.Anyone()` or `@Public()`.
|
|
102
64
|
|
|
103
65
|
## Common mistakes
|
|
104
66
|
|
|
105
|
-
### 1.
|
|
67
|
+
### 1. Importing only AuthzModule
|
|
106
68
|
|
|
107
69
|
```ts
|
|
108
|
-
// Wrong —
|
|
109
|
-
|
|
110
|
-
AuthzModule.forRoot({ superAdmin: (u) => (u as User).isAdmin });
|
|
111
|
-
|
|
112
|
-
// Correct — hydrate the entity first.
|
|
113
|
-
AuthzModule.forRoot({
|
|
114
|
-
resolveUser: (ref) => userRepo.findOneBy({ id: Number(ref.id) }),
|
|
115
|
-
superAdmin: (u) => (u as User).isAdmin,
|
|
116
|
-
});
|
|
117
|
-
```
|
|
70
|
+
// Wrong — no AuthorizationService for the Gate, no guard on routes.
|
|
71
|
+
imports: [AuthzModule.forRoot()]
|
|
118
72
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
`packages/core/src/gate.ts` (`currentUser`).
|
|
73
|
+
// Correct
|
|
74
|
+
imports: [AuthorizationModule.forRoot(), AuthzModule.forRoot()]
|
|
75
|
+
```
|
|
123
76
|
|
|
124
|
-
### 2.
|
|
77
|
+
### 2. Passing policies to AuthzModule
|
|
125
78
|
|
|
126
79
|
```ts
|
|
127
|
-
// Wrong —
|
|
128
|
-
|
|
129
|
-
export class PostsModule {}
|
|
80
|
+
// Wrong (0.x) — `policies` is gone from AuthzModuleOptions.
|
|
81
|
+
AuthzModule.forRoot({ policies: [PostPolicy] })
|
|
130
82
|
|
|
131
|
-
// Correct — register
|
|
132
|
-
|
|
133
|
-
export class AppModule {}
|
|
83
|
+
// Correct — register them as providers, or with the official module.
|
|
84
|
+
AuthorizationModule.forRoot({ policies: [PostPolicy] })
|
|
134
85
|
```
|
|
135
86
|
|
|
136
|
-
|
|
137
|
-
registration exports `Gate`, the guards, and `RESOURCE_RESOLVER` to the whole app.
|
|
138
|
-
Source: `packages/core/src/module.ts` (`forRoot` returns `global: true`).
|
|
139
|
-
|
|
140
|
-
### 3. Reaching for the `canEndpoint` before it is enabled
|
|
87
|
+
### 3. Leaving routes undeclared after upgrading from 0.x
|
|
141
88
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
AuthzModule.forRoot({ policies: [PostPolicy] });
|
|
145
|
-
|
|
146
|
-
// Correct — opt in (true = default path 'authz/can', or pass a custom string).
|
|
147
|
-
AuthzModule.forRoot({ policies: [PostPolicy], canEndpoint: true });
|
|
148
|
-
```
|
|
89
|
+
0.x let un-annotated routes through. The official guard denies them (401 for guests, 403
|
|
90
|
+
otherwise). Add `@Can.Anyone()` to routes anyone may call.
|
|
149
91
|
|
|
150
|
-
|
|
151
|
-
truthy; it is the last-resort path for abilities not already hydrated on the client.
|
|
152
|
-
Source: `packages/core/src/module.ts` (`canControllers`), `packages/core/src/types.ts`
|
|
153
|
-
(`canEndpoint`).
|
|
92
|
+
Source: `packages/core/src/module.ts`, `packages/core/src/runtime.ts`.
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import 'reflect-metadata';
|
|
2
|
-
import { type Type } from '@nestjs/common';
|
|
3
|
-
export interface CanOptions {
|
|
4
|
-
/**
|
|
5
|
-
* Class-level ability (e.g. `create`): the policy method is keyed to the
|
|
6
|
-
* resource class but receives NO instance, so the guard skips resource loading.
|
|
7
|
-
*/
|
|
8
|
-
classLevel?: boolean;
|
|
9
|
-
}
|
|
10
|
-
export interface CanMetadata {
|
|
11
|
-
ability: string;
|
|
12
|
-
/** Resource class — to load an instance, or (for class-level) to locate the policy. */
|
|
13
|
-
resource?: Type<unknown>;
|
|
14
|
-
/** When true, skip resource loading and dispatch against the resource class. */
|
|
15
|
-
classLevel?: boolean;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Guard a route with an authorization check.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* `@Can('update', Post)` // resolve Post by :id, run PostPolicy.update(user, post)
|
|
22
|
-
* `@Can('create', Post, { classLevel: true })` // run PostPolicy.create(user) — no instance loaded
|
|
23
|
-
* `@Can('access-admin')` // ad-hoc gate, no resource
|
|
24
|
-
*
|
|
25
|
-
* When a resource class is provided (and not `classLevel`), the guard loads an
|
|
26
|
-
* instance via the registered `ResourceResolver` (default: by route `:id`).
|
|
27
|
-
*/
|
|
28
|
-
export declare function Can(ability: string, resource?: Type<unknown>, options?: CanOptions): MethodDecorator & ClassDecorator;
|
|
29
|
-
//# sourceMappingURL=can.decorator.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"can.decorator.d.ts","sourceRoot":"","sources":["../../src/decorator/can.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAGxD,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,gFAAgF;IAChF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,GAAG,CACjB,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,EACxB,OAAO,CAAC,EAAE,UAAU,GACnB,eAAe,GAAG,cAAc,CAKlC"}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import 'reflect-metadata';
|
|
2
|
-
import { SetMetadata } from '@nestjs/common';
|
|
3
|
-
import { CAN_METADATA } from '../tokens.js';
|
|
4
|
-
/**
|
|
5
|
-
* Guard a route with an authorization check.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* `@Can('update', Post)` // resolve Post by :id, run PostPolicy.update(user, post)
|
|
9
|
-
* `@Can('create', Post, { classLevel: true })` // run PostPolicy.create(user) — no instance loaded
|
|
10
|
-
* `@Can('access-admin')` // ad-hoc gate, no resource
|
|
11
|
-
*
|
|
12
|
-
* When a resource class is provided (and not `classLevel`), the guard loads an
|
|
13
|
-
* instance via the registered `ResourceResolver` (default: by route `:id`).
|
|
14
|
-
*/
|
|
15
|
-
export function Can(ability, resource, options) {
|
|
16
|
-
const meta = { ability };
|
|
17
|
-
if (resource !== undefined)
|
|
18
|
-
meta.resource = resource;
|
|
19
|
-
if (options?.classLevel)
|
|
20
|
-
meta.classLevel = true;
|
|
21
|
-
return SetMetadata(CAN_METADATA, meta);
|
|
22
|
-
}
|
|
23
|
-
//# sourceMappingURL=can.decorator.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"can.decorator.js","sourceRoot":"","sources":["../../src/decorator/can.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAa,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAkB5C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,GAAG,CACjB,OAAe,EACf,QAAwB,EACxB,OAAoB;IAEpB,MAAM,IAAI,GAAgB,EAAE,OAAO,EAAE,CAAC;IACtC,IAAI,QAAQ,KAAK,SAAS;QAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACrD,IAAI,OAAO,EAAE,UAAU;QAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;IAChD,OAAO,WAAW,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import 'reflect-metadata';
|
|
2
|
-
import { type Type } from '@nestjs/common';
|
|
3
|
-
/**
|
|
4
|
-
* Marks a class as the policy for `resource`. The class methods become abilities
|
|
5
|
-
* dispatched by name (e.g. `update(user, post)` answers `gate.authorize('update', post)`).
|
|
6
|
-
*
|
|
7
|
-
* Applying `@Policy` also makes the class an `@Injectable()` provider so it can
|
|
8
|
-
* be auto-discovered and resolved from the Nest container.
|
|
9
|
-
*/
|
|
10
|
-
export declare function Policy(resource: Type<unknown>): ClassDecorator;
|
|
11
|
-
/**
|
|
12
|
-
* Returns the resource class a policy was declared for, or `undefined`.
|
|
13
|
-
*
|
|
14
|
-
* Uses `getMetadata` (not `getOwnMetadata`) so a subclass of a `@Policy`-decorated
|
|
15
|
-
* class still resolves the inherited resource via the prototype chain.
|
|
16
|
-
*/
|
|
17
|
-
export declare function getPolicyResource(policy: Type<unknown> | object): Type<unknown> | undefined;
|
|
18
|
-
//# sourceMappingURL=policy.decorator.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"policy.decorator.d.ts","sourceRoot":"","sources":["../../src/decorator/policy.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAc,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAIvD;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,cAAc,CAM9D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,SAAS,CAG3F"}
|