@reventlessdev/reventless-spec 3.0.0-alpha.134 → 3.0.0-alpha.136
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 +44 -0
- package/package.json +4 -3
- package/run-check-lifecycle.mjs +2 -1
- package/run-prepare-accounts.mjs +3 -0
- package/src/components/CapabilityManifest.res +4 -0
- package/src/components/CapabilityManifest.res.mjs +13 -2
- package/src/components/DcbScopeInference.res +224 -73
- package/src/components/DcbScopeInference.res.mjs +187 -49
- package/src/components/DcbTag.res +142 -186
- package/src/components/DcbTag.res.mjs +137 -128
- package/src/components/DcbValidation.res +50 -0
- package/src/components/DcbValidation.res.mjs +51 -0
- package/src/generator/Codegen.res +2 -1
- package/src/generator/Codegen.res.mjs +1 -1
- package/src/generator/PlatformCodegen.res +1 -0
- package/src/generator/PlatformCodegen.res.mjs +5 -0
- package/src/lifecycle/CheckLifecycleModel.res +148 -8
- package/src/lifecycle/CheckLifecycleModel.res.mjs +154 -7
- package/src/semantic/Capabilities.res +17 -0
- package/src/semantic/Capabilities.res.mjs +9 -1
- package/src/semantic/CapabilityNeed.res +12 -0
- package/src/semantic/CapabilityNeed.res.mjs +9 -4
- package/src/semantic/IdentityProvider.res +127 -0
- package/src/semantic/IdentityProvider.res.mjs +65 -0
- package/src/semantic/Secrets.res +76 -0
- package/src/semantic/Secrets.res.mjs +49 -0
- package/src/types/AccountsManifest.res +287 -0
- package/src/types/AccountsManifest.res.mjs +312 -0
- package/src/types/AdminGroup.res +27 -0
- package/src/types/AdminGroup.res.mjs +9 -0
- package/src/types/OwnerScope.res +27 -0
- package/src/types/OwnerScope.res.mjs +12 -0
- package/src/types/PrepareAccounts.res +119 -0
- package/src/types/PrepareAccounts.res.mjs +148 -0
- package/src/util/Util_Password.res +60 -0
- package/src/util/Util_Password.res.mjs +53 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Sury from "sury";
|
|
5
|
+
import * as Yaml from "yaml";
|
|
6
|
+
import * as Nodefs from "node:fs";
|
|
7
|
+
import * as Nodepath from "node:path";
|
|
8
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
9
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
11
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
|
+
import * as Util_Password$Reventless from "../util/Util_Password.res.mjs";
|
|
13
|
+
|
|
14
|
+
let entrySchema = Sury.$schema(s => ({
|
|
15
|
+
username: s.m(Sury.string),
|
|
16
|
+
password: s.m(Sury.string),
|
|
17
|
+
groups: s.m(Sury.array(Sury.string)),
|
|
18
|
+
userId: s.m(Sury.$option(Sury.string)),
|
|
19
|
+
demoOwner: s.m(Sury.$option(Sury.string))
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
let entriesSchema = Sury.array(entrySchema);
|
|
23
|
+
|
|
24
|
+
function parseString(yamlText) {
|
|
25
|
+
try {
|
|
26
|
+
return {
|
|
27
|
+
TAG: "Ok",
|
|
28
|
+
_0: S.parseOrThrow(Yaml.parse(yamlText), entriesSchema)
|
|
29
|
+
};
|
|
30
|
+
} catch (raw_err) {
|
|
31
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
32
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
33
|
+
return {
|
|
34
|
+
TAG: "Error",
|
|
35
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "YAML parse error")
|
|
36
|
+
};
|
|
37
|
+
} else {
|
|
38
|
+
return {
|
|
39
|
+
TAG: "Error",
|
|
40
|
+
_0: "YAML parse error"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseFile(path) {
|
|
47
|
+
try {
|
|
48
|
+
return parseString(Nodefs.readFileSync(path, "utf8"));
|
|
49
|
+
} catch (raw_err) {
|
|
50
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
51
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
52
|
+
return {
|
|
53
|
+
TAG: "Error",
|
|
54
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), `Cannot read ` + path)
|
|
55
|
+
};
|
|
56
|
+
} else {
|
|
57
|
+
return {
|
|
58
|
+
TAG: "Error",
|
|
59
|
+
_0: `Cannot read ` + path
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultPath() {
|
|
66
|
+
return Nodepath.join(process.cwd(), ".reventless", "users.yaml");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function path(index, field) {
|
|
70
|
+
return [
|
|
71
|
+
index,
|
|
72
|
+
field
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function currentString(doc, index, field) {
|
|
77
|
+
let s = doc.getIn(path(index, field));
|
|
78
|
+
if (typeof s === "string") {
|
|
79
|
+
return s;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function applyFills(yamlText, fills) {
|
|
84
|
+
try {
|
|
85
|
+
let doc = Yaml.parseDocument(yamlText);
|
|
86
|
+
let report = fills.map(param => {
|
|
87
|
+
let userId = param.userId;
|
|
88
|
+
let password = param.password;
|
|
89
|
+
let index = param.index;
|
|
90
|
+
let passwordWritten = password !== undefined && Stdlib_Option.getOr(currentString(doc, index, "password"), "").trim() === "" ? (doc.setIn(path(index, "password"), password), true) : false;
|
|
91
|
+
let userIdWritten = userId !== undefined && Primitive_object.notequal(currentString(doc, index, "userId"), userId) ? (doc.setIn(path(index, "userId"), userId), true) : false;
|
|
92
|
+
return {
|
|
93
|
+
index: index,
|
|
94
|
+
passwordWritten: passwordWritten,
|
|
95
|
+
userIdWritten: userIdWritten
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
TAG: "Ok",
|
|
100
|
+
_0: [
|
|
101
|
+
doc.toString(),
|
|
102
|
+
report
|
|
103
|
+
]
|
|
104
|
+
};
|
|
105
|
+
} catch (raw_err) {
|
|
106
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
107
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
108
|
+
return {
|
|
109
|
+
TAG: "Error",
|
|
110
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "Cannot rewrite the manifest")
|
|
111
|
+
};
|
|
112
|
+
} else {
|
|
113
|
+
return {
|
|
114
|
+
TAG: "Error",
|
|
115
|
+
_0: "Cannot rewrite the manifest"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function fillFile(file, fills) {
|
|
122
|
+
let e;
|
|
123
|
+
try {
|
|
124
|
+
e = {
|
|
125
|
+
TAG: "Ok",
|
|
126
|
+
_0: Nodefs.readFileSync(file, "utf8")
|
|
127
|
+
};
|
|
128
|
+
} catch (raw_err) {
|
|
129
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
130
|
+
e = err.RE_EXN_ID === "JsExn" ? ({
|
|
131
|
+
TAG: "Error",
|
|
132
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), `Cannot read ` + file)
|
|
133
|
+
}) : ({
|
|
134
|
+
TAG: "Error",
|
|
135
|
+
_0: `Cannot read ` + file
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (e.TAG !== "Ok") {
|
|
139
|
+
return e;
|
|
140
|
+
}
|
|
141
|
+
let e$1 = applyFills(e._0, fills);
|
|
142
|
+
if (e$1.TAG !== "Ok") {
|
|
143
|
+
return e$1;
|
|
144
|
+
}
|
|
145
|
+
let match = e$1._0;
|
|
146
|
+
let report = match[1];
|
|
147
|
+
if (!report.some(r => {
|
|
148
|
+
if (r.passwordWritten) {
|
|
149
|
+
return true;
|
|
150
|
+
} else {
|
|
151
|
+
return r.userIdWritten;
|
|
152
|
+
}
|
|
153
|
+
})) {
|
|
154
|
+
return {
|
|
155
|
+
TAG: "Ok",
|
|
156
|
+
_0: report
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
Nodefs.writeFileSync(file, match[0], "utf8");
|
|
161
|
+
return {
|
|
162
|
+
TAG: "Ok",
|
|
163
|
+
_0: report
|
|
164
|
+
};
|
|
165
|
+
} catch (raw_err$1) {
|
|
166
|
+
let err$1 = Primitive_exceptions.internalToException(raw_err$1);
|
|
167
|
+
if (err$1.RE_EXN_ID === "JsExn") {
|
|
168
|
+
return {
|
|
169
|
+
TAG: "Error",
|
|
170
|
+
_0: Stdlib_Option.getOr(Stdlib_JsExn.message(err$1._1), `Cannot write ` + file)
|
|
171
|
+
};
|
|
172
|
+
} else {
|
|
173
|
+
return {
|
|
174
|
+
TAG: "Error",
|
|
175
|
+
_0: `Cannot write ` + file
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let templateName = "users.example.yaml";
|
|
182
|
+
|
|
183
|
+
function pathOf(located) {
|
|
184
|
+
return located._0;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function locate(given, param) {
|
|
188
|
+
if (given !== undefined) {
|
|
189
|
+
if (Nodefs.existsSync(given)) {
|
|
190
|
+
return {
|
|
191
|
+
TAG: "Ok",
|
|
192
|
+
_0: {
|
|
193
|
+
TAG: "Declared",
|
|
194
|
+
_0: given
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
} else {
|
|
198
|
+
return {
|
|
199
|
+
TAG: "Error",
|
|
200
|
+
_0: given + ` does not exist — a named manifest is read, never created`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
let file = defaultPath();
|
|
205
|
+
if (Nodefs.existsSync(file)) {
|
|
206
|
+
return {
|
|
207
|
+
TAG: "Ok",
|
|
208
|
+
_0: {
|
|
209
|
+
TAG: "Declared",
|
|
210
|
+
_0: file
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
let template = Nodepath.join(process.cwd(), templateName);
|
|
215
|
+
if (!Nodefs.existsSync(template)) {
|
|
216
|
+
return {
|
|
217
|
+
TAG: "Error",
|
|
218
|
+
_0: `no ` + file + ` and no ` + templateName + ` here to start one from. Declare the accounts in .reventless/users.yaml — each entry a username, a password (empty to have one generated) and the groups it belongs to`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
Nodefs.mkdirSync(Nodepath.dirname(file), {
|
|
223
|
+
recursive: true
|
|
224
|
+
});
|
|
225
|
+
Nodefs.cpSync(template, file, {});
|
|
226
|
+
return {
|
|
227
|
+
TAG: "Ok",
|
|
228
|
+
_0: {
|
|
229
|
+
TAG: "SeededFrom",
|
|
230
|
+
_0: file,
|
|
231
|
+
_1: template
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
} catch (raw_err) {
|
|
235
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
236
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
237
|
+
return {
|
|
238
|
+
TAG: "Error",
|
|
239
|
+
_0: `could not copy ` + templateName + ` into place: ` + Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "unknown error")
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
return {
|
|
243
|
+
TAG: "Error",
|
|
244
|
+
_0: `could not copy ` + templateName + ` into place`
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function prepare(file) {
|
|
251
|
+
let e = parseFile(file);
|
|
252
|
+
if (e.TAG !== "Ok") {
|
|
253
|
+
return e;
|
|
254
|
+
}
|
|
255
|
+
let entries = e._0;
|
|
256
|
+
let minted = entries.map(entry => {
|
|
257
|
+
if (entry.password.trim() === "") {
|
|
258
|
+
return Util_Password$Reventless.generate();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
let fills = minted.map((password, index) => ({
|
|
262
|
+
index: index,
|
|
263
|
+
password: password,
|
|
264
|
+
userId: undefined
|
|
265
|
+
}));
|
|
266
|
+
let e$1 = fillFile(file, fills);
|
|
267
|
+
if (e$1.TAG !== "Ok") {
|
|
268
|
+
return e$1;
|
|
269
|
+
}
|
|
270
|
+
let report = e$1._0;
|
|
271
|
+
return {
|
|
272
|
+
TAG: "Ok",
|
|
273
|
+
_0: entries.map((entry, index) => {
|
|
274
|
+
let written = Stdlib_Option.mapOr(report[index], false, r => r.passwordWritten);
|
|
275
|
+
let match = minted[index];
|
|
276
|
+
if (!written) {
|
|
277
|
+
return {
|
|
278
|
+
entry: entry,
|
|
279
|
+
passwordGenerated: false
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (match === undefined) {
|
|
283
|
+
return {
|
|
284
|
+
entry: entry,
|
|
285
|
+
passwordGenerated: false
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
let newrecord = {...entry};
|
|
289
|
+
return {
|
|
290
|
+
entry: (newrecord.password = match, newrecord),
|
|
291
|
+
passwordGenerated: true
|
|
292
|
+
};
|
|
293
|
+
})
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export {
|
|
298
|
+
entrySchema,
|
|
299
|
+
entriesSchema,
|
|
300
|
+
parseString,
|
|
301
|
+
parseFile,
|
|
302
|
+
defaultPath,
|
|
303
|
+
path,
|
|
304
|
+
currentString,
|
|
305
|
+
applyFills,
|
|
306
|
+
fillFile,
|
|
307
|
+
templateName,
|
|
308
|
+
pathOf,
|
|
309
|
+
locate,
|
|
310
|
+
prepare,
|
|
311
|
+
}
|
|
312
|
+
/* entrySchema Not a pure module */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The group whose members administer a deployment.
|
|
3
|
+
|
|
4
|
+
One definition, because the name is decided **twice and independently**: an
|
|
5
|
+
authorization rule names it to allow a read, and [OwnerScope.elevatedGroups]
|
|
6
|
+
names it to exempt a caller from `@owner` narrowing. Nothing checks that the two
|
|
7
|
+
agree, and a deployment where they disagree is refused nowhere — the
|
|
8
|
+
administrator passes every authorization check and then reads *empty views*,
|
|
9
|
+
because every owner-scoped view filters them out. Empty-because-scoped and
|
|
10
|
+
empty-because-there-is-nothing look identical from outside.
|
|
11
|
+
|
|
12
|
+
In `spec` rather than in a provider package because **both platforms hard-code
|
|
13
|
+
it**: the AWS platform decorates its admin API with it, while the local platform
|
|
14
|
+
both wraps its `Platform_*` fields in it and hands it to the built-in developer
|
|
15
|
+
account. A constant only one of them could reach would leave the other exactly as
|
|
16
|
+
divergent as it is today.
|
|
17
|
+
|
|
18
|
+
🚨 **This buys agreement, not the freedom to rename.** `"Admin"` is written into
|
|
19
|
+
every example's `@@reventless.authorize` annotations, and a PPX annotation takes a
|
|
20
|
+
literal — so the consumers that matter most *cannot* read this constant. Changing
|
|
21
|
+
the value here would leave those annotations behind and silently split the two
|
|
22
|
+
mechanisms this exists to hold together, which is the failure above rather than a
|
|
23
|
+
compile error. Renaming the administrator group is a different and much larger
|
|
24
|
+
act. `AdminGroupTest` asserts that the constant and the platform's own annotation
|
|
25
|
+
still say the same thing, because that boundary can carry no other check.
|
|
26
|
+
*/
|
|
27
|
+
let name = "Admin"
|
package/src/types/OwnerScope.res
CHANGED
|
@@ -94,6 +94,33 @@ let setElevatedGroups = (groups: array<string>) => explicitElevatedGroups := Som
|
|
|
94
94
|
/** Forget an explicit setting and fall back to the environment again. */
|
|
95
95
|
let clearElevatedGroups = () => explicitElevatedGroups := None
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
Name the elevated groups only where the deployment has not already answered.
|
|
99
|
+
|
|
100
|
+
For a platform that wants a working default without overriding an operator who
|
|
101
|
+
stated one — a stack that declares an administrator group has every reason to
|
|
102
|
+
exempt it, and no business overruling a deployment that named different operators.
|
|
103
|
+
|
|
104
|
+
"Already answered" includes the **environment**, not only an explicit call. A
|
|
105
|
+
deployment that set `REVENTLESS_ELEVATED_GROUPS` in CI has named its operators,
|
|
106
|
+
and a default that beat it would leave the runtime acting on a value no source in
|
|
107
|
+
that deployment states. This is the same contract, one level up, that
|
|
108
|
+
`Util_OwnerScopeEnv.applyElevatedGroupsDefault` keeps with a Lambda's own
|
|
109
|
+
environment: the more specific statement wins, and the default only fills silence.
|
|
110
|
+
|
|
111
|
+
🚨 **An explicit empty list is an answer, and this must not fill it in.** A
|
|
112
|
+
deployment that deliberately elevates nobody has decided that; a default landing
|
|
113
|
+
on top would silently re-grant the cross-owner read it withheld. That is why this
|
|
114
|
+
reads [explicitElevatedGroups] rather than [elevatedGroups] — the latter answers
|
|
115
|
+
`[]` for "nobody" and for "nothing said" alike, and those must not be treated the
|
|
116
|
+
same here.
|
|
117
|
+
*/
|
|
118
|
+
let defaultElevatedGroups = (groups: array<string>) =>
|
|
119
|
+
switch (explicitElevatedGroups.contents, _elevatedGroupsEnv) {
|
|
120
|
+
| (None, None) => explicitElevatedGroups := Some(groups)
|
|
121
|
+
| _ => ()
|
|
122
|
+
}
|
|
123
|
+
|
|
97
124
|
let parseElevatedGroups = (raw: string): array<string> =>
|
|
98
125
|
raw
|
|
99
126
|
->String.split(",")
|
|
@@ -21,6 +21,17 @@ function clearElevatedGroups() {
|
|
|
21
21
|
explicitElevatedGroups.contents = undefined;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
function defaultElevatedGroups(groups) {
|
|
25
|
+
let match = explicitElevatedGroups.contents;
|
|
26
|
+
let match$1 = process.env.REVENTLESS_ELEVATED_GROUPS;
|
|
27
|
+
if (match !== undefined || match$1 !== undefined) {
|
|
28
|
+
return;
|
|
29
|
+
} else {
|
|
30
|
+
explicitElevatedGroups.contents = groups;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
function parseElevatedGroups(raw) {
|
|
25
36
|
return raw.split(",").map(prim => prim.trim()).filter(part => part.length > 0);
|
|
26
37
|
}
|
|
@@ -226,6 +237,7 @@ export {
|
|
|
226
237
|
explicitElevatedGroups,
|
|
227
238
|
setElevatedGroups,
|
|
228
239
|
clearElevatedGroups,
|
|
240
|
+
defaultElevatedGroups,
|
|
229
241
|
parseElevatedGroups,
|
|
230
242
|
elevatedGroups,
|
|
231
243
|
classify,
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/***
|
|
2
|
+
Make a platform's accounts manifest ready to use.
|
|
3
|
+
|
|
4
|
+
```
|
|
5
|
+
pnpm exec prepare-accounts
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Finds `.reventless/users.yaml` — copying the package's committed
|
|
9
|
+
`users.example.yaml` into place when there is none — validates it, and generates a
|
|
10
|
+
password into every field left empty.
|
|
11
|
+
|
|
12
|
+
🚨 **On the local platform this is the whole of provisioning.** The manifest *is*
|
|
13
|
+
the user store there: the in-memory auth adapter hydrates from it at startup, so
|
|
14
|
+
once every account has a password there is nothing left to create. On AWS the
|
|
15
|
+
accounts still have to be made in a pool, which is `provision-accounts` in
|
|
16
|
+
`reventless-aws`.
|
|
17
|
+
|
|
18
|
+
Here, rather than as a flag on that command, because it belongs to neither
|
|
19
|
+
platform. A local developer reaching it through an AWS package's binary would be
|
|
20
|
+
the only thing about this half that mentioned AWS at all — and the local platform
|
|
21
|
+
does not depend on `reventless-aws`, so for a project that never deploys it would
|
|
22
|
+
not resolve.
|
|
23
|
+
|
|
24
|
+
It creates nothing, contacts nothing, and needs no credentials. Re-running is safe:
|
|
25
|
+
a password already in the file is never replaced.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
type args = {
|
|
29
|
+
file: option<string>,
|
|
30
|
+
help: bool,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let parseArgs = (argv: array<string>): result<args, string> => {
|
|
34
|
+
let acc = ref(Ok({file: None, help: false}))
|
|
35
|
+
let i = ref(0)
|
|
36
|
+
let count = argv->Array.length
|
|
37
|
+
while i.contents < count {
|
|
38
|
+
let flag = argv->Array.getUnsafe(i.contents)
|
|
39
|
+
let value = argv->Array.get(i.contents + 1)
|
|
40
|
+
switch (acc.contents, flag, value) {
|
|
41
|
+
| (Error(_), _, _) => i := count
|
|
42
|
+
| (Ok(a), "--file", Some(v)) =>
|
|
43
|
+
acc := Ok({...a, file: Some(v)})
|
|
44
|
+
i := i.contents + 2
|
|
45
|
+
| (Ok(a), "--help", _) | (Ok(a), "-h", _) =>
|
|
46
|
+
acc := Ok({...a, help: true})
|
|
47
|
+
i := i.contents + 1
|
|
48
|
+
| (Ok(_), "--file", None) => acc := Error(`${flag} needs a value`)
|
|
49
|
+
| (Ok(_), unknown, _) => acc := Error(`unknown argument "${unknown}"`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
acc.contents
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let usage = `
|
|
56
|
+
Make a platform's accounts manifest ready to use.
|
|
57
|
+
|
|
58
|
+
--file <path> The manifest. Defaults to .reventless/users.yaml relative to the
|
|
59
|
+
working directory, which is where every platform keeps it. A
|
|
60
|
+
named path is read, never created.
|
|
61
|
+
|
|
62
|
+
Copies ${AccountsManifest.templateName} into place when there is no manifest yet,
|
|
63
|
+
then generates a password into every empty field. Creates no accounts and needs no
|
|
64
|
+
credentials — on the local platform that is all provisioning is. A password already
|
|
65
|
+
in the file is never replaced.
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
/** The cast, as the file now declares it. Printed on a fresh copy because that is
|
|
69
|
+
the moment somebody should read it: on AWS the next command turns each of these
|
|
70
|
+
into a real account in a real pool. */
|
|
71
|
+
let castNote = (entries: array<AccountsManifest.entry>): string =>
|
|
72
|
+
entries
|
|
73
|
+
->Array.map(entry =>
|
|
74
|
+
` ${entry.username}${entry.groups->Array.length == 0
|
|
75
|
+
? ""
|
|
76
|
+
: ` [${entry.groups->Array.join(", ")}]`}`
|
|
77
|
+
)
|
|
78
|
+
->Array.join("\n")
|
|
79
|
+
|
|
80
|
+
let run = (): result<unit, string> =>
|
|
81
|
+
// argv[0] is node, argv[1] this script.
|
|
82
|
+
switch parseArgs(NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)) {
|
|
83
|
+
| Error(_) as e => e
|
|
84
|
+
| Ok(args) if args.help =>
|
|
85
|
+
Console.log(usage)
|
|
86
|
+
Ok()
|
|
87
|
+
| Ok(args) =>
|
|
88
|
+
switch AccountsManifest.locate(~given=?args.file, ()) {
|
|
89
|
+
| Error(_) as e => e
|
|
90
|
+
| Ok(located) =>
|
|
91
|
+
let file = located->AccountsManifest.pathOf
|
|
92
|
+
switch located {
|
|
93
|
+
| SeededFrom(_, template) => Console.log(`manifest ${file} (new, copied from ${template})`)
|
|
94
|
+
| Declared(_) => Console.log(`manifest ${file}`)
|
|
95
|
+
}
|
|
96
|
+
switch AccountsManifest.prepare(~path=file) {
|
|
97
|
+
| Error(message) => Error(`${file}: ${message}`)
|
|
98
|
+
| Ok([]) => Error(`${file} declares no accounts`)
|
|
99
|
+
| Ok(prepared) =>
|
|
100
|
+
let generated = prepared->Array.filter(p => p.passwordGenerated)->Array.length
|
|
101
|
+
Console.log(
|
|
102
|
+
`accounts ${prepared
|
|
103
|
+
->Array.length
|
|
104
|
+
->Int.toString} declared, ${generated->Int.toString} password(s) generated\n\n${castNote(
|
|
105
|
+
prepared->Array.map(p => p.entry),
|
|
106
|
+
)}`,
|
|
107
|
+
)
|
|
108
|
+
Ok()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let main = async () =>
|
|
114
|
+
switch run() {
|
|
115
|
+
| Ok() => ()
|
|
116
|
+
| Error(message) =>
|
|
117
|
+
Console.error(`prepare-accounts: ${message}`)
|
|
118
|
+
NodeProcess.exit(1)
|
|
119
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as AccountsManifest$Reventless from "./AccountsManifest.res.mjs";
|
|
4
|
+
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
let acc = {
|
|
7
|
+
TAG: "Ok",
|
|
8
|
+
_0: {
|
|
9
|
+
file: undefined,
|
|
10
|
+
help: false
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
let i = 0;
|
|
14
|
+
let count = argv.length;
|
|
15
|
+
while (i < count) {
|
|
16
|
+
let flag = argv[i];
|
|
17
|
+
let value = argv[i + 1 | 0];
|
|
18
|
+
let match = acc;
|
|
19
|
+
if (match.TAG === "Ok") {
|
|
20
|
+
let a = match._0;
|
|
21
|
+
let exit = 0;
|
|
22
|
+
switch (flag) {
|
|
23
|
+
case "--file" :
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
acc = {
|
|
26
|
+
TAG: "Ok",
|
|
27
|
+
_0: {
|
|
28
|
+
file: value,
|
|
29
|
+
help: a.help
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
i = i + 2 | 0;
|
|
33
|
+
} else {
|
|
34
|
+
acc = {
|
|
35
|
+
TAG: "Error",
|
|
36
|
+
_0: flag + ` needs a value`
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case "--help" :
|
|
41
|
+
case "-h" :
|
|
42
|
+
exit = 1;
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
acc = {
|
|
46
|
+
TAG: "Error",
|
|
47
|
+
_0: `unknown argument "` + flag + `"`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (exit === 1) {
|
|
51
|
+
acc = {
|
|
52
|
+
TAG: "Ok",
|
|
53
|
+
_0: {
|
|
54
|
+
file: a.file,
|
|
55
|
+
help: true
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
i = i + 1 | 0;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
i = count;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
return acc;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let usage = `
|
|
68
|
+
Make a platform's accounts manifest ready to use.
|
|
69
|
+
|
|
70
|
+
--file <path> The manifest. Defaults to .reventless/users.yaml relative to the
|
|
71
|
+
working directory, which is where every platform keeps it. A
|
|
72
|
+
named path is read, never created.
|
|
73
|
+
|
|
74
|
+
Copies ` + AccountsManifest$Reventless.templateName + ` into place when there is no manifest yet,
|
|
75
|
+
then generates a password into every empty field. Creates no accounts and needs no
|
|
76
|
+
credentials — on the local platform that is all provisioning is. A password already
|
|
77
|
+
in the file is never replaced.
|
|
78
|
+
`;
|
|
79
|
+
|
|
80
|
+
function castNote(entries) {
|
|
81
|
+
return entries.map(entry => ` ` + entry.username + (
|
|
82
|
+
entry.groups.length === 0 ? "" : ` [` + entry.groups.join(", ") + `]`
|
|
83
|
+
)).join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function run() {
|
|
87
|
+
let e = parseArgs(process.argv.slice(2, process.argv.length));
|
|
88
|
+
if (e.TAG !== "Ok") {
|
|
89
|
+
return e;
|
|
90
|
+
}
|
|
91
|
+
let args = e._0;
|
|
92
|
+
if (args.help) {
|
|
93
|
+
console.log(usage);
|
|
94
|
+
return {
|
|
95
|
+
TAG: "Ok",
|
|
96
|
+
_0: undefined
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
let e$1 = AccountsManifest$Reventless.locate(args.file, undefined);
|
|
100
|
+
if (e$1.TAG !== "Ok") {
|
|
101
|
+
return e$1;
|
|
102
|
+
}
|
|
103
|
+
let located = e$1._0;
|
|
104
|
+
let file = AccountsManifest$Reventless.pathOf(located);
|
|
105
|
+
if (located.TAG === "Declared") {
|
|
106
|
+
console.log(`manifest ` + file);
|
|
107
|
+
} else {
|
|
108
|
+
console.log(`manifest ` + file + ` (new, copied from ` + located._1 + `)`);
|
|
109
|
+
}
|
|
110
|
+
let message = AccountsManifest$Reventless.prepare(file);
|
|
111
|
+
if (message.TAG !== "Ok") {
|
|
112
|
+
return {
|
|
113
|
+
TAG: "Error",
|
|
114
|
+
_0: file + `: ` + message._0
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
let prepared = message._0;
|
|
118
|
+
if (prepared.length === 0) {
|
|
119
|
+
return {
|
|
120
|
+
TAG: "Error",
|
|
121
|
+
_0: file + ` declares no accounts`
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
let generated = prepared.filter(p => p.passwordGenerated).length;
|
|
125
|
+
console.log(`accounts ` + prepared.length.toString() + ` declared, ` + generated.toString() + ` password(s) generated\n\n` + castNote(prepared.map(p => p.entry)));
|
|
126
|
+
return {
|
|
127
|
+
TAG: "Ok",
|
|
128
|
+
_0: undefined
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main() {
|
|
133
|
+
let message = run();
|
|
134
|
+
if (message.TAG === "Ok") {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
console.error(`prepare-accounts: ` + message._0);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export {
|
|
142
|
+
parseArgs,
|
|
143
|
+
usage,
|
|
144
|
+
castNote,
|
|
145
|
+
run,
|
|
146
|
+
main,
|
|
147
|
+
}
|
|
148
|
+
/* AccountsManifest-Reventless Not a pure module */
|