@reventlessdev/reventless-aws 3.0.0-alpha.395 → 3.0.0-alpha.396
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 +8 -0
- package/package.json +7 -7
- package/scripts/DeployApp.res +7 -1
- package/scripts/DeployApp.res.mjs +1 -1
- package/scripts/ProvisionAccounts.res +89 -4
- package/scripts/ProvisionAccounts.res.mjs +101 -5
- package/scripts/ProvisionIdentity.res +42 -18
- package/scripts/ProvisionIdentity.res.mjs +40 -26
- package/src/Platform.res +47 -10
- package/src/Platform.res.mjs +44 -10
- package/src/Platform_Stack.res +17 -0
- package/src/Platform_Stack.res.mjs +7 -1
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +16 -7
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +16 -3
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +59 -9
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +53 -6
- package/src/adapter/Auth/Auth_Cognito.res +13 -2
- package/src/adapter/Auth/Auth_Cognito.res.mjs +9 -2
- package/src/adapter/Auth/Auth_IdentityStore.res +52 -0
- package/src/adapter/Auth/Auth_IdentityStore.res.mjs +46 -0
- package/src/adapter/Auth/Auth_IdentityStore_Schema.res +39 -0
- package/src/adapter/Auth/Auth_IdentityStore_Schema.res.mjs +35 -0
- package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res +251 -0
- package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res.mjs +300 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +7 -4
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +2 -2
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +80 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +53 -2
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res +2 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +23 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +29 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res +40 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +59 -8
- package/tests/Auth_CognitoTest.res +62 -0
- package/tests/Auth_CognitoTest.res.mjs +98 -0
- package/tests/Auth_IdentityStore_SchemaTest.res +35 -0
- package/tests/Auth_IdentityStore_SchemaTest.res.mjs +43 -0
- package/tests/IdentityProvider_Cognito_BackendTest.res +314 -0
- package/tests/IdentityProvider_Cognito_BackendTest.res.mjs +442 -0
- package/tests/ProvisionAccountsTest.res.mjs +1 -1
|
@@ -13,7 +13,6 @@ import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/compon
|
|
|
13
13
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
14
14
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
15
15
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
16
|
-
import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
|
|
17
16
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
18
17
|
import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
|
|
19
18
|
import * as Messaging_Ses_Backend$ReventlessAws from "../Messaging/Messaging_Ses_Backend.res.mjs";
|
|
@@ -26,6 +25,7 @@ import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPo
|
|
|
26
25
|
import * as Geocoder_AwsLocation_Backend$ReventlessAws from "../Geocoder/Geocoder_AwsLocation_Backend.res.mjs";
|
|
27
26
|
import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
28
27
|
import * as QueryDbStorage_DynamoDb_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
|
|
28
|
+
import * as IdentityProvider_Cognito_Backend$ReventlessAws from "../Identity/IdentityProvider_Cognito_Backend.res.mjs";
|
|
29
29
|
import * as OutboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
|
|
30
30
|
import * as ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws from "../ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs";
|
|
31
31
|
|
|
@@ -275,7 +275,7 @@ function capabilities() {
|
|
|
275
275
|
return {
|
|
276
276
|
geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined),
|
|
277
277
|
messaging: messagingEmailProvider(),
|
|
278
|
-
identityProvider:
|
|
278
|
+
identityProvider: IdentityProvider_Cognito_Backend$ReventlessAws.provider(Stdlib_Option.getOr(process.env["IDENTITY_PROVIDER_ID"], ""), Stdlib_Option.getOr(process.env["IDENTITY_STORE_TABLE"], "")),
|
|
279
279
|
secrets: Secrets_Node_Backend$ReventlessCore.provider
|
|
280
280
|
};
|
|
281
281
|
}
|
|
@@ -54,6 +54,9 @@ type sliceInfo = {
|
|
|
54
54
|
// The schedules store's table, empty for a slice that maps no `Clock`. Granted to
|
|
55
55
|
// the Lambda with the to-do tables, and named in the slice's handler config.
|
|
56
56
|
schedulesResources: array<ReventlessInfra.Adapter.resource>,
|
|
57
|
+
// The capabilities the slice's spec declares. Read by grants that must not be
|
|
58
|
+
// handed to every slice on the shared Lambda for one slice's sake.
|
|
59
|
+
capabilityNeeds: array<Reventless.CapabilityNeed.t>,
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
let bundledInfos: dict<sliceInfo> = Dict.make()
|
|
@@ -103,6 +106,7 @@ let registerAutomationSlice = (
|
|
|
103
106
|
~commandTargetName=?,
|
|
104
107
|
~schedule=?,
|
|
105
108
|
~schedulesResources=[],
|
|
109
|
+
~capabilityNeeds=[],
|
|
106
110
|
) =>
|
|
107
111
|
bundledInfos->Dict.set(
|
|
108
112
|
name,
|
|
@@ -118,6 +122,7 @@ let registerAutomationSlice = (
|
|
|
118
122
|
commandTargetName,
|
|
119
123
|
schedule,
|
|
120
124
|
schedulesResources,
|
|
125
|
+
capabilityNeeds,
|
|
121
126
|
},
|
|
122
127
|
)
|
|
123
128
|
|
|
@@ -334,6 +339,80 @@ let makeMessagingGrant = (~runtime: ReventlessCore.Runtime.environment<runtimePa
|
|
|
334
339
|
)
|
|
335
340
|
}
|
|
336
341
|
|
|
342
|
+
/** Whether any slice on the shared Lambda declares the identity provider. The
|
|
343
|
+
grant below is the one that can create and delete accounts, so it follows a
|
|
344
|
+
declaration rather than the pool merely existing — which on AWS it always
|
|
345
|
+
does. */
|
|
346
|
+
let declaresIdentityProvider = (infos: array<sliceInfo>): bool =>
|
|
347
|
+
infos->Array.some(info => info.capabilityNeeds->Array.includes(IdentityProvider))
|
|
348
|
+
|
|
349
|
+
/** The admin calls the Cognito provider makes, on the one pool. */
|
|
350
|
+
let identityProviderPoolActions = [
|
|
351
|
+
"cognito-idp:AdminCreateUser",
|
|
352
|
+
"cognito-idp:AdminGetUser",
|
|
353
|
+
"cognito-idp:AdminSetUserPassword",
|
|
354
|
+
"cognito-idp:AdminAddUserToGroup",
|
|
355
|
+
"cognito-idp:AdminRemoveUserFromGroup",
|
|
356
|
+
"cognito-idp:AdminDeleteUser",
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
// Built like the two grants above: a plain bool decides, the ARNs are Outputs,
|
|
360
|
+
// and the RolePolicy is created at top level rather than inside an `apply`.
|
|
361
|
+
let makeIdentityProviderGrant = (
|
|
362
|
+
~runtime: ReventlessCore.Runtime.environment<runtimeParts>,
|
|
363
|
+
~opts,
|
|
364
|
+
) =>
|
|
365
|
+
if (
|
|
366
|
+
PluginRuntime_Builder.identityProviderProvisioned() &&
|
|
367
|
+
declaresIdentityProvider(bundledInfos->Dict.valuesToArray)
|
|
368
|
+
) {
|
|
369
|
+
let customOpts =
|
|
370
|
+
opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
371
|
+
let {
|
|
372
|
+
poolArn,
|
|
373
|
+
storeArn,
|
|
374
|
+
}: PluginRuntime_Builder.identityProviderResources = PluginRuntime_Builder.identityProvider()
|
|
375
|
+
let policyJson = Pulumi.Output.all2((poolArn, storeArn))->Pulumi.Output.apply(((
|
|
376
|
+
pool,
|
|
377
|
+
store,
|
|
378
|
+
)) => {
|
|
379
|
+
open PulumiAws.PolicyDocument
|
|
380
|
+
PulumiAws.PolicyDocument.make(
|
|
381
|
+
~id="AllAutomationSlicesIdentityProvider",
|
|
382
|
+
~statements=Array.concat(
|
|
383
|
+
[
|
|
384
|
+
{
|
|
385
|
+
sid: "AllowManagingPrincipals",
|
|
386
|
+
effect: Allow,
|
|
387
|
+
actions: Actions(identityProviderPoolActions),
|
|
388
|
+
resources: Resource(pool),
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
// A pool provisioned before the store has none, and its principals
|
|
392
|
+
// keep their `sub`; there is nothing to grant.
|
|
393
|
+
store == ""
|
|
394
|
+
? []
|
|
395
|
+
: [
|
|
396
|
+
{
|
|
397
|
+
sid: "AllowRecordingDomainIds",
|
|
398
|
+
effect: Allow,
|
|
399
|
+
actions: Actions(["dynamodb:PutItem", "dynamodb:DeleteItem"]),
|
|
400
|
+
resources: Resource(store),
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
),
|
|
404
|
+
)->PulumiAws.PolicyDocument.toJsonString
|
|
405
|
+
})
|
|
406
|
+
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
407
|
+
~name="AllAutomationSlicesIdentityProvider",
|
|
408
|
+
~args={
|
|
409
|
+
policy: policyJson->Pulumi.Output.asInput,
|
|
410
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
411
|
+
},
|
|
412
|
+
~opts=customOpts,
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
337
416
|
// The automations' schedules. The Lambda may manage the schedules in the stack's
|
|
338
417
|
// scheduler group and pass the scheduler role to them; the scheduler role may invoke
|
|
339
418
|
// the Lambda; and each deploy invokes the Lambda to reconcile the declared schedules
|
|
@@ -833,6 +912,7 @@ let finishWithDcbEventLog = (
|
|
|
833
912
|
makeAutomationSchedules(~runtime, ~schedulerRoleArn, ~sourceCodeHash, ~opts)
|
|
834
913
|
makeGeocoderGrant(~runtime, ~opts)
|
|
835
914
|
makeMessagingGrant(~runtime, ~opts)
|
|
915
|
+
makeIdentityProviderGrant(~runtime, ~opts)
|
|
836
916
|
| None =>
|
|
837
917
|
log.warn(
|
|
838
918
|
~comp="AutomationSliceRuntime_Builder_Single",
|
|
@@ -56,12 +56,13 @@ function getDcbQueueUrl() {
|
|
|
56
56
|
return dcbQueueUrlRef.contents;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName, schedule, schedulesResourcesOpt) {
|
|
59
|
+
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName, schedule, schedulesResourcesOpt, capabilityNeedsOpt) {
|
|
60
60
|
let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
|
|
61
61
|
let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
|
|
62
62
|
let sourceTopics = sourceTopicsOpt !== undefined ? sourceTopicsOpt : ({});
|
|
63
63
|
let consumesDcbLog = consumesDcbLogOpt !== undefined ? consumesDcbLogOpt : true;
|
|
64
64
|
let schedulesResources = schedulesResourcesOpt !== undefined ? schedulesResourcesOpt : [];
|
|
65
|
+
let capabilityNeeds = capabilityNeedsOpt !== undefined ? capabilityNeedsOpt : [];
|
|
65
66
|
bundledInfos[name] = {
|
|
66
67
|
specModulePath: specModulePath,
|
|
67
68
|
bodyModulePath: bodyModulePath,
|
|
@@ -73,7 +74,8 @@ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackT
|
|
|
73
74
|
consumesDcbLog: consumesDcbLog,
|
|
74
75
|
commandTargetName: commandTargetName,
|
|
75
76
|
schedule: schedule,
|
|
76
|
-
schedulesResources: schedulesResources
|
|
77
|
+
schedulesResources: schedulesResources,
|
|
78
|
+
capabilityNeeds: capabilityNeeds
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -175,6 +177,51 @@ function makeMessagingGrant(runtime, opts) {
|
|
|
175
177
|
}, customOpts);
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
function declaresIdentityProvider(infos) {
|
|
181
|
+
return infos.some(info => info.capabilityNeeds.includes("IdentityProvider"));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let identityProviderPoolActions = [
|
|
185
|
+
"cognito-idp:AdminCreateUser",
|
|
186
|
+
"cognito-idp:AdminGetUser",
|
|
187
|
+
"cognito-idp:AdminSetUserPassword",
|
|
188
|
+
"cognito-idp:AdminAddUserToGroup",
|
|
189
|
+
"cognito-idp:AdminRemoveUserFromGroup",
|
|
190
|
+
"cognito-idp:AdminDeleteUser"
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
function makeIdentityProviderGrant(runtime, opts) {
|
|
194
|
+
if (!(PluginRuntime_Builder$ReventlessAws.identityProviderProvisioned() && declaresIdentityProvider(Object.values(bundledInfos)))) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
198
|
+
let match = PluginRuntime_Builder$ReventlessAws.identityProvider();
|
|
199
|
+
let policyJson = Pulumi.all([
|
|
200
|
+
match.poolArn,
|
|
201
|
+
match.storeArn
|
|
202
|
+
]).apply(param => {
|
|
203
|
+
let store = param[1];
|
|
204
|
+
return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllAutomationSlicesIdentityProvider", [{
|
|
205
|
+
Sid: "AllowManagingPrincipals",
|
|
206
|
+
Effect: "Allow",
|
|
207
|
+
Action: identityProviderPoolActions,
|
|
208
|
+
Resource: param[0]
|
|
209
|
+
}].concat(store === "" ? [] : [{
|
|
210
|
+
Sid: "AllowRecordingDomainIds",
|
|
211
|
+
Effect: "Allow",
|
|
212
|
+
Action: [
|
|
213
|
+
"dynamodb:PutItem",
|
|
214
|
+
"dynamodb:DeleteItem"
|
|
215
|
+
],
|
|
216
|
+
Resource: store
|
|
217
|
+
}])));
|
|
218
|
+
});
|
|
219
|
+
new (Aws.iam.RolePolicy)("AllAutomationSlicesIdentityProvider", {
|
|
220
|
+
policy: policyJson,
|
|
221
|
+
role: runtime.parts.lambdaRole.id
|
|
222
|
+
}, customOpts);
|
|
223
|
+
}
|
|
224
|
+
|
|
178
225
|
function makeAutomationSchedules(runtime, schedulerRoleArn, sourceCodeHash, opts) {
|
|
179
226
|
let name = "AllAutomationSlices";
|
|
180
227
|
let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
@@ -452,6 +499,7 @@ function finishWithDcbEventLog(schedulerRoleArn, dcbEventLog) {
|
|
|
452
499
|
makeAutomationSchedules(runtime, schedulerRoleArn, sourceCodeHash, opts);
|
|
453
500
|
makeGeocoderGrant(runtime, opts);
|
|
454
501
|
makeMessagingGrant(runtime, opts);
|
|
502
|
+
makeIdentityProviderGrant(runtime, opts);
|
|
455
503
|
} else {
|
|
456
504
|
log.warn("AutomationSliceRuntime_Builder_Single", undefined, "finishWithDcbEventLog: DCB EventLog has no parent");
|
|
457
505
|
}
|
|
@@ -483,6 +531,9 @@ export {
|
|
|
483
531
|
makeSweepSchedule,
|
|
484
532
|
makeGeocoderGrant,
|
|
485
533
|
makeMessagingGrant,
|
|
534
|
+
declaresIdentityProvider,
|
|
535
|
+
identityProviderPoolActions,
|
|
536
|
+
makeIdentityProviderGrant,
|
|
486
537
|
makeAutomationSchedules,
|
|
487
538
|
finished,
|
|
488
539
|
finish,
|
|
@@ -62,7 +62,7 @@ function Make(Api) {
|
|
|
62
62
|
let queryDbTableName = tableResource.name;
|
|
63
63
|
let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(sourceNames));
|
|
64
64
|
let consumesDcbLog = sourceNames.length === 0 || sourceNames.some(name => !(name in allEventTopics));
|
|
65
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog, undefined, Spec.schedule, Stdlib_Option.mapOr(Component$ReventlessCore.outputs(as_).schedulesDb, [], db => db.resources));
|
|
65
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog, undefined, Spec.schedule, Stdlib_Option.mapOr(Component$ReventlessCore.outputs(as_).schedulesDb, [], db => db.resources), undefined);
|
|
66
66
|
return as_;
|
|
67
67
|
};
|
|
68
68
|
return {
|
|
@@ -94,6 +94,8 @@ module Make = (
|
|
|
94
94
|
~consumesDcbLog,
|
|
95
95
|
// Only the name: the target Aggregate's CommandTopic does not exist yet.
|
|
96
96
|
~commandTargetName=?Spec.targetName,
|
|
97
|
+
// What decides whether the shared Lambda may create and delete accounts.
|
|
98
|
+
~capabilityNeeds=Spec.capabilityNeeds,
|
|
97
99
|
)
|
|
98
100
|
|
|
99
101
|
ots
|
|
@@ -63,7 +63,7 @@ function Make(Api) {
|
|
|
63
63
|
let declaredSources = Spec.sourceNames;
|
|
64
64
|
let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(declaredSources));
|
|
65
65
|
let consumesDcbLog = declaredSources.length === 0 || declaredSources.some(name => !(name in allEventTopics));
|
|
66
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog, Spec.targetName, undefined, undefined);
|
|
66
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog, Spec.targetName, undefined, undefined, Spec.capabilityNeeds);
|
|
67
67
|
return ots;
|
|
68
68
|
};
|
|
69
69
|
return {
|
|
@@ -91,6 +91,29 @@ let registerMessagingSender = (sender: Pulumi.Output.t<string>) => {
|
|
|
91
91
|
let messagingSender = () => messagingSenderRef.contents
|
|
92
92
|
let messagingProvisioned = () => messagingProvisionedRef.contents
|
|
93
93
|
|
|
94
|
+
/** The pool and the identity store, carried for the grant: a slice making
|
|
95
|
+
principals needs Cognito's admin calls on the one pool and writes on the one
|
|
96
|
+
store. Granted only to a plugin with a slice that declares the need — see
|
|
97
|
+
`AutomationSliceRuntime_Builder_Single.makeIdentityProviderGrant`. */
|
|
98
|
+
type identityProviderResources = {
|
|
99
|
+
poolArn: Pulumi.Output.t<string>,
|
|
100
|
+
storeArn: Pulumi.Output.t<string>,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let identityProviderRef: ref<identityProviderResources> = ref({
|
|
104
|
+
poolArn: Pulumi.Output.make(""),
|
|
105
|
+
storeArn: Pulumi.Output.make(""),
|
|
106
|
+
})
|
|
107
|
+
let identityProviderProvisionedRef = ref(false)
|
|
108
|
+
|
|
109
|
+
let registerIdentityProvider = (resources: identityProviderResources) => {
|
|
110
|
+
identityProviderRef := resources
|
|
111
|
+
identityProviderProvisionedRef := true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let identityProvider = () => identityProviderRef.contents
|
|
115
|
+
let identityProviderProvisioned = () => identityProviderProvisionedRef.contents
|
|
116
|
+
|
|
94
117
|
type sliceModulePaths = {
|
|
95
118
|
specPath: string,
|
|
96
119
|
decisionPath: string,
|
|
@@ -91,6 +91,30 @@ function messagingProvisioned() {
|
|
|
91
91
|
return messagingProvisionedRef.contents;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
let identityProviderRef = {
|
|
95
|
+
contents: {
|
|
96
|
+
poolArn: Pulumi.output(""),
|
|
97
|
+
storeArn: Pulumi.output("")
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let identityProviderProvisionedRef = {
|
|
102
|
+
contents: false
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function registerIdentityProvider(resources) {
|
|
106
|
+
identityProviderRef.contents = resources;
|
|
107
|
+
identityProviderProvisionedRef.contents = true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function identityProvider() {
|
|
111
|
+
return identityProviderRef.contents;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function identityProviderProvisioned() {
|
|
115
|
+
return identityProviderProvisionedRef.contents;
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
let dcbConfigRef = {
|
|
95
119
|
contents: {
|
|
96
120
|
pluginName: "",
|
|
@@ -591,6 +615,11 @@ export {
|
|
|
591
615
|
registerMessagingSender,
|
|
592
616
|
messagingSender,
|
|
593
617
|
messagingProvisioned,
|
|
618
|
+
identityProviderRef,
|
|
619
|
+
identityProviderProvisionedRef,
|
|
620
|
+
registerIdentityProvider,
|
|
621
|
+
identityProvider,
|
|
622
|
+
identityProviderProvisioned,
|
|
594
623
|
dcbConfigRef,
|
|
595
624
|
registeredSliceModulePaths,
|
|
596
625
|
registerStateChangeSliceSpec,
|
|
@@ -171,3 +171,43 @@ describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
171
171
|
)->toEqual(Some(["arn:aws:iam::1:role/Shopper"]))
|
|
172
172
|
})
|
|
173
173
|
})
|
|
174
|
+
|
|
175
|
+
// ── The domain id claim ─────────────────────────────────────────────────────
|
|
176
|
+
//
|
|
177
|
+
// Rides on whatever else the mint adds. The regression line above still holds
|
|
178
|
+
// for every account created before the identity store existed, because such an
|
|
179
|
+
// account has no row and so no `userId` arrives here.
|
|
180
|
+
|
|
181
|
+
describe("Auth_ActiveRoleTrigger_Ops.respond with a domain id", () => {
|
|
182
|
+
testSync("no stored role and a domain id adds the claim and nothing else", () => {
|
|
183
|
+
let input = eventFor(~membership=["Admin", "Shopper"])
|
|
184
|
+
let event = Trigger.respond(~event=input, ~decision=Unchanged, ~userId="u_abc")
|
|
185
|
+
expect(claimOf(event, ReventlessCore.Auth_UserId.claim))->toEqual(Some("u_abc"))
|
|
186
|
+
expect(groupsOf(event))->toEqual(None)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
testSync("narrowing and a domain id carry both", () => {
|
|
190
|
+
let event = Trigger.respond(
|
|
191
|
+
~event=eventFor(~membership=["Admin", "Shopper"]),
|
|
192
|
+
~decision=Narrow({role: "Shopper", membership: ["Admin", "Shopper"]}),
|
|
193
|
+
~userId="u_abc",
|
|
194
|
+
)
|
|
195
|
+
expect(groupsOf(event))->toEqual(Some(["Shopper"]))
|
|
196
|
+
expect((
|
|
197
|
+
claimOf(event, Contract.activeRoleClaim),
|
|
198
|
+
claimOf(event, ReventlessCore.Auth_UserId.claim),
|
|
199
|
+
))->toEqual((Some("Shopper"), Some("u_abc")))
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
testSync("no domain id leaves a narrowed token without the claim", () => {
|
|
203
|
+
let event = Trigger.respond(
|
|
204
|
+
~event=eventFor(~membership=["Admin", "Shopper"]),
|
|
205
|
+
~decision=Narrow({role: "Shopper", membership: ["Admin", "Shopper"]}),
|
|
206
|
+
)
|
|
207
|
+
expect(claimOf(event, ReventlessCore.Auth_UserId.claim))->toEqual(None)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
testPromise("no identity store configured reads as no row", async () =>
|
|
211
|
+
expect(await Trigger.userIdFor(~sub="u-carol", ~table=None))->toEqual(None)
|
|
212
|
+
)
|
|
213
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Auth_UserId$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_UserId.res.mjs";
|
|
4
5
|
import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
|
|
5
6
|
import * as Auth_ActiveRoleTrigger_Ops$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs";
|
|
6
7
|
|
|
@@ -43,7 +44,7 @@ globalThis.describe("the conformance table, minted by the trigger", () => {
|
|
|
43
44
|
let requested = param.requested;
|
|
44
45
|
let membership = param.membership;
|
|
45
46
|
globalThis.test(param.label, () => {
|
|
46
|
-
let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor(membership, undefined, undefined), Auth_ActiveRoleTrigger_Ops$ReventlessAws.decide(membership, requested));
|
|
47
|
+
let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor(membership, undefined, undefined), Auth_ActiveRoleTrigger_Ops$ReventlessAws.decide(membership, requested), undefined);
|
|
47
48
|
globalThis.expect(Stdlib_Option.getOr(groupsOf(event), membership)).toEqual(Stdlib_Option.getOr(expected, membership));
|
|
48
49
|
});
|
|
49
50
|
});
|
|
@@ -84,7 +85,7 @@ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
84
85
|
"Admin",
|
|
85
86
|
"Shopper"
|
|
86
87
|
], undefined, undefined);
|
|
87
|
-
globalThis.expect(Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(input, "Unchanged")).toEqual(input);
|
|
88
|
+
globalThis.expect(Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(input, "Unchanged", undefined)).toEqual(input);
|
|
88
89
|
});
|
|
89
90
|
globalThis.test("narrowing puts the chosen role alone in the group claim", () => {
|
|
90
91
|
let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor([
|
|
@@ -97,7 +98,7 @@ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
97
98
|
"Admin",
|
|
98
99
|
"Shopper"
|
|
99
100
|
]
|
|
100
|
-
});
|
|
101
|
+
}, undefined);
|
|
101
102
|
globalThis.expect(groupsOf(event)).toEqual(["Shopper"]);
|
|
102
103
|
});
|
|
103
104
|
globalThis.test("a narrowed token records the choice and what it gave up", () => {
|
|
@@ -111,7 +112,7 @@ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
111
112
|
"Admin",
|
|
112
113
|
"Shopper"
|
|
113
114
|
]
|
|
114
|
-
});
|
|
115
|
+
}, undefined);
|
|
115
116
|
globalThis.expect([
|
|
116
117
|
claimOf(event, Auth_ActiveRole$ReventlessCore.activeRoleClaim),
|
|
117
118
|
claimOf(event, Auth_ActiveRole$ReventlessCore.availableRolesClaim)
|
|
@@ -125,7 +126,7 @@ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
125
126
|
TAG: "Stale",
|
|
126
127
|
role: "Admin",
|
|
127
128
|
membership: ["Shopper"]
|
|
128
|
-
});
|
|
129
|
+
}, undefined);
|
|
129
130
|
globalThis.expect([
|
|
130
131
|
groupsOf(event),
|
|
131
132
|
claimOf(event, Auth_ActiveRole$ReventlessCore.staleRoleClaim)
|
|
@@ -139,7 +140,7 @@ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
139
140
|
TAG: "Stale",
|
|
140
141
|
role: "Admin",
|
|
141
142
|
membership: ["Shopper"]
|
|
142
|
-
});
|
|
143
|
+
}, undefined);
|
|
143
144
|
globalThis.expect(claimOf(event, Auth_ActiveRole$ReventlessCore.activeRoleClaim)).toEqual(undefined);
|
|
144
145
|
});
|
|
145
146
|
globalThis.test("narrowing carries the caller's IAM roles through untouched", () => {
|
|
@@ -153,7 +154,7 @@ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
153
154
|
"Admin",
|
|
154
155
|
"Shopper"
|
|
155
156
|
]
|
|
156
|
-
});
|
|
157
|
+
}, undefined);
|
|
157
158
|
let details = Stdlib_Option.flatMap(overrideOf(event), c => c.groupOverrideDetails);
|
|
158
159
|
globalThis.expect([
|
|
159
160
|
Stdlib_Option.map(details, d => d.iamRolesToOverride),
|
|
@@ -168,11 +169,61 @@ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
|
|
|
168
169
|
TAG: "Stale",
|
|
169
170
|
role: "Admin",
|
|
170
171
|
membership: ["Shopper"]
|
|
171
|
-
});
|
|
172
|
+
}, undefined);
|
|
172
173
|
globalThis.expect(Stdlib_Option.map(Stdlib_Option.flatMap(overrideOf(event), c => c.groupOverrideDetails), d => d.iamRolesToOverride)).toEqual(["arn:aws:iam::1:role/Shopper"]);
|
|
173
174
|
});
|
|
174
175
|
});
|
|
175
176
|
|
|
177
|
+
globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond with a domain id", () => {
|
|
178
|
+
globalThis.test("no stored role and a domain id adds the claim and nothing else", () => {
|
|
179
|
+
let input = eventFor([
|
|
180
|
+
"Admin",
|
|
181
|
+
"Shopper"
|
|
182
|
+
], undefined, undefined);
|
|
183
|
+
let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(input, "Unchanged", "u_abc");
|
|
184
|
+
globalThis.expect(claimOf(event, Auth_UserId$ReventlessCore.claim)).toEqual("u_abc");
|
|
185
|
+
globalThis.expect(groupsOf(event)).toEqual(undefined);
|
|
186
|
+
});
|
|
187
|
+
globalThis.test("narrowing and a domain id carry both", () => {
|
|
188
|
+
let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor([
|
|
189
|
+
"Admin",
|
|
190
|
+
"Shopper"
|
|
191
|
+
], undefined, undefined), {
|
|
192
|
+
TAG: "Narrow",
|
|
193
|
+
role: "Shopper",
|
|
194
|
+
membership: [
|
|
195
|
+
"Admin",
|
|
196
|
+
"Shopper"
|
|
197
|
+
]
|
|
198
|
+
}, "u_abc");
|
|
199
|
+
globalThis.expect(groupsOf(event)).toEqual(["Shopper"]);
|
|
200
|
+
globalThis.expect([
|
|
201
|
+
claimOf(event, Auth_ActiveRole$ReventlessCore.activeRoleClaim),
|
|
202
|
+
claimOf(event, Auth_UserId$ReventlessCore.claim)
|
|
203
|
+
]).toEqual([
|
|
204
|
+
"Shopper",
|
|
205
|
+
"u_abc"
|
|
206
|
+
]);
|
|
207
|
+
});
|
|
208
|
+
globalThis.test("no domain id leaves a narrowed token without the claim", () => {
|
|
209
|
+
let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor([
|
|
210
|
+
"Admin",
|
|
211
|
+
"Shopper"
|
|
212
|
+
], undefined, undefined), {
|
|
213
|
+
TAG: "Narrow",
|
|
214
|
+
role: "Shopper",
|
|
215
|
+
membership: [
|
|
216
|
+
"Admin",
|
|
217
|
+
"Shopper"
|
|
218
|
+
]
|
|
219
|
+
}, undefined);
|
|
220
|
+
globalThis.expect(claimOf(event, Auth_UserId$ReventlessCore.claim)).toEqual(undefined);
|
|
221
|
+
});
|
|
222
|
+
globalThis.test("no identity store configured reads as no row", async () => {
|
|
223
|
+
globalThis.expect(await Auth_ActiveRoleTrigger_Ops$ReventlessAws.userIdFor("u-carol", undefined)).toEqual(undefined);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
176
227
|
let Trigger;
|
|
177
228
|
|
|
178
229
|
let Contract;
|
|
@@ -186,3 +186,65 @@ describe("Auth_Cognito.fromAppSyncIdentity", () => {
|
|
|
186
186
|
}
|
|
187
187
|
})
|
|
188
188
|
})
|
|
189
|
+
|
|
190
|
+
// ── The domain id claim ─────────────────────────────────────────────────────
|
|
191
|
+
//
|
|
192
|
+
// Both arms, on both entry points. The second arm is the one that will look
|
|
193
|
+
// redundant to a later reader and is not: every `@owner` field written before
|
|
194
|
+
// the identity store existed holds a `sub`, and it keeps matching only while a
|
|
195
|
+
// token without the claim produces exactly that `sub`.
|
|
196
|
+
|
|
197
|
+
describe("Auth_Cognito and the domain id claim", () => {
|
|
198
|
+
let claimName = ReventlessCore.Auth_UserId.claim
|
|
199
|
+
|
|
200
|
+
testAsync("authenticate: a token carrying the claim yields it, not the sub", async () => {
|
|
201
|
+
let token = buildJwt(
|
|
202
|
+
Dict.fromArray([
|
|
203
|
+
("sub", JSON.Encode.string("c3741234-aaaa")),
|
|
204
|
+
(claimName, JSON.Encode.string("u_0123")),
|
|
205
|
+
]),
|
|
206
|
+
)
|
|
207
|
+
switch await Auth_Cognito.authenticate(buildContext([("authorization", "Bearer " ++ token)])) {
|
|
208
|
+
| Authenticated(identity) => expect(identity.userId)->toBe("u_0123")
|
|
209
|
+
| _ => JsError.throwWithMessage("expected Authenticated")
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
testAsync("authenticate: a token without the claim yields exactly the sub", async () => {
|
|
214
|
+
let token = buildJwt(Dict.fromArray([("sub", JSON.Encode.string("c3741234-aaaa"))]))
|
|
215
|
+
switch await Auth_Cognito.authenticate(buildContext([("authorization", "Bearer " ++ token)])) {
|
|
216
|
+
| Authenticated(identity) => expect(identity.userId)->toBe("c3741234-aaaa")
|
|
217
|
+
| _ => JsError.throwWithMessage("expected Authenticated")
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
testSync("fromAppSyncIdentity: the claim wins over the sub", () => {
|
|
222
|
+
let id: Auth_Cognito.appSyncIdentity = {
|
|
223
|
+
sub: "c3741234-aaaa",
|
|
224
|
+
claims: Dict.fromArray([(claimName, JSON.Encode.string("u_0123"))]),
|
|
225
|
+
}
|
|
226
|
+
switch Auth_Cognito.fromAppSyncIdentity(Some(id)) {
|
|
227
|
+
| Authenticated(identity) => expect(identity.userId)->toBe("u_0123")
|
|
228
|
+
| _ => JsError.throwWithMessage("expected Authenticated")
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
testSync("fromAppSyncIdentity: no claim is exactly the sub", () => {
|
|
233
|
+
let id: Auth_Cognito.appSyncIdentity = {sub: "c3741234-aaaa", claims: Dict.make()}
|
|
234
|
+
switch Auth_Cognito.fromAppSyncIdentity(Some(id)) {
|
|
235
|
+
| Authenticated(identity) => expect(identity.userId)->toBe("c3741234-aaaa")
|
|
236
|
+
| _ => JsError.throwWithMessage("expected Authenticated")
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
testSync("an empty claim is treated as absent", () => {
|
|
241
|
+
let id: Auth_Cognito.appSyncIdentity = {
|
|
242
|
+
sub: "c3741234-aaaa",
|
|
243
|
+
claims: Dict.fromArray([(claimName, JSON.Encode.string(""))]),
|
|
244
|
+
}
|
|
245
|
+
switch Auth_Cognito.fromAppSyncIdentity(Some(id)) {
|
|
246
|
+
| Authenticated(identity) => expect(identity.userId)->toBe("c3741234-aaaa")
|
|
247
|
+
| _ => JsError.throwWithMessage("expected Authenticated")
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
})
|