@astram/identity 0.16.0 → 0.17.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/branch.object.d.ts +9 -1
- package/dist/branch.object.d.ts.map +1 -1
- package/dist/branch.object.js +14 -1
- package/dist/company-branch.object.d.ts +5 -4
- package/dist/company-branch.object.d.ts.map +1 -1
- package/dist/company-branch.object.js +6 -4
- package/dist/company-department.object.d.ts +6 -6
- package/dist/company-department.object.d.ts.map +1 -1
- package/dist/company-department.object.js +7 -6
- package/dist/department.object.d.ts +9 -1
- package/dist/department.object.d.ts.map +1 -1
- package/dist/department.object.js +14 -1
- package/dist/features/access/access-admin.access.d.ts.map +1 -1
- package/dist/features/access/access-admin.access.js +8 -1
- package/dist/features/access/index.d.ts +1 -0
- package/dist/features/access/index.d.ts.map +1 -1
- package/dist/features/access/index.js +10 -2
- package/dist/features/access/org-structure.access.d.ts +3 -0
- package/dist/features/access/org-structure.access.d.ts.map +1 -0
- package/dist/features/access/org-structure.access.js +41 -0
- package/dist/features/access/user-management.access.d.ts.map +1 -1
- package/dist/features/access/user-management.access.js +5 -0
- package/dist/features/audit/audit-read.service.d.ts +32 -1
- package/dist/features/audit/audit-read.service.d.ts.map +1 -1
- package/dist/features/audit/audit-read.service.js +67 -14
- package/dist/features/audit/auth-audit-sink.d.ts +19 -0
- package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
- package/dist/features/audit/auth-audit-sink.js +24 -0
- package/dist/features/audit/directory-label-resolver.d.ts +10 -1
- package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
- package/dist/features/audit/directory-label-resolver.js +28 -1
- package/dist/features/authz/grant-admin.controller.d.ts +4 -4
- package/dist/features/authz/grant-admin.controller.js +4 -4
- package/dist/features/authz/grant-admin.service.d.ts +9 -8
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +20 -24
- package/dist/features/authz/grant-ceiling.d.ts +35 -16
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +44 -29
- package/dist/features/authz/role-binding.d.ts +25 -24
- package/dist/features/authz/role-binding.d.ts.map +1 -1
- package/dist/features/authz/role-binding.js +31 -44
- package/dist/features/identity-feature.module.d.ts +0 -11
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +90 -1
- package/dist/features/membership/membership.controller.d.ts.map +1 -1
- package/dist/features/membership/membership.controller.js +20 -6
- package/dist/features/membership/membership.service.d.ts +16 -10
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +61 -21
- package/dist/features/membership/posting.service.d.ts +25 -6
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +63 -16
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-state.reader.js +56 -49
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
- package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.controller.js +105 -17
- package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +327 -51
- package/dist/features/org-structure/org-unit-name.d.ts +31 -0
- package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
- package/dist/features/org-structure/org-unit-name.js +50 -0
- package/dist/features/role/role.controller.d.ts +28 -13
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +54 -14
- package/dist/features/role/role.service.d.ts +76 -21
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +204 -27
- package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
- package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy-version-bump.js +65 -0
- package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
- package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.controller.js +121 -0
- package/dist/features/security-policy/security-policy.service.d.ts +94 -0
- package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.service.js +429 -0
- package/dist/features/service-account/service-account.service.d.ts +13 -3
- package/dist/features/service-account/service-account.service.d.ts.map +1 -1
- package/dist/features/service-account/service-account.service.js +48 -10
- package/dist/features/service-account/service-account.store.d.ts +11 -1
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +20 -1
- package/dist/features/user/signin-admin.tokens.d.ts +11 -0
- package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
- package/dist/features/user/signin-admin.tokens.js +13 -0
- package/dist/features/user/user-mfa-status.d.ts +38 -0
- package/dist/features/user/user-mfa-status.d.ts.map +1 -0
- package/dist/features/user/user-mfa-status.js +47 -0
- package/dist/features/user/user-signin-admin.service.d.ts +48 -0
- package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
- package/dist/features/user/user-signin-admin.service.js +232 -0
- package/dist/features/user/user.controller.d.ts +29 -2
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +71 -3
- package/dist/features/user/user.service.d.ts +40 -14
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +143 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -0
- package/dist/role.object.d.ts +1 -1
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +6 -15
- package/dist/security-policy.object.d.ts +43 -0
- package/dist/security-policy.object.d.ts.map +1 -0
- package/dist/security-policy.object.js +107 -0
- package/dist/user.object.d.ts +6 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +13 -1
- package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
- package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
- package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
- package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
- package/package.json +4 -4
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* A posting IS a role assignment, so before ANY write the actor (the request principal) must:
|
|
12
12
|
* - reach the membership (a walled `records.get` → null ⇒ 404; the engine's ADR-0034 create
|
|
13
13
|
* wall re-checks the posting's membership company inside the transaction);
|
|
14
|
-
* - for every ADDED role: the role is
|
|
15
|
-
*
|
|
16
|
-
* and the role's keys are all held by the actor
|
|
17
|
-
* assignment). Removals need no ceiling:
|
|
14
|
+
* - for every ADDED role: the role is owned by the membership's company (G-013,
|
|
15
|
+
* `GrantCeiling.assertRoleAssignable` — another company's or an ownerless role is a 400, for
|
|
16
|
+
* every actor, root included), and the role's keys are all held by the actor
|
|
17
|
+
* (`assertCanAssignRoles`, ADR-0029 applied to assignment). Removals need no ceiling:
|
|
18
|
+
* self-demotion stays allowed.
|
|
18
19
|
* - for every ADDED posting: its department and branch belong to the membership's company
|
|
19
20
|
* (`assertOrgUnitsInCompany`, Phase 9 #13b) — else 400.
|
|
20
21
|
*
|
|
@@ -150,7 +151,7 @@ let PostingService = class PostingService {
|
|
|
150
151
|
seenAdded.add(k);
|
|
151
152
|
return true;
|
|
152
153
|
});
|
|
153
|
-
// 2) Authority for every ADDED role, before any write:
|
|
154
|
+
// 2) Authority for every ADDED role, before any write: owner rule (G-013), then the ceiling.
|
|
154
155
|
const addedRoles = await this.assertMayAdd(actor, tenantUuid, added, undefined);
|
|
155
156
|
// Removing a PEER's postings strips their authority: the actor must dominate the member
|
|
156
157
|
// (security review). Removing your OWN postings (self-demotion) stays allowed.
|
|
@@ -163,11 +164,12 @@ let PostingService = class PostingService {
|
|
|
163
164
|
// 3) Write atomically: creates first, then deletes (KI #16). Only whitelisted fields are sent,
|
|
164
165
|
// and `membershipUuid` always comes from the path (review F4).
|
|
165
166
|
await this.records.transaction(async (tx) => {
|
|
166
|
-
// Review F2: serialise against a concurrent grant/
|
|
167
|
-
//
|
|
168
|
-
// re-run the
|
|
169
|
-
//
|
|
170
|
-
await this.lockAndRecheck(tx, actor, addedRoles, undefined);
|
|
167
|
+
// Review F2: serialise against a concurrent grant/delete of the same role. Lock each added
|
|
168
|
+
// role row on THIS transaction (grant administration and role delete lock the same row),
|
|
169
|
+
// then re-run the owner rule (G-013): a role deleted, or made ownerless by its company's
|
|
170
|
+
// delete, while we waited is refused before the INSERT (rolled back).
|
|
171
|
+
await this.lockAndRecheck(tx, actor, tenantUuid, addedRoles, undefined);
|
|
172
|
+
await this.lockOrgUnits(tx, tenantUuid, added);
|
|
171
173
|
for (const d of added) {
|
|
172
174
|
await tx.create('membership_posting', { fields: { ...stripEmpty(d), membershipUuid } });
|
|
173
175
|
}
|
|
@@ -208,7 +210,8 @@ let PostingService = class PostingService {
|
|
|
208
210
|
return; // nothing missing: no write, no bump
|
|
209
211
|
const addedRoles = await this.assertMayAdd(actor, tenantUuid, added, ceilingOpts);
|
|
210
212
|
await this.records.transaction(async (tx) => {
|
|
211
|
-
await this.lockAndRecheck(tx, actor, addedRoles, ceilingOpts);
|
|
213
|
+
await this.lockAndRecheck(tx, actor, tenantUuid, addedRoles, ceilingOpts);
|
|
214
|
+
await this.lockOrgUnits(tx, tenantUuid, added);
|
|
212
215
|
for (const d of added) {
|
|
213
216
|
await tx.create('membership_posting', { fields: { ...stripEmpty(d), membershipUuid } });
|
|
214
217
|
}
|
|
@@ -232,11 +235,13 @@ let PostingService = class PostingService {
|
|
|
232
235
|
: null;
|
|
233
236
|
return { membership, tenantUuid };
|
|
234
237
|
}
|
|
235
|
-
/** Authority for ADDED postings (
|
|
238
|
+
/** Authority for ADDED postings (owner rule, ceiling, org units); returns the distinct roles. */
|
|
236
239
|
async assertMayAdd(actor, tenantUuid, added, opts) {
|
|
237
240
|
const addedRoles = [...new Set(added.map((d) => d.roleUuid))];
|
|
241
|
+
// G-013 (spec §5.D, R2): every added role is the membership company's own, for every actor
|
|
242
|
+
// (root and the bootstrap operation included) — 400 otherwise, before the ceiling.
|
|
238
243
|
for (const roleUuid of addedRoles) {
|
|
239
|
-
await this.ceiling.assertRoleAssignable(actor, roleUuid, opts);
|
|
244
|
+
await this.ceiling.assertRoleAssignable(actor, roleUuid, tenantUuid, opts);
|
|
240
245
|
}
|
|
241
246
|
await this.ceiling.assertCanAssignRoles(actor, addedRoles, opts);
|
|
242
247
|
// Phase 9 #13b (review F4): an added posting's department/branch must be the membership
|
|
@@ -248,13 +253,55 @@ let PostingService = class PostingService {
|
|
|
248
253
|
});
|
|
249
254
|
return addedRoles;
|
|
250
255
|
}
|
|
251
|
-
/**
|
|
252
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Lock each added role row on the transaction, then re-check the owner rule (review F2, G-013).
|
|
258
|
+
* A role changes owner only through its company's delete (owner cleared), which also removes the
|
|
259
|
+
* memberships of that company, so the re-check after the lock sees the committed outcome.
|
|
260
|
+
*/
|
|
261
|
+
async lockAndRecheck(tx, actor, tenantUuid, addedRoles, opts) {
|
|
253
262
|
for (const roleUuid of addedRoles) {
|
|
254
263
|
await tx.lockForUpdate('role', roleUuid);
|
|
255
|
-
await this.ceiling.assertRoleAssignable(actor, roleUuid, opts);
|
|
264
|
+
await this.ceiling.assertRoleAssignable(actor, roleUuid, tenantUuid, opts);
|
|
256
265
|
}
|
|
257
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* G-012 (spec §5.A, NAES review D): lock every ADDED posting's department and branch row on THIS
|
|
269
|
+
* transaction (departments then branches, each sorted and de-duplicated: one lock order), then
|
|
270
|
+
* re-check that each still belongs to the membership's company. A department/branch delete locks
|
|
271
|
+
* the same row and counts postings after its lock, so the two serialise: delete first ⇒ the unit
|
|
272
|
+
* (and its junction row, which cascades) is gone and this add is refused with 400; add first ⇒
|
|
273
|
+
* the delete counts this posting and answers 409. The FK is never reached, so no driver error.
|
|
274
|
+
*
|
|
275
|
+
* The re-check reuses `assertOrgUnitsInCompany` (fresh reads on the authz pool, after the lock is
|
|
276
|
+
* held, so they see the committed delete) rather than an engine read: the org bootstrap's
|
|
277
|
+
* `postings` step is gate-7 capped to membership types and may not read `department`/`branch`.
|
|
278
|
+
* The pre-transaction check already passed, so a failure here can only mean the row is gone.
|
|
279
|
+
*/
|
|
280
|
+
async lockOrgUnits(tx, tenantUuid, added) {
|
|
281
|
+
const ids = (pick) => [
|
|
282
|
+
...new Set(added.map(pick).filter((v) => typeof v === 'string' && v.length > 0)),
|
|
283
|
+
].sort();
|
|
284
|
+
const departmentUuids = ids((d) => d.departmentUuid);
|
|
285
|
+
const branchUuids = ids((d) => d.branchUuid);
|
|
286
|
+
for (const id of departmentUuids)
|
|
287
|
+
await tx.lockForUpdate('department', id);
|
|
288
|
+
for (const id of branchUuids)
|
|
289
|
+
await tx.lockForUpdate('branch', id);
|
|
290
|
+
const recheck = async (units, message) => {
|
|
291
|
+
if (units.departmentUuids.length === 0 && units.branchUuids.length === 0)
|
|
292
|
+
return;
|
|
293
|
+
try {
|
|
294
|
+
await this.ceiling.assertOrgUnitsInCompany(tenantUuid, units);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
if (err instanceof identity_errors_js_1.IdentityBadRequestError)
|
|
298
|
+
throw new identity_errors_js_1.IdentityBadRequestError(message);
|
|
299
|
+
throw err;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
await recheck({ departmentUuids, branchUuids: [] }, 'That department no longer exists.');
|
|
303
|
+
await recheck({ departmentUuids: [], branchUuids }, 'That branch no longer exists.');
|
|
304
|
+
}
|
|
258
305
|
async bumpTenant(tenantUuid) {
|
|
259
306
|
// ADR-0011 STALENESS FIX: a posting add/remove changes the effective roles (→ coarse
|
|
260
307
|
// grants) of the membership's user in that company, so bump the tenant's permission
|
|
@@ -3,6 +3,8 @@ export interface OrgBootstrapRoot {
|
|
|
3
3
|
email: string;
|
|
4
4
|
/** Tenant path: `A` or `A/B` (tenant name, then child names). */
|
|
5
5
|
homeCompany: string;
|
|
6
|
+
/** Optional sign-in name (auth capabilities R1.7), normalized: lower-case `[a-z0-9._-]{3,64}`. */
|
|
7
|
+
loginName?: string;
|
|
6
8
|
}
|
|
7
9
|
export interface OrgBootstrapRole {
|
|
8
10
|
name: string;
|
|
@@ -19,6 +21,8 @@ export interface OrgBootstrapUser {
|
|
|
19
21
|
/** Path of child names relative to the tenant (`A/B` means tenant > A > B). */
|
|
20
22
|
company?: string;
|
|
21
23
|
postings?: OrgBootstrapPosting[];
|
|
24
|
+
/** Optional sign-in name (auth capabilities R1.7), normalized: lower-case `[a-z0-9._-]{3,64}`. */
|
|
25
|
+
loginName?: string;
|
|
22
26
|
}
|
|
23
27
|
export interface OrgBootstrapCompany {
|
|
24
28
|
name: string;
|
|
@@ -44,10 +48,10 @@ export interface OrgBootstrapFileOptions {
|
|
|
44
48
|
/** The closed key sets, exported so the schema parity test compares against the parser's own. */
|
|
45
49
|
export declare const ORG_FILE_KEYS: {
|
|
46
50
|
readonly file: readonly ["version", "roots", "tenants"];
|
|
47
|
-
readonly root: readonly ["username", "email", "homeCompany"];
|
|
51
|
+
readonly root: readonly ["username", "email", "homeCompany", "loginName"];
|
|
48
52
|
readonly company: readonly ["name", "departments", "branches", "roles", "users", "children"];
|
|
49
53
|
readonly role: readonly ["name", "permissions"];
|
|
50
|
-
readonly user: readonly ["username", "email", "company", "postings"];
|
|
54
|
+
readonly user: readonly ["username", "email", "company", "postings", "loginName"];
|
|
51
55
|
readonly posting: readonly ["role", "department", "branch"];
|
|
52
56
|
};
|
|
53
57
|
export declare class OrgBootstrapFileError extends Error {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-bootstrap-file.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-file.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"org-bootstrap-file.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-file.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAC;IACpB,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,kGAAkG;IAClG,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC3B,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,oFAAoF;IACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACpC;AAED,iGAAiG;AACjG,eAAO,MAAM,aAAa;;;;;;;CAOhB,CAAC;AAEX,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;gBAC7B,MAAM,EAAE,qBAAqB,EAAE;CAO5C;AAoCD,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,uBAAuB,GAC/B,gBAAgB,CA4SlB"}
|
|
@@ -8,17 +8,20 @@ exports.parseOrgBootstrapFile = parseOrgBootstrapFile;
|
|
|
8
8
|
* Hand-written (the repo carries no validator library) with closed key sets at every level;
|
|
9
9
|
* `org-bootstrap.schema.json` documents the same shape and a parity test keeps them in step.
|
|
10
10
|
* No I/O. Every problem is collected as `{ path, message }`; messages never echo input values, so a
|
|
11
|
-
* stray `password` field is reported by name only.
|
|
11
|
+
* stray `password` field is reported by name only. The one exception is the G-013 posting-role
|
|
12
|
+
* message (spec §5.E), which names the role and the company path the operator's own file declares.
|
|
12
13
|
*/
|
|
14
|
+
const core_1 = require("@astram/core");
|
|
13
15
|
const guard_1 = require("@astram/guard");
|
|
16
|
+
const org_unit_name_js_1 = require("../org-structure/org-unit-name.js");
|
|
14
17
|
const org_names_js_1 = require("./org-names.js");
|
|
15
18
|
/** The closed key sets, exported so the schema parity test compares against the parser's own. */
|
|
16
19
|
exports.ORG_FILE_KEYS = {
|
|
17
20
|
file: ['version', 'roots', 'tenants'],
|
|
18
|
-
root: ['username', 'email', 'homeCompany'],
|
|
21
|
+
root: ['username', 'email', 'homeCompany', 'loginName'],
|
|
19
22
|
company: ['name', 'departments', 'branches', 'roles', 'users', 'children'],
|
|
20
23
|
role: ['name', 'permissions'],
|
|
21
|
-
user: ['username', 'email', 'company', 'postings'],
|
|
24
|
+
user: ['username', 'email', 'company', 'postings', 'loginName'],
|
|
22
25
|
posting: ['role', 'department', 'branch'],
|
|
23
26
|
};
|
|
24
27
|
class OrgBootstrapFileError extends Error {
|
|
@@ -32,6 +35,22 @@ class OrgBootstrapFileError extends Error {
|
|
|
32
35
|
exports.OrgBootstrapFileError = OrgBootstrapFileError;
|
|
33
36
|
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
34
37
|
const join = (base, key) => (base === '' ? key : `${base}.${key}`);
|
|
38
|
+
/**
|
|
39
|
+
* G-013: the comparison key of a role name — the shared name rule (`normaliseOrgUnitName`: NFC,
|
|
40
|
+
* trim, collapse whitespace), then `nameKey` (case, accents), as the planner and the per-company
|
|
41
|
+
* unique index compare. A name the rule refuses falls back to `nameKey` (the planner reports it).
|
|
42
|
+
*/
|
|
43
|
+
function roleKey(name) {
|
|
44
|
+
const n = (0, org_unit_name_js_1.normaliseOrgUnitName)(name);
|
|
45
|
+
return (0, org_names_js_1.nameKey)(n.ok ? n.value : name);
|
|
46
|
+
}
|
|
47
|
+
/** A company's `/`-joined name path from its tenant (`Acme/Sales`), as the file names it. */
|
|
48
|
+
function namePath(node) {
|
|
49
|
+
const names = [];
|
|
50
|
+
for (let n = node; n; n = n.parent)
|
|
51
|
+
names.unshift(n.name);
|
|
52
|
+
return names.join('/');
|
|
53
|
+
}
|
|
35
54
|
function parseOrgBootstrapFile(input, options) {
|
|
36
55
|
const issues = [];
|
|
37
56
|
const add = (path, message) => issues.push({ path, message });
|
|
@@ -85,7 +104,6 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
85
104
|
if (f.version !== 1)
|
|
86
105
|
add('version', 'must be the number 1');
|
|
87
106
|
// ---- pass 1: companies (structure, roles, name uniqueness), building the lookup tree ----
|
|
88
|
-
const globalRoles = new Set();
|
|
89
107
|
const tenantNodes = new Map();
|
|
90
108
|
const nodes = []; // depth-first, parent before children
|
|
91
109
|
const walkCompany = (raw, path, parent, siblings) => {
|
|
@@ -97,6 +115,9 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
97
115
|
add(`${path}.name`, 'must not contain "/"');
|
|
98
116
|
const departments = nameList(c.departments, `${path}.departments`);
|
|
99
117
|
const branches = nameList(c.branches, `${path}.branches`);
|
|
118
|
+
// G-013 (spec §5.E): roles are owned per company, so a role name is unique within its company
|
|
119
|
+
// only (compared as stored: the name rule, then the collation); another company may declare
|
|
120
|
+
// the same name and gets its own role.
|
|
100
121
|
const roles = [];
|
|
101
122
|
const roleNames = new Set();
|
|
102
123
|
(optionalArray(c.roles, `${path}.roles`) ?? []).forEach((rr, i) => {
|
|
@@ -121,10 +142,10 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
121
142
|
});
|
|
122
143
|
}
|
|
123
144
|
if (rname !== undefined) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
roleNames.add(
|
|
145
|
+
const key = roleKey(rname);
|
|
146
|
+
if (roleNames.has(key))
|
|
147
|
+
add(`${rp}.name`, 'duplicate role name within this company');
|
|
148
|
+
roleNames.add(key);
|
|
128
149
|
roles.push({ name: rname, permissions: perms });
|
|
129
150
|
}
|
|
130
151
|
});
|
|
@@ -204,6 +225,21 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
204
225
|
emails.add(k);
|
|
205
226
|
}
|
|
206
227
|
};
|
|
228
|
+
// Auth capabilities R1.3 / R1.10: optional login name, normalized, validated, unique in the file.
|
|
229
|
+
const loginNames = new Set();
|
|
230
|
+
const loginNameOf = (v, path) => {
|
|
231
|
+
if (v === undefined)
|
|
232
|
+
return undefined;
|
|
233
|
+
const name = typeof v === 'string' ? (0, core_1.normalizeLoginName)(v) : '';
|
|
234
|
+
if (!(0, core_1.isValidLoginName)(name)) {
|
|
235
|
+
add(path, 'must be a valid sign-in name: 3-64 characters, letters a-z, digits, dot, underscore or hyphen');
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
if (loginNames.has(name))
|
|
239
|
+
add(path, 'duplicate login name (case-insensitive)');
|
|
240
|
+
loginNames.add(name);
|
|
241
|
+
return name;
|
|
242
|
+
};
|
|
207
243
|
const emailOf = (v, path) => {
|
|
208
244
|
if (typeof v !== 'string' || !(0, guard_1.isEmailShaped)(v.trim())) {
|
|
209
245
|
add(path, 'must be a valid email address');
|
|
@@ -224,8 +260,9 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
224
260
|
add(`${p}.homeCompany`, 'does not name a company declared in this file');
|
|
225
261
|
}
|
|
226
262
|
claim(username, email, p);
|
|
263
|
+
const loginName = loginNameOf(r.loginName, `${p}.loginName`);
|
|
227
264
|
if (username !== undefined && email !== undefined && home !== undefined) {
|
|
228
|
-
roots.push({ username, email, homeCompany: home });
|
|
265
|
+
roots.push({ username, email, homeCompany: home, ...(loginName ? { loginName } : {}) });
|
|
229
266
|
}
|
|
230
267
|
});
|
|
231
268
|
const inScope = (start, pick, value) => {
|
|
@@ -246,6 +283,7 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
246
283
|
const username = text(u.username, `${p}.username`);
|
|
247
284
|
const email = emailOf(u.email, `${p}.email`);
|
|
248
285
|
claim(username, email, p);
|
|
286
|
+
const loginName = loginNameOf(u.loginName, `${p}.loginName`);
|
|
249
287
|
let home = node;
|
|
250
288
|
let companyRaw;
|
|
251
289
|
if (u.company !== undefined) {
|
|
@@ -269,8 +307,11 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
269
307
|
if (!po)
|
|
270
308
|
return;
|
|
271
309
|
const role = text(po.role, `${q}.role`);
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
// G-013 (spec §5.E, R2): a role is assigned only in the company that owns it, so a
|
|
311
|
+
// posting's role must be declared on the posting's OWN company (an ancestor's role is
|
|
312
|
+
// that ancestor's, and the server would refuse it with 400).
|
|
313
|
+
if (role !== undefined && !home.roles.has(roleKey(role))) {
|
|
314
|
+
add(`${q}.role`, `Role "${role}" is not declared on company "${namePath(home)}".`);
|
|
274
315
|
}
|
|
275
316
|
const posting = { role: role ?? '' };
|
|
276
317
|
for (const [key, pick] of [
|
|
@@ -299,6 +340,8 @@ function parseOrgBootstrapFile(input, options) {
|
|
|
299
340
|
user.company = companyRaw;
|
|
300
341
|
if (u.postings !== undefined)
|
|
301
342
|
user.postings = postings;
|
|
343
|
+
if (loginName !== undefined)
|
|
344
|
+
user.loginName = loginName;
|
|
302
345
|
users.push(user);
|
|
303
346
|
}
|
|
304
347
|
});
|
|
@@ -7,8 +7,17 @@
|
|
|
7
7
|
* subtrees are looked at, so onboarding tenant B never reports tenant A.
|
|
8
8
|
*/
|
|
9
9
|
import type { OrgBootstrapFile } from './org-bootstrap-file.js';
|
|
10
|
+
/**
|
|
11
|
+
* G-012 (spec §5.A/§5.D, NAES review C): the comparison key of a department/branch name — the
|
|
12
|
+
* shared name rule (`normaliseOrgUnitName`: NFC, trim, collapse whitespace) then `nameKey` (case,
|
|
13
|
+
* accents). "Quality Assurance" in a file therefore matches "Quality Assurance" in the database.
|
|
14
|
+
* A name the rule refuses falls back to `nameKey` alone (the planner reports it as a conflict).
|
|
15
|
+
*/
|
|
16
|
+
export declare function orgUnitKey(name: string): string;
|
|
17
|
+
/** The drift note of a stored department/branch the file does not list (spec §5.D). */
|
|
18
|
+
export declare const orgUnitDriftNote: (name: string) => string;
|
|
10
19
|
export type CredentialState = 'none' | 'locked' | 'usable';
|
|
11
|
-
export declare const PLAN_KIND_ORDER: readonly ["company", "department", "branch", "company_department", "company_branch", "role", "
|
|
20
|
+
export declare const PLAN_KIND_ORDER: readonly ["company", "department", "branch", "company_department", "company_branch", "role", "grant", "user", "membership", "posting", "credential", "root_flag"];
|
|
12
21
|
export type PlanKind = (typeof PLAN_KIND_ORDER)[number];
|
|
13
22
|
export type PlanStatus = 'create' | 'unchanged' | 'drift' | 'not-in-file' | 'conflict';
|
|
14
23
|
export interface PlanEntry {
|
|
@@ -36,6 +45,8 @@ export interface OrgStateMembership {
|
|
|
36
45
|
export interface OrgStateUser {
|
|
37
46
|
username: string;
|
|
38
47
|
email: string;
|
|
48
|
+
/** The stored login name (`u_login_name`); absent or null = none. */
|
|
49
|
+
loginName?: string | null;
|
|
39
50
|
credential: CredentialState;
|
|
40
51
|
isPlatformSuperuser: boolean;
|
|
41
52
|
/** Memberships INSIDE the file's tenants' subtrees only. */
|
|
@@ -43,37 +54,45 @@ export interface OrgStateUser {
|
|
|
43
54
|
/** Holds a membership OUTSIDE the file's tenants' subtrees (another tenant's user). */
|
|
44
55
|
membershipsOutside: boolean;
|
|
45
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* A role OWNED by a company of the file's tenants' subtrees (G-013: every role belongs to one
|
|
59
|
+
* company; a name is unique within that company only).
|
|
60
|
+
*/
|
|
46
61
|
export interface OrgStateRole {
|
|
62
|
+
/** The owner company's path (`Tenant/Child/...`, names as stored). */
|
|
63
|
+
companyPath: string;
|
|
64
|
+
/** The role name as stored. */
|
|
47
65
|
name: string;
|
|
48
|
-
/** Name of the owner's top-level tenant; null = no owner. */
|
|
49
|
-
ownerTenant: string | null;
|
|
50
|
-
/** Held by a posting outside the file's tenants' subtrees. */
|
|
51
|
-
heldOutside: boolean;
|
|
52
66
|
permissions: string[];
|
|
53
67
|
}
|
|
54
68
|
export interface OrgStateCompany {
|
|
55
69
|
path: string;
|
|
70
|
+
/** The company's OWN departments (G-012: owner column), names as stored. */
|
|
56
71
|
departments: string[];
|
|
72
|
+
/** The company's OWN branches, names as stored. */
|
|
57
73
|
branches: string[];
|
|
58
74
|
}
|
|
59
75
|
export interface OrgState {
|
|
60
76
|
companies: OrgStateCompany[];
|
|
61
|
-
|
|
62
|
-
branches: string[];
|
|
77
|
+
/** The roles owned by the companies of the file's tenants' subtrees (G-013). */
|
|
63
78
|
roles: OrgStateRole[];
|
|
64
79
|
users: OrgStateUser[];
|
|
65
80
|
emailHolders: {
|
|
66
81
|
email: string;
|
|
67
82
|
username: string;
|
|
68
83
|
}[];
|
|
84
|
+
/** Holders of the file's login names (read only when the file declares any). */
|
|
85
|
+
loginNameHolders?: {
|
|
86
|
+
loginName: string;
|
|
87
|
+
username: string;
|
|
88
|
+
}[];
|
|
69
89
|
/**
|
|
70
90
|
* File company paths whose name matches more than one stored company (several top-level companies
|
|
71
91
|
* of the tenant's name, or several children of one parent with the child's name): never guessed.
|
|
72
92
|
*/
|
|
73
93
|
ambiguousCompanies: string[];
|
|
74
94
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
export declare function planOrgBootstrap(file: OrgBootstrapFile, state: OrgState, options: PlanOptions): OrgPlan;
|
|
95
|
+
/** The plan path of a company's own role (G-013, spec §5.E): `<company path>#role:<name>`. */
|
|
96
|
+
export declare const rolePath: (companyPath: string, name: string) => string;
|
|
97
|
+
export declare function planOrgBootstrap(file: OrgBootstrapFile, state: OrgState): OrgPlan;
|
|
79
98
|
//# sourceMappingURL=org-bootstrap-plan.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-bootstrap-plan.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"org-bootstrap-plan.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-plan.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAEV,gBAAgB,EAEjB,MAAM,yBAAyB,CAAC;AAKjC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MACkC,CAAC;AAEnF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3D,eAAO,MAAM,eAAe,mKAalB,CAAC;AAEX,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AACxD,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,aAAa,GAAG,UAAU,CAAC;AAEvF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,SAAS,EAAE,CAAC;IACrB,4FAA4F;IAC5F,SAAS,EAAE,SAAS,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,eAAe,CAAC;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,4DAA4D;IAC5D,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,uFAAuF;IACvF,kBAAkB,EAAE,OAAO,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,mDAAmD;IACnD,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,gFAAgF;IAChF,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,YAAY,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACpD,gFAAgF;IAChF,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7D;;;OAGG;IACH,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B;AAED,8FAA8F;AAC9F,eAAO,MAAM,QAAQ,GAAI,aAAa,MAAM,EAAE,MAAM,MAAM,KAAG,MAC9B,CAAC;AAQhC,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAobjF"}
|