@reventlessdev/reventless-aws 3.0.0-alpha.394 → 3.0.0-alpha.396

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +8 -8
  3. package/scripts/DeployApp.res +7 -1
  4. package/scripts/DeployApp.res.mjs +1 -1
  5. package/scripts/ProvisionAccounts.res +89 -4
  6. package/scripts/ProvisionAccounts.res.mjs +101 -5
  7. package/scripts/ProvisionIdentity.res +42 -18
  8. package/scripts/ProvisionIdentity.res.mjs +40 -26
  9. package/src/Platform.res +47 -10
  10. package/src/Platform.res.mjs +44 -10
  11. package/src/Platform_Stack.res +17 -0
  12. package/src/Platform_Stack.res.mjs +7 -1
  13. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +16 -7
  14. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +16 -3
  15. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res +59 -9
  16. package/src/adapter/Auth/Auth_ActiveRoleTrigger_Ops.res.mjs +53 -6
  17. package/src/adapter/Auth/Auth_Cognito.res +13 -2
  18. package/src/adapter/Auth/Auth_Cognito.res.mjs +9 -2
  19. package/src/adapter/Auth/Auth_IdentityStore.res +52 -0
  20. package/src/adapter/Auth/Auth_IdentityStore.res.mjs +46 -0
  21. package/src/adapter/Auth/Auth_IdentityStore_Schema.res +39 -0
  22. package/src/adapter/Auth/Auth_IdentityStore_Schema.res.mjs +35 -0
  23. package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res +251 -0
  24. package/src/adapter/Identity/IdentityProvider_Cognito_Backend.res.mjs +300 -0
  25. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +7 -4
  26. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +2 -2
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +80 -0
  28. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +53 -2
  29. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  30. package/src/components/OutboundTranslationSlice_Builder.res +2 -0
  31. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  32. package/src/plugin/runtime/PluginRuntime_Builder.res +23 -0
  33. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +29 -0
  34. package/tests/Auth_ActiveRoleTrigger_OpsTest.res +40 -0
  35. package/tests/Auth_ActiveRoleTrigger_OpsTest.res.mjs +59 -8
  36. package/tests/Auth_CognitoTest.res +62 -0
  37. package/tests/Auth_CognitoTest.res.mjs +98 -0
  38. package/tests/Auth_IdentityStore_SchemaTest.res +35 -0
  39. package/tests/Auth_IdentityStore_SchemaTest.res.mjs +43 -0
  40. package/tests/IdentityProvider_Cognito_BackendTest.res +314 -0
  41. package/tests/IdentityProvider_Cognito_BackendTest.res.mjs +442 -0
  42. package/tests/ProvisionAccountsTest.res.mjs +1 -1
  43. package/tests/Util_ShellConfigTest.res.mjs +1 -22
@@ -58,13 +58,24 @@ let _getHeader = (headers: dict<string>, name: string): option<string> =>
58
58
  | None => headers->Dict.get(name)
59
59
  }
60
60
 
61
+ // The domain id when the token carries one, else the `sub`. The fallback is
62
+ // what every account created before the identity store existed relies on: its
63
+ // rows are keyed by the `sub`, and a different answer here would orphan them.
64
+ let _userIdOf = (~claims: dict<JSON.t>, ~sub: string): string =>
65
+ claims
66
+ ->Dict.get(ReventlessCore.Auth_UserId.claim)
67
+ ->Option.flatMap(JSON.Decode.string)
68
+ ->Option.filter(id => id != "")
69
+ ->Option.getOr(sub)
70
+
61
71
  let _identityFromClaims = (claims: 'a): Identity.t => {
62
72
  let claimsDict: dict<JSON.t> = claims->Obj.magic
63
- let userId =
73
+ let sub =
64
74
  claimsDict
65
75
  ->Dict.get("sub")
66
76
  ->Option.flatMap(JSON.Decode.string)
67
77
  ->Option.getOr("anonymous")
78
+ let userId = _userIdOf(~claims=claimsDict, ~sub)
68
79
  let username =
69
80
  claimsDict
70
81
  ->Dict.get("cognito:username")
@@ -154,7 +165,7 @@ let fromAppSyncIdentity = (id: option<appSyncIdentity>): Identity.authResult =>
154
165
  ->Array.map(((k, v)) => (k, v->JSON.Decode.string->Option.getOr(v->JSON.stringify)))
155
166
  ->Dict.fromArray
156
167
  Authenticated({
157
- userId: sub,
168
+ userId: _userIdOf(~claims=rawClaims, ~sub),
158
169
  username,
159
170
  groups,
160
171
  claims,
@@ -4,6 +4,7 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
5
  import * as Pulumi from "@pulumi/pulumi";
6
6
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as Auth_UserId$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Auth/Auth_UserId.res.mjs";
7
8
  import * as Platform_Stack$ReventlessAws from "../../Platform_Stack.res.mjs";
8
9
 
9
10
  let _decodeJwtClaims = (function(token) {
@@ -31,8 +32,13 @@ function _getHeader(headers, name) {
31
32
  }
32
33
  }
33
34
 
35
+ function _userIdOf(claims, sub) {
36
+ return Stdlib_Option.getOr(Stdlib_Option.filter(Stdlib_Option.flatMap(claims[Auth_UserId$ReventlessCore.claim], Stdlib_JSON.Decode.string), id => id !== ""), sub);
37
+ }
38
+
34
39
  function _identityFromClaims(claims) {
35
- let userId = Stdlib_Option.getOr(Stdlib_Option.flatMap(claims["sub"], Stdlib_JSON.Decode.string), "anonymous");
40
+ let sub = Stdlib_Option.getOr(Stdlib_Option.flatMap(claims["sub"], Stdlib_JSON.Decode.string), "anonymous");
41
+ let userId = _userIdOf(claims, sub);
36
42
  let username = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.orElse(claims["cognito:username"], claims["username"]), Stdlib_JSON.Decode.string), userId);
37
43
  let groups = Stdlib_Option.getOr(Stdlib_Option.map(claims["cognito:groups"], g => g), []);
38
44
  return {
@@ -81,7 +87,7 @@ function fromAppSyncIdentity(id) {
81
87
  return {
82
88
  TAG: "Authenticated",
83
89
  _0: {
84
- userId: sub,
90
+ userId: _userIdOf(rawClaims, sub),
85
91
  username: username,
86
92
  groups: groups,
87
93
  claims: claims,
@@ -127,6 +133,7 @@ export {
127
133
  _decodeJwtClaims,
128
134
  _bearerToken,
129
135
  _getHeader,
136
+ _userIdOf,
130
137
  _identityFromClaims,
131
138
  authenticate,
132
139
  fromAppSyncIdentity,
@@ -0,0 +1,52 @@
1
+ // The rows mapping a Cognito `sub` to the domain's own user id, and where they
2
+ // live. Read by the pre-token-generation trigger on every mint, written only
3
+ // when an account is created (`provision-accounts`, and the Cognito identity
4
+ // provider when it exists).
5
+ //
6
+ // 🚨 **The rows follow the identity provider, not the stack**, exactly as the
7
+ // active-role store's do and for the same reason: a pool holds one pre-token
8
+ // trigger, so two stacks sharing a pool with a table each would have the winning
9
+ // trigger reading rows the other stack wrote nowhere near. A stack that owns its
10
+ // pool owns this table; a stack handed an `identityProviderId` looks it up at
11
+ // [Auth_IdentityStore_Schema.derivedStoreName] and never creates it.
12
+
13
+ type storeTable = Auth_ActiveRoleStore.storeTable
14
+
15
+ /** Which store a deployment's trigger reads. Total and pure, like the active-role
16
+ store's: whether the provider is ours decides it, and nothing else. */
17
+ let chooseStore = (~identityProviderId: option<string>): Auth_ActiveRoleStore.storeChoice =>
18
+ switch identityProviderId {
19
+ | None => StackScoped
20
+ | Some(id) => ProviderScoped(Auth_IdentityStore_Schema.derivedStoreName(~identityProviderId=id))
21
+ }
22
+
23
+ /** One row per subject — the key schema is [Auth_IdentityStore_Schema]'s — plus
24
+ `userId`, the domain id minted for it. */
25
+ let makeTable = (
26
+ ~name: string="IdentityStore",
27
+ ~opts: Pulumi.ComponentResource.options,
28
+ ): storeTable => {
29
+ let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
30
+ let table = Util_DynamoDb.makeTable(
31
+ ~attributes=[{name: Auth_IdentityStore_Schema.partitionKey, type_: "S"}],
32
+ ~tags=AWS.Tags.make(
33
+ ~name=name ++ "Table",
34
+ ~kind=ReventlessCore.ComponentType.Platform,
35
+ ~role=Auth,
36
+ ~scope=Platform,
37
+ ),
38
+ ~opts,
39
+ name,
40
+ )
41
+ {name: table.name, arn: table.arn}
42
+ }
43
+
44
+ /** The store a resolved provider calls for — created or looked up. */
45
+ let resolveTable = (
46
+ ~choice: Auth_ActiveRoleStore.storeChoice,
47
+ ~opts: Pulumi.ComponentResource.options,
48
+ ): storeTable =>
49
+ switch choice {
50
+ | StackScoped => makeTable(~opts)
51
+ | ProviderScoped(tableName) => Auth_ActiveRoleStore.lookupTable(~tableName)
52
+ }
@@ -0,0 +1,46 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
4
+ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
5
+ import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
6
+ import * as Auth_ActiveRoleStore$ReventlessAws from "./Auth_ActiveRoleStore.res.mjs";
7
+ import * as Auth_IdentityStore_Schema$ReventlessAws from "./Auth_IdentityStore_Schema.res.mjs";
8
+
9
+ function chooseStore(identityProviderId) {
10
+ if (identityProviderId !== undefined) {
11
+ return {
12
+ TAG: "ProviderScoped",
13
+ _0: Auth_IdentityStore_Schema$ReventlessAws.derivedStoreName(identityProviderId)
14
+ };
15
+ } else {
16
+ return "StackScoped";
17
+ }
18
+ }
19
+
20
+ function makeTable(nameOpt, opts) {
21
+ let name = nameOpt !== undefined ? nameOpt : "IdentityStore";
22
+ let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
23
+ let table = Util_DynamoDb$ReventlessAws.makeTable([{
24
+ name: Auth_IdentityStore_Schema$ReventlessAws.partitionKey,
25
+ type: "S"
26
+ }], undefined, undefined, undefined, AWS_Tags$ReventlessAws.make(name + "Table", "Platform", "Auth", "Platform", undefined, undefined, undefined, undefined), opts$1, name);
27
+ return {
28
+ name: table.name,
29
+ arn: table.arn
30
+ };
31
+ }
32
+
33
+ function resolveTable(choice, opts) {
34
+ if (typeof choice !== "object") {
35
+ return makeTable(undefined, opts);
36
+ } else {
37
+ return Auth_ActiveRoleStore$ReventlessAws.lookupTable(choice._0);
38
+ }
39
+ }
40
+
41
+ export {
42
+ chooseStore,
43
+ makeTable,
44
+ resolveTable,
45
+ }
46
+ /* AWS_Tags-ReventlessAws Not a pure module */
@@ -0,0 +1,39 @@
1
+ // The identity store's identity: what it is called, and how its rows are keyed.
2
+ // The mapping from a Cognito `sub` to the domain's own user id — see
3
+ // [ReventlessCore.Auth_UserId] for why the two differ.
4
+ //
5
+ // One definition for the deploy that creates or looks the table up
6
+ // ([Auth_IdentityStore]), the pre-token trigger that reads it
7
+ // ([Auth_ActiveRoleTrigger_Ops]), and the provisioning scripts that write it
8
+ // (`provision-identity`, `provision-accounts`). Free of Pulumi and of the AWS
9
+ // SDK for the reason [Auth_ActiveRoleStore_Schema] gives.
10
+
11
+ /** Derived from the provider, never configured, for the reason the active-role
12
+ store's name is: two stacks on one pool cannot name different stores. */
13
+ let derivedStoreName = (~identityProviderId: string): string =>
14
+ `ReventlessIdentityStore-${identityProviderId}`
15
+
16
+ /** The Cognito `sub`. The trigger reads by it on every token it mints. */
17
+ let partitionKey = "id"
18
+
19
+ /** The domain id the row maps that subject to. */
20
+ let userIdAttribute = "userId"
21
+
22
+ /** Keyed on the subject alone: one identity, one domain id, on every platform
23
+ the provider serves. The reverse direction, domain id to subject, is an
24
+ index to add when the first caller for it exists; a global index can be
25
+ added to a live table. */
26
+ let expectedKeySchema: array<(string, string)> = [(partitionKey, "HASH")]
27
+
28
+ /** Why an existing table under this name cannot serve, if it cannot. A table
29
+ with the wrong key is found by the deploy and missed by every read, so the
30
+ claim is never added and every new account silently keeps its `sub`. */
31
+ let keySchemaRefusal = (~tableName: string, ~actual: array<(string, string)>): option<string> => {
32
+ let actualKey = Auth_ActiveRoleStore_Schema.describeKeySchema(actual)
33
+ let wantedKey = Auth_ActiveRoleStore_Schema.describeKeySchema(expectedKeySchema)
34
+ actualKey == wantedKey
35
+ ? None
36
+ : Some(
37
+ `table "${tableName}" already exists with key schema [${actualKey}], but the identity store needs [${wantedKey}]. A row written under one key is invisible to a read under the other, so every new account would silently keep its provider id. Rename or remove the table; its rows are a mapping the provisioning run rewrites.`,
38
+ )
39
+ }
@@ -0,0 +1,35 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Auth_ActiveRoleStore_Schema$ReventlessAws from "./Auth_ActiveRoleStore_Schema.res.mjs";
4
+
5
+ function derivedStoreName(identityProviderId) {
6
+ return `ReventlessIdentityStore-` + identityProviderId;
7
+ }
8
+
9
+ let partitionKey = "id";
10
+
11
+ let expectedKeySchema = [[
12
+ partitionKey,
13
+ "HASH"
14
+ ]];
15
+
16
+ function keySchemaRefusal(tableName, actual) {
17
+ let actualKey = Auth_ActiveRoleStore_Schema$ReventlessAws.describeKeySchema(actual);
18
+ let wantedKey = Auth_ActiveRoleStore_Schema$ReventlessAws.describeKeySchema(expectedKeySchema);
19
+ if (actualKey === wantedKey) {
20
+ return;
21
+ } else {
22
+ return `table "` + tableName + `" already exists with key schema [` + actualKey + `], but the identity store needs [` + wantedKey + `]. A row written under one key is invisible to a read under the other, so every new account would silently keep its provider id. Rename or remove the table; its rows are a mapping the provisioning run rewrites.`;
23
+ }
24
+ }
25
+
26
+ let userIdAttribute = "userId";
27
+
28
+ export {
29
+ derivedStoreName,
30
+ partitionKey,
31
+ userIdAttribute,
32
+ expectedKeySchema,
33
+ keySchemaRefusal,
34
+ }
35
+ /* No side effect */
@@ -0,0 +1,251 @@
1
+ // The identity provider a deployed platform provisions: principals in its
2
+ // Cognito pool, and each new one's domain id in the identity store the pool's
3
+ // pre-token trigger reads. The AWS counterpart of the local platform's
4
+ // `Capability_IdentityProvider_UserStore`, and reached the same way — on the
5
+ // capability record a slice's `translate` receives.
6
+ //
7
+ // Pulumi-free so it ships in the slice Lambda. The pool calls go through an
8
+ // injected `ops` record so the sequencing below — what is undone when a later
9
+ // step fails, which failures are verdicts — is testable without a pool.
10
+ //
11
+ // The principal's handle is the username, and the username is the address the
12
+ // person is reached at, as on the local platform.
13
+
14
+ open Reventless
15
+
16
+ /** The calls a provider makes, one per pool or store operation. Each throws the
17
+ SDK's error on failure; [failureOf] classifies it. */
18
+ type ops = {
19
+ /** Creates the account and answers its `sub`. */
20
+ createUser: (~username: string, ~attributes: array<(string, string)>) => promise<option<string>>,
21
+ subOf: (~username: string) => promise<option<string>>,
22
+ setPassword: (~username: string, ~password: string) => promise<unit>,
23
+ addToGroup: (~username: string, ~group: string) => promise<unit>,
24
+ removeFromGroup: (~username: string, ~group: string) => promise<unit>,
25
+ deleteUser: (~username: string) => promise<unit>,
26
+ putUserId: (~sub: string, ~userId: string) => promise<unit>,
27
+ deleteUserId: (~sub: string) => promise<unit>,
28
+ }
29
+
30
+ @get @return(nullable) external errorName: JsExn.t => option<string> = "name"
31
+
32
+ let codeOf = (exn: exn): option<string> => exn->JsExn.fromException->Option.flatMap(errorName)
33
+
34
+ let messageOf = (exn: exn): string =>
35
+ exn
36
+ ->JsExn.fromException
37
+ ->Option.flatMap(JsExn.message)
38
+ ->Option.getOr("the identity provider failed")
39
+
40
+ /**
41
+ A pool failure as the capability's answer.
42
+
43
+ `Refused` only where the pool has judged the request itself — a password its
44
+ policy rejects, an attribute it will not take, a group that does not exist.
45
+ Everything else is `Unavailable`, retried: throttling, a network blip, a role
46
+ missing its grant. A caller reaching `createPrincipal` has already recorded that
47
+ the address was proven, and treating an outage as a verdict would strand them.
48
+ */
49
+ let failureOf = (exn: exn): IdentityProvider.failure =>
50
+ switch codeOf(exn) {
51
+ | Some("UsernameExistsException" | "AliasExistsException") => Conflict
52
+ | Some(
53
+ "InvalidPasswordException"
54
+ | "InvalidParameterException"
55
+ | "UserNotFoundException"
56
+ | "ResourceNotFoundException",
57
+ ) =>
58
+ Refused(messageOf(exn))
59
+ | _ => Unavailable(messageOf(exn))
60
+ }
61
+
62
+ let usernameOf = (contact: Messaging.recipient): result<string, IdentityProvider.failure> =>
63
+ switch contact {
64
+ | ToEmail(email) => Ok(Email.toString(email))
65
+ | ToSms(phone) => Ok(Phone.toString(phone))
66
+ | ToPush(_) => Error(Refused("a push address cannot be signed in with"))
67
+ }
68
+
69
+ /** Verified, because a principal is only created after the address was proven —
70
+ the ordering the registration chapter is built on. */
71
+ let attributesOf = (contact: Messaging.recipient): array<(string, string)> =>
72
+ switch contact {
73
+ | ToEmail(email) => [("email", Email.toString(email)), ("email_verified", "true")]
74
+ | ToSms(phone) => [("phone_number", Phone.toString(phone)), ("phone_number_verified", "true")]
75
+ | ToPush(_) => []
76
+ }
77
+
78
+ let attempt = async (run: unit => promise<'a>): result<'a, IdentityProvider.failure> =>
79
+ try Ok(await run()) catch {
80
+ | exn => Error(failureOf(exn))
81
+ }
82
+
83
+ let withOps = (ops: ops, ~hasStore: bool): IdentityProvider.t => {
84
+ // Everything after the account exists, in order. A failure here undoes the
85
+ // account before answering, so a retry starts from nothing: left in place, the
86
+ // retry would meet `UsernameExistsException` and report a `Conflict` for a
87
+ // person whose first attempt merely timed out.
88
+ let complete = async (~username, ~sub, ~credential: IdentityProvider.credential, ~groups) => {
89
+ let steps = async () => {
90
+ switch credential {
91
+ | Password(password) => await ops.setPassword(~username, ~password)
92
+ | NoCredential => ()
93
+ }
94
+ for i in 0 to groups->Array.length - 1 {
95
+ await ops.addToGroup(~username, ~group=groups->Array.getUnsafe(i))
96
+ }
97
+
98
+ // Minted here and only here: an account the pool made before the store
99
+ // existed keeps its `sub`, and nothing re-keys it.
100
+ if hasStore {
101
+ await ops.putUserId(~sub, ~userId=ReventlessCore.Auth_UserId.mint())
102
+ }
103
+ }
104
+ switch await attempt(steps) {
105
+ | Ok() => Ok({IdentityProvider.providerId: username})
106
+ | Error(_) as failed =>
107
+ let _ = await attempt(() => ops.deleteUser(~username))
108
+ failed
109
+ }
110
+ }
111
+
112
+ let createPrincipal = async (~contact, ~credential, ~groups) =>
113
+ switch usernameOf(contact) {
114
+ | Error(_) as e => e
115
+ | Ok(username) =>
116
+ switch await attempt(() => ops.createUser(~username, ~attributes=attributesOf(contact))) {
117
+ | Error(_) as e => e
118
+ | Ok(created) =>
119
+ let sub = switch created {
120
+ | Some(_) => created
121
+ | None => (await attempt(() => ops.subOf(~username)))->Result.getOr(None)
122
+ }
123
+ switch sub {
124
+ | None =>
125
+ let _ = await attempt(() => ops.deleteUser(~username))
126
+ Error(IdentityProvider.Unavailable(`the pool created ${username} but named no sub`))
127
+ | Some(sub) => await complete(~username, ~sub, ~credential, ~groups)
128
+ }
129
+ }
130
+ }
131
+
132
+ let addToGroup = (~principal: IdentityProvider.principal, ~group) =>
133
+ attempt(() => ops.addToGroup(~username=principal.providerId, ~group))
134
+
135
+ let removeFromGroup = (~principal: IdentityProvider.principal, ~group) =>
136
+ attempt(() => ops.removeFromGroup(~username=principal.providerId, ~group))
137
+
138
+ // Deleting what is gone is the retry of a delete that happened, so a missing
139
+ // account answers `Ok`. The mapping row goes last, best effort: a row whose
140
+ // principal is gone is never read on the request path, since no token is
141
+ // minted for it.
142
+ let deletePrincipal = async (~principal: IdentityProvider.principal) => {
143
+ let username = principal.providerId
144
+ switch await attempt(() => ops.subOf(~username)) {
145
+ | Error(Refused(_)) => Ok()
146
+ | Error(_) as e => e
147
+ | Ok(sub) =>
148
+ switch await attempt(() => ops.deleteUser(~username)) {
149
+ | Error(Unavailable(_)) as e => e
150
+ | Error(_) | Ok() =>
151
+ switch (sub, hasStore) {
152
+ | (Some(sub), true) =>
153
+ let _ = await attempt(() => ops.deleteUserId(~sub))
154
+ | _ => ()
155
+ }
156
+ Ok()
157
+ }
158
+ }
159
+ }
160
+
161
+ IdentityProvider.make(
162
+ ~createPrincipal,
163
+ ~addToGroup,
164
+ ~removeFromGroup,
165
+ ~deletePrincipal,
166
+ ~operations=[CreatePrincipal, AddToGroup, RemoveFromGroup, DeletePrincipal],
167
+ )
168
+ }
169
+
170
+ // ── The SDK ──────────────────────────────────────────────────────────────────
171
+
172
+ module Cognito = AwsSdk.CognitoIdentityServiceProvider
173
+
174
+ let subFrom = (attributes: option<array<Cognito.AdminCreateUserCommand.attributeType>>) =>
175
+ attributes->Option.getOr([])->Array.find(a => a.name == "sub")->Option.map(a => a.value)
176
+
177
+ let sdkOps = (~poolId: string, ~identityStore: string): ops => {
178
+ createUser: async (~username, ~attributes) => {
179
+ let out = await Cognito.AdminCreateUserCommand.make({
180
+ userPoolId: poolId,
181
+ username,
182
+ userAttributes: attributes->Array.map(((name, value)) => {
183
+ Cognito.AdminCreateUserCommand.name,
184
+ value,
185
+ }),
186
+ // No invitation: the person has proven the address already, and the
187
+ // invitation would carry a temporary password nobody should use.
188
+ messageAction: "SUPPRESS",
189
+ })->Cognito.AdminCreateUserCommand.send
190
+ out.user->Option.flatMap(u => subFrom(u.attributes))
191
+ },
192
+ subOf: async (~username) => {
193
+ let out = await Cognito.AdminGetUserCommand.make({
194
+ userPoolId: poolId,
195
+ username,
196
+ })->Cognito.AdminGetUserCommand.send
197
+ out.userAttributes->Option.getOr([])->Array.find(a => a.name == "sub")->Option.map(a => a.value)
198
+ },
199
+ setPassword: (~username, ~password) =>
200
+ Cognito.AdminSetUserPasswordCommand.make({
201
+ userPoolId: poolId,
202
+ username,
203
+ password,
204
+ permanent: true,
205
+ })->Cognito.AdminSetUserPasswordCommand.send,
206
+ addToGroup: (~username, ~group) =>
207
+ Cognito.AdminAddUserToGroupCommand.make({
208
+ userPoolId: poolId,
209
+ username,
210
+ groupName: group,
211
+ })->Cognito.AdminAddUserToGroupCommand.send,
212
+ removeFromGroup: (~username, ~group) =>
213
+ Cognito.AdminRemoveUserFromGroupCommand.make({
214
+ userPoolId: poolId,
215
+ username,
216
+ groupName: group,
217
+ })->Cognito.AdminRemoveUserFromGroupCommand.send,
218
+ deleteUser: (~username) =>
219
+ Cognito.AdminDeleteUserCommand.make({
220
+ userPoolId: poolId,
221
+ username,
222
+ })->Cognito.AdminDeleteUserCommand.send,
223
+ putUserId: async (~sub, ~userId) => {
224
+ let _ = await AwsSdk.DynamoDb_DocumentClient.PutCommand.make({
225
+ tableName: identityStore,
226
+ item: JSON.Encode.object(
227
+ Dict.fromArray([
228
+ (Auth_IdentityStore_Schema.partitionKey, JSON.Encode.string(sub)),
229
+ (Auth_IdentityStore_Schema.userIdAttribute, JSON.Encode.string(userId)),
230
+ ]),
231
+ ),
232
+ })->AwsSdk.DynamoDb_DocumentClient.PutCommand.send
233
+ },
234
+ deleteUserId: async (~sub) => {
235
+ let _ = await AwsSdk.DynamoDb_DocumentClient.DeleteCommand.make({
236
+ tableName: identityStore,
237
+ key: Dict.fromArray([(Auth_IdentityStore_Schema.partitionKey, JSON.Encode.string(sub))]),
238
+ })->AwsSdk.DynamoDb_DocumentClient.DeleteCommand.send
239
+ },
240
+ }
241
+
242
+ /**
243
+ The provider a slice Lambda is handed, from the two values the deploy put in its
244
+ environment. No pool is the deployment gap it always was, answered `Unavailable`
245
+ with no operations. No store means the pool predates it: principals are still
246
+ made, and each keeps its `sub` as its id, as every account before the store did.
247
+ */
248
+ let provider = (~poolId: string, ~identityStore: string): IdentityProvider.t =>
249
+ poolId == ""
250
+ ? IdentityProvider.unavailable(~reason="no identity provider is configured for this platform")
251
+ : withOps(sdkOps(~poolId, ~identityStore), ~hasStore=identityStore != "")