@memberjunction/core 6.1.0-edge.6 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generic/baseEngine.d.ts +52 -1
- package/dist/generic/baseEngine.d.ts.map +1 -1
- package/dist/generic/baseEngine.js +93 -4
- package/dist/generic/baseEngine.js.map +1 -1
- package/dist/generic/baseEntity.d.ts +173 -0
- package/dist/generic/baseEntity.d.ts.map +1 -1
- package/dist/generic/baseEntity.js +357 -12
- package/dist/generic/baseEntity.js.map +1 -1
- package/dist/generic/entityInfo.d.ts +461 -1
- package/dist/generic/entityInfo.d.ts.map +1 -1
- package/dist/generic/entityInfo.js +568 -6
- package/dist/generic/entityInfo.js.map +1 -1
- package/dist/generic/interfaces.d.ts +5 -0
- package/dist/generic/interfaces.d.ts.map +1 -1
- package/dist/generic/interfaces.js.map +1 -1
- package/dist/generic/localCacheManager.d.ts +30 -1
- package/dist/generic/localCacheManager.d.ts.map +1 -1
- package/dist/generic/localCacheManager.js +48 -1
- package/dist/generic/localCacheManager.js.map +1 -1
- package/dist/generic/providerBase.d.ts +164 -2
- package/dist/generic/providerBase.d.ts.map +1 -1
- package/dist/generic/providerBase.js +398 -23
- package/dist/generic/providerBase.js.map +1 -1
- package/dist/generic/recordChangeFieldSecurity.d.ts +164 -0
- package/dist/generic/recordChangeFieldSecurity.d.ts.map +1 -0
- package/dist/generic/recordChangeFieldSecurity.js +279 -0
- package/dist/generic/recordChangeFieldSecurity.js.map +1 -0
- package/dist/generic/saveEntityGraphOperation.d.ts +10 -0
- package/dist/generic/saveEntityGraphOperation.d.ts.map +1 -1
- package/dist/generic/saveEntityGraphOperation.js +2 -1
- package/dist/generic/saveEntityGraphOperation.js.map +1 -1
- package/dist/generic/wellKnownUserSource.d.ts +70 -0
- package/dist/generic/wellKnownUserSource.d.ts.map +1 -0
- package/dist/generic/wellKnownUserSource.js +82 -0
- package/dist/generic/wellKnownUserSource.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { EntityInfo } from "./entityInfo.js";
|
|
2
|
+
import { UserInfo } from "./securityInfo.js";
|
|
3
|
+
/**
|
|
4
|
+
* The two metadata lookups this projector needs, and nothing else.
|
|
5
|
+
*
|
|
6
|
+
* Narrower than `IMetadataProvider` on purpose: both `ProviderBase` (which passes `this`) and
|
|
7
|
+
* `Metadata` (which the GraphQL resolvers fall back to) satisfy this, while only the first
|
|
8
|
+
* satisfies the full provider interface. Asking for exactly what is used keeps the resolver half
|
|
9
|
+
* from having to invent a provider it does not have.
|
|
10
|
+
*/
|
|
11
|
+
export interface RecordChangeMetadataSource {
|
|
12
|
+
readonly Entities: EntityInfo[];
|
|
13
|
+
EntityByID(entityID: string): EntityInfo | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The audit entity whose rows carry OTHER entities' field values. Matched by name rather than by
|
|
17
|
+
* ID because every other enforcement point in this feature keys off the entity name too, and a
|
|
18
|
+
* name is the thing a reader can check against metadata without a lookup.
|
|
19
|
+
*/
|
|
20
|
+
export declare const RecordChangesEntityName = "MJ: Record Changes";
|
|
21
|
+
/**
|
|
22
|
+
* The Record Change column naming the entity a row is ABOUT. Everything in this module hangs off
|
|
23
|
+
* it: it is what makes the denied set per-ROW rather than per-result.
|
|
24
|
+
*/
|
|
25
|
+
export declare const RecordChangeEntityIDField = "EntityID";
|
|
26
|
+
/**
|
|
27
|
+
* Payload columns holding a JSON object keyed by the target entity's field names — projected
|
|
28
|
+
* key by key against the denied set. `ChangesJSON` is `{ [field]: { field, oldValue, newValue } }`;
|
|
29
|
+
* `FullRecordJSON` is a flat whole-row snapshot. Both are top-level-keyed by field name, which is
|
|
30
|
+
* what lets one projection handle them.
|
|
31
|
+
*/
|
|
32
|
+
export declare const RecordChangeJSONPayloadFields: readonly ["ChangesJSON", "FullRecordJSON"];
|
|
33
|
+
/**
|
|
34
|
+
* The human-prose payload column — withheld outright, never redacted. See
|
|
35
|
+
* {@link RecordChangeFieldSecurityProjector}.
|
|
36
|
+
*/
|
|
37
|
+
export declare const RecordChangeProsePayloadField = "ChangesDescription";
|
|
38
|
+
/**
|
|
39
|
+
* Every payload column this module touches, for callers that need to act on the set as a whole —
|
|
40
|
+
* notably the write guard, which must refuse a narrowed value coming back in.
|
|
41
|
+
*/
|
|
42
|
+
export declare const RecordChangePayloadFields: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Outcome of projecting one JSON payload column: either a value to write back, or a decision to
|
|
45
|
+
* withhold the column entirely because the payload could not be inspected.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately an interface with an optional `Value` rather than a discriminated union — MJCore
|
|
48
|
+
* compiles without `strictNullChecks`, under which TypeScript does not narrow a union on a
|
|
49
|
+
* boolean discriminant, so the union shape would force a cast at every read.
|
|
50
|
+
*/
|
|
51
|
+
export interface RecordChangePayloadProjection {
|
|
52
|
+
/** When true the caller drops the column; `Value` carries nothing. */
|
|
53
|
+
Withhold: boolean;
|
|
54
|
+
/** The value to write back. Meaningful only when {@link Withhold} is false. */
|
|
55
|
+
Value?: unknown;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Narrows one Record Change JSON payload to the fields the caller may read.
|
|
59
|
+
*
|
|
60
|
+
* **Withholds rather than guesses.** A payload that is not a string, or is a string that does not
|
|
61
|
+
* parse, or parses to something other than a plain object, is a payload this cannot prove is
|
|
62
|
+
* clean — so the column is dropped instead of passed through. A null/undefined/empty value is
|
|
63
|
+
* kept as-is: there is nothing stored to leak, and `ChangesJSON` is legitimately `''` on rows
|
|
64
|
+
* whose diff produced nothing.
|
|
65
|
+
*
|
|
66
|
+
* @param raw the column's stored value
|
|
67
|
+
* @param deniedLowercase field names the caller may not read, already lowercased
|
|
68
|
+
*/
|
|
69
|
+
export declare function ProjectRecordChangePayloadJSON(raw: unknown, deniedLowercase: Set<string>): RecordChangePayloadProjection;
|
|
70
|
+
/**
|
|
71
|
+
* Field-level security for the payload columns of `MJ: Record Changes`.
|
|
72
|
+
*
|
|
73
|
+
* ## Why this is not `ProviderBase.ApplyFieldSecurityProjection`
|
|
74
|
+
*
|
|
75
|
+
* Every other FLS enforcement point computes the denied set against **the entity being read**. A
|
|
76
|
+
* Record Change row is about a DIFFERENT entity — the one named by its own `EntityID` column —
|
|
77
|
+
* and `MJ: Record Changes` itself has field security switched off in every default deployment.
|
|
78
|
+
* The main projection therefore short-circuits on `EnableFieldLevelSecurity` and returns the row
|
|
79
|
+
* untouched, with a denied field's old and new values sitting in `ChangesJSON` in plain text.
|
|
80
|
+
* That is the leak this closes: a user with entity-level read on the audit trail could read a
|
|
81
|
+
* salary they were denied on `MJ: Employees` straight out of it, in the default configuration.
|
|
82
|
+
*
|
|
83
|
+
* ## What happens to each column
|
|
84
|
+
*
|
|
85
|
+
* | Column | Treatment |
|
|
86
|
+
* |---|---|
|
|
87
|
+
* | `ChangesJSON` | projected — denied field keys dropped |
|
|
88
|
+
* | `FullRecordJSON` | projected — same, it is a whole-row snapshot |
|
|
89
|
+
* | `ChangesDescription` | **withheld entirely** whenever the caller is denied anything on the target entity |
|
|
90
|
+
*
|
|
91
|
+
* `ChangesDescription` is human prose ("Salary changed from 100000 to 120000"). Redacting prose
|
|
92
|
+
* on the fly leaks on the first value that appears in an unexpected form, and regenerating it
|
|
93
|
+
* from the filtered JSON is a larger surface than this is worth. Withholding the whole column is
|
|
94
|
+
* coarse and it is safe, which is the right trade for a field whose only job is to be readable
|
|
95
|
+
* text — and callers already degrade, the record-changes UI falling back to 'Changes made'.
|
|
96
|
+
*
|
|
97
|
+
* ## Rows in one result span different entities
|
|
98
|
+
*
|
|
99
|
+
* `EntityID` varies row to row, and `EntityInfo.GetDeniedReadFields` walks every field on the
|
|
100
|
+
* entity — its documented performance contract is that it must not be called in a row loop. Every
|
|
101
|
+
* lookup here is memoized on the raw `EntityID` string for the projector's lifetime, which is one
|
|
102
|
+
* request. Construct one projector per request, never one per row.
|
|
103
|
+
*/
|
|
104
|
+
export declare class RecordChangeFieldSecurityProjector {
|
|
105
|
+
private readonly provider;
|
|
106
|
+
private readonly user;
|
|
107
|
+
private readonly deniedByEntityID;
|
|
108
|
+
private carriesDenials;
|
|
109
|
+
private payloadKeys;
|
|
110
|
+
constructor(provider: RecordChangeMetadataSource, user: UserInfo);
|
|
111
|
+
/**
|
|
112
|
+
* Whether an entity is the audit trail this projector guards. Callers gate on this before
|
|
113
|
+
* constructing a projector, so nothing is paid on the other 99% of reads.
|
|
114
|
+
*/
|
|
115
|
+
static IsRecordChangesEntity(entity: EntityInfo | null | undefined): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Projects a whole result set. Returns the ORIGINAL array when nothing needed changing, so an
|
|
118
|
+
* unrestricted caller's rows are never rebuilt — and, on the cache-hit paths, so the cache's
|
|
119
|
+
* own frozen objects are handed back by reference exactly as they are today.
|
|
120
|
+
*/
|
|
121
|
+
ProjectRows<T>(rows: T[]): T[];
|
|
122
|
+
/**
|
|
123
|
+
* Single-row form, for the GraphQL read boundary. Returns the original object untouched when
|
|
124
|
+
* there is nothing to project.
|
|
125
|
+
*/
|
|
126
|
+
ProjectRow<T>(row: T): T;
|
|
127
|
+
/**
|
|
128
|
+
* Whether this caller is denied ANY field on ANY field-security-enabled entity.
|
|
129
|
+
*
|
|
130
|
+
* The whole projector is a no-op for everyone else, and this gate is what makes that true —
|
|
131
|
+
* including for the fail-closed branch in {@link deniedFieldsFor}, which would otherwise blank
|
|
132
|
+
* audit rows on deployments that never enabled field security at all. Cost is one pass over
|
|
133
|
+
* the entity list plus one denied-set walk per ENABLED entity, and the flag is off on every
|
|
134
|
+
* entity an administrator has not explicitly opted in.
|
|
135
|
+
*
|
|
136
|
+
* Public because the WRITE side needs the same question: a caller who could have been served a
|
|
137
|
+
* narrowed payload must not be allowed to write one back. See
|
|
138
|
+
* `ResolverBase.StripRecordChangePayloadFromClientInput`.
|
|
139
|
+
*/
|
|
140
|
+
CallerCarriesAnyDenial(): boolean;
|
|
141
|
+
private callerCarriesDenials;
|
|
142
|
+
private projectOne;
|
|
143
|
+
/**
|
|
144
|
+
* The denied-read set for the entity a Record Change row is ABOUT, or `null` to fail closed.
|
|
145
|
+
*
|
|
146
|
+
* **`null` drops all three payload columns.** Two shapes reach it and both have to. The first
|
|
147
|
+
* is an `EntityID` that no longer resolves to a known entity. The second is the one that
|
|
148
|
+
* decides the policy: a row that does not carry `EntityID` at all. A caller can ask for
|
|
149
|
+
* `Fields: ['ChangesJSON']` and nothing else, and field narrowing runs BEFORE this projection
|
|
150
|
+
* — so failing open on a missing `EntityID` would be a one-parameter bypass of the entire
|
|
151
|
+
* control.
|
|
152
|
+
*
|
|
153
|
+
* The cost of failing closed is paid only by callers who already carry denials somewhere (see
|
|
154
|
+
* {@link callerCarriesDenials}), and lands on audit rows for entities that are gone from
|
|
155
|
+
* metadata — rows for which no caller can render an entity name either way.
|
|
156
|
+
*/
|
|
157
|
+
private deniedFieldsFor;
|
|
158
|
+
/**
|
|
159
|
+
* Resolves the row's actual key casing once. Returns null — cached as null — when the rows
|
|
160
|
+
* carry no payload column at all, which makes every later row a single map read.
|
|
161
|
+
*/
|
|
162
|
+
private resolvePayloadKeys;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=recordChangeFieldSecurity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordChangeFieldSecurity.d.ts","sourceRoot":"","sources":["../../src/generic/recordChangeFieldSecurity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE1C;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACvC,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;IAChC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;CACxD;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,yBAAyB,aAAa,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,4CAA6C,CAAC;AAExF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,uBAAuB,CAAC;AAElE;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,SAAS,MAAM,EAGtD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,6BAA6B;IAC1C,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,6BAA6B,CAiCxH;AA8BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,kCAAkC;IAMvC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IANzB,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAyC;IAC1E,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,WAAW,CAAyD;gBAGvD,QAAQ,EAAE,0BAA0B,EACpC,IAAI,EAAE,QAAQ;IAGnC;;;OAGG;WACW,qBAAqB,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO;IAInF;;;;OAIG;IACI,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE;IAerC;;;OAGG;IACI,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC;IAO/B;;;;;;;;;;;;OAYG;IACI,sBAAsB,IAAI,OAAO;IAIxC,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,UAAU;IAiClB;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,eAAe;IAkBvB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;CAsB7B"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The audit entity whose rows carry OTHER entities' field values. Matched by name rather than by
|
|
3
|
+
* ID because every other enforcement point in this feature keys off the entity name too, and a
|
|
4
|
+
* name is the thing a reader can check against metadata without a lookup.
|
|
5
|
+
*/
|
|
6
|
+
export const RecordChangesEntityName = 'MJ: Record Changes';
|
|
7
|
+
/**
|
|
8
|
+
* The Record Change column naming the entity a row is ABOUT. Everything in this module hangs off
|
|
9
|
+
* it: it is what makes the denied set per-ROW rather than per-result.
|
|
10
|
+
*/
|
|
11
|
+
export const RecordChangeEntityIDField = 'EntityID';
|
|
12
|
+
/**
|
|
13
|
+
* Payload columns holding a JSON object keyed by the target entity's field names — projected
|
|
14
|
+
* key by key against the denied set. `ChangesJSON` is `{ [field]: { field, oldValue, newValue } }`;
|
|
15
|
+
* `FullRecordJSON` is a flat whole-row snapshot. Both are top-level-keyed by field name, which is
|
|
16
|
+
* what lets one projection handle them.
|
|
17
|
+
*/
|
|
18
|
+
export const RecordChangeJSONPayloadFields = ['ChangesJSON', 'FullRecordJSON'];
|
|
19
|
+
/**
|
|
20
|
+
* The human-prose payload column — withheld outright, never redacted. See
|
|
21
|
+
* {@link RecordChangeFieldSecurityProjector}.
|
|
22
|
+
*/
|
|
23
|
+
export const RecordChangeProsePayloadField = 'ChangesDescription';
|
|
24
|
+
/**
|
|
25
|
+
* Every payload column this module touches, for callers that need to act on the set as a whole —
|
|
26
|
+
* notably the write guard, which must refuse a narrowed value coming back in.
|
|
27
|
+
*/
|
|
28
|
+
export const RecordChangePayloadFields = [
|
|
29
|
+
...RecordChangeJSONPayloadFields,
|
|
30
|
+
RecordChangeProsePayloadField,
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Narrows one Record Change JSON payload to the fields the caller may read.
|
|
34
|
+
*
|
|
35
|
+
* **Withholds rather than guesses.** A payload that is not a string, or is a string that does not
|
|
36
|
+
* parse, or parses to something other than a plain object, is a payload this cannot prove is
|
|
37
|
+
* clean — so the column is dropped instead of passed through. A null/undefined/empty value is
|
|
38
|
+
* kept as-is: there is nothing stored to leak, and `ChangesJSON` is legitimately `''` on rows
|
|
39
|
+
* whose diff produced nothing.
|
|
40
|
+
*
|
|
41
|
+
* @param raw the column's stored value
|
|
42
|
+
* @param deniedLowercase field names the caller may not read, already lowercased
|
|
43
|
+
*/
|
|
44
|
+
export function ProjectRecordChangePayloadJSON(raw, deniedLowercase) {
|
|
45
|
+
if (raw === null || raw === undefined) {
|
|
46
|
+
return { Withhold: false, Value: raw };
|
|
47
|
+
}
|
|
48
|
+
if (typeof raw !== 'string') {
|
|
49
|
+
return { Withhold: true };
|
|
50
|
+
}
|
|
51
|
+
if (raw.trim().length === 0) {
|
|
52
|
+
return { Withhold: false, Value: raw };
|
|
53
|
+
}
|
|
54
|
+
let parsed;
|
|
55
|
+
try {
|
|
56
|
+
parsed = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { Withhold: true };
|
|
60
|
+
}
|
|
61
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
62
|
+
return { Withhold: true };
|
|
63
|
+
}
|
|
64
|
+
const source = parsed;
|
|
65
|
+
const kept = {};
|
|
66
|
+
for (const key of Object.keys(source)) {
|
|
67
|
+
if (deniedLowercase.has(key.trim().toLowerCase())) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (isDeniedChangeEntry(source[key], deniedLowercase)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
kept[key] = source[key];
|
|
74
|
+
}
|
|
75
|
+
return { Withhold: false, Value: JSON.stringify(kept) };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A `ChangesJSON` entry is `{ field, oldValue, newValue }` stored under a key that is that same
|
|
79
|
+
* field name, so the key check already catches it. This second check costs one property read and
|
|
80
|
+
* covers the case where the two disagree — a payload written by something other than
|
|
81
|
+
* `DatabaseProviderBase.DiffObjects`, or hand-edited. Where they disagree, the entry is dropped.
|
|
82
|
+
*/
|
|
83
|
+
function isDeniedChangeEntry(value, deniedLowercase) {
|
|
84
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const field = value['field'];
|
|
88
|
+
return typeof field === 'string' && deniedLowercase.has(field.trim().toLowerCase());
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Field-level security for the payload columns of `MJ: Record Changes`.
|
|
92
|
+
*
|
|
93
|
+
* ## Why this is not `ProviderBase.ApplyFieldSecurityProjection`
|
|
94
|
+
*
|
|
95
|
+
* Every other FLS enforcement point computes the denied set against **the entity being read**. A
|
|
96
|
+
* Record Change row is about a DIFFERENT entity — the one named by its own `EntityID` column —
|
|
97
|
+
* and `MJ: Record Changes` itself has field security switched off in every default deployment.
|
|
98
|
+
* The main projection therefore short-circuits on `EnableFieldLevelSecurity` and returns the row
|
|
99
|
+
* untouched, with a denied field's old and new values sitting in `ChangesJSON` in plain text.
|
|
100
|
+
* That is the leak this closes: a user with entity-level read on the audit trail could read a
|
|
101
|
+
* salary they were denied on `MJ: Employees` straight out of it, in the default configuration.
|
|
102
|
+
*
|
|
103
|
+
* ## What happens to each column
|
|
104
|
+
*
|
|
105
|
+
* | Column | Treatment |
|
|
106
|
+
* |---|---|
|
|
107
|
+
* | `ChangesJSON` | projected — denied field keys dropped |
|
|
108
|
+
* | `FullRecordJSON` | projected — same, it is a whole-row snapshot |
|
|
109
|
+
* | `ChangesDescription` | **withheld entirely** whenever the caller is denied anything on the target entity |
|
|
110
|
+
*
|
|
111
|
+
* `ChangesDescription` is human prose ("Salary changed from 100000 to 120000"). Redacting prose
|
|
112
|
+
* on the fly leaks on the first value that appears in an unexpected form, and regenerating it
|
|
113
|
+
* from the filtered JSON is a larger surface than this is worth. Withholding the whole column is
|
|
114
|
+
* coarse and it is safe, which is the right trade for a field whose only job is to be readable
|
|
115
|
+
* text — and callers already degrade, the record-changes UI falling back to 'Changes made'.
|
|
116
|
+
*
|
|
117
|
+
* ## Rows in one result span different entities
|
|
118
|
+
*
|
|
119
|
+
* `EntityID` varies row to row, and `EntityInfo.GetDeniedReadFields` walks every field on the
|
|
120
|
+
* entity — its documented performance contract is that it must not be called in a row loop. Every
|
|
121
|
+
* lookup here is memoized on the raw `EntityID` string for the projector's lifetime, which is one
|
|
122
|
+
* request. Construct one projector per request, never one per row.
|
|
123
|
+
*/
|
|
124
|
+
export class RecordChangeFieldSecurityProjector {
|
|
125
|
+
constructor(provider, user) {
|
|
126
|
+
this.provider = provider;
|
|
127
|
+
this.user = user;
|
|
128
|
+
this.deniedByEntityID = new Map();
|
|
129
|
+
this.carriesDenials = null;
|
|
130
|
+
this.payloadKeys = undefined;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Whether an entity is the audit trail this projector guards. Callers gate on this before
|
|
134
|
+
* constructing a projector, so nothing is paid on the other 99% of reads.
|
|
135
|
+
*/
|
|
136
|
+
static IsRecordChangesEntity(entity) {
|
|
137
|
+
return entity?.Name?.trim().toLowerCase() === RecordChangesEntityName.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Projects a whole result set. Returns the ORIGINAL array when nothing needed changing, so an
|
|
141
|
+
* unrestricted caller's rows are never rebuilt — and, on the cache-hit paths, so the cache's
|
|
142
|
+
* own frozen objects are handed back by reference exactly as they are today.
|
|
143
|
+
*/
|
|
144
|
+
ProjectRows(rows) {
|
|
145
|
+
if (!rows?.length || !this.callerCarriesDenials()) {
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
148
|
+
let mutated = false;
|
|
149
|
+
const projected = rows.map((row) => {
|
|
150
|
+
const next = this.projectOne(row);
|
|
151
|
+
if (next !== row) {
|
|
152
|
+
mutated = true;
|
|
153
|
+
}
|
|
154
|
+
return next;
|
|
155
|
+
});
|
|
156
|
+
return mutated ? projected : rows;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Single-row form, for the GraphQL read boundary. Returns the original object untouched when
|
|
160
|
+
* there is nothing to project.
|
|
161
|
+
*/
|
|
162
|
+
ProjectRow(row) {
|
|
163
|
+
if (!row || !this.callerCarriesDenials()) {
|
|
164
|
+
return row;
|
|
165
|
+
}
|
|
166
|
+
return this.projectOne(row);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Whether this caller is denied ANY field on ANY field-security-enabled entity.
|
|
170
|
+
*
|
|
171
|
+
* The whole projector is a no-op for everyone else, and this gate is what makes that true —
|
|
172
|
+
* including for the fail-closed branch in {@link deniedFieldsFor}, which would otherwise blank
|
|
173
|
+
* audit rows on deployments that never enabled field security at all. Cost is one pass over
|
|
174
|
+
* the entity list plus one denied-set walk per ENABLED entity, and the flag is off on every
|
|
175
|
+
* entity an administrator has not explicitly opted in.
|
|
176
|
+
*
|
|
177
|
+
* Public because the WRITE side needs the same question: a caller who could have been served a
|
|
178
|
+
* narrowed payload must not be allowed to write one back. See
|
|
179
|
+
* `ResolverBase.StripRecordChangePayloadFromClientInput`.
|
|
180
|
+
*/
|
|
181
|
+
CallerCarriesAnyDenial() {
|
|
182
|
+
return this.callerCarriesDenials();
|
|
183
|
+
}
|
|
184
|
+
callerCarriesDenials() {
|
|
185
|
+
if (this.carriesDenials === null) {
|
|
186
|
+
this.carriesDenials = this.provider.Entities.some((e) => e.EnableFieldLevelSecurity && e.GetDeniedReadFields(this.user).size > 0);
|
|
187
|
+
}
|
|
188
|
+
return this.carriesDenials;
|
|
189
|
+
}
|
|
190
|
+
projectOne(row) {
|
|
191
|
+
const source = row;
|
|
192
|
+
if (!source || typeof source !== 'object') {
|
|
193
|
+
return row;
|
|
194
|
+
}
|
|
195
|
+
const keys = this.resolvePayloadKeys(source);
|
|
196
|
+
if (!keys) {
|
|
197
|
+
return row; // the caller selected none of the payload columns — nothing to protect
|
|
198
|
+
}
|
|
199
|
+
const denied = this.deniedFieldsFor(source, keys);
|
|
200
|
+
if (denied !== null && denied.size === 0) {
|
|
201
|
+
return row; // caller reads every field on the entity this row is about
|
|
202
|
+
}
|
|
203
|
+
const projected = { ...source };
|
|
204
|
+
if (keys.Prose) {
|
|
205
|
+
delete projected[keys.Prose];
|
|
206
|
+
}
|
|
207
|
+
for (const key of keys.JSON) {
|
|
208
|
+
if (denied === null) {
|
|
209
|
+
delete projected[key];
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const result = ProjectRecordChangePayloadJSON(source[key], denied);
|
|
213
|
+
if (result.Withhold) {
|
|
214
|
+
delete projected[key];
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
projected[key] = result.Value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return projected;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The denied-read set for the entity a Record Change row is ABOUT, or `null` to fail closed.
|
|
224
|
+
*
|
|
225
|
+
* **`null` drops all three payload columns.** Two shapes reach it and both have to. The first
|
|
226
|
+
* is an `EntityID` that no longer resolves to a known entity. The second is the one that
|
|
227
|
+
* decides the policy: a row that does not carry `EntityID` at all. A caller can ask for
|
|
228
|
+
* `Fields: ['ChangesJSON']` and nothing else, and field narrowing runs BEFORE this projection
|
|
229
|
+
* — so failing open on a missing `EntityID` would be a one-parameter bypass of the entire
|
|
230
|
+
* control.
|
|
231
|
+
*
|
|
232
|
+
* The cost of failing closed is paid only by callers who already carry denials somewhere (see
|
|
233
|
+
* {@link callerCarriesDenials}), and lands on audit rows for entities that are gone from
|
|
234
|
+
* metadata — rows for which no caller can render an entity name either way.
|
|
235
|
+
*/
|
|
236
|
+
deniedFieldsFor(source, keys) {
|
|
237
|
+
if (!keys.EntityID) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const rawEntityID = source[keys.EntityID];
|
|
241
|
+
if (typeof rawEntityID !== 'string' || rawEntityID.trim().length === 0) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const cached = this.deniedByEntityID.get(rawEntityID);
|
|
245
|
+
if (cached !== undefined) {
|
|
246
|
+
return cached;
|
|
247
|
+
}
|
|
248
|
+
const entity = this.provider.EntityByID(rawEntityID);
|
|
249
|
+
const denied = entity ? entity.GetDeniedReadFields(this.user) : null;
|
|
250
|
+
this.deniedByEntityID.set(rawEntityID, denied);
|
|
251
|
+
return denied;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Resolves the row's actual key casing once. Returns null — cached as null — when the rows
|
|
255
|
+
* carry no payload column at all, which makes every later row a single map read.
|
|
256
|
+
*/
|
|
257
|
+
resolvePayloadKeys(source) {
|
|
258
|
+
if (this.payloadKeys !== undefined) {
|
|
259
|
+
return this.payloadKeys;
|
|
260
|
+
}
|
|
261
|
+
const byLowerName = new Map();
|
|
262
|
+
for (const key of Object.keys(source)) {
|
|
263
|
+
byLowerName.set(key.trim().toLowerCase(), key);
|
|
264
|
+
}
|
|
265
|
+
const jsonKeys = RecordChangeJSONPayloadFields
|
|
266
|
+
.map((name) => byLowerName.get(name.toLowerCase()))
|
|
267
|
+
.filter((key) => key !== undefined);
|
|
268
|
+
const proseKey = byLowerName.get(RecordChangeProsePayloadField.toLowerCase()) ?? null;
|
|
269
|
+
this.payloadKeys = (jsonKeys.length === 0 && proseKey === null)
|
|
270
|
+
? null
|
|
271
|
+
: {
|
|
272
|
+
JSON: jsonKeys,
|
|
273
|
+
Prose: proseKey,
|
|
274
|
+
EntityID: byLowerName.get(RecordChangeEntityIDField.toLowerCase()) ?? null,
|
|
275
|
+
};
|
|
276
|
+
return this.payloadKeys;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=recordChangeFieldSecurity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recordChangeFieldSecurity.js","sourceRoot":"","sources":["../../src/generic/recordChangeFieldSecurity.ts"],"names":[],"mappings":"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"}
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
*
|
|
42
42
|
* @module @memberjunction/core
|
|
43
43
|
*/
|
|
44
|
+
import { type SerializedValidationError } from '@memberjunction/global';
|
|
44
45
|
import { BaseRemotableOperation } from './baseRemotableOperation.js';
|
|
45
46
|
import type { RemoteOpServerContext } from './baseRemotableOperation.js';
|
|
46
47
|
import type { EntityCompanionPayload } from './entityCompanion.js';
|
|
@@ -72,6 +73,15 @@ export type SaveEntityGraphOutput = {
|
|
|
72
73
|
Success: boolean;
|
|
73
74
|
/** Failure detail when `Success` is false. */
|
|
74
75
|
ErrorMessage?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The refusal's structured errors when `Success` is false and validation is what refused —
|
|
78
|
+
* `root.LatestResult.Errors` flattened by `SerializeValidationErrors`, one entry per offending
|
|
79
|
+
* field. `ErrorMessage` is the same refusal as prose; this is the version a form can paint from.
|
|
80
|
+
* A graph save reaches the client through the remote-operation transport, not `ResolverBase`, so
|
|
81
|
+
* without this field a `ValidateAsync()` refusal on an RRC / graph-saved entity would toast and
|
|
82
|
+
* never highlight — the parity the plain-save path gets from `extensions.validationErrors`.
|
|
83
|
+
*/
|
|
84
|
+
ValidationErrors?: SerializedValidationError[];
|
|
75
85
|
/**
|
|
76
86
|
* The root record's field values **after** the save — server-assigned primary keys, computed
|
|
77
87
|
* columns, sequence numbers, trigger-populated fields.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"saveEntityGraphOperation.d.ts","sourceRoot":"","sources":["../../src/generic/saveEntityGraphOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;
|
|
1
|
+
{"version":3,"file":"saveEntityGraphOperation.d.ts","sourceRoot":"","sources":["../../src/generic/saveEntityGraphOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH,OAAO,EAA4C,KAAK,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AAClH,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAGtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,iEAAiE;AACjE,eAAO,MAAM,+BAA+B,uBAAuB,CAAC;AAEpE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG;IAC/B,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,mEAAmE;IACnE,UAAU,EAAE,sBAAsB,EAAE,CAAC;IACrC;;;OAGG;IACH,gBAAgB,EAAE,OAAO,CAAC;CAC7B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAChC,2CAA2C;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,8CAA8C;IAC9C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,yBAAyB,EAAE,CAAC;IAC/C;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC;;;;;;;OAOG;IACH,UAAU,EAAE,sBAAsB,EAAE,CAAC;CACxC,CAAC;AAEF;;;;;;GAMG;AACH,qBACa,wBAAyB,SAAQ,sBAAsB,CAAC,oBAAoB,EAAE,qBAAqB,CAAC;IAC7G,kBAAkB;IAClB,SAAgB,YAAY,wBAAmC;IAE/D;;;;;OAKG;IACH,SAAyB,aAAa,mBAAmB;IAEzD;;;;;;;;;;;;;;;OAeG;cACsB,SAAS,CAAC,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAIlG;;;;;;;;OAQG;cACsB,eAAe,CACpC,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,iBAAiB,EAC3B,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,qBAAqB,GAChC,OAAO,CAAC,qBAAqB,CAAC;IAuBjC;;;;;;;;;;;OAWG;YACW,WAAW;CA0B5B"}
|
|
@@ -47,7 +47,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
47
47
|
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;
|
|
48
48
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
49
49
|
};
|
|
50
|
-
import { RegisterClass } from '@memberjunction/global';
|
|
50
|
+
import { RegisterClass, SerializeValidationErrors } from '@memberjunction/global';
|
|
51
51
|
import { BaseRemotableOperation } from './baseRemotableOperation.js';
|
|
52
52
|
import { CompositeKey, KeyValuePair } from './compositeKey.js';
|
|
53
53
|
import { LogError } from './logging.js';
|
|
@@ -110,6 +110,7 @@ let SaveEntityGraphOperation = class SaveEntityGraphOperation extends BaseRemota
|
|
|
110
110
|
return {
|
|
111
111
|
Success: false,
|
|
112
112
|
ErrorMessage: detail,
|
|
113
|
+
ValidationErrors: SerializeValidationErrors(root.LatestResult?.Errors),
|
|
113
114
|
Fields: root.GetAll(),
|
|
114
115
|
Companions: await root.SerializeCompanions(),
|
|
115
116
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"saveEntityGraphOperation.js","sourceRoot":"","sources":["../../src/generic/saveEntityGraphOperation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;;;;;;;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"saveEntityGraphOperation.js","sourceRoot":"","sources":["../../src/generic/saveEntityGraphOperation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { IMetadataProvider } from "./interfaces.js";
|
|
2
|
+
import type { UserInfo } from "./securityInfo.js";
|
|
3
|
+
/**
|
|
4
|
+
* Supplies the platform's **well-known users** — the built-in, non-human accounts MJ itself
|
|
5
|
+
* acts as — and answers whether a given user is one of them.
|
|
6
|
+
*
|
|
7
|
+
* This is a plug-in point, not a service. The base implementation says "there are none": it
|
|
8
|
+
* returns null and false for everything. Server-side packages register a subclass with
|
|
9
|
+
* `@RegisterClassEx(WellKnownUserSource, …)` that answers properly, and consumers resolve it
|
|
10
|
+
* through the MJGlobal ClassFactory — so a process with no server-side implementation loaded
|
|
11
|
+
* (a browser, most obviously) transparently gets the base answers, which are correct there.
|
|
12
|
+
*
|
|
13
|
+
* **Why this exists as its own seam.** MJCore is shared by browsers and servers, and which
|
|
14
|
+
* account the platform runs its own work as is a server concern. Keeping the identity — and
|
|
15
|
+
* the account's ID — out of MJCore means a browser bundle carries no knowledge of the server's
|
|
16
|
+
* service account, and the answer travels with whichever data-provider package the process
|
|
17
|
+
* actually loaded rather than with a constant compiled into core.
|
|
18
|
+
*
|
|
19
|
+
* **Why it is NOT on `IMetadataProvider`.** A metadata provider's job is data access; platform
|
|
20
|
+
* identity is policy. Keeping it separate also keeps `CurrentUser` meaning what it means today
|
|
21
|
+
* — permanently null on the server — so the many `contextUser ?? provider.CurrentUser`
|
|
22
|
+
* fallbacks keep failing loudly instead of silently escalating to the most privileged account.
|
|
23
|
+
*
|
|
24
|
+
* Today the category has one member, the system user. It has visible future members — the
|
|
25
|
+
* active-Owner fallback that startup and the telephony routers hand-roll, and the
|
|
26
|
+
* scoped-anonymous identity used for widget-guest elevation — and those belong here as their
|
|
27
|
+
* own named methods rather than as variations smuggled into the existing ones.
|
|
28
|
+
*/
|
|
29
|
+
export declare class WellKnownUserSource {
|
|
30
|
+
private static _instance;
|
|
31
|
+
/**
|
|
32
|
+
* The registered source for this process, resolved once through the class factory.
|
|
33
|
+
*
|
|
34
|
+
* Cached because {@link IsSystemUser} sits behind permission checks that run per entity and
|
|
35
|
+
* per user; resolving (and allocating) on every call would be needless churn. Registrations
|
|
36
|
+
* happen as module-load side effects of importing a data-provider package, which always
|
|
37
|
+
* precedes any permission evaluation, so caching cannot capture a stale answer in practice.
|
|
38
|
+
* Tests that register a source after first use should call {@link ResetInstance}.
|
|
39
|
+
*/
|
|
40
|
+
static get Instance(): WellKnownUserSource;
|
|
41
|
+
/**
|
|
42
|
+
* Drops the cached {@link Instance} so the next access re-resolves through the class
|
|
43
|
+
* factory. Intended for tests that register a source after the cache was populated.
|
|
44
|
+
*/
|
|
45
|
+
static ResetInstance(): void;
|
|
46
|
+
/**
|
|
47
|
+
* True when `user` is the MJ system user — the server's own service account, not a person.
|
|
48
|
+
*
|
|
49
|
+
* Synchronous on purpose: this is consumed by permission checks that cannot await. The base
|
|
50
|
+
* returns false, which is the right answer anywhere no server-side source is registered —
|
|
51
|
+
* a browser has no system account, so no user it holds can be one.
|
|
52
|
+
*
|
|
53
|
+
* Implementations must be null/undefined-safe so callers can pass an optional user directly.
|
|
54
|
+
*/
|
|
55
|
+
IsSystemUser(user: UserInfo | null | undefined): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* The MJ system user for the given provider's connection, with `UserRoles` populated, or
|
|
58
|
+
* null when this process has no such identity to offer.
|
|
59
|
+
*
|
|
60
|
+
* Implementations MUST NOT throw — a missing user row, an unconfigured provider, or a
|
|
61
|
+
* failed lookup all resolve to null so callers can degrade rather than crash. Callers MUST
|
|
62
|
+
* treat null as "no elevated identity available" and fall back to their existing behavior.
|
|
63
|
+
*
|
|
64
|
+
* @param provider The provider whose connection the user is being resolved for. Passed
|
|
65
|
+
* explicitly (rather than read from a global) so a process holding several connections
|
|
66
|
+
* resolves the account belonging to the one actually in use.
|
|
67
|
+
*/
|
|
68
|
+
GetSystemUser(provider: IMetadataProvider): Promise<UserInfo | null>;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=wellKnownUserSource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wellKnownUserSource.d.ts","sourceRoot":"","sources":["../../src/generic/wellKnownUserSource.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,mBAAmB;IAC5B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAoC;IAE5D;;;;;;;;OAQG;IACH,WAAkB,QAAQ,IAAI,mBAAmB,CAOhD;IAED;;;OAGG;WACW,aAAa,IAAI,IAAI;IAInC;;;;;;;;OAQG;IACI,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO;IAI/D;;;;;;;;;;;OAWG;IACU,aAAa,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;CAGpF"}
|