@astram/identity 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/features/authz/grant-admin.service.d.ts +8 -0
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +18 -0
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +2 -1
- package/dist/features/company/company-hierarchy.service.d.ts +7 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +17 -2
- package/dist/features/company/company.service.d.ts.map +1 -1
- package/dist/features/company/company.service.js +2 -1
- package/dist/features/shared/http.d.ts +6 -0
- package/dist/features/shared/http.d.ts.map +1 -1
- package/dist/features/shared/http.js +9 -0
- package/package.json +3 -3
|
@@ -148,6 +148,14 @@ export declare class GrantAdminService {
|
|
|
148
148
|
* role after the candidate read is not seen; the outcome equals "this change, then that edit"
|
|
149
149
|
* (a role delete leaves that agent an inert uuid: the role and its grants are gone).
|
|
150
150
|
*/
|
|
151
|
+
/**
|
|
152
|
+
* The role's uuid as stored (Phase 10 #1): `r_uuid` is `utf8mb4_unicode_ci`, so a route uuid in
|
|
153
|
+
* another letter case reaches the row, but `JSON_CONTAINS(sa_role_ids, …)` and the `sa_role_ids`
|
|
154
|
+
* rewrite are exact — an upper-cased uuid would find no holding agent (skipping their check) and
|
|
155
|
+
* write grants under a second spelling. Unknown → the input unchanged (later steps 404). A
|
|
156
|
+
* consistent read: `purgeRole` runs while the caller's delete of this row is uncommitted.
|
|
157
|
+
*/
|
|
158
|
+
private storedRoleUuid;
|
|
151
159
|
private lockAccountsHoldingRole;
|
|
152
160
|
/** All active bundles + their resolved atomic keys, for the Access UI. */
|
|
153
161
|
listBundles(): Promise<{
|
|
@@ -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;AAuC7D;;;;;;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;IA0DzB;;;;;;OAMG;YACW,gBAAgB;IA0D9B;;;;;OAKG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,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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"}
|
|
@@ -242,6 +242,7 @@ class GrantAdminService {
|
|
|
242
242
|
* (404/403) → ceiling (403) → write. Root bypasses the binding rule and the ceiling.
|
|
243
243
|
*/
|
|
244
244
|
async grant(roleUuid, key, grantor) {
|
|
245
|
+
roleUuid = await this.storedRoleUuid(roleUuid);
|
|
245
246
|
await this.assertValidKey(key); // throws BadKeyError (400) before touching the store
|
|
246
247
|
await this.assertMayChangeGrants(roleUuid, [key], grantor);
|
|
247
248
|
const actorUuid = grantor.id;
|
|
@@ -263,6 +264,7 @@ class GrantAdminService {
|
|
|
263
264
|
* role lock in `writeGrants`). Root bypasses.
|
|
264
265
|
*/
|
|
265
266
|
async revoke(roleUuid, key, actor) {
|
|
267
|
+
roleUuid = await this.storedRoleUuid(roleUuid);
|
|
266
268
|
await this.assertMayChangeGrants(roleUuid, [key], actor);
|
|
267
269
|
await this.writeGrants('role.permission_revoked', roleUuid, [key], actor, async (conn, k) => {
|
|
268
270
|
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, k]);
|
|
@@ -291,6 +293,7 @@ class GrantAdminService {
|
|
|
291
293
|
* agents: fewer permissions, never live grants for a deleted role (fail closed).
|
|
292
294
|
*/
|
|
293
295
|
async purgeRole(roleUuid, actor) {
|
|
296
|
+
roleUuid = await this.storedRoleUuid(roleUuid);
|
|
294
297
|
const conn = await this.pool.getConnection();
|
|
295
298
|
try {
|
|
296
299
|
await conn.beginTransaction();
|
|
@@ -336,6 +339,19 @@ class GrantAdminService {
|
|
|
336
339
|
* role after the candidate read is not seen; the outcome equals "this change, then that edit"
|
|
337
340
|
* (a role delete leaves that agent an inert uuid: the role and its grants are gone).
|
|
338
341
|
*/
|
|
342
|
+
/**
|
|
343
|
+
* The role's uuid as stored (Phase 10 #1): `r_uuid` is `utf8mb4_unicode_ci`, so a route uuid in
|
|
344
|
+
* another letter case reaches the row, but `JSON_CONTAINS(sa_role_ids, …)` and the `sa_role_ids`
|
|
345
|
+
* rewrite are exact — an upper-cased uuid would find no holding agent (skipping their check) and
|
|
346
|
+
* write grants under a second spelling. Unknown → the input unchanged (later steps 404). A
|
|
347
|
+
* consistent read: `purgeRole` runs while the caller's delete of this row is uncommitted.
|
|
348
|
+
*/
|
|
349
|
+
async storedRoleUuid(roleUuid) {
|
|
350
|
+
if (typeof roleUuid !== 'string' || roleUuid.length === 0)
|
|
351
|
+
return roleUuid;
|
|
352
|
+
const rows = (await this.pool.query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_uuid = ? LIMIT 1`, [roleUuid]));
|
|
353
|
+
return rows[0]?.r_uuid ?? roleUuid;
|
|
354
|
+
}
|
|
339
355
|
lockAccountsHoldingRole(conn, roleUuid) {
|
|
340
356
|
return (0, lock_service_accounts_js_1.lockServiceAccountsWhere)(conn, 'sa_id, sa_tenant_id, sa_role_ids, sa_updated_at_ms', {
|
|
341
357
|
sql: 'JSON_CONTAINS(sa_role_ids, JSON_QUOTE(?))',
|
|
@@ -361,6 +377,7 @@ class GrantAdminService {
|
|
|
361
377
|
* to; otherwise `GrantCeilingError` (403), checked before any write. Root bypasses.
|
|
362
378
|
*/
|
|
363
379
|
async grantBundle(roleUuid, bundleKey, grantor) {
|
|
380
|
+
roleUuid = await this.storedRoleUuid(roleUuid);
|
|
364
381
|
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
365
382
|
// Binding rule + ADR-0029 ceiling: a non-root grantor must hold EVERY atomic key the bundle
|
|
366
383
|
// expands to — checked before any write (no partial bundle escalation).
|
|
@@ -384,6 +401,7 @@ class GrantAdminService {
|
|
|
384
401
|
* #1: the binding rule + "only revoke what you hold" apply (root bypasses).
|
|
385
402
|
*/
|
|
386
403
|
async revokeBundle(roleUuid, bundleKey, actor) {
|
|
404
|
+
roleUuid = await this.storedRoleUuid(roleUuid);
|
|
387
405
|
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
388
406
|
await this.assertMayChangeGrants(roleUuid, [...keys], actor);
|
|
389
407
|
await this.writeGrants('role.bundle_revoked', roleUuid, keys, actor, async (conn, key) => {
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ exports.GrantCeiling = exports.GrantCeilingError = void 0;
|
|
|
4
4
|
exports.subtreeOf = subtreeOf;
|
|
5
5
|
exports.coversKey = coversKey;
|
|
6
6
|
const core_1 = require("@astram/core");
|
|
7
|
+
const http_js_1 = require("../shared/http.js");
|
|
7
8
|
const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.js");
|
|
8
9
|
const role_binding_js_1 = require("./role-binding.js");
|
|
9
10
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
@@ -192,7 +193,7 @@ class GrantCeiling {
|
|
|
192
193
|
async assertDominates(actor, targetUserId) {
|
|
193
194
|
if (actor.isRoot === true)
|
|
194
195
|
return;
|
|
195
|
-
if (this.rootIds.
|
|
196
|
+
if (this.rootIds.some((r) => (0, http_js_1.sameUuid)(r, targetUserId))) {
|
|
196
197
|
throw new identity_errors_js_1.IdentityForbiddenError('You cannot change this user: they are a platform administrator.');
|
|
197
198
|
}
|
|
198
199
|
const subtree = new Set(subtreeOf(actor));
|
|
@@ -54,6 +54,13 @@ export declare class CompanyHierarchyService {
|
|
|
54
54
|
ceiling: MemberDominance, audit?: AuditPort | undefined, now?: () => number);
|
|
55
55
|
/** The actor's own root node (see {@link ownRootOf}). */
|
|
56
56
|
private ownRoot;
|
|
57
|
+
/**
|
|
58
|
+
* The company's uuid as stored (Phase 10 #1): `c_uuid` is `utf8mb4_unicode_ci`, so a route uuid
|
|
59
|
+
* in another letter case still reaches the row, while JS-side comparisons (own root, agent
|
|
60
|
+
* tenants, `JSON_CONTAINS` over scope lists, name maps) are exact. Every entry resolves the
|
|
61
|
+
* stored spelling once and passes that down. Unknown → the input unchanged (later steps 404).
|
|
62
|
+
*/
|
|
63
|
+
private storedUuid;
|
|
57
64
|
/** Names of the given companies (unknown → absent). */
|
|
58
65
|
private names;
|
|
59
66
|
private recordAudit;
|
|
@@ -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;
|
|
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;AAEhD,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;;;;;OAKG;YACW,UAAU;IAQxB,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;IAoD3F;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB/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"}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CompanyHierarchyService = void 0;
|
|
4
4
|
exports.ownRootOf = ownRootOf;
|
|
5
|
+
const http_js_1 = require("../shared/http.js");
|
|
5
6
|
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
6
7
|
const lock_service_accounts_js_1 = require("../service-account/lock-service-accounts.js");
|
|
7
8
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
@@ -46,6 +47,16 @@ class CompanyHierarchyService {
|
|
|
46
47
|
ownRoot(actor) {
|
|
47
48
|
return ownRootOf(actor);
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The company's uuid as stored (Phase 10 #1): `c_uuid` is `utf8mb4_unicode_ci`, so a route uuid
|
|
52
|
+
* in another letter case still reaches the row, while JS-side comparisons (own root, agent
|
|
53
|
+
* tenants, `JSON_CONTAINS` over scope lists, name maps) are exact. Every entry resolves the
|
|
54
|
+
* stored spelling once and passes that down. Unknown → the input unchanged (later steps 404).
|
|
55
|
+
*/
|
|
56
|
+
async storedUuid(uuid) {
|
|
57
|
+
const rows = (await this.pool.query(`SELECT c_uuid FROM \`${APP}\`.\`companies\` WHERE c_uuid = ? LIMIT 1`, [uuid]));
|
|
58
|
+
return rows[0]?.c_uuid ?? uuid;
|
|
59
|
+
}
|
|
49
60
|
/** Names of the given companies (unknown → absent). */
|
|
50
61
|
async names(q, ids) {
|
|
51
62
|
const unique = [...new Set(ids)];
|
|
@@ -75,11 +86,14 @@ class CompanyHierarchyService {
|
|
|
75
86
|
(typeof newParentUuid !== 'string' || newParentUuid.length === 0)) {
|
|
76
87
|
throw new identity_errors_js_1.IdentityBadRequestError('parentUuid must be a company or null.');
|
|
77
88
|
}
|
|
89
|
+
nodeUuid = await this.storedUuid(nodeUuid);
|
|
90
|
+
if (newParentUuid !== null)
|
|
91
|
+
newParentUuid = await this.storedUuid(newParentUuid);
|
|
78
92
|
if (actor.isRoot !== true) {
|
|
79
93
|
const subtree = new Set((0, grant_ceiling_js_1.subtreeOf)(actor));
|
|
80
94
|
if (!subtree.has(nodeUuid))
|
|
81
95
|
throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
|
|
82
|
-
if (nodeUuid
|
|
96
|
+
if ((0, http_js_1.sameUuid)(nodeUuid, this.ownRoot(actor))) {
|
|
83
97
|
throw new identity_errors_js_1.IdentityForbiddenError('You cannot move the organisation your access is rooted at.');
|
|
84
98
|
}
|
|
85
99
|
if (newParentUuid === null || !subtree.has(newParentUuid)) {
|
|
@@ -125,9 +139,10 @@ class CompanyHierarchyService {
|
|
|
125
139
|
* depending on it as they were; a commit leaves a deleted company with no live agent or grant.
|
|
126
140
|
*/
|
|
127
141
|
async remove(actor, nodeUuid) {
|
|
142
|
+
nodeUuid = await this.storedUuid(nodeUuid); // Phase 10 #1: one spelling downstream
|
|
128
143
|
// Security review F6: the same own-root guard as `move` — a sub-admin never deletes the
|
|
129
144
|
// organisation its access is rooted at.
|
|
130
|
-
if (actor.isRoot !== true && nodeUuid
|
|
145
|
+
if (actor.isRoot !== true && (0, http_js_1.sameUuid)(nodeUuid, this.ownRoot(actor))) {
|
|
131
146
|
throw new identity_errors_js_1.IdentityForbiddenError('You cannot delete the organisation your access is rooted at.');
|
|
132
147
|
}
|
|
133
148
|
const name = (await this.names(this.pool, [nodeUuid])).get(nodeUuid) ?? '(unknown)';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"company.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAa,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"company.service.d.ts","sourceRoot":"","sources":["../../../src/features/company/company.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,uBAAuB,EAAa,MAAM,gCAAgC,CAAC;AAUpF;;;;;;;GAOG;AACH,qBACa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,uBAAuB;IAGrD;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAetE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D,2EAA2E;IAC3E,aAAa,CAAC,MAAM,CAAC,EAAE,UAAU;IAOjC,6BAA6B;IAC7B,UAAU,CAAC,IAAI,EAAE,MAAM;IAIvB;;;;;;;OAOG;IACG,aAAa,CACjB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,CAAC,IAAI,CAAC;IAiBhB,oGAAoG;IACpG,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF;;;;;OAKG;IACG,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKnE"}
|
|
@@ -15,6 +15,7 @@ const common_1 = require("@nestjs/common");
|
|
|
15
15
|
const engine_1 = require("@astram/engine");
|
|
16
16
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
17
17
|
const company_hierarchy_service_js_1 = require("./company-hierarchy.service.js");
|
|
18
|
+
const http_js_1 = require("../shared/http.js");
|
|
18
19
|
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
19
20
|
const COMPANY_SEARCH_FIELDS = ['name'];
|
|
20
21
|
const COMPANY_SORT_FIELDS = ['name', 'status', 'createdAt'];
|
|
@@ -80,7 +81,7 @@ let CompanyService = CompanyService_1 = class CompanyService {
|
|
|
80
81
|
if (typeof patch?.[key] === 'string')
|
|
81
82
|
fields[key] = patch[key];
|
|
82
83
|
}
|
|
83
|
-
if (actor.isRoot !== true && fields.status !== undefined && uuid
|
|
84
|
+
if (actor.isRoot !== true && fields.status !== undefined && (0, http_js_1.sameUuid)(uuid, (0, company_hierarchy_service_js_1.ownRootOf)(actor))) {
|
|
84
85
|
const current = await this.records.get('company', uuid);
|
|
85
86
|
if (current === null)
|
|
86
87
|
throw new identity_errors_js_1.IdentityNotFoundError('Company not found.');
|
|
@@ -3,6 +3,12 @@ import type { Principal } from '@astram/engine';
|
|
|
3
3
|
export interface AuthedRequest {
|
|
4
4
|
principal: Principal;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Same uuid, compared the way MariaDB does: the uuid columns are `utf8mb4_unicode_ci`, so a route
|
|
8
|
+
* param that differs only in case reaches the same row. Every JS-side id guard (own root, platform
|
|
9
|
+
* administrators) must use this, never `===` (Phase 10 #1).
|
|
10
|
+
*/
|
|
11
|
+
export declare function sameUuid(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
6
12
|
/** Coerce an unknown field value to a string uuid or null. */
|
|
7
13
|
export declare function asUuid(v: unknown): string | null;
|
|
8
14
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/features/shared/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,8DAA8D;AAC9D,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAQnD"}
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/features/shared/http.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,SAAS,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE5F;AAED,8DAA8D;AAC9D,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAQnD"}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sameUuid = sameUuid;
|
|
3
4
|
exports.asUuid = asUuid;
|
|
4
5
|
exports.asEpochMs = asEpochMs;
|
|
6
|
+
/**
|
|
7
|
+
* Same uuid, compared the way MariaDB does: the uuid columns are `utf8mb4_unicode_ci`, so a route
|
|
8
|
+
* param that differs only in case reaches the same row. Every JS-side id guard (own root, platform
|
|
9
|
+
* administrators) must use this, never `===` (Phase 10 #1).
|
|
10
|
+
*/
|
|
11
|
+
function sameUuid(a, b) {
|
|
12
|
+
return typeof a === 'string' && typeof b === 'string' && a.toLowerCase() === b.toLowerCase();
|
|
13
|
+
}
|
|
5
14
|
/** Coerce an unknown field value to a string uuid or null. */
|
|
6
15
|
function asUuid(v) {
|
|
7
16
|
return typeof v === 'string' && v.length > 0 ? v : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/identity",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Astram framework — the framework-owned identity + tenancy model. Hosts the identity @ObjectType classes (user, company, membership, posting), framework-authored identity migrations, and the identity resolver. Top of the dependency graph: depends on @astram/engine (for @ObjectType/@Field decorators) + @astram/guard / @astram/core (for the auth ports).",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"mariadb": "^3.5.4",
|
|
39
39
|
"@astram/core": "1.4.0",
|
|
40
|
-
"@astram/
|
|
41
|
-
"@astram/
|
|
40
|
+
"@astram/guard": "0.10.1",
|
|
41
|
+
"@astram/engine": "0.5.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@nestjs/common": "^11",
|