@reventlessdev/reventless-aws 3.0.0-alpha.345 → 3.0.0-alpha.347
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 +54 -0
- package/package.json +19 -12
- package/rescript.json +2 -1
- package/run-bake-manifest.mjs +3 -0
- package/run-deploy-app.mjs +3 -0
- package/run-provision-accounts.mjs +3 -0
- package/run-provision-admin.mjs +3 -0
- package/scripts/BakeManifest.res +395 -0
- package/scripts/BakeManifest.res.mjs +487 -0
- package/scripts/DeployApp.res +708 -0
- package/scripts/DeployApp.res.mjs +1015 -0
- package/scripts/DeployManifest.res +84 -0
- package/scripts/DeployManifest.res.mjs +112 -0
- package/scripts/ProvisionAccounts.res +339 -0
- package/scripts/ProvisionAccounts.res.mjs +351 -0
- package/scripts/ProvisionAdmin.res +275 -0
- package/scripts/ProvisionAdmin.res.mjs +289 -0
- package/scripts/ProvisionCognito.res +149 -0
- package/scripts/ProvisionCognito.res.mjs +98 -0
- package/scripts/ProvisionIdentity.res +76 -11
- package/scripts/ProvisionIdentity.res.mjs +72 -7
- package/scripts/ProvisionProvider.res +104 -0
- package/scripts/ProvisionProvider.res.mjs +123 -0
- package/scripts/PulumiCli.res +43 -0
- package/scripts/PulumiCli.res.mjs +106 -0
- package/src/Platform.res +51 -4
- package/src/Platform.res.mjs +32 -14
- package/src/Platform_Stack.res +119 -9
- package/src/Platform_Stack.res.mjs +42 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
- package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
- package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
- package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
- package/src/adapter/Auth/Auth_LoginIdentifier.res +66 -0
- package/src/adapter/Auth/Auth_LoginIdentifier.res.mjs +70 -0
- package/src/adapter/Auth/Auth_SignUpMode.res +78 -0
- package/src/adapter/Auth/Auth_SignUpMode.res.mjs +56 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +5 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +5 -1
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
- package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
- package/src/adapter/Task/TaskBucket_S3.res +1 -3
- package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
- package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
- package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
- package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
- package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
- package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
- package/src/components/Api/AppSync_Adapter.res +56 -72
- package/src/components/Api/AppSync_Adapter.res.mjs +26 -60
- package/src/components/Api/AppSync_SdlDecorate.res +82 -1
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +45 -0
- package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
- package/src/plugin/stack/Plugin_Stack.res +4 -0
- package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
- package/src/util/Util_DeadLetterQueue.res +1 -5
- package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
- package/src/util/Util_StoreLayout.res +15 -2
- package/src/util/Util_StoreLayout.res.mjs +11 -3
- package/tests/AppSync_AdapterTest.res +83 -0
- package/tests/AppSync_AdapterTest.res.mjs +69 -0
- package/tests/Auth_LoginIdentifierTest.res +51 -0
- package/tests/Auth_LoginIdentifierTest.res.mjs +51 -0
- package/tests/Auth_SignUpModeTest.res +56 -0
- package/tests/Auth_SignUpModeTest.res.mjs +53 -0
- package/tests/BakeManifestTest.res +204 -0
- package/tests/BakeManifestTest.res.mjs +259 -0
- package/tests/DcbColdStartPartitionTest.res +32 -0
- package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
- package/tests/DcbUnresolvedPartitionSlice.res +28 -0
- package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
- package/tests/DeployAppTest.res +90 -0
- package/tests/DeployAppTest.res.mjs +133 -0
- package/tests/LambdaLayerLookupTest.res +104 -0
- package/tests/LambdaLayerLookupTest.res.mjs +117 -0
- package/tests/ProvisionAccountsTest.res +168 -0
- package/tests/ProvisionAccountsTest.res.mjs +184 -0
- package/tests/ProvisionAdminTest.res +55 -0
- package/tests/ProvisionAdminTest.res.mjs +66 -0
- package/tests/ProvisionIdentityTest.res +89 -3
- package/tests/ProvisionIdentityTest.res.mjs +66 -1
- package/tests/Util_OwnerScopeEnvTest.res +25 -0
- package/tests/Util_OwnerScopeEnvTest.res.mjs +13 -0
- package/tests/Util_StoreLayoutTest.res +6 -0
- package/tests/Util_StoreLayoutTest.res.mjs +7 -4
- package/tests/dcbColdStartPartition.mjs +66 -0
- package/tests/setup/layerArn.cjs +5 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
4
|
+
import * as AdminGroup$Reventless from "@reventlessdev/reventless-spec/src/types/AdminGroup.res.mjs";
|
|
5
|
+
import * as Util_Password$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Password.res.mjs";
|
|
6
|
+
import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
|
|
7
|
+
import * as ProvisionCognito$ReventlessAws from "./ProvisionCognito.res.mjs";
|
|
8
|
+
import * as ProvisionProvider$ReventlessAws from "./ProvisionProvider.res.mjs";
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv) {
|
|
11
|
+
let acc = {
|
|
12
|
+
TAG: "Ok",
|
|
13
|
+
_0: {
|
|
14
|
+
providerId: undefined,
|
|
15
|
+
stack: undefined,
|
|
16
|
+
email: undefined,
|
|
17
|
+
help: false
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
let i = 0;
|
|
21
|
+
let count = argv.length;
|
|
22
|
+
while (i < count) {
|
|
23
|
+
let flag = argv[i];
|
|
24
|
+
let value = argv[i + 1 | 0];
|
|
25
|
+
let match = acc;
|
|
26
|
+
let exit = 0;
|
|
27
|
+
if (match.TAG === "Ok") {
|
|
28
|
+
let a = match._0;
|
|
29
|
+
let exit$1 = 0;
|
|
30
|
+
switch (flag) {
|
|
31
|
+
case "--email" :
|
|
32
|
+
if (value !== undefined) {
|
|
33
|
+
acc = {
|
|
34
|
+
TAG: "Ok",
|
|
35
|
+
_0: {
|
|
36
|
+
providerId: a.providerId,
|
|
37
|
+
stack: a.stack,
|
|
38
|
+
email: value,
|
|
39
|
+
help: a.help
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
i = i + 2 | 0;
|
|
43
|
+
} else {
|
|
44
|
+
exit = 1;
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case "--provider-id" :
|
|
48
|
+
if (value !== undefined) {
|
|
49
|
+
acc = {
|
|
50
|
+
TAG: "Ok",
|
|
51
|
+
_0: {
|
|
52
|
+
providerId: value,
|
|
53
|
+
stack: a.stack,
|
|
54
|
+
email: a.email,
|
|
55
|
+
help: a.help
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
i = i + 2 | 0;
|
|
59
|
+
} else {
|
|
60
|
+
exit = 1;
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
case "--stack" :
|
|
64
|
+
if (value !== undefined) {
|
|
65
|
+
acc = {
|
|
66
|
+
TAG: "Ok",
|
|
67
|
+
_0: {
|
|
68
|
+
providerId: a.providerId,
|
|
69
|
+
stack: value,
|
|
70
|
+
email: a.email,
|
|
71
|
+
help: a.help
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
i = i + 2 | 0;
|
|
75
|
+
} else {
|
|
76
|
+
exit = 1;
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case "--help" :
|
|
80
|
+
case "-h" :
|
|
81
|
+
exit$1 = 2;
|
|
82
|
+
break;
|
|
83
|
+
default:
|
|
84
|
+
acc = {
|
|
85
|
+
TAG: "Error",
|
|
86
|
+
_0: `unknown argument "` + flag + `"`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (exit$1 === 2) {
|
|
90
|
+
acc = {
|
|
91
|
+
TAG: "Ok",
|
|
92
|
+
_0: {
|
|
93
|
+
providerId: a.providerId,
|
|
94
|
+
stack: a.stack,
|
|
95
|
+
email: a.email,
|
|
96
|
+
help: true
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
i = i + 1 | 0;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
i = count;
|
|
103
|
+
}
|
|
104
|
+
if (exit === 1) {
|
|
105
|
+
acc = {
|
|
106
|
+
TAG: "Error",
|
|
107
|
+
_0: flag + ` needs a value`
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
return acc;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let usage = `
|
|
115
|
+
Make the first administrator of a Reventless deployment.
|
|
116
|
+
|
|
117
|
+
--provider-id <id> The identity provider to create the account in. Usually
|
|
118
|
+
omitted: it falls back to ` + ProvisionProvider$ReventlessAws.envKey + `,
|
|
119
|
+
then to the identityProviderId exported by the selected
|
|
120
|
+
Pulumi stack — which every deployment exports, whether it
|
|
121
|
+
created the pool or was handed one.
|
|
122
|
+
--stack <name> Read that output from this stack instead of the selected
|
|
123
|
+
one. The run always names the stack it used.
|
|
124
|
+
--email <address> The address they sign in with.
|
|
125
|
+
|
|
126
|
+
Creates the "` + AdminGroup$Reventless.name + `" group if it is missing, the account if
|
|
127
|
+
it is missing, a working password, and the membership between them. Provisions
|
|
128
|
+
nothing that a platform stack owns. Region and credentials come from the
|
|
129
|
+
environment, as for any AWS SDK call.
|
|
130
|
+
`;
|
|
131
|
+
|
|
132
|
+
async function checkPoolAcceptsEmail(providerId) {
|
|
133
|
+
let e = await ProvisionCognito$ReventlessAws.usernameAttributes(providerId);
|
|
134
|
+
if (e.TAG !== "Ok") {
|
|
135
|
+
return e;
|
|
136
|
+
}
|
|
137
|
+
let attributes = e._0;
|
|
138
|
+
if (attributes.length === 0 || attributes.includes("email")) {
|
|
139
|
+
console.log(`pool ` + providerId);
|
|
140
|
+
return {
|
|
141
|
+
TAG: "Ok",
|
|
142
|
+
_0: undefined
|
|
143
|
+
};
|
|
144
|
+
} else {
|
|
145
|
+
return {
|
|
146
|
+
TAG: "Error",
|
|
147
|
+
_0: `pool "` + providerId + `" signs in on ` + attributes.join(", ") + `, not email, so an account created from --email could never authenticate. The sign-in attribute is fixed at pool creation and no update can change it.`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function describe(outcome) {
|
|
153
|
+
if (outcome === "Created") {
|
|
154
|
+
return "created";
|
|
155
|
+
} else {
|
|
156
|
+
return "already present";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function ensureGroup(providerId, group) {
|
|
161
|
+
let outcome = await ProvisionCognito$ReventlessAws.ensureGroup(providerId, group, "Reventless administrators");
|
|
162
|
+
console.log(`group ` + group + ` (` + describe(outcome) + `)`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function ensureUser(providerId, email) {
|
|
166
|
+
let attributes = [
|
|
167
|
+
{
|
|
168
|
+
Name: "email",
|
|
169
|
+
Value: email
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
Name: "email_verified",
|
|
173
|
+
Value: "true"
|
|
174
|
+
}
|
|
175
|
+
];
|
|
176
|
+
let outcome = await ProvisionCognito$ReventlessAws.ensureUser(providerId, email, attributes);
|
|
177
|
+
console.log(`user ` + email + ` (` + describe(outcome) + `)`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function setPassword(providerId, email, password) {
|
|
181
|
+
await ProvisionCognito$ReventlessAws.setPassword(providerId, email, password);
|
|
182
|
+
console.log(`password (set, permanent)`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function addToGroup(providerId, email, group) {
|
|
186
|
+
await ProvisionCognito$ReventlessAws.addToGroup(providerId, email, group);
|
|
187
|
+
console.log(`member ` + email + ` in ` + group);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let passwordNote = `
|
|
191
|
+
🚨 This is a bootstrap credential and it is printed above because that is the only
|
|
192
|
+
place it exists — Cognito keeps no recoverable copy. It is now in this
|
|
193
|
+
terminal's scrollback. Sign in, change it, and do not paste it where logs are
|
|
194
|
+
kept. Running this again mints a new one.`;
|
|
195
|
+
|
|
196
|
+
function signInDetails(providerId, email, password, group) {
|
|
197
|
+
return `
|
|
198
|
+
The first administrator is ready.
|
|
199
|
+
|
|
200
|
+
provider ` + providerId + `
|
|
201
|
+
sign in as ` + email + `
|
|
202
|
+
password ` + password + `
|
|
203
|
+
group ` + group + `
|
|
204
|
+
` + passwordNote + `
|
|
205
|
+
|
|
206
|
+
This script bootstraps the first account only, which is the one that cannot be
|
|
207
|
+
made through a signed-in session because there is no signed-in session yet. The
|
|
208
|
+
rest of the cast is a list rather than a command: declare it in
|
|
209
|
+
.reventless/users.yaml and run \`pnpm exec provision-accounts\`.`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function run() {
|
|
213
|
+
let e = parseArgs(process.argv.slice(2, process.argv.length));
|
|
214
|
+
if (e.TAG !== "Ok") {
|
|
215
|
+
return e;
|
|
216
|
+
}
|
|
217
|
+
let args = e._0;
|
|
218
|
+
if (args.help) {
|
|
219
|
+
console.log(usage);
|
|
220
|
+
return {
|
|
221
|
+
TAG: "Ok",
|
|
222
|
+
_0: undefined
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
let email = args.email;
|
|
226
|
+
if (email === undefined) {
|
|
227
|
+
return {
|
|
228
|
+
TAG: "Error",
|
|
229
|
+
_0: "--email is required — it is the address the administrator signs in with"
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
let e$1 = ProvisionProvider$ReventlessAws.resolve(args.providerId, args.stack);
|
|
233
|
+
if (e$1.TAG !== "Ok") {
|
|
234
|
+
return e$1;
|
|
235
|
+
}
|
|
236
|
+
let match = e$1._0;
|
|
237
|
+
let providerId = match[0];
|
|
238
|
+
console.log(`provider ` + providerId + ` (from ` + ProvisionProvider$ReventlessAws.describe(match[1]) + `)`);
|
|
239
|
+
let e$2 = await checkPoolAcceptsEmail(providerId);
|
|
240
|
+
if (e$2.TAG !== "Ok") {
|
|
241
|
+
return e$2;
|
|
242
|
+
}
|
|
243
|
+
let password = Util_Password$Reventless.generate();
|
|
244
|
+
await ensureGroup(providerId, AdminGroup$Reventless.name);
|
|
245
|
+
await ensureUser(providerId, email);
|
|
246
|
+
await setPassword(providerId, email, password);
|
|
247
|
+
await addToGroup(providerId, email, AdminGroup$Reventless.name);
|
|
248
|
+
console.log(signInDetails(providerId, email, password, AdminGroup$Reventless.name));
|
|
249
|
+
return {
|
|
250
|
+
TAG: "Ok",
|
|
251
|
+
_0: undefined
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function main() {
|
|
256
|
+
let message;
|
|
257
|
+
try {
|
|
258
|
+
message = await run();
|
|
259
|
+
} catch (raw_exn) {
|
|
260
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
261
|
+
console.error(`provision-admin: ` + Util_AwsError$ReventlessAws.describe(exn));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (message.TAG === "Ok") {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
console.error(`provision-admin: ` + message._0);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let Cognito;
|
|
273
|
+
|
|
274
|
+
export {
|
|
275
|
+
Cognito,
|
|
276
|
+
parseArgs,
|
|
277
|
+
usage,
|
|
278
|
+
checkPoolAcceptsEmail,
|
|
279
|
+
describe,
|
|
280
|
+
ensureGroup,
|
|
281
|
+
ensureUser,
|
|
282
|
+
setPassword,
|
|
283
|
+
addToGroup,
|
|
284
|
+
passwordNote,
|
|
285
|
+
signInDetails,
|
|
286
|
+
run,
|
|
287
|
+
main,
|
|
288
|
+
}
|
|
289
|
+
/* Util_Password-Reventless Not a pure module */
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/***
|
|
2
|
+
The Cognito operations a provisioning run performs, once.
|
|
3
|
+
|
|
4
|
+
Two bins now do the same four things — ensure a group, ensure an account, set a
|
|
5
|
+
permanent password, apply a membership — and `provision-admin` did them first.
|
|
6
|
+
Extracted here rather than copied for the reason [Auth_LoginIdentifier] and
|
|
7
|
+
[Auth_SignUpMode] were: a second copy drifts, and the half that drifts is
|
|
8
|
+
whichever one nobody re-reads.
|
|
9
|
+
|
|
10
|
+
🚨 **These report rather than print.** `provision-admin` has a line format its
|
|
11
|
+
tests and its docs both describe, and an extraction that took the printing with it
|
|
12
|
+
would have changed that script's output while claiming to change nothing. Each
|
|
13
|
+
caller phrases its own run; what is shared is what the API does.
|
|
14
|
+
|
|
15
|
+
The password generator that used to sit beside these moved further, to
|
|
16
|
+
[Util_Password] in `spec` — the manifest's platform-neutral half needs it, and
|
|
17
|
+
that half must not reach into this package.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
open AwsSdk
|
|
21
|
+
|
|
22
|
+
module Cognito = CognitoIdentityServiceProvider
|
|
23
|
+
|
|
24
|
+
/** Whether the call made the thing or found it. Both are success; the difference
|
|
25
|
+
is only worth a word in the output, and a caller that treats "already there" as
|
|
26
|
+
a failure makes the second run of a provisioning script an error. */
|
|
27
|
+
type outcome =
|
|
28
|
+
| Created
|
|
29
|
+
| AlreadyPresent
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
The sign-in attributes a pool was created with.
|
|
33
|
+
|
|
34
|
+
Fixed at pool creation and unchangeable — see [Auth_LoginIdentifier] — so this is
|
|
35
|
+
the one pool fact a provisioning run must read before it writes. An empty list
|
|
36
|
+
means the pool signs in on a plain username, which takes an email-shaped one
|
|
37
|
+
happily; a list naming `email` means a plain username can never authenticate.
|
|
38
|
+
|
|
39
|
+
Described rather than assumed because the symptom of getting it wrong is a
|
|
40
|
+
correctly-created account that simply cannot log in, arriving at a login screen
|
|
41
|
+
far from the run that caused it.
|
|
42
|
+
*/
|
|
43
|
+
let usernameAttributes = async (~providerId: string): result<array<string>, string> => {
|
|
44
|
+
let described = await Cognito.DescribeUserPoolCommand.make({
|
|
45
|
+
userPoolId: providerId,
|
|
46
|
+
})->Cognito.DescribeUserPoolCommand.send
|
|
47
|
+
switch described.userPool {
|
|
48
|
+
| None => Error(`DescribeUserPool returned nothing for "${providerId}"`)
|
|
49
|
+
| Some(pool) => Ok(pool.usernameAttributes->Option.getOr([]))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The group, where a stack has not already declared it.
|
|
54
|
+
|
|
55
|
+
In auto mode the platform stack declares the administrator group as an ordinary
|
|
56
|
+
child of the pool it owns, so this is a no-op. On a supplied pool no stack does,
|
|
57
|
+
because two stacks sharing a provider would both declare it and the second would
|
|
58
|
+
fail on a name that already exists. Every other group a manifest names is in the
|
|
59
|
+
same position: nothing else creates it. */
|
|
60
|
+
let ensureGroup = async (~providerId: string, ~group: string, ~description: string): outcome =>
|
|
61
|
+
try {
|
|
62
|
+
let _ = await Cognito.CreateGroupCommand.make({
|
|
63
|
+
groupName: group,
|
|
64
|
+
userPoolId: providerId,
|
|
65
|
+
description,
|
|
66
|
+
})->Cognito.CreateGroupCommand.send
|
|
67
|
+
Created
|
|
68
|
+
} catch {
|
|
69
|
+
| exn if exn->Util_AwsError.hasCode(~code="GroupExistsException") => AlreadyPresent
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
The account, where it is not already there.
|
|
74
|
+
|
|
75
|
+
`MessageAction: "SUPPRESS"` stops Cognito emailing an invitation. The invitation
|
|
76
|
+
carries the temporary password the run is about to replace, so sending it would
|
|
77
|
+
tell the new account holder to sign in with a credential that no longer works —
|
|
78
|
+
and on an admin-create-only pool these accounts may carry no deliverable address
|
|
79
|
+
at all.
|
|
80
|
+
|
|
81
|
+
Attributes are the caller's: an administrator created from an email address wants
|
|
82
|
+
`email` and `email_verified` stamped, and a demo account called `shopper` has
|
|
83
|
+
neither and needs none.
|
|
84
|
+
*/
|
|
85
|
+
let ensureUser = async (
|
|
86
|
+
~providerId: string,
|
|
87
|
+
~username: string,
|
|
88
|
+
~attributes: array<Cognito.AdminCreateUserCommand.attributeType>,
|
|
89
|
+
): outcome =>
|
|
90
|
+
try {
|
|
91
|
+
let _ = await Cognito.AdminCreateUserCommand.make({
|
|
92
|
+
userPoolId: providerId,
|
|
93
|
+
username,
|
|
94
|
+
userAttributes: attributes,
|
|
95
|
+
messageAction: "SUPPRESS",
|
|
96
|
+
})->Cognito.AdminCreateUserCommand.send
|
|
97
|
+
Created
|
|
98
|
+
} catch {
|
|
99
|
+
| exn if exn->Util_AwsError.hasCode(~code="UsernameExistsException") => AlreadyPresent
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
A password the account can actually be used with.
|
|
104
|
+
|
|
105
|
+
🚨 **The step it is easiest to leave out, and leaving it out reproduces the defect
|
|
106
|
+
these scripts exist to remove.** An administrator-created account holds a
|
|
107
|
+
*temporary* password and lands in `FORCE_CHANGE_PASSWORD`: the first sign-in meets
|
|
108
|
+
a `NEW_PASSWORD_REQUIRED` challenge, which the host UI is not known to handle and
|
|
109
|
+
which the seed client rejects outright — it gets a `ChallengeName` where it
|
|
110
|
+
expects a token. So this sets a permanent password, and the account is `CONFIRMED`
|
|
111
|
+
when it returns.
|
|
112
|
+
*/
|
|
113
|
+
let setPassword = async (~providerId: string, ~username: string, ~password: string): unit =>
|
|
114
|
+
await Cognito.AdminSetUserPasswordCommand.make({
|
|
115
|
+
userPoolId: providerId,
|
|
116
|
+
username,
|
|
117
|
+
password,
|
|
118
|
+
permanent: true,
|
|
119
|
+
})->Cognito.AdminSetUserPasswordCommand.send
|
|
120
|
+
|
|
121
|
+
/** Idempotent at the API: adding a user already in the group is not an error, so
|
|
122
|
+
this needs no existence check of its own. */
|
|
123
|
+
let addToGroup = async (~providerId: string, ~username: string, ~group: string): unit =>
|
|
124
|
+
await Cognito.AdminAddUserToGroupCommand.make({
|
|
125
|
+
username,
|
|
126
|
+
groupName: group,
|
|
127
|
+
userPoolId: providerId,
|
|
128
|
+
})->Cognito.AdminAddUserToGroupCommand.send
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
The id the pool minted for an account.
|
|
132
|
+
|
|
133
|
+
No request supplies it and `AdminCreateUser` does not return it, so it is read
|
|
134
|
+
back — the same way for an account this run just made and one that was already
|
|
135
|
+
there, which is what lets a re-run correct a manifest written by hand.
|
|
136
|
+
|
|
137
|
+
`None` when the pool answers without a `sub`, which should not happen; the caller
|
|
138
|
+
reports it rather than writing a blank id into a file that is read as authority.
|
|
139
|
+
*/
|
|
140
|
+
let subOf = async (~providerId: string, ~username: string): option<string> => {
|
|
141
|
+
let user = await Cognito.AdminGetUserCommand.make({
|
|
142
|
+
userPoolId: providerId,
|
|
143
|
+
username,
|
|
144
|
+
})->Cognito.AdminGetUserCommand.send
|
|
145
|
+
user.userAttributes
|
|
146
|
+
->Option.getOr([])
|
|
147
|
+
->Array.find(attribute => attribute.name == "sub")
|
|
148
|
+
->Option.map(attribute => attribute.value)
|
|
149
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
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 Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
5
|
+
import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
|
|
6
|
+
import * as CognitoIdentityServiceProvider$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/CognitoIdentityServiceProvider.res.mjs";
|
|
7
|
+
import * as ClientCognitoIdentityProvider from "@aws-sdk/client-cognito-identity-provider";
|
|
8
|
+
|
|
9
|
+
async function usernameAttributes(providerId) {
|
|
10
|
+
let described = await CognitoIdentityServiceProvider$AwsSdk.DescribeUserPoolCommand.send(new ClientCognitoIdentityProvider.DescribeUserPoolCommand({
|
|
11
|
+
UserPoolId: providerId
|
|
12
|
+
}));
|
|
13
|
+
let pool = described.UserPool;
|
|
14
|
+
if (pool !== undefined) {
|
|
15
|
+
return {
|
|
16
|
+
TAG: "Ok",
|
|
17
|
+
_0: Stdlib_Option.getOr(pool.UsernameAttributes, [])
|
|
18
|
+
};
|
|
19
|
+
} else {
|
|
20
|
+
return {
|
|
21
|
+
TAG: "Error",
|
|
22
|
+
_0: `DescribeUserPool returned nothing for "` + providerId + `"`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function ensureGroup(providerId, group, description) {
|
|
28
|
+
try {
|
|
29
|
+
await CognitoIdentityServiceProvider$AwsSdk.CreateGroupCommand.send(new ClientCognitoIdentityProvider.CreateGroupCommand({
|
|
30
|
+
GroupName: group,
|
|
31
|
+
UserPoolId: providerId,
|
|
32
|
+
Description: description
|
|
33
|
+
}));
|
|
34
|
+
return "Created";
|
|
35
|
+
} catch (raw_exn) {
|
|
36
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
37
|
+
if (Util_AwsError$ReventlessAws.hasCode(exn, "GroupExistsException")) {
|
|
38
|
+
return "AlreadyPresent";
|
|
39
|
+
}
|
|
40
|
+
throw exn;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function ensureUser(providerId, username, attributes) {
|
|
45
|
+
try {
|
|
46
|
+
await CognitoIdentityServiceProvider$AwsSdk.AdminCreateUserCommand.send(new ClientCognitoIdentityProvider.AdminCreateUserCommand({
|
|
47
|
+
UserPoolId: providerId,
|
|
48
|
+
Username: username,
|
|
49
|
+
UserAttributes: attributes,
|
|
50
|
+
MessageAction: "SUPPRESS"
|
|
51
|
+
}));
|
|
52
|
+
return "Created";
|
|
53
|
+
} catch (raw_exn) {
|
|
54
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
55
|
+
if (Util_AwsError$ReventlessAws.hasCode(exn, "UsernameExistsException")) {
|
|
56
|
+
return "AlreadyPresent";
|
|
57
|
+
}
|
|
58
|
+
throw exn;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function setPassword(providerId, username, password) {
|
|
63
|
+
return await CognitoIdentityServiceProvider$AwsSdk.AdminSetUserPasswordCommand.send(new ClientCognitoIdentityProvider.AdminSetUserPasswordCommand({
|
|
64
|
+
UserPoolId: providerId,
|
|
65
|
+
Username: username,
|
|
66
|
+
Password: password,
|
|
67
|
+
Permanent: true
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function addToGroup(providerId, username, group) {
|
|
72
|
+
return await CognitoIdentityServiceProvider$AwsSdk.AdminAddUserToGroupCommand.send(new ClientCognitoIdentityProvider.AdminAddUserToGroupCommand({
|
|
73
|
+
Username: username,
|
|
74
|
+
GroupName: group,
|
|
75
|
+
UserPoolId: providerId
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function subOf(providerId, username) {
|
|
80
|
+
let user = await CognitoIdentityServiceProvider$AwsSdk.AdminGetUserCommand.send(new ClientCognitoIdentityProvider.AdminGetUserCommand({
|
|
81
|
+
UserPoolId: providerId,
|
|
82
|
+
Username: username
|
|
83
|
+
}));
|
|
84
|
+
return Stdlib_Option.map(Stdlib_Option.getOr(user.UserAttributes, []).find(attribute => attribute.Name === "sub"), attribute => attribute.Value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let Cognito;
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
Cognito,
|
|
91
|
+
usernameAttributes,
|
|
92
|
+
ensureGroup,
|
|
93
|
+
ensureUser,
|
|
94
|
+
setPassword,
|
|
95
|
+
addToGroup,
|
|
96
|
+
subOf,
|
|
97
|
+
}
|
|
98
|
+
/* Util_AwsError-ReventlessAws Not a pure module */
|
|
@@ -38,6 +38,8 @@ module Schema = Auth_ActiveRoleStore_Schema
|
|
|
38
38
|
type args = {
|
|
39
39
|
poolName: string,
|
|
40
40
|
providerId: option<string>,
|
|
41
|
+
loginIdentifier: Auth_LoginIdentifier.t,
|
|
42
|
+
signUpMode: Auth_SignUpMode.t,
|
|
41
43
|
help: bool,
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -47,7 +49,15 @@ let defaultPoolName = "ReventlessIdentity"
|
|
|
47
49
|
`--provider-id` that parsed as "absent" would create a *second* pool beside the
|
|
48
50
|
one the operator meant to extend. */
|
|
49
51
|
let parseArgs = (argv: array<string>): result<args, string> => {
|
|
50
|
-
let acc = ref(
|
|
52
|
+
let acc = ref(
|
|
53
|
+
Ok({
|
|
54
|
+
poolName: defaultPoolName,
|
|
55
|
+
providerId: None,
|
|
56
|
+
loginIdentifier: Auth_LoginIdentifier.default,
|
|
57
|
+
signUpMode: Auth_SignUpMode.default,
|
|
58
|
+
help: false,
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
51
61
|
let i = ref(0)
|
|
52
62
|
let count = argv->Array.length
|
|
53
63
|
while i.contents < count {
|
|
@@ -61,10 +71,23 @@ let parseArgs = (argv: array<string>): result<args, string> => {
|
|
|
61
71
|
| (Ok(a), "--provider-id", Some(v)) =>
|
|
62
72
|
acc := Ok({...a, providerId: Some(v)})
|
|
63
73
|
i := i.contents + 2
|
|
74
|
+
| (Ok(a), "--login-identifier", Some(v)) =>
|
|
75
|
+
acc :=
|
|
76
|
+
Auth_LoginIdentifier.parse(Some(v))->Result.map(loginIdentifier => {
|
|
77
|
+
...a,
|
|
78
|
+
loginIdentifier,
|
|
79
|
+
})
|
|
80
|
+
i := i.contents + 2
|
|
81
|
+
| (Ok(a), "--sign-up-mode", Some(v)) =>
|
|
82
|
+
acc := Auth_SignUpMode.parse(Some(v))->Result.map(signUpMode => {...a, signUpMode})
|
|
83
|
+
i := i.contents + 2
|
|
64
84
|
| (Ok(a), "--help", _) | (Ok(a), "-h", _) =>
|
|
65
85
|
acc := Ok({...a, help: true})
|
|
66
86
|
i := i.contents + 1
|
|
67
|
-
| (Ok(_), "--name", None)
|
|
87
|
+
| (Ok(_), "--name", None)
|
|
88
|
+
| (Ok(_), "--provider-id", None)
|
|
89
|
+
| (Ok(_), "--login-identifier", None)
|
|
90
|
+
| (Ok(_), "--sign-up-mode", None) =>
|
|
68
91
|
acc := Error(`${flag} needs a value`)
|
|
69
92
|
| (Ok(_), unknown, _) => acc := Error(`unknown argument "${unknown}"`)
|
|
70
93
|
}
|
|
@@ -72,12 +95,28 @@ let parseArgs = (argv: array<string>): result<args, string> => {
|
|
|
72
95
|
acc.contents
|
|
73
96
|
}
|
|
74
97
|
|
|
98
|
+
let _loginIdentifiers =
|
|
99
|
+
Auth_LoginIdentifier.all->Array.map(Auth_LoginIdentifier.toString)->Array.join(" | ")
|
|
100
|
+
|
|
101
|
+
let _defaultLoginIdentifier = Auth_LoginIdentifier.toString(Auth_LoginIdentifier.default)
|
|
102
|
+
|
|
103
|
+
let _signUpModes = Auth_SignUpMode.all->Array.map(Auth_SignUpMode.toString)->Array.join(" | ")
|
|
104
|
+
|
|
105
|
+
let _defaultSignUpMode = Auth_SignUpMode.toString(Auth_SignUpMode.default)
|
|
106
|
+
|
|
75
107
|
let usage = `
|
|
76
108
|
Provision a Reventless identity provider and its active-role store.
|
|
77
109
|
|
|
78
110
|
--name <name> Pool name to create or adopt (default: ${defaultPoolName})
|
|
79
111
|
--provider-id <id> Use this existing pool instead of creating one; only the
|
|
80
112
|
store is provisioned
|
|
113
|
+
--login-identifier <a> Sign-in attribute for a pool this creates: ${_loginIdentifiers}
|
|
114
|
+
(default: ${_defaultLoginIdentifier}). Fixed at creation —
|
|
115
|
+
no later change is possible without replacing the pool
|
|
116
|
+
and losing every account in it.
|
|
117
|
+
--sign-up-mode <m> Whether a stranger may register themselves: ${_signUpModes}
|
|
118
|
+
(default: ${_defaultSignUpMode}). Correctable later, but on a
|
|
119
|
+
pool shared between stacks it applies to all of them.
|
|
81
120
|
|
|
82
121
|
Creates nothing that a platform stack owns, and never attaches a trigger.
|
|
83
122
|
Region and credentials come from the environment, as for any AWS SDK call.
|
|
@@ -85,17 +124,28 @@ Region and credentials come from the environment, as for any AWS SDK call.
|
|
|
85
124
|
|
|
86
125
|
// ── The pool ─────────────────────────────────────────────────────────────────
|
|
87
126
|
|
|
88
|
-
/** SPA-friendly defaults, matching what auto mode declares:
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
127
|
+
/** SPA-friendly defaults, matching what auto mode declares: a 12-character
|
|
128
|
+
password policy, no MFA, admin-only user creation. An operator wanting
|
|
129
|
+
something else edits the pool afterwards rather than having this script grow a
|
|
130
|
+
flag per Cognito setting.
|
|
131
|
+
|
|
132
|
+
Two settings are exceptions and have flags, for two different reasons. The
|
|
133
|
+
sign-in attribute, because it is the one setting no later edit can reach —
|
|
134
|
+
Cognito offers no update for it. Sign-up mode, because a stack pointed at a
|
|
135
|
+
supplied pool holds no handle to set it on, so "edit it afterwards" is the only
|
|
136
|
+
other answer and it leaves the pool's two identity choices in two places. Both
|
|
137
|
+
describe the pool itself rather than a policy detail. */
|
|
92
138
|
let poolSettings = (
|
|
93
139
|
~poolName: string,
|
|
140
|
+
~loginIdentifier: Auth_LoginIdentifier.t,
|
|
141
|
+
~signUpMode: Auth_SignUpMode.t,
|
|
94
142
|
): CognitoIdentityServiceProvider.CreateUserPoolCommand.input => {
|
|
95
143
|
poolName,
|
|
96
|
-
usernameAttributes:
|
|
144
|
+
usernameAttributes: loginIdentifier->Auth_LoginIdentifier.usernameAttributes,
|
|
97
145
|
mfaConfiguration: "OFF",
|
|
98
|
-
adminCreateUserConfig: {
|
|
146
|
+
adminCreateUserConfig: {
|
|
147
|
+
allowAdminCreateUserOnly: signUpMode->Auth_SignUpMode.allowAdminCreateUserOnly,
|
|
148
|
+
},
|
|
99
149
|
policies: {
|
|
100
150
|
passwordPolicy: {
|
|
101
151
|
minimumLength: 12,
|
|
@@ -169,12 +219,20 @@ let resolvePool = async (~args: args): result<string, string> =>
|
|
|
169
219
|
Ok(existing)
|
|
170
220
|
| Ok(None) =>
|
|
171
221
|
let created = await CognitoIdentityServiceProvider.CreateUserPoolCommand.make(
|
|
172
|
-
poolSettings(
|
|
222
|
+
poolSettings(
|
|
223
|
+
~poolName=args.poolName,
|
|
224
|
+
~loginIdentifier=args.loginIdentifier,
|
|
225
|
+
~signUpMode=args.signUpMode,
|
|
226
|
+
),
|
|
173
227
|
)->CognitoIdentityServiceProvider.CreateUserPoolCommand.send
|
|
174
228
|
switch created.userPool->Option.flatMap(p => p.id) {
|
|
175
229
|
| None => Error("CreateUserPool returned no pool id")
|
|
176
230
|
| Some(id) =>
|
|
177
|
-
Console.log(
|
|
231
|
+
Console.log(
|
|
232
|
+
`pool ${id} (created, named "${args.poolName}", sign-in on ${Auth_LoginIdentifier.toString(
|
|
233
|
+
args.loginIdentifier,
|
|
234
|
+
)}, sign-up ${Auth_SignUpMode.toString(args.signUpMode)})`,
|
|
235
|
+
)
|
|
178
236
|
Ok(id)
|
|
179
237
|
}
|
|
180
238
|
}
|
|
@@ -260,7 +318,14 @@ or set REVENTLESS_IDENTITY_PROVIDER_ID in CI. Every stack on this provider
|
|
|
260
318
|
derives the same store, so there is nothing else to configure.
|
|
261
319
|
|
|
262
320
|
The pre-token-generation trigger is attached by the deploy, not by this script.
|
|
263
|
-
|
|
321
|
+
|
|
322
|
+
Once a stack has deployed against this provider, make the first administrator:
|
|
323
|
+
|
|
324
|
+
pnpm exec provision-admin --provider-id ${providerId} --email you@example.com
|
|
325
|
+
|
|
326
|
+
That is a separate bin because it provisions nothing a stack owns, and because it
|
|
327
|
+
is needed on a pool created here and on one a stack created for itself alike. See
|
|
328
|
+
[ProvisionAdmin].`
|
|
264
329
|
|
|
265
330
|
let run = async (): result<unit, string> =>
|
|
266
331
|
// argv[0] is node, argv[1] this script.
|