@reventlessdev/reventless-aws 3.0.0-alpha.395 → 3.0.0-alpha.396
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +7 -7
- package/scripts/DeployApp.res +7 -1
- package/scripts/DeployApp.res.mjs +1 -1
- package/scripts/ProvisionAccounts.res +89 -4
- package/scripts/ProvisionAccounts.res.mjs +101 -5
- package/scripts/ProvisionIdentity.res +42 -18
- package/scripts/ProvisionIdentity.res.mjs +40 -26
- package/src/Platform.res +47 -10
- package/src/Platform.res.mjs +44 -10
- package/src/Platform_Stack.res +17 -0
- package/src/Platform_Stack.res.mjs +7 -1
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +16 -7
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +16 -3
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +59 -9
- package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +53 -6
- package/src/adapter/Auth/Auth_Cognito.res +13 -2
- package/src/adapter/Auth/Auth_Cognito.res.mjs +9 -2
- package/src/adapter/Auth/Auth_IdentityStore.res +52 -0
- package/src/adapter/Auth/Auth_IdentityStore.res.mjs +46 -0
- package/src/adapter/Auth/Auth_IdentityStore_Schema.res +39 -0
- package/src/adapter/Auth/Auth_IdentityStore_Schema.res.mjs +35 -0
- package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res +251 -0
- package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res.mjs +300 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +7 -4
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +2 -2
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +80 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +53 -2
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res +2 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +23 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +29 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res +40 -0
- package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +59 -8
- package/tests/Auth_CognitoTest.res +62 -0
- package/tests/Auth_CognitoTest.res.mjs +98 -0
- package/tests/Auth_IdentityStore_SchemaTest.res +35 -0
- package/tests/Auth_IdentityStore_SchemaTest.res.mjs +43 -0
- package/tests/IdentityProvider_Cognito_BackendTest.res +314 -0
- package/tests/IdentityProvider_Cognito_BackendTest.res.mjs +442 -0
- package/tests/ProvisionAccountsTest.res.mjs +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
4
4
|
import * as Auth_Cognito$ReventlessAws from "../src/adapter/Auth/Auth_Cognito.res.mjs";
|
|
5
|
+
import * as Auth_UserId$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_UserId.res.mjs";
|
|
5
6
|
|
|
6
7
|
function buildContext(headers) {
|
|
7
8
|
return {
|
|
@@ -240,6 +241,103 @@ globalThis.describe("Auth_Cognito.fromAppSyncIdentity", () => {
|
|
|
240
241
|
});
|
|
241
242
|
});
|
|
242
243
|
|
|
244
|
+
globalThis.describe("Auth_Cognito and the domain id claim", () => {
|
|
245
|
+
globalThis.test("authenticate: a token carrying the claim yields it, not the sub", async () => {
|
|
246
|
+
let token = buildJwt(Object.fromEntries([
|
|
247
|
+
[
|
|
248
|
+
"sub",
|
|
249
|
+
"c3741234-aaaa"
|
|
250
|
+
],
|
|
251
|
+
[
|
|
252
|
+
Auth_UserId$ReventlessCore.claim,
|
|
253
|
+
"u_0123"
|
|
254
|
+
]
|
|
255
|
+
]));
|
|
256
|
+
let identity = await Auth_Cognito$ReventlessAws.authenticate(buildContext([[
|
|
257
|
+
"authorization",
|
|
258
|
+
"Bearer " + token
|
|
259
|
+
]]));
|
|
260
|
+
if (typeof identity !== "object") {
|
|
261
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
262
|
+
}
|
|
263
|
+
if (identity.TAG !== "Authenticated") {
|
|
264
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
265
|
+
}
|
|
266
|
+
globalThis.expect(identity._0.userId).toBe("u_0123");
|
|
267
|
+
});
|
|
268
|
+
globalThis.test("authenticate: a token without the claim yields exactly the sub", async () => {
|
|
269
|
+
let token = buildJwt(Object.fromEntries([[
|
|
270
|
+
"sub",
|
|
271
|
+
"c3741234-aaaa"
|
|
272
|
+
]]));
|
|
273
|
+
let identity = await Auth_Cognito$ReventlessAws.authenticate(buildContext([[
|
|
274
|
+
"authorization",
|
|
275
|
+
"Bearer " + token
|
|
276
|
+
]]));
|
|
277
|
+
if (typeof identity !== "object") {
|
|
278
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
279
|
+
}
|
|
280
|
+
if (identity.TAG !== "Authenticated") {
|
|
281
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
282
|
+
}
|
|
283
|
+
globalThis.expect(identity._0.userId).toBe("c3741234-aaaa");
|
|
284
|
+
});
|
|
285
|
+
globalThis.test("fromAppSyncIdentity: the claim wins over the sub", () => {
|
|
286
|
+
let id_sub = "c3741234-aaaa";
|
|
287
|
+
let id_claims = Object.fromEntries([[
|
|
288
|
+
Auth_UserId$ReventlessCore.claim,
|
|
289
|
+
"u_0123"
|
|
290
|
+
]]);
|
|
291
|
+
let id = {
|
|
292
|
+
sub: id_sub,
|
|
293
|
+
claims: id_claims
|
|
294
|
+
};
|
|
295
|
+
let identity = Auth_Cognito$ReventlessAws.fromAppSyncIdentity(id);
|
|
296
|
+
if (typeof identity !== "object") {
|
|
297
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
298
|
+
}
|
|
299
|
+
if (identity.TAG !== "Authenticated") {
|
|
300
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
301
|
+
}
|
|
302
|
+
globalThis.expect(identity._0.userId).toBe("u_0123");
|
|
303
|
+
});
|
|
304
|
+
globalThis.test("fromAppSyncIdentity: no claim is exactly the sub", () => {
|
|
305
|
+
let id_sub = "c3741234-aaaa";
|
|
306
|
+
let id_claims = {};
|
|
307
|
+
let id = {
|
|
308
|
+
sub: id_sub,
|
|
309
|
+
claims: id_claims
|
|
310
|
+
};
|
|
311
|
+
let identity = Auth_Cognito$ReventlessAws.fromAppSyncIdentity(id);
|
|
312
|
+
if (typeof identity !== "object") {
|
|
313
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
314
|
+
}
|
|
315
|
+
if (identity.TAG !== "Authenticated") {
|
|
316
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
317
|
+
}
|
|
318
|
+
globalThis.expect(identity._0.userId).toBe("c3741234-aaaa");
|
|
319
|
+
});
|
|
320
|
+
globalThis.test("an empty claim is treated as absent", () => {
|
|
321
|
+
let id_sub = "c3741234-aaaa";
|
|
322
|
+
let id_claims = Object.fromEntries([[
|
|
323
|
+
Auth_UserId$ReventlessCore.claim,
|
|
324
|
+
""
|
|
325
|
+
]]);
|
|
326
|
+
let id = {
|
|
327
|
+
sub: id_sub,
|
|
328
|
+
claims: id_claims
|
|
329
|
+
};
|
|
330
|
+
let identity = Auth_Cognito$ReventlessAws.fromAppSyncIdentity(id);
|
|
331
|
+
if (typeof identity !== "object") {
|
|
332
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
333
|
+
}
|
|
334
|
+
if (identity.TAG !== "Authenticated") {
|
|
335
|
+
return Stdlib_JsError.throwWithMessage("expected Authenticated");
|
|
336
|
+
}
|
|
337
|
+
globalThis.expect(identity._0.userId).toBe("c3741234-aaaa");
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
243
341
|
export {
|
|
244
342
|
buildContext,
|
|
245
343
|
b64urlEncode,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The identity store's name and key, shared by the deploy, the trigger and the
|
|
4
|
+
// provisioning scripts. The symptom of any two disagreeing is quiet: the claim
|
|
5
|
+
// is never added, and every new account silently keeps its `sub`.
|
|
6
|
+
|
|
7
|
+
module Schema = Auth_IdentityStore_Schema
|
|
8
|
+
|
|
9
|
+
describe("Auth_IdentityStore_Schema", () => {
|
|
10
|
+
testSync("names the store after the provider whose trigger reads it", () =>
|
|
11
|
+
expect(Schema.derivedStoreName(~identityProviderId="eu-west-1_CQTwafSeX"))->toBe(
|
|
12
|
+
"ReventlessIdentityStore-eu-west-1_CQTwafSeX",
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
testSync("does not collide with the active-role store on the same provider", () =>
|
|
17
|
+
expect(
|
|
18
|
+
Schema.derivedStoreName(~identityProviderId="eu-west-1_A") ==
|
|
19
|
+
Auth_ActiveRoleStore_Schema.derivedStoreName(~identityProviderId="eu-west-1_A"),
|
|
20
|
+
)->toBe(false)
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
testSync("is keyed on the subject alone", () =>
|
|
24
|
+
expect(Schema.expectedKeySchema)->toEqual([("id", "HASH")])
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
testSync("adopts a table with the right key and refuses one with another", () => {
|
|
28
|
+
expect(Schema.keySchemaRefusal(~tableName="t", ~actual=[("id", "HASH")]))->toEqual(None)
|
|
29
|
+
expect(
|
|
30
|
+
Schema.keySchemaRefusal(~tableName="t", ~actual=[("id", "HASH"), ("clientId", "RANGE")])
|
|
31
|
+
->Option.map(m => m->String.includes("identity store needs [id:HASH]"))
|
|
32
|
+
->Option.getOr(false),
|
|
33
|
+
)->toBe(true)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
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_IdentityStore_Schema$ReventlessAws from "../src/adapter/Auth/Auth_IdentityStore_Schema.res.mjs";
|
|
5
|
+
import * as Auth_ActiveRoleStore_Schema$ReventlessAws from "../src/adapter/Auth/Auth_ActiveRoleStore_Schema.res.mjs";
|
|
6
|
+
|
|
7
|
+
globalThis.describe("Auth_IdentityStore_Schema", () => {
|
|
8
|
+
globalThis.test("names the store after the provider whose trigger reads it", () => {
|
|
9
|
+
globalThis.expect(Auth_IdentityStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_CQTwafSeX")).toBe("ReventlessIdentityStore-eu-west-1_CQTwafSeX");
|
|
10
|
+
});
|
|
11
|
+
globalThis.test("does not collide with the active-role store on the same provider", () => {
|
|
12
|
+
globalThis.expect(Auth_IdentityStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_A") === Auth_ActiveRoleStore_Schema$ReventlessAws.derivedStoreName("eu-west-1_A")).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
globalThis.test("is keyed on the subject alone", () => {
|
|
15
|
+
globalThis.expect(Auth_IdentityStore_Schema$ReventlessAws.expectedKeySchema).toEqual([[
|
|
16
|
+
"id",
|
|
17
|
+
"HASH"
|
|
18
|
+
]]);
|
|
19
|
+
});
|
|
20
|
+
globalThis.test("adopts a table with the right key and refuses one with another", () => {
|
|
21
|
+
globalThis.expect(Auth_IdentityStore_Schema$ReventlessAws.keySchemaRefusal("t", [[
|
|
22
|
+
"id",
|
|
23
|
+
"HASH"
|
|
24
|
+
]])).toEqual(undefined);
|
|
25
|
+
globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.map(Auth_IdentityStore_Schema$ReventlessAws.keySchemaRefusal("t", [
|
|
26
|
+
[
|
|
27
|
+
"id",
|
|
28
|
+
"HASH"
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"clientId",
|
|
32
|
+
"RANGE"
|
|
33
|
+
]
|
|
34
|
+
]), m => m.includes("identity store needs [id:HASH]")), false)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
let Schema;
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
Schema,
|
|
42
|
+
}
|
|
43
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// The Cognito identity provider's sequencing, against fake pool calls: what is
|
|
4
|
+
// undone when a later step fails, which failures are verdicts, and that a new
|
|
5
|
+
// principal's domain id is minted into the store. The SDK calls themselves are
|
|
6
|
+
// one line each and are exercised by a deployed stack.
|
|
7
|
+
|
|
8
|
+
module Backend = IdentityProvider_Cognito_Backend
|
|
9
|
+
module IP = Reventless.IdentityProvider
|
|
10
|
+
|
|
11
|
+
@new external makeAwsError: string => exn = "Error"
|
|
12
|
+
@set external setName: (exn, string) => unit = "name"
|
|
13
|
+
|
|
14
|
+
let awsError = (code: string): exn => {
|
|
15
|
+
let e = makeAwsError(`${code}: from the fake pool`)
|
|
16
|
+
e->setName(code)
|
|
17
|
+
e
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type pool = {
|
|
21
|
+
users: dict<(string, array<string>)>,
|
|
22
|
+
rows: dict<string>,
|
|
23
|
+
calls: array<string>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let fakeOps = (~pool: pool, ~failOn: option<(string, string)>=?): Backend.ops => {
|
|
27
|
+
let fail = step =>
|
|
28
|
+
switch failOn {
|
|
29
|
+
| Some((at, code)) if at == step => throw(awsError(code))
|
|
30
|
+
| _ => ()
|
|
31
|
+
}
|
|
32
|
+
let record = step => {
|
|
33
|
+
let _ = pool.calls->Array.push(step)
|
|
34
|
+
fail(step)
|
|
35
|
+
}
|
|
36
|
+
{
|
|
37
|
+
createUser: async (~username, ~attributes as _) => {
|
|
38
|
+
record("createUser")
|
|
39
|
+
if pool.users->Dict.has(username) {
|
|
40
|
+
throw(awsError("UsernameExistsException"))
|
|
41
|
+
}
|
|
42
|
+
let sub = `sub-${username}`
|
|
43
|
+
pool.users->Dict.set(username, (sub, []))
|
|
44
|
+
Some(sub)
|
|
45
|
+
},
|
|
46
|
+
subOf: async (~username) => {
|
|
47
|
+
record("subOf")
|
|
48
|
+
switch pool.users->Dict.get(username) {
|
|
49
|
+
| Some((sub, _)) => Some(sub)
|
|
50
|
+
| None => throw(awsError("UserNotFoundException"))
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
setPassword: async (~username as _, ~password as _) => record("setPassword"),
|
|
54
|
+
addToGroup: async (~username, ~group) => {
|
|
55
|
+
record("addToGroup")
|
|
56
|
+
switch pool.users->Dict.get(username) {
|
|
57
|
+
| Some((sub, groups)) => pool.users->Dict.set(username, (sub, Array.concat(groups, [group])))
|
|
58
|
+
| None => throw(awsError("UserNotFoundException"))
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
removeFromGroup: async (~username, ~group) => {
|
|
62
|
+
record("removeFromGroup")
|
|
63
|
+
switch pool.users->Dict.get(username) {
|
|
64
|
+
| Some((sub, groups)) =>
|
|
65
|
+
pool.users->Dict.set(username, (sub, groups->Array.filter(g => g != group)))
|
|
66
|
+
| None => throw(awsError("UserNotFoundException"))
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
deleteUser: async (~username) => {
|
|
70
|
+
record("deleteUser")
|
|
71
|
+
if !(pool.users->Dict.has(username)) {
|
|
72
|
+
throw(awsError("UserNotFoundException"))
|
|
73
|
+
}
|
|
74
|
+
pool.users->Dict.delete(username)
|
|
75
|
+
},
|
|
76
|
+
putUserId: async (~sub, ~userId) => {
|
|
77
|
+
record("putUserId")
|
|
78
|
+
pool.rows->Dict.set(sub, userId)
|
|
79
|
+
},
|
|
80
|
+
deleteUserId: async (~sub) => {
|
|
81
|
+
record("deleteUserId")
|
|
82
|
+
pool.rows->Dict.delete(sub)
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let emptyPool = () => {users: Dict.make(), rows: Dict.make(), calls: []}
|
|
88
|
+
let email = address => Reventless.Messaging.ToEmail(Reventless.Email.unsafe(address))
|
|
89
|
+
|
|
90
|
+
describe("IdentityProvider_Cognito_Backend", () => {
|
|
91
|
+
testPromise("creates the principal, sets its password and groups, and mints its id", async () => {
|
|
92
|
+
let pool = emptyPool()
|
|
93
|
+
let provider = Backend.withOps(fakeOps(~pool), ~hasStore=true)
|
|
94
|
+
let answer = await provider.createPrincipal(
|
|
95
|
+
~contact=email("new@example.com"),
|
|
96
|
+
~credential=Password("pw"),
|
|
97
|
+
~groups=["Shopper"],
|
|
98
|
+
)
|
|
99
|
+
expect(answer)->toEqual(Ok({IP.providerId: "new@example.com"}))
|
|
100
|
+
expect(pool.users->Dict.get("new@example.com"))->toEqual(
|
|
101
|
+
Some(("sub-new@example.com", ["Shopper"])),
|
|
102
|
+
)
|
|
103
|
+
expect(
|
|
104
|
+
pool.rows->Dict.get("sub-new@example.com")->Option.map(ReventlessCore.Auth_UserId.isMinted),
|
|
105
|
+
)->toEqual(Some(true))
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
testPromise("an existing account is a Conflict, not an outage", async () => {
|
|
109
|
+
let pool = emptyPool()
|
|
110
|
+
let provider = Backend.withOps(fakeOps(~pool), ~hasStore=true)
|
|
111
|
+
let create = () =>
|
|
112
|
+
provider.createPrincipal(
|
|
113
|
+
~contact=email("twice@example.com"),
|
|
114
|
+
~credential=NoCredential,
|
|
115
|
+
~groups=[],
|
|
116
|
+
)
|
|
117
|
+
let _ = await create()
|
|
118
|
+
expect(await create())->toEqual(Error(IP.Conflict))
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// The case the undo exists for: left in place, the retry would meet the
|
|
122
|
+
// account and answer Conflict to someone whose first attempt timed out.
|
|
123
|
+
testPromise("a later step failing undoes the account, so a retry starts clean", async () => {
|
|
124
|
+
let pool = emptyPool()
|
|
125
|
+
let failing = Backend.withOps(
|
|
126
|
+
fakeOps(~pool, ~failOn=("addToGroup", "TooManyRequestsException")),
|
|
127
|
+
~hasStore=true,
|
|
128
|
+
)
|
|
129
|
+
let first = await failing.createPrincipal(
|
|
130
|
+
~contact=email("retry@example.com"),
|
|
131
|
+
~credential=Password("pw"),
|
|
132
|
+
~groups=["Shopper"],
|
|
133
|
+
)
|
|
134
|
+
expect(
|
|
135
|
+
switch first {
|
|
136
|
+
| Error(Unavailable(_)) => true
|
|
137
|
+
| _ => false
|
|
138
|
+
},
|
|
139
|
+
)->toBe(true)
|
|
140
|
+
expect(pool.users->Dict.has("retry@example.com"))->toBe(false)
|
|
141
|
+
expect(pool.rows->Dict.keysToArray)->toEqual([])
|
|
142
|
+
|
|
143
|
+
let healthy = Backend.withOps(fakeOps(~pool), ~hasStore=true)
|
|
144
|
+
let second = await healthy.createPrincipal(
|
|
145
|
+
~contact=email("retry@example.com"),
|
|
146
|
+
~credential=Password("pw"),
|
|
147
|
+
~groups=["Shopper"],
|
|
148
|
+
)
|
|
149
|
+
expect(second)->toEqual(Ok({IP.providerId: "retry@example.com"}))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
testPromise("a password the pool's policy rejects is Refused, and undone", async () => {
|
|
153
|
+
let pool = emptyPool()
|
|
154
|
+
let provider = Backend.withOps(
|
|
155
|
+
fakeOps(~pool, ~failOn=("setPassword", "InvalidPasswordException")),
|
|
156
|
+
~hasStore=true,
|
|
157
|
+
)
|
|
158
|
+
let answer = await provider.createPrincipal(
|
|
159
|
+
~contact=email("weak@example.com"),
|
|
160
|
+
~credential=Password("1"),
|
|
161
|
+
~groups=[],
|
|
162
|
+
)
|
|
163
|
+
expect(
|
|
164
|
+
switch answer {
|
|
165
|
+
| Error(Refused(_)) => true
|
|
166
|
+
| _ => false
|
|
167
|
+
},
|
|
168
|
+
)->toBe(true)
|
|
169
|
+
expect(pool.users->Dict.has("weak@example.com"))->toBe(false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
testPromise("NoCredential sets no password", async () => {
|
|
173
|
+
let pool = emptyPool()
|
|
174
|
+
let provider = Backend.withOps(fakeOps(~pool), ~hasStore=true)
|
|
175
|
+
let _ = await provider.createPrincipal(
|
|
176
|
+
~contact=email("later@example.com"),
|
|
177
|
+
~credential=NoCredential,
|
|
178
|
+
~groups=[],
|
|
179
|
+
)
|
|
180
|
+
expect(pool.calls->Array.includes("setPassword"))->toBe(false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// A pool provisioned before the identity store: principals are still made,
|
|
184
|
+
// and each keeps its `sub` as its id, as every account before the store did.
|
|
185
|
+
testPromise("without a store, no id is minted", async () => {
|
|
186
|
+
let pool = emptyPool()
|
|
187
|
+
let provider = Backend.withOps(fakeOps(~pool), ~hasStore=false)
|
|
188
|
+
let _ = await provider.createPrincipal(
|
|
189
|
+
~contact=email("old@example.com"),
|
|
190
|
+
~credential=NoCredential,
|
|
191
|
+
~groups=[],
|
|
192
|
+
)
|
|
193
|
+
expect(pool.calls->Array.includes("putUserId"))->toBe(false)
|
|
194
|
+
expect(pool.users->Dict.has("old@example.com"))->toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
testPromise("a push address is refused before the pool is asked", async () => {
|
|
198
|
+
let pool = emptyPool()
|
|
199
|
+
let provider = Backend.withOps(fakeOps(~pool), ~hasStore=true)
|
|
200
|
+
let answer = await provider.createPrincipal(
|
|
201
|
+
~contact=ToPush(Fcm({registrationToken: "tok"})),
|
|
202
|
+
~credential=NoCredential,
|
|
203
|
+
~groups=[],
|
|
204
|
+
)
|
|
205
|
+
expect(
|
|
206
|
+
switch answer {
|
|
207
|
+
| Error(Refused(_)) => true
|
|
208
|
+
| _ => false
|
|
209
|
+
},
|
|
210
|
+
)->toBe(true)
|
|
211
|
+
expect(pool.calls)->toEqual([])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
testPromise("groups change on the pool, and an unknown principal is Refused", async () => {
|
|
215
|
+
let pool = emptyPool()
|
|
216
|
+
let provider = Backend.withOps(fakeOps(~pool), ~hasStore=true)
|
|
217
|
+
let principal: IP.principal = {providerId: "grp@example.com"}
|
|
218
|
+
let _ = await provider.createPrincipal(
|
|
219
|
+
~contact=email("grp@example.com"),
|
|
220
|
+
~credential=NoCredential,
|
|
221
|
+
~groups=["Shopper"],
|
|
222
|
+
)
|
|
223
|
+
expect(await provider.addToGroup(~principal, ~group="Reviewer"))->toEqual(Ok())
|
|
224
|
+
expect(await provider.removeFromGroup(~principal, ~group="Shopper"))->toEqual(Ok())
|
|
225
|
+
expect(pool.users->Dict.get("grp@example.com"))->toEqual(
|
|
226
|
+
Some(("sub-grp@example.com", ["Reviewer"])),
|
|
227
|
+
)
|
|
228
|
+
expect(
|
|
229
|
+
switch await provider.addToGroup(~principal={providerId: "ghost"}, ~group="Shopper") {
|
|
230
|
+
| Error(Refused(_)) => true
|
|
231
|
+
| _ => false
|
|
232
|
+
},
|
|
233
|
+
)->toBe(true)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
testPromise("deleting removes the account and its row, and a second delete is Ok", async () => {
|
|
237
|
+
let pool = emptyPool()
|
|
238
|
+
let provider = Backend.withOps(fakeOps(~pool), ~hasStore=true)
|
|
239
|
+
let principal: IP.principal = {providerId: "gone@example.com"}
|
|
240
|
+
let _ = await provider.createPrincipal(
|
|
241
|
+
~contact=email("gone@example.com"),
|
|
242
|
+
~credential=NoCredential,
|
|
243
|
+
~groups=[],
|
|
244
|
+
)
|
|
245
|
+
expect(await provider.deletePrincipal(~principal))->toEqual(Ok())
|
|
246
|
+
expect(pool.users->Dict.has("gone@example.com"))->toBe(false)
|
|
247
|
+
expect(pool.rows->Dict.has("sub-gone@example.com"))->toBe(false)
|
|
248
|
+
expect(await provider.deletePrincipal(~principal))->toEqual(Ok())
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
testPromise("an outage while deleting is retried, not reported as done", async () => {
|
|
252
|
+
let pool = emptyPool()
|
|
253
|
+
let _ = await Backend.withOps(fakeOps(~pool), ~hasStore=true).createPrincipal(
|
|
254
|
+
~contact=email("busy@example.com"),
|
|
255
|
+
~credential=NoCredential,
|
|
256
|
+
~groups=[],
|
|
257
|
+
)
|
|
258
|
+
let failing = Backend.withOps(
|
|
259
|
+
fakeOps(~pool, ~failOn=("deleteUser", "TooManyRequestsException")),
|
|
260
|
+
~hasStore=true,
|
|
261
|
+
)
|
|
262
|
+
expect(
|
|
263
|
+
switch await failing.deletePrincipal(~principal={providerId: "busy@example.com"}) {
|
|
264
|
+
| Error(Unavailable(_)) => true
|
|
265
|
+
| _ => false
|
|
266
|
+
},
|
|
267
|
+
)->toBe(true)
|
|
268
|
+
expect(pool.users->Dict.has("busy@example.com"))->toBe(true)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
testSync("no pool configured answers Unavailable with no operations", () =>
|
|
272
|
+
expect(Backend.provider(~poolId="", ~identityStore="").operations)->toEqual([])
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
testSync("a configured pool publishes all four operations", () =>
|
|
276
|
+
expect(Backend.provider(~poolId="eu-west-1_X", ~identityStore="t").operations)->toEqual([
|
|
277
|
+
IP.CreatePrincipal,
|
|
278
|
+
AddToGroup,
|
|
279
|
+
RemoveFromGroup,
|
|
280
|
+
DeletePrincipal,
|
|
281
|
+
])
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe("AutomationSliceRuntime_Builder_Single.declaresIdentityProvider", () => {
|
|
286
|
+
let info = (needs): AutomationSliceRuntime_Builder_Single.sliceInfo => {
|
|
287
|
+
specModulePath: "",
|
|
288
|
+
bodyModulePath: "",
|
|
289
|
+
callbackType: "outbound",
|
|
290
|
+
queryDbTableName: Pulumi.Output.make(""),
|
|
291
|
+
queryDbResources: [],
|
|
292
|
+
context: None,
|
|
293
|
+
sourceTopics: Dict.make(),
|
|
294
|
+
consumesDcbLog: true,
|
|
295
|
+
commandTargetName: None,
|
|
296
|
+
schedule: None,
|
|
297
|
+
schedulesResources: [],
|
|
298
|
+
capabilityNeeds: needs,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// The grant that can create and delete accounts follows a declaration, not
|
|
302
|
+
// the pool merely existing, which on AWS it always does.
|
|
303
|
+
testSync("only a slice declaring the need earns the grant", () => {
|
|
304
|
+
expect(
|
|
305
|
+
AutomationSliceRuntime_Builder_Single.declaresIdentityProvider([info([Messaging]), info([])]),
|
|
306
|
+
)->toBe(false)
|
|
307
|
+
expect(
|
|
308
|
+
AutomationSliceRuntime_Builder_Single.declaresIdentityProvider([
|
|
309
|
+
info([Messaging]),
|
|
310
|
+
info([IdentityProvider]),
|
|
311
|
+
]),
|
|
312
|
+
)->toBe(true)
|
|
313
|
+
})
|
|
314
|
+
})
|