@dudousxd/nestjs-authz 0.2.0 → 0.3.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 +70 -0
- package/dist/can-endpoint.controller.d.ts +35 -0
- package/dist/can-endpoint.controller.d.ts.map +1 -0
- package/dist/can-endpoint.controller.js +68 -0
- package/dist/can-endpoint.controller.js.map +1 -0
- package/dist/decorator/roles.decorator.d.ts +15 -0
- package/dist/decorator/roles.decorator.d.ts.map +1 -0
- package/dist/decorator/roles.decorator.js +19 -0
- package/dist/decorator/roles.decorator.js.map +1 -0
- package/dist/diagnostics.d.ts +42 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +68 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/gate.d.ts +42 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +116 -12
- package/dist/gate.js.map +1 -1
- package/dist/guard/roles.guard.d.ts +21 -0
- package/dist/guard/roles.guard.d.ts.map +1 -0
- package/dist/guard/roles.guard.js +50 -0
- package/dist/guard/roles.guard.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +5 -0
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +27 -1
- package/dist/module.js.map +1 -1
- package/dist/permission-provider.d.ts +2 -0
- package/dist/permission-provider.d.ts.map +1 -1
- package/dist/policy-registry.d.ts +21 -0
- package/dist/policy-registry.d.ts.map +1 -1
- package/dist/policy-registry.js +42 -0
- package/dist/policy-registry.js.map +1 -1
- package/dist/role-provider.d.ts +40 -0
- package/dist/role-provider.d.ts.map +1 -0
- package/dist/role-provider.js +32 -0
- package/dist/role-provider.js.map +1 -0
- package/dist/tokens.d.ts +14 -0
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +14 -0
- package/dist/tokens.js.map +1 -1
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# @dudousxd/nestjs-authz
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#2](https://github.com/DavideCarvalho/nestjs-authz/pull/2) [`2ecb0f4`](https://github.com/DavideCarvalho/nestjs-authz/commit/2ecb0f46342fa4527fc01f1097720f2e7bcf9aa7) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add coarse, role-based authorization alongside the granular ability checks.
|
|
8
|
+
|
|
9
|
+
- **`@Roles('admin', 'teacher')` + `RolesGuard`**: a route is allowed when the current
|
|
10
|
+
user holds ANY of the listed roles. Registered as an `APP_GUARD` by `AuthzModule`
|
|
11
|
+
(inert on un-annotated routes), it resolves the current user exactly as the `Gate`
|
|
12
|
+
does and denies an unauthenticated request by default.
|
|
13
|
+
- **`Gate.hasRole(role)` / `Gate.hasAnyRole(roles[])`** (and `gate.forUser(user).hasRole(...)`),
|
|
14
|
+
async, resolving the user's effective roles and testing membership.
|
|
15
|
+
- **Pluggable role source, two layers (unioned):**
|
|
16
|
+
1. A default `RoleResolver` that reads roles off the user object — `user.roles`
|
|
17
|
+
(`string[]`) OR `user.role` (`string | string[]`), normalized to a `string[]`.
|
|
18
|
+
This makes role checks work with ZERO RBAC tables. Override via
|
|
19
|
+
`AuthzModule.forRoot({ resolveRoles })`.
|
|
20
|
+
2. An OPTIONAL `ROLE_PROVIDER` seam (`Symbol.for('@dudousxd/nestjs-authz:role-provider')`,
|
|
21
|
+
consulted with `@Optional()`) — mirroring `PERMISSION_PROVIDER` — so an RBAC adapter
|
|
22
|
+
can supply roles from a store. When both yield roles, the Gate unions them; when
|
|
23
|
+
neither does, the check denies.
|
|
24
|
+
|
|
25
|
+
Exports the `Roles` decorator, `RolesGuard`, `RoleResolver`, `defaultRoleResolver`,
|
|
26
|
+
`ROLE_PROVIDER`, `ROLES_METADATA`, and a `RoleProvider` interface. Purely additive — the
|
|
27
|
+
existing permission-provider / can-endpoint / diagnostics behavior is unchanged.
|
|
28
|
+
|
|
29
|
+
### Patch Changes
|
|
30
|
+
|
|
31
|
+
- [#2](https://github.com/DavideCarvalho/nestjs-authz/pull/2) [`2ecb0f4`](https://github.com/DavideCarvalho/nestjs-authz/commit/2ecb0f46342fa4527fc01f1097720f2e7bcf9aa7) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add `@dudousxd/nestjs-authz-inertia`: a 3-tier Inertia integration that lets client `can(...)`
|
|
32
|
+
checks resolve **without a network request** (the Laravel/Inertia model).
|
|
33
|
+
|
|
34
|
+
- **Tier 1 — shared props (no request):** `AuthzInertiaModule` (a global interceptor that calls
|
|
35
|
+
`req.inertia.share(...)`) or `createAuthzShare(gate, policyRegistry)` (a factory for
|
|
36
|
+
`InertiaModule.forRoot({ share })`) resolves the current user's class-level abilities — all
|
|
37
|
+
ad-hoc gates + class-level `@Policy` methods — into `props.auth.can` via direct in-process
|
|
38
|
+
`gate.allows(...)` calls.
|
|
39
|
+
- **Tier 2 — per-resource map (no request):** `authorizeResource(gate, instance, abilities)`
|
|
40
|
+
returns a `{ update, delete }` map a controller attaches to a serialized resource.
|
|
41
|
+
- **Tier 3 — fallback endpoint (last resort):** consumes core's opt-in `POST /authz/can`.
|
|
42
|
+
- **Framework-neutral client** (`@dudousxd/nestjs-authz-inertia/client`): an `AbilityStore`,
|
|
43
|
+
`hydrateFromInertiaProps` / `hydrateResource`, and a `createCan` resolver that reads hydrated
|
|
44
|
+
decisions synchronously (no fetch on a cache hit) and only falls back (fetch the endpoint, or
|
|
45
|
+
deny) when the ability/resource is unknown.
|
|
46
|
+
- **Denial filter (`AuthzDenialFilter`):** `AuthzInertiaModule.forRoot` also registers a global
|
|
47
|
+
`APP_FILTER` that converts `@Roles`/`@Can` denials (`ForbiddenException`) into a friendly 303
|
|
48
|
+
redirect on Inertia requests (`X-Inertia` header) — `/login` when unauthenticated, `/403` when
|
|
49
|
+
authenticated-but-forbidden (auth-state read from the optional `CONTEXT_ACCESSOR`). Non-Inertia
|
|
50
|
+
(REST) requests are rethrown and keep the normal 403 JSON. Configurable via
|
|
51
|
+
`forRoot({ denial: { loginUrl, forbiddenUrl, enabled, handler } })` (`enabled: false` opts out;
|
|
52
|
+
`handler(exception, host)` is a full escape hatch). Targets pass the same open-redirect guard
|
|
53
|
+
nestjs-inertia uses, falling back to a safe default if unsafe.
|
|
54
|
+
|
|
55
|
+
`@dudousxd/nestjs-authz` (patch): add an opt-in `POST /authz/can` fallback controller behind
|
|
56
|
+
`AuthzModule.forRoot({ canEndpoint: true })` (default off; accepts a custom path string). It runs
|
|
57
|
+
`gate.allows(ability, resource?)` for the current context user and returns `{ allowed }`, failing
|
|
58
|
+
closed for unresolved abilities. Also exposes `Gate.gateNames()` and `PolicyRegistry.classAbilities()`
|
|
59
|
+
so integrations can enumerate a user's class-level abilities.
|
|
60
|
+
|
|
61
|
+
- [#2](https://github.com/DavideCarvalho/nestjs-authz/pull/2) [`2ecb0f4`](https://github.com/DavideCarvalho/nestjs-authz/commit/2ecb0f46342fa4527fc01f1097720f2e7bcf9aa7) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add `@dudousxd/nestjs-authz-telescope`: a `@dudousxd/nestjs-telescope` extension that records every
|
|
62
|
+
authorization decision the `Gate` reaches (ability, allow/deny, the reason it was decided, the user
|
|
63
|
+
and the resource) as an `authorization` Telescope entry plus an "Authorization" dashboard page (a
|
|
64
|
+
top-N of denied abilities and a table of recent decisions) — so a 403 is debuggable. The extension's
|
|
65
|
+
`AuthorizationWatcher` subscribes to the new `nestjs-authz:decision` diagnostics channel; nothing is
|
|
66
|
+
emitted (and nothing recorded) when no observer is listening.
|
|
67
|
+
|
|
68
|
+
The core `Gate` now publishes each decision on a dependency-free `node:diagnostics_channel`
|
|
69
|
+
(`nestjs-authz:decision`, exported as `AUTHZ_DECISION_CHANNEL`) after a verdict is reached. The
|
|
70
|
+
emission is gated on `channel.hasSubscribers` and fully guarded, so it is zero-overhead with no
|
|
71
|
+
subscriber and can never affect a check. No existing behavior changes.
|
|
72
|
+
|
|
3
73
|
## 0.2.0
|
|
4
74
|
|
|
5
75
|
### Minor Changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Type } from '@nestjs/common';
|
|
2
|
+
/**
|
|
3
|
+
* Request body for the fallback `can` endpoint. Mirrors the payload emitted by
|
|
4
|
+
* the codegen `can()` helper: `{ ability, resource?: { type, id } | null }`.
|
|
5
|
+
*/
|
|
6
|
+
export interface CanRequestBody {
|
|
7
|
+
ability: string;
|
|
8
|
+
resource?: {
|
|
9
|
+
type: string;
|
|
10
|
+
id?: string | number;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
/** Response shape: a single boolean verdict. */
|
|
14
|
+
export interface CanResponseBody {
|
|
15
|
+
allowed: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build the opt-in fallback controller class, mounted at `path`. Kept as a
|
|
19
|
+
* factory so the route path is configurable via `AuthzModule.forRoot({ canEndpoint })`.
|
|
20
|
+
*
|
|
21
|
+
* The endpoint runs `gate.allows(ability, resource?)` for the CURRENT (context)
|
|
22
|
+
* user.
|
|
23
|
+
*
|
|
24
|
+
* LIMITATION — read this before relying on the fallback for resource decisions:
|
|
25
|
+
* the `{ type, id }` resource shim is forwarded as-is and NEVER matches a
|
|
26
|
+
* registered `@Policy` by constructor, so this endpoint can ONLY resolve
|
|
27
|
+
* class-level abilities and ad-hoc gates. Per-instance decisions MUST be
|
|
28
|
+
* hydrated via shared props / `authorizeResource` (tiers 1-2); a resource-bound
|
|
29
|
+
* ability that misses the client cache and falls through to `POST /authz/can`
|
|
30
|
+
* will DENY. An unresolved ability is likewise treated as a deny (never a 500).
|
|
31
|
+
*/
|
|
32
|
+
export declare function createCanController(path: string): Type<unknown>;
|
|
33
|
+
/** Default mount path for the fallback endpoint (matches codegen's `/authz/can`). */
|
|
34
|
+
export declare const DEFAULT_CAN_ENDPOINT_PATH = "authz/can";
|
|
35
|
+
//# sourceMappingURL=can-endpoint.controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"can-endpoint.controller.d.ts","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAI7D;;;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;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CA0B/D;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,cAAc,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
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 { Body, Controller } from '@nestjs/common';
|
|
14
|
+
import { Post as HttpPost } from '@nestjs/common';
|
|
15
|
+
import { Gate } from './gate.js';
|
|
16
|
+
/**
|
|
17
|
+
* Build the opt-in fallback controller class, mounted at `path`. Kept as a
|
|
18
|
+
* factory so the route path is configurable via `AuthzModule.forRoot({ canEndpoint })`.
|
|
19
|
+
*
|
|
20
|
+
* The endpoint runs `gate.allows(ability, resource?)` for the CURRENT (context)
|
|
21
|
+
* user.
|
|
22
|
+
*
|
|
23
|
+
* LIMITATION — read this before relying on the fallback for resource decisions:
|
|
24
|
+
* the `{ type, id }` resource shim is forwarded as-is and NEVER matches a
|
|
25
|
+
* registered `@Policy` by constructor, so this endpoint can ONLY resolve
|
|
26
|
+
* class-level abilities and ad-hoc gates. Per-instance decisions MUST be
|
|
27
|
+
* hydrated via shared props / `authorizeResource` (tiers 1-2); a resource-bound
|
|
28
|
+
* ability that misses the client cache and falls through to `POST /authz/can`
|
|
29
|
+
* will DENY. An unresolved ability is likewise treated as a deny (never a 500).
|
|
30
|
+
*/
|
|
31
|
+
export function createCanController(path) {
|
|
32
|
+
let AuthzCanController = class AuthzCanController {
|
|
33
|
+
gate;
|
|
34
|
+
constructor(gate) {
|
|
35
|
+
this.gate = gate;
|
|
36
|
+
}
|
|
37
|
+
async can(body) {
|
|
38
|
+
const ability = body?.ability;
|
|
39
|
+
if (typeof ability !== 'string' || ability.length === 0) {
|
|
40
|
+
return { allowed: false };
|
|
41
|
+
}
|
|
42
|
+
const resource = body?.resource ?? undefined;
|
|
43
|
+
try {
|
|
44
|
+
const allowed = await this.gate.allows(ability, resource == null ? undefined : resource);
|
|
45
|
+
return { allowed };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Unresolved/ambiguous ability — fail closed.
|
|
49
|
+
return { allowed: false };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
__decorate([
|
|
54
|
+
HttpPost(),
|
|
55
|
+
__param(0, Body()),
|
|
56
|
+
__metadata("design:type", Function),
|
|
57
|
+
__metadata("design:paramtypes", [Object]),
|
|
58
|
+
__metadata("design:returntype", Promise)
|
|
59
|
+
], AuthzCanController.prototype, "can", null);
|
|
60
|
+
AuthzCanController = __decorate([
|
|
61
|
+
Controller(path),
|
|
62
|
+
__metadata("design:paramtypes", [Gate])
|
|
63
|
+
], AuthzCanController);
|
|
64
|
+
return AuthzCanController;
|
|
65
|
+
}
|
|
66
|
+
/** Default mount path for the fallback endpoint (matches codegen's `/authz/can`). */
|
|
67
|
+
export const DEFAULT_CAN_ENDPOINT_PATH = 'authz/can';
|
|
68
|
+
//# sourceMappingURL=can-endpoint.controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"can-endpoint.controller.js","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAa,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAgBjC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IACM,kBAAkB,GADxB,MACM,kBAAkB;QACO;QAA7B,YAA6B,IAAU;YAAV,SAAI,GAAJ,IAAI,CAAM;QAAG,CAAC;QAGrC,AAAN,KAAK,CAAC,GAAG,CAAS,IAAoB;YACpC,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC;YAC9B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC5B,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,IAAI,SAAS,CAAC;YAC7C,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CACpC,OAAO,EACP,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,QAAmB,CACpD,CAAC;gBACF,OAAO,EAAE,OAAO,EAAE,CAAC;YACrB,CAAC;YAAC,MAAM,CAAC;gBACP,8CAA8C;gBAC9C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC5B,CAAC;QACH,CAAC;KACF,CAAA;IAjBO;QADL,QAAQ,EAAE;QACA,WAAA,IAAI,EAAE,CAAA;;;;iDAgBhB;IApBG,kBAAkB;QADvB,UAAU,CAAC,IAAI,CAAC;yCAEoB,IAAI;OADnC,kBAAkB,CAqBvB;IAED,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,MAAM,yBAAyB,GAAG,WAAW,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
/**
|
|
3
|
+
* Guard a route by COARSE role membership: the request is allowed when the current
|
|
4
|
+
* user holds ANY of the listed roles. This is the broad "is teacher?" check, as
|
|
5
|
+
* opposed to the granular ability check of `@Can`.
|
|
6
|
+
*
|
|
7
|
+
* Roles come from the user object (`user.roles` / `user.role`) by default, plus the
|
|
8
|
+
* optional `ROLE_PROVIDER` seam (a persisted RBAC store) when registered — see
|
|
9
|
+
* {@link RolesGuard}.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* `@Roles('admin', 'teacher')` // allow if the user is an admin OR a teacher
|
|
13
|
+
*/
|
|
14
|
+
export declare function Roles(...roles: string[]): MethodDecorator & ClassDecorator;
|
|
15
|
+
//# sourceMappingURL=roles.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.decorator.d.ts","sourceRoot":"","sources":["../../src/decorator/roles.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAI1B;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,eAAe,GAAG,cAAc,CAE1E"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import { SetMetadata } from '@nestjs/common';
|
|
3
|
+
import { ROLES_METADATA } from '../tokens.js';
|
|
4
|
+
/**
|
|
5
|
+
* Guard a route by COARSE role membership: the request is allowed when the current
|
|
6
|
+
* user holds ANY of the listed roles. This is the broad "is teacher?" check, as
|
|
7
|
+
* opposed to the granular ability check of `@Can`.
|
|
8
|
+
*
|
|
9
|
+
* Roles come from the user object (`user.roles` / `user.role`) by default, plus the
|
|
10
|
+
* optional `ROLE_PROVIDER` seam (a persisted RBAC store) when registered — see
|
|
11
|
+
* {@link RolesGuard}.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* `@Roles('admin', 'teacher')` // allow if the user is an admin OR a teacher
|
|
15
|
+
*/
|
|
16
|
+
export function Roles(...roles) {
|
|
17
|
+
return SetMetadata(ROLES_METADATA, roles);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=roles.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.decorator.js","sourceRoot":"","sources":["../../src/decorator/roles.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,KAAK,CAAC,GAAG,KAAe;IACtC,OAAO,WAAW,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import diagnostics_channel from 'node:diagnostics_channel';
|
|
2
|
+
import type { Resource, User } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Channel name — the cross-repo wire contract with `@dudousxd/nestjs-authz-telescope`'s
|
|
5
|
+
* authorization watcher (and any other observer). Versioned by the payload's `v` field,
|
|
6
|
+
* not by name. Keep this string byte-identical on both sides.
|
|
7
|
+
*/
|
|
8
|
+
export declare const AUTHZ_DECISION_CHANNEL = "nestjs-authz:decision";
|
|
9
|
+
/**
|
|
10
|
+
* Memoized by Node: the same channel object is returned for the same name. Read
|
|
11
|
+
* `.hasSubscribers` to gate any work before publishing — when nothing subscribes
|
|
12
|
+
* (the common case) emitting a decision is effectively free.
|
|
13
|
+
*/
|
|
14
|
+
export declare const authzDecisionChannel: diagnostics_channel.Channel<unknown, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Every authorization decision the {@link Gate} reaches, published once per
|
|
17
|
+
* `allows`/`authorize`/`denies` call. `reason` names which resolution path
|
|
18
|
+
* produced the verdict so a 403 is debuggable.
|
|
19
|
+
*/
|
|
20
|
+
export interface AuthzDecisionDiagnostic {
|
|
21
|
+
v: 1;
|
|
22
|
+
/** The ability checked, e.g. `'update'` or `'access-admin'`. */
|
|
23
|
+
ability: string;
|
|
24
|
+
/** The verdict — `true` allow, `false` deny. */
|
|
25
|
+
allowed: boolean;
|
|
26
|
+
/** Which resolution path decided it (see {@link AuthzDecisionReason}). */
|
|
27
|
+
reason: AuthzDecisionReason;
|
|
28
|
+
/** A label for the resolved user (`<type>#<id>` / constructor name), or `null` when anonymous. */
|
|
29
|
+
userRef: string | null;
|
|
30
|
+
/** The resource class name the ability targeted, or `null` for a model-less ability. */
|
|
31
|
+
resourceType: string | null;
|
|
32
|
+
/** The resource's `id` when discoverable on the instance, else `null`. */
|
|
33
|
+
resourceId: string | number | null;
|
|
34
|
+
}
|
|
35
|
+
/** The resolution path that produced a decision — the "why" behind a 403 (or a grant). */
|
|
36
|
+
export type AuthzDecisionReason = 'super-admin' | 'permission-provider' | 'policy-before' | 'policy' | 'gate' | 'anonymous';
|
|
37
|
+
/**
|
|
38
|
+
* Publish a decision to the channel, but only when something is listening. Never
|
|
39
|
+
* throws — building the payload or notifying a subscriber must not break a check.
|
|
40
|
+
*/
|
|
41
|
+
export declare function publishAuthzDecision(ability: string, allowed: boolean, reason: AuthzDecisionReason, user: User, resource: Resource | undefined): void;
|
|
42
|
+
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,0BAA0B,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,+CAAsD,CAAC;AAExF;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,CAAC,EAAE,CAAC,CAAC;IACL,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,MAAM,EAAE,mBAAmB,CAAC;IAC5B,kGAAkG;IAClG,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wFAAwF;IACxF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,0EAA0E;IAC1E,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,0FAA0F;AAC1F,MAAM,MAAM,mBAAmB,GAC3B,aAAa,GACb,qBAAqB,GACrB,eAAe,GACf,QAAQ,GACR,MAAM,GACN,WAAW,CAAC;AAEhB;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAC7B,IAAI,CAgBN"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import diagnostics_channel from 'node:diagnostics_channel';
|
|
2
|
+
/**
|
|
3
|
+
* Channel name — the cross-repo wire contract with `@dudousxd/nestjs-authz-telescope`'s
|
|
4
|
+
* authorization watcher (and any other observer). Versioned by the payload's `v` field,
|
|
5
|
+
* not by name. Keep this string byte-identical on both sides.
|
|
6
|
+
*/
|
|
7
|
+
export const AUTHZ_DECISION_CHANNEL = 'nestjs-authz:decision';
|
|
8
|
+
/**
|
|
9
|
+
* Memoized by Node: the same channel object is returned for the same name. Read
|
|
10
|
+
* `.hasSubscribers` to gate any work before publishing — when nothing subscribes
|
|
11
|
+
* (the common case) emitting a decision is effectively free.
|
|
12
|
+
*/
|
|
13
|
+
export const authzDecisionChannel = diagnostics_channel.channel(AUTHZ_DECISION_CHANNEL);
|
|
14
|
+
/**
|
|
15
|
+
* Publish a decision to the channel, but only when something is listening. Never
|
|
16
|
+
* throws — building the payload or notifying a subscriber must not break a check.
|
|
17
|
+
*/
|
|
18
|
+
export function publishAuthzDecision(ability, allowed, reason, user, resource) {
|
|
19
|
+
if (!authzDecisionChannel.hasSubscribers)
|
|
20
|
+
return;
|
|
21
|
+
try {
|
|
22
|
+
const payload = {
|
|
23
|
+
v: 1,
|
|
24
|
+
ability,
|
|
25
|
+
allowed,
|
|
26
|
+
reason,
|
|
27
|
+
userRef: labelUser(user),
|
|
28
|
+
resourceType: resourceType(resource),
|
|
29
|
+
resourceId: resourceId(resource),
|
|
30
|
+
};
|
|
31
|
+
authzDecisionChannel.publish(payload);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Observability must never break authorization.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** A short, JSON-safe label for the user — `<type>#<id>`, a constructor name, or `null`. */
|
|
38
|
+
function labelUser(user) {
|
|
39
|
+
if (user == null)
|
|
40
|
+
return null;
|
|
41
|
+
if (typeof user === 'object') {
|
|
42
|
+
const obj = user;
|
|
43
|
+
const type = (obj.type ?? obj.constructor?.name);
|
|
44
|
+
const id = obj.id;
|
|
45
|
+
if (type && (typeof id === 'string' || typeof id === 'number'))
|
|
46
|
+
return `${type}#${id}`;
|
|
47
|
+
if (typeof obj.id === 'string' || typeof obj.id === 'number')
|
|
48
|
+
return String(obj.id);
|
|
49
|
+
return obj.constructor?.name ?? null;
|
|
50
|
+
}
|
|
51
|
+
return String(user);
|
|
52
|
+
}
|
|
53
|
+
/** The resource's class name (`Post`), or `null` when model-less. A class itself maps to its name. */
|
|
54
|
+
function resourceType(resource) {
|
|
55
|
+
if (resource == null)
|
|
56
|
+
return null;
|
|
57
|
+
if (typeof resource === 'function')
|
|
58
|
+
return resource.name ?? null;
|
|
59
|
+
return resource.constructor?.name ?? null;
|
|
60
|
+
}
|
|
61
|
+
/** The resource instance's `id`, when present and primitive; otherwise `null`. */
|
|
62
|
+
function resourceId(resource) {
|
|
63
|
+
if (resource == null || typeof resource === 'function')
|
|
64
|
+
return null;
|
|
65
|
+
const id = resource.id;
|
|
66
|
+
return typeof id === 'string' || typeof id === 'number' ? id : null;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,MAAM,0BAA0B,CAAC;AAG3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,uBAAuB,CAAC;AAE9D;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAgCxF;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,OAAgB,EAChB,MAA2B,EAC3B,IAAU,EACV,QAA8B;IAE9B,IAAI,CAAC,oBAAoB,CAAC,cAAc;QAAE,OAAO;IACjD,IAAI,CAAC;QACH,MAAM,OAAO,GAA4B;YACvC,CAAC,EAAE,CAAC;YACJ,OAAO;YACP,OAAO;YACP,MAAM;YACN,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC;YACxB,YAAY,EAAE,YAAY,CAAC,QAAQ,CAAC;YACpC,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC;SACjC,CAAC;QACF,oBAAoB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,gDAAgD;IAClD,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,SAAS,SAAS,CAAC,IAAU;IAC3B,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,IAA+B,CAAC;QAC5C,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,WAAW,EAAE,IAAI,CAAuB,CAAC;QACvE,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC;QAClB,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,CAAC;YAAE,OAAO,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;QACvF,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpF,OAAO,GAAG,CAAC,WAAW,EAAE,IAAI,IAAI,IAAI,CAAC;IACvC,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,sGAAsG;AACtG,SAAS,YAAY,CAAC,QAA8B;IAClD,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAQ,QAA8B,CAAC,IAAI,IAAI,IAAI,CAAC;IACxF,OAAQ,QAAgD,CAAC,WAAW,EAAE,IAAI,IAAI,IAAI,CAAC;AACrF,CAAC;AAED,kFAAkF;AAClF,SAAS,UAAU,CAAC,QAA8B;IAChD,IAAI,QAAQ,IAAI,IAAI,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACpE,MAAM,EAAE,GAAI,QAAoC,CAAC,EAAE,CAAC;IACpD,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC"}
|
package/dist/gate.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { ModuleRef } from '@nestjs/core';
|
|
|
2
2
|
import type { ContextAccessor } from './context-accessor.js';
|
|
3
3
|
import type { PermissionProvider } from './permission-provider.js';
|
|
4
4
|
import { PolicyRegistry } from './policy-registry.js';
|
|
5
|
+
import { type RoleProvider } from './role-provider.js';
|
|
5
6
|
import type { AuthzModuleOptions, GateFn, Resource, User } from './types.js';
|
|
6
7
|
declare const NO_USER: unique symbol;
|
|
7
8
|
type MaybeUser = User | typeof NO_USER;
|
|
@@ -21,10 +22,12 @@ export declare class Gate {
|
|
|
21
22
|
private readonly context?;
|
|
22
23
|
private readonly moduleRef?;
|
|
23
24
|
private readonly permissionProvider?;
|
|
25
|
+
private readonly roleProvider?;
|
|
24
26
|
private readonly gates;
|
|
25
27
|
private readonly superAdmin;
|
|
26
28
|
private readonly resolveUser;
|
|
27
|
-
|
|
29
|
+
private readonly roleResolver;
|
|
30
|
+
constructor(policies: PolicyRegistry, options: AuthzModuleOptions | undefined, context?: ContextAccessor | undefined, moduleRef?: ModuleRef | undefined, permissionProvider?: PermissionProvider | undefined, roleProvider?: RoleProvider | undefined);
|
|
28
31
|
/**
|
|
29
32
|
* Locate the context accessor. Prefers the value injected into this module;
|
|
30
33
|
* falls back to a non-strict {@link ModuleRef} lookup so an accessor provided
|
|
@@ -37,10 +40,22 @@ export declare class Gate {
|
|
|
37
40
|
* provider registered by ANY module (e.g. the RBAC adapter's global module) is found.
|
|
38
41
|
*/
|
|
39
42
|
private resolvePermissionProvider;
|
|
43
|
+
/**
|
|
44
|
+
* Locate the optional {@link RoleProvider} (the coarse role seam). Prefers the value
|
|
45
|
+
* injected into this module; falls back to a non-strict {@link ModuleRef} lookup so a
|
|
46
|
+
* provider registered by ANY module (e.g. the RBAC adapter's global module) is found.
|
|
47
|
+
*/
|
|
48
|
+
private resolveRoleProvider;
|
|
40
49
|
/** Register an ad-hoc, model-less gate resolved by `ability` name. */
|
|
41
50
|
define(ability: string, fn: GateFn): this;
|
|
42
51
|
/** True when an ad-hoc gate is registered for `ability`. */
|
|
43
52
|
hasGate(ability: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Names of every ad-hoc gate registered via {@link define}. Used by integrations
|
|
55
|
+
* (e.g. `@dudousxd/nestjs-authz-inertia`) that enumerate the user's class-level
|
|
56
|
+
* abilities to share them as Inertia props — no network round-trip needed.
|
|
57
|
+
*/
|
|
58
|
+
gateNames(): string[];
|
|
44
59
|
/**
|
|
45
60
|
* Bind an explicit user, bypassing the context accessor. Use when no
|
|
46
61
|
* nestjs-context is wired, or to check a user other than the current one.
|
|
@@ -61,9 +76,31 @@ export declare class Gate {
|
|
|
61
76
|
allows(ability: string, resource?: Resource): Promise<boolean>;
|
|
62
77
|
denies(ability: string, resource?: Resource): Promise<boolean>;
|
|
63
78
|
authorize(ability: string, resource?: Resource): Promise<void>;
|
|
79
|
+
/** True when the current user holds `role`. */
|
|
80
|
+
hasRole(role: string): Promise<boolean>;
|
|
81
|
+
/** True when the current user holds ANY of `roles`. */
|
|
82
|
+
hasAnyRole(roles: string[]): Promise<boolean>;
|
|
64
83
|
/** @internal */
|
|
65
84
|
allowsForUser(user: MaybeUser, ability: string, resource?: Resource): Promise<boolean>;
|
|
85
|
+
/** @internal */
|
|
86
|
+
hasAnyRoleForUser(user: MaybeUser, roles: string[]): Promise<boolean>;
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the user's effective roles and test membership against `roles`. Returns
|
|
89
|
+
* `false` for an anonymous (NO_USER) caller and whenever no source yields a role
|
|
90
|
+
* (deny-by-default). Roles come from the UNION of the default/overridden
|
|
91
|
+
* {@link RoleResolver} (reads the user object) and the optional {@link RoleProvider}
|
|
92
|
+
* seam (a persisted store) — so an app needs neither to opt in.
|
|
93
|
+
*/
|
|
94
|
+
private checkRoles;
|
|
95
|
+
/** The current user's effective role names (resolver ∪ provider). */
|
|
96
|
+
private rolesOf;
|
|
66
97
|
private check;
|
|
98
|
+
/**
|
|
99
|
+
* Resolve an ability to a verdict plus the path that decided it. Throws
|
|
100
|
+
* {@link AbilityNotResolvedException}/{@link AmbiguousAbilityException} when no
|
|
101
|
+
* decision can be reached (those paths emit no decision).
|
|
102
|
+
*/
|
|
103
|
+
private resolve;
|
|
67
104
|
private resolvePolicy;
|
|
68
105
|
}
|
|
69
106
|
/**
|
|
@@ -76,6 +113,10 @@ export declare class BoundGate {
|
|
|
76
113
|
allows(ability: string, resource?: Resource): Promise<boolean>;
|
|
77
114
|
denies(ability: string, resource?: Resource): Promise<boolean>;
|
|
78
115
|
authorize(ability: string, resource?: Resource): Promise<void>;
|
|
116
|
+
/** True when the bound user holds `role`. */
|
|
117
|
+
hasRole(role: string): Promise<boolean>;
|
|
118
|
+
/** True when the bound user holds ANY of `roles`. */
|
|
119
|
+
hasAnyRole(roles: string[]): Promise<boolean>;
|
|
79
120
|
}
|
|
80
121
|
export {};
|
|
81
122
|
//# sourceMappingURL=gate.d.ts.map
|
package/dist/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,eAAe,EAAW,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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"}
|