@reventlessdev/reventless-aws 3.0.0-alpha.291 → 3.0.0-alpha.293

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.
Files changed (30) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +9 -9
  3. package/src/Platform.res +85 -5
  4. package/src/Platform.res.mjs +62 -6
  5. package/src/Platform_Stack.res +78 -0
  6. package/src/Platform_Stack.res.mjs +17 -2
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +10 -0
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +8 -1
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +89 -12
  10. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +55 -4
  11. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +309 -0
  12. package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +230 -0
  13. package/src/adapter/Auth/Auth_ActiveRoleStore.res +280 -0
  14. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +163 -0
  15. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res +148 -0
  16. package/src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs +128 -0
  17. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +180 -0
  18. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +105 -0
  19. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +201 -0
  20. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +126 -0
  21. package/src/plugin/runtime/PluginRuntime_Builder.res +16 -0
  22. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +6 -0
  23. package/tests/Auth_ActiveRolePoolAttachmentTest.res +211 -0
  24. package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +232 -0
  25. package/tests/Auth_ActiveRoleStoreTest.res +42 -0
  26. package/tests/Auth_ActiveRoleStoreTest.res.mjs +41 -0
  27. package/tests/Auth_ActiveRoleTrigger_OpsTest.res +169 -0
  28. package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +189 -0
  29. package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +110 -0
  30. package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +87 -0
@@ -0,0 +1,232 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Auth_ActiveRolePoolAttachment$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs";
6
+
7
+ function str(prim) {
8
+ return prim;
9
+ }
10
+
11
+ function describedPool() {
12
+ return Object.fromEntries([
13
+ [
14
+ "Id",
15
+ "eu-west-1_abc123"
16
+ ],
17
+ [
18
+ "Name",
19
+ "CustomerPool"
20
+ ],
21
+ [
22
+ "Arn",
23
+ "arn:aws:cognito-idp:eu-west-1:1:userpool/eu-west-1_abc123"
24
+ ],
25
+ [
26
+ "Status",
27
+ "Enabled"
28
+ ],
29
+ [
30
+ "CreationDate",
31
+ "2020-01-01T00:00:00Z"
32
+ ],
33
+ [
34
+ "LastModifiedDate",
35
+ "2024-01-01T00:00:00Z"
36
+ ],
37
+ [
38
+ "SchemaAttributes",
39
+ ["email"]
40
+ ],
41
+ [
42
+ "UsernameAttributes",
43
+ ["email"]
44
+ ],
45
+ [
46
+ "EstimatedNumberOfUsers",
47
+ 4200
48
+ ],
49
+ [
50
+ "IssuerConfiguration",
51
+ Object.fromEntries([[
52
+ "IssuerUri",
53
+ "https://x"
54
+ ]])
55
+ ],
56
+ [
57
+ "KeyConfiguration",
58
+ Object.fromEntries([[
59
+ "KmsKeyId",
60
+ "k-1"
61
+ ]])
62
+ ],
63
+ [
64
+ "UserPoolTier",
65
+ "PLUS"
66
+ ],
67
+ [
68
+ "MfaConfiguration",
69
+ "ON"
70
+ ],
71
+ [
72
+ "DeletionProtection",
73
+ "ACTIVE"
74
+ ],
75
+ [
76
+ "EmailVerificationMessage",
77
+ "Your code is {####}, do not share it"
78
+ ],
79
+ [
80
+ "Policies",
81
+ Object.fromEntries([[
82
+ "PasswordPolicy",
83
+ Object.fromEntries([[
84
+ "MinimumLength",
85
+ 24
86
+ ]])
87
+ ]])
88
+ ],
89
+ [
90
+ "UserPoolTags",
91
+ Object.fromEntries([[
92
+ "CostCentre",
93
+ "identity"
94
+ ]])
95
+ ],
96
+ [
97
+ "LambdaConfig",
98
+ Object.fromEntries([
99
+ [
100
+ "PreSignUp",
101
+ "arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp"
102
+ ],
103
+ [
104
+ "CustomMessage",
105
+ "arn:aws:lambda:eu-west-1:1:function:TheirCustomMessage"
106
+ ]
107
+ ])
108
+ ]
109
+ ]);
110
+ }
111
+
112
+ function merged(preTokenGenerationArn) {
113
+ return Auth_ActiveRolePoolAttachment$ReventlessAws.mergedUpdateInput(describedPool(), "eu-west-1_abc123", preTokenGenerationArn);
114
+ }
115
+
116
+ let triggerArn = "arn:aws:lambda:eu-west-1:1:function:ActiveRoleTrigger";
117
+
118
+ function lambdaConfigOf(input) {
119
+ return Stdlib_Option.flatMap(input["LambdaConfig"], Stdlib_JSON.Decode.object);
120
+ }
121
+
122
+ globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — settings survive the attach", () => {
123
+ let input = merged(triggerArn);
124
+ globalThis.test("MFA stays on rather than defaulting off", () => {
125
+ globalThis.expect(input["MfaConfiguration"]).toEqual("ON");
126
+ });
127
+ globalThis.test("deletion protection stays active", () => {
128
+ globalThis.expect(input["DeletionProtection"]).toEqual("ACTIVE");
129
+ });
130
+ globalThis.test("a customised verification message is not reset", () => {
131
+ globalThis.expect(input["EmailVerificationMessage"]).toEqual("Your code is {####}, do not share it");
132
+ });
133
+ globalThis.test("the password policy travels whole", () => {
134
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(input["Policies"], Stdlib_JSON.Decode.object), p => p["PasswordPolicy"]), Stdlib_JSON.Decode.object), p => p["MinimumLength"])).toEqual(24);
135
+ });
136
+ globalThis.test("tags are not dropped", () => {
137
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(input["UserPoolTags"], Stdlib_JSON.Decode.object), t => t["CostCentre"])).toEqual("identity");
138
+ });
139
+ globalThis.test("a paid tier is not reverted to Lite", () => {
140
+ globalThis.expect(input["UserPoolTier"]).toEqual("PLUS");
141
+ });
142
+ globalThis.test("the pool's existing triggers stay attached", () => {
143
+ globalThis.expect([
144
+ Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreSignUp"]),
145
+ Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["CustomMessage"])
146
+ ]).toEqual([
147
+ "arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp",
148
+ "arn:aws:lambda:eu-west-1:1:function:TheirCustomMessage"
149
+ ]);
150
+ });
151
+ globalThis.test("and ours is added beside them", () => {
152
+ globalThis.expect(Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreTokenGeneration"])).toEqual(triggerArn);
153
+ });
154
+ });
155
+
156
+ globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — shaping for the API", () => {
157
+ let input = merged(triggerArn);
158
+ globalThis.test("UserPoolId names the pool being updated", () => {
159
+ globalThis.expect(input["UserPoolId"]).toEqual("eu-west-1_abc123");
160
+ });
161
+ globalThis.test("Name is renamed to PoolName rather than dropped", () => {
162
+ globalThis.expect([
163
+ input["PoolName"],
164
+ input["Name"]
165
+ ]).toEqual([
166
+ "CustomerPool",
167
+ undefined
168
+ ]);
169
+ });
170
+ globalThis.test("read-only fields UpdateUserPool rejects are not sent", () => {
171
+ globalThis.expect([
172
+ "Id",
173
+ "Arn",
174
+ "Status",
175
+ "CreationDate",
176
+ "LastModifiedDate",
177
+ "SchemaAttributes",
178
+ "UsernameAttributes",
179
+ "EstimatedNumberOfUsers"
180
+ ].filter(k => Stdlib_Option.isSome(input[k]))).toEqual([]);
181
+ });
182
+ });
183
+
184
+ globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — detaching", () => {
185
+ let input = merged(undefined);
186
+ globalThis.test("clearing removes our trigger", () => {
187
+ globalThis.expect(Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreTokenGeneration"])).toEqual(undefined);
188
+ });
189
+ globalThis.test("clearing leaves the customer's own triggers alone", () => {
190
+ globalThis.expect(Stdlib_Option.flatMap(lambdaConfigOf(input), c => c["PreSignUp"])).toEqual("arn:aws:lambda:eu-west-1:1:function:TheirPreSignUp");
191
+ });
192
+ globalThis.test("clearing still sends the rest of the pool back whole", () => {
193
+ globalThis.expect(input["MfaConfiguration"]).toEqual("ON");
194
+ });
195
+ });
196
+
197
+ globalThis.describe("Auth_ActiveRolePoolAttachment.mergedUpdateInput — a pool with nothing set", () => {
198
+ globalThis.test("a pool with no LambdaConfig gains one holding only our trigger", () => {
199
+ let input = Auth_ActiveRolePoolAttachment$ReventlessAws.mergedUpdateInput(Object.fromEntries([[
200
+ "Name",
201
+ "Bare"
202
+ ]]), "eu-west-1_bare", triggerArn);
203
+ globalThis.expect(lambdaConfigOf(input)).toEqual(Object.fromEntries([[
204
+ "PreTokenGeneration",
205
+ triggerArn
206
+ ]]));
207
+ });
208
+ });
209
+
210
+ globalThis.describe("Auth_ActiveRolePoolAttachment.attachedTrigger", () => {
211
+ globalThis.test("reports the trigger a described pool carries", () => {
212
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTrigger(merged(triggerArn))).toEqual(triggerArn);
213
+ });
214
+ globalThis.test("reports none when the pool carries no trigger", () => {
215
+ globalThis.expect(Auth_ActiveRolePoolAttachment$ReventlessAws.attachedTrigger(Object.fromEntries([[
216
+ "Name",
217
+ "Bare"
218
+ ]]))).toEqual(undefined);
219
+ });
220
+ });
221
+
222
+ let Attachment;
223
+
224
+ export {
225
+ Attachment,
226
+ str,
227
+ describedPool,
228
+ merged,
229
+ triggerArn,
230
+ lambdaConfigOf,
231
+ }
232
+ /* Not a pure module */
@@ -0,0 +1,42 @@
1
+ open JestGlobals
2
+
3
+ // The subset rule on the Cognito path's write door, driven by the conformance
4
+ // table in `ReventlessCore.Auth_ActiveRole`.
5
+ //
6
+ // That table is the shared artifact §6 of the plan calls for: the check itself is
7
+ // written twice (this one runs in a resolver Lambda against an AppSync event, the
8
+ // trigger's runs in Cognito's runtime against Cognito's event shape), so the cases
9
+ // are what keeps the two from drifting on the question that carries the security.
10
+
11
+ module Ops = Auth_ActiveRoleStore_Ops
12
+ module Contract = ReventlessCore.Auth_ActiveRole
13
+
14
+ describe("Auth_ActiveRoleStore_Ops.mayActAs — the conformance table", () => {
15
+ Contract.conformanceCases->Array.forEach(({label, membership, requested, expected}) =>
16
+ switch requested {
17
+ // "no role requested" has no subset decision to make — it is the clearing
18
+ // path, exercised against the handler rather than the predicate.
19
+ | None => ()
20
+ | Some(role) =>
21
+ testSync(label, () =>
22
+ expect(Ops.mayActAs(~membership, ~requested=role))->toBe(expected->Option.isSome)
23
+ )
24
+ }
25
+ )
26
+ })
27
+
28
+ // The table above is the contract; these are the properties it encodes, asserted
29
+ // directly so a future edit that weakens a case is visible as a failure here too.
30
+ describe("Auth_ActiveRoleStore_Ops.mayActAs — narrowing only", () => {
31
+ testSync("a role outside membership is never permitted", () =>
32
+ expect(Ops.mayActAs(~membership=["Shopper"], ~requested="Admin"))->toBe(false)
33
+ )
34
+
35
+ testSync("membership is matched exactly, so no case folding widens it", () =>
36
+ expect(Ops.mayActAs(~membership=["Admin"], ~requested="ADMIN"))->toBe(false)
37
+ )
38
+
39
+ testSync("an empty membership permits nothing at all", () =>
40
+ expect(Ops.mayActAs(~membership=[], ~requested="Shopper"))->toBe(false)
41
+ )
42
+ })
@@ -0,0 +1,41 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
5
+ import * as Auth_ActiveRoleStore_Ops$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleStore_Ops.res.mjs";
6
+
7
+ globalThis.describe("Auth_ActiveRoleStore_Ops.mayActAs — the conformance table", () => {
8
+ Auth_ActiveRole$ReventlessCore.conformanceCases.forEach(param => {
9
+ let requested = param.requested;
10
+ if (requested === undefined) {
11
+ return;
12
+ }
13
+ let expected = param.expected;
14
+ let membership = param.membership;
15
+ globalThis.test(param.label, () => {
16
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs(membership, requested)).toBe(Stdlib_Option.isSome(expected));
17
+ });
18
+ });
19
+ });
20
+
21
+ globalThis.describe("Auth_ActiveRoleStore_Ops.mayActAs — narrowing only", () => {
22
+ globalThis.test("a role outside membership is never permitted", () => {
23
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs(["Shopper"], "Admin")).toBe(false);
24
+ });
25
+ globalThis.test("membership is matched exactly, so no case folding widens it", () => {
26
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs(["Admin"], "ADMIN")).toBe(false);
27
+ });
28
+ globalThis.test("an empty membership permits nothing at all", () => {
29
+ globalThis.expect(Auth_ActiveRoleStore_Ops$ReventlessAws.mayActAs([], "Shopper")).toBe(false);
30
+ });
31
+ });
32
+
33
+ let Ops;
34
+
35
+ let Contract;
36
+
37
+ export {
38
+ Ops,
39
+ Contract,
40
+ }
41
+ /* Not a pure module */
@@ -0,0 +1,169 @@
1
+ open JestGlobals
2
+
3
+ // The Cognito minting point, unit-tested with no pool attached to it. The
4
+ // pre-token-generation event shape is fixed and documented, so the decision and
5
+ // the response it produces are checkable before any pool exists — which is what
6
+ // makes the attach step the only unknown left when it runs.
7
+
8
+ module Trigger = Auth_ActiveRoleTrigger_Ops
9
+ module Contract = ReventlessCore.Auth_ActiveRole
10
+
11
+ let eventFor = (
12
+ ~membership: array<string>,
13
+ ~iamRoles: array<string>=[],
14
+ ~preferredRole: option<string>=?,
15
+ ): Trigger.event => {
16
+ request: {
17
+ userAttributes: {sub: "u-carol"},
18
+ groupConfiguration: {
19
+ groupsToOverride: membership,
20
+ iamRolesToOverride: iamRoles,
21
+ preferredRole: ?preferredRole->Option.map(Nullable.make),
22
+ },
23
+ },
24
+ userName: "carol",
25
+ }
26
+
27
+ let overrideOf = (e: Trigger.event) =>
28
+ e.response->Option.flatMap(r => r.claimsOverrideDetails)
29
+
30
+ let groupsOf = (e: Trigger.event) =>
31
+ overrideOf(e)->Option.flatMap(c => c.groupOverrideDetails)->Option.map(g => g.groupsToOverride)
32
+
33
+ let claimOf = (e: Trigger.event, key) =>
34
+ overrideOf(e)->Option.flatMap(c => c.claimsToAddOrOverride)->Option.flatMap(c => c->Dict.get(key))
35
+
36
+ // ── The shared conformance table ──────────────────────────────────────────
37
+ //
38
+ // The same cases the local minting path runs (`LocalAuthLoginTest`). The two
39
+ // implementations cannot be shared across a process boundary, so the table is
40
+ // what keeps them from drifting.
41
+ //
42
+ // The cases expecting a **refusal** read differently here, and deliberately:
43
+ // this path has no client to refuse — it is reading stored state on an ordinary
44
+ // refresh with nobody asking for anything. It mints the full set instead. What
45
+ // both paths must satisfy is the rule underneath: a role outside membership
46
+ // never ends up in the group claim, and what the caller is left holding is
47
+ // exactly what the pool granted.
48
+ //
49
+ // So the assertion is on the groups the token *ends up* carrying, not on how the
50
+ // trigger got there — an absent override and an override naming the full set are
51
+ // the same token. That an absent override really is absent (rather than an empty
52
+ // one, which is not the same thing) is asserted separately below.
53
+
54
+ let effectiveGroups = (e: Trigger.event, ~membership) =>
55
+ groupsOf(e)->Option.getOr(membership)
56
+
57
+ describe("the conformance table, minted by the trigger", () => {
58
+ Contract.conformanceCases->Array.forEach(({label, membership, requested, expected}) =>
59
+ testSync(label, () => {
60
+ let event = Trigger.respond(
61
+ ~event=eventFor(~membership),
62
+ ~decision=Trigger.decide(~membership, ~storedRole=requested),
63
+ )
64
+ expect(effectiveGroups(event, ~membership))->toEqual(expected->Option.getOr(membership))
65
+ })
66
+ )
67
+ })
68
+
69
+ describe("Auth_ActiveRoleTrigger_Ops.decide", () => {
70
+ testSync("no stored role leaves the token alone", () =>
71
+ expect(Trigger.decide(~membership=["Admin"], ~storedRole=None))->toEqual(Trigger.Unchanged)
72
+ )
73
+
74
+ testSync("an empty stored role is the same as none, not a request for nothing", () =>
75
+ expect(Trigger.decide(~membership=["Admin"], ~storedRole=Some("")))->toEqual(Trigger.Unchanged)
76
+ )
77
+
78
+ testSync("a held role narrows", () =>
79
+ expect(
80
+ Trigger.decide(~membership=["Admin", "Shopper"], ~storedRole=Some("Shopper")),
81
+ )->toEqual(Trigger.Narrow({role: "Shopper", membership: ["Admin", "Shopper"]}))
82
+ )
83
+
84
+ // The case the local path never had to answer: the row outlives the membership.
85
+ testSync("a role the pool no longer grants is stale, not applied", () =>
86
+ expect(Trigger.decide(~membership=["Shopper"], ~storedRole=Some("Admin")))->toEqual(
87
+ Trigger.Stale({role: "Admin", membership: ["Shopper"]}),
88
+ )
89
+ )
90
+ })
91
+
92
+ describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
93
+ // The regression line for the whole feature.
94
+ testSync("an unnarrowed refresh mints exactly what it minted before", () => {
95
+ let input = eventFor(~membership=["Admin", "Shopper"])
96
+ expect(Trigger.respond(~event=input, ~decision=Unchanged))->toEqual(input)
97
+ })
98
+
99
+ testSync("narrowing puts the chosen role alone in the group claim", () => {
100
+ let event = Trigger.respond(
101
+ ~event=eventFor(~membership=["Admin", "Shopper"]),
102
+ ~decision=Narrow({role: "Shopper", membership: ["Admin", "Shopper"]}),
103
+ )
104
+ expect(groupsOf(event))->toEqual(Some(["Shopper"]))
105
+ })
106
+
107
+ testSync("a narrowed token records the choice and what it gave up", () => {
108
+ let event = Trigger.respond(
109
+ ~event=eventFor(~membership=["Admin", "Shopper"]),
110
+ ~decision=Narrow({role: "Shopper", membership: ["Admin", "Shopper"]}),
111
+ )
112
+ expect((
113
+ claimOf(event, Contract.activeRoleClaim),
114
+ claimOf(event, Contract.availableRolesClaim),
115
+ ))->toEqual((Some("Shopper"), Some("Admin,Shopper")))
116
+ })
117
+
118
+ // "…and says so where it can be seen": without this the caller is silently
119
+ // un-narrowed with nothing to explain why their chosen role stopped applying.
120
+ testSync("a stale role mints the full set and marks itself as stale", () => {
121
+ let event = Trigger.respond(
122
+ ~event=eventFor(~membership=["Shopper"]),
123
+ ~decision=Stale({role: "Admin", membership: ["Shopper"]}),
124
+ )
125
+ expect((groupsOf(event), claimOf(event, Contract.staleRoleClaim)))->toEqual((
126
+ Some(["Shopper"]),
127
+ Some("Admin"),
128
+ ))
129
+ })
130
+
131
+ testSync("a stale role is never the active role", () => {
132
+ let event = Trigger.respond(
133
+ ~event=eventFor(~membership=["Shopper"]),
134
+ ~decision=Stale({role: "Admin", membership: ["Shopper"]}),
135
+ )
136
+ expect(claimOf(event, Contract.activeRoleClaim))->toEqual(None)
137
+ })
138
+
139
+ // 🚨 `groupOverrideDetails` replaces the whole group configuration. An override
140
+ // that names only `groupsToOverride` silently drops the other two — the same
141
+ // reset-by-omission shape `UpdateUserPool` has, and just as quiet.
142
+ testSync("narrowing carries the caller's IAM roles through untouched", () => {
143
+ let event = Trigger.respond(
144
+ ~event=eventFor(
145
+ ~membership=["Admin", "Shopper"],
146
+ ~iamRoles=["arn:aws:iam::1:role/Shopper"],
147
+ ~preferredRole="arn:aws:iam::1:role/Shopper",
148
+ ),
149
+ ~decision=Narrow({role: "Shopper", membership: ["Admin", "Shopper"]}),
150
+ )
151
+ let details = overrideOf(event)->Option.flatMap(c => c.groupOverrideDetails)
152
+ expect((
153
+ details->Option.map(d => d.iamRolesToOverride),
154
+ details->Option.flatMap(d => d.preferredRole),
155
+ ))->toEqual((Some(["arn:aws:iam::1:role/Shopper"]), Some(Nullable.make("arn:aws:iam::1:role/Shopper"))))
156
+ })
157
+
158
+ testSync("a stale mint carries them through too", () => {
159
+ let event = Trigger.respond(
160
+ ~event=eventFor(~membership=["Shopper"], ~iamRoles=["arn:aws:iam::1:role/Shopper"]),
161
+ ~decision=Stale({role: "Admin", membership: ["Shopper"]}),
162
+ )
163
+ expect(
164
+ overrideOf(event)
165
+ ->Option.flatMap(c => c.groupOverrideDetails)
166
+ ->Option.map(d => d.iamRolesToOverride),
167
+ )->toEqual(Some(["arn:aws:iam::1:role/Shopper"]))
168
+ })
169
+ })
@@ -0,0 +1,189 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Auth_ActiveRole$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_ActiveRole.res.mjs";
5
+ import * as Auth_ActiveRoleTrigger_Ops$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs";
6
+
7
+ function eventFor(membership, iamRolesOpt, preferredRole) {
8
+ let iamRoles = iamRolesOpt !== undefined ? iamRolesOpt : [];
9
+ return {
10
+ request: {
11
+ userAttributes: {
12
+ sub: "u-carol"
13
+ },
14
+ groupConfiguration: {
15
+ groupsToOverride: membership,
16
+ iamRolesToOverride: iamRoles,
17
+ preferredRole: Stdlib_Option.map(preferredRole, prim => prim)
18
+ }
19
+ },
20
+ userName: "carol"
21
+ };
22
+ }
23
+
24
+ function overrideOf(e) {
25
+ return Stdlib_Option.flatMap(e.response, r => r.claimsOverrideDetails);
26
+ }
27
+
28
+ function groupsOf(e) {
29
+ return Stdlib_Option.map(Stdlib_Option.flatMap(overrideOf(e), c => c.groupOverrideDetails), g => g.groupsToOverride);
30
+ }
31
+
32
+ function claimOf(e, key) {
33
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(overrideOf(e), c => c.claimsToAddOrOverride), c => c[key]);
34
+ }
35
+
36
+ function effectiveGroups(e, membership) {
37
+ return Stdlib_Option.getOr(groupsOf(e), membership);
38
+ }
39
+
40
+ globalThis.describe("the conformance table, minted by the trigger", () => {
41
+ Auth_ActiveRole$ReventlessCore.conformanceCases.forEach(param => {
42
+ let expected = param.expected;
43
+ let requested = param.requested;
44
+ let membership = param.membership;
45
+ globalThis.test(param.label, () => {
46
+ let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor(membership, undefined, undefined), Auth_ActiveRoleTrigger_Ops$ReventlessAws.decide(membership, requested));
47
+ globalThis.expect(Stdlib_Option.getOr(groupsOf(event), membership)).toEqual(Stdlib_Option.getOr(expected, membership));
48
+ });
49
+ });
50
+ });
51
+
52
+ globalThis.describe("Auth_ActiveRoleTrigger_Ops.decide", () => {
53
+ globalThis.test("no stored role leaves the token alone", () => {
54
+ globalThis.expect(Auth_ActiveRoleTrigger_Ops$ReventlessAws.decide(["Admin"], undefined)).toEqual("Unchanged");
55
+ });
56
+ globalThis.test("an empty stored role is the same as none, not a request for nothing", () => {
57
+ globalThis.expect(Auth_ActiveRoleTrigger_Ops$ReventlessAws.decide(["Admin"], "")).toEqual("Unchanged");
58
+ });
59
+ globalThis.test("a held role narrows", () => {
60
+ globalThis.expect(Auth_ActiveRoleTrigger_Ops$ReventlessAws.decide([
61
+ "Admin",
62
+ "Shopper"
63
+ ], "Shopper")).toEqual({
64
+ TAG: "Narrow",
65
+ role: "Shopper",
66
+ membership: [
67
+ "Admin",
68
+ "Shopper"
69
+ ]
70
+ });
71
+ });
72
+ globalThis.test("a role the pool no longer grants is stale, not applied", () => {
73
+ globalThis.expect(Auth_ActiveRoleTrigger_Ops$ReventlessAws.decide(["Shopper"], "Admin")).toEqual({
74
+ TAG: "Stale",
75
+ role: "Admin",
76
+ membership: ["Shopper"]
77
+ });
78
+ });
79
+ });
80
+
81
+ globalThis.describe("Auth_ActiveRoleTrigger_Ops.respond", () => {
82
+ globalThis.test("an unnarrowed refresh mints exactly what it minted before", () => {
83
+ let input = eventFor([
84
+ "Admin",
85
+ "Shopper"
86
+ ], undefined, undefined);
87
+ globalThis.expect(Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(input, "Unchanged")).toEqual(input);
88
+ });
89
+ globalThis.test("narrowing puts the chosen role alone in the group claim", () => {
90
+ let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor([
91
+ "Admin",
92
+ "Shopper"
93
+ ], undefined, undefined), {
94
+ TAG: "Narrow",
95
+ role: "Shopper",
96
+ membership: [
97
+ "Admin",
98
+ "Shopper"
99
+ ]
100
+ });
101
+ globalThis.expect(groupsOf(event)).toEqual(["Shopper"]);
102
+ });
103
+ globalThis.test("a narrowed token records the choice and what it gave up", () => {
104
+ let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor([
105
+ "Admin",
106
+ "Shopper"
107
+ ], undefined, undefined), {
108
+ TAG: "Narrow",
109
+ role: "Shopper",
110
+ membership: [
111
+ "Admin",
112
+ "Shopper"
113
+ ]
114
+ });
115
+ globalThis.expect([
116
+ claimOf(event, Auth_ActiveRole$ReventlessCore.activeRoleClaim),
117
+ claimOf(event, Auth_ActiveRole$ReventlessCore.availableRolesClaim)
118
+ ]).toEqual([
119
+ "Shopper",
120
+ "Admin,Shopper"
121
+ ]);
122
+ });
123
+ globalThis.test("a stale role mints the full set and marks itself as stale", () => {
124
+ let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor(["Shopper"], undefined, undefined), {
125
+ TAG: "Stale",
126
+ role: "Admin",
127
+ membership: ["Shopper"]
128
+ });
129
+ globalThis.expect([
130
+ groupsOf(event),
131
+ claimOf(event, Auth_ActiveRole$ReventlessCore.staleRoleClaim)
132
+ ]).toEqual([
133
+ ["Shopper"],
134
+ "Admin"
135
+ ]);
136
+ });
137
+ globalThis.test("a stale role is never the active role", () => {
138
+ let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor(["Shopper"], undefined, undefined), {
139
+ TAG: "Stale",
140
+ role: "Admin",
141
+ membership: ["Shopper"]
142
+ });
143
+ globalThis.expect(claimOf(event, Auth_ActiveRole$ReventlessCore.activeRoleClaim)).toEqual(undefined);
144
+ });
145
+ globalThis.test("narrowing carries the caller's IAM roles through untouched", () => {
146
+ let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor([
147
+ "Admin",
148
+ "Shopper"
149
+ ], ["arn:aws:iam::1:role/Shopper"], "arn:aws:iam::1:role/Shopper"), {
150
+ TAG: "Narrow",
151
+ role: "Shopper",
152
+ membership: [
153
+ "Admin",
154
+ "Shopper"
155
+ ]
156
+ });
157
+ let details = Stdlib_Option.flatMap(overrideOf(event), c => c.groupOverrideDetails);
158
+ globalThis.expect([
159
+ Stdlib_Option.map(details, d => d.iamRolesToOverride),
160
+ Stdlib_Option.flatMap(details, d => d.preferredRole)
161
+ ]).toEqual([
162
+ ["arn:aws:iam::1:role/Shopper"],
163
+ "arn:aws:iam::1:role/Shopper"
164
+ ]);
165
+ });
166
+ globalThis.test("a stale mint carries them through too", () => {
167
+ let event = Auth_ActiveRoleTrigger_Ops$ReventlessAws.respond(eventFor(["Shopper"], ["arn:aws:iam::1:role/Shopper"], undefined), {
168
+ TAG: "Stale",
169
+ role: "Admin",
170
+ membership: ["Shopper"]
171
+ });
172
+ 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
+ });
175
+
176
+ let Trigger;
177
+
178
+ let Contract;
179
+
180
+ export {
181
+ Trigger,
182
+ Contract,
183
+ eventFor,
184
+ overrideOf,
185
+ groupsOf,
186
+ claimOf,
187
+ effectiveGroups,
188
+ }
189
+ /* Not a pure module */