@memberjunction/generic-database-provider 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 (39) hide show
  1. package/README.md +25 -0
  2. package/dist/DatabaseWellKnownUserSource.d.ts +48 -0
  3. package/dist/DatabaseWellKnownUserSource.d.ts.map +1 -0
  4. package/dist/DatabaseWellKnownUserSource.js +101 -0
  5. package/dist/DatabaseWellKnownUserSource.js.map +1 -0
  6. package/dist/GenericDatabaseProvider.d.ts +215 -3
  7. package/dist/GenericDatabaseProvider.d.ts.map +1 -1
  8. package/dist/GenericDatabaseProvider.js +793 -136
  9. package/dist/GenericDatabaseProvider.js.map +1 -1
  10. package/dist/SqlLogger.d.ts +8 -0
  11. package/dist/SqlLogger.d.ts.map +1 -1
  12. package/dist/SqlLogger.js +31 -10
  13. package/dist/SqlLogger.js.map +1 -1
  14. package/dist/SystemUserFieldAccessCheck.d.ts +64 -0
  15. package/dist/SystemUserFieldAccessCheck.d.ts.map +1 -0
  16. package/dist/SystemUserFieldAccessCheck.js +137 -0
  17. package/dist/SystemUserFieldAccessCheck.js.map +1 -0
  18. package/dist/UserCache.d.ts.map +1 -1
  19. package/dist/UserCache.js +2 -2
  20. package/dist/UserCache.js.map +1 -1
  21. package/dist/index.d.ts +5 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +9 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/queryPagingEngine.d.ts +19 -0
  26. package/dist/queryPagingEngine.d.ts.map +1 -1
  27. package/dist/queryPagingEngine.js +45 -0
  28. package/dist/queryPagingEngine.js.map +1 -1
  29. package/dist/saveTypes.d.ts +3 -3
  30. package/dist/saveTypes.d.ts.map +1 -1
  31. package/dist/systemUser.d.ts +27 -0
  32. package/dist/systemUser.d.ts.map +1 -0
  33. package/dist/systemUser.js +29 -0
  34. package/dist/systemUser.js.map +1 -0
  35. package/dist/systemUserFieldAccess.d.ts +114 -0
  36. package/dist/systemUserFieldAccess.d.ts.map +1 -0
  37. package/dist/systemUserFieldAccess.js +198 -0
  38. package/dist/systemUserFieldAccess.js.map +1 -0
  39. package/package.json +14 -14
@@ -42,11 +42,11 @@ export type SaveCallBinding =
42
42
  */
43
43
  {
44
44
  kind: 'mssql-declare-exec';
45
- /** `DECLARE @x_uuid type, @y_uuid type` — empty if no params. */
45
+ /** `DECLARE @x_<suffix> type, @y_<suffix> type` — empty if no params. Suffix: GenericDatabaseProvider.allocateSaveCallSuffix. */
46
46
  preambleSQL: string;
47
- /** `SET @x_uuid = N'value'\nSET @y_uuid = 1` — empty if no params. */
47
+ /** `SET @x_<suffix> = N'value'\nSET @y_<suffix> = 1` — empty if no params. */
48
48
  setSQL: string;
49
- /** Comma-joined `@CodeName=@x_uuid, ...` for the EXEC line. */
49
+ /** Comma-joined `@CodeName=@x_<suffix>, ...` for the EXEC line. */
50
50
  callArgsSQL: string;
51
51
  /** Back-compat inline single-line `@CodeName=N'value', ...` form. */
52
52
  simpleParamsSQL: string;
@@ -1 +1 @@
1
- {"version":3,"file":"saveTypes.d.ts","sourceRoot":"","sources":["../src/saveTypes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GACtB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvB;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe;AACvB;;;;GAIG;AACD;IACI,IAAI,EAAE,oBAAoB,CAAC;IAC3B,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;CAC3B;AACH;;;GAGG;GACD;IACI,IAAI,EAAE,eAAe,CAAC;IACtB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,MAAM,EAAE,OAAO,EAAE,CAAC;CACrB;AACH;;;;GAIG;GACD;IACI,IAAI,EAAE,aAAa,CAAC;IACpB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;CACpB,CAAC;AAER;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC;CAC1B;AAKD,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"saveTypes.d.ts","sourceRoot":"","sources":["../src/saveTypes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;;;;;;;GASG;AACH,MAAM,MAAM,gBAAgB,GACtB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvB;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe;AACvB;;;;GAIG;AACD;IACI,IAAI,EAAE,oBAAoB,CAAC;IAC3B,iIAAiI;IACjI,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;CAC3B;AACH;;;GAGG;GACD;IACI,IAAI,EAAE,eAAe,CAAC;IACtB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,MAAM,EAAE,OAAO,EAAE,CAAC;CACrB;AACH;;;;GAIG;GACD;IACI,IAAI,EAAE,aAAa,CAAC;IACpB,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;CACpB,CAAC;AAER;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC;CAC1B;AAKD,YAAY,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { UserInfo } from '@memberjunction/core';
2
+ /**
3
+ * ID of the MJ **system user** — the account the server runs its own work as.
4
+ *
5
+ * Canonical definition. Seeded by the baseline on both platforms; do not change it without a
6
+ * migration.
7
+ *
8
+ * It lives in a server-side package because it is a server concept — a browser has no system
9
+ * account and no use for its ID. Both concrete providers (SQL Server and PostgreSQL) depend on
10
+ * this package and neither depends on the other, so one definition serves both.
11
+ *
12
+ * Code that needs to ASK whether a user is the system user should call
13
+ * `WellKnownUserSource.Instance.IsSystemUser(user)` instead; this constant is for the
14
+ * implementations that have to know the actual value.
15
+ */
16
+ export declare const SystemUserID: string;
17
+ /**
18
+ * True when this user is the MJ system user. Null/undefined-safe, and case-insensitive because
19
+ * the same UUID arrives with different casing depending on its source (a client-minted
20
+ * lowercase value vs. an uppercase one loaded from SQL Server).
21
+ *
22
+ * Not exported from the package: callers ask
23
+ * `WellKnownUserSource.Instance.IsSystemUser(user)`, which works in shared code and answers
24
+ * false where no server-side source is registered. This backs the implementation of that seam.
25
+ */
26
+ export declare function IsSystemUser(user: UserInfo | null | undefined): boolean;
27
+ //# sourceMappingURL=systemUser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemUser.d.ts","sourceRoot":"","sources":["../src/systemUser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGhD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,MAA+C,CAAC;AAE3E;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAEvE"}
@@ -0,0 +1,29 @@
1
+ import { UUIDsEqual } from '@memberjunction/global';
2
+ /**
3
+ * ID of the MJ **system user** — the account the server runs its own work as.
4
+ *
5
+ * Canonical definition. Seeded by the baseline on both platforms; do not change it without a
6
+ * migration.
7
+ *
8
+ * It lives in a server-side package because it is a server concept — a browser has no system
9
+ * account and no use for its ID. Both concrete providers (SQL Server and PostgreSQL) depend on
10
+ * this package and neither depends on the other, so one definition serves both.
11
+ *
12
+ * Code that needs to ASK whether a user is the system user should call
13
+ * `WellKnownUserSource.Instance.IsSystemUser(user)` instead; this constant is for the
14
+ * implementations that have to know the actual value.
15
+ */
16
+ export const SystemUserID = 'ecafccec-6a37-ef11-86d4-000d3a4e707e';
17
+ /**
18
+ * True when this user is the MJ system user. Null/undefined-safe, and case-insensitive because
19
+ * the same UUID arrives with different casing depending on its source (a client-minted
20
+ * lowercase value vs. an uppercase one loaded from SQL Server).
21
+ *
22
+ * Not exported from the package: callers ask
23
+ * `WellKnownUserSource.Instance.IsSystemUser(user)`, which works in shared code and answers
24
+ * false where no server-side source is registered. This backs the implementation of that seam.
25
+ */
26
+ export function IsSystemUser(user) {
27
+ return !!user?.ID && UUIDsEqual(user.ID, SystemUserID);
28
+ }
29
+ //# sourceMappingURL=systemUser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemUser.js","sourceRoot":"","sources":["../src/systemUser.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,YAAY,GAAW,sCAAsC,CAAC;AAE3E;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,IAAiC;IAC1D,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,YAAY,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,114 @@
1
+ /**
2
+ * @fileoverview Keeping the MJ system user's own field access intact, now that field-level
3
+ * security has no runtime exemption for anybody.
4
+ *
5
+ * ## Why this file exists
6
+ *
7
+ * The system user is the account the server runs its own work as. It pre-warms the shared engine
8
+ * caches at startup, and in task mode (job and agent runners) engines instead load on first touch,
9
+ * so whichever caller gets there first configures the engine for the whole process. Those caches
10
+ * are process-wide. A system user that cannot read a column therefore does not just fail its own
11
+ * query — it leaves a partially loaded record in a cache every later user reads, with nothing at
12
+ * the point of failure pointing back at the permission row that caused it.
13
+ *
14
+ * Field security deliberately has **no exempt user** at runtime: `GetUserFieldPermissions` has no
15
+ * identity branch at all, and the system user's access comes from ordinary `Allow` rows that
16
+ * snapshot initialization writes for the standard roles it holds. That is a better design — an
17
+ * administrator can see the rows and reason about them, where a code bypass is invisible exactly
18
+ * where access is decided. But it moves the burden here: something has to stop those rows being
19
+ * taken away.
20
+ *
21
+ * ## Why "does this rule contain a Deny?" is not enough
22
+ *
23
+ * Whether a change restricts a user is a property of the **aggregate across every role they
24
+ * hold**, never of one rule in isolation. Three ways to lock the system user out without writing
25
+ * a single `Deny`:
26
+ *
27
+ * 1. set `ReadAccess = 'No Access'` on each of its roles in turn — every save individually
28
+ * harmless, the last one leaves no `Allow` standing;
29
+ * 2. delete its roles' `Allow` rows;
30
+ * 3. reach either state through direct SQL, which no entity-layer guard sees at all.
31
+ *
32
+ * So the save-time guard evaluates the **projected aggregate** — the rules as they would stand
33
+ * after the proposed change — and {@link FindSystemUserFieldAccessViolations} sweeps for state
34
+ * that got there by some other route.
35
+ *
36
+ * @module @memberjunction/generic-database-provider
37
+ */
38
+ import { EntityFieldInfo, EntityInfo, FieldPermissionRuleForRole, IMetadataProvider, UserInfo } from '@memberjunction/core';
39
+ /**
40
+ * One field the MJ system user cannot fully use on an entity where field security is enabled.
41
+ */
42
+ export type SystemUserFieldAccessViolation = {
43
+ EntityName: string;
44
+ FieldName: string;
45
+ /** The verb the system user has lost, relative to its entity-level access. */
46
+ Verb: 'read' | 'update' | 'create';
47
+ };
48
+ /**
49
+ * Why a proposed field-permission state would leave the MJ system user short of its entity-level
50
+ * access, or null when it would not.
51
+ *
52
+ * Callers pass the rules **as they would be after** their change — with the edited row's new
53
+ * values substituted, or the deleted row removed. Classifying the changed row on its own cannot
54
+ * answer this question; see the module comment.
55
+ *
56
+ * Answers null (permits the change) in three cases, each deliberate:
57
+ *
58
+ * - **No system user resolves.** A cold `UserCache` is the normal state during a process's own
59
+ * bootstrap. Blocking an administrator on missing state would be worse than the risk, and
60
+ * {@link FindSystemUserFieldAccessViolations} runs at startup precisely to catch whatever slips
61
+ * through here.
62
+ * - **The field is unrestrictable.** Primary keys, `__mj_` columns and the security-configuration
63
+ * entities are forced open by the aggregation regardless of any row, so no rule about them can
64
+ * restrict anyone.
65
+ * - **The system user has no entity-level read.** It is already denied one level up, where the
66
+ * entity permission check has no exemption either, so field rules change nothing for it here.
67
+ *
68
+ * Note this does NOT gate on `EnableFieldLevelSecurity`. Rules on a disabled entity are dormant
69
+ * rather than gone — disabling deliberately preserves them — so gating would leave a three-step
70
+ * hole: disable, strip the system user's rows, re-enable.
71
+ *
72
+ * @param entity the entity the field belongs to
73
+ * @param field the field whose rules are changing
74
+ * @param projectedRules every rule that would bind to this field after the change
75
+ */
76
+ export declare function SystemUserFieldAccessLossReason(entity: EntityInfo | null | undefined, field: EntityFieldInfo | null | undefined, projectedRules: readonly FieldPermissionRuleForRole[]): string | null;
77
+ /** Narrowing for {@link FindSystemUserFieldAccessViolations}. */
78
+ export type SystemUserFieldAccessSweepOptions = {
79
+ /**
80
+ * Evaluate as though the system user did NOT hold this role.
81
+ *
82
+ * For the guard on **removing** a role from the account: taking a role away removes its rules
83
+ * from the aggregate, which can strip the last `Allow` just as surely as editing one. Both the
84
+ * field rules and the entity-level ceiling are re-evaluated without it, so a removal that also
85
+ * costs the account its entity-level read is correctly permitted — it is then denied one level
86
+ * up, and field rules decide nothing.
87
+ */
88
+ WithoutRoleID?: string;
89
+ };
90
+ /**
91
+ * Every field on an FLS-enabled entity that the system user cannot fully use.
92
+ *
93
+ * The backstop for state the save-time guard never saw: direct SQL, a migration, or a save made
94
+ * while the user cache was cold. Walks loaded metadata only — no queries — and skips entities with
95
+ * field security switched off, which is nearly all of them.
96
+ *
97
+ * @param provider the provider whose metadata to walk
98
+ * @param systemUser the account to check; defaults to the user cache's system user
99
+ * @param options optional projection — see {@link SystemUserFieldAccessSweepOptions}
100
+ */
101
+ export declare function FindSystemUserFieldAccessViolations(provider: IMetadataProvider | null | undefined, systemUser?: UserInfo | null, options?: SystemUserFieldAccessSweepOptions): SystemUserFieldAccessViolation[];
102
+ /**
103
+ * Whether the MJ system user holds the given role.
104
+ *
105
+ * The cheap pre-check for the save-time guard. Field aggregation only ever consults rules bound to
106
+ * a role the user holds, so a change to any other role's rule provably cannot move the system
107
+ * user's access — which lets callers skip the work of projecting and re-aggregating for the
108
+ * overwhelming majority of permission edits.
109
+ *
110
+ * False on a cold cache, matching {@link SystemUserFieldAccessLossReason}'s posture: with no system
111
+ * user to reason about there is nothing to protect, and the startup sweep is the backstop.
112
+ */
113
+ export declare function SystemUserHoldsRole(roleID: string | null | undefined): boolean;
114
+ //# sourceMappingURL=systemUserFieldAccess.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemUserFieldAccess.d.ts","sourceRoot":"","sources":["../src/systemUserFieldAccess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,EACH,eAAe,EACf,UAAU,EACV,0BAA0B,EAC1B,iBAAiB,EACjB,QAAQ,EACX,MAAM,sBAAsB,CAAC;AAI9B;;GAEG;AACH,MAAM,MAAM,8BAA8B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;CACtC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,+BAA+B,CAC3C,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,EACrC,KAAK,EAAE,eAAe,GAAG,IAAI,GAAG,SAAS,EACzC,cAAc,EAAE,SAAS,0BAA0B,EAAE,GACtD,MAAM,GAAG,IAAI,CAwBf;AAED,iEAAiE;AACjE,MAAM,MAAM,iCAAiC,GAAG;IAC5C;;;;;;;;OAQG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,mCAAmC,CAC/C,QAAQ,EAAE,iBAAiB,GAAG,IAAI,GAAG,SAAS,EAC9C,UAAU,CAAC,EAAE,QAAQ,GAAG,IAAI,EAC5B,OAAO,GAAE,iCAAsC,GAChD,8BAA8B,EAAE,CAuBlC;AA2CD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAM9E"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @fileoverview Keeping the MJ system user's own field access intact, now that field-level
3
+ * security has no runtime exemption for anybody.
4
+ *
5
+ * ## Why this file exists
6
+ *
7
+ * The system user is the account the server runs its own work as. It pre-warms the shared engine
8
+ * caches at startup, and in task mode (job and agent runners) engines instead load on first touch,
9
+ * so whichever caller gets there first configures the engine for the whole process. Those caches
10
+ * are process-wide. A system user that cannot read a column therefore does not just fail its own
11
+ * query — it leaves a partially loaded record in a cache every later user reads, with nothing at
12
+ * the point of failure pointing back at the permission row that caused it.
13
+ *
14
+ * Field security deliberately has **no exempt user** at runtime: `GetUserFieldPermissions` has no
15
+ * identity branch at all, and the system user's access comes from ordinary `Allow` rows that
16
+ * snapshot initialization writes for the standard roles it holds. That is a better design — an
17
+ * administrator can see the rows and reason about them, where a code bypass is invisible exactly
18
+ * where access is decided. But it moves the burden here: something has to stop those rows being
19
+ * taken away.
20
+ *
21
+ * ## Why "does this rule contain a Deny?" is not enough
22
+ *
23
+ * Whether a change restricts a user is a property of the **aggregate across every role they
24
+ * hold**, never of one rule in isolation. Three ways to lock the system user out without writing
25
+ * a single `Deny`:
26
+ *
27
+ * 1. set `ReadAccess = 'No Access'` on each of its roles in turn — every save individually
28
+ * harmless, the last one leaves no `Allow` standing;
29
+ * 2. delete its roles' `Allow` rows;
30
+ * 3. reach either state through direct SQL, which no entity-layer guard sees at all.
31
+ *
32
+ * So the save-time guard evaluates the **projected aggregate** — the rules as they would stand
33
+ * after the proposed change — and {@link FindSystemUserFieldAccessViolations} sweeps for state
34
+ * that got there by some other route.
35
+ *
36
+ * @module @memberjunction/generic-database-provider
37
+ */
38
+ import { EntityFieldInfo, UserInfo, } from '@memberjunction/core';
39
+ import { UUIDsEqual } from '@memberjunction/global';
40
+ import { UserCache } from './UserCache.js';
41
+ /**
42
+ * Why a proposed field-permission state would leave the MJ system user short of its entity-level
43
+ * access, or null when it would not.
44
+ *
45
+ * Callers pass the rules **as they would be after** their change — with the edited row's new
46
+ * values substituted, or the deleted row removed. Classifying the changed row on its own cannot
47
+ * answer this question; see the module comment.
48
+ *
49
+ * Answers null (permits the change) in three cases, each deliberate:
50
+ *
51
+ * - **No system user resolves.** A cold `UserCache` is the normal state during a process's own
52
+ * bootstrap. Blocking an administrator on missing state would be worse than the risk, and
53
+ * {@link FindSystemUserFieldAccessViolations} runs at startup precisely to catch whatever slips
54
+ * through here.
55
+ * - **The field is unrestrictable.** Primary keys, `__mj_` columns and the security-configuration
56
+ * entities are forced open by the aggregation regardless of any row, so no rule about them can
57
+ * restrict anyone.
58
+ * - **The system user has no entity-level read.** It is already denied one level up, where the
59
+ * entity permission check has no exemption either, so field rules change nothing for it here.
60
+ *
61
+ * Note this does NOT gate on `EnableFieldLevelSecurity`. Rules on a disabled entity are dormant
62
+ * rather than gone — disabling deliberately preserves them — so gating would leave a three-step
63
+ * hole: disable, strip the system user's rows, re-enable.
64
+ *
65
+ * @param entity the entity the field belongs to
66
+ * @param field the field whose rules are changing
67
+ * @param projectedRules every rule that would bind to this field after the change
68
+ */
69
+ export function SystemUserFieldAccessLossReason(entity, field, projectedRules) {
70
+ if (!entity || !field) {
71
+ return null;
72
+ }
73
+ const systemUser = resolveSystemUser();
74
+ if (!systemUser) {
75
+ return null;
76
+ }
77
+ if (field.IsUnrestrictableField || field.IsOnUnrestrictableEntity) {
78
+ return null;
79
+ }
80
+ const lost = lostVerb(entity, field, projectedRules, systemUser);
81
+ if (!lost) {
82
+ return null;
83
+ }
84
+ return (`This change would leave the MJ system user unable to ${lost} field '${field.Name}' on ` +
85
+ `'${entity.Name}'. The server runs background work as that account and shares one engine cache ` +
86
+ `across all users, so restricting it would let partially loaded records reach everyone. ` +
87
+ `Field security has no exempt user — the system user's access comes from these rows — so at ` +
88
+ `least one role it holds must keep 'Allow' here. Apply this rule to a different role, or ` +
89
+ `remove the role from the system user first.`);
90
+ }
91
+ /**
92
+ * Every field on an FLS-enabled entity that the system user cannot fully use.
93
+ *
94
+ * The backstop for state the save-time guard never saw: direct SQL, a migration, or a save made
95
+ * while the user cache was cold. Walks loaded metadata only — no queries — and skips entities with
96
+ * field security switched off, which is nearly all of them.
97
+ *
98
+ * @param provider the provider whose metadata to walk
99
+ * @param systemUser the account to check; defaults to the user cache's system user
100
+ * @param options optional projection — see {@link SystemUserFieldAccessSweepOptions}
101
+ */
102
+ export function FindSystemUserFieldAccessViolations(provider, systemUser, options = {}) {
103
+ const resolved = systemUser ?? resolveSystemUser();
104
+ if (!provider || !resolved) {
105
+ return [];
106
+ }
107
+ const user = options.WithoutRoleID ? projectUserWithoutRole(resolved, options.WithoutRoleID) : resolved;
108
+ const violations = [];
109
+ for (const entity of provider.Entities ?? []) {
110
+ if (!entity.EnableFieldLevelSecurity) {
111
+ continue; // dormant rules cannot restrict anything
112
+ }
113
+ for (const field of entity.Fields) {
114
+ if (field.IsUnrestrictableField || field.IsOnUnrestrictableEntity) {
115
+ continue;
116
+ }
117
+ const lost = lostVerb(entity, field, field.FieldPermissions, user);
118
+ if (lost) {
119
+ violations.push({ EntityName: entity.Name, FieldName: field.Name, Verb: lost });
120
+ }
121
+ }
122
+ }
123
+ return violations;
124
+ }
125
+ /**
126
+ * Which verb the system user would lose relative to its ENTITY-level access, or null.
127
+ *
128
+ * Compared against the entity-level ceiling rather than against "all three verbs" on purpose. The
129
+ * snapshot mirrors entity permissions, so a role holding only entity read legitimately produces
130
+ * `Allow / No Access / No Access`. Demanding update and create unconditionally would make that
131
+ * ordinary, correct state look like a violation and refuse the very rows the guard exists to
132
+ * protect.
133
+ */
134
+ function lostVerb(entity, field, rules, systemUser) {
135
+ const entityAccess = entity.GetUserPermisions(systemUser);
136
+ if (!entityAccess?.CanRead) {
137
+ return null; // already denied one level up; field rules add nothing
138
+ }
139
+ const after = EntityFieldInfo.AggregateFieldRulesForUser(rules, systemUser);
140
+ if (!after.CanRead) {
141
+ return 'read';
142
+ }
143
+ // A read-only field cannot be written through the API by ANY user — it is excluded from the
144
+ // generated create input and from the update SET list — so the write verbs decide nothing on
145
+ // it and their absence costs the system user nothing. Reconciliation deliberately authors
146
+ // `No Access` there rather than an inert `Allow`; without this the guard would read that as
147
+ // lost access and report every joined display column on an FLS-enabled entity as a violation.
148
+ if (field.ReadOnly) {
149
+ return null;
150
+ }
151
+ if (entityAccess.CanUpdate && !after.CanUpdate) {
152
+ return 'update';
153
+ }
154
+ if (entityAccess.CanCreate && !after.CanCreate) {
155
+ return 'create';
156
+ }
157
+ return null;
158
+ }
159
+ /**
160
+ * Whether the MJ system user holds the given role.
161
+ *
162
+ * The cheap pre-check for the save-time guard. Field aggregation only ever consults rules bound to
163
+ * a role the user holds, so a change to any other role's rule provably cannot move the system
164
+ * user's access — which lets callers skip the work of projecting and re-aggregating for the
165
+ * overwhelming majority of permission edits.
166
+ *
167
+ * False on a cold cache, matching {@link SystemUserFieldAccessLossReason}'s posture: with no system
168
+ * user to reason about there is nothing to protect, and the startup sweep is the backstop.
169
+ */
170
+ export function SystemUserHoldsRole(roleID) {
171
+ if (!roleID) {
172
+ return false;
173
+ }
174
+ const systemUser = resolveSystemUser();
175
+ return !!systemUser?.UserRoles?.some(ur => UUIDsEqual(ur.RoleID, roleID));
176
+ }
177
+ /**
178
+ * A copy of the user holding every role except `roleID`.
179
+ *
180
+ * `UserRoles` is a read-only accessor over a private backing field, so this rebuilds through the
181
+ * constructor rather than mutating. `copyInitData` skips accessors (it only assigns own
182
+ * properties), and the constructor then seeds the backing field from `initData.UserRoles` — so the
183
+ * filtered list is what the projection ends up holding.
184
+ */
185
+ function projectUserWithoutRole(user, roleID) {
186
+ // No provider: `UserInfo` only stores it for lookups this projection never performs, and the
187
+ // aggregation reads nothing but `UserRoles`.
188
+ return new UserInfo(undefined, {
189
+ ...user,
190
+ UserRoles: (user.UserRoles ?? []).filter(ur => !UUIDsEqual(ur.RoleID, roleID)),
191
+ });
192
+ }
193
+ /** The system user, or null when the cache is cold. Never throws. */
194
+ function resolveSystemUser() {
195
+ const user = UserCache.Instance?.GetSystemUser?.();
196
+ return user?.UserRoles?.length ? user : null;
197
+ }
198
+ //# sourceMappingURL=systemUserFieldAccess.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"systemUserFieldAccess.js","sourceRoot":"","sources":["../src/systemUserFieldAccess.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@memberjunction/generic-database-provider",
3
3
  "type": "module",
4
- "version": "6.1.0-edge.5",
4
+ "version": "6.1.0-edge.7",
5
5
  "description": "Shared database provider logic for MemberJunction — intermediate base class between DatabaseProviderBase and platform-specific providers",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -16,19 +16,19 @@
16
16
  "vitest": "^3.1.1"
17
17
  },
18
18
  "dependencies": {
19
- "@memberjunction/actions": "6.1.0-edge.5",
20
- "@memberjunction/actions-base": "6.1.0-edge.5",
21
- "@memberjunction/ai-vectors-memory": "6.1.0-edge.5",
22
- "@memberjunction/aiengine": "6.1.0-edge.5",
23
- "@memberjunction/core": "6.1.0-edge.5",
24
- "@memberjunction/core-entities": "6.1.0-edge.5",
25
- "@memberjunction/encryption": "6.1.0-edge.5",
26
- "@memberjunction/geo-core": "6.1.0-edge.5",
27
- "@memberjunction/global": "6.1.0-edge.5",
28
- "@memberjunction/query-processor": "6.1.0-edge.5",
29
- "@memberjunction/sql-dialect": "6.1.0-edge.5",
30
- "@memberjunction/sql-parser": "6.1.0-edge.5",
31
- "@memberjunction/queue": "6.1.0-edge.5",
19
+ "@memberjunction/actions": "6.1.0-edge.7",
20
+ "@memberjunction/actions-base": "6.1.0-edge.7",
21
+ "@memberjunction/ai-vectors-memory": "6.1.0-edge.7",
22
+ "@memberjunction/aiengine": "6.1.0-edge.7",
23
+ "@memberjunction/core": "6.1.0-edge.7",
24
+ "@memberjunction/core-entities": "6.1.0-edge.7",
25
+ "@memberjunction/encryption": "6.1.0-edge.7",
26
+ "@memberjunction/geo-core": "6.1.0-edge.7",
27
+ "@memberjunction/global": "6.1.0-edge.7",
28
+ "@memberjunction/query-processor": "6.1.0-edge.7",
29
+ "@memberjunction/sql-dialect": "6.1.0-edge.7",
30
+ "@memberjunction/sql-parser": "6.1.0-edge.7",
31
+ "@memberjunction/queue": "6.1.0-edge.7",
32
32
  "sql-formatter": "^15.7.0",
33
33
  "uuid": "^13.0.0"
34
34
  },