@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.3
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/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +928 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +8482 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +43 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +142 -27
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +115 -49
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +1 -83
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +4 -206
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -94
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +22138 -18770
- package/src/generic/ResolverBase.ts +171 -27
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +127 -50
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +6 -252
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -169,7 +169,24 @@ export declare class ResolverBase {
|
|
|
169
169
|
* into the view. Subqueries against another entity's BaseView therefore do not inherit that
|
|
170
170
|
* entity's RLS; they are still restricted to the view (not the table).
|
|
171
171
|
*/
|
|
172
|
-
protected assertClientClauseUsesEntityBaseViews(clause: string | undefined | null, label: string, provider?: IMetadataProvider): void;
|
|
172
|
+
protected assertClientClauseUsesEntityBaseViews(clause: string | undefined | null, label: string, provider?: IMetadataProvider, user?: UserInfo): void;
|
|
173
|
+
/**
|
|
174
|
+
* SECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
|
|
175
|
+
* to the SELECT-only validation the caller performs, every table reference must resolve to an
|
|
176
|
+
* entity BaseView the acting user can read UNSCOPED. CTE names defined by the statement itself
|
|
177
|
+
* are excluded from the check. Fails closed: no user, or an unresolvable reference, refuses.
|
|
178
|
+
*
|
|
179
|
+
* Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with
|
|
180
|
+
* row-level security and denied-field projection, and raw SQL applies neither. Rather than try
|
|
181
|
+
* to rewrite arbitrary SQL (joins, CTEs) to compose that policy, this refuses any entity that
|
|
182
|
+
* carries a row filter or a denied field for the caller — those reads belong on RunView. For
|
|
183
|
+
* the remaining entities CanRead genuinely means "every row and field of the view".
|
|
184
|
+
*/
|
|
185
|
+
protected assertFullQueryUsesReadableEntityViews(sqlText: string, provider: IMetadataProvider | undefined, user: UserInfo | undefined, label?: string): void;
|
|
186
|
+
/** Refuses an entity whose read is narrowed for this user by row-level security or field-level denials. */
|
|
187
|
+
private assertUnscopedRead;
|
|
188
|
+
/** Walks a parsed AST collecting the names of CTEs the statement itself defines. */
|
|
189
|
+
private collectCTENames;
|
|
173
190
|
/**
|
|
174
191
|
* Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
|
|
175
192
|
* a view request can carry that is actually a SQL fragment. GraphQL entry points
|
|
@@ -197,19 +214,42 @@ export declare class ResolverBase {
|
|
|
197
214
|
orderBy?: string | null;
|
|
198
215
|
userSearchString?: string | null;
|
|
199
216
|
overrideExcludeFilter?: string | null;
|
|
200
|
-
}, provider?: IMetadataProvider): void;
|
|
217
|
+
}, provider?: IMetadataProvider, user?: UserInfo): void;
|
|
201
218
|
/** Same write-node walk as EDS `sqlReadOnlyScreen.astContainsWriteNode`. */
|
|
202
219
|
private astContainsWriteNode;
|
|
203
220
|
private static readonly WRITE_NODE_TYPES;
|
|
204
221
|
private dialectForProvider;
|
|
222
|
+
/** True for a reference with no schema — which `SQLParser.ExtractTableRefs` reports as `dbo`. */
|
|
223
|
+
private isUnqualifiedSchema;
|
|
205
224
|
private stripSqlIdent;
|
|
225
|
+
/**
|
|
226
|
+
* `bare` maps an unqualified view name to its entity, or to `null` when the name exists in
|
|
227
|
+
* more than one schema — the database, not this screen, would decide which one runs.
|
|
228
|
+
*/
|
|
206
229
|
private entityBaseViewAllowList;
|
|
230
|
+
/**
|
|
231
|
+
* SECURITY — resolves a table reference against the entity BaseView allow-list and, when an
|
|
232
|
+
* acting user is supplied, additionally requires that user to hold CanRead on the referenced
|
|
233
|
+
* entity. Base views do not embed RLS and entity permissions are otherwise checked only on
|
|
234
|
+
* the TOP entity of a request, so without this check a subquery (or ad-hoc query) could read
|
|
235
|
+
* entities the caller has no read grant on.
|
|
236
|
+
*
|
|
237
|
+
* A schema-qualified reference must match that exact schema — `secret.vwFoo` never resolves
|
|
238
|
+
* through another schema's `vwFoo`. An unqualified reference resolves only when the view name
|
|
239
|
+
* is unique across schemas; an ambiguous one must be qualified, because the database's
|
|
240
|
+
* default-schema resolution, not this screen, would pick which view actually runs.
|
|
241
|
+
*
|
|
242
|
+
* `SQLParser.ExtractTableRefs` reports an unqualified reference as schema `dbo`, so `dbo` is
|
|
243
|
+
* indistinguishable from "no schema" here and both take the unqualified path (an exact
|
|
244
|
+
* `dbo.<view>` entity wins first).
|
|
245
|
+
*/
|
|
246
|
+
private assertTableRefReadable;
|
|
207
247
|
/**
|
|
208
248
|
* Optimized RunViewGenericInternal implementation with:
|
|
209
249
|
* - Field filtering at source (Fix #7)
|
|
210
250
|
* - Improved error handling (Fix #9)
|
|
211
251
|
*/
|
|
212
|
-
protected RunViewGenericInternal(provider: DatabaseProviderBase, viewInfo: MJUserViewEntityExtended, extraFilter: string, orderBy: string, userSearchString: string, excludeUserViewRunID: string | undefined, overrideExcludeFilter: string | undefined, saveViewResults: boolean | undefined, fields: string[] | undefined, ignoreMaxRows: boolean | undefined, excludeDataFromAllPriorViewRuns: boolean | undefined, forceAuditLog: boolean | undefined, auditLogDescription: string | undefined, resultType: string | undefined, userPayload: UserPayload | null, maxRows: number | undefined, startRow: number | undefined, aggregates?: AggregateExpression[], afterKey?: CompositeKey, bypassCache?: boolean, dataSource?: 'Live' | 'Materialized'): Promise<RunViewResult<any>>;
|
|
252
|
+
protected RunViewGenericInternal(provider: DatabaseProviderBase, viewInfo: MJUserViewEntityExtended, extraFilter: string, orderBy: string, userSearchString: string, excludeUserViewRunID: string | undefined, overrideExcludeFilter: string | undefined, saveViewResults: boolean | undefined, fields: string[] | undefined, ignoreMaxRows: boolean | undefined, excludeDataFromAllPriorViewRuns: boolean | undefined, forceAuditLog: boolean | undefined, auditLogDescription: string | undefined, resultType: string | undefined, userPayload: UserPayload | null, maxRows: number | undefined, startRow: number | undefined, aggregates?: AggregateExpression[], afterKey?: CompositeKey, bypassCache?: boolean, dataSource?: 'Live' | 'Materialized', includeBinaryFields?: boolean): Promise<RunViewResult<any>>;
|
|
213
253
|
/**
|
|
214
254
|
* Optimized implementation that:
|
|
215
255
|
* 1. Fetches user info only once (fixes N+1 query)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResolverBase.d.ts","sourceRoot":"","sources":["../../src/generic/ResolverBase.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,mBAAmB,EACnB,UAAU,EAEV,YAAY,EACZ,oBAAoB,EACpB,eAAe,EAEf,UAAU,EACV,oBAAoB,EAEpB,iBAAiB,EAQjB,kCAAkC,EAIlC,aAAa,EACb,QAAQ,EACT,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAsC,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAG7G,OAAO,EAAE,YAAY,EAAsB,MAAM,cAAc,CAAC;AAOhE,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACjG,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"ResolverBase.d.ts","sourceRoot":"","sources":["../../src/generic/ResolverBase.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, KeyValuePair, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
|
|
1
|
+
import { BaseEntity, BaseEntityEvent, CompositeKey, EntityFieldTSType, EntityPermissionType, KeyValuePair, LogDebug, LogError, LogStatus, Metadata, ReadableFieldsTransportKey, RecordChangeFieldSecurityProjector, RecordChangePayloadFields, } from '@memberjunction/core';
|
|
2
2
|
import { UserCache } from '@memberjunction/generic-database-provider';
|
|
3
3
|
import { AuthorizationError } from 'type-graphql';
|
|
4
4
|
import { GraphQLError } from 'graphql';
|
|
@@ -6,7 +6,7 @@ import { RefusalExtensions } from './refusalExtensions.js';
|
|
|
6
6
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
7
7
|
import { httpTransport, CloudEvent, emitterFor } from 'cloudevents';
|
|
8
8
|
import { DeleteOptionsInput } from './DeleteOptionsInput.js';
|
|
9
|
-
import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
9
|
+
import { MJEventType, MJGlobal, ENCRYPTED_SENTINEL, ReplaceByteArraysWithBase64, EscapeSQLString, IsValueEncrypted, IsOnlyTimezoneShift } from '@memberjunction/global';
|
|
10
10
|
import { SQLParser } from '@memberjunction/sql-parser';
|
|
11
11
|
import { PostgreSQLDialect, SQLServerDialect } from '@memberjunction/sql-dialect';
|
|
12
12
|
import { EncryptionEngine } from '@memberjunction/encryption';
|
|
@@ -57,7 +57,10 @@ export class ResolverBase {
|
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
59
|
// Shallow copy up front so every write below lands on our object, never the caller's.
|
|
60
|
-
|
|
60
|
+
// Binary values are base64 strings everywhere above the providers; any byte array that still
|
|
61
|
+
// reaches a resolver (custom code, an external driver) is converted here, because GraphQL's
|
|
62
|
+
// String scalar cannot serialize a Buffer and would fail the whole response.
|
|
63
|
+
dataObject = ReplaceByteArraysWithBase64({ ...dataObject });
|
|
61
64
|
// for the given entity name provided, check to see if there are any fields
|
|
62
65
|
// where the code name is different from the field name, and for just those
|
|
63
66
|
// fields, iterate through the dataObject and REPLACE the property that has the field name
|
|
@@ -462,7 +465,7 @@ export class ResolverBase {
|
|
|
462
465
|
const viewInfo = result.Results[0];
|
|
463
466
|
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, viewInput.AfterKey
|
|
464
467
|
? CompositeKey.FromKeyValuePairs(viewInput.AfterKey.KeyValuePairs)
|
|
465
|
-
: undefined, viewInput.BypassCache, viewInput.DataSource);
|
|
468
|
+
: undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
|
|
466
469
|
}
|
|
467
470
|
else {
|
|
468
471
|
LogError(`RunViewByNameGeneric: View ${viewInput.ViewName} not found or no results returned`);
|
|
@@ -483,7 +486,7 @@ export class ResolverBase {
|
|
|
483
486
|
const contextUser = this.GetUserFromPayload(userPayload);
|
|
484
487
|
const viewInfo = await provider.GetEntityObject('MJ: User Views', contextUser);
|
|
485
488
|
await viewInfo.Load(viewInput.ViewID);
|
|
486
|
-
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource);
|
|
489
|
+
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, viewInput.SaveViewResults, viewInput.Fields, viewInput.IgnoreMaxRows, viewInput.ExcludeDataFromAllPriorViewRuns, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
|
|
487
490
|
}
|
|
488
491
|
catch (err) {
|
|
489
492
|
console.log(err);
|
|
@@ -506,7 +509,7 @@ export class ResolverBase {
|
|
|
506
509
|
EntityID: entity.ID,
|
|
507
510
|
EntityBaseView: entity.BaseView,
|
|
508
511
|
}; // only providing a few bits of data here, but it's enough to get the view to run
|
|
509
|
-
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource);
|
|
512
|
+
return this.RunViewGenericInternal(provider, viewInfo, viewInput.ExtraFilter, viewInput.OrderBy, viewInput.UserSearchString, viewInput.ExcludeUserViewRunID, viewInput.OverrideExcludeFilter, false, viewInput.Fields, viewInput.IgnoreMaxRows, false, viewInput.ForceAuditLog, viewInput.AuditLogDescription, viewInput.ResultType, userPayload, viewInput.MaxRows, viewInput.StartRow, viewInput.Aggregates, undefined, viewInput.BypassCache, viewInput.DataSource, viewInput.IncludeBinaryFields);
|
|
510
513
|
}
|
|
511
514
|
catch (err) {
|
|
512
515
|
console.log(err);
|
|
@@ -576,6 +579,7 @@ export class ResolverBase {
|
|
|
576
579
|
aggregates: viewInput.Aggregates,
|
|
577
580
|
bypassCache: viewInput.BypassCache,
|
|
578
581
|
dataSource: viewInput.DataSource,
|
|
582
|
+
includeBinaryFields: viewInput.IncludeBinaryFields,
|
|
579
583
|
});
|
|
580
584
|
}
|
|
581
585
|
catch (err) {
|
|
@@ -729,7 +733,7 @@ export class ResolverBase {
|
|
|
729
733
|
* into the view. Subqueries against another entity's BaseView therefore do not inherit that
|
|
730
734
|
* entity's RLS; they are still restricted to the view (not the table).
|
|
731
735
|
*/
|
|
732
|
-
assertClientClauseUsesEntityBaseViews(clause, label, provider) {
|
|
736
|
+
assertClientClauseUsesEntityBaseViews(clause, label, provider, user) {
|
|
733
737
|
if (!clause?.trim())
|
|
734
738
|
return;
|
|
735
739
|
const dialect = this.dialectForProvider(provider);
|
|
@@ -753,12 +757,78 @@ export class ResolverBase {
|
|
|
753
757
|
const schema = this.stripSqlIdent(t.SchemaName);
|
|
754
758
|
if (table.toLowerCase() === '__mj_clause_screen')
|
|
755
759
|
continue;
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
760
|
+
this.assertTableRefReadable(allowed, schema, table, label, user);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* SECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
|
|
765
|
+
* to the SELECT-only validation the caller performs, every table reference must resolve to an
|
|
766
|
+
* entity BaseView the acting user can read UNSCOPED. CTE names defined by the statement itself
|
|
767
|
+
* are excluded from the check. Fails closed: no user, or an unresolvable reference, refuses.
|
|
768
|
+
*
|
|
769
|
+
* Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with
|
|
770
|
+
* row-level security and denied-field projection, and raw SQL applies neither. Rather than try
|
|
771
|
+
* to rewrite arbitrary SQL (joins, CTEs) to compose that policy, this refuses any entity that
|
|
772
|
+
* carries a row filter or a denied field for the caller — those reads belong on RunView. For
|
|
773
|
+
* the remaining entities CanRead genuinely means "every row and field of the view".
|
|
774
|
+
*/
|
|
775
|
+
assertFullQueryUsesReadableEntityViews(sqlText, provider, user, label = 'SQL') {
|
|
776
|
+
if (!user) {
|
|
777
|
+
throw new Error(`Invalid ${label}: no acting user resolved for ad-hoc SQL — refusing`);
|
|
778
|
+
}
|
|
779
|
+
const dialect = this.dialectForProvider(provider);
|
|
780
|
+
const parser = new SQLParser(sqlText, dialect);
|
|
781
|
+
if (!parser.IsValid || parser.HasWriteStatement || parser.StatementKind !== 'select') {
|
|
782
|
+
throw new Error(`Invalid ${label}: not a safe read-only statement — refusing under uncertainty`);
|
|
783
|
+
}
|
|
784
|
+
if (this.astContainsWriteNode(parser.AST)) {
|
|
785
|
+
throw new Error(`Invalid ${label}: write/DDL nested in a subquery is not permitted`);
|
|
786
|
+
}
|
|
787
|
+
const cteNames = new Set();
|
|
788
|
+
this.collectCTENames(parser.AST, cteNames);
|
|
789
|
+
const allowed = this.entityBaseViewAllowList(provider);
|
|
790
|
+
const tables = SQLParser.ExtractTableRefs(sqlText, dialect);
|
|
791
|
+
for (const t of tables) {
|
|
792
|
+
const table = this.stripSqlIdent(t.TableName);
|
|
793
|
+
const schema = this.stripSqlIdent(t.SchemaName);
|
|
794
|
+
if (this.isUnqualifiedSchema(schema) && cteNames.has(table.toLowerCase()))
|
|
759
795
|
continue;
|
|
760
|
-
|
|
796
|
+
const entity = this.assertTableRefReadable(allowed, schema, table, label, user);
|
|
797
|
+
this.assertUnscopedRead(entity, user, label);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/** Refuses an entity whose read is narrowed for this user by row-level security or field-level denials. */
|
|
801
|
+
assertUnscopedRead(entity, user, label) {
|
|
802
|
+
if (entity.GetEffectiveRowFilterWhereClause(user, EntityPermissionType.Read, '').length > 0) {
|
|
803
|
+
throw new Error(`Invalid ${label}: entity '${entity.Name}' is row-level-security filtered for you — ad-hoc SQL cannot apply that filter; use RunView`);
|
|
804
|
+
}
|
|
805
|
+
if (entity.GetDeniedReadFields(user).size > 0) {
|
|
806
|
+
throw new Error(`Invalid ${label}: entity '${entity.Name}' has fields you are not permitted to read — ad-hoc SQL cannot project them away; use RunView`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/** Walks a parsed AST collecting the names of CTEs the statement itself defines. */
|
|
810
|
+
collectCTENames(node, names) {
|
|
811
|
+
if (!node || typeof node !== 'object')
|
|
812
|
+
return;
|
|
813
|
+
if (Array.isArray(node)) {
|
|
814
|
+
for (const n of node)
|
|
815
|
+
this.collectCTENames(n, names);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const obj = node;
|
|
819
|
+
const withClause = obj.with;
|
|
820
|
+
if (Array.isArray(withClause)) {
|
|
821
|
+
for (const cte of withClause) {
|
|
822
|
+
const name = cte?.name;
|
|
823
|
+
if (typeof name === 'string')
|
|
824
|
+
names.add(name.toLowerCase());
|
|
825
|
+
else if (name && typeof name === 'object' && typeof name.value === 'string') {
|
|
826
|
+
names.add(name.value.toLowerCase());
|
|
827
|
+
}
|
|
828
|
+
}
|
|
761
829
|
}
|
|
830
|
+
for (const v of Object.values(obj))
|
|
831
|
+
this.collectCTENames(v, names);
|
|
762
832
|
}
|
|
763
833
|
/**
|
|
764
834
|
* Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
|
|
@@ -782,10 +852,10 @@ export class ResolverBase {
|
|
|
782
852
|
* admin-authored format may splice the term in unquoted; `createViewUserSearchSQL` re-applies
|
|
783
853
|
* `ValidateUserProvidedSQLClause` for exactly those entities.
|
|
784
854
|
*/
|
|
785
|
-
screenClientViewClauses(clauses, provider) {
|
|
786
|
-
this.assertClientClauseUsesEntityBaseViews(clauses.extraFilter, 'ExtraFilter', provider);
|
|
787
|
-
this.assertClientClauseUsesEntityBaseViews(clauses.orderBy, 'OrderBy', provider);
|
|
788
|
-
this.assertClientClauseUsesEntityBaseViews(clauses.overrideExcludeFilter, 'OverrideExcludeFilter', provider);
|
|
855
|
+
screenClientViewClauses(clauses, provider, user) {
|
|
856
|
+
this.assertClientClauseUsesEntityBaseViews(clauses.extraFilter, 'ExtraFilter', provider, user);
|
|
857
|
+
this.assertClientClauseUsesEntityBaseViews(clauses.orderBy, 'OrderBy', provider, user);
|
|
858
|
+
this.assertClientClauseUsesEntityBaseViews(clauses.overrideExcludeFilter, 'OverrideExcludeFilter', provider, user);
|
|
789
859
|
}
|
|
790
860
|
/** Same write-node walk as EDS `sqlReadOnlyScreen.astContainsWriteNode`. */
|
|
791
861
|
astContainsWriteNode(node) {
|
|
@@ -810,41 +880,77 @@ export class ResolverBase {
|
|
|
810
880
|
return new PostgreSQLDialect();
|
|
811
881
|
return new SQLServerDialect();
|
|
812
882
|
}
|
|
883
|
+
/** True for a reference with no schema — which `SQLParser.ExtractTableRefs` reports as `dbo`. */
|
|
884
|
+
isUnqualifiedSchema(schema) {
|
|
885
|
+
return !schema || schema.toLowerCase() === 'dbo';
|
|
886
|
+
}
|
|
813
887
|
stripSqlIdent(name) {
|
|
814
888
|
if (!name)
|
|
815
889
|
return '';
|
|
816
890
|
return name.replace(/^\[|\]$/g, '').replace(/^"|"$/g, '').replace(/^`|`$/g, '');
|
|
817
891
|
}
|
|
892
|
+
/**
|
|
893
|
+
* `bare` maps an unqualified view name to its entity, or to `null` when the name exists in
|
|
894
|
+
* more than one schema — the database, not this screen, would decide which one runs.
|
|
895
|
+
*/
|
|
818
896
|
entityBaseViewAllowList(provider) {
|
|
819
|
-
const qualified = new
|
|
820
|
-
const bare = new
|
|
897
|
+
const qualified = new Map();
|
|
898
|
+
const bare = new Map();
|
|
821
899
|
const entities = provider?.Entities ?? [];
|
|
822
900
|
for (const e of entities) {
|
|
823
901
|
const view = this.stripSqlIdent(e.BaseView);
|
|
824
902
|
if (!view)
|
|
825
903
|
continue;
|
|
826
904
|
const schema = this.stripSqlIdent(e.SchemaName);
|
|
827
|
-
|
|
905
|
+
const bareKey = view.toLowerCase();
|
|
906
|
+
bare.set(bareKey, bare.has(bareKey) ? null : e);
|
|
828
907
|
if (schema)
|
|
829
|
-
qualified.
|
|
908
|
+
qualified.set(`${schema}.${view}`.toLowerCase(), e);
|
|
830
909
|
}
|
|
831
910
|
return { qualified, bare };
|
|
832
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* SECURITY — resolves a table reference against the entity BaseView allow-list and, when an
|
|
914
|
+
* acting user is supplied, additionally requires that user to hold CanRead on the referenced
|
|
915
|
+
* entity. Base views do not embed RLS and entity permissions are otherwise checked only on
|
|
916
|
+
* the TOP entity of a request, so without this check a subquery (or ad-hoc query) could read
|
|
917
|
+
* entities the caller has no read grant on.
|
|
918
|
+
*
|
|
919
|
+
* A schema-qualified reference must match that exact schema — `secret.vwFoo` never resolves
|
|
920
|
+
* through another schema's `vwFoo`. An unqualified reference resolves only when the view name
|
|
921
|
+
* is unique across schemas; an ambiguous one must be qualified, because the database's
|
|
922
|
+
* default-schema resolution, not this screen, would pick which view actually runs.
|
|
923
|
+
*
|
|
924
|
+
* `SQLParser.ExtractTableRefs` reports an unqualified reference as schema `dbo`, so `dbo` is
|
|
925
|
+
* indistinguishable from "no schema" here and both take the unqualified path (an exact
|
|
926
|
+
* `dbo.<view>` entity wins first).
|
|
927
|
+
*/
|
|
928
|
+
assertTableRefReadable(allowed, schema, table, label, user) {
|
|
929
|
+
const qualified = allowed.qualified.get(`${schema}.${table}`.toLowerCase());
|
|
930
|
+
const entity = this.isUnqualifiedSchema(schema) ? qualified ?? allowed.bare.get(table.toLowerCase()) : qualified;
|
|
931
|
+
if (entity === null) {
|
|
932
|
+
throw new Error(`Invalid ${label}: '${table}' is a base view in more than one schema — qualify it with its schema`);
|
|
933
|
+
}
|
|
934
|
+
if (!entity) {
|
|
935
|
+
throw new Error(`Invalid ${label}: subquery must use an entity base view, not '${schema ? schema + '.' : ''}${table}'`);
|
|
936
|
+
}
|
|
937
|
+
if (user) {
|
|
938
|
+
const perms = entity.GetUserPermisions(user);
|
|
939
|
+
if (!perms.CanRead) {
|
|
940
|
+
throw new Error(`Invalid ${label}: you do not have read permission on entity '${entity.Name}' referenced by '${schema ? schema + '.' : ''}${table}'`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
return entity;
|
|
944
|
+
}
|
|
833
945
|
/**
|
|
834
946
|
* Optimized RunViewGenericInternal implementation with:
|
|
835
947
|
* - Field filtering at source (Fix #7)
|
|
836
948
|
* - Improved error handling (Fix #9)
|
|
837
949
|
*/
|
|
838
|
-
async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource) {
|
|
950
|
+
async RunViewGenericInternal(provider, viewInfo, extraFilter, orderBy, userSearchString, excludeUserViewRunID, overrideExcludeFilter, saveViewResults, fields, ignoreMaxRows, excludeDataFromAllPriorViewRuns, forceAuditLog, auditLogDescription, resultType, userPayload, maxRows, startRow, aggregates, afterKey, bypassCache, dataSource, includeBinaryFields) {
|
|
839
951
|
try {
|
|
840
952
|
if (!viewInfo || !userPayload)
|
|
841
953
|
return null;
|
|
842
|
-
// SECURITY: GraphQL ExtraFilter may contain IN (SELECT … FROM <base view>).
|
|
843
|
-
// Screen at this boundary: parse, reject writes, allow only entity BaseViews.
|
|
844
|
-
this.screenClientViewClauses({ extraFilter, orderBy, userSearchString, overrideExcludeFilter }, provider);
|
|
845
|
-
// Check API key scope authorization for view operations
|
|
846
|
-
await this.CheckAPIKeyScopeAuthorization('view:run', viewInfo.Entity, userPayload);
|
|
847
|
-
const md = provider;
|
|
848
954
|
// Prefer the authenticated session's payload user — it is the authoritative per-request
|
|
849
955
|
// identity and (for magic-link sessions) carries the per-session resource scope / synthesized
|
|
850
956
|
// roles that drive RLS. The cached lookup is a fallback for paths where the payload user
|
|
@@ -853,6 +959,13 @@ export class ResolverBase {
|
|
|
853
959
|
?? UserCache.Users.find((u) => u.Email.toLowerCase().trim() === userPayload?.email.toLowerCase().trim());
|
|
854
960
|
if (!user)
|
|
855
961
|
throw new Error(`User ${userPayload?.email} not found in metadata`);
|
|
962
|
+
// SECURITY: GraphQL ExtraFilter may contain IN (SELECT … FROM <base view>).
|
|
963
|
+
// Screen at this boundary: parse, reject writes, allow only entity BaseViews the
|
|
964
|
+
// acting user can read (subqueries do not inherit the top entity's permission check).
|
|
965
|
+
this.screenClientViewClauses({ extraFilter, orderBy, userSearchString, overrideExcludeFilter }, provider, user);
|
|
966
|
+
// Check API key scope authorization for view operations
|
|
967
|
+
await this.CheckAPIKeyScopeAuthorization('view:run', viewInfo.Entity, userPayload);
|
|
968
|
+
const md = provider;
|
|
856
969
|
const entityInfo = md.Entities.find((e) => e.Name === viewInfo.Entity);
|
|
857
970
|
if (!entityInfo)
|
|
858
971
|
throw new Error(`Entity ${viewInfo.Entity} not found in metadata`);
|
|
@@ -899,6 +1012,7 @@ export class ResolverBase {
|
|
|
899
1012
|
Aggregates: aggregates,
|
|
900
1013
|
BypassCache: bypassCache,
|
|
901
1014
|
DataSource: dataSource,
|
|
1015
|
+
IncludeBinaryFields: includeBinaryFields,
|
|
902
1016
|
}, user);
|
|
903
1017
|
// Log aggregate results for debugging
|
|
904
1018
|
if (aggregates?.length) {
|
|
@@ -988,7 +1102,7 @@ export class ResolverBase {
|
|
|
988
1102
|
orderBy: param.orderBy,
|
|
989
1103
|
userSearchString: param.userSearchString,
|
|
990
1104
|
overrideExcludeFilter: param.overrideExcludeFilter,
|
|
991
|
-
}, md);
|
|
1105
|
+
}, md, contextUser ?? undefined);
|
|
992
1106
|
if (param.viewInfo) {
|
|
993
1107
|
// Validate entity only once per entity type
|
|
994
1108
|
const entityName = param.viewInfo.Entity;
|
|
@@ -1028,6 +1142,7 @@ export class ResolverBase {
|
|
|
1028
1142
|
Aggregates: param.aggregates,
|
|
1029
1143
|
BypassCache: param.bypassCache,
|
|
1030
1144
|
DataSource: param.dataSource,
|
|
1145
|
+
IncludeBinaryFields: param.includeBinaryFields,
|
|
1031
1146
|
});
|
|
1032
1147
|
}
|
|
1033
1148
|
// Fix #4: Run views in a single batch through RunViews
|