@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.
- package/README.md +15 -0
- package/dist/bin/sa-keys-report.d.ts +3 -0
- package/dist/bin/sa-keys-report.d.ts.map +1 -0
- package/dist/bin/sa-keys-report.js +18 -0
- package/dist/features/authz/grant-admin.service.d.ts +14 -5
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +27 -10
- package/dist/features/authz/grant-ceiling.d.ts +19 -6
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +34 -7
- package/dist/features/authz/record-authorization.d.ts +15 -32
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +15 -69
- package/dist/features/company/company-hierarchy.service.d.ts +6 -1
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +19 -7
- package/dist/features/company/company.controller.d.ts +1 -1
- package/dist/features/company/company.controller.d.ts.map +1 -1
- package/dist/features/company/company.controller.js +9 -5
- package/dist/features/company/company.service.d.ts +9 -2
- package/dist/features/company/company.service.d.ts.map +1 -1
- package/dist/features/company/company.service.js +17 -2
- package/dist/features/identity-feature.module.d.ts +15 -8
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +75 -28
- package/dist/features/membership/posting.service.d.ts +2 -0
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +9 -0
- package/dist/features/role/role.service.d.ts +14 -7
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +15 -8
- package/dist/features/service-account/lock-service-accounts.d.ts +29 -0
- package/dist/features/service-account/lock-service-accounts.d.ts.map +1 -0
- package/dist/features/service-account/lock-service-accounts.js +35 -0
- package/dist/features/service-account/no-expiry-key-report.d.ts +39 -0
- package/dist/features/service-account/no-expiry-key-report.d.ts.map +1 -0
- package/dist/features/service-account/no-expiry-key-report.js +91 -0
- package/dist/features/user/user.service.d.ts +4 -4
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +9 -6
- package/dist/index.d.ts +1 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -12
- package/dist/internal.d.ts +3 -2
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -2
- 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 @@
|
|
|
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
|
|
137
|
-
* has already run the binding rule and user-holder dominance
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
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;
|
|
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
|
|
281
|
-
* has already run the binding rule and user-holder dominance
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
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 =
|
|
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
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
*
|
|
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":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9D,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;
|
|
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
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
-
*
|
|
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(
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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`
|
|
24
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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`
|
|
45
|
-
*
|
|
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
|
|
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 (
|
|
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;
|
|
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 (
|
|
45
|
+
/** The actor's own root node (see {@link ownRootOf}). */
|
|
37
46
|
ownRoot(actor) {
|
|
38
|
-
return actor
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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,
|
|
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"}
|