@astram/identity 0.6.0 → 0.7.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.
Files changed (47) hide show
  1. package/README.md +15 -0
  2. package/dist/bin/sa-keys-report.d.ts +3 -0
  3. package/dist/bin/sa-keys-report.d.ts.map +1 -0
  4. package/dist/bin/sa-keys-report.js +18 -0
  5. package/dist/features/authz/grant-admin.service.d.ts +14 -5
  6. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  7. package/dist/features/authz/grant-admin.service.js +27 -10
  8. package/dist/features/authz/grant-ceiling.d.ts +19 -6
  9. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  10. package/dist/features/authz/grant-ceiling.js +34 -7
  11. package/dist/features/authz/record-authorization.d.ts +15 -32
  12. package/dist/features/authz/record-authorization.d.ts.map +1 -1
  13. package/dist/features/authz/record-authorization.js +15 -69
  14. package/dist/features/company/company-hierarchy.service.d.ts +6 -1
  15. package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
  16. package/dist/features/company/company-hierarchy.service.js +19 -7
  17. package/dist/features/company/company.controller.d.ts +1 -1
  18. package/dist/features/company/company.controller.d.ts.map +1 -1
  19. package/dist/features/company/company.controller.js +9 -5
  20. package/dist/features/company/company.service.d.ts +9 -2
  21. package/dist/features/company/company.service.d.ts.map +1 -1
  22. package/dist/features/company/company.service.js +17 -2
  23. package/dist/features/identity-feature.module.d.ts +15 -8
  24. package/dist/features/identity-feature.module.d.ts.map +1 -1
  25. package/dist/features/identity-feature.module.js +75 -28
  26. package/dist/features/membership/posting.service.d.ts +2 -0
  27. package/dist/features/membership/posting.service.d.ts.map +1 -1
  28. package/dist/features/membership/posting.service.js +9 -0
  29. package/dist/features/role/role.service.d.ts +14 -7
  30. package/dist/features/role/role.service.d.ts.map +1 -1
  31. package/dist/features/role/role.service.js +15 -8
  32. package/dist/features/service-account/lock-service-accounts.d.ts +29 -0
  33. package/dist/features/service-account/lock-service-accounts.d.ts.map +1 -0
  34. package/dist/features/service-account/lock-service-accounts.js +35 -0
  35. package/dist/features/service-account/no-expiry-key-report.d.ts +39 -0
  36. package/dist/features/service-account/no-expiry-key-report.d.ts.map +1 -0
  37. package/dist/features/service-account/no-expiry-key-report.js +91 -0
  38. package/dist/features/user/user.service.d.ts +4 -4
  39. package/dist/features/user/user.service.d.ts.map +1 -1
  40. package/dist/features/user/user.service.js +9 -6
  41. package/dist/index.d.ts +1 -11
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +8 -12
  44. package/dist/internal.d.ts +3 -2
  45. package/dist/internal.d.ts.map +1 -1
  46. package/dist/internal.js +3 -2
  47. package/package.json +7 -4
package/README.md CHANGED
@@ -14,3 +14,18 @@ destination for the identity model currently declared in consuming applications
14
14
  once relocated, apps declare only role-maps, extra columns, and session
15
15
  extensions, while the canonical identity objects, migrations, and resolver live
16
16
  here in the framework.
17
+
18
+ ## `astram-sa-keys-report`
19
+
20
+ Service-account keys issued before an expiry became mandatory never expire. This read-only command
21
+ lists each agent's current, unrevoked key with no expiry (agent label, company name, status, key
22
+ version, issue date; never a secret, hash or id), so you can rotate it with an expiry:
23
+
24
+ ```sh
25
+ APP_DB_HOST=… APP_DB_PORT=… APP_DB_USER=… APP_DB_PASSWORD=… APP_DB_NAME=shield_app \
26
+ npx astram-sa-keys-report # exit 0
27
+ npx astram-sa-keys-report --fail-on-found # exit 1 if any key is listed (CI)
28
+ ```
29
+
30
+ It uses the same `APP_DB_*` variables as the identity module's authz pool. Exit code 2 means it
31
+ could not run (bad option, no database).
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=sa-keys-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sa-keys-report.d.ts","sourceRoot":"","sources":["../../src/bin/sa-keys-report.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ /**
5
+ * `astram-sa-keys-report` — list service-account keys that never expire (security hardening
6
+ * Phase 8 #13 / Phase 9 #8). Read-only; connects with the identity authz pool's `APP_DB_*` env.
7
+ * `--fail-on-found` exits 1 when any key is listed. Logic: `no-expiry-key-report.ts`.
8
+ */
9
+ const record_authorization_js_1 = require("../features/authz/record-authorization.js");
10
+ const no_expiry_key_report_js_1 = require("../features/service-account/no-expiry-key-report.js");
11
+ void (0, no_expiry_key_report_js_1.runNonExpiringKeyReport)({
12
+ argv: process.argv.slice(2),
13
+ openDb: record_authorization_js_1.createIdentityAuthzPool,
14
+ out: (text) => process.stdout.write(text),
15
+ err: (text) => process.stderr.write(text),
16
+ }).then((code) => {
17
+ process.exitCode = code;
18
+ });
@@ -133,13 +133,22 @@ export declare class GrantAdminService {
133
133
  * 4. audit as `role.permission_revoked` (`roleDeleted: true`), commit, publish the versions.
134
134
  * Returns the bumped tenants.
135
135
  *
136
- * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection and
137
- * has already run the binding rule and user-holder dominance; this method therefore does NOT
138
- * lock the role row (that would wait on the caller). It commits BEFORE the caller deletes the
139
- * role, so a failure in between leaves a role with no grants and no agents: fewer permissions,
140
- * never live grants for a deleted role (fail closed).
136
+ * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection,
137
+ * has already run the binding rule and user-holder dominance, and has already run the engine's
138
+ * delete (gate included, Phase 9 #5) on that still-open transaction; this method therefore does
139
+ * NOT lock the role row (that would wait on the caller) and reads only committed state. It
140
+ * commits BEFORE the caller does, so a failure in between leaves a role with no grants and no
141
+ * agents: fewer permissions, never live grants for a deleted role (fail closed).
141
142
  */
142
143
  purgeRole(roleUuid: string, actor: Principal): Promise<string[]>;
144
+ /**
145
+ * The service accounts holding `roleUuid`, read with a row lock on the transaction `conn` (a
146
+ * concurrent edit of one of them waits until this transaction ends). Phase 9 #11: only those
147
+ * rows are locked, never the whole table (`lockServiceAccountsWhere`). An agent that gains the
148
+ * role after the candidate read is not seen; the outcome equals "this change, then that edit"
149
+ * (a role delete leaves that agent an inert uuid: the role and its grants are gone).
150
+ */
151
+ private lockAccountsHoldingRole;
143
152
  /** All active bundles + their resolved atomic keys, for the Access UI. */
144
153
  listBundles(): Promise<{
145
154
  bundles: BundleView[];
@@ -1 +1 @@
1
- {"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,cAAc,CAAC;AAE1D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,YAAY,EAAa,MAAM,oBAAoB,CAAC;AAsC7D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;6EAC6E;AAC7E,qBAAa,WAAY,SAAQ,KAAK;gBACxB,GAAG,EAAE,MAAM;CAIxB;AAED,KAAK,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElG,sFAAsF;AACtF,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,qBAAa,iBAAiB;IAa1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IAhBzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAE5C;;;;;;;;OAQG;gBAEgB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,uBAAuB,EAChD,WAAW,CAAC,EAAE,iBAAiB,EACd,OAAO,CAAC,EAAE,YAAY,YAAA,EACtB,KAAK,CAAC,EAAE,SAAS,YAAA;IAKpC,+EAA+E;IACzE,WAAW,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAmC5E;;;;OAIG;IACG,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA0BhC,0FAA0F;YAC5E,cAAc;IAM5B;;;;;;OAMG;YACW,qBAAqB;IAUnC;;;;;OAKG;YACW,WAAW;IAgDzB;;;;;;OAMG;YACW,gBAAgB;IA0D9B;;;;;OAKG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7E;;;;;;OAMG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAU5E;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IA+DtE,0EAA0E;IACpE,WAAW,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,CAAA;KAAE,CAAC;IASvD;;;;;;OAMG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IA2BzF;;;;;;OAMG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBxF,uFAAuF;YACzE,wBAAwB;IAWtC;;;;;;;;;;;;OAYG;IACG,iBAAiB,CACrB,MAAM,EACF;QACE,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,SAAS,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;KAC5B,GACD,SAAS,EACb,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC;QAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAgIrF"}
1
+ {"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.
7
7
  const grant_ceiling_js_1 = require("./grant-ceiling.js");
8
8
  const role_binding_js_1 = require("./role-binding.js");
9
9
  const identity_errors_js_1 = require("../shared/identity-errors.js");
10
+ const lock_service_accounts_js_1 = require("../service-account/lock-service-accounts.js");
10
11
  const AUTHZ = 'shield_authz';
11
12
  const APP = 'shield_app';
12
13
  /** The grant-admin actions that REMOVE authority from a role's holders (Phase 8 #2). */
@@ -153,8 +154,13 @@ class GrantAdminService {
153
154
  grant_ceiling_js_1.GrantCeiling.checkAdministrable(await (0, role_binding_js_1.readRoleBinding)(conn, roleUuid, (0, grant_ceiling_js_1.subtreeOf)(actor)));
154
155
  // Phase 8 #2: a revocation strips authority from every holder of the role, so the actor
155
156
  // must dominate each of them — checked under the role lock (no holder can appear unseen).
157
+ // Phase 9 #9: that includes the service accounts holding the role, judged as a role
158
+ // delete judges them (each account's tenant in the actor's subtree, the role held by the
159
+ // actor), over the accounts read under a row lock.
156
160
  if (REVOKE_ACTIONS.has(action)) {
157
161
  await this.ceiling.assertDominatesRoleHolders(actor, roleUuid);
162
+ const accounts = await this.lockAccountsHoldingRole(conn, roleUuid);
163
+ await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id), 'revoke');
158
164
  }
159
165
  }
160
166
  for (const key of keys)
@@ -277,22 +283,20 @@ class GrantAdminService {
277
283
  * 4. audit as `role.permission_revoked` (`roleDeleted: true`), commit, publish the versions.
278
284
  * Returns the bumped tenants.
279
285
  *
280
- * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection and
281
- * has already run the binding rule and user-holder dominance; this method therefore does NOT
282
- * lock the role row (that would wait on the caller). It commits BEFORE the caller deletes the
283
- * role, so a failure in between leaves a role with no grants and no agents: fewer permissions,
284
- * never live grants for a deleted role (fail closed).
286
+ * The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection,
287
+ * has already run the binding rule and user-holder dominance, and has already run the engine's
288
+ * delete (gate included, Phase 9 #5) on that still-open transaction; this method therefore does
289
+ * NOT lock the role row (that would wait on the caller) and reads only committed state. It
290
+ * commits BEFORE the caller does, so a failure in between leaves a role with no grants and no
291
+ * agents: fewer permissions, never live grants for a deleted role (fail closed).
285
292
  */
286
293
  async purgeRole(roleUuid, actor) {
287
294
  const conn = await this.pool.getConnection();
288
295
  try {
289
296
  await conn.beginTransaction();
290
- const accounts = (await conn.query(`SELECT sa_id, sa_tenant_id, sa_role_ids, sa_updated_at_ms
291
- FROM \`${APP}\`.\`service_account\`
292
- WHERE JSON_CONTAINS(sa_role_ids, JSON_QUOTE(?))
293
- FOR UPDATE`, [roleUuid]));
297
+ const accounts = await this.lockAccountsHoldingRole(conn, roleUuid);
294
298
  if (this.ceiling && actor.isRoot !== true) {
295
- await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id));
299
+ await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id), 'delete');
296
300
  }
297
301
  const keys = (await conn.query(`SELECT rp_perm_key FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid])).map((r) => r.rp_perm_key);
298
302
  const bumped = await this.bumper.bumpForRole(roleUuid, conn);
@@ -325,6 +329,19 @@ class GrantAdminService {
325
329
  await conn.release();
326
330
  }
327
331
  }
332
+ /**
333
+ * The service accounts holding `roleUuid`, read with a row lock on the transaction `conn` (a
334
+ * concurrent edit of one of them waits until this transaction ends). Phase 9 #11: only those
335
+ * rows are locked, never the whole table (`lockServiceAccountsWhere`). An agent that gains the
336
+ * role after the candidate read is not seen; the outcome equals "this change, then that edit"
337
+ * (a role delete leaves that agent an inert uuid: the role and its grants are gone).
338
+ */
339
+ lockAccountsHoldingRole(conn, roleUuid) {
340
+ return (0, lock_service_accounts_js_1.lockServiceAccountsWhere)(conn, 'sa_id, sa_tenant_id, sa_role_ids, sa_updated_at_ms', {
341
+ sql: 'JSON_CONTAINS(sa_role_ids, JSON_QUOTE(?))',
342
+ params: [roleUuid],
343
+ });
344
+ }
328
345
  // ── Access bundles ────────────────────────────────────────────────────────
329
346
  /** All active bundles + their resolved atomic keys, for the Access UI. */
330
347
  async listBundles() {
@@ -107,10 +107,9 @@ export declare class GrantCeiling {
107
107
  * {@link assertDominates} the actor would need to change any one of them directly. Holders are
108
108
  * the users with a posting of the role in any membership and any window (potential authority).
109
109
  * The actor itself is skipped: giving up your own authority stays allowed (self-demotion).
110
- * Service-account holders: a revoke is covered by the binding rule (`assertRoleAdministrable`:
111
- * no binding outside the actor's subtree) plus "only revoke what you hold"; a role DELETE also
112
- * strips the role from each account and checks {@link assertMayStripRoleFromServiceAccounts}.
113
- * Root bypasses. Store outage → 503.
110
+ * Service-account holders are checked separately by {@link assertMayStripRoleFromServiceAccounts}
111
+ * (a role delete strips the role from each account; a revoke narrows what each account holds —
112
+ * Phase 9 #9). Root bypasses. Store outage → 503.
114
113
  *
115
114
  * Callers serialise against a concurrent posting by locking the role row first (the posting
116
115
  * path locks the same row before it inserts), then call this — so no holder appears unseen.
@@ -123,9 +122,10 @@ export declare class GrantCeiling {
123
122
  * {@link ceilingOf}): each account's tenant is inside the actor's subtree, and the role is one
124
123
  * the actor holds itself — a non-root admin's edit preserves (cannot strip) a role it does not
125
124
  * hold. `accountTenantIds` are the holding accounts' tenants, read by the caller under a row lock.
126
- * Root bypasses. Refusals name no account. Store outage → 503.
125
+ * Phase 9 #9: a grant revoke (`operation: 'revoke'`) narrows every holding account the same way
126
+ * and is judged by the same rule. Root bypasses. Refusals name no account. Store outage → 503.
127
127
  */
128
- assertMayStripRoleFromServiceAccounts(actor: Principal, roleUuid: string, accountTenantIds: readonly string[]): Promise<void>;
128
+ assertMayStripRoleFromServiceAccounts(actor: Principal, roleUuid: string, accountTenantIds: readonly string[], operation?: 'delete' | 'revoke'): Promise<void>;
129
129
  /**
130
130
  * Assert the actor may RETIRE (disable + revoke the keys of) each of these service accounts
131
131
  * (Phase 8 #3: a company delete retires the deleted company's own agents). Judged by the SAME
@@ -140,6 +140,19 @@ export declare class GrantCeiling {
140
140
  readonly scopeSubtreeNodeIds: readonly string[];
141
141
  readonly roleIds: readonly string[];
142
142
  }>): Promise<void>;
143
+ /**
144
+ * Assert every department and branch a posting names belongs to the membership's company
145
+ * (Phase 9 #13b, review F4): a `company_departments` / `company_branches` junction row links it
146
+ * to `companyUuid`. Otherwise a posting could carry another tenant's department or branch.
147
+ * Applies to every actor, root included (it is the data's shape, not a privilege). A missing
148
+ * company (`null`) admits none. A foreign and a nonexistent unit give the same 400. Read through
149
+ * the shared authz pool, not the engine: the posting admin need not hold `company_department` /
150
+ * `company_branch` read. Store outage → 503.
151
+ */
152
+ assertOrgUnitsInCompany(companyUuid: string | null, units: {
153
+ readonly departmentUuids: readonly string[];
154
+ readonly branchUuids: readonly string[];
155
+ }): Promise<void>;
143
156
  /** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
144
157
  roleBinding(actor: Principal, roleUuid: string): Promise<RoleBinding>;
145
158
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
@@ -236,10 +236,9 @@ class GrantCeiling {
236
236
  * {@link assertDominates} the actor would need to change any one of them directly. Holders are
237
237
  * the users with a posting of the role in any membership and any window (potential authority).
238
238
  * The actor itself is skipped: giving up your own authority stays allowed (self-demotion).
239
- * Service-account holders: a revoke is covered by the binding rule (`assertRoleAdministrable`:
240
- * no binding outside the actor's subtree) plus "only revoke what you hold"; a role DELETE also
241
- * strips the role from each account and checks {@link assertMayStripRoleFromServiceAccounts}.
242
- * Root bypasses. Store outage → 503.
239
+ * Service-account holders are checked separately by {@link assertMayStripRoleFromServiceAccounts}
240
+ * (a role delete strips the role from each account; a revoke narrows what each account holds —
241
+ * Phase 9 #9). Root bypasses. Store outage → 503.
243
242
  *
244
243
  * Callers serialise against a concurrent posting by locking the role row first (the posting
245
244
  * path locks the same row before it inserts), then call this — so no holder appears unseen.
@@ -276,9 +275,10 @@ class GrantCeiling {
276
275
  * {@link ceilingOf}): each account's tenant is inside the actor's subtree, and the role is one
277
276
  * the actor holds itself — a non-root admin's edit preserves (cannot strip) a role it does not
278
277
  * hold. `accountTenantIds` are the holding accounts' tenants, read by the caller under a row lock.
279
- * Root bypasses. Refusals name no account. Store outage → 503.
278
+ * Phase 9 #9: a grant revoke (`operation: 'revoke'`) narrows every holding account the same way
279
+ * and is judged by the same rule. Root bypasses. Refusals name no account. Store outage → 503.
280
280
  */
281
- async assertMayStripRoleFromServiceAccounts(actor, roleUuid, accountTenantIds) {
281
+ async assertMayStripRoleFromServiceAccounts(actor, roleUuid, accountTenantIds, operation = 'delete') {
282
282
  if (actor.isRoot === true || accountTenantIds.length === 0)
283
283
  return;
284
284
  const admin = {
@@ -287,7 +287,9 @@ class GrantCeiling {
287
287
  scopeSubtreeNodeIds: subtreeOf(actor),
288
288
  isRoot: false,
289
289
  };
290
- const refuse = () => new identity_errors_js_1.IdentityForbiddenError('You cannot delete this role: it is assigned to an agent you cannot change.');
290
+ const refuse = () => new identity_errors_js_1.IdentityForbiddenError(operation === 'revoke'
291
+ ? 'You cannot change this role: it is assigned to an agent you cannot change.'
292
+ : 'You cannot delete this role: it is assigned to an agent you cannot change.');
291
293
  const nodes = (0, service_account_service_js_1.ceilingOf)(admin, []);
292
294
  if (accountTenantIds.some((t) => !(0, service_account_service_js_1.ceilingAllowsNode)(nodes, t)))
293
295
  throw refuse();
@@ -327,6 +329,31 @@ class GrantCeiling {
327
329
  throw refuse();
328
330
  }
329
331
  }
332
+ /**
333
+ * Assert every department and branch a posting names belongs to the membership's company
334
+ * (Phase 9 #13b, review F4): a `company_departments` / `company_branches` junction row links it
335
+ * to `companyUuid`. Otherwise a posting could carry another tenant's department or branch.
336
+ * Applies to every actor, root included (it is the data's shape, not a privilege). A missing
337
+ * company (`null`) admits none. A foreign and a nonexistent unit give the same 400. Read through
338
+ * the shared authz pool, not the engine: the posting admin need not hold `company_department` /
339
+ * `company_branch` read. Store outage → 503.
340
+ */
341
+ async assertOrgUnitsInCompany(companyUuid, units) {
342
+ const check = async (ids, table, companyCol, unitCol, message) => {
343
+ const wanted = [...new Set(ids)];
344
+ if (wanted.length === 0)
345
+ return;
346
+ if (companyUuid === null)
347
+ throw new identity_errors_js_1.IdentityBadRequestError(message);
348
+ const rows = await this.facts('company org units', async () => (await this.pool.query(`SELECT ${unitCol} AS unit FROM \`${APP}\`.\`${table}\`
349
+ WHERE ${companyCol} = ? AND ${unitCol} IN (${wanted.map(() => '?').join(', ')})`, [companyUuid, ...wanted])));
350
+ const linked = new Set(rows.map((r) => r.unit));
351
+ if (wanted.some((u) => !linked.has(u)))
352
+ throw new identity_errors_js_1.IdentityBadRequestError(message);
353
+ };
354
+ await check(units.departmentUuids, 'company_departments', 'cd_company_uuid', 'cd_department_uuid', 'The department is not one of this company’s departments.');
355
+ await check(units.branchUuids, 'company_branches', 'cb_company_uuid', 'cb_branch_uuid', 'The branch is not one of this company’s branches.');
356
+ }
330
357
  /** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
331
358
  async roleBinding(actor, roleUuid) {
332
359
  return this.facts('role binding', () => (0, role_binding_js_1.readRoleBinding)(this.pool, roleUuid, subtreeOf(actor)));
@@ -1,52 +1,35 @@
1
1
  /**
2
- * Assemble the record-tier authorization wiring for `@astram/identity`: the lean grant-read
3
- * pool factory + the `recordAuthorization` inputs the consumer passes to
4
- * `ShieldModule.forRoot({ recordAuthorization })`. Once wired, `decide()` becomes REAL (the
5
- * framework's four record-tier adapters are built internally over the supplied pool) instead
6
- * of the deny-everything stub.
2
+ * The record-tier authorization inputs for `@astram/identity`: the lean grant-read pool factory and
3
+ * the `RecordAuthorizationInputs` builder. `IdentityFeatureModule.forRoot({ extraObjects })` builds
4
+ * the inputs ONCE and uses them everywhere (security hardening Phase 9 #7):
5
+ * - provided app-wide under guard's `SHIELD_RECORD_AUTHORIZATION`, so `ShieldModule` builds the
6
+ * request path's decision authority from them (the framework's four record-tier adapters are
7
+ * built inside guard);
8
+ * - the module's own cross-tenant / policy authority, list scope and grant ceiling.
9
+ * One registry, one source of `extraObjects`: nothing can disagree.
7
10
  *
8
11
  * Modeled on astram3's `buildAuthorizationInputs` (`server/src/features/authz/authz.wiring.ts`),
9
- * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix
10
- * (`APP_DB_*`, per the Task-7 rename).
12
+ * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix (`APP_DB_*`).
11
13
  */
12
14
  import { type Pool } from 'mariadb';
13
15
  import { type RecordAuthorizationInputs } from '@astram/guard';
14
16
  import type { ObjectTypeInput } from '@astram/engine';
15
- import type { ModuleRegistry } from '@astram/core';
16
17
  /**
17
18
  * A lean `mariadb` pool for the authz grant-reads (built from `APP_DB_*` env via guard's
18
19
  * `loadDbConfig`). Kept small — these are pre-gate coarse-grant reads, not the main CRUD pool.
19
- * Opens the pool at CALL time (never at import) — the consumer (app lifecycle) owns closing it.
20
+ * Opens the pool at CALL time (never at import) — the caller owns closing it.
20
21
  */
21
22
  export declare function createIdentityAuthzPool(): Pool;
22
23
  /**
23
- * Build the `RecordAuthorizationInputs` the consumer passes to
24
- * `ShieldModule.forRoot({ recordAuthorization })`. The framework owns the four record-tier
25
- * adapters internally; `@astram/identity` supplies only the app-schema ports:
24
+ * Build the `RecordAuthorizationInputs` over `pool`. `@astram/identity` supplies only the
25
+ * app-schema ports:
26
26
  * - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
27
27
  * `company_membership`/`membership_posting`,
28
28
  * - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
29
- * - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
29
+ * - `registry` — bridged from `identityObjects` PLUS the consumer's `extraObjects` (their
30
+ * `<module>:<type>:<action>` keys project into a principal's grants at gate 5; without them a
31
+ * consumer type is `resource_not_registered`),
30
32
  * - `operationManifest` — empty (identity declares no trusted delegated operations).
31
- *
32
- * `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
33
- * `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
34
- * coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
35
- * and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
36
- * never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
37
- * the same object types the app registered with the engine closes that gap: the app's grants on
38
- * those keys now project, and the framework still owns the four record-tier adapters + scope fn.
39
- */
40
- export declare function buildIdentityRecordAuthorization(pool: Pool, extraObjects?: readonly ObjectTypeInput[]): RecordAuthorizationInputs;
41
- /**
42
- * The inputs builder WITHOUT the boot parity record — for the identity module's own internal
43
- * authorities (they are not the decision path the consumer hands `ShieldModule`).
44
33
  */
45
34
  export declare function buildRecordAuthorizationInputs(pool: Pool, extraObjects?: readonly ObjectTypeInput[]): RecordAuthorizationInputs;
46
- /** The boot error: the two registries' action-key sets differ (names the first differing keys). */
47
- export declare class IdentityRegistryMismatchError extends Error {
48
- constructor(onlyDecision: readonly string[], onlyCeiling: readonly string[]);
49
- }
50
- /** Called by `IdentityFeatureModule.forRoot` with the grant ceiling's registry (Phase 8 #4). */
51
- export declare function noteIdentityCeilingRegistry(registry: ModuleRegistry): void;
52
35
  //# sourceMappingURL=record-authorization.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAGL,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAKnD;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gCAAgC,CAC9C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAI3B;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAS3B;AAmBD,mGAAmG;AACnG,qBAAa,6BAA8B,SAAQ,KAAK;gBAC1C,YAAY,EAAE,SAAS,MAAM,EAAE,EAAE,WAAW,EAAE,SAAS,MAAM,EAAE;CAW5E;AAoBD,gGAAgG;AAChG,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI,CAE1E"}
1
+ {"version":3,"file":"record-authorization.d.ts","sourceRoot":"","sources":["../../../src/features/authz/record-authorization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAgB,EAAE,KAAK,IAAI,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAGL,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAKtD;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAU9C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,YAAY,GAAE,SAAS,eAAe,EAAO,GAC5C,yBAAyB,CAS3B"}
@@ -3,21 +3,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.IdentityRegistryMismatchError = void 0;
7
6
  exports.createIdentityAuthzPool = createIdentityAuthzPool;
8
- exports.buildIdentityRecordAuthorization = buildIdentityRecordAuthorization;
9
7
  exports.buildRecordAuthorizationInputs = buildRecordAuthorizationInputs;
10
- exports.noteIdentityCeilingRegistry = noteIdentityCeilingRegistry;
11
8
  /**
12
- * Assemble the record-tier authorization wiring for `@astram/identity`: the lean grant-read
13
- * pool factory + the `recordAuthorization` inputs the consumer passes to
14
- * `ShieldModule.forRoot({ recordAuthorization })`. Once wired, `decide()` becomes REAL (the
15
- * framework's four record-tier adapters are built internally over the supplied pool) instead
16
- * of the deny-everything stub.
9
+ * The record-tier authorization inputs for `@astram/identity`: the lean grant-read pool factory and
10
+ * the `RecordAuthorizationInputs` builder. `IdentityFeatureModule.forRoot({ extraObjects })` builds
11
+ * the inputs ONCE and uses them everywhere (security hardening Phase 9 #7):
12
+ * - provided app-wide under guard's `SHIELD_RECORD_AUTHORIZATION`, so `ShieldModule` builds the
13
+ * request path's decision authority from them (the framework's four record-tier adapters are
14
+ * built inside guard);
15
+ * - the module's own cross-tenant / policy authority, list scope and grant ceiling.
16
+ * One registry, one source of `extraObjects`: nothing can disagree.
17
17
  *
18
18
  * Modeled on astram3's `buildAuthorizationInputs` (`server/src/features/authz/authz.wiring.ts`),
19
- * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix
20
- * (`APP_DB_*`, per the Task-7 rename).
19
+ * adapted to `@astram/identity`'s own registrations (`identityObjects`) and env prefix (`APP_DB_*`).
21
20
  */
22
21
  const mariadb_1 = __importDefault(require("mariadb"));
23
22
  const guard_1 = require("@astram/guard");
@@ -27,7 +26,7 @@ const session_facts_reader_js_1 = require("./session-facts.reader.js");
27
26
  /**
28
27
  * A lean `mariadb` pool for the authz grant-reads (built from `APP_DB_*` env via guard's
29
28
  * `loadDbConfig`). Kept small — these are pre-gate coarse-grant reads, not the main CRUD pool.
30
- * Opens the pool at CALL time (never at import) — the consumer (app lifecycle) owns closing it.
29
+ * Opens the pool at CALL time (never at import) — the caller owns closing it.
31
30
  */
32
31
  function createIdentityAuthzPool() {
33
32
  const db = (0, guard_1.loadDbConfig)();
@@ -41,31 +40,15 @@ function createIdentityAuthzPool() {
41
40
  });
42
41
  }
43
42
  /**
44
- * Build the `RecordAuthorizationInputs` the consumer passes to
45
- * `ShieldModule.forRoot({ recordAuthorization })`. The framework owns the four record-tier
46
- * adapters internally; `@astram/identity` supplies only the app-schema ports:
43
+ * Build the `RecordAuthorizationInputs` over `pool`. `@astram/identity` supplies only the
44
+ * app-schema ports:
47
45
  * - `permissionFacts` — coarse grants over `shield_authz.role_permission` +
48
46
  * `company_membership`/`membership_posting`,
49
47
  * - `sessionFacts` — the ADR-0011 ALS-backed per-request reader,
50
- * - `registry` — bridged from `identityObjects` PLUS any consumer-supplied `extraObjects`,
48
+ * - `registry` — bridged from `identityObjects` PLUS the consumer's `extraObjects` (their
49
+ * `<module>:<type>:<action>` keys project into a principal's grants at gate 5; without them a
50
+ * consumer type is `resource_not_registered`),
51
51
  * - `operationManifest` — empty (identity declares no trusted delegated operations).
52
- *
53
- * `extraObjects` (optional): a CONSUMER app that registers its OWN gated object types via
54
- * `EngineModule.forFeature([...])` must ALSO pass those types here, so their actions enter the
55
- * coarse-grant permission-facts registry (gate 5). Without this, a custom type's routes boot
56
- * and list-scope correctly (gate 6) but their `<module>:<type>:<action>` permission keys are
57
- * never projected into a principal's grants — so gate 5 denies them (deny-by-default). Passing
58
- * the same object types the app registered with the engine closes that gap: the app's grants on
59
- * those keys now project, and the framework still owns the four record-tier adapters + scope fn.
60
- */
61
- function buildIdentityRecordAuthorization(pool, extraObjects = []) {
62
- const inputs = buildRecordAuthorizationInputs(pool, extraObjects);
63
- noteRegistry('decision', inputs.registry);
64
- return inputs;
65
- }
66
- /**
67
- * The inputs builder WITHOUT the boot parity record — for the identity module's own internal
68
- * authorities (they are not the decision path the consumer hands `ShieldModule`).
69
52
  */
70
53
  function buildRecordAuthorizationInputs(pool, extraObjects = []) {
71
54
  const registry = (0, guard_1.buildIdentityModuleRegistry)([...objects_js_1.identityObjects, ...extraObjects]);
@@ -77,40 +60,3 @@ function buildRecordAuthorizationInputs(pool, extraObjects = []) {
77
60
  operationManifest: [],
78
61
  };
79
62
  }
80
- const recordedKeySets = {
81
- decision: new Map(),
82
- ceiling: new Map(),
83
- };
84
- /** The boot error: the two registries' action-key sets differ (names the first differing keys). */
85
- class IdentityRegistryMismatchError extends Error {
86
- constructor(onlyDecision, onlyCeiling) {
87
- const sample = (keys) => keys.length === 0 ? 'none' : keys.slice(0, 10).join(', ') + (keys.length > 10 ? ', …' : '');
88
- super('IdentityFeatureModule.forRoot({ extraObjects }) and buildIdentityRecordAuthorization(pool, ' +
89
- 'extraObjects) were given different object types. Pass the SAME array to both. ' +
90
- `Only in the decision path: ${sample(onlyDecision)}. ` +
91
- `Only in IdentityFeatureModule: ${sample(onlyCeiling)}.`);
92
- this.name = 'IdentityRegistryMismatchError';
93
- }
94
- }
95
- exports.IdentityRegistryMismatchError = IdentityRegistryMismatchError;
96
- /** Record one side's registry and throw if it disagrees with any recorded registry of the other. */
97
- function noteRegistry(role, registry) {
98
- const keys = [...new Set(registry.listActions().map((a) => a.permissionKey))].sort();
99
- const other = recordedKeySets[role === 'decision' ? 'ceiling' : 'decision'];
100
- for (const otherKeys of other.values()) {
101
- const mine = new Set(keys);
102
- const theirs = new Set(otherKeys);
103
- const onlyMine = keys.filter((k) => !theirs.has(k));
104
- const onlyTheirs = otherKeys.filter((k) => !mine.has(k));
105
- if (onlyMine.length > 0 || onlyTheirs.length > 0) {
106
- throw role === 'decision'
107
- ? new IdentityRegistryMismatchError(onlyMine, onlyTheirs)
108
- : new IdentityRegistryMismatchError(onlyTheirs, onlyMine);
109
- }
110
- }
111
- recordedKeySets[role].set(keys.join('\n'), keys);
112
- }
113
- /** Called by `IdentityFeatureModule.forRoot` with the grant ceiling's registry (Phase 8 #4). */
114
- function noteIdentityCeilingRegistry(registry) {
115
- noteRegistry('ceiling', registry);
116
- }
@@ -37,6 +37,11 @@ export interface MemberDominance {
37
37
  readonly roleIds: readonly string[];
38
38
  }>): Promise<void>;
39
39
  }
40
+ /**
41
+ * The actor's own root node (the active node its subtree hangs from). A non-root actor may not
42
+ * move, delete (F6) or change the status of (Phase 9 #13c) this node.
43
+ */
44
+ export declare function ownRootOf(actor: Principal): string;
40
45
  export declare class CompanyHierarchyService {
41
46
  private readonly pool;
42
47
  private readonly closure;
@@ -47,7 +52,7 @@ export declare class CompanyHierarchyService {
47
52
  constructor(pool: HierarchyQueryable, closure: ScopeClosureService,
48
53
  /** Phase 8 #3: required — a company delete must dominate every member (no silent skip). */
49
54
  ceiling: MemberDominance, audit?: AuditPort | undefined, now?: () => number);
50
- /** The actor's own root node (the active node its subtree hangs from). */
55
+ /** The actor's own root node (see {@link ownRootOf}). */
51
56
  private ownRoot;
52
57
  /** Names of the given companies (unknown → absent). */
53
58
  private names;
@@ -1 +1 @@
1
- {"version":3,"file":"company-hierarchy.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company-hierarchy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAkB,MAAM,mCAAmC,CAAC;AAUxF,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,8BAA8B,CAC5B,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,aAAa,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;QAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;KACrC,CAAC,GACD,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAuCD,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,2FAA2F;IAC3F,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBALH,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,mBAAmB;IAC7C,2FAA2F;IAC1E,OAAO,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,SAAS,YAAA,EACjB,GAAG,GAAE,MAAM,MAAiB;IAG/C,0EAA0E;IAC1E,OAAO,CAAC,OAAO;IAIf,uDAAuD;YACzC,KAAK;YAUL,WAAW;IAqBzB,iGAAiG;IAC3F,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkD3F;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB/D;;;;;;;;;;;;;;;;;;;;;;;OAuBG;YACW,gBAAgB;IA6E9B,0FAA0F;YAC5E,kBAAkB;IAmDhC,wFAAwF;YAC1E,sBAAsB;IA+BpC,wFAAwF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
1
+ {"version":3,"file":"company-hierarchy.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company-hierarchy.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAkB,MAAM,mCAAmC,CAAC;AAWxF,6FAA6F;AAC7F,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACnE;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,8BAA8B,CAC5B,KAAK,EAAE,SAAS,EAChB,QAAQ,EAAE,aAAa,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;QAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;KACrC,CAAC,GACD,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;AAuCD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAElD;AAED,qBAAa,uBAAuB;IAEhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,2FAA2F;IAC3F,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;IACvB,OAAO,CAAC,QAAQ,CAAC,GAAG;gBALH,IAAI,EAAE,kBAAkB,EACxB,OAAO,EAAE,mBAAmB;IAC7C,2FAA2F;IAC1E,OAAO,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,SAAS,YAAA,EACjB,GAAG,GAAE,MAAM,MAAiB;IAG/C,yDAAyD;IACzD,OAAO,CAAC,OAAO;IAIf,uDAAuD;YACzC,KAAK;YAUL,WAAW;IAqBzB,iGAAiG;IAC3F,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAkD3F;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB/D;;;;;;;;;;;;;;;;;;;;;;;OAuBG;YACW,gBAAgB;IAiF9B,0FAA0F;YAC5E,kBAAkB;IAmDhC,wFAAwF;YAC1E,sBAAsB;IA+BpC,wFAAwF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGhD"}
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CompanyHierarchyService = void 0;
4
+ exports.ownRootOf = ownRootOf;
4
5
  const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
6
+ const lock_service_accounts_js_1 = require("../service-account/lock-service-accounts.js");
5
7
  const identity_errors_js_1 = require("../shared/identity-errors.js");
6
8
  const APP = 'shield_app';
7
9
  const AUTHZ = 'shield_authz';
@@ -18,6 +20,13 @@ function jsonIdArray(v) {
18
20
  }
19
21
  return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string') : [];
20
22
  }
23
+ /**
24
+ * The actor's own root node (the active node its subtree hangs from). A non-root actor may not
25
+ * move, delete (F6) or change the status of (Phase 9 #13c) this node.
26
+ */
27
+ function ownRootOf(actor) {
28
+ return actor.activeScopeNodeId ?? actor.tenantId;
29
+ }
21
30
  class CompanyHierarchyService {
22
31
  pool;
23
32
  closure;
@@ -33,9 +42,9 @@ class CompanyHierarchyService {
33
42
  this.audit = audit;
34
43
  this.now = now;
35
44
  }
36
- /** The actor's own root node (the active node its subtree hangs from). */
45
+ /** The actor's own root node (see {@link ownRootOf}). */
37
46
  ownRoot(actor) {
38
- return actor.activeScopeNodeId ?? actor.tenantId;
47
+ return ownRootOf(actor);
39
48
  }
40
49
  /** Names of the given companies (unknown → absent). */
41
50
  async names(q, ids) {
@@ -152,11 +161,14 @@ class CompanyHierarchyService {
152
161
  * Every change is audited on this transaction (names; ids only in structured attributes).
153
162
  */
154
163
  async retireDependents(conn, actor, nodeUuid, companyName) {
155
- const agents = (await conn.query(`SELECT sa_id, sa_tenant_id, sa_label, sa_scope_subtree, sa_role_ids, sa_updated_at_ms
156
- FROM \`${APP}\`.\`service_account\`
157
- WHERE sa_tenant_id = ? OR JSON_CONTAINS(sa_scope_subtree, JSON_QUOTE(?))
158
- ORDER BY sa_id
159
- FOR UPDATE`, [nodeUuid, nodeUuid]));
164
+ // Phase 9 #11: lock only the matching agents, never the whole table. An agent created in the
165
+ // company, or given it in scope, after the candidate read is not seen: the outcome equals
166
+ // "delete, then that edit" (the API-key path joins `companies`, so an agent of a deleted
167
+ // tenant cannot authenticate; a deleted node in a scope reaches nothing).
168
+ const agents = await (0, lock_service_accounts_js_1.lockServiceAccountsWhere)(conn, 'sa_id, sa_tenant_id, sa_label, sa_scope_subtree, sa_role_ids, sa_updated_at_ms', {
169
+ sql: 'sa_tenant_id = ? OR JSON_CONTAINS(sa_scope_subtree, JSON_QUOTE(?))',
170
+ params: [nodeUuid, nodeUuid],
171
+ });
160
172
  const own = agents.filter((a) => a.sa_tenant_id === nodeUuid);
161
173
  const others = agents.filter((a) => a.sa_tenant_id !== nodeUuid);
162
174
  // Root bypasses inside; a refusal (403, names no agent) rolls the whole delete back.
@@ -41,7 +41,7 @@ export declare class CompanyController {
41
41
  * Update a company's name/status — gated on `identity:company:update`. Any other key
42
42
  * (parentUuid, type, …) is ignored: use `PUT companies/:uuid/parent` to move a company.
43
43
  */
44
- updateCompany(uuid: string, body: {
44
+ updateCompany(req: AuthedRequest, uuid: string, body: {
45
45
  name?: string;
46
46
  status?: string;
47
47
  }): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"company.controller.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.controller.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,qBAEa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,cAAc;IAEtD;;;;OAIG;IAGG,aAAa,CAAU,KAAK,EAAE,YAAY;;;;;;IAMhD;;;OAGG;IAGG,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAK9E,2DAA2D;IAG3D,UAAU,CAAgB,IAAI,EAAE,MAAM;IAItC;;;;;OAKG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;IAa9C;;;OAGG;IAGG,aAAa,CACF,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMlD,iGAAiG;IAG3F,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAI3E"}
1
+ {"version":3,"file":"company.controller.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.controller.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAGvD,qBAEa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,cAAc;IAEtD;;;;OAIG;IAGG,aAAa,CAAU,KAAK,EAAE,YAAY;;;;;;IAMhD;;;OAGG;IAGG,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAK9E,2DAA2D;IAG3D,UAAU,CAAgB,IAAI,EAAE,MAAM;IAItC;;;;;OAKG;IAGG,WAAW,CACR,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;;;IAa9C;;;OAGG;IAGG,aAAa,CACV,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;;;IASlD,iGAAiG;IAG3F,aAAa,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAI3E"}