@astram/identity 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/branch.object.d.ts +9 -1
- package/dist/branch.object.d.ts.map +1 -1
- package/dist/branch.object.js +14 -1
- package/dist/company-branch.object.d.ts +5 -4
- package/dist/company-branch.object.d.ts.map +1 -1
- package/dist/company-branch.object.js +6 -4
- package/dist/company-department.object.d.ts +6 -6
- package/dist/company-department.object.d.ts.map +1 -1
- package/dist/company-department.object.js +7 -6
- package/dist/department.object.d.ts +9 -1
- package/dist/department.object.d.ts.map +1 -1
- package/dist/department.object.js +14 -1
- package/dist/features/access/access-admin.access.d.ts.map +1 -1
- package/dist/features/access/access-admin.access.js +8 -1
- package/dist/features/access/index.d.ts +1 -0
- package/dist/features/access/index.d.ts.map +1 -1
- package/dist/features/access/index.js +10 -2
- package/dist/features/access/org-structure.access.d.ts +3 -0
- package/dist/features/access/org-structure.access.d.ts.map +1 -0
- package/dist/features/access/org-structure.access.js +41 -0
- package/dist/features/access/user-management.access.d.ts.map +1 -1
- package/dist/features/access/user-management.access.js +5 -0
- package/dist/features/audit/audit-read.service.d.ts +32 -1
- package/dist/features/audit/audit-read.service.d.ts.map +1 -1
- package/dist/features/audit/audit-read.service.js +67 -14
- package/dist/features/audit/auth-audit-sink.d.ts +19 -0
- package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
- package/dist/features/audit/auth-audit-sink.js +24 -0
- package/dist/features/audit/directory-label-resolver.d.ts +10 -1
- package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
- package/dist/features/audit/directory-label-resolver.js +28 -1
- package/dist/features/authz/grant-admin.controller.d.ts +4 -4
- package/dist/features/authz/grant-admin.controller.js +4 -4
- package/dist/features/authz/grant-admin.service.d.ts +9 -8
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +20 -24
- package/dist/features/authz/grant-ceiling.d.ts +35 -16
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +44 -29
- package/dist/features/authz/role-binding.d.ts +25 -24
- package/dist/features/authz/role-binding.d.ts.map +1 -1
- package/dist/features/authz/role-binding.js +31 -44
- package/dist/features/identity-feature.module.d.ts +0 -11
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +90 -1
- package/dist/features/membership/membership.controller.d.ts.map +1 -1
- package/dist/features/membership/membership.controller.js +20 -6
- package/dist/features/membership/membership.service.d.ts +16 -10
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +61 -21
- package/dist/features/membership/posting.service.d.ts +25 -6
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +63 -16
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
- package/dist/features/org-bootstrap/org-state.reader.js +56 -49
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
- package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
- package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.controller.js +105 -17
- package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +327 -51
- package/dist/features/org-structure/org-unit-name.d.ts +31 -0
- package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
- package/dist/features/org-structure/org-unit-name.js +50 -0
- package/dist/features/role/role.controller.d.ts +28 -13
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +54 -14
- package/dist/features/role/role.service.d.ts +76 -21
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +204 -27
- package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
- package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy-version-bump.js +65 -0
- package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
- package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.controller.js +121 -0
- package/dist/features/security-policy/security-policy.service.d.ts +94 -0
- package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
- package/dist/features/security-policy/security-policy.service.js +429 -0
- package/dist/features/service-account/service-account.service.d.ts +13 -3
- package/dist/features/service-account/service-account.service.d.ts.map +1 -1
- package/dist/features/service-account/service-account.service.js +48 -10
- package/dist/features/service-account/service-account.store.d.ts +11 -1
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +20 -1
- package/dist/features/user/signin-admin.tokens.d.ts +11 -0
- package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
- package/dist/features/user/signin-admin.tokens.js +13 -0
- package/dist/features/user/user-mfa-status.d.ts +38 -0
- package/dist/features/user/user-mfa-status.d.ts.map +1 -0
- package/dist/features/user/user-mfa-status.js +47 -0
- package/dist/features/user/user-signin-admin.service.d.ts +48 -0
- package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
- package/dist/features/user/user-signin-admin.service.js +232 -0
- package/dist/features/user/user.controller.d.ts +29 -2
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +71 -3
- package/dist/features/user/user.service.d.ts +40 -14
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +143 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +2 -0
- package/dist/role.object.d.ts +1 -1
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +6 -15
- package/dist/security-policy.object.d.ts +43 -0
- package/dist/security-policy.object.d.ts.map +1 -0
- package/dist/security-policy.object.js +107 -0
- package/dist/user.object.d.ts +6 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +13 -1
- package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
- package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
- package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
- package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
- package/package.json +4 -4
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import type { LabelResolver, LabelKind } from '@astram/core';
|
|
2
2
|
import type { AuditQueryable } from './mariadb-audit-reader.js';
|
|
3
3
|
import { type IdentitySchemaNames } from '../../schema.js';
|
|
4
|
+
import type { ActorLabelResolver, ResolvedActor } from './audit-read.service.js';
|
|
4
5
|
/**
|
|
5
6
|
* The id->human-label chokepoint (ADR-0012, design §2.2). A deleted/unknown
|
|
6
7
|
* entity resolves to a FALLBACK label (never the id); a directory OUTAGE throws
|
|
7
8
|
* (fail-closed — a transient must not masquerade as a deletion).
|
|
8
9
|
*/
|
|
9
|
-
export declare class DirectoryLabelResolver implements LabelResolver {
|
|
10
|
+
export declare class DirectoryLabelResolver implements LabelResolver, ActorLabelResolver {
|
|
10
11
|
private readonly db;
|
|
11
12
|
private readonly schemas;
|
|
12
13
|
/** @param schemas resolved framework names (absent ⇒ resolved from the namespace, once). */
|
|
13
14
|
constructor(db: AuditQueryable, schemas?: IdentitySchemaNames);
|
|
14
15
|
resolve(kind: LabelKind, id: string, _tenantId?: string): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Who an audit `actorId` is (KI #140): a user (`users.u_uuid`), else a service account (an
|
|
18
|
+
* `ask_…` API-key principal carries `sa_id`), else unknown → "Deleted user" as before. Both are
|
|
19
|
+
* primary-key lookups; the second runs only when the first misses. Reserved operation
|
|
20
|
+
* principals never reach here (the read service labels them from guard's registry).
|
|
21
|
+
*/
|
|
22
|
+
resolveActor(id: string): Promise<ResolvedActor>;
|
|
23
|
+
private serviceAccountLabel;
|
|
15
24
|
private fallback;
|
|
16
25
|
}
|
|
17
26
|
//# sourceMappingURL=directory-label-resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"directory-label-resolver.d.ts","sourceRoot":"","sources":["../../../src/features/audit/directory-label-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"directory-label-resolver.d.ts","sourceRoot":"","sources":["../../../src/features/audit/directory-label-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAA2B,KAAK,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACpF,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAEjF;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,aAAa,EAAE,kBAAkB;IAG5E,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAH1B,4FAA4F;gBAEzE,EAAE,EAAE,cAAc,EAClB,OAAO,GAAE,mBAA+C;IAGrE,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAkC/E;;;;;OAKG;IACG,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;YAYxC,mBAAmB;IAQjC,OAAO,CAAC,QAAQ;CAOjB"}
|
|
@@ -31,11 +31,36 @@ class DirectoryLabelResolver {
|
|
|
31
31
|
const rows = (await this.db.query(`SELECT r_name AS label FROM \`${this.schemas.app}\`.\`roles\` WHERE r_uuid = ? LIMIT 1`, [id]));
|
|
32
32
|
return rows[0]?.label ?? this.fallback(kind);
|
|
33
33
|
}
|
|
34
|
+
case 'service_account': {
|
|
35
|
+
const label = await this.serviceAccountLabel(id);
|
|
36
|
+
return label ?? this.fallback(kind);
|
|
37
|
+
}
|
|
34
38
|
default:
|
|
35
|
-
//
|
|
39
|
+
// object_type / permission not shown in v1 columns → safe fallback.
|
|
36
40
|
return '(unknown)';
|
|
37
41
|
}
|
|
38
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Who an audit `actorId` is (KI #140): a user (`users.u_uuid`), else a service account (an
|
|
45
|
+
* `ask_…` API-key principal carries `sa_id`), else unknown → "Deleted user" as before. Both are
|
|
46
|
+
* primary-key lookups; the second runs only when the first misses. Reserved operation
|
|
47
|
+
* principals never reach here (the read service labels them from guard's registry).
|
|
48
|
+
*/
|
|
49
|
+
async resolveActor(id) {
|
|
50
|
+
if (!id)
|
|
51
|
+
return { kind: 'unknown', label: this.fallback('principal') };
|
|
52
|
+
const users = (await this.db.query(`SELECT u_username AS label FROM \`${this.schemas.app}\`.\`users\` WHERE u_uuid = ? LIMIT 1`, [id]));
|
|
53
|
+
if (users[0]?.label !== undefined)
|
|
54
|
+
return { kind: 'user', label: users[0].label };
|
|
55
|
+
const sa = await this.serviceAccountLabel(id);
|
|
56
|
+
if (sa !== undefined)
|
|
57
|
+
return { kind: 'service_account', label: sa };
|
|
58
|
+
return { kind: 'unknown', label: this.fallback('principal') };
|
|
59
|
+
}
|
|
60
|
+
async serviceAccountLabel(id) {
|
|
61
|
+
const rows = (await this.db.query(`SELECT sa_label AS label FROM \`${this.schemas.app}\`.\`service_account\` WHERE sa_id = ? LIMIT 1`, [id]));
|
|
62
|
+
return rows[0]?.label;
|
|
63
|
+
}
|
|
39
64
|
fallback(kind) {
|
|
40
65
|
if (kind === 'principal')
|
|
41
66
|
return 'Deleted user';
|
|
@@ -43,6 +68,8 @@ class DirectoryLabelResolver {
|
|
|
43
68
|
return 'Deleted tenant';
|
|
44
69
|
if (kind === 'role')
|
|
45
70
|
return 'Deleted role';
|
|
71
|
+
if (kind === 'service_account')
|
|
72
|
+
return 'Deleted service account';
|
|
46
73
|
return '(unknown)';
|
|
47
74
|
}
|
|
48
75
|
}
|
|
@@ -14,10 +14,10 @@ interface AuthedRequest {
|
|
|
14
14
|
* registered object type — it is administered THROUGH `role`, never classified directly.)
|
|
15
15
|
*
|
|
16
16
|
* Mutating routes pass `req.principal` as the actor: it stamps who granted what (surfaced by the
|
|
17
|
-
* grants-summary listing as "by <name>"), and grant AND revoke run the
|
|
18
|
-
*
|
|
19
|
-
* revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling
|
|
20
|
-
*
|
|
17
|
+
* grants-summary listing as "by <name>"), and grant AND revoke run the owner rule (G-013, ADR-0047:
|
|
18
|
+
* the role's owner company must be in the caller's subtree) and the ADR-0029 ceiling ("only grant or
|
|
19
|
+
* revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling → 403,
|
|
20
|
+
* a role owned outside the caller's subtree (or ownerless, for non-root) → 404, unknown key/bundle → 400.
|
|
21
21
|
*/
|
|
22
22
|
export declare class GrantAdminController {
|
|
23
23
|
private readonly grants;
|
|
@@ -40,10 +40,10 @@ function subtreeScopeOf(req) {
|
|
|
40
40
|
* registered object type — it is administered THROUGH `role`, never classified directly.)
|
|
41
41
|
*
|
|
42
42
|
* Mutating routes pass `req.principal` as the actor: it stamps who granted what (surfaced by the
|
|
43
|
-
* grants-summary listing as "by <name>"), and grant AND revoke run the
|
|
44
|
-
*
|
|
45
|
-
* revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling
|
|
46
|
-
*
|
|
43
|
+
* grants-summary listing as "by <name>"), and grant AND revoke run the owner rule (G-013, ADR-0047:
|
|
44
|
+
* the role's owner company must be in the caller's subtree) and the ADR-0029 ceiling ("only grant or
|
|
45
|
+
* revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling → 403,
|
|
46
|
+
* a role owned outside the caller's subtree (or ownerless, for non-root) → 404, unknown key/bundle → 400.
|
|
47
47
|
*/
|
|
48
48
|
let GrantAdminController = class GrantAdminController {
|
|
49
49
|
grants;
|
|
@@ -74,9 +74,10 @@ export declare class GrantAdminService {
|
|
|
74
74
|
wildcards: string[];
|
|
75
75
|
}>;
|
|
76
76
|
/**
|
|
77
|
-
* A role's current grants.
|
|
78
|
-
* grants ONLY IF
|
|
79
|
-
* (fail-closed — never leak another
|
|
77
|
+
* A role's current grants. Subtree-guarded by the G-013 owner rule (spec §5.D): a non-root caller
|
|
78
|
+
* may read a role's grants ONLY IF the role's owner company is in their reachable subtree;
|
|
79
|
+
* otherwise `{ grants: [] }` (fail-closed — never leak another company's role's permissions by
|
|
80
|
+
* uuid; an ownerless role is root only, R5). An empty subtree matches nothing. Root skips the guard.
|
|
80
81
|
*/
|
|
81
82
|
listRoleGrants(roleUuid: string, subtreeScope: SubtreeScope): Promise<{
|
|
82
83
|
grants: string[];
|
|
@@ -84,11 +85,11 @@ export declare class GrantAdminService {
|
|
|
84
85
|
/** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
|
|
85
86
|
private assertValidKey;
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
88
|
-
* must be administrable by the actor (
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
88
|
+
* G-013 owner rule (spec §5.B, replacing D1) + ADR-0029, in this order, BEFORE any write: the role
|
|
89
|
+
* must be administrable by the actor (its owner company in the actor's subtree, else 404), then
|
|
90
|
+
* every key must be covered by the actor's own effective keys ("only grant — or revoke — what you
|
|
91
|
+
* hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒ neither check runs; the identity
|
|
92
|
+
* module always wires it.
|
|
92
93
|
*/
|
|
93
94
|
private assertMayChangeGrants;
|
|
94
95
|
/**
|
|
@@ -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,EAAe,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -91,22 +91,18 @@ class GrantAdminService {
|
|
|
91
91
|
return { catalog, wildcards };
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
|
-
* A role's current grants.
|
|
95
|
-
* grants ONLY IF
|
|
96
|
-
* (fail-closed — never leak another
|
|
94
|
+
* A role's current grants. Subtree-guarded by the G-013 owner rule (spec §5.D): a non-root caller
|
|
95
|
+
* may read a role's grants ONLY IF the role's owner company is in their reachable subtree;
|
|
96
|
+
* otherwise `{ grants: [] }` (fail-closed — never leak another company's role's permissions by
|
|
97
|
+
* uuid; an ownerless role is root only, R5). An empty subtree matches nothing. Root skips the guard.
|
|
97
98
|
*/
|
|
98
99
|
async listRoleGrants(roleUuid, subtreeScope) {
|
|
99
100
|
try {
|
|
100
101
|
if (!subtreeScope.isRoot) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
JOIN \`${this.schemas.app}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid
|
|
106
|
-
WHERE mp.mp_role_uuid = ? AND cm.cm_company_uuid IN (${ph})
|
|
107
|
-
LIMIT 1`, [roleUuid, ...subtreeScope.nodeIds]))
|
|
108
|
-
: [];
|
|
109
|
-
if (bound.length === 0)
|
|
102
|
+
if (subtreeScope.nodeIds.length === 0)
|
|
103
|
+
return { grants: [] };
|
|
104
|
+
const role = await (0, role_binding_js_1.readRoleOwner)(this.pool, roleUuid, this.schemas);
|
|
105
|
+
if (!(0, role_binding_js_1.ownerInSubtree)(role, subtreeScope.nodeIds))
|
|
110
106
|
return { grants: [] };
|
|
111
107
|
}
|
|
112
108
|
const rows = (await this.pool.query(`SELECT rp_perm_key FROM \`${this.schemas.authz}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid]));
|
|
@@ -124,11 +120,11 @@ class GrantAdminService {
|
|
|
124
120
|
throw new BadKeyError(key);
|
|
125
121
|
}
|
|
126
122
|
/**
|
|
127
|
-
*
|
|
128
|
-
* must be administrable by the actor (
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
123
|
+
* G-013 owner rule (spec §5.B, replacing D1) + ADR-0029, in this order, BEFORE any write: the role
|
|
124
|
+
* must be administrable by the actor (its owner company in the actor's subtree, else 404), then
|
|
125
|
+
* every key must be covered by the actor's own effective keys ("only grant — or revoke — what you
|
|
126
|
+
* hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒ neither check runs; the identity
|
|
127
|
+
* module always wires it.
|
|
132
128
|
*/
|
|
133
129
|
async assertMayChangeGrants(roleUuid, keys, actor) {
|
|
134
130
|
if (!this.ceiling)
|
|
@@ -146,13 +142,13 @@ class GrantAdminService {
|
|
|
146
142
|
const conn = await this.pool.getConnection();
|
|
147
143
|
try {
|
|
148
144
|
await conn.beginTransaction();
|
|
149
|
-
// Security review F2: serialise against a concurrent posting of this role
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
145
|
+
// Security review F2: serialise against a concurrent posting or delete of this role. Lock the
|
|
146
|
+
// role row FIRST — the posting and delete paths lock the same row — then re-run the owner
|
|
147
|
+
// rule (G-013) on THIS connection, so a role deleted (or ownerless after its company was
|
|
148
|
+
// deleted) while we waited is refused (rolled back).
|
|
153
149
|
await conn.query(`SELECT r_uuid FROM \`${this.schemas.app}\`.\`roles\` WHERE r_uuid = ? FOR UPDATE`, [roleUuid]);
|
|
154
150
|
if (this.ceiling && actor.isRoot !== true) {
|
|
155
|
-
grant_ceiling_js_1.GrantCeiling.checkAdministrable(await (0, role_binding_js_1.
|
|
151
|
+
grant_ceiling_js_1.GrantCeiling.checkAdministrable(actor, await (0, role_binding_js_1.readRoleOwner)(conn, roleUuid, this.schemas));
|
|
156
152
|
// Phase 8 #2: a revocation strips authority from every holder of the role, so the actor
|
|
157
153
|
// must dominate each of them — checked under the role lock (no holder can appear unseen).
|
|
158
154
|
// Phase 9 #9: that includes the service accounts holding the role, judged as a role
|
|
@@ -448,13 +444,13 @@ class GrantAdminService {
|
|
|
448
444
|
const { catalog } = await this.listCatalog();
|
|
449
445
|
const allKeys = catalog.map((c) => c.key);
|
|
450
446
|
const { bundles } = await this.listBundles();
|
|
451
|
-
// Task 4.2 subtree scope: a non-root caller sees ONLY roles
|
|
447
|
+
// Task 4.2 subtree scope: a non-root caller sees ONLY roles OWNED within their reachable
|
|
452
448
|
// subtree (the EXISTS predicate); root spans all tenants (no predicate). The scope clause
|
|
453
449
|
// sits FIRST in the WHERE (before the LIKE), so its ?-bound node params lead the arg list —
|
|
454
450
|
// identical on BOTH the count and page queries so they can never disagree.
|
|
455
451
|
const scope = subtreeScope.isRoot
|
|
456
452
|
? { sql: '', params: [] }
|
|
457
|
-
: (0, role_binding_js_1.
|
|
453
|
+
: (0, role_binding_js_1.ownedInSubtree)(subtreeScope.nodeIds);
|
|
458
454
|
// Distinct granted roles (a role appears iff it has ≥1 role_permission row), filtered
|
|
459
455
|
// by name search. Bind the LIKE term as a param (wildcards escaped).
|
|
460
456
|
const likeClause = search ? `AND r.r_name LIKE ?` : '';
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { Pool } from 'mariadb';
|
|
16
16
|
import type { ModuleRegistry, Principal } from '@astram/core';
|
|
17
|
-
import { type
|
|
17
|
+
import { type RoleOwner } from './role-binding.js';
|
|
18
|
+
import type { Queryable } from './permission-version.bumper.js';
|
|
18
19
|
import { type IdentitySchemaNames } from '../../schema.js';
|
|
19
20
|
/** The grantor exceeds their own authority — cannot grant a key they do not hold (→ 403). */
|
|
20
21
|
export declare class GrantCeilingError extends Error {
|
|
@@ -28,8 +29,10 @@ export declare class GrantCeilingError extends Error {
|
|
|
28
29
|
export interface OperationRoleOptions {
|
|
29
30
|
readonly operationRoleIds?: readonly string[];
|
|
30
31
|
}
|
|
32
|
+
/** The parts of a principal its reachable subtree is computed from. */
|
|
33
|
+
export type SubtreeActor = Pick<Principal, 'tenantId' | 'scopeSubtreeNodeIds'>;
|
|
31
34
|
/** The actor's reachable subtree: the session's cached closure, else the self node (non-widening). */
|
|
32
|
-
export declare function subtreeOf(actor:
|
|
35
|
+
export declare function subtreeOf(actor: SubtreeActor): readonly string[];
|
|
33
36
|
/**
|
|
34
37
|
* Does `effective` (the grantor's held EXACT keys) cover `granted`?
|
|
35
38
|
*
|
|
@@ -67,10 +70,12 @@ export declare class GrantCeiling {
|
|
|
67
70
|
now?: () => number,
|
|
68
71
|
/** Resolved framework names (absent ⇒ resolved from the namespace, once, here). */
|
|
69
72
|
schemas?: IdentitySchemaNames);
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The G-013 administer rule for a NON-ROOT actor over an owner read (spec §5.B): the role's owner
|
|
75
|
+
* company is in the actor's subtree. Missing, ownerless (root only, R5) or owned outside is "not
|
|
76
|
+
* found" (404, no existence leak, no uuid in the message). Root is decided by the caller.
|
|
77
|
+
*/
|
|
78
|
+
static checkAdministrable(actor: SubtreeActor, role: RoleOwner): void;
|
|
74
79
|
/** The registry's exact-key universe (wildcard expansion bound). */
|
|
75
80
|
private registeredKeys;
|
|
76
81
|
/** The raw `role_permission` keys of `roleIds` (exact or wildcard). Store errors propagate. */
|
|
@@ -166,20 +171,34 @@ export declare class GrantCeiling {
|
|
|
166
171
|
readonly departmentUuids: readonly string[];
|
|
167
172
|
readonly branchUuids: readonly string[];
|
|
168
173
|
}): Promise<void>;
|
|
169
|
-
/** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
|
|
170
|
-
roleBinding(actor: Principal, roleUuid: string): Promise<RoleBinding>;
|
|
171
174
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
* `roleUuid`'s owner company (G-013), read on `q` (default: the shared authz pool; a caller holding
|
|
176
|
+
* the role row lock passes its transaction connection). Store outage → 503 (fail closed).
|
|
177
|
+
*/
|
|
178
|
+
roleOwner(roleUuid: string, q?: Queryable): Promise<RoleOwner>;
|
|
179
|
+
/**
|
|
180
|
+
* Is the role visible to the actor by the owner rule (spec §5.D, security-policy role targets)?
|
|
181
|
+
* Root: the role exists (ownerless included, R5). Non-root: its owner company is in the actor's
|
|
182
|
+
* subtree. Store outage → 503.
|
|
183
|
+
*/
|
|
184
|
+
roleOwnedInReach(actor: Principal, roleUuid: string): Promise<boolean>;
|
|
185
|
+
/**
|
|
186
|
+
* Assert the actor may ADMINISTER the role — edit its grants, rename or delete it (G-013, spec
|
|
187
|
+
* §5.B, replacing D1): the role's owner company is in the actor's subtree. Missing, ownerless
|
|
188
|
+
* (root only, R5) or owned outside ⇒ 404 "Role not found." (no existence leak). Root bypasses; so
|
|
189
|
+
* does the actor bound to the org bootstrap operation for the roles it declared.
|
|
176
190
|
*/
|
|
177
191
|
assertRoleAdministrable(actor: Principal, roleUuid: string, opts?: OperationRoleOptions): Promise<void>;
|
|
178
192
|
/**
|
|
179
|
-
* Assert the
|
|
180
|
-
*
|
|
181
|
-
*
|
|
193
|
+
* Assert the role may be ASSIGNED in `companyUuid` — a posting of a membership of that company,
|
|
194
|
+
* or a service account of that company (G-013, spec §5.B, R2): the role's owner IS that company.
|
|
195
|
+
* Applies to every actor, root and the org bootstrap operation included (it is the data's shape,
|
|
196
|
+
* not a privilege, as {@link assertOrgUnitsInCompany}); `actor` and `opts` therefore relax nothing.
|
|
197
|
+
* A missing role, an ownerless role, another company's role, or no company (`null`) give the same
|
|
198
|
+
* 400. A non-root actor's reach to the role follows from its reach to the company (the caller
|
|
199
|
+
* reached the membership or account through the subtree wall). Pair with
|
|
200
|
+
* {@link assertCanAssignRoles}. Store outage → 503.
|
|
182
201
|
*/
|
|
183
|
-
assertRoleAssignable(
|
|
202
|
+
assertRoleAssignable(_actor: Pick<Principal, 'id' | 'tenantId' | 'isRoot'>, roleUuid: string, companyUuid: string | null, _opts?: OperationRoleOptions): Promise<void>;
|
|
184
203
|
}
|
|
185
204
|
//# sourceMappingURL=grant-ceiling.d.ts.map
|
|
@@ -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;AAM9D,OAAO,
|
|
1
|
+
{"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
|
|
@@ -87,19 +87,14 @@ class GrantCeiling {
|
|
|
87
87
|
}
|
|
88
88
|
this.rootIds = rootIds ?? [];
|
|
89
89
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
/**
|
|
91
|
+
* The G-013 administer rule for a NON-ROOT actor over an owner read (spec §5.B): the role's owner
|
|
92
|
+
* company is in the actor's subtree. Missing, ownerless (root only, R5) or owned outside is "not
|
|
93
|
+
* found" (404, no existence leak, no uuid in the message). Root is decided by the caller.
|
|
94
|
+
*/
|
|
95
|
+
static checkAdministrable(actor, role) {
|
|
96
|
+
if (!(0, role_binding_js_1.ownerInSubtree)(role, subtreeOf(actor)))
|
|
93
97
|
throw new identity_errors_js_1.IdentityNotFoundError('Role not found.');
|
|
94
|
-
if (b.outside) {
|
|
95
|
-
throw new identity_errors_js_1.IdentityForbiddenError('This role is also used outside the organisations you manage; only a platform administrator can change it.');
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
/** The #11 assign rule over a binding read: never a role also used outside (403). */
|
|
99
|
-
static checkAssignable(b) {
|
|
100
|
-
if (b.outside) {
|
|
101
|
-
throw new identity_errors_js_1.IdentityForbiddenError('This role is also used outside the organisations you manage; you cannot assign it.');
|
|
102
|
-
}
|
|
103
98
|
}
|
|
104
99
|
/** The registry's exact-key universe (wildcard expansion bound). */
|
|
105
100
|
registeredKeys() {
|
|
@@ -375,34 +370,54 @@ class GrantCeiling {
|
|
|
375
370
|
await check(units.departmentUuids, 'company_departments', 'cd_company_uuid', 'cd_department_uuid', 'The department is not one of this company’s departments.');
|
|
376
371
|
await check(units.branchUuids, 'company_branches', 'cb_company_uuid', 'cb_branch_uuid', 'The branch is not one of this company’s branches.');
|
|
377
372
|
}
|
|
378
|
-
/**
|
|
379
|
-
|
|
380
|
-
|
|
373
|
+
/**
|
|
374
|
+
* `roleUuid`'s owner company (G-013), read on `q` (default: the shared authz pool; a caller holding
|
|
375
|
+
* the role row lock passes its transaction connection). Store outage → 503 (fail closed).
|
|
376
|
+
*/
|
|
377
|
+
async roleOwner(roleUuid, q = this.pool) {
|
|
378
|
+
return this.facts('role owner', () => (0, role_binding_js_1.readRoleOwner)(q, roleUuid, this.schemas));
|
|
381
379
|
}
|
|
382
380
|
/**
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* used outside (a shared role) is root-only. Root bypasses.
|
|
381
|
+
* Is the role visible to the actor by the owner rule (spec §5.D, security-policy role targets)?
|
|
382
|
+
* Root: the role exists (ownerless included, R5). Non-root: its owner company is in the actor's
|
|
383
|
+
* subtree. Store outage → 503.
|
|
387
384
|
*/
|
|
388
|
-
async
|
|
385
|
+
async roleOwnedInReach(actor, roleUuid) {
|
|
386
|
+
const role = await this.roleOwner(roleUuid);
|
|
389
387
|
if (actor.isRoot === true)
|
|
390
|
-
return;
|
|
391
|
-
|
|
392
|
-
return;
|
|
393
|
-
GrantCeiling.checkAdministrable(await this.roleBinding(actor, roleUuid));
|
|
388
|
+
return role.exists;
|
|
389
|
+
return (0, role_binding_js_1.ownerInSubtree)(role, subtreeOf(actor));
|
|
394
390
|
}
|
|
395
391
|
/**
|
|
396
|
-
* Assert the actor may
|
|
397
|
-
*
|
|
398
|
-
*
|
|
392
|
+
* Assert the actor may ADMINISTER the role — edit its grants, rename or delete it (G-013, spec
|
|
393
|
+
* §5.B, replacing D1): the role's owner company is in the actor's subtree. Missing, ownerless
|
|
394
|
+
* (root only, R5) or owned outside ⇒ 404 "Role not found." (no existence leak). Root bypasses; so
|
|
395
|
+
* does the actor bound to the org bootstrap operation for the roles it declared.
|
|
399
396
|
*/
|
|
400
|
-
async
|
|
397
|
+
async assertRoleAdministrable(actor, roleUuid, opts) {
|
|
401
398
|
if (actor.isRoot === true)
|
|
402
399
|
return;
|
|
403
400
|
if (operationCovers(actor, [roleUuid], opts))
|
|
404
401
|
return;
|
|
405
|
-
GrantCeiling.
|
|
402
|
+
GrantCeiling.checkAdministrable(actor, await this.roleOwner(roleUuid));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Assert the role may be ASSIGNED in `companyUuid` — a posting of a membership of that company,
|
|
406
|
+
* or a service account of that company (G-013, spec §5.B, R2): the role's owner IS that company.
|
|
407
|
+
* Applies to every actor, root and the org bootstrap operation included (it is the data's shape,
|
|
408
|
+
* not a privilege, as {@link assertOrgUnitsInCompany}); `actor` and `opts` therefore relax nothing.
|
|
409
|
+
* A missing role, an ownerless role, another company's role, or no company (`null`) give the same
|
|
410
|
+
* 400. A non-root actor's reach to the role follows from its reach to the company (the caller
|
|
411
|
+
* reached the membership or account through the subtree wall). Pair with
|
|
412
|
+
* {@link assertCanAssignRoles}. Store outage → 503.
|
|
413
|
+
*/
|
|
414
|
+
async assertRoleAssignable(_actor, roleUuid, companyUuid, _opts) {
|
|
415
|
+
const refuse = () => new identity_errors_js_1.IdentityBadRequestError(role_binding_js_1.ROLE_NOT_IN_COMPANY);
|
|
416
|
+
if (companyUuid === null || companyUuid.length === 0)
|
|
417
|
+
throw refuse();
|
|
418
|
+
const role = await this.roleOwner(roleUuid);
|
|
419
|
+
if (!role.exists || role.owner === null || !(0, http_js_1.sameUuid)(role.owner, companyUuid))
|
|
420
|
+
throw refuse();
|
|
406
421
|
}
|
|
407
422
|
}
|
|
408
423
|
exports.GrantCeiling = GrantCeiling;
|
|
@@ -1,37 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Role
|
|
2
|
+
* Role ownership reads — where a role (`<app>.roles`) belongs, relative to a caller's subtree.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* role" is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* G-013 (spec §5.B, ADR-0047): every role is owned by exactly one company (`r_owner_company_uuid`),
|
|
5
|
+
* so "may this caller administer the role" is "is its owner in the caller's subtree", and "may this
|
|
6
|
+
* role be assigned in company X" is "is X its owner". `NULL` owner = an ownerless legacy row (or a
|
|
7
|
+
* deleted company's role): root only, never assignable. This replaces the D1 binding rule (a role
|
|
8
|
+
* bound by postings in several subtrees, root-only), which no longer applies: a role is never bound
|
|
9
|
+
* outside its owner.
|
|
10
10
|
*
|
|
11
|
-
* ONE definition, shared by grant administration (the `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* ONE definition, shared by grant administration (the `ownedInSubtree` read predicate of the grants
|
|
12
|
+
* summary and the owner rule), role update/delete, postings, service accounts and security policies,
|
|
13
|
+
* via {@link GrantCeiling}. The role id and node ids are `?`-bound (never interpolated).
|
|
14
14
|
*/
|
|
15
15
|
import type { Queryable } from './permission-version.bumper.js';
|
|
16
16
|
import type { IdentitySchemaNames } from '../../schema.js';
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
/** The 400 of a posting or service-account role owned by another company (G-013, spec §10). */
|
|
18
|
+
export declare const ROLE_NOT_IN_COMPANY = "The role is not one of this company's roles.";
|
|
19
|
+
/** A role's owner as stored: `exists` false ⇒ no such role; `owner` null ⇒ ownerless legacy row. */
|
|
20
|
+
export interface RoleOwner {
|
|
21
|
+
readonly exists: boolean;
|
|
22
|
+
readonly owner: string | null;
|
|
21
23
|
}
|
|
22
24
|
/**
|
|
23
|
-
* The
|
|
24
|
-
*
|
|
25
|
-
*
|
|
25
|
+
* The grants-summary READ fragment for a non-root caller (G-013): a role row `r` qualifies only if
|
|
26
|
+
* its owner company is in the caller's subtree (the same owner rule as every other role route;
|
|
27
|
+
* before G-013 it was "posted to a node in the subtree", which hid owned roles nobody held yet). An
|
|
28
|
+
* empty subtree matches nothing. The caller splices `params` where `sql` sits in the query.
|
|
26
29
|
*/
|
|
27
|
-
export declare function
|
|
30
|
+
export declare function ownedInSubtree(nodeIds: readonly string[]): {
|
|
28
31
|
sql: string;
|
|
29
32
|
params: string[];
|
|
30
33
|
};
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
*/
|
|
36
|
-
export declare function readRoleBinding(q: Queryable, roleUuid: string, nodeIds: readonly string[], schemas: IdentitySchemaNames): Promise<RoleBinding>;
|
|
34
|
+
/** Read `roleUuid`'s owner company on `q` (a pool or a transaction connection). */
|
|
35
|
+
export declare function readRoleOwner(q: Queryable, roleUuid: string, schemas: IdentitySchemaNames): Promise<RoleOwner>;
|
|
36
|
+
/** Is the stored role owned by a node of `nodeIds`? An ownerless or missing role never is. */
|
|
37
|
+
export declare function ownerInSubtree(role: RoleOwner, nodeIds: readonly string[]): boolean;
|
|
37
38
|
//# sourceMappingURL=role-binding.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-binding.d.ts","sourceRoot":"","sources":["../../../src/features/authz/role-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"role-binding.d.ts","sourceRoot":"","sources":["../../../src/features/authz/role-binding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAG3D,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,iDAAiD,CAAC;AAElF,oGAAoG;AACpG,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAI5F;AAED,mFAAmF;AACnF,wBAAsB,aAAa,CACjC,CAAC,EAAE,SAAS,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,SAAS,CAAC,CAWpB;AAED,8FAA8F;AAC9F,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAInF"}
|