@memberjunction/core-entities-server 6.1.0-edge.5 → 6.1.0-edge.7

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 (57) hide show
  1. package/dist/custom/MJAIBridgeProviderEntityServer.server.d.ts +1 -5
  2. package/dist/custom/MJAIBridgeProviderEntityServer.server.d.ts.map +1 -1
  3. package/dist/custom/MJAIBridgeProviderEntityServer.server.js +13 -12
  4. package/dist/custom/MJAIBridgeProviderEntityServer.server.js.map +1 -1
  5. package/dist/custom/MJAIRemoteBrowserProviderEntityServer.server.d.ts +2 -6
  6. package/dist/custom/MJAIRemoteBrowserProviderEntityServer.server.d.ts.map +1 -1
  7. package/dist/custom/MJAIRemoteBrowserProviderEntityServer.server.js +16 -14
  8. package/dist/custom/MJAIRemoteBrowserProviderEntityServer.server.js.map +1 -1
  9. package/dist/custom/MJActionEntityServer.server.d.ts +2 -2
  10. package/dist/custom/MJActionEntityServer.server.d.ts.map +1 -1
  11. package/dist/custom/MJActionEntityServer.server.js +8 -6
  12. package/dist/custom/MJActionEntityServer.server.js.map +1 -1
  13. package/dist/custom/MJApplicationEntityServer.server.d.ts +2 -2
  14. package/dist/custom/MJApplicationEntityServer.server.d.ts.map +1 -1
  15. package/dist/custom/MJApplicationEntityServer.server.js +4 -4
  16. package/dist/custom/MJApplicationEntityServer.server.js.map +1 -1
  17. package/dist/custom/MJEntityEntityServer.server.d.ts +36 -0
  18. package/dist/custom/MJEntityEntityServer.server.d.ts.map +1 -0
  19. package/dist/custom/MJEntityEntityServer.server.js +77 -0
  20. package/dist/custom/MJEntityEntityServer.server.js.map +1 -0
  21. package/dist/custom/MJEntityFieldPermissionEntityServer.server.d.ts +167 -0
  22. package/dist/custom/MJEntityFieldPermissionEntityServer.server.d.ts.map +1 -0
  23. package/dist/custom/MJEntityFieldPermissionEntityServer.server.js +347 -0
  24. package/dist/custom/MJEntityFieldPermissionEntityServer.server.js.map +1 -0
  25. package/dist/custom/MJRemoteOperationEntityServer.server.d.ts +2 -2
  26. package/dist/custom/MJRemoteOperationEntityServer.server.d.ts.map +1 -1
  27. package/dist/custom/MJRemoteOperationEntityServer.server.js +6 -4
  28. package/dist/custom/MJRemoteOperationEntityServer.server.js.map +1 -1
  29. package/dist/custom/MJRoleEntityServer.server.d.ts +78 -0
  30. package/dist/custom/MJRoleEntityServer.server.d.ts.map +1 -0
  31. package/dist/custom/MJRoleEntityServer.server.js +131 -0
  32. package/dist/custom/MJRoleEntityServer.server.js.map +1 -0
  33. package/dist/custom/MJUserEntityServer.server.d.ts +243 -0
  34. package/dist/custom/MJUserEntityServer.server.d.ts.map +1 -0
  35. package/dist/custom/MJUserEntityServer.server.js +335 -0
  36. package/dist/custom/MJUserEntityServer.server.js.map +1 -0
  37. package/dist/custom/MJUserRoleEntityServer.server.d.ts +243 -0
  38. package/dist/custom/MJUserRoleEntityServer.server.d.ts.map +1 -0
  39. package/dist/custom/MJUserRoleEntityServer.server.js +376 -0
  40. package/dist/custom/MJUserRoleEntityServer.server.js.map +1 -0
  41. package/dist/custom/MJUserViewEntityServer.server.d.ts +12 -1
  42. package/dist/custom/MJUserViewEntityServer.server.d.ts.map +1 -1
  43. package/dist/custom/MJUserViewEntityServer.server.js +33 -1
  44. package/dist/custom/MJUserViewEntityServer.server.js.map +1 -1
  45. package/dist/custom/fieldPermissionDelta.d.ts +57 -0
  46. package/dist/custom/fieldPermissionDelta.d.ts.map +1 -0
  47. package/dist/custom/fieldPermissionDelta.js +196 -0
  48. package/dist/custom/fieldPermissionDelta.js.map +1 -0
  49. package/dist/custom/fieldPermissionReconciler.d.ts +34 -0
  50. package/dist/custom/fieldPermissionReconciler.d.ts.map +1 -0
  51. package/dist/custom/fieldPermissionReconciler.js +96 -0
  52. package/dist/custom/fieldPermissionReconciler.js.map +1 -0
  53. package/dist/index.d.ts +7 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +7 -0
  56. package/dist/index.js.map +1 -1
  57. package/package.json +28 -28
@@ -0,0 +1,335 @@
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
+ import { BaseEntity, BaseEntityResult, ValidationErrorInfo, ValidationErrorType } from '@memberjunction/core';
8
+ import { RegisterClass, UUIDsEqual } from '@memberjunction/global';
9
+ import { MJUserEntity } from '@memberjunction/core-entities';
10
+ /**
11
+ * Server-side `MJ: Users` entity enforcing MJ's privilege-elevation invariant (issue #4260).
12
+ *
13
+ * WHY THIS EXISTS AT ALL. `User.Type` is the column every Owner check in the platform reads —
14
+ * `SqlLoggingConfigResolver`, magic-link `canIssueInvites`, the backup-system-user fallback in
15
+ * MJServer's bootstrap. Writing it is therefore equivalent to granting yourself the platform's
16
+ * superuser level. Nothing below this class stops that: on the baseline seed the `Developer` and
17
+ * `Integration` roles hold unfiltered `CanCreate`/`CanUpdate`/`CanDelete` on `MJ: Users` (verified
18
+ * against a live database, not just the seed), `AllowUpdateAPI`/`AllowDeleteAPI` are true on the
19
+ * entity, `AllowUpdateAPI` is also true on both the `Type` and `Name` fields, `Name` has no unique
20
+ * index, and CodeGen has already issued `GRANT EXECUTE` on the relevant stored procedures to those
21
+ * roles' DB roles.
22
+ *
23
+ * WHY NOT ROW-LEVEL SECURITY. MJ's only field-adjacent access control is `RowLevelSecurityFilter`,
24
+ * and an "own row only" update filter does NOT close this: scoping the update to the caller's own
25
+ * row still permits setting one's OWN `Type` to `'Owner'`. There is no per-role FIELD permission in
26
+ * MJ, and setting `EntityField.AllowUpdateAPI = 0` on `Type` would block Owners too, breaking every
27
+ * legitimate admin path. An invariant in the save/delete path is the only mechanism that expresses
28
+ * the actual rule.
29
+ *
30
+ * WHY HERE RATHER THAN IN A RESOLVER. `Validate()` runs inside `BaseEntity.Save()` and this class
31
+ * also overrides `Save()` and `Delete()`, so all five hold on every write path — GraphQL resolvers,
32
+ * Remote Operations, the Create/Update/Delete Record actions, metadata sync, one-off scripts — and
33
+ * for any role a deployment invents, not just the two seeded ones. A resolver-level check would
34
+ * cover one door in a building with several.
35
+ *
36
+ * The `Save()` override is what makes that sentence literally true rather than nearly true.
37
+ * `Validate()` alone does NOT cover every write path: `BaseEntity.Save()` force-passes validation
38
+ * without calling `Validate()` when `EntitySaveOptions.ReplayOnly` is set (`baseEntity.ts:3725`),
39
+ * and `ReplayOnly` does not suppress the write. Invariants 1-4 were therefore skippable by an
40
+ * option, while invariant 5 was not — `Delete()` being an override. See `Save()` below for the
41
+ * reachability analysis and why the fix refuses rather than re-validates.
42
+ *
43
+ * THE INVARIANTS, for a caller whose `Type` is not `'Owner'`:
44
+ *
45
+ * 1. **Creating a `MJ: Users` row at all is refused.** This is stricter than "may not create an
46
+ * Owner": there are FOUR automated creators of this entity — `NewUserBase.createNewUser`
47
+ * (`MJServer/src/auth/newUsers.ts`), `MagicLinkService`'s provisioning path
48
+ * (`MJServer/src/auth/magicLink/MagicLinkService.ts`), and `CreateNewUserBase.createNewUser`
49
+ * (`CodeGenLib/src/Misc/createNewUser.ts:33`, a CLI provisioning tool that sets `Type='Owner'`
50
+ * unconditionally at `:39` — already refused for a non-Owner caller before this round,
51
+ * regardless of the analysis below). The first two run as `ResolveConfiguredPrincipal(...)`,
52
+ * whose ladder is: rung 1 matches the configured string against `User.Name`, rung 2 against
53
+ * `User.Email` — NEITHER rung filters by `Type` (`MJServer/src/auth/principals.ts:134,141`) —
54
+ * and only rungs 3-4 (System-by-ID, then lowest-ID-among-ACTIVE-Owners) guarantee an Owner.
55
+ * So "no legitimate path creates a user row as a non-Owner" is NOT unconditional: it holds
56
+ * only while a deployment's `contextUserForNewUserCreation` / `contextUserForProvisioning`
57
+ * names an Owner-type user's `Name` or `Email` (the shipped default, `not.set@nowhere.com`,
58
+ * resolves by Email to the seeded Owner, so default installs are unaffected). A deployment
59
+ * that instead points either setting at a non-Owner user will have JWT auto-provisioning and
60
+ * magic-link provisioning fail CLOSED at `Save()` after this change — loudly, not silently —
61
+ * rather than continuing to create rows as that non-Owner; see the changeset for the upgrade
62
+ * note. Separately, Explorer's user-management UI has NO Owner gate today (no such guard
63
+ * exists in `user-management.component.ts` or its module), so a Developer-role non-Owner
64
+ * reaches it in practice and will now receive this same create/delete refusal there — a real
65
+ * consequence for such deployments, not a hypothetical one.
66
+ * A FOURTH creator exists and is not config-driven: `SyncRolesUsersResolver.AddNewUsers`
67
+ * (`MJServer/src/resolvers/SyncRolesUsersResolver.ts:343`), whose sibling `UpdateExistingUsers`
68
+ * also sets `Name` AND `Type` unconditionally on every synced row and whose `DeleteSingleUser`
69
+ * deletes. All three carry `@RequireSystemUser()`, and `getSystemUser()` resolves the seeded
70
+ * `Type='Owner'` system user, so the guard exempts them on a default install — but a deployment
71
+ * whose system user is NOT an Owner will see that sync path fail closed too, for the same
72
+ * reason as the config-driven pair above. Note also that `DeleteSingleUser` reads a `false`
73
+ * from `Delete()` as an FK-constraint condition and downgrades to a soft delete; invariant 5
74
+ * gives that same `false` a second meaning (non-Owner caller), which that call site does not
75
+ * distinguish.
76
+ * Refusing only `Type='Owner'` on create is NOT enough on its own: `Name` has no unique index
77
+ * and both seeded non-Owner roles hold `CanCreate`, so a non-Owner could otherwise repeatedly
78
+ * `Create` rows named to match the configured principal string until one sorts below the real
79
+ * system user by ID — the exact principal-redirection invariant 4 (below) exists to prevent,
80
+ * just reached through INSERT instead of UPDATE.
81
+ * 2. `Type` may not be changed on an EXISTING row. It is a two-value CHECK column
82
+ * (`'User' | 'Owner'`), so any change by a non-Owner is either self-promotion or demoting
83
+ * somebody else. (Creation is already covered by invariant 1, so this only needs to consider
84
+ * updates.)
85
+ * 3. The row must be the caller's own, compared against the PRE-SAVE `ID`. `ID` is a primary-key
86
+ * field — `EntityFieldInfo.ReadOnly` is `true` for `IsPrimaryKey`, and `EntityField.Value`'s
87
+ * setter silently ignores writes to a `ReadOnly` field after the record's initial hydration —
88
+ * so on a genuinely LOADED row `this.ID` is not reassignable through ordinary means. The
89
+ * pre-save value still matters for a narrower reason: it ties this guard to the row identity
90
+ * established the last time the object was actually loaded/hydrated from the database, which
91
+ * is what `ResolverBase.UpdateRecord` does first for any entity with `TrackRecordChanges=1`
92
+ * (as `MJ: Users` has) — rather than trusting whatever the in-memory object merely arrived
93
+ * holding. If that pre-save identity cannot be established at all, the guard fails CLOSED
94
+ * (refuses the save) instead of silently permitting it — see `validateOwnRowOnly`.
95
+ * 4. `Name` may not be changed on an existing row. Invariants 2-3 do NOT cover this — renaming
96
+ * yourself is editing your own row. `resolvePrincipalFrom` (MJServer `auth/principals.ts`)
97
+ * resolves `contextUserForNewUserCreation` / `contextUserForProvisioning` /
98
+ * `contextUserForLookup` against `User.Name` FIRST, breaking ties by lowest ID, so a user
99
+ * who can rename themselves to the configured string and who sorts below the real system
100
+ * user becomes the principal the server acts as. That ordering is deliberate (backward
101
+ * compatibility) and is only sound while `Name` is not writable by untrusted parties; this
102
+ * invariant is what makes that true. `Name` is the login identifier — auto-provisioning sets
103
+ * `Name = email` — while `FirstName` / `LastName` / `Title` are the display fields and stay
104
+ * freely editable.
105
+ * `Email` is the ladder's OTHER rung (`principals.ts:141`) with the IDENTICAL lack of a `Type`
106
+ * filter, and is deliberately left mutable here — not overlooked. Two things narrow it: (a)
107
+ * `MJ: Users.Email` carries the database's `UQ_User_Email` unique constraint, so redirecting
108
+ * the Email rung to your own row requires the configured candidate to match NO active user at
109
+ * all — already the misconfiguration case this file's docs (and `resolvePrincipalFrom`'s own
110
+ * per-redeem logging) already surface loudly, not a quiet success path; and (b) even granting
111
+ * that misconfiguration, invariants 1 and 3 mean the payoff is no longer elevation: whatever
112
+ * row a provisioning path resolves its principal to, THIS guard still checks that resolved
113
+ * principal's actual `Type` before permitting the write it is attempting, so a non-Owner who
114
+ * gets themselves matched by the Email rung still cannot create or promote anything through
115
+ * it — the provisioning operation that would have run as them instead fails CLOSED. Freezing
116
+ * `Email` too would add friction to an already-narrow, already-loud misconfiguration path
117
+ * without closing any route that is still open.
118
+ * 5. **Deleting a `MJ: Users` row at all is refused** (see the `Delete()` override below). MJ
119
+ * deactivates users via `IsActive`; it does not delete them. An unguarded delete would let a
120
+ * non-Owner remove ANY account — Owners included, which destroys the very accounts every
121
+ * exemption above depends on.
122
+ *
123
+ * Owner-type callers are exempt from all five — CONDITIONALLY on the deployment's own configuration
124
+ * keeping `contextUserForNewUserCreation` / `contextUserForProvisioning` pointed at an Owner (see
125
+ * invariant 1 above for why that is not automatic). Provided it is, this keeps admin user management
126
+ * working, and it keeps auto-provisioning working — `NewUserBase.createNewUser` runs as
127
+ * `contextUserForNewUserCreation`, which resolves to the seeded system user (`Type='Owner'`) under
128
+ * the shipped default.
129
+ * A caller-less save (no `ActiveUser` at all — e.g. a system/CLI path running under a bound provider
130
+ * default) is likewise treated as exempt, though for `Save()` this is effectively decorative:
131
+ * `BaseEntity.CheckPermissions` already throws on a falsy `ActiveUser` and runs BEFORE `Validate()`
132
+ * inside `Save()`, so in production a caller-less `Save()` never reaches this guard's `Validate()`
133
+ * body at all — see the "no caller" test for the exact call ordering. `Delete()`'s sequencing is the
134
+ * OPPOSITE and the "no caller ⇒ exempt" default IS load-bearing there — see `callerIsOwner()`'s
135
+ * docstring.
136
+ *
137
+ * Pure: reads only this record's own field state and the caller. No `RunView`, no provider, no
138
+ * engine, no I/O — so it costs nothing per save/delete and is unit-testable without a database.
139
+ */
140
+ let MJUserEntityServer = class MJUserEntityServer extends MJUserEntity {
141
+ Validate() {
142
+ const result = super.Validate();
143
+ if (!this.callerIsOwner()) {
144
+ if (!this.IsSaved) {
145
+ this.validateCreateRefused(result);
146
+ }
147
+ else {
148
+ this.validateNoTypeChange(result);
149
+ this.validateOwnRowOnly(result);
150
+ this.validateNameImmutable(result);
151
+ }
152
+ }
153
+ result.Success = result.Success && result.Errors.length === 0;
154
+ return result;
155
+ }
156
+ /**
157
+ * Refuses a `ReplayOnly` save by a non-Owner caller.
158
+ *
159
+ * WHY THIS OVERRIDE EXISTS. Invariants 1-4 are enforced in `Validate()`, and `Validate()` is the
160
+ * one enforcement point `BaseEntity.Save()` can be told to skip: under `EntitySaveOptions.ReplayOnly`
161
+ * it force-passes validation WITHOUT calling `Validate()` at all (`baseEntity.ts:3725`), and
162
+ * `ReplayOnly` does NOT suppress the write — the provider only uses it to bypass the
163
+ * `AllowUpdateAPI`/`AllowCreateAPI` gates before building and executing the SQL
164
+ * (`databaseProviderBase.ts:1436-1443`). So a `ReplayOnly` save skipped all four Save-side
165
+ * invariants while invariant 5 stayed enforced, because `Delete()` is an override and an
166
+ * override cannot be switched off. This restores the symmetry: now neither half depends on the
167
+ * caller's options.
168
+ *
169
+ * NOT CURRENTLY REACHABLE BY AN UNTRUSTED CALLER, and this is deliberately belt-and-braces
170
+ * rather than a live hole. Every wire path that accepts `ReplayOnly` was enumerated: the
171
+ * GraphQL `options___`/`DeleteOptionsInput` input exists only on the DELETE mutation (create and
172
+ * update carry no options input, and `ResolverBase.CreateRecord`/`UpdateRecord` call `Save()`
173
+ * with none); REST's `EntityCRUDHandler` does accept it, but calls `entity.Validate()`
174
+ * explicitly before `Save()`, so the guard still runs there; and `graphQLSystemUserClient`
175
+ * requires the system API key, i.e. a caller who is already superuser. The point is that the
176
+ * class docstring's "holds on EVERY write path" is a promise a future wire path forwarding save
177
+ * options would otherwise quietly break — a guard whose protection is one option away from off
178
+ * is not the guard this file claims to be.
179
+ *
180
+ * WHY REFUSE RATHER THAN RE-RUN THE INVARIANTS. Re-running invariants 1-4 here would duplicate
181
+ * `Validate()`'s logic in a second place that must then be kept in step with it — the exact
182
+ * duplicated-decision this repo's design rules call out. Refusing outright is smaller and
183
+ * strictly safer: `ReplayOnly` is a replication/replay facility for trusted sync paths, and a
184
+ * non-Owner has no legitimate reason to replay writes onto the user table. Owners are exempt,
185
+ * so replication and admin paths that run as an Owner are unaffected.
186
+ */
187
+ async Save(options) {
188
+ if (options?.ReplayOnly && !this.callerIsOwner()) {
189
+ return this.refuseSave();
190
+ }
191
+ return super.Save(options);
192
+ }
193
+ refuseSave() {
194
+ const result = new BaseEntityResult();
195
+ result.Success = false;
196
+ result.Type = this.IsSaved ? 'update' : 'create';
197
+ result.Message =
198
+ 'Only an Owner may perform a ReplayOnly save on a user record. ReplayOnly bypasses ' +
199
+ 'validation, which is where this entity\'s privilege-elevation invariants are enforced.';
200
+ result.StartedAt = new Date();
201
+ result.EndedAt = new Date();
202
+ this.RegisterResultHistoryEntry(result);
203
+ return false;
204
+ }
205
+ /**
206
+ * Refuses deletion of any `MJ: Users` row by a non-Owner caller. See invariant 5 above for why:
207
+ * MJ deactivates users via `IsActive` rather than deleting them, and an unguarded delete would
208
+ * let a non-Owner remove any account, including Owner accounts this class's other exemptions
209
+ * depend on.
210
+ *
211
+ * Reports the refusal the way `MJListEntityServer.Delete` does for its own row-level DELETE
212
+ * authorization check — a `BaseEntityResult` on the result history so `LatestResult.CompleteMessage`
213
+ * carries the reason (`Delete()` returns `false` on a logical rejection rather than throwing; see
214
+ * the CLAUDE.md Save/Delete error-handling contract) — rather than `MJUserRoutineEntityServer`'s
215
+ * `Delete()` override, which is FK-cleanup ordering, not an authorization decision, and reports
216
+ * nothing beyond a bare `false`.
217
+ */
218
+ async Delete(options) {
219
+ if (!this.callerIsOwner()) {
220
+ return this.refuseDelete();
221
+ }
222
+ return super.Delete(options);
223
+ }
224
+ refuseDelete() {
225
+ const result = new BaseEntityResult();
226
+ result.Success = false;
227
+ result.Type = 'delete';
228
+ result.Message =
229
+ 'Only an Owner may delete a user record. MJ deactivates users via IsActive rather than deleting them.';
230
+ result.StartedAt = new Date();
231
+ result.EndedAt = new Date();
232
+ this.RegisterResultHistoryEntry(result);
233
+ return false;
234
+ }
235
+ /**
236
+ * Invariant 1 — a non-Owner may not create a `MJ: Users` row at all. See the class docstring
237
+ * for why this is the correct scope (not merely "may not create an Owner"): `Name` has no
238
+ * unique index, so restricting only `Type='Owner'` on create would leave the INSERT path open
239
+ * to the same principal-redirection invariant 4 blocks on UPDATE.
240
+ */
241
+ validateCreateRefused(result) {
242
+ // Whole-record refusal, not a field-value problem — no field in this repo's convention
243
+ // exists for that (surveyed every ValidationErrorInfo call site under custom/*.server.ts;
244
+ // all attribute to the specific field the value is wrong for). Attributing to 'Type' would
245
+ // make a form highlight Type for a refusal that has nothing to do with its value; 'ID' is
246
+ // the closer fit — it is the field that identifies WHICH record is being refused.
247
+ result.Errors.push(new ValidationErrorInfo('ID', 'Only an Owner may create a user record.', this.ID, ValidationErrorType.Failure));
248
+ }
249
+ /**
250
+ * Invariant 2 — a non-Owner may not change `Type` on an EXISTING row (self-promotion, or
251
+ * demoting somebody else). Non-Owner creation is refused entirely by `validateCreateRefused`,
252
+ * so this method only needs to consider updates — `Validate()` only calls it on that branch.
253
+ */
254
+ validateNoTypeChange(result) {
255
+ if (!(this.GetFieldByName('Type')?.Dirty ?? false)) {
256
+ return;
257
+ }
258
+ result.Errors.push(new ValidationErrorInfo('Type', 'Only an Owner may set or change a user\'s Type. Type is the column MJ\'s Owner checks read, ' +
259
+ 'so changing it grants platform-superuser access.', this.Type, ValidationErrorType.Failure));
260
+ }
261
+ /**
262
+ * Invariant 3 — a non-Owner may only modify their own user row, compared against the PRE-SAVE
263
+ * `ID` (see the class docstring for why the pre-save value is the meaningful comparison here).
264
+ *
265
+ * Fails CLOSED: if the pre-save identity cannot be established at all, the save is refused
266
+ * rather than silently permitted. This is currently unreachable in production only because
267
+ * `MJ: Users` has `TrackRecordChanges=1`, which forces `ResolverBase.UpdateRecord` to genuinely
268
+ * load the row from the database before applying edits — but that is an unrelated entity flag,
269
+ * not a guarantee this class should assume will always hold.
270
+ */
271
+ validateOwnRowOnly(result) {
272
+ const preSaveId = this.GetFieldByName('ID')?.OldValue;
273
+ if (!preSaveId) {
274
+ result.Errors.push(new ValidationErrorInfo('ID', 'Could not determine this record\'s pre-save identity, so ownership cannot be verified. ' +
275
+ 'Refusing to save rather than risk permitting an edit to another user\'s row.', preSaveId ?? null, ValidationErrorType.Failure));
276
+ return;
277
+ }
278
+ if (!UUIDsEqual(preSaveId, this.ActiveUser.ID)) {
279
+ result.Errors.push(new ValidationErrorInfo('ID', 'You may only modify your own user record. Changing another user\'s record requires an Owner.', preSaveId, ValidationErrorType.Failure));
280
+ }
281
+ }
282
+ /**
283
+ * Invariant 4 — a non-Owner may not change `Name` on an existing row.
284
+ *
285
+ * `Name` is the column the context-user ladder resolves against first, so it is effectively a
286
+ * capability name, not a display name. Non-Owner creation (where auto-provisioning legitimately
287
+ * sets `Name = email`) is already refused entirely by `validateCreateRefused`, so this method
288
+ * only runs on updates.
289
+ *
290
+ * `Email` is deliberately NOT frozen alongside `Name` — see the class docstring's invariant 4
291
+ * paragraph for why the ladder's other rung doesn't need the same treatment.
292
+ */
293
+ validateNameImmutable(result) {
294
+ if (!(this.GetFieldByName('Name')?.Dirty ?? false)) {
295
+ return;
296
+ }
297
+ result.Errors.push(new ValidationErrorInfo('Name', 'Only an Owner may change a user\'s Name. Name is the identifier MJ\'s configured-principal ' +
298
+ 'resolution matches against, so changing it can redirect which user the server acts as. ' +
299
+ 'Update FirstName, LastName or Title instead.', this.Name, ValidationErrorType.Failure));
300
+ }
301
+ /**
302
+ * True when the caller is an Owner (or when there is no caller to evaluate — the guard has
303
+ * nothing to compare against).
304
+ *
305
+ * The "no caller ⇒ exempt" default has DIFFERENT reachability for `Save()` vs. `Delete()`:
306
+ * - `Save()`: `BaseEntity.CheckPermissions` throws on a falsy `ActiveUser`
307
+ * (`baseEntity.ts:4003-4005`) and runs at `baseEntity.ts:3702`, BEFORE `Validate()` is
308
+ * called at `baseEntity.ts:3730` — so a caller-less `Save()` never reaches this method at
309
+ * all in production. The default is effectively decorative there.
310
+ * - `Delete()`: the sequencing is the OPPOSITE. THIS class's `Delete()` override calls
311
+ * `callerIsOwner()` as its very first statement, before `super.Delete()` is ever invoked —
312
+ * `CheckPermissions` only runs later, INSIDE `super.Delete()` (`baseEntity.ts:4612`). So a
313
+ * caller-less `Delete()` call DOES reach this method first, and the "no caller ⇒ exempt"
314
+ * default here is load-bearing: it lets the call proceed into `super.Delete()`, where
315
+ * `CheckPermissions` is the thing that actually refuses it. If this default were flipped to
316
+ * "no caller ⇒ refuse", a caller-less delete would be refused by `refuseDelete()` instead —
317
+ * same ultimate outcome (refused), different refusal mechanism and message.
318
+ *
319
+ * `Type` is an `NCHAR` column, so it arrives space-padded; casing is normalized for the same
320
+ * reason `principals.ts` does. Reads `ActiveUser` rather than `ContextCurrentUser` directly so
321
+ * a per-request provider's `CurrentUser` is honored on multi-provider servers.
322
+ */
323
+ callerIsOwner() {
324
+ const caller = this.ActiveUser;
325
+ if (!caller) {
326
+ return true;
327
+ }
328
+ return caller.Type?.trim().toLowerCase() === 'owner';
329
+ }
330
+ };
331
+ MJUserEntityServer = __decorate([
332
+ RegisterClass(BaseEntity, 'MJ: Users')
333
+ ], MJUserEntityServer);
334
+ export { MJUserEntityServer };
335
+ //# sourceMappingURL=MJUserEntityServer.server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MJUserEntityServer.server.js","sourceRoot":"","sources":["../../src/custom/MJUserEntityServer.server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,243 @@
1
+ import { EntityDeleteOptions, EntitySaveOptions, IMetadataProvider, ValidationResult } from '@memberjunction/core';
2
+ import { MJUserRoleEntity } from '@memberjunction/core-entities';
3
+ /**
4
+ * Server-side `MJ: User Roles` entity. It carries TWO independent guards that happen to protect the
5
+ * same table from opposite directions. They share no state and no helpers, and every write path
6
+ * must satisfy both:
7
+ *
8
+ * A. **Role elevation** (issue #4282) — bounds what the CALLER may do, by their own roles.
9
+ * B. **System-user field access** — bounds what may be done TO the system user's role set.
10
+ *
11
+ * Guard A and guard B are orthogonal: A asks "is this caller entitled to make this change", B asks
12
+ * "does this change leave the system user field-restricted". A change must clear both, so the
13
+ * checks compose as a conjunction and are evaluated A-then-B (A is an in-memory role-list test; B
14
+ * walks cached metadata and only ever runs when the system user is the target).
15
+ *
16
+ * ---------------------------------------------------------------------------------------------
17
+ * GUARD A — ROLE ELEVATION (issue #4282)
18
+ * ---------------------------------------------------------------------------------------------
19
+ *
20
+ * WHY THIS EXISTS AT ALL. Issue #4260 (`MJUserEntityServer`) closed the `User.Type` route to
21
+ * elevated capability. Role assignment is the platform's OTHER authority mechanism and was
22
+ * unguarded: verified against a live database, the `Developer` and `Integration` roles hold
23
+ * unfiltered `CanCreate`/`CanUpdate`/`CanDelete` on `MJ: User Roles`, `AllowCreateAPI` /
24
+ * `AllowUpdateAPI` / `AllowDeleteAPI` are all true on the entity, and no server-side subclass
25
+ * existed — `ClassFactory` resolved the generated `MJUserRoleEntity`, whose `Validate()` knows
26
+ * nothing about who is calling. Reproduced end to end before this class was written: a caller whose
27
+ * `Type` is `'User'`, holding only `Developer` and `UI`, inserted a row granting itself
28
+ * `Integration`; `Validate()` returned `Success=true` and `Save()` returned `true`.
29
+ *
30
+ * THE INVARIANT: **a non-Owner may only grant, move or revoke a role they themselves hold.**
31
+ *
32
+ * Why that rule and not a stricter one. Unlike `User.Type` — two values, no legitimate non-Owner
33
+ * write — role assignment has real non-Owner use cases the platform ships with: delegated
34
+ * administration, IdP/group sync, onboarding automation. The subset rule is the weakest rule that
35
+ * still closes elevation completely, because it is a CEILING: whatever a non-Owner does through
36
+ * this entity, the authority they can hand out is authority they already had, so no sequence of
37
+ * calls lets any caller exceed their own grant. Issue #4282 also floated adding "and may not grant
38
+ * a role that confers write access to `MJ: Roles`/`MJ: User Roles`". That was considered and
39
+ * declined: it does not close any elevation the subset rule leaves open (granting a peer a role you
40
+ * hold is delegation, not escape), it breaks the peer-onboarding case above, and it would require
41
+ * this guard to read `EntityInfo.Permissions` — trading the purity below for protection that
42
+ * `MJ: Entity Permissions` (which carries the SAME unfiltered `Developer`/`Integration` grant, and
43
+ * is a separate, still-open route) would undercut anyway.
44
+ *
45
+ * WHY HERE RATHER THAN IN A RESOLVER, and why `Save()`/`Delete()` are overridden alongside
46
+ * `Validate()`: identical reasoning to `MJUserEntityServer`, and see that file for the full
47
+ * argument. In short — `Validate()` runs inside `BaseEntity.Save()`, so the rule holds on every
48
+ * write path (GraphQL resolvers, Remote Operations, the Create/Update/Delete Record actions,
49
+ * metadata sync, one-off scripts) and for any role a deployment invents; but `BaseEntity.Save()`
50
+ * force-passes validation WITHOUT calling `Validate()` under `EntitySaveOptions.ReplayOnly`
51
+ * (`baseEntity.ts:3725`) while still performing the write, and `Delete()` never consults
52
+ * `Validate()` at all. Overriding all three is what makes "every write path" literal.
53
+ *
54
+ * THE INVARIANTS, for a caller whose `Type` is not `'Owner'`:
55
+ *
56
+ * 1. **Create** — `RoleID` must name a role the caller holds. This is the #4282 escalation
57
+ * itself: the self-grant of an unheld role.
58
+ * 2. **Update** — the NEW `RoleID` must be held (invariant 1, reached through UPDATE instead of
59
+ * INSERT), and so must the PRE-SAVE `RoleID`. The second half is not redundant: moving an
60
+ * existing grant off a role you do not hold is a revocation you were not entitled to make,
61
+ * and without it a non-Owner could strip an Owner of any role by repointing the row.
62
+ * `UserID` is deliberately NOT frozen — moving a grant between users stays within the
63
+ * caller's ceiling, which is the whole point of the rule.
64
+ * 3. **Delete** — the row's `RoleID` must be held by the caller, for the same reason as the
65
+ * pre-save half of invariant 2. Revocation is bounded by the same ceiling as granting.
66
+ * 4. **A `ReplayOnly` save is refused outright**, because `ReplayOnly` is the one option that
67
+ * skips `Validate()` and therefore invariants 1-2. Refusing rather than re-running them keeps
68
+ * the decision in ONE place; `ReplayOnly` is a replication facility for trusted sync paths and
69
+ * a non-Owner has no legitimate reason to replay writes onto the role-assignment table.
70
+ *
71
+ * A caller holding NO roles can therefore grant nothing — correct, and it is also why this guard
72
+ * needs no special case for an unpopulated `UserInfo.UserRoles`: the subset test fails closed on
73
+ * an empty list on its own. (Server-side that list is populated — `MJServer`'s `context.ts` resolves
74
+ * the request principal from `UserCache` precisely "to ensure UserRoles is properly populated", and
75
+ * `CloneUserForSessionContext` carries it onto the per-session clone.)
76
+ *
77
+ * NOT COVERED, deliberately: a magic-link session's SYNTHESIZED roles (`context.ts`
78
+ * `buildMagicLinkSessionUser`) count as roles held, so a scope-limited principal is measured by the
79
+ * same rule as any other caller. Narrowing scope-limited principals further is a magic-link concern,
80
+ * not a role-elevation one — `IsScopeLimitedPrincipal` lives in MJServer, which this package cannot
81
+ * import, and duplicating its predicate here would put one decision in two places. Such a session
82
+ * still needs `CanCreate` on this entity to reach the guard at all, which the `Magic Link Baseline`
83
+ * role does not grant.
84
+ *
85
+ * Owner-type callers are exempt from all four, so admin role management, and the
86
+ * `SyncRolesAndUsers` / `SyncUsers` mutations (both `@RequireSystemUser()`, whose `getSystemUser()`
87
+ * resolves the seeded `Type='Owner'` system user), are unaffected on a default install. A
88
+ * deployment whose system user is NOT an Owner will see those sync paths fail closed — loudly,
89
+ * at the save — exactly as #4260 documented for `MJ: Users`.
90
+ *
91
+ * Guard A is pure: it reads only this record's own field state and the caller's cached roles. No
92
+ * `RunView`, no provider, no engine, no I/O — so it costs nothing per save/delete and is
93
+ * unit-testable without a database. (Guard B is NOT pure in that sense; see below.)
94
+ *
95
+ * ---------------------------------------------------------------------------------------------
96
+ * GUARD B — SYSTEM-USER FIELD ACCESS
97
+ * ---------------------------------------------------------------------------------------------
98
+ *
99
+ * This is the other half of the system-user guard for field-level security.
100
+ * `MJEntityFieldPermissionEntityServer` guards the RULES; this guards the account's ROLE SET, from
101
+ * both directions. Without both halves an administrator reaches the forbidden state simply by
102
+ * doing the steps in a different order.
103
+ *
104
+ * - **Assignment** ({@link SystemUserRejectionReason}) — refuses giving the system user a role that
105
+ * already denies a field. Only DENYING rules count here, because adding a role can only add rules
106
+ * to the aggregate: its `Allow` rows grant, its `No Access` rows are inert, and only a `Deny`
107
+ * can take something away. A guard that counted every rule would refuse to reassemble the
108
+ * account's own role set the moment field security was enabled anywhere.
109
+ * - **Removal** ({@link Delete}) — refuses taking a role away when that would leave the account
110
+ * short of its entity-level access. Removal is the opposite shape: it drops rules OUT of the
111
+ * aggregate, so what matters is not what the departing role said but whether an `Allow` survives
112
+ * without it.
113
+ *
114
+ * Why the system user must stay unrestricted: the server runs background work as that account.
115
+ * It pre-warms the shared engine caches at startup, and in task mode — which job and agent
116
+ * runners use — engines instead load on first touch, so whichever caller gets there first
117
+ * configures the engine for the entire process. Engine caches are process-wide and shared
118
+ * across users. A restricted system user could therefore leave partially loaded records in a
119
+ * cache that everyone reads afterward, with nothing at the point of failure pointing back at
120
+ * the role assignment that caused it.
121
+ *
122
+ * Unlike guard A, guard B reads cached metadata (`Metadata.Entities`, the provider, `UserCache`).
123
+ * It still performs no database I/O, but it is not state-free, and it needs those caches populated
124
+ * to have an opinion — a cold `UserCache` skips the check rather than blocking an administrator.
125
+ *
126
+ * This restricts CONFIGURATION only. There is no user who is exempt from a Deny at runtime —
127
+ * not even the system user, whose access comes from the same rows as everyone else's.
128
+ */
129
+ export declare class MJUserRoleEntityServer extends MJUserRoleEntity {
130
+ Validate(): ValidationResult;
131
+ /**
132
+ * Refuses a `ReplayOnly` save by a non-Owner caller — invariant 4. `ReplayOnly` force-passes
133
+ * validation without calling `Validate()` (`baseEntity.ts:3725`) yet does not suppress the
134
+ * write, so without this override invariants 1-2 were one caller-supplied option away from off
135
+ * while invariant 3 stayed enforced (an override cannot be switched off). Mirrors
136
+ * `MJUserEntityServer.Save()`, including its reasoning for refusing rather than re-validating.
137
+ */
138
+ Save(options?: EntitySaveOptions): Promise<boolean>;
139
+ /**
140
+ * Enforces both guards on the delete path.
141
+ *
142
+ * Guard A, invariant 3 — a non-Owner may only revoke a role they hold themselves.
143
+ *
144
+ * Guard B — refuses REMOVING a role from the system user when that would cost the account its
145
+ * field access. The mirror of the assignment guard, and needed for the same reason the
146
+ * field-permission subclass guards its own delete path: the system user's access is ordinary
147
+ * `Allow` rows, and taking a role away drops that role's rows out of the aggregate. If the
148
+ * remaining roles have `No Access` on a field, the last removal denies it — with no `Deny`
149
+ * written anywhere and no field-permission row touched. It permits the removal when it also
150
+ * costs the account its entity-level read, since it is then denied one level up and field rules
151
+ * decide nothing — which is why the projection re-evaluates the entity ceiling too, rather than
152
+ * only the field rules.
153
+ *
154
+ * Reports refusals the way `MJUserEntityServer.Delete` does — a `BaseEntityResult` on the
155
+ * result history, so `LatestResult.CompleteMessage` carries the reason, `Delete()` returning
156
+ * `false` on a logical rejection rather than throwing (the CLAUDE.md Save/Delete error-handling
157
+ * contract). Note that `SyncRolesUsersResolver.SyncUserRoles` treats a `false` from this method
158
+ * as a hard error and throws, rolling its transaction back; that mutation runs as the system
159
+ * Owner, so it is exempt from guard A and never sees that refusal on a default install.
160
+ */
161
+ Delete(options?: EntityDeleteOptions): Promise<boolean>;
162
+ /**
163
+ * Invariants 1 and 2's forward half — the role being assigned must be one the caller holds.
164
+ * Attributed to `RoleID` because that IS the field whose value is refused, so a form highlights
165
+ * the control the user must change.
166
+ */
167
+ private validateRoleHeld;
168
+ /**
169
+ * Invariant 2's second half — the role this assignment granted BEFORE the edit must also be one
170
+ * the caller holds, compared against the pre-save value rather than whatever the in-memory
171
+ * object arrived holding. `MJ: User Roles` has `TrackRecordChanges=1`, which forces
172
+ * `ResolverBase.UpdateRecord` to genuinely load the row from the database before applying edits,
173
+ * so that pre-save value is the row's real prior state on every wire path — but that is an
174
+ * unrelated entity flag, not a guarantee this class should assume will always hold, so a
175
+ * pre-save value that cannot be established fails CLOSED with its own message rather than
176
+ * falling through to "you do not hold role null".
177
+ */
178
+ private validatePriorRoleHeld;
179
+ /** Records a logical refusal on the result history so `LatestResult.CompleteMessage` carries the reason. */
180
+ private refuse;
181
+ /**
182
+ * True when the caller's own role assignments include `roleId`.
183
+ *
184
+ * `UUIDsEqual` rather than `===` because role IDs reach this class from two different sources
185
+ * with two different casings — the cached `UserInfo.UserRoles` and the entity field the client
186
+ * sent (see `guides/UUID_COMPARISON_GUIDE.md`). A falsy `roleId`, or a caller with no roles,
187
+ * yields `false`: both are the fail-closed answer, and neither needs a branch of its own.
188
+ */
189
+ private callerHoldsRole;
190
+ /**
191
+ * True when the caller is an Owner (or when there is no caller to evaluate — the guard has
192
+ * nothing to compare against). `Type` is an `NCHAR` column, so it arrives space-padded; casing
193
+ * is normalized for the same reason `MJServer`'s `principals.ts` does.
194
+ *
195
+ * The "no caller ⇒ exempt" default has the same asymmetric reachability documented on
196
+ * `MJUserEntityServer.callerIsOwner()`: `BaseEntity.CheckPermissions` throws on a falsy
197
+ * `ActiveUser` BEFORE `Validate()` runs inside `Save()`, so the default is decorative there;
198
+ * in `Delete()` it is load-bearing, letting a caller-less delete proceed into `super.Delete()`
199
+ * where `CheckPermissions` is what actually refuses it.
200
+ *
201
+ * Reads `ActiveUser` rather than `ContextCurrentUser` directly so a per-request provider's
202
+ * `CurrentUser` is honored on multi-provider servers.
203
+ *
204
+ * Applies to guard A only. Guard B has no Owner exemption — see `Validate()`.
205
+ */
206
+ private callerIsOwner;
207
+ /**
208
+ * Why this role may not be taken off this user, or null when it may.
209
+ * Only ever rejects for the system user; every other user is unaffected.
210
+ */
211
+ private systemUserRoleRemovalReason;
212
+ /**
213
+ * Why this role may not be given to this user, or null when it may.
214
+ * Only ever rejects for the system user; every other user is unaffected.
215
+ */
216
+ static SystemUserRejectionReason(userID: string | null, roleID: string | null, provider?: IMetadataProvider): string | null;
217
+ /**
218
+ * Names of entities where this role carries at least one RESTRICTING field rule — a `Deny`
219
+ * on any verb. Walks cached metadata only — no database access. Runs only when the system
220
+ * user is the save target, which is rare.
221
+ *
222
+ * Grants and neutrals are ignored, and must be: the system user holds the standard roles
223
+ * (UI, Developer, Integration), snapshot initialization writes those roles `Allow` rows on
224
+ * every entity they can read, and field security has no runtime exemption to fall back on.
225
+ * A guard that counted any rule at all would refuse to reassemble the system user's own role
226
+ * set the moment field security was enabled anywhere.
227
+ *
228
+ * Deliberately does NOT gate on {@link EntityInfo.EnableFieldLevelSecurity}, even though
229
+ * rules on a disabled entity are inactive and gating would be the cheaper walk. Gating
230
+ * would leave the two halves of this guard unable to compose, and the gap is reachable in
231
+ * three ordinary steps: disable field security on an entity, assign the role (now carrying
232
+ * no active rules) to the system user, re-enable. Each step is permitted and the end state
233
+ * is the one both guards exist to prevent. Disabling preserves rules so re-enabling does
234
+ * not lose them, so a rule on a disabled entity is dormant rather than gone.
235
+ */
236
+ private static EntitiesWithRestrictingFieldRulesForRole;
237
+ }
238
+ /**
239
+ * Loader stub — prevents the class from being tree-shaken out of the bundle. Mirrors the
240
+ * pattern used by the other server-side entity subclasses in this package.
241
+ */
242
+ export declare function LoadMJUserRoleEntityServer(): void;
243
+ //# sourceMappingURL=MJUserRoleEntityServer.server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MJUserRoleEntityServer.server.d.ts","sourceRoot":"","sources":["../../src/custom/MJUserRoleEntityServer.server.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,mBAAmB,EAEnB,iBAAiB,EACjB,iBAAiB,EAMjB,gBAAgB,EACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6HG;AACH,qBACa,sBAAuB,SAAQ,gBAAgB;IACxC,QAAQ,IAAI,gBAAgB;IAgC5C;;;;;;OAMG;IACmB,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,OAAO,CAAC;IAWzE;;;;;;;;;;;;;;;;;;;;;OAqBG;IACmB,MAAM,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB7E;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAOxB;;;;;;;;;OASG;IACH,OAAO,CAAC,qBAAqB;IAqB7B,4GAA4G;IAC5G,OAAO,CAAC,MAAM;IAWd;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAQvB;;;;;;;;;;;;;;;OAeG;IACH,OAAO,CAAC,aAAa;IAQrB;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IAqBnC;;;OAGG;WACW,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI;IAyBlI;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,MAAM,CAAC,wCAAwC;CAe1D;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}