@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,288 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLAN_KIND_ORDER = void 0;
|
|
4
|
+
exports.planOrgBootstrap = planOrgBootstrap;
|
|
5
|
+
const org_names_js_1 = require("./org-names.js");
|
|
6
|
+
exports.PLAN_KIND_ORDER = [
|
|
7
|
+
'company',
|
|
8
|
+
'department',
|
|
9
|
+
'branch',
|
|
10
|
+
'company_department',
|
|
11
|
+
'company_branch',
|
|
12
|
+
'role',
|
|
13
|
+
'role_claim',
|
|
14
|
+
'grant',
|
|
15
|
+
'user',
|
|
16
|
+
'membership',
|
|
17
|
+
'posting',
|
|
18
|
+
'credential',
|
|
19
|
+
'root_flag',
|
|
20
|
+
];
|
|
21
|
+
function planOrgBootstrap(file, state, options) {
|
|
22
|
+
const out = [];
|
|
23
|
+
const add = (kind, path, status, detail) => {
|
|
24
|
+
out.push(detail === undefined ? { kind, path, status } : { kind, path, status, detail });
|
|
25
|
+
};
|
|
26
|
+
// ---- flatten the file (depth first, parent before children) ----
|
|
27
|
+
const companies = [];
|
|
28
|
+
const walk = (c, parent, tenant) => {
|
|
29
|
+
const path = parent === undefined ? c.name : `${parent}/${c.name}`;
|
|
30
|
+
companies.push({ path, tenant, node: c });
|
|
31
|
+
for (const child of c.children ?? [])
|
|
32
|
+
walk(child, path, tenant);
|
|
33
|
+
};
|
|
34
|
+
for (const t of file.tenants)
|
|
35
|
+
walk(t, undefined, t.name);
|
|
36
|
+
const tenantKeys = new Set(file.tenants.map((t) => (0, org_names_js_1.nameKey)(t.name)));
|
|
37
|
+
const inFileTenant = (path) => tenantKeys.has((0, org_names_js_1.nameKey)(path.split('/')[0] ?? ''));
|
|
38
|
+
// ---- companies, org units ----
|
|
39
|
+
const stateCompany = new Map(state.companies.map((c) => [(0, org_names_js_1.pathKey)(c.path), c]));
|
|
40
|
+
const stateDepts = new Set(state.departments.map(org_names_js_1.nameKey));
|
|
41
|
+
const stateBranches = new Set(state.branches.map(org_names_js_1.nameKey));
|
|
42
|
+
const declaredCompanyKeys = new Set(companies.map((c) => (0, org_names_js_1.pathKey)(c.path)));
|
|
43
|
+
const ambiguous = new Set(state.ambiguousCompanies.map(org_names_js_1.pathKey));
|
|
44
|
+
for (const c of companies) {
|
|
45
|
+
if (ambiguous.has((0, org_names_js_1.pathKey)(c.path))) {
|
|
46
|
+
add('company', c.path, 'conflict', `company "${c.path}" matches more than one existing company; the bootstrap never guesses`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
add('company', c.path, stateCompany.has((0, org_names_js_1.pathKey)(c.path)) ? 'unchanged' : 'create');
|
|
50
|
+
}
|
|
51
|
+
for (const c of state.companies) {
|
|
52
|
+
if (inFileTenant(c.path) && !declaredCompanyKeys.has((0, org_names_js_1.pathKey)(c.path))) {
|
|
53
|
+
add('company', c.path, 'not-in-file');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const seenDept = new Set();
|
|
57
|
+
const seenBranch = new Set();
|
|
58
|
+
for (const c of companies) {
|
|
59
|
+
for (const d of c.node.departments ?? []) {
|
|
60
|
+
if (seenDept.has((0, org_names_js_1.nameKey)(d)))
|
|
61
|
+
continue;
|
|
62
|
+
seenDept.add((0, org_names_js_1.nameKey)(d));
|
|
63
|
+
add('department', `dept:${d}`, stateDepts.has((0, org_names_js_1.nameKey)(d)) ? 'unchanged' : 'create');
|
|
64
|
+
}
|
|
65
|
+
for (const b of c.node.branches ?? []) {
|
|
66
|
+
if (seenBranch.has((0, org_names_js_1.nameKey)(b)))
|
|
67
|
+
continue;
|
|
68
|
+
seenBranch.add((0, org_names_js_1.nameKey)(b));
|
|
69
|
+
add('branch', `branch:${b}`, stateBranches.has((0, org_names_js_1.nameKey)(b)) ? 'unchanged' : 'create');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const c of companies) {
|
|
73
|
+
const sc = stateCompany.get((0, org_names_js_1.pathKey)(c.path));
|
|
74
|
+
const haveD = new Set((sc?.departments ?? []).map(org_names_js_1.nameKey));
|
|
75
|
+
const haveB = new Set((sc?.branches ?? []).map(org_names_js_1.nameKey));
|
|
76
|
+
for (const d of c.node.departments ?? []) {
|
|
77
|
+
add('company_department', `${c.path}#dept:${d}`, haveD.has((0, org_names_js_1.nameKey)(d)) ? 'unchanged' : 'create');
|
|
78
|
+
}
|
|
79
|
+
for (const b of c.node.branches ?? []) {
|
|
80
|
+
add('company_branch', `${c.path}#branch:${b}`, haveB.has((0, org_names_js_1.nameKey)(b)) ? 'unchanged' : 'create');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ---- roles and grants ----
|
|
84
|
+
const stateRole = new Map(state.roles.map((r) => [(0, org_names_js_1.nameKey)(r.name), r]));
|
|
85
|
+
const declaredRoles = new Set();
|
|
86
|
+
for (const c of companies) {
|
|
87
|
+
for (const r of c.node.roles ?? []) {
|
|
88
|
+
const key = (0, org_names_js_1.nameKey)(r.name);
|
|
89
|
+
declaredRoles.add(key);
|
|
90
|
+
const path = `role:${r.name}`;
|
|
91
|
+
const existing = stateRole.get(key);
|
|
92
|
+
if (!existing) {
|
|
93
|
+
add('role', path, 'create');
|
|
94
|
+
for (const k of r.permissions)
|
|
95
|
+
add('grant', `${path}#${k}`, 'create');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (existing.ownerTenant !== null && !tenantKeys.has((0, org_names_js_1.nameKey)(existing.ownerTenant))) {
|
|
99
|
+
add('role', path, 'conflict', `role "${r.name}" is owned by another tenant`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (existing.ownerTenant !== null && (0, org_names_js_1.nameKey)(existing.ownerTenant) !== (0, org_names_js_1.nameKey)(c.tenant)) {
|
|
103
|
+
add('role', path, 'conflict', `role "${r.name}" is owned by tenant "${existing.ownerTenant}", not by "${c.tenant}" which declares it`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (existing.ownerTenant === null) {
|
|
107
|
+
if (existing.heldOutside) {
|
|
108
|
+
add('role', path, 'conflict', `role "${r.name}" has no owner and is held outside the file's tenants`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!options.claimUnownedRoles) {
|
|
112
|
+
add('role', path, 'conflict', `role "${r.name}" has no owner; pass --claim-unowned-roles to claim it`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
add('role', path, 'unchanged');
|
|
116
|
+
add('role_claim', path, 'create', `claim "${r.name}" for tenant ${c.tenant}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
add('role', path, 'unchanged');
|
|
120
|
+
}
|
|
121
|
+
const have = new Set(existing.permissions);
|
|
122
|
+
for (const k of r.permissions)
|
|
123
|
+
add('grant', `${path}#${k}`, have.has(k) ? 'unchanged' : 'create');
|
|
124
|
+
const want = new Set(r.permissions);
|
|
125
|
+
for (const k of existing.permissions)
|
|
126
|
+
if (!want.has(k))
|
|
127
|
+
add('grant', `${path}#${k}`, 'not-in-file');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const r of state.roles) {
|
|
131
|
+
if (r.ownerTenant !== null &&
|
|
132
|
+
tenantKeys.has((0, org_names_js_1.nameKey)(r.ownerTenant)) &&
|
|
133
|
+
!declaredRoles.has((0, org_names_js_1.nameKey)(r.name))) {
|
|
134
|
+
add('role', `role:${r.name}`, 'not-in-file');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---- users, memberships, postings, credentials ----
|
|
138
|
+
const stateUser = new Map(state.users.map((u) => [(0, org_names_js_1.nameKey)(u.username), u]));
|
|
139
|
+
const emailHolder = new Map(state.emailHolders.map((h) => [(0, org_names_js_1.nameKey)(h.email), h.username]));
|
|
140
|
+
const declaredUsers = new Set();
|
|
141
|
+
const postingKey = (p) => [(0, org_names_js_1.nameKey)(p.role), (0, org_names_js_1.nameKey)(p.department ?? ''), (0, org_names_js_1.nameKey)(p.branch ?? '')].join('|');
|
|
142
|
+
const postingPath = (user, company, p) => `user:${user}@${company} role:${p.role}${p.department ? ` dept:${p.department}` : ''}${p.branch ? ` branch:${p.branch}` : ''}`;
|
|
143
|
+
const planUser = (username, email, company, postings, isRoot) => {
|
|
144
|
+
declaredUsers.add((0, org_names_js_1.nameKey)(username));
|
|
145
|
+
const path = `user:${username}`;
|
|
146
|
+
const existing = stateUser.get((0, org_names_js_1.nameKey)(username));
|
|
147
|
+
if (!existing) {
|
|
148
|
+
const holder = emailHolder.get((0, org_names_js_1.nameKey)(email));
|
|
149
|
+
if (holder !== undefined && (0, org_names_js_1.nameKey)(holder) !== (0, org_names_js_1.nameKey)(username)) {
|
|
150
|
+
add('user', path, 'conflict', `email of "${username}" is already held by another user`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
add('user', path, 'create');
|
|
154
|
+
if (isRoot)
|
|
155
|
+
add('credential', path, 'create', 'password credential from the operator-supplied root password');
|
|
156
|
+
for (const p of postings)
|
|
157
|
+
add('posting', postingPath(username, company, p), 'create');
|
|
158
|
+
if (isRoot)
|
|
159
|
+
add('root_flag', path, 'create');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// R1-1: usernames are global, so a user the file names must provably be this file's user before
|
|
163
|
+
// anything is written for it. A non-root that holds a membership inside the file's tenants is; its
|
|
164
|
+
// differing email is only reported (OD-5). A root (which holds no membership) or a user with no
|
|
165
|
+
// membership in the file's tenants is tied to the file by its email alone, so a differing email
|
|
166
|
+
// means another account and the run refuses. A root holding any membership, or a non-root that
|
|
167
|
+
// belongs only to other tenants, is refused too.
|
|
168
|
+
const emailDiffers = (0, org_names_js_1.nameKey)(existing.email) !== (0, org_names_js_1.nameKey)(email);
|
|
169
|
+
const memberHere = existing.memberships.length > 0;
|
|
170
|
+
if (emailDiffers && (isRoot || !memberHere)) {
|
|
171
|
+
add('user', path, 'conflict', `user "${username}" already exists with a different email; the bootstrap never adopts or changes it`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const holdsAny = memberHere || existing.membershipsOutside;
|
|
175
|
+
if (isRoot && holdsAny) {
|
|
176
|
+
add('user', path, 'conflict', `user "${username}" already exists and holds memberships; a root holds none, so it cannot be made a root`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (!isRoot && existing.memberships.length === 0 && existing.membershipsOutside) {
|
|
180
|
+
add('user', path, 'conflict', `user "${username}" already exists as a member of another tenant only; the bootstrap never adopts it`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!isRoot && !holdsAny) {
|
|
184
|
+
// N3: an account with no membership anywhere cannot be read by the anchored people principal
|
|
185
|
+
// (the user read wall), so adding it would fail mid-apply. Report it and plan nothing for it.
|
|
186
|
+
add('user', path, 'drift', 'user exists with no membership; add it from the admin console, the bootstrap plans nothing for it');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
add('user', path, emailDiffers ? 'drift' : 'unchanged', emailDiffers ? 'email differs from the file; not overwritten' : undefined);
|
|
190
|
+
// A root holds no membership: root is barred from the data plane, and its home company only
|
|
191
|
+
// anchors the credential (`ic_tenant_id`). Planning one would be denied and never converge.
|
|
192
|
+
const declaredKey = (0, org_names_js_1.pathKey)(company);
|
|
193
|
+
const membership = existing.memberships.find((m) => (0, org_names_js_1.pathKey)(m.companyPath) === declaredKey);
|
|
194
|
+
const mpath = `${path}@${company}`;
|
|
195
|
+
if (isRoot) {
|
|
196
|
+
// nothing to plan for memberships or postings
|
|
197
|
+
}
|
|
198
|
+
else if (!membership) {
|
|
199
|
+
add('membership', mpath, 'create');
|
|
200
|
+
for (const p of postings)
|
|
201
|
+
add('posting', postingPath(username, company, p), 'create');
|
|
202
|
+
}
|
|
203
|
+
else if (!membership.active) {
|
|
204
|
+
add('membership', mpath, 'drift', 'inactive membership; the bootstrap never activates it');
|
|
205
|
+
const have = new Set(membership.postings.map(postingKey));
|
|
206
|
+
for (const p of postings) {
|
|
207
|
+
add('posting', postingPath(username, company, p), have.has(postingKey(p)) ? 'unchanged' : 'drift', have.has(postingKey(p)) ? undefined : 'inactive membership; posting not added');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
add('membership', mpath, 'unchanged');
|
|
212
|
+
const have = new Set(membership.postings.map(postingKey));
|
|
213
|
+
for (const p of postings) {
|
|
214
|
+
add('posting', postingPath(username, company, p), have.has(postingKey(p)) ? 'unchanged' : 'create');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (isRoot) {
|
|
218
|
+
if (existing.credential === 'none') {
|
|
219
|
+
add('credential', path, 'create', 'password credential from the operator-supplied root password');
|
|
220
|
+
}
|
|
221
|
+
else if (existing.credential === 'locked') {
|
|
222
|
+
add('credential', path, 'drift', 'credential is locked, not a password; the bootstrap does not replace it. Reissue a set-password link');
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
add('credential', path, 'unchanged');
|
|
226
|
+
}
|
|
227
|
+
add('root_flag', path, existing.isPlatformSuperuser ? 'unchanged' : 'create');
|
|
228
|
+
}
|
|
229
|
+
else if (existing.credential === 'none' && existing.membershipsOutside) {
|
|
230
|
+
// N1: the account also reaches another tenant; this file's operator must not get its link.
|
|
231
|
+
add('credential', path, 'drift', 'no credential and the user also belongs to another tenant; none created (no link issued)');
|
|
232
|
+
}
|
|
233
|
+
else if (existing.credential === 'none' && emailDiffers) {
|
|
234
|
+
// The login hint would be the file's email, which the bootstrap does not adopt (R1-6).
|
|
235
|
+
add('credential', path, 'drift', 'no credential and the email differs from the file; none created (set the email first)');
|
|
236
|
+
}
|
|
237
|
+
else if (existing.credential === 'none') {
|
|
238
|
+
add('credential', path, 'create', 'locked credential; a set-password link is issued on apply');
|
|
239
|
+
}
|
|
240
|
+
// Memberships and postings in the file's companies that the file does not declare.
|
|
241
|
+
const declaredPostings = new Set(postings.map(postingKey));
|
|
242
|
+
for (const m of existing.memberships) {
|
|
243
|
+
if (!inFileTenant(m.companyPath))
|
|
244
|
+
continue;
|
|
245
|
+
if ((0, org_names_js_1.pathKey)(m.companyPath) === declaredKey) {
|
|
246
|
+
for (const p of m.postings) {
|
|
247
|
+
if (!declaredPostings.has(postingKey(p))) {
|
|
248
|
+
add('posting', postingPath(username, company, p), 'not-in-file');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
add('membership', `${path}@${m.companyPath}`, 'not-in-file');
|
|
254
|
+
for (const p of m.postings)
|
|
255
|
+
add('posting', postingPath(username, m.companyPath, p), 'not-in-file');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
const tenantPath = (tenantName, rel) => rel === undefined ? tenantName : `${tenantName}/${rel}`;
|
|
260
|
+
for (const r of file.roots)
|
|
261
|
+
planUser(r.username, r.email, r.homeCompany, [], true);
|
|
262
|
+
for (const c of companies) {
|
|
263
|
+
for (const u of c.node.users ?? []) {
|
|
264
|
+
const company = u.company === undefined ? c.path : tenantPath(c.tenant, u.company);
|
|
265
|
+
planUser(u.username, u.email, company, u.postings ?? [], false);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
for (const u of state.users) {
|
|
269
|
+
if (declaredUsers.has((0, org_names_js_1.nameKey)(u.username)))
|
|
270
|
+
continue;
|
|
271
|
+
const inside = u.memberships.filter((m) => inFileTenant(m.companyPath));
|
|
272
|
+
if (inside.length === 0)
|
|
273
|
+
continue;
|
|
274
|
+
const path = `user:${u.username}`;
|
|
275
|
+
add('user', path, 'not-in-file');
|
|
276
|
+
for (const m of inside) {
|
|
277
|
+
for (const p of m.postings) {
|
|
278
|
+
add('posting', postingPath(u.username, m.companyPath, p), 'not-in-file');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// R10 order: stable by kind.
|
|
283
|
+
const entries = out
|
|
284
|
+
.map((e, i) => ({ e, i }))
|
|
285
|
+
.sort((a, b) => exports.PLAN_KIND_ORDER.indexOf(a.e.kind) - exports.PLAN_KIND_ORDER.indexOf(b.e.kind) || a.i - b.i)
|
|
286
|
+
.map((x) => x.e);
|
|
287
|
+
return { entries, conflicts: entries.filter((e) => e.status === 'conflict') };
|
|
288
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gate-7 operation manifest of `shield:org-bootstrap` (G-009 R6, AC-7; Shield #9).
|
|
3
|
+
*
|
|
4
|
+
* Each bootstrap step runs as its own operation-bound principal (`operationOf(actor)`, the engine's
|
|
5
|
+
* WeakMap binding) and is capped by the entry below, root-flagged or not: gate 7 denies any type or
|
|
6
|
+
* action outside the step's sets. Sets are explicit, never a wildcard. Core's `DelegatedAuthority` is
|
|
7
|
+
* a type SET times an action SET (not pairs), so a step can also do a listed action on a sibling listed
|
|
8
|
+
* type (e.g. `postings` can create a `company_membership` in its own anchored company).
|
|
9
|
+
*/
|
|
10
|
+
import { ORG_BOOTSTRAP_PRINCIPAL_ID, type OperationManifestEntry } from '@astram/guard';
|
|
11
|
+
export declare const ORG_BOOTSTRAP_OPERATION = "shield:org-bootstrap";
|
|
12
|
+
/**
|
|
13
|
+
* The reserved id every step principal carries. It has NO credential and must NEVER be placed in
|
|
14
|
+
* `SHIELD_ROOT_IDS`: no login can mint this principal, so its authority exists only through the
|
|
15
|
+
* runner's operation binding.
|
|
16
|
+
*/
|
|
17
|
+
export { ORG_BOOTSTRAP_PRINCIPAL_ID };
|
|
18
|
+
export declare const ORG_BOOTSTRAP_MANIFEST: readonly OperationManifestEntry[];
|
|
19
|
+
//# sourceMappingURL=org-bootstrap.manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-bootstrap.manifest.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap.manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,0BAA0B,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAExF,eAAO,MAAM,uBAAuB,yBAAyB,CAAC;AAE9D;;;;GAIG;AACH,OAAO,EAAE,0BAA0B,EAAE,CAAC;AActC,eAAO,MAAM,sBAAsB,EAAE,SAAS,sBAAsB,EAQnE,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ORG_BOOTSTRAP_MANIFEST = exports.ORG_BOOTSTRAP_PRINCIPAL_ID = exports.ORG_BOOTSTRAP_OPERATION = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* The gate-7 operation manifest of `shield:org-bootstrap` (G-009 R6, AC-7; Shield #9).
|
|
6
|
+
*
|
|
7
|
+
* Each bootstrap step runs as its own operation-bound principal (`operationOf(actor)`, the engine's
|
|
8
|
+
* WeakMap binding) and is capped by the entry below, root-flagged or not: gate 7 denies any type or
|
|
9
|
+
* action outside the step's sets. Sets are explicit, never a wildcard. Core's `DelegatedAuthority` is
|
|
10
|
+
* a type SET times an action SET (not pairs), so a step can also do a listed action on a sibling listed
|
|
11
|
+
* type (e.g. `postings` can create a `company_membership` in its own anchored company).
|
|
12
|
+
*/
|
|
13
|
+
const guard_1 = require("@astram/guard");
|
|
14
|
+
Object.defineProperty(exports, "ORG_BOOTSTRAP_PRINCIPAL_ID", { enumerable: true, get: function () { return guard_1.ORG_BOOTSTRAP_PRINCIPAL_ID; } });
|
|
15
|
+
exports.ORG_BOOTSTRAP_OPERATION = 'shield:org-bootstrap';
|
|
16
|
+
const step = (stepKey, objectTypeKeys, allowedActions) => ({
|
|
17
|
+
operationKey: exports.ORG_BOOTSTRAP_OPERATION,
|
|
18
|
+
stepKey,
|
|
19
|
+
authority: { objectTypeKeys, allowedActions },
|
|
20
|
+
});
|
|
21
|
+
const CREATE_READ = ['create', 'read'];
|
|
22
|
+
exports.ORG_BOOTSTRAP_MANIFEST = [
|
|
23
|
+
step('tenants', ['company'], CREATE_READ),
|
|
24
|
+
step('companies', ['company'], CREATE_READ),
|
|
25
|
+
step('org-units', ['department', 'branch', 'company_department', 'company_branch'], CREATE_READ),
|
|
26
|
+
step('roles', ['role'], CREATE_READ),
|
|
27
|
+
step('roles-claim', ['role'], ['read']),
|
|
28
|
+
step('users', ['user', 'company_membership'], CREATE_READ),
|
|
29
|
+
step('postings', ['company_membership', 'membership_posting'], CREATE_READ),
|
|
30
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DI token for the ONE shared authz pool `IdentityFeatureModule.forRoot()` builds (G-009 T10). The org
|
|
3
|
+
* bootstrap runner reads it with `app.get(IDENTITY_AUTHZ_POOL, { strict: false })` for its named lock,
|
|
4
|
+
* its read-only state reader and the few raw writes the spec allows (role-owner claim, root flag).
|
|
5
|
+
* It is the existing pool, not a new one; it is not exported from the module or the package entry.
|
|
6
|
+
*/
|
|
7
|
+
export declare const IDENTITY_AUTHZ_POOL: unique symbol;
|
|
8
|
+
//# sourceMappingURL=org-bootstrap.tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-bootstrap.tokens.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap.tokens.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,eAAgC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IDENTITY_AUTHZ_POOL = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* DI token for the ONE shared authz pool `IdentityFeatureModule.forRoot()` builds (G-009 T10). The org
|
|
6
|
+
* bootstrap runner reads it with `app.get(IDENTITY_AUTHZ_POOL, { strict: false })` for its named lock,
|
|
7
|
+
* its read-only state reader and the few raw writes the spec allows (role-owner claim, root flag).
|
|
8
|
+
* It is the existing pool, not a new one; it is not exported from the module or the package entry.
|
|
9
|
+
*/
|
|
10
|
+
exports.IDENTITY_AUTHZ_POOL = Symbol('IDENTITY_AUTHZ_POOL');
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-009 T9: the one name-comparison helper for the org bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* The identity name columns are `utf8mb4_unicode_ci`, so their unique indexes treat names that differ
|
|
5
|
+
* only by case or accents as equal. `nameKey` mirrors that closely (lower-case, NFD, combining marks
|
|
6
|
+
* stripped, trailing pad ignored) and is applied to BOTH sides of every comparison, so a clash is a
|
|
7
|
+
* plan-time `conflict`, not an insert-time failure.
|
|
8
|
+
*/
|
|
9
|
+
export declare function nameKey(value: string): string;
|
|
10
|
+
/** The key of a `/`-joined company path, segment by segment. */
|
|
11
|
+
export declare function pathKey(path: string): string;
|
|
12
|
+
//# sourceMappingURL=org-names.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-names.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-names.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C;AAED,gEAAgE;AAChE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK5C"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.nameKey = nameKey;
|
|
4
|
+
exports.pathKey = pathKey;
|
|
5
|
+
/**
|
|
6
|
+
* G-009 T9: the one name-comparison helper for the org bootstrap.
|
|
7
|
+
*
|
|
8
|
+
* The identity name columns are `utf8mb4_unicode_ci`, so their unique indexes treat names that differ
|
|
9
|
+
* only by case or accents as equal. `nameKey` mirrors that closely (lower-case, NFD, combining marks
|
|
10
|
+
* stripped, trailing pad ignored) and is applied to BOTH sides of every comparison, so a clash is a
|
|
11
|
+
* plan-time `conflict`, not an insert-time failure.
|
|
12
|
+
*/
|
|
13
|
+
function nameKey(value) {
|
|
14
|
+
return value
|
|
15
|
+
.normalize('NFD')
|
|
16
|
+
.replace(/\p{M}+/gu, '')
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/ +$/u, '');
|
|
19
|
+
}
|
|
20
|
+
/** The key of a `/`-joined company path, segment by segment. */
|
|
21
|
+
function pathKey(path) {
|
|
22
|
+
return path
|
|
23
|
+
.split('/')
|
|
24
|
+
.map((seg) => nameKey(seg.trim()))
|
|
25
|
+
.join('/');
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { FrameworkSchemas } from '@astram/core';
|
|
2
|
+
import type { OrgBootstrapFile } from './org-bootstrap-file.js';
|
|
3
|
+
import type { OrgState } from './org-bootstrap-plan.js';
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
export interface OrgStateReaderOptions {
|
|
6
|
+
query: (sql: string, params?: unknown[]) => Promise<Row[]>;
|
|
7
|
+
schemas: Pick<FrameworkSchemas, 'app' | 'identity' | 'authz'>;
|
|
8
|
+
}
|
|
9
|
+
export declare class OrgStateReader {
|
|
10
|
+
private readonly opts;
|
|
11
|
+
constructor(opts: OrgStateReaderOptions);
|
|
12
|
+
read(file: OrgBootstrapFile): Promise<OrgState>;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=org-state.reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-state.reader.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-state.reader.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,KAAK,EAAuB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAErF,OAAO,KAAK,EAEV,QAAQ,EAMT,MAAM,yBAAyB,CAAC;AAEjC,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnC,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3D,OAAO,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,UAAU,GAAG,OAAO,CAAC,CAAC;CAC/D;AA6CD,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,qBAAqB;IAElD,IAAI,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC;CA6NtD"}
|