@reventlessdev/reventless-aws 3.0.0-alpha.319 → 3.0.0-alpha.320
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 +9 -5
- package/rescript.json +4 -0
- package/run-provision-identity.mjs +3 -0
- package/scripts/ProvisionIdentity.res +303 -0
- package/scripts/ProvisionIdentity.res.mjs +376 -0
- package/src/Platform.res +54 -30
- package/src/Platform.res.mjs +34 -32
- package/src/Platform_Stack.res +102 -19
- package/src/Platform_Stack.res.mjs +43 -7
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +267 -32
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +124 -14
- package/src/adapter/Auth/Auth_ActiveRoleStore.res +93 -5
- package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +53 -4
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +50 -4
- package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +24 -3
- package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res +92 -0
- package/src/adapter/Auth/Auth_ActiveRoleStore_Schema.res.mjs +49 -0
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +21 -7
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +15 -7
- package/src/util/Util_AwsError.res +56 -0
- package/src/util/Util_AwsError.res.mjs +59 -0
- package/src/util/Util_ShellConfig.res +28 -0
- package/src/util/Util_ShellConfig.res.mjs +22 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res +175 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +136 -0
- package/tests/Auth_ActiveRoleStoreTest.res +65 -0
- package/tests/Auth_ActiveRoleStoreTest.res.mjs +48 -0
- package/tests/Auth_ActiveRoleStore_SchemaTest.res +98 -0
- package/tests/Auth_ActiveRoleStore_SchemaTest.res.mjs +97 -0
- package/tests/AwsErrorFixtures.mjs +16 -0
- package/tests/ProvisionIdentityTest.res +109 -0
- package/tests/ProvisionIdentityTest.res.mjs +132 -0
- package/tests/Util_AwsErrorTest.res +127 -0
- package/tests/Util_AwsErrorTest.res.mjs +76 -0
- package/tests/Util_ShellConfigTest.res +51 -0
- package/tests/Util_ShellConfigTest.res.mjs +42 -0
|
@@ -16,6 +16,16 @@
|
|
|
16
16
|
never a hand-run command against a live pool, which would leave the
|
|
17
17
|
deployment's behaviour depending on an act no source describes.
|
|
18
18
|
|
|
19
|
+
🚨 **The slot is the pool's, and this resource takes it only when it is
|
|
20
|
+
free or already ours.** Cognito allows a pool exactly one
|
|
21
|
+
`PreTokenGeneration` trigger, so an unconditional attach is last-writer-wins:
|
|
22
|
+
it silently replaces a BYO customer's own claims-enrichment trigger, and it
|
|
23
|
+
lets two platform stacks sharing a pool each read a store the other never
|
|
24
|
+
writes — every role switch reporting success and doing nothing. So the
|
|
25
|
+
describe this already performs is also read for *what is attached*, and a
|
|
26
|
+
slot held by anything else fails the deploy naming it. See [classifySlot] and
|
|
27
|
+
[docs/plans/active-role-store-scoped-to-the-pool.md].
|
|
28
|
+
|
|
19
29
|
🚨 **`UpdateUserPool` resets by omission.** The API requires "a value for all
|
|
20
30
|
parameters that you don't want set to a default value". An attach that sends
|
|
21
31
|
only `LambdaConfig` silently returns every other setting on that pool to its
|
|
@@ -135,10 +145,33 @@ let mergedUpdateInput = (
|
|
|
135
145
|
out
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
/**
|
|
148
|
+
/** Whatever function currently holds the pool's pre-token-generation slot,
|
|
149
|
+
regardless of the contract version it is held at.
|
|
150
|
+
|
|
151
|
+
Prefers `PreTokenGenerationConfig`, since that is the field AWS populates.
|
|
152
|
+
|
|
153
|
+
Version-blind on purpose, and that is what separates it from
|
|
154
|
+
[attachedTrigger]: this answers "is the slot occupied, and by whom" — the
|
|
155
|
+
question the takeover check asks. A foreign trigger pinned at `V2_0` is very
|
|
156
|
+
much attached, and a reader that reported it as absent would have this
|
|
157
|
+
resource quietly replace exactly the trigger it is meant to refuse. */
|
|
158
|
+
let attachedTriggerArn = (~described: dict<JSON.t>): option<string> => {
|
|
159
|
+
let lambdaConfig = described->Dict.get("LambdaConfig")->Option.flatMap(JSON.Decode.object)
|
|
160
|
+
switch lambdaConfig
|
|
161
|
+
->Option.flatMap(c => c->Dict.get("PreTokenGenerationConfig"))
|
|
162
|
+
->Option.flatMap(JSON.Decode.object)
|
|
163
|
+
->Option.flatMap(config => config->Dict.get("LambdaArn"))
|
|
164
|
+
->Option.flatMap(JSON.Decode.string) {
|
|
165
|
+
| Some(_) as arn => arn
|
|
166
|
+
| None =>
|
|
167
|
+
lambdaConfig
|
|
168
|
+
->Option.flatMap(c => c->Dict.get("PreTokenGeneration"))
|
|
169
|
+
->Option.flatMap(JSON.Decode.string)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
139
172
|
|
|
140
|
-
|
|
141
|
-
|
|
173
|
+
/** The trigger currently attached to a described pool **at a version this
|
|
174
|
+
handler implements**, if any.
|
|
142
175
|
|
|
143
176
|
A pool whose `LambdaVersion` names a contract [triggerVersion] does not
|
|
144
177
|
implement reports `None` — *not attached* — even when it points at our own
|
|
@@ -163,6 +196,83 @@ let attachedTrigger = (~described: dict<JSON.t>): option<string> => {
|
|
|
163
196
|
}
|
|
164
197
|
}
|
|
165
198
|
|
|
199
|
+
// ── Whose slot is it ─────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
/** What this deployment found in the pool's one pre-token-generation slot. */
|
|
202
|
+
type slot =
|
|
203
|
+
/** Nothing attached. */
|
|
204
|
+
| Vacant
|
|
205
|
+
/** This deployment's own trigger. */
|
|
206
|
+
| Ours
|
|
207
|
+
/** Another deployment's active-role trigger, reading the **same** store.
|
|
208
|
+
|
|
209
|
+
Not a conflict. Two platform stacks sharing a pool run the same code over the
|
|
210
|
+
same rows, so whichever holds the slot serves both — which is the whole point
|
|
211
|
+
of naming one store with `platform:activeRoleStore`. */
|
|
212
|
+
| SharedWith(string)
|
|
213
|
+
/** Another deployment's active-role trigger, reading a **different** store.
|
|
214
|
+
|
|
215
|
+
The defect itself, caught at the only moment anything can see both halves:
|
|
216
|
+
this stack's write door would write rows that trigger never reads. */
|
|
217
|
+
| DifferentStore({arn: string, theirStore: string})
|
|
218
|
+
/** Something that is not one of ours at all. */
|
|
219
|
+
| Foreign(string)
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
Which of the five a described pool presents.
|
|
223
|
+
|
|
224
|
+
Pure and total, like [mergedUpdateInput] and [probeVerdict], so every branch is
|
|
225
|
+
checkable without a pool. `attachedStore` is the `ACTIVE_ROLE_TABLE` the attached
|
|
226
|
+
function is configured with — `None` when it carries none, or when it could not
|
|
227
|
+
be read at all.
|
|
228
|
+
|
|
229
|
+
Reading the *store* rather than matching a function name is what makes this a
|
|
230
|
+
check on the invariant rather than on a convention: two triggers agree exactly
|
|
231
|
+
when the rows they read are the rows the other's resolver writes, and that is a
|
|
232
|
+
fact about their configuration, not about what they are called.
|
|
233
|
+
*/
|
|
234
|
+
let classifySlot = (
|
|
235
|
+
~attachedArn: option<string>,
|
|
236
|
+
~ourArn: string,
|
|
237
|
+
~ourStore: string,
|
|
238
|
+
~attachedStore: option<string>,
|
|
239
|
+
): slot =>
|
|
240
|
+
switch attachedArn {
|
|
241
|
+
| None | Some("") => Vacant
|
|
242
|
+
| Some(arn) if arn == ourArn => Ours
|
|
243
|
+
| Some(arn) =>
|
|
244
|
+
switch attachedStore {
|
|
245
|
+
| Some(store) if store == ourStore => SharedWith(arn)
|
|
246
|
+
| Some(store) => DifferentStore({arn, theirStore: store})
|
|
247
|
+
| None => Foreign(arn)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
The sentence to fail the deploy with, or `None` to proceed.
|
|
253
|
+
|
|
254
|
+
🚨 **The same trade [readOnlyKeys] makes.** Today this resource replaces whatever
|
|
255
|
+
it finds, so a BYO customer's own claims-enrichment trigger is silently detached
|
|
256
|
+
by a deploy — on a pool the framework went to considerable lengths not to
|
|
257
|
+
otherwise disturb. Given "a customer's pool quietly loses a trigger" and "the
|
|
258
|
+
deploy fails naming what is in the way", the second is the one to design for.
|
|
259
|
+
|
|
260
|
+
Both refusals name what is attached, because the operator cannot act on either
|
|
261
|
+
without knowing which function holds the slot.
|
|
262
|
+
*/
|
|
263
|
+
let refusalFor = (~slot: slot, ~userPoolId: string, ~ourStore: string): option<string> =>
|
|
264
|
+
switch slot {
|
|
265
|
+
| Vacant | Ours | SharedWith(_) => None
|
|
266
|
+
| DifferentStore({arn, theirStore}) =>
|
|
267
|
+
Some(
|
|
268
|
+
`user pool ${userPoolId} already carries the active-role trigger ${arn}, which reads "${theirStore}" — but this deployment's Platform_SetActiveRole writes "${ourStore}". A pool holds one pre-token-generation trigger, so attaching would leave one of the two stacks writing rows nothing reads and every role switch silently doing nothing. Point both stacks at one store with platform:activeRoleStore, or give them separate user pools.`,
|
|
269
|
+
)
|
|
270
|
+
| Foreign(arn) =>
|
|
271
|
+
Some(
|
|
272
|
+
`user pool ${userPoolId} already carries the pre-token-generation trigger ${arn}, which is not an active-role trigger of this framework — attaching would silently replace it, and Cognito allows a pool only one. Detach it deliberately if it is obsolete, or give this deployment its own user pool. (If it *is* a Reventless trigger, this deployment could not read its configuration: the deploying principal needs lambda:GetFunctionConfiguration on it.)`,
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
166
276
|
// ── AWS SDK bindings (lazily imported — see AppSync_SourceApiAssociation_Retrying) ──
|
|
167
277
|
|
|
168
278
|
type cognitoClient
|
|
@@ -222,9 +332,12 @@ let getClient = async (): cognitoClient =>
|
|
|
222
332
|
type lambdaClient
|
|
223
333
|
type invokeCmd
|
|
224
334
|
|
|
335
|
+
type configCmd
|
|
336
|
+
|
|
225
337
|
type lambdaSdkModule = {
|
|
226
338
|
@as("LambdaClient") clientCtor: unit => lambdaClient,
|
|
227
339
|
@as("InvokeCommand") invokeCtor: dict<JSON.t> => invokeCmd,
|
|
340
|
+
@as("GetFunctionConfigurationCommand") configCtor: dict<JSON.t> => configCmd,
|
|
228
341
|
}
|
|
229
342
|
|
|
230
343
|
type invokeResult = {
|
|
@@ -232,7 +345,10 @@ type invokeResult = {
|
|
|
232
345
|
@as("Payload") payload?: JSON.t,
|
|
233
346
|
}
|
|
234
347
|
|
|
348
|
+
type configResult = {@as("Environment") environment?: JSON.t}
|
|
349
|
+
|
|
235
350
|
@send external sendInvoke: (lambdaClient, invokeCmd) => promise<invokeResult> = "send"
|
|
351
|
+
@send external sendGetConfig: (lambdaClient, configCmd) => promise<configResult> = "send"
|
|
236
352
|
@val external dynImportLambda: string => promise<lambdaSdkModule> = "import"
|
|
237
353
|
|
|
238
354
|
let _lambdaSdk: ref<option<lambdaSdkModule>> = ref(None)
|
|
@@ -257,6 +373,33 @@ let getLambdaClient = async (): lambdaClient =>
|
|
|
257
373
|
c
|
|
258
374
|
}
|
|
259
375
|
|
|
376
|
+
/** The store an already-attached function reads, from its own configuration.
|
|
377
|
+
|
|
378
|
+
`None` covers three cases on purpose — no `Environment`, no
|
|
379
|
+
`ACTIVE_ROLE_TABLE` in it, and the call failing outright (a deleted function,
|
|
380
|
+
a cross-account ARN, a deploying principal without
|
|
381
|
+
`lambda:GetFunctionConfiguration`). All three mean the same thing here: this
|
|
382
|
+
deployment cannot establish that the attached trigger reads what its own
|
|
383
|
+
resolver writes, and [refusalFor]'s `Foreign` sentence names every one of
|
|
384
|
+
them. Failing closed is the point — the alternative is taking the slot on the
|
|
385
|
+
strength of a lookup that did not happen. */
|
|
386
|
+
let activeRoleStoreOf = async (~functionArn: string): option<string> =>
|
|
387
|
+
try {
|
|
388
|
+
let sdk = await getLambdaSdk()
|
|
389
|
+
let client = await getLambdaClient()
|
|
390
|
+
let input = Dict.make()
|
|
391
|
+
input->Dict.set("FunctionName", JSON.Encode.string(functionArn))
|
|
392
|
+
let result = await client->sendGetConfig(newOf1(sdk.configCtor, input))
|
|
393
|
+
result.environment
|
|
394
|
+
->Option.flatMap(JSON.Decode.object)
|
|
395
|
+
->Option.flatMap(env => env->Dict.get("Variables"))
|
|
396
|
+
->Option.flatMap(JSON.Decode.object)
|
|
397
|
+
->Option.flatMap(vars => vars->Dict.get("ACTIVE_ROLE_TABLE"))
|
|
398
|
+
->Option.flatMap(JSON.Decode.string)
|
|
399
|
+
} catch {
|
|
400
|
+
| _ => None
|
|
401
|
+
}
|
|
402
|
+
|
|
260
403
|
/** Decodes the SDK's `Payload`, which arrives as a byte array rather than a
|
|
261
404
|
string. */
|
|
262
405
|
let decodePayload: JSON.t => string = %raw(`(p) => {
|
|
@@ -282,6 +425,11 @@ let isPoolGoneError = (jsErr: JsExn.t): bool =>
|
|
|
282
425
|
is here to check. */
|
|
283
426
|
let probeSubject = "reventless-attachment-probe"
|
|
284
427
|
|
|
428
|
+
/** The app client the probe presents, and like [probeSubject] it must match no
|
|
429
|
+
real one — together they form the row key, and a collision would exercise a
|
|
430
|
+
stored preference rather than the empty path this is checking. */
|
|
431
|
+
let probeClientId = "reventless-attachment-probe-client"
|
|
432
|
+
|
|
285
433
|
/** A minimal `V1_0` pre-token-generation event.
|
|
286
434
|
|
|
287
435
|
Empty membership on purpose — with no groups and no stored role there is
|
|
@@ -300,11 +448,19 @@ let probeEvent = (~userPoolId: string): JSON.t => {
|
|
|
300
448
|
request->Dict.set("userAttributes", JSON.Encode.object(userAttributes))
|
|
301
449
|
request->Dict.set("groupConfiguration", JSON.Encode.object(groupConfiguration))
|
|
302
450
|
|
|
451
|
+
// Carried because Cognito carries it, and the handler keys its row on it. A
|
|
452
|
+
// probe without one takes the "no client id, nothing to look up" branch and
|
|
453
|
+
// would report a function healthy without ever reaching the store — proving
|
|
454
|
+
// less than it appears to.
|
|
455
|
+
let callerContext = Dict.make()
|
|
456
|
+
callerContext->Dict.set("clientId", JSON.Encode.string(probeClientId))
|
|
457
|
+
|
|
303
458
|
let event = Dict.make()
|
|
304
459
|
event->Dict.set("version", JSON.Encode.string("1"))
|
|
305
460
|
event->Dict.set("triggerSource", JSON.Encode.string("TokenGeneration_Authentication"))
|
|
306
461
|
event->Dict.set("userPoolId", JSON.Encode.string(userPoolId))
|
|
307
462
|
event->Dict.set("userName", JSON.Encode.string(probeSubject))
|
|
463
|
+
event->Dict.set("callerContext", JSON.Encode.object(callerContext))
|
|
308
464
|
event->Dict.set("request", JSON.Encode.object(request))
|
|
309
465
|
event->Dict.set("response", JSON.Encode.object(Dict.make()))
|
|
310
466
|
JSON.Encode.object(event)
|
|
@@ -406,23 +562,72 @@ let describePool = async (~userPoolId: string): option<dict<JSON.t>> => {
|
|
|
406
562
|
result.userPool->Option.flatMap(JSON.Decode.object)
|
|
407
563
|
}
|
|
408
564
|
|
|
409
|
-
/**
|
|
410
|
-
|
|
411
|
-
|
|
565
|
+
/** Send the merge for an already-described pool. */
|
|
566
|
+
let sendMerged = async (
|
|
567
|
+
~described: dict<JSON.t>,
|
|
568
|
+
~userPoolId: string,
|
|
569
|
+
~preTokenGenerationArn: option<string>,
|
|
570
|
+
): unit => {
|
|
412
571
|
let sdk = await getSdk()
|
|
413
572
|
let client = await getClient()
|
|
573
|
+
let input = mergedUpdateInput(~described, ~userPoolId, ~preTokenGenerationArn)
|
|
574
|
+
await client->sendUpdate(newOf1(sdk.updateCtor, input))
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Describe succeeded but returned no pool body. Sending an update built from
|
|
578
|
+
nothing is precisely the reset this resource exists to avoid. */
|
|
579
|
+
let throwOnEmptyDescribe = (~userPoolId: string) =>
|
|
580
|
+
JsError.throwWithMessage(
|
|
581
|
+
`DescribeUserPool returned no pool for "${userPoolId}"; refusing to send an UpdateUserPool that would reset it`,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
Attach this deployment's trigger — after establishing that the slot is this
|
|
586
|
+
deployment's to take.
|
|
587
|
+
|
|
588
|
+
The order is the whole design. Describe once, then in turn: refuse a slot held by
|
|
589
|
+
a trigger that is not ours or reads a different store; prove our own function can
|
|
590
|
+
run; only then write. Every refusal happens before any `UpdateUserPool`, so a
|
|
591
|
+
pool that fails this is left exactly as it was found.
|
|
592
|
+
*/
|
|
593
|
+
let attachTrigger = async (
|
|
594
|
+
~userPoolId: string,
|
|
595
|
+
~preTokenGenerationArn: string,
|
|
596
|
+
~activeRoleStore: string,
|
|
597
|
+
): unit =>
|
|
414
598
|
switch await describePool(~userPoolId) {
|
|
415
|
-
| None =>
|
|
416
|
-
// Describe succeeded but returned no pool body. Sending an update built from
|
|
417
|
-
// nothing is precisely the reset this resource exists to avoid.
|
|
418
|
-
JsError.throwWithMessage(
|
|
419
|
-
`DescribeUserPool returned no pool for "${userPoolId}"; refusing to send an UpdateUserPool that would reset it`,
|
|
420
|
-
)
|
|
599
|
+
| None => throwOnEmptyDescribe(~userPoolId)
|
|
421
600
|
| Some(described) =>
|
|
422
|
-
let
|
|
423
|
-
|
|
601
|
+
let attachedArn = attachedTriggerArn(~described)
|
|
602
|
+
// Only for a slot held by something other than us — our own function's store
|
|
603
|
+
// is the one we were handed, so asking AWS about it would prove nothing and
|
|
604
|
+
// cost a call on the common path.
|
|
605
|
+
let attachedStore = switch attachedArn {
|
|
606
|
+
| Some(arn) if arn != preTokenGenerationArn && arn != "" =>
|
|
607
|
+
await activeRoleStoreOf(~functionArn=arn)
|
|
608
|
+
| _ => None
|
|
609
|
+
}
|
|
610
|
+
let slot = classifySlot(
|
|
611
|
+
~attachedArn,
|
|
612
|
+
~ourArn=preTokenGenerationArn,
|
|
613
|
+
~ourStore=activeRoleStore,
|
|
614
|
+
~attachedStore,
|
|
615
|
+
)
|
|
616
|
+
switch refusalFor(~slot, ~userPoolId, ~ourStore=activeRoleStore) {
|
|
617
|
+
| Some(message) => JsError.throwWithMessage(message)
|
|
618
|
+
| None => ()
|
|
619
|
+
}
|
|
620
|
+
switch slot {
|
|
621
|
+
| SharedWith(arn) =>
|
|
622
|
+
log.info(
|
|
623
|
+
~comp="Auth_ActiveRolePoolAttachment",
|
|
624
|
+
`user pool ${userPoolId} carries active-role trigger ${arn} from another deployment, reading the same store "${activeRoleStore}" — taking the slot serves both`,
|
|
625
|
+
)
|
|
626
|
+
| _ => ()
|
|
627
|
+
}
|
|
628
|
+
await verifyTrigger(~userPoolId, ~preTokenGenerationArn)
|
|
629
|
+
await sendMerged(~described, ~userPoolId, ~preTokenGenerationArn=Some(preTokenGenerationArn))
|
|
424
630
|
}
|
|
425
|
-
}
|
|
426
631
|
|
|
427
632
|
// ── Dynamic provider ─────────────────────────────────────────────────────────
|
|
428
633
|
|
|
@@ -437,12 +642,19 @@ type providerInputs = {
|
|
|
437
642
|
function once, on the deploy that created it, and never for any version
|
|
438
643
|
shipped afterwards. A check that only ever runs once is not a check. */
|
|
439
644
|
codeHash: string,
|
|
645
|
+
/** The store this deployment's trigger reads and its write door writes.
|
|
646
|
+
|
|
647
|
+
Carried so the takeover check can compare it against the store of whatever
|
|
648
|
+
already holds the pool's slot — the comparison that decides whether two
|
|
649
|
+
deployments sharing this pool agree or silently disagree. */
|
|
650
|
+
activeRoleStore: string,
|
|
440
651
|
}
|
|
441
652
|
|
|
442
653
|
type createOuts = {
|
|
443
654
|
userPoolId: string,
|
|
444
655
|
preTokenGenerationArn: string,
|
|
445
656
|
codeHash: string,
|
|
657
|
+
activeRoleStore: string,
|
|
446
658
|
}
|
|
447
659
|
|
|
448
660
|
type createResult = {id: string, outs: createOuts}
|
|
@@ -451,13 +663,10 @@ type diffResult = {changes: bool, replaces: array<string>, deleteBeforeReplace:
|
|
|
451
663
|
type readResult = {id?: string, props?: createOuts}
|
|
452
664
|
|
|
453
665
|
let create = async (inputs: providerInputs): createResult => {
|
|
454
|
-
await
|
|
666
|
+
await attachTrigger(
|
|
455
667
|
~userPoolId=inputs.userPoolId,
|
|
456
668
|
~preTokenGenerationArn=inputs.preTokenGenerationArn,
|
|
457
|
-
|
|
458
|
-
await applyTrigger(
|
|
459
|
-
~userPoolId=inputs.userPoolId,
|
|
460
|
-
~preTokenGenerationArn=Some(inputs.preTokenGenerationArn),
|
|
669
|
+
~activeRoleStore=inputs.activeRoleStore,
|
|
461
670
|
)
|
|
462
671
|
{
|
|
463
672
|
id: inputs.userPoolId,
|
|
@@ -465,27 +674,28 @@ let create = async (inputs: providerInputs): createResult => {
|
|
|
465
674
|
userPoolId: inputs.userPoolId,
|
|
466
675
|
preTokenGenerationArn: inputs.preTokenGenerationArn,
|
|
467
676
|
codeHash: inputs.codeHash,
|
|
677
|
+
activeRoleStore: inputs.activeRoleStore,
|
|
468
678
|
},
|
|
469
679
|
}
|
|
470
680
|
}
|
|
471
681
|
|
|
472
682
|
let update = async (_id: string, _olds: createOuts, news: providerInputs): updateResult => {
|
|
473
|
-
// Re-proved on every update, not only on create: the usual
|
|
474
|
-
// that the function's code changed, which is exactly when
|
|
475
|
-
//
|
|
476
|
-
|
|
683
|
+
// Re-checked and re-proved on every update, not only on create: the usual
|
|
684
|
+
// reason this runs is that the function's code changed, which is exactly when
|
|
685
|
+
// it might have stopped working — and a slot this deployment held once can
|
|
686
|
+
// have been taken since. The pool keeps the trigger it already has until this
|
|
687
|
+
// succeeds.
|
|
688
|
+
await attachTrigger(
|
|
477
689
|
~userPoolId=news.userPoolId,
|
|
478
690
|
~preTokenGenerationArn=news.preTokenGenerationArn,
|
|
479
|
-
|
|
480
|
-
await applyTrigger(
|
|
481
|
-
~userPoolId=news.userPoolId,
|
|
482
|
-
~preTokenGenerationArn=Some(news.preTokenGenerationArn),
|
|
691
|
+
~activeRoleStore=news.activeRoleStore,
|
|
483
692
|
)
|
|
484
693
|
{
|
|
485
694
|
outs: {
|
|
486
695
|
userPoolId: news.userPoolId,
|
|
487
696
|
preTokenGenerationArn: news.preTokenGenerationArn,
|
|
488
697
|
codeHash: news.codeHash,
|
|
698
|
+
activeRoleStore: news.activeRoleStore,
|
|
489
699
|
},
|
|
490
700
|
}
|
|
491
701
|
}
|
|
@@ -494,10 +704,29 @@ let update = async (_id: string, _olds: createOuts, news: providerInputs): updat
|
|
|
494
704
|
of the stack; a pool left pointing at a deleted function fails **every**
|
|
495
705
|
sign-in, and it is a pool the framework does not own — so nothing else in
|
|
496
706
|
this deployment would ever put it right. A pool that has already gone is
|
|
497
|
-
nothing to detach from.
|
|
707
|
+
nothing to detach from.
|
|
708
|
+
|
|
709
|
+
🚨 **Only when the slot still holds *our* function.** Where two stacks share
|
|
710
|
+
a pool the later one takes the slot, and a blind detach here would then tear
|
|
711
|
+
out a trigger this deployment does not own while destroying — reintroducing
|
|
712
|
+
the outage this exists to prevent, from the opposite direction. What is left
|
|
713
|
+
behind in that case is a healthy trigger reading the store both stacks share. */
|
|
498
714
|
let delete_ = async (_id: string, props: createOuts): unit =>
|
|
499
715
|
try {
|
|
500
|
-
await
|
|
716
|
+
switch await describePool(~userPoolId=props.userPoolId) {
|
|
717
|
+
| None => throwOnEmptyDescribe(~userPoolId=props.userPoolId)
|
|
718
|
+
| Some(described) =>
|
|
719
|
+
switch attachedTriggerArn(~described) {
|
|
720
|
+
| Some(arn) if arn == props.preTokenGenerationArn =>
|
|
721
|
+
await sendMerged(~described, ~userPoolId=props.userPoolId, ~preTokenGenerationArn=None)
|
|
722
|
+
| Some(arn) =>
|
|
723
|
+
log.info(
|
|
724
|
+
~comp="Auth_ActiveRolePoolAttachment",
|
|
725
|
+
`user pool ${props.userPoolId} carries ${arn}, not this deployment's trigger; leaving it attached`,
|
|
726
|
+
)
|
|
727
|
+
| None => ()
|
|
728
|
+
}
|
|
729
|
+
}
|
|
501
730
|
} catch {
|
|
502
731
|
| exn if exn->JsExn.fromException->Option.mapOr(false, isPoolGoneError) =>
|
|
503
732
|
log.info(
|
|
@@ -508,13 +737,15 @@ let delete_ = async (_id: string, props: createOuts): unit =>
|
|
|
508
737
|
|
|
509
738
|
/** A different pool is a different attachment: the old pool must be detached
|
|
510
739
|
before the new one is attached, so this replaces rather than updates in
|
|
511
|
-
place. Changing only the function ARN
|
|
740
|
+
place. Changing only the function ARN, its code, or the store it reads is an
|
|
741
|
+
ordinary update — each is a reason to run the takeover check again. */
|
|
512
742
|
let diff_ = (_id: string, olds: createOuts, news: providerInputs): diffResult => {
|
|
513
743
|
let poolChanged = olds.userPoolId != news.userPoolId
|
|
514
744
|
{
|
|
515
745
|
changes: poolChanged ||
|
|
516
746
|
olds.preTokenGenerationArn != news.preTokenGenerationArn ||
|
|
517
|
-
olds.codeHash != news.codeHash
|
|
747
|
+
olds.codeHash != news.codeHash ||
|
|
748
|
+
olds.activeRoleStore != news.activeRoleStore,
|
|
518
749
|
replaces: poolChanged ? ["userPoolId"] : [],
|
|
519
750
|
deleteBeforeReplace: true,
|
|
520
751
|
}
|
|
@@ -536,6 +767,9 @@ let read_ = async (id: string, props: createOuts): readResult =>
|
|
|
536
767
|
// recorded hash is carried through unchanged. Refresh answers "is the
|
|
537
768
|
// right function attached", which is what the pool actually knows.
|
|
538
769
|
codeHash: props.codeHash,
|
|
770
|
+
// Likewise not the pool's to report — it is a property of whatever
|
|
771
|
+
// function holds the slot, and refresh is about the pool.
|
|
772
|
+
activeRoleStore: props.activeRoleStore,
|
|
539
773
|
},
|
|
540
774
|
}
|
|
541
775
|
}
|
|
@@ -561,6 +795,7 @@ type constructorProps = {
|
|
|
561
795
|
userPoolId: Pulumi.Input.t<string>,
|
|
562
796
|
preTokenGenerationArn: Pulumi.Input.t<string>,
|
|
563
797
|
codeHash: Pulumi.Input.t<string>,
|
|
798
|
+
activeRoleStore: Pulumi.Input.t<string>,
|
|
564
799
|
}
|
|
565
800
|
|
|
566
801
|
// Explicit /index.js path because @pulumi/pulumi/dynamic is a directory import
|