@astram/identity 0.13.0 → 0.15.0
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/dist/features/authz/grant-admin.service.d.ts +1 -1
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +8 -0
- package/dist/features/authz/grant-ceiling.d.ts +11 -3
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +20 -3
- package/dist/features/authz/record-authorization.d.ts +1 -1
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +3 -2
- package/dist/features/company/company-hierarchy.service.d.ts +6 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +19 -0
- package/dist/features/identity-feature.module.d.ts +7 -1
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +46 -1
- package/dist/features/membership/membership.service.d.ts +9 -0
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +40 -0
- package/dist/features/membership/posting.service.d.ts +18 -0
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +85 -27
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts +20 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.js +24 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +58 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.js +323 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +79 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +288 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts +19 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.js +30 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts +8 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.js +10 -0
- package/dist/features/org-bootstrap/org-names.d.ts +12 -0
- package/dist/features/org-bootstrap/org-names.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-names.js +26 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts +15 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-state.reader.js +225 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts +9 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts.map +1 -0
- package/dist/features/org-bootstrap/root-password-source.js +57 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +61 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.js +774 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts +42 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts.map +1 -0
- package/dist/features/org-bootstrap/set-password-links.js +76 -0
- package/dist/features/org-structure/org-catalog.service.d.ts +13 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +40 -0
- package/dist/features/role/role-owner-transition.d.ts +17 -0
- package/dist/features/role/role-owner-transition.d.ts.map +1 -0
- package/dist/features/role/role-owner-transition.js +23 -0
- package/dist/features/role/role.controller.d.ts +1 -1
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +6 -4
- package/dist/features/role/role.service.d.ts +8 -2
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +13 -3
- package/dist/features/user/credential-provisioner.token.d.ts +8 -0
- package/dist/features/user/credential-provisioner.token.d.ts.map +1 -0
- package/dist/features/user/credential-provisioner.token.js +10 -0
- package/dist/features/user/invite-message.d.ts +8 -0
- package/dist/features/user/invite-message.d.ts.map +1 -0
- package/dist/features/user/invite-message.js +23 -0
- package/dist/features/user/invite.config.d.ts +15 -0
- package/dist/features/user/invite.config.d.ts.map +1 -0
- package/dist/features/user/invite.config.js +55 -0
- package/dist/features/user/invite.service.d.ts +62 -0
- package/dist/features/user/invite.service.d.ts.map +1 -0
- package/dist/features/user/invite.service.js +254 -0
- package/dist/features/user/invite.tokens.d.ts +11 -0
- package/dist/features/user/invite.tokens.d.ts.map +1 -0
- package/dist/features/user/invite.tokens.js +13 -0
- package/dist/features/user/user.controller.d.ts +13 -6
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +30 -4
- package/dist/features/user/user.service.d.ts +51 -10
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +96 -27
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/role.object.d.ts +1 -0
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +8 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +3 -0
- package/migrations/1710000000101-AddRoleOwnerCompany.cjs +98 -0
- package/package.json +4 -4
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OrgStateReader = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* G-009 T9: the read-only org state reader.
|
|
6
|
+
*
|
|
7
|
+
* Issues SELECTs only, every name bound as a parameter, database names taken from the injected
|
|
8
|
+
* `schemas` (never a literal). Scope: the file's tenants' subtrees (top-level companies matched by name,
|
|
9
|
+
* then `scope_closure`), plus the global lookups the planner's conflicts need (roles by name and the
|
|
10
|
+
* tenant that owns them, usernames, emails, credential state). The row-to-state mapping is proven live.
|
|
11
|
+
* A query failure propagates: a partial state is never returned.
|
|
12
|
+
*/
|
|
13
|
+
const guard_1 = require("@astram/guard");
|
|
14
|
+
const org_names_js_1 = require("./org-names.js");
|
|
15
|
+
/** `?, ?, ?`; an empty list reads as `NULL` so the clause stays valid SQL and matches nothing. */
|
|
16
|
+
const marks = (n) => n === 0 ? 'NULL' : Array.from({ length: n }, () => '?').join(', ');
|
|
17
|
+
const str = (v) => (v === null || v === undefined ? '' : String(v));
|
|
18
|
+
const strOrNull = (v) => (v === null || v === undefined ? null : String(v));
|
|
19
|
+
function collect(file) {
|
|
20
|
+
const names = {
|
|
21
|
+
tenants: [],
|
|
22
|
+
roles: new Set(),
|
|
23
|
+
departments: new Set(),
|
|
24
|
+
branches: new Set(),
|
|
25
|
+
usernames: new Set(),
|
|
26
|
+
emails: new Set(),
|
|
27
|
+
};
|
|
28
|
+
const walk = (c) => {
|
|
29
|
+
for (const d of c.departments ?? [])
|
|
30
|
+
names.departments.add(d);
|
|
31
|
+
for (const b of c.branches ?? [])
|
|
32
|
+
names.branches.add(b);
|
|
33
|
+
for (const r of c.roles ?? [])
|
|
34
|
+
names.roles.add(r.name);
|
|
35
|
+
for (const u of c.users ?? []) {
|
|
36
|
+
names.usernames.add(u.username);
|
|
37
|
+
names.emails.add(u.email);
|
|
38
|
+
}
|
|
39
|
+
for (const ch of c.children ?? [])
|
|
40
|
+
walk(ch);
|
|
41
|
+
};
|
|
42
|
+
for (const t of file.tenants) {
|
|
43
|
+
names.tenants.push(t.name);
|
|
44
|
+
walk(t);
|
|
45
|
+
}
|
|
46
|
+
for (const r of file.roots) {
|
|
47
|
+
names.usernames.add(r.username);
|
|
48
|
+
names.emails.add(r.email);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
tenants: names.tenants,
|
|
52
|
+
roles: [...names.roles],
|
|
53
|
+
departments: [...names.departments],
|
|
54
|
+
branches: [...names.branches],
|
|
55
|
+
usernames: [...names.usernames],
|
|
56
|
+
emails: [...names.emails],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
class OrgStateReader {
|
|
60
|
+
opts;
|
|
61
|
+
constructor(opts) {
|
|
62
|
+
this.opts = opts;
|
|
63
|
+
}
|
|
64
|
+
async read(file) {
|
|
65
|
+
const { query, schemas } = this.opts;
|
|
66
|
+
const app = `\`${schemas.app}\``;
|
|
67
|
+
const authz = `\`${schemas.authz}\``;
|
|
68
|
+
const identity = `\`${schemas.identity}\``;
|
|
69
|
+
const names = collect(file);
|
|
70
|
+
// 1. the file's top-level tenants, by name (the column collation does the matching)
|
|
71
|
+
const tenantRows = await query(`SELECT c_uuid, c_name FROM ${app}.companies WHERE c_parent_uuid IS NULL AND c_name IN (${marks(names.tenants.length)})`, names.tenants);
|
|
72
|
+
const tenantUuids = tenantRows.map((r) => str(r.c_uuid));
|
|
73
|
+
// 2. their subtrees, through scope_closure
|
|
74
|
+
const subtreeRows = await query(`SELECT c.c_uuid, c.c_name, c.c_parent_uuid FROM ${authz}.scope_closure sc ` +
|
|
75
|
+
`JOIN ${app}.companies c ON c.c_uuid = sc.descendant_uuid WHERE sc.ancestor_uuid IN (${marks(tenantUuids.length)})`, tenantUuids);
|
|
76
|
+
const byUuid = new Map(subtreeRows.map((r) => [str(r.c_uuid), r]));
|
|
77
|
+
const pathOf = (uuid) => {
|
|
78
|
+
const segs = [];
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (let r = byUuid.get(uuid); r && !seen.has(str(r.c_uuid)); r = byUuid.get(str(r.c_parent_uuid))) {
|
|
81
|
+
seen.add(str(r.c_uuid));
|
|
82
|
+
segs.unshift(str(r.c_name));
|
|
83
|
+
}
|
|
84
|
+
return segs.join('/');
|
|
85
|
+
};
|
|
86
|
+
const subtreeUuids = [...byUuid.keys()];
|
|
87
|
+
// R1-3: a name matching several top-level companies, or several children of one parent, is
|
|
88
|
+
// reported (the planner refuses); the run never picks one.
|
|
89
|
+
const ambiguousCompanies = [];
|
|
90
|
+
const markAmbiguous = (rows, groupOf, pathFor) => {
|
|
91
|
+
const groups = new Map();
|
|
92
|
+
for (const r of rows) {
|
|
93
|
+
const key = groupOf(r);
|
|
94
|
+
groups.set(key, [...(groups.get(key) ?? []), r]);
|
|
95
|
+
}
|
|
96
|
+
for (const group of groups.values()) {
|
|
97
|
+
if (group.length > 1)
|
|
98
|
+
ambiguousCompanies.push(pathFor(group[0]));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
markAmbiguous(tenantRows, (r) => (0, org_names_js_1.nameKey)(str(r.c_name)), (r) => str(r.c_name));
|
|
102
|
+
markAmbiguous(subtreeRows.filter((r) => r.c_parent_uuid !== null && r.c_parent_uuid !== undefined), (r) => `${str(r.c_parent_uuid)}\u0000${(0, org_names_js_1.nameKey)(str(r.c_name))}`, (r) => pathOf(str(r.c_uuid)));
|
|
103
|
+
// 3. catalogs and the company junctions
|
|
104
|
+
const deptCatalog = await query(`SELECT d_name FROM ${app}.departments WHERE d_name IN (${marks(names.departments.length)})`, names.departments);
|
|
105
|
+
const branchCatalog = await query(`SELECT b_name FROM ${app}.branches WHERE b_name IN (${marks(names.branches.length)})`, names.branches);
|
|
106
|
+
const deptJunction = await query(`SELECT cd.cd_company_uuid, d.d_name FROM ${app}.company_departments cd ` +
|
|
107
|
+
`JOIN ${app}.departments d ON d.d_uuid = cd.cd_department_uuid WHERE cd.cd_company_uuid IN (${marks(subtreeUuids.length)})`, subtreeUuids);
|
|
108
|
+
const branchJunction = await query(`SELECT cb.cb_company_uuid, b.b_name FROM ${app}.company_branches cb ` +
|
|
109
|
+
`JOIN ${app}.branches b ON b.b_uuid = cb.cb_branch_uuid WHERE cb.cb_company_uuid IN (${marks(subtreeUuids.length)})`, subtreeUuids);
|
|
110
|
+
const companies = new Map();
|
|
111
|
+
for (const uuid of subtreeUuids) {
|
|
112
|
+
companies.set(uuid, { path: pathOf(uuid), departments: [], branches: [] });
|
|
113
|
+
}
|
|
114
|
+
for (const r of deptJunction)
|
|
115
|
+
companies.get(str(r.cd_company_uuid))?.departments.push(str(r.d_name));
|
|
116
|
+
for (const r of branchJunction)
|
|
117
|
+
companies.get(str(r.cb_company_uuid))?.branches.push(str(r.b_name));
|
|
118
|
+
// 4. roles by name or owned in the subtrees; owner's top-level tenant; held outside the subtrees
|
|
119
|
+
const roleRows = await query(`SELECT r.r_uuid, r.r_name, r.r_owner_company_uuid, t.c_name AS owner_tenant, ` +
|
|
120
|
+
`EXISTS (SELECT 1 FROM ${app}.membership_posting mp JOIN ${app}.company_membership cm ON cm.cm_uuid = mp.mp_membership_uuid ` +
|
|
121
|
+
`WHERE mp.mp_role_uuid = r.r_uuid AND cm.cm_company_uuid NOT IN (` +
|
|
122
|
+
`SELECT sc2.descendant_uuid FROM ${authz}.scope_closure sc2 WHERE sc2.ancestor_uuid IN (${marks(tenantUuids.length)}))) AS held_outside ` +
|
|
123
|
+
`FROM ${app}.roles r ` +
|
|
124
|
+
`LEFT JOIN ${authz}.scope_closure oc ON oc.descendant_uuid = r.r_owner_company_uuid ` +
|
|
125
|
+
`LEFT JOIN ${app}.companies t ON t.c_uuid = oc.ancestor_uuid AND t.c_parent_uuid IS NULL ` +
|
|
126
|
+
`WHERE r.r_name IN (${marks(names.roles.length)}) OR r.r_owner_company_uuid IN (${marks(subtreeUuids.length)})`, [...tenantUuids, ...names.roles, ...subtreeUuids]);
|
|
127
|
+
const roleUuids = roleRows.map((r) => str(r.r_uuid));
|
|
128
|
+
const grantRows = await query(`SELECT rp_role_uuid, rp_perm_key FROM ${authz}.role_permission WHERE rp_role_uuid IN (${marks(roleUuids.length)})`, roleUuids);
|
|
129
|
+
const grants = new Map();
|
|
130
|
+
for (const g of grantRows) {
|
|
131
|
+
const list = grants.get(str(g.rp_role_uuid)) ?? [];
|
|
132
|
+
list.push(str(g.rp_perm_key));
|
|
133
|
+
grants.set(str(g.rp_role_uuid), list);
|
|
134
|
+
}
|
|
135
|
+
const roles = [];
|
|
136
|
+
for (const r of roleRows) {
|
|
137
|
+
if (roles.some((x) => x.name === str(r.r_name)))
|
|
138
|
+
continue; // duplicate rows from closure joins
|
|
139
|
+
roles.push({
|
|
140
|
+
name: str(r.r_name),
|
|
141
|
+
ownerTenant: strOrNull(r.owner_tenant),
|
|
142
|
+
heldOutside: Number(r.held_outside) === 1,
|
|
143
|
+
permissions: grants.get(str(r.r_uuid)) ?? [],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// 5. users (by username, or a member of the subtrees) with their credential
|
|
147
|
+
const userRows = await query(`SELECT u.u_uuid, u.u_username, u.u_email, u.u_is_platform_superuser, ic.ic_password_hash ` +
|
|
148
|
+
`FROM ${app}.users u LEFT JOIN ${identity}.identity_credential ic ON ic.ic_principal_id = u.u_uuid ` +
|
|
149
|
+
`WHERE u.u_username IN (${marks(names.usernames.length)}) ` +
|
|
150
|
+
`OR u.u_uuid IN (SELECT cm_user_uuid FROM ${app}.company_membership WHERE cm_company_uuid IN (${marks(subtreeUuids.length)}))`, [...names.usernames, ...subtreeUuids]);
|
|
151
|
+
// R1-1: usernames are global, so say which of those users also belong somewhere outside the file's
|
|
152
|
+
// tenants (another tenant's account); the planner refuses to adopt them.
|
|
153
|
+
const userUuids = [...new Set(userRows.map((u) => str(u.u_uuid)))];
|
|
154
|
+
const outsideRows = await query(`SELECT DISTINCT cm_user_uuid FROM ${app}.company_membership ` +
|
|
155
|
+
`WHERE cm_user_uuid IN (${marks(userUuids.length)}) AND cm_company_uuid NOT IN (` +
|
|
156
|
+
`SELECT sc3.descendant_uuid FROM ${authz}.scope_closure sc3 WHERE sc3.ancestor_uuid IN (${marks(tenantUuids.length)}))`, [...userUuids, ...tenantUuids]);
|
|
157
|
+
const outsideUsers = new Set(outsideRows.map((r) => str(r.cm_user_uuid)));
|
|
158
|
+
const emailRows = await query(`SELECT u_email, u_username FROM ${app}.users WHERE u_email IN (${marks(names.emails.length)})`, names.emails);
|
|
159
|
+
// 6. memberships and postings inside the subtrees
|
|
160
|
+
const memberRows = await query(`SELECT cm_uuid, cm_user_uuid, cm_company_uuid, cm_is_active FROM ${app}.company_membership ` +
|
|
161
|
+
`WHERE cm_company_uuid IN (${marks(subtreeUuids.length)})`, subtreeUuids);
|
|
162
|
+
const postingRows = await query(`SELECT mp.mp_membership_uuid, r.r_name, d.d_name, b.b_name FROM ${app}.membership_posting mp ` +
|
|
163
|
+
`JOIN ${app}.company_membership cm ON cm.cm_uuid = mp.mp_membership_uuid ` +
|
|
164
|
+
`JOIN ${app}.roles r ON r.r_uuid = mp.mp_role_uuid ` +
|
|
165
|
+
`LEFT JOIN ${app}.departments d ON d.d_uuid = mp.mp_department_uuid ` +
|
|
166
|
+
`LEFT JOIN ${app}.branches b ON b.b_uuid = mp.mp_branch_uuid ` +
|
|
167
|
+
`WHERE cm.cm_company_uuid IN (${marks(subtreeUuids.length)})`, subtreeUuids);
|
|
168
|
+
const postings = new Map();
|
|
169
|
+
for (const p of postingRows) {
|
|
170
|
+
const posting = { role: str(p.r_name) };
|
|
171
|
+
if (p.d_name !== null && p.d_name !== undefined)
|
|
172
|
+
posting.department = str(p.d_name);
|
|
173
|
+
if (p.b_name !== null && p.b_name !== undefined)
|
|
174
|
+
posting.branch = str(p.b_name);
|
|
175
|
+
const list = postings.get(str(p.mp_membership_uuid)) ?? [];
|
|
176
|
+
list.push(posting);
|
|
177
|
+
postings.set(str(p.mp_membership_uuid), list);
|
|
178
|
+
}
|
|
179
|
+
const memberships = new Map();
|
|
180
|
+
for (const m of memberRows) {
|
|
181
|
+
const list = memberships.get(str(m.cm_user_uuid)) ?? [];
|
|
182
|
+
list.push({
|
|
183
|
+
companyPath: pathOf(str(m.cm_company_uuid)),
|
|
184
|
+
active: Number(m.cm_is_active) === 1,
|
|
185
|
+
postings: postings.get(str(m.cm_uuid)) ?? [],
|
|
186
|
+
});
|
|
187
|
+
memberships.set(str(m.cm_user_uuid), list);
|
|
188
|
+
}
|
|
189
|
+
const rank = { none: 0, locked: 1, usable: 2 };
|
|
190
|
+
const users = new Map();
|
|
191
|
+
for (const u of userRows) {
|
|
192
|
+
const hash = u.ic_password_hash;
|
|
193
|
+
const credential = hash === null || hash === undefined
|
|
194
|
+
? 'none'
|
|
195
|
+
: (0, guard_1.isLockedHash)(str(hash))
|
|
196
|
+
? 'locked'
|
|
197
|
+
: 'usable';
|
|
198
|
+
const id = str(u.u_uuid);
|
|
199
|
+
const prior = users.get(id);
|
|
200
|
+
if (prior) {
|
|
201
|
+
if (rank[credential] > rank[prior.credential])
|
|
202
|
+
prior.credential = credential;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
users.set(id, {
|
|
206
|
+
username: str(u.u_username),
|
|
207
|
+
email: str(u.u_email),
|
|
208
|
+
credential,
|
|
209
|
+
isPlatformSuperuser: Number(u.u_is_platform_superuser) === 1,
|
|
210
|
+
memberships: memberships.get(id) ?? [],
|
|
211
|
+
membershipsOutside: outsideUsers.has(id),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
companies: [...companies.values()],
|
|
216
|
+
departments: deptCatalog.map((r) => str(r.d_name)),
|
|
217
|
+
branches: branchCatalog.map((r) => str(r.b_name)),
|
|
218
|
+
roles,
|
|
219
|
+
users: [...users.values()],
|
|
220
|
+
emailHolders: emailRows.map((r) => ({ email: str(r.u_email), username: str(r.u_username) })),
|
|
221
|
+
ambiguousCompanies,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
exports.OrgStateReader = OrgStateReader;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const rootPasswordVariable: (username: string) => string;
|
|
2
|
+
/**
|
|
3
|
+
* Two roots whose names fold to one variable (`a.b`, `a_b`) would share a password: refused before
|
|
4
|
+
* any write. The refusal names the variable only.
|
|
5
|
+
*/
|
|
6
|
+
export declare function assertDistinctRootPasswordVariables(usernames: readonly string[]): void;
|
|
7
|
+
/** The password of every listed root, or a refusal before anything is written. */
|
|
8
|
+
export declare function resolveRootPasswords(usernames: readonly string[], env: NodeJS.ProcessEnv, prompt: ((username: string) => Promise<string>) | undefined): Promise<Map<string, string>>;
|
|
9
|
+
//# sourceMappingURL=root-password-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"root-password-source.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/root-password-source.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,oBAAoB,GAAI,UAAU,MAAM,KAAG,MACgC,CAAC;AAEzF;;;GAGG;AACH,wBAAgB,mCAAmC,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAYtF;AAED,kFAAkF;AAClF,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,GAAG,EAAE,MAAM,CAAC,UAAU,EACtB,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,GAC1D,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiC9B"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rootPasswordVariable = void 0;
|
|
4
|
+
exports.assertDistinctRootPasswordVariables = assertDistinctRootPasswordVariables;
|
|
5
|
+
exports.resolveRootPasswords = resolveRootPasswords;
|
|
6
|
+
/**
|
|
7
|
+
* Where a root's first password comes from (G-009 R7): `ASTRAM_BOOTSTRAP_ROOT_PASSWORD_<USERNAME>`,
|
|
8
|
+
* which outside development/test must have been resolved from a `secret-ref:` value, or an
|
|
9
|
+
* interactive prompt the caller supplies. A password is never echoed: every error names only the
|
|
10
|
+
* variable.
|
|
11
|
+
*/
|
|
12
|
+
const guard_1 = require("@astram/guard");
|
|
13
|
+
const org_bootstrap_error_js_1 = require("./org-bootstrap-error.js");
|
|
14
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
15
|
+
const rootPasswordVariable = (username) => `ASTRAM_BOOTSTRAP_ROOT_PASSWORD_${username.toUpperCase().replace(/[^A-Z0-9]/gu, '_')}`;
|
|
16
|
+
exports.rootPasswordVariable = rootPasswordVariable;
|
|
17
|
+
/**
|
|
18
|
+
* Two roots whose names fold to one variable (`a.b`, `a_b`) would share a password: refused before
|
|
19
|
+
* any write. The refusal names the variable only.
|
|
20
|
+
*/
|
|
21
|
+
function assertDistinctRootPasswordVariables(usernames) {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
for (const username of usernames) {
|
|
24
|
+
const variable = (0, exports.rootPasswordVariable)(username);
|
|
25
|
+
if (seen.has(variable)) {
|
|
26
|
+
throw new org_bootstrap_error_js_1.OrgBootstrapError('root_password_collision', `Two roots map to the same password variable ${variable}; rename one so each root has its own.`);
|
|
27
|
+
}
|
|
28
|
+
seen.add(variable);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** The password of every listed root, or a refusal before anything is written. */
|
|
32
|
+
async function resolveRootPasswords(usernames, env, prompt) {
|
|
33
|
+
const passwords = new Map();
|
|
34
|
+
const devOrTest = (0, guard_1.isDevOrTestNodeEnv)(env.NODE_ENV);
|
|
35
|
+
for (const username of usernames) {
|
|
36
|
+
const variable = (0, exports.rootPasswordVariable)(username);
|
|
37
|
+
let password;
|
|
38
|
+
if (prompt !== undefined) {
|
|
39
|
+
password = await prompt(username);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const value = env[variable];
|
|
43
|
+
if (value === undefined || value === '') {
|
|
44
|
+
throw new org_bootstrap_error_js_1.OrgBootstrapError('root_password_missing', `No password source for root "${username}": set ${variable} (a secret-ref value outside development/test) or run interactively.`);
|
|
45
|
+
}
|
|
46
|
+
if (!devOrTest && !(0, guard_1.wasResolvedFromSecretRef)(variable, env)) {
|
|
47
|
+
throw new org_bootstrap_error_js_1.OrgBootstrapError('root_password_literal', `${variable} must be a secret-ref value resolved at boot outside development/test; a literal password is refused.`);
|
|
48
|
+
}
|
|
49
|
+
password = value;
|
|
50
|
+
}
|
|
51
|
+
if ([...password].length < MIN_PASSWORD_LENGTH) {
|
|
52
|
+
throw new org_bootstrap_error_js_1.OrgBootstrapError('root_password_weak', `The password for root "${username}" must be at least ${MIN_PASSWORD_LENGTH} characters.`);
|
|
53
|
+
}
|
|
54
|
+
passwords.set(username, password);
|
|
55
|
+
}
|
|
56
|
+
return passwords;
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { INestApplicationContext } from '@nestjs/common';
|
|
2
|
+
import { type OrgBootstrapWritten } from './org-bootstrap-error.js';
|
|
3
|
+
import { type OrgPlan } from './org-bootstrap-plan.js';
|
|
4
|
+
export { OrgBootstrapError } from './org-bootstrap-error.js';
|
|
5
|
+
export type { OrgBootstrapErrorCode, OrgBootstrapWritten } from './org-bootstrap-error.js';
|
|
6
|
+
export interface RunOrgBootstrapOptions {
|
|
7
|
+
/** Path of a JSON file, or the already parsed object. */
|
|
8
|
+
file: string | object;
|
|
9
|
+
/** Write. Without it the run is a dry run: it prints the plan and writes nothing. */
|
|
10
|
+
apply?: boolean;
|
|
11
|
+
/** The human operator (audit `onBehalfOf`); else `ASTRAM_BOOTSTRAP_OPERATOR`. Required with `apply`. */
|
|
12
|
+
operator?: string;
|
|
13
|
+
env?: NodeJS.ProcessEnv;
|
|
14
|
+
/** Claim roles that have no owner and that nobody outside the file's tenants holds. */
|
|
15
|
+
claimUnownedRoles?: boolean;
|
|
16
|
+
/** Issue a fresh set-password link to users whose credential is still locked. */
|
|
17
|
+
reissueLinks?: boolean;
|
|
18
|
+
/** Link lifetime in whole hours, 1 to 168 (default 72). */
|
|
19
|
+
linkTtlHours?: number;
|
|
20
|
+
/** Print links although stdout is not a terminal. */
|
|
21
|
+
linksToStdout?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* G-010 R12: email each link through the message dispatcher (purpose `invite`) instead of printing
|
|
24
|
+
* it. Needs a bound dispatcher with mail on (`ShieldModule.forRoot({ emailSender })`), else the run
|
|
25
|
+
* is refused before any write. A link that cannot be sent is killed and not printed. ROOT links
|
|
26
|
+
* are never emailed (a root link is a platform takeover): they print under the G-009 rule (a
|
|
27
|
+
* terminal, or `linksToStdout`), else they are not issued and are reported as not delivered.
|
|
28
|
+
*/
|
|
29
|
+
emailLinks?: boolean;
|
|
30
|
+
isTTY?: boolean;
|
|
31
|
+
/** Seconds to wait for the run lock (default 30). */
|
|
32
|
+
lockTimeoutSeconds?: number;
|
|
33
|
+
/** Line writer for the plan, links and root ids (default: stdout). */
|
|
34
|
+
out?: (line: string) => void;
|
|
35
|
+
/** Interactive root password source; replaces `ASTRAM_BOOTSTRAP_ROOT_PASSWORD_<USERNAME>`. */
|
|
36
|
+
promptRootPassword?: (username: string) => Promise<string>;
|
|
37
|
+
}
|
|
38
|
+
/** Never contains a link, a token or a password. */
|
|
39
|
+
export interface OrgBootstrapReport {
|
|
40
|
+
applied: boolean;
|
|
41
|
+
plan: OrgPlan;
|
|
42
|
+
/** Plan entries of each kind this run created. */
|
|
43
|
+
written: OrgBootstrapWritten;
|
|
44
|
+
/** Set-password links issued. */
|
|
45
|
+
links: number;
|
|
46
|
+
/**
|
|
47
|
+
* With `emailLinks`: links emailed, and links not delivered (a failed send, killed; or a root
|
|
48
|
+
* link with no terminal to print it on, never issued). Nothing undelivered is printed.
|
|
49
|
+
*/
|
|
50
|
+
linksEmailed: number;
|
|
51
|
+
linksNotEmailed: number;
|
|
52
|
+
/**
|
|
53
|
+
* With `emailLinks`: root links printed to the terminal instead. A root link is a platform
|
|
54
|
+
* takeover, so it is never emailed; it follows the G-009 print rule.
|
|
55
|
+
*/
|
|
56
|
+
linksPrintedRoots: number;
|
|
57
|
+
/** Uuid of every root in the file that exists after the run. */
|
|
58
|
+
rootIds: string[];
|
|
59
|
+
}
|
|
60
|
+
export declare function runOrgBootstrap(app: INestApplicationContext, opts: RunOrgBootstrapOptions): Promise<OrgBootstrapReport>;
|
|
61
|
+
//# sourceMappingURL=run-org-bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-org-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/run-org-bootstrap.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,uBAAuB,EAAQ,MAAM,gBAAgB,CAAC;AA+BpE,OAAO,EAAqB,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,EAEL,KAAK,OAAO,EAIb,MAAM,yBAAyB,CAAC;AAiBjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE3F,MAAM,WAAW,sBAAsB;IACrC,yDAAyD;IACzD,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IACtB,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,uFAAuF;IACvF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,iFAAiF;IACjF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qDAAqD;IACrD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,sEAAsE;IACtE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,8FAA8F;IAC9F,kBAAkB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5D;AAED,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,kDAAkD;IAClD,OAAO,EAAE,mBAAmB,CAAC;IAC7B,iCAAiC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAwFD,wBAAsB,eAAe,CACnC,GAAG,EAAE,uBAAuB,EAC5B,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,kBAAkB,CAAC,CAqtB7B"}
|