@spinajs/rbac 2.0.521 → 2.0.523
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/lib/cjs/actions.d.ts +269 -93
- package/lib/cjs/actions.d.ts.map +1 -1
- package/lib/cjs/actions.js +695 -354
- package/lib/cjs/actions.js.map +1 -1
- package/lib/cjs/auth.d.ts.map +1 -1
- package/lib/cjs/auth.js +6 -6
- package/lib/cjs/auth.js.map +1 -1
- package/lib/cjs/cli/ChangeUserPassword.d.ts.map +1 -1
- package/lib/cjs/cli/ChangeUserPassword.js +2 -2
- package/lib/cjs/cli/ChangeUserPassword.js.map +1 -1
- package/lib/cjs/cli/CreateUser.d.ts.map +1 -1
- package/lib/cjs/cli/CreateUser.js +8 -1
- package/lib/cjs/cli/CreateUser.js.map +1 -1
- package/lib/cjs/config/rbac.d.ts +40 -0
- package/lib/cjs/config/rbac.d.ts.map +1 -1
- package/lib/cjs/config/rbac.js +40 -0
- package/lib/cjs/config/rbac.js.map +1 -1
- package/lib/cjs/events/UserCreated.d.ts +2 -1
- package/lib/cjs/events/UserCreated.d.ts.map +1 -1
- package/lib/cjs/events/UserCreated.js +13 -3
- package/lib/cjs/events/UserCreated.js.map +1 -1
- package/lib/cjs/exceptions.d.ts +69 -0
- package/lib/cjs/exceptions.d.ts.map +1 -0
- package/lib/cjs/exceptions.js +86 -0
- package/lib/cjs/exceptions.js.map +1 -0
- package/lib/cjs/index.d.ts +3 -0
- package/lib/cjs/index.d.ts.map +1 -1
- package/lib/cjs/index.js +3 -0
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/interfaces.d.ts +2 -31
- package/lib/cjs/interfaces.d.ts.map +1 -1
- package/lib/cjs/interfaces.js +1 -12
- package/lib/cjs/interfaces.js.map +1 -1
- package/lib/cjs/middleware.d.ts +16 -6
- package/lib/cjs/middleware.d.ts.map +1 -1
- package/lib/cjs/middleware.js +151 -90
- package/lib/cjs/middleware.js.map +1 -1
- package/lib/cjs/orm-permission.d.ts +77 -0
- package/lib/cjs/orm-permission.d.ts.map +1 -0
- package/lib/cjs/orm-permission.js +128 -0
- package/lib/cjs/orm-permission.js.map +1 -0
- package/lib/cjs/password.d.ts +18 -1
- package/lib/cjs/password.d.ts.map +1 -1
- package/lib/cjs/password.js +61 -5
- package/lib/cjs/password.js.map +1 -1
- package/lib/cjs/permission-service.d.ts +80 -0
- package/lib/cjs/permission-service.d.ts.map +1 -0
- package/lib/cjs/permission-service.js +144 -0
- package/lib/cjs/permission-service.js.map +1 -0
- package/lib/cjs/profile.js +1 -1
- package/lib/cjs/profile.js.map +1 -1
- package/lib/mjs/actions.d.ts +269 -93
- package/lib/mjs/actions.d.ts.map +1 -1
- package/lib/mjs/actions.js +682 -358
- package/lib/mjs/actions.js.map +1 -1
- package/lib/mjs/auth.d.ts.map +1 -1
- package/lib/mjs/auth.js +7 -7
- package/lib/mjs/auth.js.map +1 -1
- package/lib/mjs/cli/ChangeUserPassword.d.ts.map +1 -1
- package/lib/mjs/cli/ChangeUserPassword.js +3 -3
- package/lib/mjs/cli/ChangeUserPassword.js.map +1 -1
- package/lib/mjs/cli/CreateUser.d.ts.map +1 -1
- package/lib/mjs/cli/CreateUser.js +8 -1
- package/lib/mjs/cli/CreateUser.js.map +1 -1
- package/lib/mjs/config/rbac.d.ts +40 -0
- package/lib/mjs/config/rbac.d.ts.map +1 -1
- package/lib/mjs/config/rbac.js +40 -0
- package/lib/mjs/config/rbac.js.map +1 -1
- package/lib/mjs/events/UserCreated.d.ts +2 -1
- package/lib/mjs/events/UserCreated.d.ts.map +1 -1
- package/lib/mjs/events/UserCreated.js +13 -3
- package/lib/mjs/events/UserCreated.js.map +1 -1
- package/lib/mjs/exceptions.d.ts +69 -0
- package/lib/mjs/exceptions.d.ts.map +1 -0
- package/lib/mjs/exceptions.js +71 -0
- package/lib/mjs/exceptions.js.map +1 -0
- package/lib/mjs/index.d.ts +3 -0
- package/lib/mjs/index.d.ts.map +1 -1
- package/lib/mjs/index.js +3 -0
- package/lib/mjs/index.js.map +1 -1
- package/lib/mjs/interfaces.d.ts +2 -31
- package/lib/mjs/interfaces.d.ts.map +1 -1
- package/lib/mjs/interfaces.js +0 -11
- package/lib/mjs/interfaces.js.map +1 -1
- package/lib/mjs/middleware.d.ts +16 -6
- package/lib/mjs/middleware.d.ts.map +1 -1
- package/lib/mjs/middleware.js +152 -91
- package/lib/mjs/middleware.js.map +1 -1
- package/lib/mjs/orm-permission.d.ts +77 -0
- package/lib/mjs/orm-permission.d.ts.map +1 -0
- package/lib/mjs/orm-permission.js +119 -0
- package/lib/mjs/orm-permission.js.map +1 -0
- package/lib/mjs/password.d.ts +18 -1
- package/lib/mjs/password.d.ts.map +1 -1
- package/lib/mjs/password.js +62 -6
- package/lib/mjs/password.js.map +1 -1
- package/lib/mjs/permission-service.d.ts +80 -0
- package/lib/mjs/permission-service.d.ts.map +1 -0
- package/lib/mjs/permission-service.js +137 -0
- package/lib/mjs/permission-service.js.map +1 -0
- package/lib/mjs/profile.js +2 -2
- package/lib/mjs/profile.js.map +1 -1
- package/lib/tsconfig.cjs.tsbuildinfo +1 -1
- package/lib/tsconfig.mjs.tsbuildinfo +1 -1
- package/package.json +11 -11
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Class } from '@spinajs/di';
|
|
2
|
+
import { InsertQueryBuilder, IWhereBuilder, ModelBase } from '@spinajs/orm';
|
|
3
|
+
import type { User } from './models/User.js';
|
|
4
|
+
export declare const DEFAULT_PERMISSION_SCOPE = "default";
|
|
5
|
+
/**
|
|
6
|
+
* Attribute prefix marking a named permission scope on a role grant (eg. `'scope:pool'`),
|
|
7
|
+
* read by `RbacModelPermissionMiddleware.policiesFor()` to pick a non-default
|
|
8
|
+
* `OrmPermissionPolicy`.
|
|
9
|
+
*
|
|
10
|
+
* WARNING: `accesscontrol`'s attribute union across a role and every role it `$extend`s
|
|
11
|
+
* collapses `['*']` merged with `['scope:x']` down to `['*']` (glob union absorption). A role
|
|
12
|
+
* granting `read:own: ['scope:pool']` while a role it `$extend`s (or that extends it) grants
|
|
13
|
+
* the same resource `['*']` silently loses the `scope:pool` token, and resolution falls
|
|
14
|
+
* through to the DEFAULT policy instead of the named one. This is a known limitation, not
|
|
15
|
+
* handled by this module — never mix `['*']` and `scope:` attributes for the SAME resource
|
|
16
|
+
* across an `$extend` chain.
|
|
17
|
+
*/
|
|
18
|
+
export declare const PERMISSION_SCOPE_ATTR_PREFIX = "scope:";
|
|
19
|
+
export declare const ORM_PERMISSION_POLICY_MAP = "__orm_permission_policy__";
|
|
20
|
+
/**
|
|
21
|
+
* Bound model recorded on a policy class by `@OrmPermission`. Needed because the map is
|
|
22
|
+
* keyed on the shared resource STRING (minification-safe), which two unrelated models can
|
|
23
|
+
* legally share for grant purposes — the model recorded here is what lets `policiesFor()`
|
|
24
|
+
* in middleware.ts pick the right policy for `builder.Model` instead of an arbitrary one.
|
|
25
|
+
*/
|
|
26
|
+
export declare const ORM_PERMISSION_MODEL: unique symbol;
|
|
27
|
+
/**
|
|
28
|
+
* Keyed on the DECLARED `@OrmResource` string, never on `constructor.name` — grants are
|
|
29
|
+
* keyed on the same string, so a model's resource name is the single permission identity
|
|
30
|
+
* everywhere, and models sharing a resource (a view over the base table) share policies.
|
|
31
|
+
*/
|
|
32
|
+
export declare function policyMapKey(resource: string, scope: string): string;
|
|
33
|
+
export declare abstract class OrmPermissionPolicy<M extends ModelBase<unknown> = ModelBase<unknown>> {
|
|
34
|
+
/**
|
|
35
|
+
* Default throws so an accidentally inert policy fails loud instead of running an
|
|
36
|
+
* unscoped query.
|
|
37
|
+
*/
|
|
38
|
+
scope(_query: IWhereBuilder<M>, _user: User): void;
|
|
39
|
+
scopeRead(query: IWhereBuilder<M>, user: User): void;
|
|
40
|
+
scopeUpdate(query: IWhereBuilder<M>, user: User): void;
|
|
41
|
+
scopeDelete(query: IWhereBuilder<M>, user: User): void;
|
|
42
|
+
/**
|
|
43
|
+
* Default denies: a policy that does not opt into insert control must not silently
|
|
44
|
+
* allow inserts under a `:own` grant. An authorization denial, not a config error, so
|
|
45
|
+
* `Forbidden` (403) — contrast `scope()`'s default, which stays `OrmException` because an
|
|
46
|
+
* unimplemented scope is a config bug. Callers with an `OwnerField` never reach this: see
|
|
47
|
+
* `RbacModelPermissionMiddleware.beforeQueryExecution`'s OwnerField fallback.
|
|
48
|
+
*/
|
|
49
|
+
authorizeCreate(_query: InsertQueryBuilder, _user: User): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export type OrmPermissionPolicyClass = Class<OrmPermissionPolicy>;
|
|
52
|
+
/** The model `@OrmPermission` bound `policy` to, or `undefined` for a policy built by hand
|
|
53
|
+
* (eg. the `OwnerFieldPolicy` fallback) that never went through the decorator.
|
|
54
|
+
*
|
|
55
|
+
* Read as an OWN property: `target[ORM_PERMISSION_MODEL] = model` stamps the constructor
|
|
56
|
+
* object itself, and ES class inheritance chains constructors ( `Sub.__proto__ === Base` ), so
|
|
57
|
+
* a plain property read here would let an undecorated `class Sub extends Base {}` silently
|
|
58
|
+
* inherit `Base`'s bound model instead of having none.
|
|
59
|
+
*/
|
|
60
|
+
export declare function ormPermissionModel(policy: OrmPermissionPolicyClass): Class<ModelBase<unknown>> | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* The single accessor for the policy map — used by both the registration side
|
|
63
|
+
* (`OrmPermission`) and the resolution side (`RbacModelPermissionMiddleware.policiesFor`) so
|
|
64
|
+
* the two can never read/write through differing DI cache accessors and drift apart.
|
|
65
|
+
*/
|
|
66
|
+
export declare function getOrCreatePolicyMap(): Map<string, OrmPermissionPolicyClass[]>;
|
|
67
|
+
/**
|
|
68
|
+
* Registers `target` as a policy for `model`'s resource + `scope` in the DI hashmap.
|
|
69
|
+
* Refuses a model without `@OrmResource` (cannot be permission-guarded). The map value is a
|
|
70
|
+
* LIST: two unrelated models are allowed to share one resource+scope key (the
|
|
71
|
+
* CampaignFileAttachment / ArrowV1CampaignView shape) — only the same (model, scope) pair
|
|
72
|
+
* registered twice is a duplicate/config bug.
|
|
73
|
+
*/
|
|
74
|
+
export declare function OrmPermission<M extends ModelBase<unknown>>(model: Class<M>, scope?: string): (target: Class<OrmPermissionPolicy<M>>) => void;
|
|
75
|
+
/** Test helper — wipes all registrations from the DI cache. */
|
|
76
|
+
export declare function clearOrmPermissionRegistry(): void;
|
|
77
|
+
//# sourceMappingURL=orm-permission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orm-permission.d.ts","sourceRoot":"","sources":["../../src/orm-permission.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAM,MAAM,aAAa,CAAC;AACxC,OAAO,EAA0B,kBAAkB,EAAE,aAAa,EAAE,SAAS,EAAgB,MAAM,cAAc,CAAC;AAGlH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,eAAO,MAAM,wBAAwB,YAAY,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,4BAA4B,WAAW,CAAC;AACrD,eAAO,MAAM,yBAAyB,8BAA8B,CAAC;AAErE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,eAAqC,CAAC;AAEvE;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED,8BAAsB,mBAAmB,CAAC,CAAC,SAAS,SAAS,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC;IACzF;;;OAGG;IACI,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,IAAI,CAExD;IAEM,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAE1D;IAEM,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAE5D;IAEM,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAE5D;IAED;;;;;;OAMG;IACU,eAAe,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAEnF;CACF;AAED,MAAM,MAAM,wBAAwB,GAAG,KAAK,CAAC,mBAAmB,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,wBAAwB,GAAG,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,GAAG,SAAS,CAG1G;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,GAAG,CAAC,MAAM,EAAE,wBAAwB,EAAE,CAAC,CAO9E;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,SAAS,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,GAAE,MAAiC,YACnG,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,KAAG,IAAI,CAqBrD;AAED,+DAA+D;AAC/D,wBAAgB,0BAA0B,IAAI,IAAI,CAIjD"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrmPermissionPolicy = exports.ORM_PERMISSION_MODEL = exports.ORM_PERMISSION_POLICY_MAP = exports.PERMISSION_SCOPE_ATTR_PREFIX = exports.DEFAULT_PERMISSION_SCOPE = void 0;
|
|
4
|
+
exports.policyMapKey = policyMapKey;
|
|
5
|
+
exports.ormPermissionModel = ormPermissionModel;
|
|
6
|
+
exports.getOrCreatePolicyMap = getOrCreatePolicyMap;
|
|
7
|
+
exports.OrmPermission = OrmPermission;
|
|
8
|
+
exports.clearOrmPermissionRegistry = clearOrmPermissionRegistry;
|
|
9
|
+
const di_1 = require("@spinajs/di");
|
|
10
|
+
const orm_1 = require("@spinajs/orm");
|
|
11
|
+
const exceptions_1 = require("@spinajs/exceptions");
|
|
12
|
+
exports.DEFAULT_PERMISSION_SCOPE = 'default';
|
|
13
|
+
/**
|
|
14
|
+
* Attribute prefix marking a named permission scope on a role grant (eg. `'scope:pool'`),
|
|
15
|
+
* read by `RbacModelPermissionMiddleware.policiesFor()` to pick a non-default
|
|
16
|
+
* `OrmPermissionPolicy`.
|
|
17
|
+
*
|
|
18
|
+
* WARNING: `accesscontrol`'s attribute union across a role and every role it `$extend`s
|
|
19
|
+
* collapses `['*']` merged with `['scope:x']` down to `['*']` (glob union absorption). A role
|
|
20
|
+
* granting `read:own: ['scope:pool']` while a role it `$extend`s (or that extends it) grants
|
|
21
|
+
* the same resource `['*']` silently loses the `scope:pool` token, and resolution falls
|
|
22
|
+
* through to the DEFAULT policy instead of the named one. This is a known limitation, not
|
|
23
|
+
* handled by this module — never mix `['*']` and `scope:` attributes for the SAME resource
|
|
24
|
+
* across an `$extend` chain.
|
|
25
|
+
*/
|
|
26
|
+
exports.PERMISSION_SCOPE_ATTR_PREFIX = 'scope:';
|
|
27
|
+
exports.ORM_PERMISSION_POLICY_MAP = '__orm_permission_policy__';
|
|
28
|
+
/**
|
|
29
|
+
* Bound model recorded on a policy class by `@OrmPermission`. Needed because the map is
|
|
30
|
+
* keyed on the shared resource STRING (minification-safe), which two unrelated models can
|
|
31
|
+
* legally share for grant purposes — the model recorded here is what lets `policiesFor()`
|
|
32
|
+
* in middleware.ts pick the right policy for `builder.Model` instead of an arbitrary one.
|
|
33
|
+
*/
|
|
34
|
+
exports.ORM_PERMISSION_MODEL = Symbol.for('orm-permission-model');
|
|
35
|
+
/**
|
|
36
|
+
* Keyed on the DECLARED `@OrmResource` string, never on `constructor.name` — grants are
|
|
37
|
+
* keyed on the same string, so a model's resource name is the single permission identity
|
|
38
|
+
* everywhere, and models sharing a resource (a view over the base table) share policies.
|
|
39
|
+
*/
|
|
40
|
+
function policyMapKey(resource, scope) {
|
|
41
|
+
return `${resource}:${scope}`;
|
|
42
|
+
}
|
|
43
|
+
class OrmPermissionPolicy {
|
|
44
|
+
/**
|
|
45
|
+
* Default throws so an accidentally inert policy fails loud instead of running an
|
|
46
|
+
* unscoped query.
|
|
47
|
+
*/
|
|
48
|
+
scope(_query, _user) {
|
|
49
|
+
throw new orm_1.OrmException(`${this.constructor.name} defines neither a generic scope() nor a specific hook for this operation`);
|
|
50
|
+
}
|
|
51
|
+
scopeRead(query, user) {
|
|
52
|
+
this.scope(query, user);
|
|
53
|
+
}
|
|
54
|
+
scopeUpdate(query, user) {
|
|
55
|
+
this.scope(query, user);
|
|
56
|
+
}
|
|
57
|
+
scopeDelete(query, user) {
|
|
58
|
+
this.scope(query, user);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Default denies: a policy that does not opt into insert control must not silently
|
|
62
|
+
* allow inserts under a `:own` grant. An authorization denial, not a config error, so
|
|
63
|
+
* `Forbidden` (403) — contrast `scope()`'s default, which stays `OrmException` because an
|
|
64
|
+
* unimplemented scope is a config bug. Callers with an `OwnerField` never reach this: see
|
|
65
|
+
* `RbacModelPermissionMiddleware.beforeQueryExecution`'s OwnerField fallback.
|
|
66
|
+
*/
|
|
67
|
+
async authorizeCreate(_query, _user) {
|
|
68
|
+
throw new exceptions_1.Forbidden(`${this.constructor.name} does not implement authorizeCreate — INSERT under :own is not permitted by this policy`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.OrmPermissionPolicy = OrmPermissionPolicy;
|
|
72
|
+
/** The model `@OrmPermission` bound `policy` to, or `undefined` for a policy built by hand
|
|
73
|
+
* (eg. the `OwnerFieldPolicy` fallback) that never went through the decorator.
|
|
74
|
+
*
|
|
75
|
+
* Read as an OWN property: `target[ORM_PERMISSION_MODEL] = model` stamps the constructor
|
|
76
|
+
* object itself, and ES class inheritance chains constructors ( `Sub.__proto__ === Base` ), so
|
|
77
|
+
* a plain property read here would let an undecorated `class Sub extends Base {}` silently
|
|
78
|
+
* inherit `Base`'s bound model instead of having none.
|
|
79
|
+
*/
|
|
80
|
+
function ormPermissionModel(policy) {
|
|
81
|
+
const target = policy;
|
|
82
|
+
return Object.prototype.hasOwnProperty.call(policy, exports.ORM_PERMISSION_MODEL) ? target[exports.ORM_PERMISSION_MODEL] : undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The single accessor for the policy map — used by both the registration side
|
|
86
|
+
* (`OrmPermission`) and the resolution side (`RbacModelPermissionMiddleware.policiesFor`) so
|
|
87
|
+
* the two can never read/write through differing DI cache accessors and drift apart.
|
|
88
|
+
*/
|
|
89
|
+
function getOrCreatePolicyMap() {
|
|
90
|
+
if (di_1.DI.RootContainer.Cache.has(exports.ORM_PERMISSION_POLICY_MAP)) {
|
|
91
|
+
return di_1.DI.RootContainer.Cache.get(exports.ORM_PERMISSION_POLICY_MAP)[0];
|
|
92
|
+
}
|
|
93
|
+
const map = new Map();
|
|
94
|
+
di_1.DI.RootContainer.Cache.add(exports.ORM_PERMISSION_POLICY_MAP, map);
|
|
95
|
+
return map;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Registers `target` as a policy for `model`'s resource + `scope` in the DI hashmap.
|
|
99
|
+
* Refuses a model without `@OrmResource` (cannot be permission-guarded). The map value is a
|
|
100
|
+
* LIST: two unrelated models are allowed to share one resource+scope key (the
|
|
101
|
+
* CampaignFileAttachment / ArrowV1CampaignView shape) — only the same (model, scope) pair
|
|
102
|
+
* registered twice is a duplicate/config bug.
|
|
103
|
+
*/
|
|
104
|
+
function OrmPermission(model, scope = exports.DEFAULT_PERMISSION_SCOPE) {
|
|
105
|
+
return (target) => {
|
|
106
|
+
const descriptor = (0, orm_1.extractModelDescriptor)(model);
|
|
107
|
+
const resource = descriptor?.RbacResource;
|
|
108
|
+
if (!resource) {
|
|
109
|
+
throw new orm_1.OrmException(`cannot register ${target.name} for ${model.name}: model has no @OrmResource declaration`);
|
|
110
|
+
}
|
|
111
|
+
target[exports.ORM_PERMISSION_MODEL] = model;
|
|
112
|
+
const key = policyMapKey(resource, scope);
|
|
113
|
+
const map = getOrCreatePolicyMap();
|
|
114
|
+
const list = map.get(key) ?? [];
|
|
115
|
+
if (list.some((p) => ormPermissionModel(p) === model)) {
|
|
116
|
+
throw new orm_1.OrmException(`duplicate OrmPermission registration for ${model.name}/${scope} (${target.name} already registered for this model+scope)`);
|
|
117
|
+
}
|
|
118
|
+
list.push(target);
|
|
119
|
+
map.set(key, list);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Test helper — wipes all registrations from the DI cache. */
|
|
123
|
+
function clearOrmPermissionRegistry() {
|
|
124
|
+
if (di_1.DI.RootContainer.Cache.has(exports.ORM_PERMISSION_POLICY_MAP)) {
|
|
125
|
+
di_1.DI.RootContainer.Cache.remove(exports.ORM_PERMISSION_POLICY_MAP);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=orm-permission.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orm-permission.js","sourceRoot":"","sources":["../../src/orm-permission.ts"],"names":[],"mappings":"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"}
|
package/lib/cjs/password.d.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { PasswordProvider, PasswordValidationProvider } from './interfaces.js';
|
|
2
2
|
import { DataValidator } from '@spinajs/validation';
|
|
3
3
|
/**
|
|
4
|
-
* Simple password service that use argon2 hash alghoritm
|
|
4
|
+
* Simple password service that use argon2 hash alghoritm
|
|
5
5
|
*/
|
|
6
6
|
export declare class BasicPasswordProvider implements PasswordProvider {
|
|
7
|
+
protected GeneratorOptions: {
|
|
8
|
+
length: number;
|
|
9
|
+
characters: string[];
|
|
10
|
+
};
|
|
11
|
+
protected Validation: PasswordValidationProvider;
|
|
7
12
|
hash(input: string): Promise<string>;
|
|
8
13
|
/**
|
|
9
14
|
*
|
|
@@ -13,6 +18,18 @@ export declare class BasicPasswordProvider implements PasswordProvider {
|
|
|
13
18
|
* @param password - password to validate
|
|
14
19
|
*/
|
|
15
20
|
verify(hash: string, password: string): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* A random password drawn from `rbac.password.generator` and guaranteed to
|
|
23
|
+
* satisfy `rbac.password.validation.rule`.
|
|
24
|
+
*
|
|
25
|
+
* The guarantee matters: a generated password is what a freshly created
|
|
26
|
+
* account holds, and one that fails the application's own rule is a password
|
|
27
|
+
* the account can never legitimately return to.
|
|
28
|
+
*
|
|
29
|
+
* @throws UnexpectedServerError when the configured pool cannot produce a
|
|
30
|
+
* password the rule accepts. That is a configuration fault nobody calling
|
|
31
|
+
* this can fix, so it must never reach a client as a 400.
|
|
32
|
+
*/
|
|
16
33
|
generate(): string;
|
|
17
34
|
}
|
|
18
35
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/password.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAI/E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"password.d.ts","sourceRoot":"","sources":["../../src/password.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAI/E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AA6BpD;;GAEG;AACH,qBACa,qBAAsB,YAAW,gBAAgB;IAE5D,SAAS,CAAC,gBAAgB,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAGrE,SAAS,CAAC,UAAU,EAAE,0BAA0B,CAAC;IAEpC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGhD;IAED;;;;;;OAMG;IACU,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEpE;IAED;;;;;;;;;;;OAWG;IACI,QAAQ,IAAI,MAAM,CAoBxB;CACF;AAED;;GAEG;AACH,qBACa,+BAAgC,SAAQ,0BAA0B;IAE7E,SAAS,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAGnC,SAAS,CAAC,SAAS,EAAE,aAAa,CAAC;IAE5B,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGtC;CACF"}
|
package/lib/cjs/password.js
CHANGED
|
@@ -49,9 +49,33 @@ const argon = __importStar(require("argon2"));
|
|
|
49
49
|
const di_1 = require("@spinajs/di");
|
|
50
50
|
const configuration_1 = require("@spinajs/configuration");
|
|
51
51
|
const validation_1 = require("@spinajs/validation");
|
|
52
|
-
const
|
|
52
|
+
const exceptions_1 = require("@spinajs/exceptions");
|
|
53
|
+
const crypto_1 = require("crypto");
|
|
53
54
|
/**
|
|
54
|
-
*
|
|
55
|
+
* How many draws `generate()` makes before declaring the generator pool and the
|
|
56
|
+
* validation rule incompatible.
|
|
57
|
+
*
|
|
58
|
+
* The pool says which characters may appear, not which classes are MANDATORY, so
|
|
59
|
+
* a uniform draw can legitimately miss a class the rule demands - with a
|
|
60
|
+
* 62-character alphanumeric pool at length 16, about 6% of draws contain no
|
|
61
|
+
* digit. One draw plus an assertion would therefore fail 6% of the time, which
|
|
62
|
+
* is a broken generator rather than a misconfiguration signal. Ten draws bring a
|
|
63
|
+
* spurious failure to roughly 6e-13, while a pool that genuinely cannot satisfy
|
|
64
|
+
* the rule still exhausts every attempt and throws.
|
|
65
|
+
*/
|
|
66
|
+
const GENERATE_ATTEMPTS = 10;
|
|
67
|
+
/**
|
|
68
|
+
* Used when an application has not declared `rbac.password.generator`. Mirrors
|
|
69
|
+
* the shipped config default, so an application that never configures a
|
|
70
|
+
* generator still creates usable accounts - the same reason `_create_middleware`
|
|
71
|
+
* carries a fallback for `rbac.actions`.
|
|
72
|
+
*
|
|
73
|
+
* A DECLARED but empty pool still throws: that is a misconfiguration, not an
|
|
74
|
+
* omission.
|
|
75
|
+
*/
|
|
76
|
+
const DEFAULT_GENERATOR = { length: 16, characters: ['abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', '0123456789'] };
|
|
77
|
+
/**
|
|
78
|
+
* Simple password service that use argon2 hash alghoritm
|
|
55
79
|
*/
|
|
56
80
|
let BasicPasswordProvider = class BasicPasswordProvider {
|
|
57
81
|
async hash(input) {
|
|
@@ -68,13 +92,45 @@ let BasicPasswordProvider = class BasicPasswordProvider {
|
|
|
68
92
|
async verify(hash, password) {
|
|
69
93
|
return await argon.verify(hash, password);
|
|
70
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* A random password drawn from `rbac.password.generator` and guaranteed to
|
|
97
|
+
* satisfy `rbac.password.validation.rule`.
|
|
98
|
+
*
|
|
99
|
+
* The guarantee matters: a generated password is what a freshly created
|
|
100
|
+
* account holds, and one that fails the application's own rule is a password
|
|
101
|
+
* the account can never legitimately return to.
|
|
102
|
+
*
|
|
103
|
+
* @throws UnexpectedServerError when the configured pool cannot produce a
|
|
104
|
+
* password the rule accepts. That is a configuration fault nobody calling
|
|
105
|
+
* this can fix, so it must never reach a client as a 400.
|
|
106
|
+
*/
|
|
71
107
|
generate() {
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
108
|
+
const options = this.GeneratorOptions ?? DEFAULT_GENERATOR;
|
|
109
|
+
const length = options.length ?? DEFAULT_GENERATOR.length;
|
|
110
|
+
const pool = (options.characters ?? DEFAULT_GENERATOR.characters).join('');
|
|
111
|
+
if (length < 1 || pool.length === 0) {
|
|
112
|
+
throw new exceptions_1.UnexpectedServerError('rbac.password.generator must define a positive length and a non-empty character pool');
|
|
113
|
+
}
|
|
114
|
+
for (let attempt = 0; attempt < GENERATE_ATTEMPTS; attempt++) {
|
|
115
|
+
// randomInt is the CSPRNG - Math.random is predictable from a handful of
|
|
116
|
+
// outputs, and this value guards an account.
|
|
117
|
+
const candidate = Array.from({ length }, () => pool[(0, crypto_1.randomInt)(pool.length)]).join('');
|
|
118
|
+
if (this.Validation.check(candidate)) {
|
|
119
|
+
return candidate;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw new exceptions_1.UnexpectedServerError(`Could not generate a password satisfying rbac.password.validation.rule in ${GENERATE_ATTEMPTS} attempts. The generator character pool and the validation rule disagree - check rbac.password.generator.`);
|
|
75
123
|
}
|
|
76
124
|
};
|
|
77
125
|
exports.BasicPasswordProvider = BasicPasswordProvider;
|
|
126
|
+
__decorate([
|
|
127
|
+
(0, configuration_1.Config)('rbac.password.generator'),
|
|
128
|
+
__metadata("design:type", Object)
|
|
129
|
+
], BasicPasswordProvider.prototype, "GeneratorOptions", void 0);
|
|
130
|
+
__decorate([
|
|
131
|
+
(0, configuration_1.AutoinjectService)('rbac.password.validation'),
|
|
132
|
+
__metadata("design:type", interfaces_js_1.PasswordValidationProvider)
|
|
133
|
+
], BasicPasswordProvider.prototype, "Validation", void 0);
|
|
78
134
|
exports.BasicPasswordProvider = BasicPasswordProvider = __decorate([
|
|
79
135
|
(0, di_1.Injectable)(interfaces_js_1.PasswordProvider)
|
|
80
136
|
], BasicPasswordProvider);
|
package/lib/cjs/password.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/password.ts"],"names":[],"mappings":";AAAA,4CAA4C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE5C,mDAA+E;AAC/E,MAAY,KAAK,mCAAe;AAChC,oCAAqD;AACrD,
|
|
1
|
+
{"version":3,"file":"password.js","sourceRoot":"","sources":["../../src/password.ts"],"names":[],"mappings":";AAAA,4CAA4C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE5C,mDAA+E;AAC/E,MAAY,KAAK,mCAAe;AAChC,oCAAqD;AACrD,0DAAmE;AACnE,oDAAoD;AACpD,oDAA4D;AAC5D,mCAAmC;AAEnC;;;;;;;;;;;GAWG;AACH,MAAM,iBAAiB,GAAG,EAAE,CAAC;AAE7B;;;;;;;;GAQG;AACH,MAAM,iBAAiB,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,4BAA4B,EAAE,4BAA4B,EAAE,YAAY,CAAC,EAAE,CAAC;AAEjI;;GAEG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAOzB,KAAK,CAAC,IAAI,CAAC,KAAa;QAC7B,gDAAgD;QAChD,OAAO,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACjC,CAAC;IAED;;;;;;OAMG;IACI,KAAK,CAAC,MAAM,CAAC,IAAY,EAAE,QAAgB;QAChD,OAAO,MAAM,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IAC5C,CAAC;IAED;;;;;;;;;;;OAWG;IACI,QAAQ;QACb,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,IAAI,iBAAiB,CAAC;QAC3D,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,iBAAiB,CAAC,MAAM,CAAC;QAC1D,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,UAAU,IAAI,iBAAiB,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAE3E,IAAI,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,kCAAqB,CAAC,sFAAsF,CAAC,CAAC;QAC1H,CAAC;QAED,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,iBAAiB,EAAE,OAAO,EAAE,EAAE,CAAC;YAC7D,yEAAyE;YACzE,6CAA6C;YAC7C,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAA,kBAAS,EAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAEtF,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;gBACrC,OAAO,SAAS,CAAC;YACnB,CAAC;QACH,CAAC;QAED,MAAM,IAAI,kCAAqB,CAAC,6EAA6E,iBAAiB,2GAA2G,CAAC,CAAC;IAC7O,CAAC;CACF,CAAA;;AAtDW;IADT,IAAA,sBAAM,EAAC,yBAAyB,CAAC;;+DACmC;AAG3D;IADT,IAAA,iCAAiB,EAAC,0BAA0B,CAAC;8BACxB,0CAA0B;yDAAC;gCALtC,qBAAqB;IADjC,IAAA,eAAU,EAAC,gCAAgB,CAAC;GAChB,qBAAqB,CAwDjC;AAED;;GAEG;AAEI,IAAM,+BAA+B,GAArC,MAAM,+BAAgC,SAAQ,0CAA0B;IAOtE,KAAK,CAAC,QAAgB;QAC3B,MAAM,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAe,CAAC,CAAC;QACpF,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAA;;AATW;IADT,IAAA,sBAAM,EAAC,+BAA+B,CAAC;;yEACL;AAGzB;IADT,IAAA,eAAU,GAAE;8BACQ,0BAAa;kEAAC;0CALxB,+BAA+B;IAD3C,IAAA,eAAU,EAAC,0CAA0B,CAAC;GAC1B,+BAA+B,CAW3C"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Permission } from 'accesscontrol';
|
|
2
|
+
import { Class } from '@spinajs/di';
|
|
3
|
+
import type { User } from './models/User.js';
|
|
4
|
+
export type PermissionVerb = 'create' | 'read' | 'update' | 'delete';
|
|
5
|
+
export type PermissionGrantScope = 'any' | 'own' | 'none';
|
|
6
|
+
/**
|
|
7
|
+
* Base for per-domain permission services: decides and explains authorization at the
|
|
8
|
+
* domain layer, BEFORE any effect — the ORM rbac middleware stays the enforcement
|
|
9
|
+
* backstop. One concrete class per rbac resource; public API is domain-named assert
|
|
10
|
+
* methods built from the protected primitives below. The rbac User model is the actor —
|
|
11
|
+
* no wrapper objects.
|
|
12
|
+
*
|
|
13
|
+
* Stateless by contract: no instance fields in subclasses. Spinajs DI caches resolved
|
|
14
|
+
* instances, so `@FromDI()` injection in controllers, `@Autoinject` fields and
|
|
15
|
+
* `usePermission()` anywhere else share one instance safely.
|
|
16
|
+
*/
|
|
17
|
+
export declare abstract class PermissionService {
|
|
18
|
+
/** Rbac resource name this service guards — same string the grants config uses. */
|
|
19
|
+
protected abstract readonly Resource: string;
|
|
20
|
+
/**
|
|
21
|
+
* The acting user: explicit argument wins, else the rbac AsyncLocalStorage store.
|
|
22
|
+
* No user anywhere throws — a permission decision must never silently pass without
|
|
23
|
+
* an identity.
|
|
24
|
+
*/
|
|
25
|
+
protected user(user?: User): User;
|
|
26
|
+
/** Roles the decision runs against — the shared ActiveRole rule against the ambient store. */
|
|
27
|
+
protected effectiveRoles(user: User): string[];
|
|
28
|
+
protected grantScope(user: User, verb: PermissionVerb): PermissionGrantScope;
|
|
29
|
+
/** Single guarded probe against this.Resource — an unknown role answers false. */
|
|
30
|
+
protected can(roles: string[], permission: string): boolean;
|
|
31
|
+
/** Resolve acting user + scope, refusing `none` with the (overridable) domain error. */
|
|
32
|
+
protected assertGrant(verb: PermissionVerb, user?: User): {
|
|
33
|
+
user: User;
|
|
34
|
+
scope: 'any' | 'own';
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Runs `fn` as the given user, so rbac-scoped ORM queries inside answer "what does
|
|
38
|
+
* THIS user see" — ownership rules stay stated once, in the query layer. The store's
|
|
39
|
+
* own user runs in the ambient store untouched; any other user impersonates: User is
|
|
40
|
+
* swapped and ActiveRole cleared (it belongs to the ambient user only), everything
|
|
41
|
+
* else in the store kept.
|
|
42
|
+
*/
|
|
43
|
+
protected withUser<T>(user: User, fn: () => T | Promise<T>): Promise<T>;
|
|
44
|
+
protected noPermissionError(user: User, verb: PermissionVerb): Error;
|
|
45
|
+
protected noUserError(): Error;
|
|
46
|
+
private storage;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Retrieves a permission service from DI — sugar for domain services and plain
|
|
50
|
+
* functions down the callstack, so code reads as intent
|
|
51
|
+
* (`usePermission(ContentEntriesPermission)`) instead of container plumbing.
|
|
52
|
+
* Answers the same DI-cached instance controllers receive via `@FromDI()`.
|
|
53
|
+
*
|
|
54
|
+
* Accepts ONLY PermissionService subclasses — the generic constraint is erased at
|
|
55
|
+
* runtime, so this is enforced here too instead of degrading into a bare DI.resolve.
|
|
56
|
+
*/
|
|
57
|
+
export declare function usePermission<T extends PermissionService>(type: Class<T>): T;
|
|
58
|
+
/**
|
|
59
|
+
* THE guarded accesscontrol probe — single home for the AccessControlError semantics
|
|
60
|
+
* previously copy-pasted into the ORM query middleware and rbac-http's RbacPolicy.
|
|
61
|
+
* Answers the Permission object (grant attributes stay reachable), or null for a role
|
|
62
|
+
* unknown to accesscontrol — which throws AccessControlError for any role absent from
|
|
63
|
+
* the grants map and rejects the whole role array on one unknown member. A genuine
|
|
64
|
+
* programming error (bad permission name) still propagates loud.
|
|
65
|
+
*/
|
|
66
|
+
export declare function probeGrant(roles: string | string[], permission: string, resource: string): Permission | null;
|
|
67
|
+
/**
|
|
68
|
+
* THE ActiveRole rule — single home for the narrowing previously inlined in the ORM
|
|
69
|
+
* query middleware and rbac-http's checkRoutePermission. The session's ActiveRole
|
|
70
|
+
* applies only to the user it was selected for — matched by id; any other user answers
|
|
71
|
+
* their full role list.
|
|
72
|
+
*
|
|
73
|
+
* The storage parameter is structural (not IRbacAsyncStorage) so both the rbac ALS
|
|
74
|
+
* store and http's request storage (whose User is `User | null`) fit.
|
|
75
|
+
*/
|
|
76
|
+
export declare function effectiveRoles(user: User, storage?: {
|
|
77
|
+
User?: User | null;
|
|
78
|
+
ActiveRole?: string;
|
|
79
|
+
}): string[];
|
|
80
|
+
//# sourceMappingURL=permission-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-service.d.ts","sourceRoot":"","sources":["../../src/permission-service.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAM,MAAM,aAAa,CAAC;AAGxC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AACrE,MAAM,MAAM,oBAAoB,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,8BAAsB,iBAAiB;IACrC,mFAAmF;IACnF,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE7C;;;;OAIG;IACH,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAWhC;IAED,8FAA8F;IAC9F,SAAS,CAAC,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAE7C;IAED,SAAS,CAAC,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,GAAG,oBAAoB,CAU3E;IAED,kFAAkF;IAClF,SAAS,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAE1D;IAED,wFAAwF;IACxF,SAAS,CAAC,WAAW,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,KAAK,GAAG,KAAK,CAAA;KAAE,CAS7F;IAED;;;;;;OAMG;IACH,UAAgB,QAAQ,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAS5E;IAED,SAAS,CAAC,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,GAAG,KAAK,CAEnE;IAED,SAAS,CAAC,WAAW,IAAI,KAAK,CAE7B;IAED,OAAO,CAAC,OAAO;CAGhB;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,iBAAiB,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAM5E;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAc5G;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,EAAE,CAK1G"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PermissionService = void 0;
|
|
4
|
+
exports.usePermission = usePermission;
|
|
5
|
+
exports.probeGrant = probeGrant;
|
|
6
|
+
exports.effectiveRoles = effectiveRoles;
|
|
7
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
8
|
+
const accesscontrol_1 = require("accesscontrol");
|
|
9
|
+
const di_1 = require("@spinajs/di");
|
|
10
|
+
const exceptions_1 = require("@spinajs/exceptions");
|
|
11
|
+
/**
|
|
12
|
+
* Base for per-domain permission services: decides and explains authorization at the
|
|
13
|
+
* domain layer, BEFORE any effect — the ORM rbac middleware stays the enforcement
|
|
14
|
+
* backstop. One concrete class per rbac resource; public API is domain-named assert
|
|
15
|
+
* methods built from the protected primitives below. The rbac User model is the actor —
|
|
16
|
+
* no wrapper objects.
|
|
17
|
+
*
|
|
18
|
+
* Stateless by contract: no instance fields in subclasses. Spinajs DI caches resolved
|
|
19
|
+
* instances, so `@FromDI()` injection in controllers, `@Autoinject` fields and
|
|
20
|
+
* `usePermission()` anywhere else share one instance safely.
|
|
21
|
+
*/
|
|
22
|
+
class PermissionService {
|
|
23
|
+
/**
|
|
24
|
+
* The acting user: explicit argument wins, else the rbac AsyncLocalStorage store.
|
|
25
|
+
* No user anywhere throws — a permission decision must never silently pass without
|
|
26
|
+
* an identity.
|
|
27
|
+
*/
|
|
28
|
+
user(user) {
|
|
29
|
+
if (user) {
|
|
30
|
+
return user;
|
|
31
|
+
}
|
|
32
|
+
const store = this.storage().getStore();
|
|
33
|
+
if (!store?.User) {
|
|
34
|
+
throw this.noUserError();
|
|
35
|
+
}
|
|
36
|
+
return store.User;
|
|
37
|
+
}
|
|
38
|
+
/** Roles the decision runs against — the shared ActiveRole rule against the ambient store. */
|
|
39
|
+
effectiveRoles(user) {
|
|
40
|
+
return effectiveRoles(user, this.storage().getStore());
|
|
41
|
+
}
|
|
42
|
+
grantScope(user, verb) {
|
|
43
|
+
const roles = this.effectiveRoles(user);
|
|
44
|
+
if (this.can(roles, `${verb}Any`)) {
|
|
45
|
+
return 'any';
|
|
46
|
+
}
|
|
47
|
+
if (this.can(roles, `${verb}Own`)) {
|
|
48
|
+
return 'own';
|
|
49
|
+
}
|
|
50
|
+
return 'none';
|
|
51
|
+
}
|
|
52
|
+
/** Single guarded probe against this.Resource — an unknown role answers false. */
|
|
53
|
+
can(roles, permission) {
|
|
54
|
+
return probeGrant(roles, permission, this.Resource)?.granted ?? false;
|
|
55
|
+
}
|
|
56
|
+
/** Resolve acting user + scope, refusing `none` with the (overridable) domain error. */
|
|
57
|
+
assertGrant(verb, user) {
|
|
58
|
+
const acting = this.user(user);
|
|
59
|
+
const scope = this.grantScope(acting, verb);
|
|
60
|
+
if (scope === 'none') {
|
|
61
|
+
throw this.noPermissionError(acting, verb);
|
|
62
|
+
}
|
|
63
|
+
return { user: acting, scope };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Runs `fn` as the given user, so rbac-scoped ORM queries inside answer "what does
|
|
67
|
+
* THIS user see" — ownership rules stay stated once, in the query layer. The store's
|
|
68
|
+
* own user runs in the ambient store untouched; any other user impersonates: User is
|
|
69
|
+
* swapped and ActiveRole cleared (it belongs to the ambient user only), everything
|
|
70
|
+
* else in the store kept.
|
|
71
|
+
*/
|
|
72
|
+
async withUser(user, fn) {
|
|
73
|
+
const storage = this.storage();
|
|
74
|
+
const parent = storage.getStore() ?? {};
|
|
75
|
+
if (parent.User?.Id === user.Id) {
|
|
76
|
+
return fn();
|
|
77
|
+
}
|
|
78
|
+
return storage.run({ ...parent, User: user, ActiveRole: undefined }, async () => fn());
|
|
79
|
+
}
|
|
80
|
+
noPermissionError(user, verb) {
|
|
81
|
+
return new exceptions_1.Forbidden(`user ${user.Id} has no ${verb} grant for resource ${this.Resource}`);
|
|
82
|
+
}
|
|
83
|
+
noUserError() {
|
|
84
|
+
return new exceptions_1.Forbidden('no acting user: none was given and none is present in the execution context');
|
|
85
|
+
}
|
|
86
|
+
storage() {
|
|
87
|
+
return di_1.DI.resolve(node_async_hooks_1.AsyncLocalStorage);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
exports.PermissionService = PermissionService;
|
|
91
|
+
/**
|
|
92
|
+
* Retrieves a permission service from DI — sugar for domain services and plain
|
|
93
|
+
* functions down the callstack, so code reads as intent
|
|
94
|
+
* (`usePermission(ContentEntriesPermission)`) instead of container plumbing.
|
|
95
|
+
* Answers the same DI-cached instance controllers receive via `@FromDI()`.
|
|
96
|
+
*
|
|
97
|
+
* Accepts ONLY PermissionService subclasses — the generic constraint is erased at
|
|
98
|
+
* runtime, so this is enforced here too instead of degrading into a bare DI.resolve.
|
|
99
|
+
*/
|
|
100
|
+
function usePermission(type) {
|
|
101
|
+
if (typeof type !== 'function' || !(type.prototype instanceof PermissionService)) {
|
|
102
|
+
throw new Error(`usePermission accepts only PermissionService subclasses, got ${typeof type === 'function' ? type.name : typeof type}`);
|
|
103
|
+
}
|
|
104
|
+
return di_1.DI.resolve(type);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* THE guarded accesscontrol probe — single home for the AccessControlError semantics
|
|
108
|
+
* previously copy-pasted into the ORM query middleware and rbac-http's RbacPolicy.
|
|
109
|
+
* Answers the Permission object (grant attributes stay reachable), or null for a role
|
|
110
|
+
* unknown to accesscontrol — which throws AccessControlError for any role absent from
|
|
111
|
+
* the grants map and rejects the whole role array on one unknown member. A genuine
|
|
112
|
+
* programming error (bad permission name) still propagates loud.
|
|
113
|
+
*/
|
|
114
|
+
function probeGrant(roles, permission, resource) {
|
|
115
|
+
const ac = di_1.DI.get('AccessControl');
|
|
116
|
+
if (!ac) {
|
|
117
|
+
throw new Error('no AccessControl registered in DI — rbac is not bootstrapped');
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
return ac.can(roles)[permission](resource);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (accesscontrol_1.AccessControl.isAccessControlError(err)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* THE ActiveRole rule — single home for the narrowing previously inlined in the ORM
|
|
131
|
+
* query middleware and rbac-http's checkRoutePermission. The session's ActiveRole
|
|
132
|
+
* applies only to the user it was selected for — matched by id; any other user answers
|
|
133
|
+
* their full role list.
|
|
134
|
+
*
|
|
135
|
+
* The storage parameter is structural (not IRbacAsyncStorage) so both the rbac ALS
|
|
136
|
+
* store and http's request storage (whose User is `User | null`) fit.
|
|
137
|
+
*/
|
|
138
|
+
function effectiveRoles(user, storage) {
|
|
139
|
+
if (storage?.ActiveRole && storage.User?.Id === user.Id) {
|
|
140
|
+
return [storage.ActiveRole];
|
|
141
|
+
}
|
|
142
|
+
return user.Role;
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=permission-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-service.js","sourceRoot":"","sources":["../../src/permission-service.ts"],"names":[],"mappings":"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"}
|
package/lib/cjs/profile.js
CHANGED
|
@@ -15,7 +15,7 @@ const interfaces_js_1 = require("./interfaces.js");
|
|
|
15
15
|
*/
|
|
16
16
|
let BasicProfileProvider = class BasicProfileProvider {
|
|
17
17
|
async retrieve(user) {
|
|
18
|
-
const u = await (0, index_js_1.
|
|
18
|
+
const u = await (0, index_js_1.getUser)(user);
|
|
19
19
|
return new interfaces_js_1.UserProfile(u);
|
|
20
20
|
}
|
|
21
21
|
};
|
package/lib/cjs/profile.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/profile.ts"],"names":[],"mappings":";;;;;;;;;AAAA,oCAAyC;AACzC,
|
|
1
|
+
{"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/profile.ts"],"names":[],"mappings":";;;;;;;;;AAAA,oCAAyC;AACzC,yCAAqC;AACrC,mDAAmE;AAGnE;;GAEG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACtB,KAAK,CAAC,QAAQ,CAAI,IAA4B;QACjD,MAAM,CAAC,GAAG,MAAM,IAAA,kBAAO,EAAC,IAAI,CAAC,CAAC;QAC9B,OAAO,IAAI,2BAAW,CAAC,CAAC,CAAC,CAAA;IAC7B,CAAC;CAEJ,CAAA;;+BANY,oBAAoB;IADhC,IAAA,eAAU,EAAC,mCAAmB,CAAC;GACnB,oBAAoB,CAMhC"}
|