@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.
Files changed (105) hide show
  1. package/lib/cjs/actions.d.ts +269 -93
  2. package/lib/cjs/actions.d.ts.map +1 -1
  3. package/lib/cjs/actions.js +695 -354
  4. package/lib/cjs/actions.js.map +1 -1
  5. package/lib/cjs/auth.d.ts.map +1 -1
  6. package/lib/cjs/auth.js +6 -6
  7. package/lib/cjs/auth.js.map +1 -1
  8. package/lib/cjs/cli/ChangeUserPassword.d.ts.map +1 -1
  9. package/lib/cjs/cli/ChangeUserPassword.js +2 -2
  10. package/lib/cjs/cli/ChangeUserPassword.js.map +1 -1
  11. package/lib/cjs/cli/CreateUser.d.ts.map +1 -1
  12. package/lib/cjs/cli/CreateUser.js +8 -1
  13. package/lib/cjs/cli/CreateUser.js.map +1 -1
  14. package/lib/cjs/config/rbac.d.ts +40 -0
  15. package/lib/cjs/config/rbac.d.ts.map +1 -1
  16. package/lib/cjs/config/rbac.js +40 -0
  17. package/lib/cjs/config/rbac.js.map +1 -1
  18. package/lib/cjs/events/UserCreated.d.ts +2 -1
  19. package/lib/cjs/events/UserCreated.d.ts.map +1 -1
  20. package/lib/cjs/events/UserCreated.js +13 -3
  21. package/lib/cjs/events/UserCreated.js.map +1 -1
  22. package/lib/cjs/exceptions.d.ts +69 -0
  23. package/lib/cjs/exceptions.d.ts.map +1 -0
  24. package/lib/cjs/exceptions.js +86 -0
  25. package/lib/cjs/exceptions.js.map +1 -0
  26. package/lib/cjs/index.d.ts +3 -0
  27. package/lib/cjs/index.d.ts.map +1 -1
  28. package/lib/cjs/index.js +3 -0
  29. package/lib/cjs/index.js.map +1 -1
  30. package/lib/cjs/interfaces.d.ts +2 -31
  31. package/lib/cjs/interfaces.d.ts.map +1 -1
  32. package/lib/cjs/interfaces.js +1 -12
  33. package/lib/cjs/interfaces.js.map +1 -1
  34. package/lib/cjs/middleware.d.ts +16 -6
  35. package/lib/cjs/middleware.d.ts.map +1 -1
  36. package/lib/cjs/middleware.js +151 -90
  37. package/lib/cjs/middleware.js.map +1 -1
  38. package/lib/cjs/orm-permission.d.ts +77 -0
  39. package/lib/cjs/orm-permission.d.ts.map +1 -0
  40. package/lib/cjs/orm-permission.js +128 -0
  41. package/lib/cjs/orm-permission.js.map +1 -0
  42. package/lib/cjs/password.d.ts +18 -1
  43. package/lib/cjs/password.d.ts.map +1 -1
  44. package/lib/cjs/password.js +61 -5
  45. package/lib/cjs/password.js.map +1 -1
  46. package/lib/cjs/permission-service.d.ts +80 -0
  47. package/lib/cjs/permission-service.d.ts.map +1 -0
  48. package/lib/cjs/permission-service.js +144 -0
  49. package/lib/cjs/permission-service.js.map +1 -0
  50. package/lib/cjs/profile.js +1 -1
  51. package/lib/cjs/profile.js.map +1 -1
  52. package/lib/mjs/actions.d.ts +269 -93
  53. package/lib/mjs/actions.d.ts.map +1 -1
  54. package/lib/mjs/actions.js +682 -358
  55. package/lib/mjs/actions.js.map +1 -1
  56. package/lib/mjs/auth.d.ts.map +1 -1
  57. package/lib/mjs/auth.js +7 -7
  58. package/lib/mjs/auth.js.map +1 -1
  59. package/lib/mjs/cli/ChangeUserPassword.d.ts.map +1 -1
  60. package/lib/mjs/cli/ChangeUserPassword.js +3 -3
  61. package/lib/mjs/cli/ChangeUserPassword.js.map +1 -1
  62. package/lib/mjs/cli/CreateUser.d.ts.map +1 -1
  63. package/lib/mjs/cli/CreateUser.js +8 -1
  64. package/lib/mjs/cli/CreateUser.js.map +1 -1
  65. package/lib/mjs/config/rbac.d.ts +40 -0
  66. package/lib/mjs/config/rbac.d.ts.map +1 -1
  67. package/lib/mjs/config/rbac.js +40 -0
  68. package/lib/mjs/config/rbac.js.map +1 -1
  69. package/lib/mjs/events/UserCreated.d.ts +2 -1
  70. package/lib/mjs/events/UserCreated.d.ts.map +1 -1
  71. package/lib/mjs/events/UserCreated.js +13 -3
  72. package/lib/mjs/events/UserCreated.js.map +1 -1
  73. package/lib/mjs/exceptions.d.ts +69 -0
  74. package/lib/mjs/exceptions.d.ts.map +1 -0
  75. package/lib/mjs/exceptions.js +71 -0
  76. package/lib/mjs/exceptions.js.map +1 -0
  77. package/lib/mjs/index.d.ts +3 -0
  78. package/lib/mjs/index.d.ts.map +1 -1
  79. package/lib/mjs/index.js +3 -0
  80. package/lib/mjs/index.js.map +1 -1
  81. package/lib/mjs/interfaces.d.ts +2 -31
  82. package/lib/mjs/interfaces.d.ts.map +1 -1
  83. package/lib/mjs/interfaces.js +0 -11
  84. package/lib/mjs/interfaces.js.map +1 -1
  85. package/lib/mjs/middleware.d.ts +16 -6
  86. package/lib/mjs/middleware.d.ts.map +1 -1
  87. package/lib/mjs/middleware.js +152 -91
  88. package/lib/mjs/middleware.js.map +1 -1
  89. package/lib/mjs/orm-permission.d.ts +77 -0
  90. package/lib/mjs/orm-permission.d.ts.map +1 -0
  91. package/lib/mjs/orm-permission.js +119 -0
  92. package/lib/mjs/orm-permission.js.map +1 -0
  93. package/lib/mjs/password.d.ts +18 -1
  94. package/lib/mjs/password.d.ts.map +1 -1
  95. package/lib/mjs/password.js +62 -6
  96. package/lib/mjs/password.js.map +1 -1
  97. package/lib/mjs/permission-service.d.ts +80 -0
  98. package/lib/mjs/permission-service.d.ts.map +1 -0
  99. package/lib/mjs/permission-service.js +137 -0
  100. package/lib/mjs/permission-service.js.map +1 -0
  101. package/lib/mjs/profile.js +2 -2
  102. package/lib/mjs/profile.js.map +1 -1
  103. package/lib/tsconfig.cjs.tsbuildinfo +1 -1
  104. package/lib/tsconfig.mjs.tsbuildinfo +1 -1
  105. 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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"}
@@ -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 and entropy-string to generate password
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;AAGpD;;GAEG;AACH,qBACa,qBAAsB,YAAW,gBAAgB;IAC/C,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;IAEM,QAAQ,IAAI,MAAM,CAIxB;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"}
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"}
@@ -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 entropy_string_1 = require("entropy-string");
52
+ const exceptions_1 = require("@spinajs/exceptions");
53
+ const crypto_1 = require("crypto");
53
54
  /**
54
- * Simple password service that use argon2 hash alghoritm and entropy-string to generate password
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
- // generates password with entropy of 60 bits ( balance of ease vs value )
73
- const random = new entropy_string_1.Entropy({ charset: entropy_string_1.charset32 });
74
- return random.string(60);
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);
@@ -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,0DAAgD;AAChD,oDAAoD;AACpD,mDAAoD;AAEpD;;GAEG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IACzB,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;IAEM,QAAQ;QACb,0EAA0E;QAC1E,MAAM,MAAM,GAAG,IAAI,wBAAO,CAAC,EAAE,OAAO,EAAE,0BAAS,EAAE,CAAC,CAAC;QACnD,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC3B,CAAC;CACF,CAAA;;gCAtBY,qBAAqB;IADjC,IAAA,eAAU,EAAC,gCAAgB,CAAC;GAChB,qBAAqB,CAsBjC;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"}
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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"}
@@ -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._user)(user)();
18
+ const u = await (0, index_js_1.getUser)(user);
19
19
  return new interfaces_js_1.UserProfile(u);
20
20
  }
21
21
  };
@@ -1 +1 @@
1
- {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/profile.ts"],"names":[],"mappings":";;;;;;;;;AAAA,oCAAyC;AACzC,yCAAmC;AACnC,mDAAmE;AAGnE;;GAEG;AAEI,IAAM,oBAAoB,GAA1B,MAAM,oBAAoB;IACtB,KAAK,CAAC,QAAQ,CAAI,IAA4B;QACjD,MAAM,CAAC,GAAG,MAAM,IAAA,gBAAK,EAAC,IAAI,CAAC,EAAE,CAAC;QAC9B,OAAO,IAAI,2BAAW,CAAC,CAAC,CAAC,CAAA;IAC7B,CAAC;CAEJ,CAAA;;+BANY,oBAAoB;IADhC,IAAA,eAAU,EAAC,mCAAmB,CAAC;GACnB,oBAAoB,CAMhC"}
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"}