@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,351 @@
|
|
|
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 AccountsManifest$Reventless from "@reventlessdev/reventless-spec/src/types/AccountsManifest.res.mjs";
|
|
5
|
+
import * as Util_AwsError$ReventlessAws from "../src/util/Util_AwsError.res.mjs";
|
|
6
|
+
import * as ProvisionCognito$ReventlessAws from "./ProvisionCognito.res.mjs";
|
|
7
|
+
import * as ProvisionProvider$ReventlessAws from "./ProvisionProvider.res.mjs";
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
let acc = {
|
|
11
|
+
TAG: "Ok",
|
|
12
|
+
_0: {
|
|
13
|
+
providerId: undefined,
|
|
14
|
+
stack: undefined,
|
|
15
|
+
file: undefined,
|
|
16
|
+
help: false
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
let i = 0;
|
|
20
|
+
let count = argv.length;
|
|
21
|
+
while (i < count) {
|
|
22
|
+
let flag = argv[i];
|
|
23
|
+
let value = argv[i + 1 | 0];
|
|
24
|
+
let match = acc;
|
|
25
|
+
let exit = 0;
|
|
26
|
+
if (match.TAG === "Ok") {
|
|
27
|
+
let a = match._0;
|
|
28
|
+
let exit$1 = 0;
|
|
29
|
+
switch (flag) {
|
|
30
|
+
case "--file" :
|
|
31
|
+
if (value !== undefined) {
|
|
32
|
+
acc = {
|
|
33
|
+
TAG: "Ok",
|
|
34
|
+
_0: {
|
|
35
|
+
providerId: a.providerId,
|
|
36
|
+
stack: a.stack,
|
|
37
|
+
file: value,
|
|
38
|
+
help: a.help
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
i = i + 2 | 0;
|
|
42
|
+
} else {
|
|
43
|
+
exit = 1;
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case "--provider-id" :
|
|
47
|
+
if (value !== undefined) {
|
|
48
|
+
acc = {
|
|
49
|
+
TAG: "Ok",
|
|
50
|
+
_0: {
|
|
51
|
+
providerId: value,
|
|
52
|
+
stack: a.stack,
|
|
53
|
+
file: a.file,
|
|
54
|
+
help: a.help
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
i = i + 2 | 0;
|
|
58
|
+
} else {
|
|
59
|
+
exit = 1;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
case "--stack" :
|
|
63
|
+
if (value !== undefined) {
|
|
64
|
+
acc = {
|
|
65
|
+
TAG: "Ok",
|
|
66
|
+
_0: {
|
|
67
|
+
providerId: a.providerId,
|
|
68
|
+
stack: value,
|
|
69
|
+
file: a.file,
|
|
70
|
+
help: a.help
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
i = i + 2 | 0;
|
|
74
|
+
} else {
|
|
75
|
+
exit = 1;
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "--help" :
|
|
79
|
+
case "-h" :
|
|
80
|
+
exit$1 = 2;
|
|
81
|
+
break;
|
|
82
|
+
default:
|
|
83
|
+
acc = {
|
|
84
|
+
TAG: "Error",
|
|
85
|
+
_0: `unknown argument "` + flag + `"`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (exit$1 === 2) {
|
|
89
|
+
acc = {
|
|
90
|
+
TAG: "Ok",
|
|
91
|
+
_0: {
|
|
92
|
+
providerId: a.providerId,
|
|
93
|
+
stack: a.stack,
|
|
94
|
+
file: a.file,
|
|
95
|
+
help: true
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
i = i + 1 | 0;
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
i = count;
|
|
102
|
+
}
|
|
103
|
+
if (exit === 1) {
|
|
104
|
+
acc = {
|
|
105
|
+
TAG: "Error",
|
|
106
|
+
_0: flag + ` needs a value`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
return acc;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let usage = `
|
|
114
|
+
Turn a declared cast of accounts into working sign-ins.
|
|
115
|
+
|
|
116
|
+
--provider-id <id> The identity provider to create the accounts in. Usually
|
|
117
|
+
omitted: it falls back to ` + ProvisionProvider$ReventlessAws.envKey + `,
|
|
118
|
+
then to the identityProviderId exported by the selected
|
|
119
|
+
Pulumi stack — which every deployment exports, whether it
|
|
120
|
+
created the pool or was handed one.
|
|
121
|
+
--stack <name> Read that output from this stack instead of the selected
|
|
122
|
+
one. The run always names the stack it used.
|
|
123
|
+
--file <path> The manifest. Defaults to .reventless/users.yaml relative
|
|
124
|
+
to the working directory, which is where both platforms
|
|
125
|
+
keep it.
|
|
126
|
+
|
|
127
|
+
For every entry: ensures the groups it names, ensures the account, sets a
|
|
128
|
+
permanent password, and applies the memberships. Generated passwords and the ids
|
|
129
|
+
the pool mints are written back into the manifest, so the seed client can read
|
|
130
|
+
them without anyone retyping a UUID.
|
|
131
|
+
|
|
132
|
+
A password already in the manifest is never replaced. Region and credentials come
|
|
133
|
+
from the environment, as for any AWS SDK call.
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
function looksLikeEmail(username) {
|
|
137
|
+
if (username.includes("@")) {
|
|
138
|
+
return username.includes(".");
|
|
139
|
+
} else {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function checkPoolAcceptsUsernames(providerId, usernames) {
|
|
145
|
+
let e = await ProvisionCognito$ReventlessAws.usernameAttributes(providerId);
|
|
146
|
+
if (e.TAG !== "Ok") {
|
|
147
|
+
return e;
|
|
148
|
+
}
|
|
149
|
+
let attributes = e._0;
|
|
150
|
+
let signsInOnEmail = attributes.includes("email");
|
|
151
|
+
let unusable = signsInOnEmail ? usernames.filter(u => !looksLikeEmail(u)) : [];
|
|
152
|
+
if (unusable.length === 0) {
|
|
153
|
+
console.log(`pool signs in on ` + (
|
|
154
|
+
attributes.length === 0 ? "username" : attributes.join(", ")
|
|
155
|
+
));
|
|
156
|
+
return {
|
|
157
|
+
TAG: "Ok",
|
|
158
|
+
_0: undefined
|
|
159
|
+
};
|
|
160
|
+
} else {
|
|
161
|
+
return {
|
|
162
|
+
TAG: "Error",
|
|
163
|
+
_0: `pool "` + providerId + `" signs in on ` + attributes.join(", ") + `, so ` + unusable.join(", ") + ` could never authenticate. The sign-in attribute is fixed at pool creation and no update can change it — either name these accounts by address in the manifest, or provision them in a pool that signs in on a username.`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function describe(outcome) {
|
|
169
|
+
if (outcome === "Created") {
|
|
170
|
+
return "created";
|
|
171
|
+
} else {
|
|
172
|
+
return "already present";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function ensureGroups(providerId, entries) {
|
|
177
|
+
let named = [];
|
|
178
|
+
entries.forEach(entry => {
|
|
179
|
+
entry.groups.forEach(group => {
|
|
180
|
+
if (!named.includes(group)) {
|
|
181
|
+
named.push(group);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
for (let index = 0, index_finish = named.length; index < index_finish; ++index) {
|
|
187
|
+
let group = named[index];
|
|
188
|
+
let outcome = await ProvisionCognito$ReventlessAws.ensureGroup(providerId, group, "Declared in a Reventless accounts manifest");
|
|
189
|
+
console.log(`group ` + group + ` (` + describe(outcome) + `)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function applyEntry(providerId, entry) {
|
|
194
|
+
let attributes = looksLikeEmail(entry.username) ? [
|
|
195
|
+
{
|
|
196
|
+
Name: "email",
|
|
197
|
+
Value: entry.username
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
Name: "email_verified",
|
|
201
|
+
Value: "true"
|
|
202
|
+
}
|
|
203
|
+
] : [];
|
|
204
|
+
let outcome = await ProvisionCognito$ReventlessAws.ensureUser(providerId, entry.username, attributes);
|
|
205
|
+
console.log(`user ` + entry.username + ` (` + describe(outcome) + `)`);
|
|
206
|
+
await ProvisionCognito$ReventlessAws.setPassword(providerId, entry.username, entry.password);
|
|
207
|
+
for (let index = 0, index_finish = entry.groups.length; index < index_finish; ++index) {
|
|
208
|
+
let group = entry.groups[index];
|
|
209
|
+
await ProvisionCognito$ReventlessAws.addToGroup(providerId, entry.username, group);
|
|
210
|
+
console.log(`member ` + entry.username + ` in ` + group);
|
|
211
|
+
}
|
|
212
|
+
return await ProvisionCognito$ReventlessAws.subOf(providerId, entry.username);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function manifestNote(file) {
|
|
216
|
+
return `
|
|
217
|
+
🚨 Generated passwords were written into ` + file + `. They are not printed here
|
|
218
|
+
because that file is where they are read from — by you and by \`pnpm run seed\`.
|
|
219
|
+
It is gitignored on every platform; keep it that way. These are bootstrap
|
|
220
|
+
credentials for a deployment, not secrets to reuse anywhere real.`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function provision(file, providerId, checkPoolOpt) {
|
|
224
|
+
let checkPool = checkPoolOpt !== undefined ? checkPoolOpt : checkPoolAcceptsUsernames;
|
|
225
|
+
let message = AccountsManifest$Reventless.parseFile(file);
|
|
226
|
+
if (message.TAG !== "Ok") {
|
|
227
|
+
return {
|
|
228
|
+
TAG: "Error",
|
|
229
|
+
_0: file + `: ` + message._0
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
let declared = message._0;
|
|
233
|
+
if (declared.length === 0) {
|
|
234
|
+
return {
|
|
235
|
+
TAG: "Error",
|
|
236
|
+
_0: file + ` declares no accounts`
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
let e = await checkPool(providerId, declared.map(e => e.username));
|
|
240
|
+
if (e.TAG !== "Ok") {
|
|
241
|
+
return e;
|
|
242
|
+
}
|
|
243
|
+
let message$1 = AccountsManifest$Reventless.prepare(file);
|
|
244
|
+
if (message$1.TAG !== "Ok") {
|
|
245
|
+
return {
|
|
246
|
+
TAG: "Error",
|
|
247
|
+
_0: file + `: ` + message$1._0
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
let prepared = message$1._0;
|
|
251
|
+
let generated = prepared.filter(p => p.passwordGenerated).length;
|
|
252
|
+
console.log(`manifest ` + file + ` (` + prepared.length.toString() + ` accounts, ` + generated.toString() + ` password(s) generated)`);
|
|
253
|
+
let entries = prepared.map(p => p.entry);
|
|
254
|
+
await ensureGroups(providerId, entries);
|
|
255
|
+
let fills = [];
|
|
256
|
+
for (let index = 0, index_finish = entries.length; index < index_finish; ++index) {
|
|
257
|
+
let entry = entries[index];
|
|
258
|
+
let userId = await applyEntry(providerId, entry);
|
|
259
|
+
fills.push({
|
|
260
|
+
index: index,
|
|
261
|
+
password: undefined,
|
|
262
|
+
userId: userId
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
let message$2 = AccountsManifest$Reventless.fillFile(file, fills);
|
|
266
|
+
if (message$2.TAG !== "Ok") {
|
|
267
|
+
return {
|
|
268
|
+
TAG: "Error",
|
|
269
|
+
_0: file + `: ` + message$2._0
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
let corrected = message$2._0.filter(r => r.userIdWritten).length;
|
|
273
|
+
console.log(`manifest ` + file + ` (` + corrected.toString() + ` id(s) written back)` + manifestNote(file));
|
|
274
|
+
return {
|
|
275
|
+
TAG: "Ok",
|
|
276
|
+
_0: undefined
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function run() {
|
|
281
|
+
let e = parseArgs(process.argv.slice(2, process.argv.length));
|
|
282
|
+
if (e.TAG !== "Ok") {
|
|
283
|
+
return e;
|
|
284
|
+
}
|
|
285
|
+
let args = e._0;
|
|
286
|
+
if (args.help) {
|
|
287
|
+
console.log(usage);
|
|
288
|
+
return {
|
|
289
|
+
TAG: "Ok",
|
|
290
|
+
_0: undefined
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
|
|
294
|
+
if (e$1.TAG !== "Ok") {
|
|
295
|
+
return e$1;
|
|
296
|
+
}
|
|
297
|
+
let located = e$1._0;
|
|
298
|
+
let file = AccountsManifest$Reventless.pathOf(located);
|
|
299
|
+
if (located.TAG !== "Declared") {
|
|
300
|
+
console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
|
|
301
|
+
}
|
|
302
|
+
let message = ProvisionProvider$ReventlessAws.resolve(args.providerId, args.stack);
|
|
303
|
+
if (message.TAG !== "Ok") {
|
|
304
|
+
return {
|
|
305
|
+
TAG: "Error",
|
|
306
|
+
_0: message._0 + `. To fill in the manifest without creating anything, run prepare-accounts`
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
let match = message._0;
|
|
310
|
+
let providerId = match[0];
|
|
311
|
+
console.log(`provider ` + providerId + ` (from ` + ProvisionProvider$ReventlessAws.describe(match[1]) + `)`);
|
|
312
|
+
return await provision(file, providerId, undefined);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function main() {
|
|
316
|
+
let message;
|
|
317
|
+
try {
|
|
318
|
+
message = await run();
|
|
319
|
+
} catch (raw_exn) {
|
|
320
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
321
|
+
console.error(`provision-accounts: ` + Util_AwsError$ReventlessAws.describe(exn));
|
|
322
|
+
process.exit(1);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (message.TAG === "Ok") {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
console.error(`provision-accounts: ` + message._0);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let Cognito;
|
|
333
|
+
|
|
334
|
+
let Manifest;
|
|
335
|
+
|
|
336
|
+
export {
|
|
337
|
+
Cognito,
|
|
338
|
+
Manifest,
|
|
339
|
+
parseArgs,
|
|
340
|
+
usage,
|
|
341
|
+
looksLikeEmail,
|
|
342
|
+
checkPoolAcceptsUsernames,
|
|
343
|
+
describe,
|
|
344
|
+
ensureGroups,
|
|
345
|
+
applyEntry,
|
|
346
|
+
manifestNote,
|
|
347
|
+
provision,
|
|
348
|
+
run,
|
|
349
|
+
main,
|
|
350
|
+
}
|
|
351
|
+
/* AccountsManifest-Reventless Not a pure module */
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Make the first administrator of a deployment.
|
|
3
|
+
|
|
4
|
+
A stack creates a user pool, an app client and — in auto mode — the
|
|
5
|
+
administrator group. It creates no *accounts*, and it should not: a pool full of
|
|
6
|
+
people is not something a `pulumi destroy` should be able to empty, and Cognito
|
|
7
|
+
exports no password material, so a replaced account is a lost one. Something has
|
|
8
|
+
to make the first one, once, outside every stack. That is this.
|
|
9
|
+
|
|
10
|
+
Until it existed the answer was the AWS console, and nothing said so. A deploy
|
|
11
|
+
succeeded, the app loaded, sign-in failed, and no page in the repo covered what to
|
|
12
|
+
do next — the most expensive kind of first impression, met by whoever has the
|
|
13
|
+
least context: someone evaluating the framework on their first cloud deploy.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
pnpm exec provision-admin --provider-id eu-west-1_AbCdEfGhI --email me@example.com
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Works the same on a pool the stack owns and one it does not, which is why it is
|
|
20
|
+
its own bin rather than a flag on `provision-identity`. That script provisions the
|
|
21
|
+
*provider* — a pool and the active-role store derived from its id — and both are
|
|
22
|
+
BYO concerns; run against an auto-mode pool it would create a second store at a
|
|
23
|
+
derived name that no stack ever reads. The first administrator is needed in both
|
|
24
|
+
modes and is infrastructure in neither, so it is asked for separately.
|
|
25
|
+
|
|
26
|
+
🚨 **It provisions nothing a stack owns.** No pool, no store, no trigger. The
|
|
27
|
+
group is the one exception and only because it may be missing: on a supplied pool
|
|
28
|
+
no stack declares it — see [Platform_Stack] for why a pool-level fact is not a
|
|
29
|
+
borrowed pool's stack's to declare.
|
|
30
|
+
|
|
31
|
+
Re-running is safe, and the first thing anyone does with a provisioning script is
|
|
32
|
+
run it twice: an existing group and an existing user are both reported rather than
|
|
33
|
+
refused. The password is re-minted on every run, which is the one thing that does
|
|
34
|
+
change — see [passwordNote].
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
open AwsSdk
|
|
38
|
+
|
|
39
|
+
module Cognito = CognitoIdentityServiceProvider
|
|
40
|
+
|
|
41
|
+
// ── Arguments ────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
type args = {
|
|
44
|
+
providerId: option<string>,
|
|
45
|
+
stack: option<string>,
|
|
46
|
+
email: option<string>,
|
|
47
|
+
help: bool,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Parsed rather than positional, and unknown flags are an error — the reason
|
|
51
|
+
[ProvisionIdentity.parseArgs] gives, arriving somewhere worse: a typo'd
|
|
52
|
+
`--email` that parsed as "absent" would be caught by the required-argument check
|
|
53
|
+
below, but a typo'd `--provider-id` would fall through to the environment and
|
|
54
|
+
create the administrator in a *different pool* than the operator named. */
|
|
55
|
+
let parseArgs = (argv: array<string>): result<args, string> => {
|
|
56
|
+
let acc = ref(Ok({providerId: None, stack: None, email: None, help: false}))
|
|
57
|
+
let i = ref(0)
|
|
58
|
+
let count = argv->Array.length
|
|
59
|
+
while i.contents < count {
|
|
60
|
+
let flag = argv->Array.getUnsafe(i.contents)
|
|
61
|
+
let value = argv->Array.get(i.contents + 1)
|
|
62
|
+
switch (acc.contents, flag, value) {
|
|
63
|
+
| (Error(_), _, _) => i := count
|
|
64
|
+
| (Ok(a), "--provider-id", Some(v)) =>
|
|
65
|
+
acc := Ok({...a, providerId: Some(v)})
|
|
66
|
+
i := i.contents + 2
|
|
67
|
+
| (Ok(a), "--stack", Some(v)) =>
|
|
68
|
+
acc := Ok({...a, stack: Some(v)})
|
|
69
|
+
i := i.contents + 2
|
|
70
|
+
| (Ok(a), "--email", Some(v)) =>
|
|
71
|
+
acc := Ok({...a, email: Some(v)})
|
|
72
|
+
i := i.contents + 2
|
|
73
|
+
| (Ok(a), "--help", _) | (Ok(a), "-h", _) =>
|
|
74
|
+
acc := Ok({...a, help: true})
|
|
75
|
+
i := i.contents + 1
|
|
76
|
+
| (Ok(_), "--provider-id", None) | (Ok(_), "--email", None) | (Ok(_), "--stack", None) =>
|
|
77
|
+
acc := Error(`${flag} needs a value`)
|
|
78
|
+
| (Ok(_), unknown, _) => acc := Error(`unknown argument "${unknown}"`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
acc.contents
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let usage = `
|
|
85
|
+
Make the first administrator of a Reventless deployment.
|
|
86
|
+
|
|
87
|
+
--provider-id <id> The identity provider to create the account in. Usually
|
|
88
|
+
omitted: it falls back to ${ProvisionProvider.envKey},
|
|
89
|
+
then to the identityProviderId exported by the selected
|
|
90
|
+
Pulumi stack — which every deployment exports, whether it
|
|
91
|
+
created the pool or was handed one.
|
|
92
|
+
--stack <name> Read that output from this stack instead of the selected
|
|
93
|
+
one. The run always names the stack it used.
|
|
94
|
+
--email <address> The address they sign in with.
|
|
95
|
+
|
|
96
|
+
Creates the "${Reventless.AdminGroup.name}" group if it is missing, the account if
|
|
97
|
+
it is missing, a working password, and the membership between them. Provisions
|
|
98
|
+
nothing that a platform stack owns. Region and credentials come from the
|
|
99
|
+
environment, as for any AWS SDK call.
|
|
100
|
+
`
|
|
101
|
+
|
|
102
|
+
// ── The pool ─────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
Refuse a pool this account could never sign in to.
|
|
106
|
+
|
|
107
|
+
The sign-in attribute is fixed at pool creation and no update can change it — see
|
|
108
|
+
[Auth_LoginIdentifier] — so an email address handed to a phone-only pool produces
|
|
109
|
+
a correctly-created account that simply cannot authenticate, and the symptom
|
|
110
|
+
arrives at a login screen far from the run that caused it. Described rather than
|
|
111
|
+
assumed for the same reason `provision-identity` describes a supplied pool.
|
|
112
|
+
|
|
113
|
+
No `UsernameAttributes` at all means the pool signs in on a plain username, which
|
|
114
|
+
takes an email-shaped one happily.
|
|
115
|
+
*/
|
|
116
|
+
let checkPoolAcceptsEmail = async (~providerId: string): result<unit, string> =>
|
|
117
|
+
switch await ProvisionCognito.usernameAttributes(~providerId) {
|
|
118
|
+
| Error(_) as e => e
|
|
119
|
+
| Ok(attributes) =>
|
|
120
|
+
if attributes->Array.length == 0 || attributes->Array.includes("email") {
|
|
121
|
+
Console.log(`pool ${providerId}`)
|
|
122
|
+
Ok()
|
|
123
|
+
} else {
|
|
124
|
+
Error(
|
|
125
|
+
`pool "${providerId}" signs in on ${attributes->Array.join(
|
|
126
|
+
", ",
|
|
127
|
+
)}, 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.`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── The account ──────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
let describe = (outcome: ProvisionCognito.outcome) =>
|
|
135
|
+
switch outcome {
|
|
136
|
+
| Created => "created"
|
|
137
|
+
| AlreadyPresent => "already present"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The group, where a stack has not already declared it. See
|
|
141
|
+
[ProvisionCognito.ensureGroup] for why a supplied pool needs one made here. */
|
|
142
|
+
let ensureGroup = async (~providerId: string, ~group: string): unit => {
|
|
143
|
+
let outcome = await ProvisionCognito.ensureGroup(
|
|
144
|
+
~providerId,
|
|
145
|
+
~group,
|
|
146
|
+
~description="Reventless administrators",
|
|
147
|
+
)
|
|
148
|
+
Console.log(`group ${group} (${outcome->describe})`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
The account, where it is not already there.
|
|
153
|
+
|
|
154
|
+
`email_verified` is stamped true because nothing here can complete a verification
|
|
155
|
+
round-trip, and an unverified address leaves the account able to sign in but
|
|
156
|
+
unable to recover a password — a bootstrap that works once and strands its owner.
|
|
157
|
+
*/
|
|
158
|
+
let ensureUser = async (~providerId: string, ~email: string): unit => {
|
|
159
|
+
let attributes: array<Cognito.AdminCreateUserCommand.attributeType> = [
|
|
160
|
+
{name: "email", value: email},
|
|
161
|
+
{name: "email_verified", value: "true"},
|
|
162
|
+
]
|
|
163
|
+
let outcome = await ProvisionCognito.ensureUser(~providerId, ~username=email, ~attributes)
|
|
164
|
+
Console.log(`user ${email} (${outcome->describe})`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A password the account can actually be used with — see
|
|
168
|
+
[ProvisionCognito.setPassword] for why it is permanent rather than temporary.
|
|
169
|
+
|
|
170
|
+
Unconditional rather than skipped for an account that already exists, which is
|
|
171
|
+
what makes a second run useful instead of merely harmless: the operator who runs
|
|
172
|
+
this again is usually the one who has lost the password. */
|
|
173
|
+
let setPassword = async (~providerId: string, ~email: string, ~password: string): unit => {
|
|
174
|
+
await ProvisionCognito.setPassword(~providerId, ~username=email, ~password)
|
|
175
|
+
Console.log(`password (set, permanent)`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let addToGroup = async (~providerId: string, ~email: string, ~group: string): unit => {
|
|
179
|
+
await ProvisionCognito.addToGroup(~providerId, ~username=email, ~group)
|
|
180
|
+
Console.log(`member ${email} in ${group}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
🚨 **Printed, and this is a deliberate trade rather than an oversight.**
|
|
187
|
+
|
|
188
|
+
Cognito keeps no recoverable copy, so a generated password exists in this output
|
|
189
|
+
or nowhere. Printing it is friendliest for the case this script is for — a
|
|
190
|
+
developer at a terminal on their first deploy — and the cost is that it lands in
|
|
191
|
+
scrollback, and in a build log if anyone runs this unattended. Taking the password
|
|
192
|
+
as a flag instead would trade the log for shell history, which is not obviously
|
|
193
|
+
better and is worse for the target case.
|
|
194
|
+
|
|
195
|
+
So: generated and printed, and said plainly to be a bootstrap credential. A
|
|
196
|
+
deployment that runs this in a pipeline should be using its own provisioning
|
|
197
|
+
instead. Revisit if that stops being true.
|
|
198
|
+
*/
|
|
199
|
+
let passwordNote = `
|
|
200
|
+
🚨 This is a bootstrap credential and it is printed above because that is the only
|
|
201
|
+
place it exists — Cognito keeps no recoverable copy. It is now in this
|
|
202
|
+
terminal's scrollback. Sign in, change it, and do not paste it where logs are
|
|
203
|
+
kept. Running this again mints a new one.`
|
|
204
|
+
|
|
205
|
+
let signInDetails = (~providerId: string, ~email: string, ~password: string, ~group: string) =>
|
|
206
|
+
`
|
|
207
|
+
The first administrator is ready.
|
|
208
|
+
|
|
209
|
+
provider ${providerId}
|
|
210
|
+
sign in as ${email}
|
|
211
|
+
password ${password}
|
|
212
|
+
group ${group}
|
|
213
|
+
${passwordNote}
|
|
214
|
+
|
|
215
|
+
This script bootstraps the first account only, which is the one that cannot be
|
|
216
|
+
made through a signed-in session because there is no signed-in session yet. The
|
|
217
|
+
rest of the cast is a list rather than a command: declare it in
|
|
218
|
+
.reventless/users.yaml and run \`pnpm exec provision-accounts\`.`
|
|
219
|
+
|
|
220
|
+
let run = async (): result<unit, string> =>
|
|
221
|
+
// argv[0] is node, argv[1] this script.
|
|
222
|
+
switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
|
|
223
|
+
| Error(_) as e => e
|
|
224
|
+
| Ok(args) if args.help =>
|
|
225
|
+
Console.log(usage)
|
|
226
|
+
Ok()
|
|
227
|
+
| Ok(args) =>
|
|
228
|
+
// The address is checked before the provider is resolved: it costs nothing,
|
|
229
|
+
// while resolving may shell out to Pulumi, and a run missing both arguments
|
|
230
|
+
// should say the cheap thing rather than fail on a stack lookup it never
|
|
231
|
+
// needed.
|
|
232
|
+
switch args.email {
|
|
233
|
+
| None => Error("--email is required — it is the address the administrator signs in with")
|
|
234
|
+
| Some(email) =>
|
|
235
|
+
switch ProvisionProvider.resolve(~given=args.providerId, ~stack=args.stack) {
|
|
236
|
+
| Error(_) as e => e
|
|
237
|
+
| Ok((providerId, source)) =>
|
|
238
|
+
Console.log(`provider ${providerId} (from ${source->ProvisionProvider.describe})`)
|
|
239
|
+
switch await checkPoolAcceptsEmail(~providerId) {
|
|
240
|
+
| Error(_) as e => e
|
|
241
|
+
| Ok() =>
|
|
242
|
+
let group = Reventless.AdminGroup.name
|
|
243
|
+
let password = Reventless.Util_Password.generate()
|
|
244
|
+
await ensureGroup(~providerId, ~group)
|
|
245
|
+
await ensureUser(~providerId, ~email)
|
|
246
|
+
await setPassword(~providerId, ~email, ~password)
|
|
247
|
+
await addToGroup(~providerId, ~email, ~group)
|
|
248
|
+
Console.log(signInDetails(~providerId, ~email, ~password, ~group))
|
|
249
|
+
Ok()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
🚨 **Catches thrown exceptions, not only `Error` results** — the reason
|
|
257
|
+
[ProvisionIdentity.main] gives. Without it an SDK failure escapes an async
|
|
258
|
+
function nothing awaits and Node reports `UnhandledPromiseRejection ...
|
|
259
|
+
"#<Object>"`, naming neither the call that failed nor why.
|
|
260
|
+
*/
|
|
261
|
+
let main = async () =>
|
|
262
|
+
switch await run() {
|
|
263
|
+
| Ok() => ()
|
|
264
|
+
| Error(message) =>
|
|
265
|
+
Console.error(`provision-admin: ${message}`)
|
|
266
|
+
NodeProcess.exit(1)
|
|
267
|
+
| exception exn =>
|
|
268
|
+
Console.error(`provision-admin: ${Util_AwsError.describe(exn)}`)
|
|
269
|
+
NodeProcess.exit(1)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 🚨 **No top-level call.** `../run-provision-admin.mjs` invokes [main]; this
|
|
273
|
+
// module only defines it. A module that ran itself on import could not be
|
|
274
|
+
// imported by a test — which is how a guard that could never match once shipped
|
|
275
|
+
// in the sibling script.
|