@reventlessdev/reventless-aws 3.0.0-alpha.394 → 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 +22 -0
- package/package.json +8 -8
- 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
- package/tests/Util_ShellConfigTest.res.mjs +1 -22
package/src/Platform.res
CHANGED
|
@@ -1287,9 +1287,9 @@ module MakeWithConfig = (
|
|
|
1287
1287
|
// door: unlike geocoding, a browser has no business sending mail from the
|
|
1288
1288
|
// deployment's identity. Unset ⇒ no export, no grant.
|
|
1289
1289
|
messagingSender?: ReventlessInfra.Platform.messagingSender,
|
|
1290
|
-
// Ignored here: an in-process provider cannot reach a Lambda
|
|
1291
|
-
// pool
|
|
1292
|
-
//
|
|
1290
|
+
// Ignored here: an in-process provider cannot reach a Lambda. Every AWS
|
|
1291
|
+
// platform has a pool, and a plugin whose slice declares the need is handed
|
|
1292
|
+
// a provider over it with no root opt-in — see `deployPlugin`.
|
|
1293
1293
|
identityProvider?: Reventless.IdentityProvider.t,
|
|
1294
1294
|
// Optional object store for direct-to-S3 uploads, as returned by
|
|
1295
1295
|
// `Capability_ObjectStore_S3.make`. When set, the deploy registers it with
|
|
@@ -2760,6 +2760,37 @@ module MakeWithConfig = (
|
|
|
2760
2760
|
}
|
|
2761
2761
|
PluginRuntime_Builder.registerCapabilityEnv("MESSAGING_EMAIL_PROVIDER", messagingEmailProvider)
|
|
2762
2762
|
|
|
2763
|
+
// The pool and its identity store, read across the same two halves. Every
|
|
2764
|
+
// AWS platform has a pool, so unlike the two above there is no root opt-in:
|
|
2765
|
+
// what a plugin declares decides whether its slices are granted the admin
|
|
2766
|
+
// calls, in the slice runtime's finalizer.
|
|
2767
|
+
let platformOutput = (name: string, ~local: unit => Pulumi.Output.t<string>) =>
|
|
2768
|
+
switch platformStackRef {
|
|
2769
|
+
| Some(stackRef) =>
|
|
2770
|
+
(
|
|
2771
|
+
stackRef->Pulumi.StackReference.getOutput(name): Pulumi.Output.t<option<string>>
|
|
2772
|
+
)->Pulumi.Output.apply(o => o->Option.getOr(""))
|
|
2773
|
+
| None => local()
|
|
2774
|
+
}
|
|
2775
|
+
let identityProviderId = platformOutput("identityProviderId", ~local=() =>
|
|
2776
|
+
Platform_Stack.resolveCognitoUserPool().poolId
|
|
2777
|
+
)
|
|
2778
|
+
PluginRuntime_Builder.registerCapabilityEnv("IDENTITY_PROVIDER_ID", identityProviderId)
|
|
2779
|
+
PluginRuntime_Builder.registerCapabilityEnv(
|
|
2780
|
+
"IDENTITY_STORE_TABLE",
|
|
2781
|
+
platformOutput("identityStore", ~local=() =>
|
|
2782
|
+
Platform_Stack.resolveCognitoUserPool().identityTable.name
|
|
2783
|
+
),
|
|
2784
|
+
)
|
|
2785
|
+
PluginRuntime_Builder.registerIdentityProvider({
|
|
2786
|
+
poolArn: platformOutput("identityProviderArn", ~local=() =>
|
|
2787
|
+
Platform_Stack.resolveCognitoUserPool().poolArn
|
|
2788
|
+
),
|
|
2789
|
+
storeArn: platformOutput("identityStoreArn", ~local=() =>
|
|
2790
|
+
Platform_Stack.resolveCognitoUserPool().identityTable.arn
|
|
2791
|
+
),
|
|
2792
|
+
})
|
|
2793
|
+
|
|
2763
2794
|
module P = unpack(plugin)
|
|
2764
2795
|
let pluginComponent = P.make()
|
|
2765
2796
|
ReventlessCore.Plugin_Helpers.clearOffload()
|
|
@@ -2895,10 +2926,17 @@ module MakeWithConfig = (
|
|
|
2895
2926
|
stackRef->Pulumi.StackReference.getOutput("objectStores"),
|
|
2896
2927
|
geocoderPlaceIndex,
|
|
2897
2928
|
messagingEmailSender,
|
|
2929
|
+
identityProviderId,
|
|
2898
2930
|
)
|
|
2899
|
-
->Pulumi.Output.
|
|
2931
|
+
->Pulumi.Output.all5
|
|
2900
2932
|
->Pulumi.Output.apply((
|
|
2901
|
-
(structure, objectStores, placeIndex, emailSender): (
|
|
2933
|
+
(structure, objectStores, placeIndex, emailSender, poolId): (
|
|
2934
|
+
_,
|
|
2935
|
+
option<JSON.t>,
|
|
2936
|
+
string,
|
|
2937
|
+
string,
|
|
2938
|
+
string,
|
|
2939
|
+
),
|
|
2902
2940
|
) => {
|
|
2903
2941
|
// ── Declared capabilities ──────────────────────────────────────────
|
|
2904
2942
|
//
|
|
@@ -2922,10 +2960,11 @@ module MakeWithConfig = (
|
|
|
2922
2960
|
// The values the slice Lambdas are actually handed, so this reads
|
|
2923
2961
|
// what decides the outcome rather than a second record of the same
|
|
2924
2962
|
// intent. `""` is what an unprovisioned platform exports.
|
|
2925
|
-
~provisioned=
|
|
2963
|
+
~provisioned=[
|
|
2926
2964
|
placeIndex == "" ? [] : [Reventless.CapabilityNeed.Geocoding],
|
|
2927
2965
|
emailSender == "" ? [] : [Reventless.CapabilityNeed.Messaging],
|
|
2928
|
-
|
|
2966
|
+
poolId == "" ? [] : [Reventless.CapabilityNeed.IdentityProvider],
|
|
2967
|
+
]->Array.flat,
|
|
2929
2968
|
) {
|
|
2930
2969
|
| [] => ()
|
|
2931
2970
|
| missing =>
|
|
@@ -2942,9 +2981,7 @@ module MakeWithConfig = (
|
|
|
2942
2981
|
`default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or ` ++
|
|
2943
2982
|
`REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES — or ` ++ `\`platform:messagingEmailProvider: log\` to log every message and send none.`
|
|
2944
2983
|
| IdentityProvider =>
|
|
2945
|
-
`\n IdentityProvider
|
|
2946
|
-
`refusal exist, the Cognito and local implementations do not. A plugin ` ++
|
|
2947
|
-
`declaring this need cannot be deployed until one lands; nothing about this ` ++ `deployment's configuration will change that.`
|
|
2984
|
+
`\n IdentityProvider is the platform's Cognito pool, exported as ` ++ `\`identityProviderId\`. This platform stack exports none — deploy it first.`
|
|
2948
2985
|
}
|
|
2949
2986
|
)
|
|
2950
2987
|
// One line per capability, not per declaring component: two slices
|
package/src/Platform.res.mjs
CHANGED
|
@@ -1412,6 +1412,20 @@ function MakeWithConfig(Config) {
|
|
|
1412
1412
|
PluginRuntime_Builder$ReventlessAws.registerMessagingSender(messagingEmailSender);
|
|
1413
1413
|
let messagingEmailProvider = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailProvider").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailProviderRef.contents;
|
|
1414
1414
|
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_PROVIDER", messagingEmailProvider);
|
|
1415
|
+
let platformOutput = (name, local) => {
|
|
1416
|
+
if (platformStackRef !== undefined) {
|
|
1417
|
+
return Primitive_option.valFromOption(platformStackRef).getOutput(name).apply(o => Stdlib_Option.getOr(o, ""));
|
|
1418
|
+
} else {
|
|
1419
|
+
return local();
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
let identityProviderId = platformOutput("identityProviderId", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().poolId);
|
|
1423
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("IDENTITY_PROVIDER_ID", identityProviderId);
|
|
1424
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("IDENTITY_STORE_TABLE", platformOutput("identityStore", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().identityTable.name));
|
|
1425
|
+
PluginRuntime_Builder$ReventlessAws.registerIdentityProvider({
|
|
1426
|
+
poolArn: platformOutput("identityProviderArn", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().poolArn),
|
|
1427
|
+
storeArn: platformOutput("identityStoreArn", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().identityTable.arn)
|
|
1428
|
+
});
|
|
1415
1429
|
let pluginComponent = plugin.make();
|
|
1416
1430
|
Plugin_Helpers$ReventlessCore.clearOffload();
|
|
1417
1431
|
currentDeployTarget.contents = "Domain";
|
|
@@ -1469,15 +1483,18 @@ function MakeWithConfig(Config) {
|
|
|
1469
1483
|
pluginOutputs.pluginStructure,
|
|
1470
1484
|
stackRef.getOutput("objectStores"),
|
|
1471
1485
|
geocoderPlaceIndex,
|
|
1472
|
-
messagingEmailSender
|
|
1486
|
+
messagingEmailSender,
|
|
1487
|
+
identityProviderId
|
|
1473
1488
|
]).apply(param => {
|
|
1474
1489
|
let structure = param[0];
|
|
1475
1490
|
let missing = CapabilityNeed$Reventless.unmet(Stdlib_Option.getOr(Stdlib_Option.flatMap(structure, s => s.requiredCapabilities), []).map(d => [
|
|
1476
1491
|
d.capability,
|
|
1477
1492
|
d.component
|
|
1478
|
-
]),
|
|
1479
|
-
param[2] === "" ? [] : ["Geocoding"]
|
|
1480
|
-
|
|
1493
|
+
]), [
|
|
1494
|
+
param[2] === "" ? [] : ["Geocoding"],
|
|
1495
|
+
param[3] === "" ? [] : ["Messaging"],
|
|
1496
|
+
param[4] === "" ? [] : ["IdentityProvider"]
|
|
1497
|
+
].flat());
|
|
1481
1498
|
if (missing.length !== 0) {
|
|
1482
1499
|
Stdlib_JsError.throwWithMessage(CapabilityNeed$Reventless.unmetMessage(missing) + Belt_SetString.toArray(Belt_SetString.fromArray(missing.map(u => {
|
|
1483
1500
|
let match = u.need;
|
|
@@ -1487,7 +1504,7 @@ function MakeWithConfig(Config) {
|
|
|
1487
1504
|
case "Messaging" :
|
|
1488
1505
|
return `\n Messaging is \`Capability_Messaging.make\`, passed to the platform as \`~messagingSender\`. The sender itself is configuration and has no default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES — or \`platform:messagingEmailProvider: log\` to log every message and send none.`;
|
|
1489
1506
|
case "IdentityProvider" :
|
|
1490
|
-
return `\n IdentityProvider
|
|
1507
|
+
return `\n IdentityProvider is the platform's Cognito pool, exported as \`identityProviderId\`. This platform stack exports none — deploy it first.`;
|
|
1491
1508
|
}
|
|
1492
1509
|
}))).join(""));
|
|
1493
1510
|
}
|
|
@@ -2804,6 +2821,20 @@ function Make($star) {
|
|
|
2804
2821
|
PluginRuntime_Builder$ReventlessAws.registerMessagingSender(messagingEmailSender);
|
|
2805
2822
|
let messagingEmailProvider = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailProvider").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailProviderRef.contents;
|
|
2806
2823
|
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_PROVIDER", messagingEmailProvider);
|
|
2824
|
+
let platformOutput = (name, local) => {
|
|
2825
|
+
if (platformStackRef !== undefined) {
|
|
2826
|
+
return Primitive_option.valFromOption(platformStackRef).getOutput(name).apply(o => Stdlib_Option.getOr(o, ""));
|
|
2827
|
+
} else {
|
|
2828
|
+
return local();
|
|
2829
|
+
}
|
|
2830
|
+
};
|
|
2831
|
+
let identityProviderId = platformOutput("identityProviderId", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().poolId);
|
|
2832
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("IDENTITY_PROVIDER_ID", identityProviderId);
|
|
2833
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("IDENTITY_STORE_TABLE", platformOutput("identityStore", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().identityTable.name));
|
|
2834
|
+
PluginRuntime_Builder$ReventlessAws.registerIdentityProvider({
|
|
2835
|
+
poolArn: platformOutput("identityProviderArn", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().poolArn),
|
|
2836
|
+
storeArn: platformOutput("identityStoreArn", () => Platform_Stack$ReventlessAws.resolveCognitoUserPool().identityTable.arn)
|
|
2837
|
+
});
|
|
2807
2838
|
let pluginComponent = plugin.make();
|
|
2808
2839
|
Plugin_Helpers$ReventlessCore.clearOffload();
|
|
2809
2840
|
currentDeployTarget.contents = "Domain";
|
|
@@ -2861,15 +2892,18 @@ function Make($star) {
|
|
|
2861
2892
|
pluginOutputs.pluginStructure,
|
|
2862
2893
|
stackRef.getOutput("objectStores"),
|
|
2863
2894
|
geocoderPlaceIndex,
|
|
2864
|
-
messagingEmailSender
|
|
2895
|
+
messagingEmailSender,
|
|
2896
|
+
identityProviderId
|
|
2865
2897
|
]).apply(param => {
|
|
2866
2898
|
let structure = param[0];
|
|
2867
2899
|
let missing = CapabilityNeed$Reventless.unmet(Stdlib_Option.getOr(Stdlib_Option.flatMap(structure, s => s.requiredCapabilities), []).map(d => [
|
|
2868
2900
|
d.capability,
|
|
2869
2901
|
d.component
|
|
2870
|
-
]),
|
|
2871
|
-
param[2] === "" ? [] : ["Geocoding"]
|
|
2872
|
-
|
|
2902
|
+
]), [
|
|
2903
|
+
param[2] === "" ? [] : ["Geocoding"],
|
|
2904
|
+
param[3] === "" ? [] : ["Messaging"],
|
|
2905
|
+
param[4] === "" ? [] : ["IdentityProvider"]
|
|
2906
|
+
].flat());
|
|
2873
2907
|
if (missing.length !== 0) {
|
|
2874
2908
|
Stdlib_JsError.throwWithMessage(CapabilityNeed$Reventless.unmetMessage(missing) + Belt_SetString.toArray(Belt_SetString.fromArray(missing.map(u => {
|
|
2875
2909
|
let match = u.need;
|
|
@@ -2879,7 +2913,7 @@ function Make($star) {
|
|
|
2879
2913
|
case "Messaging" :
|
|
2880
2914
|
return `\n Messaging is \`Capability_Messaging.make\`, passed to the platform as \`~messagingSender\`. The sender itself is configuration and has no default: set \`platform:messagingEmailSender\` in Pulumi.<stack>.yaml (or REVENTLESS_MESSAGING_EMAIL_SENDER), then verify the address with SES — or \`platform:messagingEmailProvider: log\` to log every message and send none.`;
|
|
2881
2915
|
case "IdentityProvider" :
|
|
2882
|
-
return `\n IdentityProvider
|
|
2916
|
+
return `\n IdentityProvider is the platform's Cognito pool, exported as \`identityProviderId\`. This platform stack exports none — deploy it first.`;
|
|
2883
2917
|
}
|
|
2884
2918
|
}))).join(""));
|
|
2885
2919
|
}
|
package/src/Platform_Stack.res
CHANGED
|
@@ -12,6 +12,9 @@ type cognitoUserPool = {
|
|
|
12
12
|
is declared with the trigger's ARN), while the mutation that writes it is
|
|
13
13
|
provisioned after the API — see [Auth_ActiveRoleStore.res]. */
|
|
14
14
|
activeRoleTable: Auth_ActiveRoleStore.storeTable,
|
|
15
|
+
/** Rows mapping each subject to the domain's own user id, read by the same
|
|
16
|
+
trigger and written when an account is created — see [Auth_IdentityStore.res]. */
|
|
17
|
+
identityTable: Auth_IdentityStore.storeTable,
|
|
15
18
|
/** The sign-in attribute this stack created the pool with. `None` in BYO mode:
|
|
16
19
|
the pool's own choice was made elsewhere and no lookup here reads it back,
|
|
17
20
|
so reporting one would be a guess. */
|
|
@@ -179,9 +182,16 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
179
182
|
~choice=Auth_ActiveRoleStore.chooseStore(~identityProviderId=existingPoolId),
|
|
180
183
|
~opts={},
|
|
181
184
|
)
|
|
185
|
+
// Same scope rule, same reason: the trigger that reads it is the pool's.
|
|
186
|
+
let identityTable = Auth_IdentityStore.resolveTable(
|
|
187
|
+
~choice=Auth_IdentityStore.chooseStore(~identityProviderId=existingPoolId),
|
|
188
|
+
~opts={},
|
|
189
|
+
)
|
|
182
190
|
let activeRoleTrigger = Auth_ActiveRoleTrigger.make(
|
|
183
191
|
~activeRoleTableName=activeRoleTable.name->Pulumi.Output.asInput,
|
|
184
192
|
~activeRoleTableArn=activeRoleTable.arn->Pulumi.Output.asInput,
|
|
193
|
+
~identityTableName=identityTable.name->Pulumi.Output.asInput,
|
|
194
|
+
~identityTableArn=identityTable.arn->Pulumi.Output.asInput,
|
|
185
195
|
~opts={},
|
|
186
196
|
)
|
|
187
197
|
|
|
@@ -239,6 +249,7 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
239
249
|
poolArn: lookup->Pulumi.Output.apply(r => r.arn),
|
|
240
250
|
managed: false,
|
|
241
251
|
activeRoleTable,
|
|
252
|
+
identityTable,
|
|
242
253
|
loginIdentifier: None,
|
|
243
254
|
}
|
|
244
255
|
|
|
@@ -334,6 +345,7 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
334
345
|
poolArn: pool.arn,
|
|
335
346
|
managed: true,
|
|
336
347
|
activeRoleTable,
|
|
348
|
+
identityTable,
|
|
337
349
|
loginIdentifier: Some(loginIdentifier),
|
|
338
350
|
}
|
|
339
351
|
}
|
|
@@ -409,6 +421,11 @@ let _resolveUncached = (): cognitoUserPool => {
|
|
|
409
421
|
// the name is derived, not configured, so this is a convenience for checking
|
|
410
422
|
// rather than a value anything is meant to copy into a config.
|
|
411
423
|
Pulumi.Pulumi.export("activeRoleStore", result.activeRoleTable.name)
|
|
424
|
+
// Read by `provision-accounts` on a stack-owned pool, whose table carries a
|
|
425
|
+
// generated suffix no derivation can produce.
|
|
426
|
+
Pulumi.Pulumi.export("identityStore", result.identityTable.name)
|
|
427
|
+
// What a plugin stack grants its principal-making slices write access to.
|
|
428
|
+
Pulumi.Pulumi.export("identityStoreArn", result.identityTable.arn)
|
|
412
429
|
|
|
413
430
|
result
|
|
414
431
|
}
|
|
@@ -10,6 +10,7 @@ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/
|
|
|
10
10
|
import * as AWS_Tags$ReventlessAws from "./adapter/AWS_Tags.res.mjs";
|
|
11
11
|
import * as Auth_SignUpMode$ReventlessAws from "./adapter/Auth/Auth_SignUpMode.res.mjs";
|
|
12
12
|
import * as Util_LocalConfig$ReventlessAws from "./util/Util_LocalConfig.res.mjs";
|
|
13
|
+
import * as Auth_IdentityStore$ReventlessAws from "./adapter/Auth/Auth_IdentityStore.res.mjs";
|
|
13
14
|
import * as Auth_ActiveRoleStore$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleStore.res.mjs";
|
|
14
15
|
import * as Auth_LoginIdentifier$ReventlessAws from "./adapter/Auth/Auth_LoginIdentifier.res.mjs";
|
|
15
16
|
import * as Util_CognitoGroupUser$ReventlessAws from "./util/Util_CognitoGroupUser.res.mjs";
|
|
@@ -66,7 +67,8 @@ function _resolveUncached() {
|
|
|
66
67
|
let cfg = new Pulumi.Config("platform");
|
|
67
68
|
let existingPoolId = _identityProviderId(cfg);
|
|
68
69
|
let activeRoleTable = Auth_ActiveRoleStore$ReventlessAws.resolveTable(Auth_ActiveRoleStore$ReventlessAws.chooseStore(existingPoolId), {});
|
|
69
|
-
let
|
|
70
|
+
let identityTable = Auth_IdentityStore$ReventlessAws.resolveTable(Auth_IdentityStore$ReventlessAws.chooseStore(existingPoolId), {});
|
|
71
|
+
let activeRoleTrigger = Auth_ActiveRoleTrigger$ReventlessAws.make(activeRoleTable.name, activeRoleTable.arn, identityTable.name, identityTable.arn, undefined, {});
|
|
70
72
|
let result;
|
|
71
73
|
if (existingPoolId !== undefined) {
|
|
72
74
|
let tokenUnits_accessToken = "minutes";
|
|
@@ -105,6 +107,7 @@ function _resolveUncached() {
|
|
|
105
107
|
poolArn: lookup.apply(r => r.arn),
|
|
106
108
|
managed: false,
|
|
107
109
|
activeRoleTable: activeRoleTable,
|
|
110
|
+
identityTable: identityTable,
|
|
108
111
|
loginIdentifier: undefined
|
|
109
112
|
};
|
|
110
113
|
} else {
|
|
@@ -161,6 +164,7 @@ function _resolveUncached() {
|
|
|
161
164
|
poolArn: pool.arn,
|
|
162
165
|
managed: true,
|
|
163
166
|
activeRoleTable: activeRoleTable,
|
|
167
|
+
identityTable: identityTable,
|
|
164
168
|
loginIdentifier: loginIdentifier
|
|
165
169
|
};
|
|
166
170
|
}
|
|
@@ -183,6 +187,8 @@ function _resolveUncached() {
|
|
|
183
187
|
Pulumi$Pulumi.$$export("cognitoUserPoolManaged", managedStr);
|
|
184
188
|
Pulumi$Pulumi.$$export("identityProviderAdminGroup", Pulumi.output(Role$Reventless.adminGroup()));
|
|
185
189
|
Pulumi$Pulumi.$$export("activeRoleStore", result.activeRoleTable.name);
|
|
190
|
+
Pulumi$Pulumi.$$export("identityStore", result.identityTable.name);
|
|
191
|
+
Pulumi$Pulumi.$$export("identityStoreArn", result.identityTable.arn);
|
|
186
192
|
return result;
|
|
187
193
|
}
|
|
188
194
|
|
|
@@ -30,6 +30,8 @@ type triggerOutputs = {
|
|
|
30
30
|
let make = (
|
|
31
31
|
~activeRoleTableName: Pulumi.Input.t<string>,
|
|
32
32
|
~activeRoleTableArn: Pulumi.Input.t<string>,
|
|
33
|
+
~identityTableName: Pulumi.Input.t<string>,
|
|
34
|
+
~identityTableArn: Pulumi.Input.t<string>,
|
|
33
35
|
~name: string="ActiveRoleTrigger",
|
|
34
36
|
~opts: Pulumi.ComponentResource.options,
|
|
35
37
|
): triggerOutputs => {
|
|
@@ -47,14 +49,14 @@ let make = (
|
|
|
47
49
|
~opts,
|
|
48
50
|
)
|
|
49
51
|
|
|
50
|
-
// Logs and
|
|
51
|
-
// permission at all, because the membership it checks
|
|
52
|
-
// event as `request.groupConfiguration.groupsToOverride`.
|
|
53
|
-
// this function must never be able to write
|
|
52
|
+
// Logs and single-item reads of the two stores. Nothing else: the trigger
|
|
53
|
+
// needs no Cognito permission at all, because the membership it checks
|
|
54
|
+
// against arrives in the event as `request.groupConfiguration.groupsToOverride`.
|
|
55
|
+
// `GetItem` alone — this function must never be able to write what it reads.
|
|
54
56
|
let _policy =
|
|
55
|
-
activeRoleTableArn
|
|
56
|
-
->Pulumi.Output.
|
|
57
|
-
->Pulumi.Output.apply(tableArn => {
|
|
57
|
+
(activeRoleTableArn->Pulumi.Output.fromInput, identityTableArn->Pulumi.Output.fromInput)
|
|
58
|
+
->Pulumi.Output.all2
|
|
59
|
+
->Pulumi.Output.apply(((tableArn, identityArn)) => {
|
|
58
60
|
let _ = IAM.RolePolicy.make(
|
|
59
61
|
~name=name ++ "Policy",
|
|
60
62
|
~args={
|
|
@@ -77,6 +79,12 @@ let make = (
|
|
|
77
79
|
actions: Action("dynamodb:GetItem"),
|
|
78
80
|
resources: Resource(tableArn),
|
|
79
81
|
},
|
|
82
|
+
{
|
|
83
|
+
sid: "AllowReadingDomainId",
|
|
84
|
+
effect: Allow,
|
|
85
|
+
actions: Action("dynamodb:GetItem"),
|
|
86
|
+
resources: Resource(identityArn),
|
|
87
|
+
},
|
|
80
88
|
],
|
|
81
89
|
)
|
|
82
90
|
->PolicyDocument.toJsonString
|
|
@@ -140,6 +148,7 @@ let make = (
|
|
|
140
148
|
Lambda.Function.variables: Dict.fromArray([
|
|
141
149
|
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
142
150
|
("ACTIVE_ROLE_TABLE", activeRoleTableName),
|
|
151
|
+
("IDENTITY_STORE_TABLE", identityTableName),
|
|
143
152
|
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
144
153
|
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
145
154
|
Util_LambdaLogging.logLevelEntry(),
|
|
@@ -11,11 +11,14 @@ import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
|
11
11
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
12
12
|
import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
|
|
13
13
|
|
|
14
|
-
function make(activeRoleTableName, activeRoleTableArn, nameOpt, opts) {
|
|
14
|
+
function make(activeRoleTableName, activeRoleTableArn, identityTableName, identityTableArn, nameOpt, opts) {
|
|
15
15
|
let name = nameOpt !== undefined ? nameOpt : "ActiveRoleTrigger";
|
|
16
16
|
let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
17
17
|
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name, "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
18
|
-
|
|
18
|
+
Pulumi.all([
|
|
19
|
+
activeRoleTableArn,
|
|
20
|
+
identityTableArn
|
|
21
|
+
]).apply(param => {
|
|
19
22
|
new (Aws.iam.RolePolicy)(name + "Policy", {
|
|
20
23
|
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "Policy", [
|
|
21
24
|
{
|
|
@@ -32,7 +35,13 @@ function make(activeRoleTableName, activeRoleTableArn, nameOpt, opts) {
|
|
|
32
35
|
Sid: "AllowReadingStoredRole",
|
|
33
36
|
Effect: "Allow",
|
|
34
37
|
Action: "dynamodb:GetItem",
|
|
35
|
-
Resource:
|
|
38
|
+
Resource: param[0]
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
Sid: "AllowReadingDomainId",
|
|
42
|
+
Effect: "Allow",
|
|
43
|
+
Action: "dynamodb:GetItem",
|
|
44
|
+
Resource: param[1]
|
|
36
45
|
}
|
|
37
46
|
])),
|
|
38
47
|
role: lambdaRole.id
|
|
@@ -65,6 +74,10 @@ function make(activeRoleTableName, activeRoleTableArn, nameOpt, opts) {
|
|
|
65
74
|
"ACTIVE_ROLE_TABLE",
|
|
66
75
|
activeRoleTableName
|
|
67
76
|
],
|
|
77
|
+
[
|
|
78
|
+
"IDENTITY_STORE_TABLE",
|
|
79
|
+
identityTableName
|
|
80
|
+
],
|
|
68
81
|
[
|
|
69
82
|
"NODE_OPTIONS",
|
|
70
83
|
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
@@ -41,6 +41,11 @@ let tableName = (): string =>
|
|
|
41
41
|
| None => JsError.throwWithMessage("ACTIVE_ROLE_TABLE is not configured")
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** The identity store, read for the domain id claim. Optional where the role
|
|
45
|
+
store is not: a trigger deployed before the store existed must keep minting
|
|
46
|
+
tokens, and an absent store means exactly what an absent row means. */
|
|
47
|
+
let identityTableName = (): option<string> => getEnv("IDENTITY_STORE_TABLE")
|
|
48
|
+
|
|
44
49
|
// ── Cognito pre-token-generation event (V1_0) ───────────────────────────────
|
|
45
50
|
|
|
46
51
|
type userAttributes = {sub?: string}
|
|
@@ -124,16 +129,24 @@ exactly one thing.
|
|
|
124
129
|
`claimsOverrideDetails` is not the same as none, and the regression line for this
|
|
125
130
|
whole feature is that a caller with no stored role gets byte-identical output.
|
|
126
131
|
*/
|
|
127
|
-
let respond = (~event: event, ~decision: decision): event => {
|
|
132
|
+
let respond = (~event: event, ~decision: decision, ~userId: option<string>=?): event => {
|
|
128
133
|
let incoming = event.request->Option.flatMap(r => r.groupConfiguration)
|
|
129
134
|
let iamRolesToOverride = incoming->Option.flatMap(g => g.iamRolesToOverride)->Option.getOr([])
|
|
130
135
|
let preferredRole = incoming->Option.flatMap(g => g.preferredRole)
|
|
131
136
|
|
|
137
|
+
// The domain id rides on whatever else the mint adds. On its own it adds only
|
|
138
|
+
// the claim and leaves the group configuration alone, so a caller with a
|
|
139
|
+
// domain id and no stored role keeps exactly the groups the pool granted.
|
|
140
|
+
let withUserId = (claims: dict<string>) => {
|
|
141
|
+
userId->Option.forEach(id => claims->Dict.set(ReventlessCore.Auth_UserId.claim, id))
|
|
142
|
+
claims
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
let overrideWith = (~groups: array<string>, ~claims: dict<string>) => {
|
|
133
146
|
...event,
|
|
134
147
|
response: {
|
|
135
148
|
claimsOverrideDetails: {
|
|
136
|
-
claimsToAddOrOverride: claims,
|
|
149
|
+
claimsToAddOrOverride: withUserId(claims),
|
|
137
150
|
groupOverrideDetails: {
|
|
138
151
|
groupsToOverride: groups,
|
|
139
152
|
iamRolesToOverride,
|
|
@@ -143,9 +156,13 @@ let respond = (~event: event, ~decision: decision): event => {
|
|
|
143
156
|
},
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
switch decision {
|
|
147
|
-
| Unchanged => event
|
|
148
|
-
|
|
|
159
|
+
switch (decision, userId) {
|
|
160
|
+
| (Unchanged, None) => event
|
|
161
|
+
| (Unchanged, Some(_)) => {
|
|
162
|
+
...event,
|
|
163
|
+
response: {claimsOverrideDetails: {claimsToAddOrOverride: withUserId(Dict.make())}},
|
|
164
|
+
}
|
|
165
|
+
| (Narrow({role, membership}), _) =>
|
|
149
166
|
overrideWith(
|
|
150
167
|
~groups=[role],
|
|
151
168
|
~claims=Dict.fromArray([
|
|
@@ -153,7 +170,7 @@ let respond = (~event: event, ~decision: decision): event => {
|
|
|
153
170
|
(ReventlessCore.Auth_ActiveRole.availableRolesClaim, membership->Array.join(",")),
|
|
154
171
|
]),
|
|
155
172
|
)
|
|
156
|
-
| Stale({role, membership}) =>
|
|
173
|
+
| (Stale({role, membership}), _) =>
|
|
157
174
|
// The full set, as the pool granted it — plus the marker that explains why the
|
|
158
175
|
// stored choice did not apply.
|
|
159
176
|
overrideWith(
|
|
@@ -194,6 +211,33 @@ let storedRoleFor = async (~sub: string, ~clientId: string, ~table: string): opt
|
|
|
194
211
|
| _ => None
|
|
195
212
|
}
|
|
196
213
|
|
|
214
|
+
/**
|
|
215
|
+
The domain id minted for this subject, or `None` for an account created before
|
|
216
|
+
the store existed — whose id is its `sub`, and must stay so: every row it has
|
|
217
|
+
written is keyed by it.
|
|
218
|
+
|
|
219
|
+
Fails to `None` for the reason [storedRoleFor] does: a claim this trigger could
|
|
220
|
+
not read is a login that must still succeed, and a caller without the claim
|
|
221
|
+
lands on the fallback every older account already uses.
|
|
222
|
+
*/
|
|
223
|
+
let userIdFor = async (~sub: string, ~table: option<string>): option<string> =>
|
|
224
|
+
switch table {
|
|
225
|
+
| None => None
|
|
226
|
+
| Some(table) =>
|
|
227
|
+
try {
|
|
228
|
+
let out = await DynamoDb_DocumentClient.GetCommand.make({
|
|
229
|
+
tableName: table,
|
|
230
|
+
key: Dict.fromArray([(Auth_IdentityStore_Schema.partitionKey, JSON.Encode.string(sub))]),
|
|
231
|
+
})->DynamoDb_DocumentClient.GetCommand.send
|
|
232
|
+
out.item
|
|
233
|
+
->Option.flatMap(JSON.Decode.object)
|
|
234
|
+
->Option.flatMap(o => o->Dict.get(Auth_IdentityStore_Schema.userIdAttribute))
|
|
235
|
+
->Option.flatMap(JSON.Decode.string)
|
|
236
|
+
} catch {
|
|
237
|
+
| _ => None
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
197
241
|
// ── Handler ─────────────────────────────────────────────────────────────────
|
|
198
242
|
|
|
199
243
|
let handler = async (event: event): event => {
|
|
@@ -216,10 +260,16 @@ let handler = async (event: event): event => {
|
|
|
216
260
|
// granted — the safe direction, and the same one a read failure takes. An
|
|
217
261
|
// absent client id is not worth failing a login over: the caller lands on their
|
|
218
262
|
// full membership, which is their existing privileges and not more.
|
|
219
|
-
if sub == ""
|
|
263
|
+
if sub == "" {
|
|
220
264
|
event
|
|
265
|
+
} else if clientId == "" {
|
|
266
|
+
respond(~event, ~decision=Unchanged, ~userId=?await userIdFor(~sub, ~table=identityTableName()))
|
|
221
267
|
} else {
|
|
222
|
-
|
|
223
|
-
|
|
268
|
+
// Both reads at once: this function is a login's latency.
|
|
269
|
+
let (storedRole, userId) = await Promise.all2((
|
|
270
|
+
storedRoleFor(~sub, ~clientId, ~table=tableName()),
|
|
271
|
+
userIdFor(~sub, ~table=identityTableName()),
|
|
272
|
+
))
|
|
273
|
+
respond(~event, ~decision=decide(~membership, ~storedRole), ~userId?)
|
|
224
274
|
}
|
|
225
275
|
}
|
|
@@ -4,8 +4,10 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
6
6
|
import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
|
|
7
|
+
import * as Auth_UserId$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_UserId.res.mjs";
|
|
7
8
|
import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
|
|
8
9
|
import * as DynamoDb_DocumentClient$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DocumentClient.res.mjs";
|
|
10
|
+
import * as Auth_IdentityStore_Schema$ReventlessAws from "./Auth_IdentityStore_Schema.res.mjs";
|
|
9
11
|
import * as Auth_ActiveRoleStore_Schema$ReventlessAws from "./Auth_ActiveRoleStore_Schema.res.mjs";
|
|
10
12
|
|
|
11
13
|
function getEnv(k) {
|
|
@@ -24,6 +26,10 @@ function tableName() {
|
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function identityTableName() {
|
|
30
|
+
return getEnv("IDENTITY_STORE_TABLE");
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
function decide(membership, storedRole) {
|
|
28
34
|
if (storedRole !== undefined && storedRole !== "") {
|
|
29
35
|
if (membership.includes(storedRole)) {
|
|
@@ -44,15 +50,21 @@ function decide(membership, storedRole) {
|
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
function respond(event, decision) {
|
|
53
|
+
function respond(event, decision, userId) {
|
|
48
54
|
let incoming = Stdlib_Option.flatMap(event.request, r => r.groupConfiguration);
|
|
49
55
|
let iamRolesToOverride = Stdlib_Option.getOr(Stdlib_Option.flatMap(incoming, g => g.iamRolesToOverride), []);
|
|
50
56
|
let preferredRole = Stdlib_Option.flatMap(incoming, g => g.preferredRole);
|
|
57
|
+
let withUserId = claims => {
|
|
58
|
+
Stdlib_Option.forEach(userId, id => {
|
|
59
|
+
claims[Auth_UserId$ReventlessCore.claim] = id;
|
|
60
|
+
});
|
|
61
|
+
return claims;
|
|
62
|
+
};
|
|
51
63
|
let overrideWith = (groups, claims) => {
|
|
52
64
|
let newrecord = {...event};
|
|
53
65
|
newrecord.response = {
|
|
54
66
|
claimsOverrideDetails: {
|
|
55
|
-
claimsToAddOrOverride: claims,
|
|
67
|
+
claimsToAddOrOverride: withUserId(claims),
|
|
56
68
|
groupOverrideDetails: {
|
|
57
69
|
groupsToOverride: groups,
|
|
58
70
|
iamRolesToOverride: iamRolesToOverride,
|
|
@@ -63,7 +75,16 @@ function respond(event, decision) {
|
|
|
63
75
|
return newrecord;
|
|
64
76
|
};
|
|
65
77
|
if (typeof decision !== "object") {
|
|
66
|
-
|
|
78
|
+
if (userId === undefined) {
|
|
79
|
+
return event;
|
|
80
|
+
}
|
|
81
|
+
let newrecord = {...event};
|
|
82
|
+
newrecord.response = {
|
|
83
|
+
claimsOverrideDetails: {
|
|
84
|
+
claimsToAddOrOverride: withUserId({})
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return newrecord;
|
|
67
88
|
}
|
|
68
89
|
if (decision.TAG === "Narrow") {
|
|
69
90
|
let role = decision.role;
|
|
@@ -112,23 +133,49 @@ async function storedRoleFor(sub, clientId, table) {
|
|
|
112
133
|
}
|
|
113
134
|
}
|
|
114
135
|
|
|
136
|
+
async function userIdFor(sub, table) {
|
|
137
|
+
if (table === undefined) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
let out = await DynamoDb_DocumentClient$AwsSdk.GetCommand.send(new LibDynamodb.GetCommand({
|
|
142
|
+
TableName: table,
|
|
143
|
+
Key: Object.fromEntries([[
|
|
144
|
+
Auth_IdentityStore_Schema$ReventlessAws.partitionKey,
|
|
145
|
+
sub
|
|
146
|
+
]])
|
|
147
|
+
}));
|
|
148
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(out.Item, Stdlib_JSON.Decode.object), o => o[Auth_IdentityStore_Schema$ReventlessAws.userIdAttribute]), Stdlib_JSON.Decode.string);
|
|
149
|
+
} catch (exn) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
115
154
|
async function handler(event) {
|
|
116
155
|
let membership = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(event.request, r => r.groupConfiguration), g => g.groupsToOverride), []);
|
|
117
156
|
let sub = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(event.request, r => r.userAttributes), u => u.sub), "");
|
|
118
157
|
let clientId = Stdlib_Option.getOr(Stdlib_Option.flatMap(event.callerContext, c => c.clientId), "");
|
|
119
|
-
if (sub === ""
|
|
158
|
+
if (sub === "") {
|
|
120
159
|
return event;
|
|
121
160
|
}
|
|
122
|
-
|
|
123
|
-
|
|
161
|
+
if (clientId === "") {
|
|
162
|
+
return respond(event, "Unchanged", await userIdFor(sub, getEnv("IDENTITY_STORE_TABLE")));
|
|
163
|
+
}
|
|
164
|
+
let match = await Promise.all([
|
|
165
|
+
storedRoleFor(sub, clientId, tableName()),
|
|
166
|
+
userIdFor(sub, getEnv("IDENTITY_STORE_TABLE"))
|
|
167
|
+
]);
|
|
168
|
+
return respond(event, decide(membership, match[0]), match[1]);
|
|
124
169
|
}
|
|
125
170
|
|
|
126
171
|
export {
|
|
127
172
|
getEnv,
|
|
128
173
|
tableName,
|
|
174
|
+
identityTableName,
|
|
129
175
|
decide,
|
|
130
176
|
respond,
|
|
131
177
|
storedRoleFor,
|
|
178
|
+
userIdFor,
|
|
132
179
|
handler,
|
|
133
180
|
}
|
|
134
181
|
/* @aws-sdk/lib-dynamodb Not a pure module */
|