@spinajs/rbac-http-admin 2.0.485 → 2.0.487

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 (66) hide show
  1. package/README.md +104 -5
  2. package/lib/cjs/config/rbac-http-admin.d.ts +25 -0
  3. package/lib/cjs/config/rbac-http-admin.d.ts.map +1 -1
  4. package/lib/cjs/config/rbac-http-admin.js +25 -0
  5. package/lib/cjs/config/rbac-http-admin.js.map +1 -1
  6. package/lib/cjs/controllers/Users/Profile.d.ts.map +1 -1
  7. package/lib/cjs/controllers/Users/Profile.js +3 -1
  8. package/lib/cjs/controllers/Users/Profile.js.map +1 -1
  9. package/lib/cjs/controllers/Users/Roles.d.ts +15 -8
  10. package/lib/cjs/controllers/Users/Roles.d.ts.map +1 -1
  11. package/lib/cjs/controllers/Users/Roles.js +34 -16
  12. package/lib/cjs/controllers/Users/Roles.js.map +1 -1
  13. package/lib/cjs/controllers/Users/Security.d.ts +151 -11
  14. package/lib/cjs/controllers/Users/Security.d.ts.map +1 -1
  15. package/lib/cjs/controllers/Users/Security.js +303 -30
  16. package/lib/cjs/controllers/Users/Security.js.map +1 -1
  17. package/lib/cjs/controllers/Users/Users.d.ts +97 -14
  18. package/lib/cjs/controllers/Users/Users.d.ts.map +1 -1
  19. package/lib/cjs/controllers/Users/Users.js +278 -73
  20. package/lib/cjs/controllers/Users/Users.js.map +1 -1
  21. package/lib/cjs/index.d.ts +6 -1
  22. package/lib/cjs/index.d.ts.map +1 -1
  23. package/lib/cjs/index.js +5 -0
  24. package/lib/cjs/index.js.map +1 -1
  25. package/lib/cjs/interfaces.d.ts +81 -0
  26. package/lib/cjs/interfaces.d.ts.map +1 -0
  27. package/lib/cjs/interfaces.js +20 -0
  28. package/lib/cjs/interfaces.js.map +1 -0
  29. package/lib/cjs/services/RoleGuard.d.ts +96 -0
  30. package/lib/cjs/services/RoleGuard.d.ts.map +1 -0
  31. package/lib/cjs/services/RoleGuard.js +258 -0
  32. package/lib/cjs/services/RoleGuard.js.map +1 -0
  33. package/lib/mjs/config/rbac-http-admin.d.ts +25 -0
  34. package/lib/mjs/config/rbac-http-admin.d.ts.map +1 -1
  35. package/lib/mjs/config/rbac-http-admin.js +25 -0
  36. package/lib/mjs/config/rbac-http-admin.js.map +1 -1
  37. package/lib/mjs/controllers/Users/Profile.d.ts.map +1 -1
  38. package/lib/mjs/controllers/Users/Profile.js +4 -2
  39. package/lib/mjs/controllers/Users/Profile.js.map +1 -1
  40. package/lib/mjs/controllers/Users/Roles.d.ts +15 -8
  41. package/lib/mjs/controllers/Users/Roles.d.ts.map +1 -1
  42. package/lib/mjs/controllers/Users/Roles.js +35 -17
  43. package/lib/mjs/controllers/Users/Roles.js.map +1 -1
  44. package/lib/mjs/controllers/Users/Security.d.ts +151 -11
  45. package/lib/mjs/controllers/Users/Security.d.ts.map +1 -1
  46. package/lib/mjs/controllers/Users/Security.js +304 -31
  47. package/lib/mjs/controllers/Users/Security.js.map +1 -1
  48. package/lib/mjs/controllers/Users/Users.d.ts +97 -14
  49. package/lib/mjs/controllers/Users/Users.d.ts.map +1 -1
  50. package/lib/mjs/controllers/Users/Users.js +279 -74
  51. package/lib/mjs/controllers/Users/Users.js.map +1 -1
  52. package/lib/mjs/index.d.ts +6 -1
  53. package/lib/mjs/index.d.ts.map +1 -1
  54. package/lib/mjs/index.js +6 -1
  55. package/lib/mjs/index.js.map +1 -1
  56. package/lib/mjs/interfaces.d.ts +81 -0
  57. package/lib/mjs/interfaces.d.ts.map +1 -0
  58. package/lib/mjs/interfaces.js +16 -0
  59. package/lib/mjs/interfaces.js.map +1 -0
  60. package/lib/mjs/services/RoleGuard.d.ts +96 -0
  61. package/lib/mjs/services/RoleGuard.d.ts.map +1 -0
  62. package/lib/mjs/services/RoleGuard.js +252 -0
  63. package/lib/mjs/services/RoleGuard.js.map +1 -0
  64. package/lib/tsconfig.cjs.tsbuildinfo +1 -1
  65. package/lib/tsconfig.mjs.tsbuildinfo +1 -1
  66. package/package.json +14 -12
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../../src/interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAErC;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAEhB,+EAA+E;IAC/E,gBAAgB,EAAE,OAAO,CAAC;IAE1B,gFAAgF;IAChF,iBAAiB,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,iBAAiB,EAAE,OAAO,CAAC;IAE3B;;;OAGG;IACH,kBAAkB,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,4BAA4B,EAAE,OAAO,CAAC;IAEtC;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,8BAAsB,SAAS;IAC7B;;;;;;OAMG;aACa,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAEtG;;OAEG;aACa,mBAAmB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAE3F;;OAEG;aACa,uBAAuB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAE/G;;;;OAIG;aACa,eAAe,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE;CACvD;AAED,uDAAuD;AACvD,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG,QAAQ,GAAG,KAAK,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Role and account-state guard for the admin API.
3
+ *
4
+ * The checks live here rather than in the controllers because the same rule has
5
+ * to hold on every path that can change what an account may do: granting a role,
6
+ * revoking one, rewriting the whole role list through `PATCH /users/:uuid`, and
7
+ * taking an account out of service. A rule implemented per-controller is a rule
8
+ * that will be missing from the next controller.
9
+ *
10
+ * All methods THROW on refusal ( `Forbidden` / `InvalidArgument`, which the http
11
+ * error map turns into 403 / 400 ) and return normally when the operation is
12
+ * allowed.
13
+ */
14
+ export class RoleGuard {
15
+ }
16
+ //# sourceMappingURL=interfaces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../../src/interfaces.ts"],"names":[],"mappings":"AAiDA;;;;;;;;;;;;GAYG;AACH,MAAM,OAAgB,SAAS;CA0B9B"}
@@ -0,0 +1,96 @@
1
+ import { Log } from '@spinajs/log';
2
+ import { AccessControl, User } from '@spinajs/rbac';
3
+ import { AccountDisableAction, IRoleGuardConfig, RoleGuard } from '../interfaces.js';
4
+ /** A resolved grant map: resource -> action -> attribute list. */
5
+ type GrantMap = {
6
+ [resource: string]: {
7
+ [action: string]: string[];
8
+ };
9
+ };
10
+ /**
11
+ * The shipped {@link RoleGuard}.
12
+ *
13
+ * Every check reads its switch from `rbac.admin.roleGuard`, so an application
14
+ * turns one off in configuration instead of replacing the whole service — and
15
+ * an application that needs a different rule entirely registers its own class
16
+ * under {@link RoleGuard} and names it in `rbac.admin.roleGuard.service`.
17
+ */
18
+ export declare class DefaultRoleGuard extends RoleGuard {
19
+ protected Log: Log;
20
+ protected Options: IRoleGuardConfig;
21
+ protected SystemRole: string;
22
+ /**
23
+ * Declared roles, which is NOT the same set as the roles accesscontrol knows
24
+ * about: a role may be declared for assignment and carry no grants yet.
25
+ */
26
+ protected DeclaredRoles: Array<{
27
+ Name: string;
28
+ }>;
29
+ /**
30
+ * Resolved late, never cached in a field: the container entry is replaced
31
+ * whenever the rbac bootstrapper runs ( every test case does ), and a guard
32
+ * holding the stale instance would answer from grants nobody configured.
33
+ */
34
+ protected get AC(): AccessControl;
35
+ assertCanAssignRoles(actor: User, target: User | null, roles: string[]): Promise<void>;
36
+ assertCanRevokeRole(actor: User, target: User, role: string): Promise<void>;
37
+ assertCanDisableAccount(actor: User, target: User, action: AccountDisableAction): Promise<void>;
38
+ assignableRoles(actor: User): string[];
39
+ /**
40
+ * A missing actor is a programming error on a route that must be behind
41
+ * `AuthorizedPolicy` — refused rather than waved through, because every
42
+ * escalation check below is meaningless without one.
43
+ */
44
+ protected assertActor(actor: User): void;
45
+ protected assertKnownRole(role: string): void;
46
+ protected assertNotSystemRole(role: string, verb: string): void;
47
+ protected isSelf(actor: User, target: User): boolean;
48
+ /**
49
+ * Throws when `target` is the last account that can still act with `role`.
50
+ *
51
+ * Counts accounts that could actually log in — inactive and soft-deleted rows
52
+ * are not a way back into the installation.
53
+ */
54
+ protected assertNotLastHolder(role: string, action: string): Promise<void>;
55
+ /**
56
+ * Active accounts holding `role`.
57
+ *
58
+ * Counted in the database. This used to narrow with LIKE and finish the job in
59
+ * memory, because `withRole` compiled to `FIND_IN_SET` — MySQL only — and threw
60
+ * a driver error on SQLite, turning "is this the last administrator" into a 500
61
+ * on every deployment that is not MySQL. Set membership is a per-dialect
62
+ * statement now, so the scope answers everywhere and the workaround is gone.
63
+ */
64
+ protected countActiveHolders(role: string): Promise<number>;
65
+ protected isPrivileged(role: string): boolean;
66
+ protected rolesWithGrants(): string[];
67
+ protected allGrants(): GrantMap;
68
+ /** Grants of one role, with its `$extend` chain resolved. */
69
+ protected grantsOfRole(role: string): GrantMap;
70
+ /**
71
+ * The caller's ceiling: the union of every role they hold.
72
+ *
73
+ * Deliberately the union and not the session's active role — an administrator
74
+ * who can switch to a stronger role could otherwise be blocked from an
75
+ * operation they can perform by switching first, which teaches people to
76
+ * switch rather than to stay in the weaker role.
77
+ */
78
+ protected grantsOf(actor: User): GrantMap;
79
+ /**
80
+ * True when `holder` permits everything `wanted` does.
81
+ *
82
+ * Attributes are compared as sets, with `*` covering anything. Negated
83
+ * attributes ( `!Password` ) are treated as ordinary members: a holder that
84
+ * lists the same negation still covers, one that does not is reported as not
85
+ * covering. That errs towards refusing an assignment, which is the safe
86
+ * direction for a guard.
87
+ *
88
+ * `:any` covers the matching `:own`, exactly as accesscontrol resolves them at
89
+ * enforcement time. Without that an administrator holding `read:any` on users
90
+ * would be told they cannot hand out a role holding `read:own` — a role
91
+ * strictly weaker than their own.
92
+ */
93
+ protected covers(holder: GrantMap, wanted: GrantMap): boolean;
94
+ }
95
+ export {};
96
+ //# sourceMappingURL=RoleGuard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoleGuard.d.ts","sourceRoot":"","sources":["../../../src/services/RoleGuard.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAU,MAAM,cAAc,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,IAAI,EAAiC,MAAM,eAAe,CAAC;AAGnF,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAErF,kEAAkE;AAClE,KAAK,QAAQ,GAAG;IAAE,CAAC,QAAQ,EAAE,MAAM,GAAG;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;KAAE,CAAA;CAAE,CAAC;AAEvE;;;;;;;GAOG;AACH,qBACa,gBAAiB,SAAQ,SAAS;IAE7C,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC;IAGnB,SAAS,CAAC,OAAO,EAAE,gBAAgB,CAAC;IAGpC,SAAS,CAAC,UAAU,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IAEH,SAAS,CAAC,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEjD;;;;OAIG;IACH,SAAS,KAAK,EAAE,IAAI,aAAa,CAEhC;IAEY,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BtF,mBAAmB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2B3E,uBAAuB,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBrG,eAAe,CAAC,KAAK,EAAE,IAAI,GAAG,MAAM,EAAE;IAa7C;;;;OAIG;IACH,SAAS,CAAC,WAAW,CAAC,KAAK,EAAE,IAAI,GAAG,IAAI;IAMxC,SAAS,CAAC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAY7C,SAAS,CAAC,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAM/D,SAAS,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAG,OAAO;IAUpD;;;;;OAKG;cACa,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhF;;;;;;;;OAQG;cACa,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIjE,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAO7C,SAAS,CAAC,eAAe,IAAI,MAAM,EAAE;IASrC,SAAS,CAAC,SAAS,IAAI,QAAQ;IAQ/B,6DAA6D;IAC7D,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ;IAI9C;;;;;;;OAOG;IACH,SAAS,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,GAAG,QAAQ;IAKzC;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO;CA2B9D"}
@@ -0,0 +1,252 @@
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
+ import { Injectable, DI } from '@spinajs/di';
11
+ import { Config } from '@spinajs/configuration';
12
+ import { Log, Logger } from '@spinajs/log';
13
+ import { Forbidden, InvalidArgument } from '@spinajs/exceptions';
14
+ import { User, _combineGrants, _unwindGrants } from '@spinajs/rbac';
15
+ import _ from 'lodash';
16
+ import { RoleGuard } from '../interfaces.js';
17
+ /**
18
+ * The shipped {@link RoleGuard}.
19
+ *
20
+ * Every check reads its switch from `rbac.admin.roleGuard`, so an application
21
+ * turns one off in configuration instead of replacing the whole service — and
22
+ * an application that needs a different rule entirely registers its own class
23
+ * under {@link RoleGuard} and names it in `rbac.admin.roleGuard.service`.
24
+ */
25
+ let DefaultRoleGuard = class DefaultRoleGuard extends RoleGuard {
26
+ /**
27
+ * Resolved late, never cached in a field: the container entry is replaced
28
+ * whenever the rbac bootstrapper runs ( every test case does ), and a guard
29
+ * holding the stale instance would answer from grants nobody configured.
30
+ */
31
+ get AC() {
32
+ return DI.get('AccessControl');
33
+ }
34
+ async assertCanAssignRoles(actor, target, roles) {
35
+ const wanted = _.uniq((roles ?? []).map((r) => String(r ?? '').trim()).filter((r) => r.length > 0));
36
+ if (wanted.length === 0) {
37
+ return;
38
+ }
39
+ this.assertActor(actor);
40
+ for (const role of wanted) {
41
+ this.assertKnownRole(role);
42
+ this.assertNotSystemRole(role, 'assigned');
43
+ if (this.Options?.preventEscalation !== false && !this.covers(this.grantsOf(actor), this.grantsOfRole(role))) {
44
+ throw new Forbidden(`Role '${role}' grants more than the caller holds and cannot be assigned`);
45
+ }
46
+ }
47
+ // The guard passed — the assignment itself is the caller's next step. Logged
48
+ // with the actor because the rbac events carry the TARGET only, so this line
49
+ // is the one record of who handed out which role.
50
+ this.Log.info(`Role assignment allowed`, {
51
+ Actor: actor?.Uuid,
52
+ Target: target?.Uuid ?? null,
53
+ Roles: wanted,
54
+ });
55
+ }
56
+ async assertCanRevokeRole(actor, target, role) {
57
+ const wanted = String(role ?? '').trim();
58
+ if (wanted.length === 0) {
59
+ throw new InvalidArgument('role cannot be empty');
60
+ }
61
+ this.assertActor(actor);
62
+ this.assertKnownRole(wanted);
63
+ this.assertNotSystemRole(wanted, 'revoked');
64
+ if (!this.isPrivileged(wanted)) {
65
+ return;
66
+ }
67
+ // Taking a privileged role off YOUR OWN account through the admin API is
68
+ // almost always a mistake and is never recoverable through the same API —
69
+ // the request that would put the role back needs the role.
70
+ if (this.Options?.preventSelfLockout !== false && this.isSelf(actor, target)) {
71
+ throw new Forbidden(`Cannot revoke your own '${wanted}' role`);
72
+ }
73
+ if (this.Options?.preventLastPrivilegedRemoval !== false && target?.Role?.includes(wanted)) {
74
+ await this.assertNotLastHolder(wanted, `revoke '${wanted}'`);
75
+ }
76
+ }
77
+ async assertCanDisableAccount(actor, target, action) {
78
+ this.assertActor(actor);
79
+ if (this.Options?.preventSelfLockout !== false && this.isSelf(actor, target)) {
80
+ throw new Forbidden(`Cannot ${action} your own account`);
81
+ }
82
+ if (this.Options?.preventLastPrivilegedRemoval === false) {
83
+ return;
84
+ }
85
+ for (const role of target?.Role ?? []) {
86
+ if (this.isPrivileged(role)) {
87
+ await this.assertNotLastHolder(role, action);
88
+ }
89
+ }
90
+ }
91
+ assignableRoles(actor) {
92
+ const all = _.uniq([...(this.DeclaredRoles ?? []).map((r) => r?.Name).filter(Boolean), ...this.rolesWithGrants()]);
93
+ const withoutSystem = this.Options?.protectSystemRole === false ? all : all.filter((r) => r !== this.SystemRole);
94
+ if (this.Options?.preventEscalation === false) {
95
+ return withoutSystem;
96
+ }
97
+ const actorGrants = this.grantsOf(actor);
98
+ return withoutSystem.filter((r) => this.covers(actorGrants, this.grantsOfRole(r)));
99
+ }
100
+ /**
101
+ * A missing actor is a programming error on a route that must be behind
102
+ * `AuthorizedPolicy` — refused rather than waved through, because every
103
+ * escalation check below is meaningless without one.
104
+ */
105
+ assertActor(actor) {
106
+ if (!actor) {
107
+ throw new Forbidden('Role changes require an authenticated caller');
108
+ }
109
+ }
110
+ assertKnownRole(role) {
111
+ if (this.Options?.requireKnownRole === false) {
112
+ return;
113
+ }
114
+ const known = _.uniq([...(this.DeclaredRoles ?? []).map((r) => r?.Name).filter(Boolean), ...this.rolesWithGrants(), this.SystemRole]);
115
+ if (!known.includes(role)) {
116
+ throw new InvalidArgument(`Role '${role}' is not declared in rbac configuration`);
117
+ }
118
+ }
119
+ assertNotSystemRole(role, verb) {
120
+ if (this.Options?.protectSystemRole !== false && role === this.SystemRole) {
121
+ throw new Forbidden(`The system role cannot be ${verb} through the API`);
122
+ }
123
+ }
124
+ isSelf(actor, target) {
125
+ if (!actor || !target) {
126
+ return false;
127
+ }
128
+ // Uuid first: an actor resolved from a session and a target loaded by route
129
+ // parameter are two different model instances of the same row.
130
+ return (actor.Uuid && actor.Uuid === target.Uuid) || (!!actor.Id && actor.Id === target.Id);
131
+ }
132
+ /**
133
+ * Throws when `target` is the last account that can still act with `role`.
134
+ *
135
+ * Counts accounts that could actually log in — inactive and soft-deleted rows
136
+ * are not a way back into the installation.
137
+ */
138
+ async assertNotLastHolder(role, action) {
139
+ const holders = await this.countActiveHolders(role);
140
+ // The target itself is one of them ( it is active and holds the role ), so
141
+ // one holder means the operation empties the role.
142
+ if (holders <= 1) {
143
+ throw new Forbidden(`Cannot ${action}: it would leave no active account holding '${role}'`);
144
+ }
145
+ }
146
+ /**
147
+ * Active accounts holding `role`.
148
+ *
149
+ * Counted in the database. This used to narrow with LIKE and finish the job in
150
+ * memory, because `withRole` compiled to `FIND_IN_SET` — MySQL only — and threw
151
+ * a driver error on SQLite, turning "is this the last administrator" into a 500
152
+ * on every deployment that is not MySQL. Set membership is a per-dialect
153
+ * statement now, so the scope answers everywhere and the workaround is gone.
154
+ */
155
+ async countActiveHolders(role) {
156
+ return User.query().isActiveUser().withRole([role]).selectCount();
157
+ }
158
+ isPrivileged(role) {
159
+ const resource = this.Options?.privilegedResource ?? 'users';
160
+ const action = this.Options?.privilegedAction ?? 'update:any';
161
+ return Boolean(this.grantsOfRole(role)?.[resource]?.[action]);
162
+ }
163
+ rolesWithGrants() {
164
+ try {
165
+ return this.AC?.getRoles() ?? [];
166
+ }
167
+ catch {
168
+ // accesscontrol throws when no grants were ever set
169
+ return [];
170
+ }
171
+ }
172
+ allGrants() {
173
+ try {
174
+ return (this.AC?.getGrants() ?? {});
175
+ }
176
+ catch {
177
+ return {};
178
+ }
179
+ }
180
+ /** Grants of one role, with its `$extend` chain resolved. */
181
+ grantsOfRole(role) {
182
+ return _unwindGrants(role, this.allGrants());
183
+ }
184
+ /**
185
+ * The caller's ceiling: the union of every role they hold.
186
+ *
187
+ * Deliberately the union and not the session's active role — an administrator
188
+ * who can switch to a stronger role could otherwise be blocked from an
189
+ * operation they can perform by switching first, which teaches people to
190
+ * switch rather than to stay in the weaker role.
191
+ */
192
+ grantsOf(actor) {
193
+ const grants = this.allGrants();
194
+ return _combineGrants(...(actor?.Role ?? []).map((r) => _unwindGrants(r, grants)));
195
+ }
196
+ /**
197
+ * True when `holder` permits everything `wanted` does.
198
+ *
199
+ * Attributes are compared as sets, with `*` covering anything. Negated
200
+ * attributes ( `!Password` ) are treated as ordinary members: a holder that
201
+ * lists the same negation still covers, one that does not is reported as not
202
+ * covering. That errs towards refusing an assignment, which is the safe
203
+ * direction for a guard.
204
+ *
205
+ * `:any` covers the matching `:own`, exactly as accesscontrol resolves them at
206
+ * enforcement time. Without that an administrator holding `read:any` on users
207
+ * would be told they cannot hand out a role holding `read:own` — a role
208
+ * strictly weaker than their own.
209
+ */
210
+ covers(holder, wanted) {
211
+ for (const [resource, actions] of Object.entries(wanted ?? {})) {
212
+ const held = holder?.[resource];
213
+ if (!held) {
214
+ return false;
215
+ }
216
+ for (const [action, attributes] of Object.entries(actions ?? {})) {
217
+ const heldAttributes = held[action] ?? (action.endsWith(':own') ? held[action.replace(/:own$/, ':any')] : undefined);
218
+ if (!heldAttributes) {
219
+ return false;
220
+ }
221
+ if (heldAttributes.includes('*')) {
222
+ continue;
223
+ }
224
+ if ((attributes ?? []).some((a) => !heldAttributes.includes(a))) {
225
+ return false;
226
+ }
227
+ }
228
+ }
229
+ return true;
230
+ }
231
+ };
232
+ __decorate([
233
+ Logger('rbac-admin'),
234
+ __metadata("design:type", Log)
235
+ ], DefaultRoleGuard.prototype, "Log", void 0);
236
+ __decorate([
237
+ Config('rbac.admin.roleGuard'),
238
+ __metadata("design:type", Object)
239
+ ], DefaultRoleGuard.prototype, "Options", void 0);
240
+ __decorate([
241
+ Config('rbac.systemRole', { defaultValue: 'system' }),
242
+ __metadata("design:type", String)
243
+ ], DefaultRoleGuard.prototype, "SystemRole", void 0);
244
+ __decorate([
245
+ Config('rbac.roles', { defaultValue: [] }),
246
+ __metadata("design:type", Array)
247
+ ], DefaultRoleGuard.prototype, "DeclaredRoles", void 0);
248
+ DefaultRoleGuard = __decorate([
249
+ Injectable(RoleGuard)
250
+ ], DefaultRoleGuard);
251
+ export { DefaultRoleGuard };
252
+ //# sourceMappingURL=RoleGuard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoleGuard.js","sourceRoot":"","sources":["../../../src/services/RoleGuard.ts"],"names":[],"mappings":"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"}