@reventlessdev/reventless-aws 3.0.0-alpha.292 → 3.0.0-alpha.294

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.
@@ -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 */