@reventlessdev/reventless-aws 3.0.0-alpha.172 → 3.0.0-alpha.174
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/CHANGELOG.md +20 -0
- package/package.json +6 -6
- package/src/Platform.res +77 -7
- package/src/Platform.res.mjs +82 -8
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
- package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +188 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +141 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +72 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +40 -0
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
- package/src/components/Api/AppSync_Adapter.res +72 -8
- package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
- package/src/components/Plugin.res +1 -1
- package/src/components/Plugin.res.mjs +2 -2
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +10 -2
- package/tests/AppSync_AdapterTest.res +140 -0
- package/tests/AppSync_AdapterTest.res.mjs +182 -6
- package/tests/PgChangeFeedRelay_RuntimeTest.res +75 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +135 -0
|
@@ -266,6 +266,21 @@ let getClient = () =>
|
|
|
266
266
|
// reaching request is already authenticated). `AllowGroups([])` and
|
|
267
267
|
// `DenyAll` emit a sentinel `__deny_all__` group that no Cognito user can
|
|
268
268
|
// belong to — effectively blocking the field at the API layer.
|
|
269
|
+
//
|
|
270
|
+
// ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
|
|
271
|
+
// A field flagged `systemCallable` (via `mutationSchemaEntry`/`querySchemaEntry`)
|
|
272
|
+
// must be reachable by BOTH the console UI (Cognito) and a deploy-time system
|
|
273
|
+
// caller signing SigV4 with ambient AWS credentials (`Util_AppSync_Caller`,
|
|
274
|
+
// the AWS_IAM additional auth provider). `@aws_auth(...)` is the single-mode
|
|
275
|
+
// directive form and does not admit IAM on a multi-auth API. For these fields
|
|
276
|
+
// we emit the multi-auth form instead:
|
|
277
|
+
// @aws_cognito_user_pools(cognito_groups: [...]) @aws_iam
|
|
278
|
+
// The Cognito arm preserves the field's existing group gating (or, when the
|
|
279
|
+
// field carries no Cognito group restriction, a bare `@aws_cognito_user_pools`
|
|
280
|
+
// keeps it open to any authenticated Cognito user); the `@aws_iam` arm admits
|
|
281
|
+
// the system caller. IAM access must be constrained by the API resource policy
|
|
282
|
+
// and a least-privilege deploy-role policy — see
|
|
283
|
+
// `docs/guides/appsync-iam-system-caller.md`.
|
|
269
284
|
|
|
270
285
|
let _permissionToCognitoGroups = (
|
|
271
286
|
permission: Reventless.Authorization.permission,
|
|
@@ -282,6 +297,20 @@ let _formatGroupsDirective = (groups: array<string>): string => {
|
|
|
282
297
|
`@aws_auth(cognito_groups: [${quoted}])`
|
|
283
298
|
}
|
|
284
299
|
|
|
300
|
+
// Multi-auth directive for a field that must accept BOTH Cognito and IAM.
|
|
301
|
+
// `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps the
|
|
302
|
+
// field open to any authenticated Cognito user. `@aws_iam` admits the
|
|
303
|
+
// deploy-time SigV4 system caller. See the dual-auth note above.
|
|
304
|
+
let _formatDualAuthDirective = (groups: option<array<string>>): string => {
|
|
305
|
+
let cognito = switch groups {
|
|
306
|
+
| Some(g) =>
|
|
307
|
+
let quoted = g->Array.map(x => `"${x}"`)->Array.join(", ")
|
|
308
|
+
`@aws_cognito_user_pools(cognito_groups: [${quoted}])`
|
|
309
|
+
| None => `@aws_cognito_user_pools`
|
|
310
|
+
}
|
|
311
|
+
`${cognito} @aws_iam`
|
|
312
|
+
}
|
|
313
|
+
|
|
285
314
|
let injectAwsAuth = (
|
|
286
315
|
fragment: Reventless.Plugin.apiSchemaFragment,
|
|
287
316
|
~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
|
|
@@ -292,8 +321,16 @@ let injectAwsAuth = (
|
|
|
292
321
|
// Build authorization lookup from mutation entries: fieldName -> groups.
|
|
293
322
|
// Spec-level `fieldPermissions` takes precedence over the legacy
|
|
294
323
|
// `authorization.group` per-entry single-group form.
|
|
324
|
+
// Fields opted into deploy-time IAM invocation (dual-auth). Membership here
|
|
325
|
+
// switches a field from the single-mode `@aws_auth` form to the multi-auth
|
|
326
|
+
// `@aws_cognito_user_pools(...) @aws_iam` form.
|
|
327
|
+
let iamFields: Dict.t<bool> = Dict.make()
|
|
328
|
+
|
|
295
329
|
let mutationAuthMap: Dict.t<array<string>> = Dict.make()
|
|
296
330
|
mutationEntries->Array.forEach(entry => {
|
|
331
|
+
if entry.systemCallable->Option.getOr(false) {
|
|
332
|
+
entry.fieldNames->Array.forEach(fieldName => iamFields->Dict.set(fieldName, true))
|
|
333
|
+
}
|
|
297
334
|
switch entry.authorization {
|
|
298
335
|
| Some({group}) =>
|
|
299
336
|
entry.fieldNames->Array.forEach(fieldName =>
|
|
@@ -319,6 +356,10 @@ let injectAwsAuth = (
|
|
|
319
356
|
// Spec-level `permission` takes precedence over the legacy authorization.
|
|
320
357
|
let queryAuthMap: Dict.t<array<string>> = Dict.make()
|
|
321
358
|
queryEntries->Array.forEach(entry => {
|
|
359
|
+
if entry.systemCallable->Option.getOr(false) {
|
|
360
|
+
iamFields->Dict.set(entry.singleFieldName, true)
|
|
361
|
+
iamFields->Dict.set(entry.listFieldName, true)
|
|
362
|
+
}
|
|
322
363
|
switch entry.authorization {
|
|
323
364
|
| Some({group}) =>
|
|
324
365
|
queryAuthMap->Dict.set(entry.singleFieldName, [group])
|
|
@@ -341,17 +382,27 @@ let injectAwsAuth = (
|
|
|
341
382
|
|
|
342
383
|
let augmentedMutations = parts.mutations->Array.map(field => {
|
|
343
384
|
let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
385
|
+
let groups = mutationAuthMap->Dict.get(fieldName)
|
|
386
|
+
if iamFields->Dict.get(fieldName)->Option.getOr(false) {
|
|
387
|
+
`${field}\n ${_formatDualAuthDirective(groups)}`
|
|
388
|
+
} else {
|
|
389
|
+
switch groups {
|
|
390
|
+
| Some(groups) => `${field}\n ${_formatGroupsDirective(groups)}`
|
|
391
|
+
| None => field
|
|
392
|
+
}
|
|
347
393
|
}
|
|
348
394
|
})
|
|
349
395
|
|
|
350
396
|
let augmentedQueries = parts.queries->Array.map(field => {
|
|
351
397
|
let fieldName = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
398
|
+
let groups = queryAuthMap->Dict.get(fieldName)
|
|
399
|
+
if iamFields->Dict.get(fieldName)->Option.getOr(false) {
|
|
400
|
+
`${field} ${_formatDualAuthDirective(groups)}`
|
|
401
|
+
} else {
|
|
402
|
+
switch groups {
|
|
403
|
+
| Some(groups) => `${field} ${_formatGroupsDirective(groups)}`
|
|
404
|
+
| None => field
|
|
405
|
+
}
|
|
355
406
|
}
|
|
356
407
|
})
|
|
357
408
|
|
|
@@ -374,17 +425,30 @@ let injectAwsAuth = (
|
|
|
374
425
|
// Injects @aws_auth with the given group on ALL mutation, query, and subscription
|
|
375
426
|
// fields in a fragment. Used for the base fragment where all fields share the
|
|
376
427
|
// same authorization group.
|
|
428
|
+
//
|
|
429
|
+
// `~iamFieldNames` opts the named mutation/query fields into deploy-time IAM
|
|
430
|
+
// (dual-auth): those fields emit `@aws_cognito_user_pools(cognito_groups:
|
|
431
|
+
// ["<group>"]) @aws_iam` instead of the single-mode `@aws_auth(...)`, keeping
|
|
432
|
+
// the same Cognito group gating while also admitting the SigV4 system caller.
|
|
433
|
+
// Subscriptions are never IAM-marked (the deploy caller does not subscribe).
|
|
377
434
|
let injectAwsAuthAll = (
|
|
378
435
|
fragment: Reventless.Plugin.apiSchemaFragment,
|
|
379
436
|
~group: string,
|
|
437
|
+
~iamFieldNames: array<string>=[],
|
|
380
438
|
): Reventless.Plugin.apiSchemaFragment => {
|
|
381
439
|
let parts = ReventlessCore.GraphQL_Stitcher.decode(fragment)
|
|
440
|
+
let isIam = (field: string): bool =>
|
|
441
|
+
iamFieldNames->Array.includes(ReventlessCore.GraphQL_Stitcher.extractLeadingName(field))
|
|
382
442
|
|
|
383
443
|
let augmentedMutations = parts.mutations->Array.map(field =>
|
|
384
|
-
|
|
444
|
+
isIam(field)
|
|
445
|
+
? `${field}\n ${_formatDualAuthDirective(Some([group]))}`
|
|
446
|
+
: `${field}\n @aws_auth(cognito_groups: ["${group}"])`
|
|
385
447
|
)
|
|
386
448
|
let augmentedQueries = parts.queries->Array.map(field =>
|
|
387
|
-
|
|
449
|
+
isIam(field)
|
|
450
|
+
? `${field} ${_formatDualAuthDirective(Some([group]))}`
|
|
451
|
+
: `${field} @aws_auth(cognito_groups: ["${group}"])`
|
|
388
452
|
)
|
|
389
453
|
let augmentedSubscriptions = parts.subscriptions->Array.map(field =>
|
|
390
454
|
`${field}\n @aws_auth(cognito_groups: ["${group}"])`
|
|
@@ -204,10 +204,27 @@ function _formatGroupsDirective(groups) {
|
|
|
204
204
|
return `@aws_auth(cognito_groups: [` + quoted + `])`;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
function _formatDualAuthDirective(groups) {
|
|
208
|
+
let cognito;
|
|
209
|
+
if (groups !== undefined) {
|
|
210
|
+
let quoted = groups.map(x => `"` + x + `"`).join(", ");
|
|
211
|
+
cognito = `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
|
|
212
|
+
} else {
|
|
213
|
+
cognito = `@aws_cognito_user_pools`;
|
|
214
|
+
}
|
|
215
|
+
return cognito + ` @aws_iam`;
|
|
216
|
+
}
|
|
217
|
+
|
|
207
218
|
function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
208
219
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
|
|
220
|
+
let iamFields = {};
|
|
209
221
|
let mutationAuthMap = {};
|
|
210
222
|
mutationEntries.forEach(entry => {
|
|
223
|
+
if (Stdlib_Option.getOr(entry.systemCallable, false)) {
|
|
224
|
+
entry.fieldNames.forEach(fieldName => {
|
|
225
|
+
iamFields[fieldName] = true;
|
|
226
|
+
});
|
|
227
|
+
}
|
|
211
228
|
let match = entry.authorization;
|
|
212
229
|
if (match !== undefined) {
|
|
213
230
|
let group = match.group;
|
|
@@ -232,6 +249,10 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
232
249
|
});
|
|
233
250
|
let queryAuthMap = {};
|
|
234
251
|
queryEntries.forEach(entry => {
|
|
252
|
+
if (Stdlib_Option.getOr(entry.systemCallable, false)) {
|
|
253
|
+
iamFields[entry.singleFieldName] = true;
|
|
254
|
+
iamFields[entry.listFieldName] = true;
|
|
255
|
+
}
|
|
235
256
|
let match = entry.authorization;
|
|
236
257
|
if (match !== undefined) {
|
|
237
258
|
let group = match.group;
|
|
@@ -255,7 +276,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
255
276
|
let augmentedMutations = parts.mutations.map(field => {
|
|
256
277
|
let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
|
|
257
278
|
let groups = mutationAuthMap[fieldName];
|
|
258
|
-
if (
|
|
279
|
+
if (Stdlib_Option.getOr(iamFields[fieldName], false)) {
|
|
280
|
+
return field + `\n ` + _formatDualAuthDirective(groups);
|
|
281
|
+
} else if (groups !== undefined) {
|
|
259
282
|
return field + `\n ` + _formatGroupsDirective(groups);
|
|
260
283
|
} else {
|
|
261
284
|
return field;
|
|
@@ -264,7 +287,9 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
264
287
|
let augmentedQueries = parts.queries.map(field => {
|
|
265
288
|
let fieldName = GraphQL_Stitcher$ReventlessCore.extractLeadingName(field);
|
|
266
289
|
let groups = queryAuthMap[fieldName];
|
|
267
|
-
if (
|
|
290
|
+
if (Stdlib_Option.getOr(iamFields[fieldName], false)) {
|
|
291
|
+
return field + ` ` + _formatDualAuthDirective(groups);
|
|
292
|
+
} else if (groups !== undefined) {
|
|
268
293
|
return field + ` ` + _formatGroupsDirective(groups);
|
|
269
294
|
} else {
|
|
270
295
|
return field;
|
|
@@ -294,10 +319,23 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
294
319
|
};
|
|
295
320
|
}
|
|
296
321
|
|
|
297
|
-
function injectAwsAuthAll(fragment, group) {
|
|
322
|
+
function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
|
|
323
|
+
let iamFieldNames = iamFieldNamesOpt !== undefined ? iamFieldNamesOpt : [];
|
|
298
324
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
|
|
299
|
-
let augmentedMutations = parts.mutations.map(field =>
|
|
300
|
-
|
|
325
|
+
let augmentedMutations = parts.mutations.map(field => {
|
|
326
|
+
if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
|
|
327
|
+
return field + `\n ` + _formatDualAuthDirective([group]);
|
|
328
|
+
} else {
|
|
329
|
+
return field + `\n @aws_auth(cognito_groups: ["` + group + `"])`;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
let augmentedQueries = parts.queries.map(field => {
|
|
333
|
+
if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
|
|
334
|
+
return field + ` ` + _formatDualAuthDirective([group]);
|
|
335
|
+
} else {
|
|
336
|
+
return field + ` @aws_auth(cognito_groups: ["` + group + `"])`;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
301
339
|
let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n @aws_auth(cognito_groups: ["` + group + `"])`);
|
|
302
340
|
let encoded = JSON.stringify(Object.fromEntries([
|
|
303
341
|
[
|
|
@@ -359,7 +397,7 @@ function generateFragment(mutationEntries, queryEntries) {
|
|
|
359
397
|
}
|
|
360
398
|
|
|
361
399
|
function updateSchema(api, baseFragment, pluginFragments) {
|
|
362
|
-
let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin");
|
|
400
|
+
let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
363
401
|
let sdl = GraphQL_Stitcher$ReventlessCore.stitch(augmentedBaseFragment, pluginFragments);
|
|
364
402
|
let resultPromise = {
|
|
365
403
|
contents: Promise.resolve()
|
|
@@ -387,6 +425,7 @@ export {
|
|
|
387
425
|
getClient,
|
|
388
426
|
_permissionToCognitoGroups,
|
|
389
427
|
_formatGroupsDirective,
|
|
428
|
+
_formatDualAuthDirective,
|
|
390
429
|
injectAwsAuth,
|
|
391
430
|
injectAwsAuthAll,
|
|
392
431
|
makeApiResource,
|
|
@@ -21,7 +21,7 @@ module Make = (HooksConfig: ReventlessCore.Plugin_Helpers.HooksConfig) => {
|
|
|
21
21
|
CommandTopicRemoteChannel.SQS,
|
|
22
22
|
HeartbeatRunner.CloudwatchEvents,
|
|
23
23
|
PluginRuntime_Builder.Make(EventCollectorChannel),
|
|
24
|
-
DcbEventLogStorage.
|
|
24
|
+
DcbEventLogStorage.Selectable,
|
|
25
25
|
EventTopicPublisher.DynamoDbStream,
|
|
26
26
|
CommandTopicChannel.SQS_Sync,
|
|
27
27
|
CommandTopicChannel.SQS_Async,
|
|
@@ -4,13 +4,13 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
4
4
|
import * as Pulumi from "@pulumi/pulumi";
|
|
5
5
|
import * as AppSync_Adapter$ReventlessAws from "./Api/AppSync_Adapter.res.mjs";
|
|
6
6
|
import * as Plugin_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Builder.res.mjs";
|
|
7
|
+
import * as DcbEventLogStorage$ReventlessAws from "../adapter/DcbEventLog/DcbEventLogStorage.res.mjs";
|
|
7
8
|
import * as Util_ResourceNaming$ReventlessAws from "../util/Util_ResourceNaming.res.mjs";
|
|
8
9
|
import * as QueryEngine_DynamoDb$ReventlessAws from "../adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs";
|
|
9
10
|
import * as PluginRuntime_Builder$ReventlessAws from "../plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
10
11
|
import * as PluginRuntimeOperations$ReventlessAws from "../plugin/runtime/PluginRuntimeOperations.res.mjs";
|
|
11
12
|
import * as EventCollectorChannel_SQS$ReventlessAws from "../adapter/EventCollector/EventCollectorChannel_SQS.res.mjs";
|
|
12
13
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
13
|
-
import * as DcbEventLogStorage_DynamoDb$ReventlessAws from "../adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs";
|
|
14
14
|
import * as CommandTopicChannel_SQS_Sync$ReventlessAws from "../adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs";
|
|
15
15
|
import * as CommandTopicChannel_SQS_Async$ReventlessAws from "../adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs";
|
|
16
16
|
import * as CommandTopicRemoteChannel_SQS$ReventlessAws from "../adapter/CommandTopic/CommandTopicRemoteChannel_SQS.res.mjs";
|
|
@@ -44,7 +44,7 @@ function Make(HooksConfig) {
|
|
|
44
44
|
})(CommandTopicRemoteChannel_SQS$ReventlessAws)(HeartbeatRunner_CloudWatchEvents$ReventlessAws)(PluginRuntime_Builder$ReventlessAws.Make({
|
|
45
45
|
make: EventCollectorChannel_SQS$ReventlessAws.make,
|
|
46
46
|
connect: EventCollectorChannel_SQS$ReventlessAws.connect
|
|
47
|
-
}))(
|
|
47
|
+
}))(DcbEventLogStorage$ReventlessAws.Selectable)(EventTopicPublisher_DynamoDbStream$ReventlessAws)({
|
|
48
48
|
make: CommandTopicChannel_SQS_Sync$ReventlessAws.make
|
|
49
49
|
})({
|
|
50
50
|
make: CommandTopicChannel_SQS_Async$ReventlessAws.make
|
|
@@ -66,7 +66,7 @@ function forCommandTopic(param, connect, memorySizeOpt, timeoutOpt, param$1, par
|
|
|
66
66
|
});
|
|
67
67
|
envVars["HANDLER_CONFIG"] = handlerConfigJson;
|
|
68
68
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs", {}, undefined);
|
|
69
|
-
return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts));
|
|
69
|
+
return connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function finish() {
|
|
@@ -196,6 +196,26 @@ module Make = (
|
|
|
196
196
|
let channelParts: Util.SQS.channelParts = Obj.magic(channel.parts)
|
|
197
197
|
let queue = channelParts.queue
|
|
198
198
|
|
|
199
|
+
// B2.3d: if this plugin's DCB log is Postgres-backed, hand its EventCollector
|
|
200
|
+
// SQS queue to the change-feed relay registry. `resource.name` is the plugin
|
|
201
|
+
// component name (`<plugin>Plugin`), so the canonical log_name is
|
|
202
|
+
// `<plugin>DcbEventLog` — the key DcbEventLogStorage_Postgres registered under.
|
|
203
|
+
if DcbBackend.isPostgres() {
|
|
204
|
+
switch resource.name {
|
|
205
|
+
| Some(compName) =>
|
|
206
|
+
let pluginName =
|
|
207
|
+
compName->String.endsWith("Plugin")
|
|
208
|
+
? compName->String.slice(~start=0, ~end=compName->String.length - 6)
|
|
209
|
+
: compName
|
|
210
|
+
DcbBackend.attachCollectorQueue(
|
|
211
|
+
~logName=pluginName ++ "DcbEventLog",
|
|
212
|
+
~url=queue.id,
|
|
213
|
+
~arn=queue.arn,
|
|
214
|
+
)
|
|
215
|
+
| None => ()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
199
219
|
// Pull the per-EventCollector context registered by Plugin_Helpers.connect
|
|
200
220
|
// (plugins) or synthesise an admin one if none registered.
|
|
201
221
|
let context: ReventlessCore.Plugin_Helpers.eventCollectorContext =
|
|
@@ -7,6 +7,7 @@ import * as Pulumi from "@pulumi/pulumi";
|
|
|
7
7
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
8
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
9
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
10
|
+
import * as DcbBackend$ReventlessAws from "../../adapter/DcbEventLog/DcbBackend.res.mjs";
|
|
10
11
|
import * as Heartbeat$ReventlessCore from "@reventlessdev/reventless-core/src/components/Heartbeat/Heartbeat.res.mjs";
|
|
11
12
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
13
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
@@ -166,6 +167,13 @@ function Make(EventCollectorChannel) {
|
|
|
166
167
|
let channel = eventCollector.channel;
|
|
167
168
|
let channelParts = channel.parts;
|
|
168
169
|
let queue = channelParts.queue;
|
|
170
|
+
if (DcbBackend$ReventlessAws.isPostgres()) {
|
|
171
|
+
let compName = resource.__name;
|
|
172
|
+
if (compName !== undefined) {
|
|
173
|
+
let pluginName = compName.endsWith("Plugin") ? compName.slice(0, compName.length - 6 | 0) : compName;
|
|
174
|
+
DcbBackend$ReventlessAws.attachCollectorQueue(pluginName + "DcbEventLog", queue.id, queue.arn);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
169
177
|
let ctx = Plugin_Helpers$ReventlessCore.eventCollectorContextRef.contents[name];
|
|
170
178
|
let context = ctx !== undefined ? ctx : synthesizeAdminContext();
|
|
171
179
|
let envVars = {};
|
|
@@ -300,7 +308,7 @@ function Make(EventCollectorChannel) {
|
|
|
300
308
|
});
|
|
301
309
|
let codeOutput = bundleOutput.apply(b => b.code);
|
|
302
310
|
let sourceCodeHashOutput = bundleOutput.apply(b => b.sourceCodeHash);
|
|
303
|
-
let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, codeOutput, sourceCodeHashOutput, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts);
|
|
311
|
+
let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, codeOutput, sourceCodeHashOutput, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts);
|
|
304
312
|
EventCollectorChannel.connect(name, [{
|
|
305
313
|
channel: eventCollector.channel,
|
|
306
314
|
eventTopics: eventTopics,
|
|
@@ -422,7 +430,7 @@ function Make(EventCollectorChannel) {
|
|
|
422
430
|
envVars["PLUGIN_ID"] = Pulumi.output(hbConfig.pluginId);
|
|
423
431
|
envVars["HEARTBEAT_TIMEOUT"] = Pulumi.output(hbConfig.heartbeatTimeout.toString());
|
|
424
432
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/HeartbeatEntryPoint.mjs", {}, undefined);
|
|
425
|
-
connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, opts));
|
|
433
|
+
connect(RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, memorySize, timeout, undefined, undefined, undefined, undefined, undefined, opts));
|
|
426
434
|
};
|
|
427
435
|
let forDcbCommandTopic = (param, connect, $staropt$star, $staropt$star$1, dcbCommandTopic) => {
|
|
428
436
|
$staropt$star !== undefined;
|
|
@@ -347,6 +347,146 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
347
347
|
})
|
|
348
348
|
})
|
|
349
349
|
|
|
350
|
+
// ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
|
|
351
|
+
//
|
|
352
|
+
// A field flagged `systemCallable` must be reachable by BOTH the console UI
|
|
353
|
+
// (Cognito) and the deploy-time SigV4 system caller (AWS_IAM). It switches from
|
|
354
|
+
// the single-mode `@aws_auth(...)` form to the multi-auth
|
|
355
|
+
// `@aws_cognito_user_pools(...) @aws_iam` form.
|
|
356
|
+
|
|
357
|
+
describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
358
|
+
let makeFragment = (mutationFields, queryFields) =>
|
|
359
|
+
ReventlessCore.GraphQL_Stitcher.encode({
|
|
360
|
+
types: [],
|
|
361
|
+
mutations: mutationFields,
|
|
362
|
+
queries: queryFields,
|
|
363
|
+
subscriptions: [],
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
testSync("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
367
|
+
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
368
|
+
fieldNames: ["p_Sync"],
|
|
369
|
+
commandSchema: S.unknown,
|
|
370
|
+
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
|
|
371
|
+
systemCallable: true,
|
|
372
|
+
}
|
|
373
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], [])
|
|
374
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
|
|
375
|
+
let m = decodeFragment(aug).mutations->Array.getUnsafe(0)
|
|
376
|
+
expect(m)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
377
|
+
expect(m)->toContain("@aws_iam")
|
|
378
|
+
// Must NOT emit the single-mode form, which does not admit IAM.
|
|
379
|
+
expect(m)->not_->toContain("@aws_auth")
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
testSync("systemCallable mutation without a group restriction stays open to Cognito + IAM", () => {
|
|
383
|
+
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
384
|
+
fieldNames: ["p_Sync"],
|
|
385
|
+
commandSchema: S.unknown,
|
|
386
|
+
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowAuthenticated)]),
|
|
387
|
+
systemCallable: true,
|
|
388
|
+
}
|
|
389
|
+
let frag = makeFragment(["p_Sync(id: ID!): String"], [])
|
|
390
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
|
|
391
|
+
let m = decodeFragment(aug).mutations->Array.getUnsafe(0)
|
|
392
|
+
// Bare @aws_cognito_user_pools (any authenticated Cognito user) + IAM.
|
|
393
|
+
expect(m)->toContain("@aws_cognito_user_pools @aws_iam")
|
|
394
|
+
expect(m)->not_->toContain("cognito_groups")
|
|
395
|
+
expect(m)->not_->toContain("@aws_auth")
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
testSync("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
|
|
399
|
+
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
400
|
+
fieldNames: ["p_Sync", "p_Other"],
|
|
401
|
+
commandSchema: S.unknown,
|
|
402
|
+
fieldPermissions: Dict.fromArray([
|
|
403
|
+
("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
|
|
404
|
+
("p_Other", Reventless.Authorization.AllowGroups(["Admin"])),
|
|
405
|
+
]),
|
|
406
|
+
// systemCallable applies to every field in the entry; split the fields so only
|
|
407
|
+
// p_Sync opts in.
|
|
408
|
+
systemCallable: false,
|
|
409
|
+
}
|
|
410
|
+
let iamEntry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
411
|
+
fieldNames: ["p_Sync"],
|
|
412
|
+
commandSchema: S.unknown,
|
|
413
|
+
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
|
|
414
|
+
systemCallable: true,
|
|
415
|
+
}
|
|
416
|
+
let frag = makeFragment(["p_Sync(id: ID!): String", "p_Other(id: ID!): String"], [])
|
|
417
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry, iamEntry], ~queryEntries=[])
|
|
418
|
+
let parts = decodeFragment(aug)
|
|
419
|
+
switch parts.mutations->Array.find(f => f->String.includes("p_Other")) {
|
|
420
|
+
| Some(other) =>
|
|
421
|
+
expect(other)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
|
|
422
|
+
expect(other)->not_->toContain("@aws_iam")
|
|
423
|
+
| None => JsError.throwWithMessage("missing p_Other field")
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
testSync("systemCallable query applies dual-auth to BOTH single and list fields", () => {
|
|
428
|
+
let entry: ReventlessInfra.Api.querySchemaEntry = {
|
|
429
|
+
singleFieldName: "p_Item",
|
|
430
|
+
listFieldName: "p_Items",
|
|
431
|
+
returnTypeName: "p_Item",
|
|
432
|
+
stateSchema: S.unknown,
|
|
433
|
+
authorization: None,
|
|
434
|
+
permission: Reventless.Authorization.AllowGroups(["Admin"]),
|
|
435
|
+
systemCallable: true,
|
|
436
|
+
}
|
|
437
|
+
let frag = makeFragment(
|
|
438
|
+
[],
|
|
439
|
+
["p_Item(id: ID!): Item", "p_Items(first: Int, after: String): ItemConnection!"],
|
|
440
|
+
)
|
|
441
|
+
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[], ~queryEntries=[entry])
|
|
442
|
+
let parts = decodeFragment(aug)
|
|
443
|
+
switch (
|
|
444
|
+
parts.queries->Array.find(f => f->String.includes("p_Item(")),
|
|
445
|
+
parts.queries->Array.find(f => f->String.includes("p_Items(")),
|
|
446
|
+
) {
|
|
447
|
+
| (Some(single), Some(list)) =>
|
|
448
|
+
expect(single)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
449
|
+
expect(single)->toContain("@aws_iam")
|
|
450
|
+
expect(list)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
451
|
+
expect(list)->toContain("@aws_iam")
|
|
452
|
+
| _ => JsError.throwWithMessage("missing query fields")
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
describe("AppSync_Adapter.injectAwsAuthAll — ~iamFieldNames", () => {
|
|
458
|
+
let baseFragment = ReventlessCore.AdminApi.baseFragment(~cloner=true)
|
|
459
|
+
|
|
460
|
+
testSync("marks only the named field dual-auth, leaving others single-mode", () => {
|
|
461
|
+
// Pick a real mutation field name from the admin base fragment.
|
|
462
|
+
let firstMutationName =
|
|
463
|
+
decodeFragment(baseFragment).mutations
|
|
464
|
+
->Array.getUnsafe(0)
|
|
465
|
+
->ReventlessCore.GraphQL_Stitcher.extractLeadingName
|
|
466
|
+
let augmented =
|
|
467
|
+
AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin", ~iamFieldNames=[firstMutationName])
|
|
468
|
+
let parts = decodeFragment(augmented)
|
|
469
|
+
parts.mutations->Array.forEach(field => {
|
|
470
|
+
let name = ReventlessCore.GraphQL_Stitcher.extractLeadingName(field)
|
|
471
|
+
if name == firstMutationName {
|
|
472
|
+
expect(field)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"])`)
|
|
473
|
+
expect(field)->toContain("@aws_iam")
|
|
474
|
+
expect(field)->not_->toContain("@aws_auth")
|
|
475
|
+
} else {
|
|
476
|
+
expect(field)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
|
|
477
|
+
expect(field)->not_->toContain("@aws_iam")
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
testSync("default (no ~iamFieldNames) leaves every field single-mode @aws_auth", () => {
|
|
483
|
+
let augmented = AppSync_Adapter.injectAwsAuthAll(baseFragment, ~group="Admin")
|
|
484
|
+
let parts = decodeFragment(augmented)
|
|
485
|
+
parts.mutations->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
|
|
486
|
+
parts.queries->Array.forEach(field => expect(field)->not_->toContain("@aws_iam"))
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
|
|
350
490
|
describe("Split mode — empty base fragment", () => {
|
|
351
491
|
testSync("empty stitcher encode produces fragment with no fields", () => {
|
|
352
492
|
let emptyFragment = ReventlessCore.GraphQL_Stitcher.encode({
|