@astram/identity 0.15.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.
Files changed (129) hide show
  1. package/dist/branch.object.d.ts +9 -1
  2. package/dist/branch.object.d.ts.map +1 -1
  3. package/dist/branch.object.js +14 -1
  4. package/dist/company-branch.object.d.ts +5 -4
  5. package/dist/company-branch.object.d.ts.map +1 -1
  6. package/dist/company-branch.object.js +6 -4
  7. package/dist/company-department.object.d.ts +6 -6
  8. package/dist/company-department.object.d.ts.map +1 -1
  9. package/dist/company-department.object.js +7 -6
  10. package/dist/department.object.d.ts +9 -1
  11. package/dist/department.object.d.ts.map +1 -1
  12. package/dist/department.object.js +14 -1
  13. package/dist/features/access/access-admin.access.d.ts.map +1 -1
  14. package/dist/features/access/access-admin.access.js +8 -1
  15. package/dist/features/access/index.d.ts +1 -0
  16. package/dist/features/access/index.d.ts.map +1 -1
  17. package/dist/features/access/index.js +10 -2
  18. package/dist/features/access/org-structure.access.d.ts +3 -0
  19. package/dist/features/access/org-structure.access.d.ts.map +1 -0
  20. package/dist/features/access/org-structure.access.js +41 -0
  21. package/dist/features/access/user-management.access.d.ts.map +1 -1
  22. package/dist/features/access/user-management.access.js +5 -0
  23. package/dist/features/audit/audit-read.service.d.ts +32 -1
  24. package/dist/features/audit/audit-read.service.d.ts.map +1 -1
  25. package/dist/features/audit/audit-read.service.js +67 -14
  26. package/dist/features/audit/auth-audit-sink.d.ts +19 -0
  27. package/dist/features/audit/auth-audit-sink.d.ts.map +1 -0
  28. package/dist/features/audit/auth-audit-sink.js +24 -0
  29. package/dist/features/audit/directory-label-resolver.d.ts +10 -1
  30. package/dist/features/audit/directory-label-resolver.d.ts.map +1 -1
  31. package/dist/features/audit/directory-label-resolver.js +28 -1
  32. package/dist/features/authz/grant-admin.controller.d.ts +4 -4
  33. package/dist/features/authz/grant-admin.controller.js +4 -4
  34. package/dist/features/authz/grant-admin.service.d.ts +9 -8
  35. package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
  36. package/dist/features/authz/grant-admin.service.js +20 -24
  37. package/dist/features/authz/grant-ceiling.d.ts +35 -16
  38. package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
  39. package/dist/features/authz/grant-ceiling.js +44 -29
  40. package/dist/features/authz/role-binding.d.ts +25 -24
  41. package/dist/features/authz/role-binding.d.ts.map +1 -1
  42. package/dist/features/authz/role-binding.js +31 -44
  43. package/dist/features/identity-feature.module.d.ts +0 -11
  44. package/dist/features/identity-feature.module.d.ts.map +1 -1
  45. package/dist/features/identity-feature.module.js +90 -1
  46. package/dist/features/membership/membership.controller.d.ts.map +1 -1
  47. package/dist/features/membership/membership.controller.js +20 -6
  48. package/dist/features/membership/membership.service.d.ts +16 -10
  49. package/dist/features/membership/membership.service.d.ts.map +1 -1
  50. package/dist/features/membership/membership.service.js +61 -21
  51. package/dist/features/membership/posting.service.d.ts +25 -6
  52. package/dist/features/membership/posting.service.d.ts.map +1 -1
  53. package/dist/features/membership/posting.service.js +63 -16
  54. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +6 -2
  55. package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -1
  56. package/dist/features/org-bootstrap/org-bootstrap-file.js +54 -11
  57. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +30 -11
  58. package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -1
  59. package/dist/features/org-bootstrap/org-bootstrap-plan.js +172 -68
  60. package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -1
  61. package/dist/features/org-bootstrap/org-state.reader.js +56 -49
  62. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +6 -1
  63. package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -1
  64. package/dist/features/org-bootstrap/run-org-bootstrap.js +81 -92
  65. package/dist/features/org-structure/org-catalog.controller.d.ts +29 -6
  66. package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
  67. package/dist/features/org-structure/org-catalog.controller.js +105 -17
  68. package/dist/features/org-structure/org-catalog.service.d.ts +99 -18
  69. package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
  70. package/dist/features/org-structure/org-catalog.service.js +327 -51
  71. package/dist/features/org-structure/org-unit-name.d.ts +31 -0
  72. package/dist/features/org-structure/org-unit-name.d.ts.map +1 -0
  73. package/dist/features/org-structure/org-unit-name.js +50 -0
  74. package/dist/features/role/role.controller.d.ts +28 -13
  75. package/dist/features/role/role.controller.d.ts.map +1 -1
  76. package/dist/features/role/role.controller.js +54 -14
  77. package/dist/features/role/role.service.d.ts +76 -21
  78. package/dist/features/role/role.service.d.ts.map +1 -1
  79. package/dist/features/role/role.service.js +204 -27
  80. package/dist/features/security-policy/security-policy-version-bump.d.ts +41 -0
  81. package/dist/features/security-policy/security-policy-version-bump.d.ts.map +1 -0
  82. package/dist/features/security-policy/security-policy-version-bump.js +65 -0
  83. package/dist/features/security-policy/security-policy.controller.d.ts +51 -0
  84. package/dist/features/security-policy/security-policy.controller.d.ts.map +1 -0
  85. package/dist/features/security-policy/security-policy.controller.js +121 -0
  86. package/dist/features/security-policy/security-policy.service.d.ts +94 -0
  87. package/dist/features/security-policy/security-policy.service.d.ts.map +1 -0
  88. package/dist/features/security-policy/security-policy.service.js +429 -0
  89. package/dist/features/service-account/service-account.service.d.ts +13 -3
  90. package/dist/features/service-account/service-account.service.d.ts.map +1 -1
  91. package/dist/features/service-account/service-account.service.js +48 -10
  92. package/dist/features/service-account/service-account.store.d.ts +11 -1
  93. package/dist/features/service-account/service-account.store.d.ts.map +1 -1
  94. package/dist/features/service-account/service-account.store.js +20 -1
  95. package/dist/features/user/signin-admin.tokens.d.ts +11 -0
  96. package/dist/features/user/signin-admin.tokens.d.ts.map +1 -0
  97. package/dist/features/user/signin-admin.tokens.js +13 -0
  98. package/dist/features/user/user-mfa-status.d.ts +38 -0
  99. package/dist/features/user/user-mfa-status.d.ts.map +1 -0
  100. package/dist/features/user/user-mfa-status.js +47 -0
  101. package/dist/features/user/user-signin-admin.service.d.ts +48 -0
  102. package/dist/features/user/user-signin-admin.service.d.ts.map +1 -0
  103. package/dist/features/user/user-signin-admin.service.js +232 -0
  104. package/dist/features/user/user.controller.d.ts +29 -2
  105. package/dist/features/user/user.controller.d.ts.map +1 -1
  106. package/dist/features/user/user.controller.js +71 -3
  107. package/dist/features/user/user.service.d.ts +40 -14
  108. package/dist/features/user/user.service.d.ts.map +1 -1
  109. package/dist/features/user/user.service.js +143 -10
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +3 -1
  113. package/dist/objects.d.ts.map +1 -1
  114. package/dist/objects.js +2 -0
  115. package/dist/role.object.d.ts +1 -1
  116. package/dist/role.object.d.ts.map +1 -1
  117. package/dist/role.object.js +6 -15
  118. package/dist/security-policy.object.d.ts +43 -0
  119. package/dist/security-policy.object.d.ts.map +1 -0
  120. package/dist/security-policy.object.js +107 -0
  121. package/dist/user.object.d.ts +6 -0
  122. package/dist/user.object.d.ts.map +1 -1
  123. package/dist/user.object.js +13 -1
  124. package/migrations/1710000000102-CreateAuditChain.cjs +71 -0
  125. package/migrations/1710000000103-AddUserLoginName.cjs +87 -0
  126. package/migrations/1710000000104-CreateSecurityPolicy.cjs +90 -0
  127. package/migrations/1710000000105-OwnDepartmentsAndBranches.cjs +327 -0
  128. package/migrations/1710000000106-OwnRolesPerCompany.cjs +622 -0
  129. package/package.json +9 -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;AAEpF;;;;GAIG;AACH,qBAAa,sBAAuB,YAAW,aAAa;IAGxD,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;IA8B/E,OAAO,CAAC,QAAQ;CAMjB"}
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
- // service_account / object_type / permission not shown in v1 columns → safe fallback.
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 role-binding rule (a role
18
- * also used outside the caller's subtree is root-only) and the ADR-0029 ceiling ("only grant or
19
- * revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling and
20
- * binding → 403, a role outside the caller's subtree → 404, unknown key/bundle → 400.
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 role-binding rule (a role
44
- * also used outside the caller's subtree is root-only) and the ADR-0029 ceiling ("only grant or
45
- * revoke what you hold"). Errors map in one place ({@link IdentityErrorInterceptor}): ceiling and
46
- * binding → 403, a role outside the caller's subtree → 404, unknown key/bundle → 400.
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. Task 4.2: subtree-guarded — a non-root caller may read a role's
78
- * grants ONLY IF that role is bound within their reachable subtree; otherwise `{ grants: [] }`
79
- * (fail-closed — never leak another tenant's role's permissions by uuid). Root skips the guard.
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
- * Security hardening #1 (D1 interim rule) + ADR-0029, in this order, BEFORE any write: the role
88
- * must be administrable by the actor (bound inside its subtree and nowhere outside — a shared
89
- * role is root-only), then every key must be covered by the actor's own effective keys ("only
90
- * grant — or revoke — what you hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒
91
- * neither check runs; the identity module always wires it.
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":"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"}
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. Task 4.2: subtree-guarded — a non-root caller may read a role's
95
- * grants ONLY IF that role is bound within their reachable subtree; otherwise `{ grants: [] }`
96
- * (fail-closed — never leak another tenant's role's permissions by uuid). Root skips the guard.
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
- const ph = subtreeScope.nodeIds.map(() => '?').join(', ');
102
- // Empty subtree → no placeholders → the check can't match → fail-closed empty.
103
- const bound = subtreeScope.nodeIds.length
104
- ? (await this.pool.query(`SELECT 1 FROM \`${this.schemas.app}\`.\`membership_posting\` mp
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
- * Security hardening #1 (D1 interim rule) + ADR-0029, in this order, BEFORE any write: the role
128
- * must be administrable by the actor (bound inside its subtree and nowhere outside — a shared
129
- * role is root-only), then every key must be covered by the actor's own effective keys ("only
130
- * grant — or revoke — what you hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒
131
- * neither check runs; the identity module always wires it.
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 (in any tenant).
150
- // Lock the role row FIRST — the posting path locks the same row before it inserts — then
151
- // re-run the binding rule on THIS connection, so a binding committed by a writer we waited
152
- // for is seen, and a role that became shared in the meantime is refused (rolled back).
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.readRoleBinding)(conn, roleUuid, (0, grant_ceiling_js_1.subtreeOf)(actor), this.schemas));
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 bound within their reachable
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.subtreeExists)(subtreeScope.nodeIds, this.schemas);
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 RoleBinding } from './role-binding.js';
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: Principal): readonly string[];
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
- /** The D1 administer rule over a binding read: used inside and nowhere outside (404 / 403). */
71
- static checkAdministrable(b: RoleBinding): void;
72
- /** The #11 assign rule over a binding read: never a role also used outside (403). */
73
- static checkAssignable(b: RoleBinding): void;
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
- * Assert the actor may ADMINISTER the role — edit its grants, rename or delete it (#1, D1
173
- * interim rule): the role is bound inside the actor's subtree AND bound nowhere outside it. A
174
- * role not bound in the subtree is "not found" for the actor (no existence leak); a role also
175
- * used outside (a shared role) is root-only. Root bypasses.
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 actor may POST someone to the role (#11): the role is bound within the actor's
180
- * subtree or not bound at all — never a role also used outside it (a shared role's assignment
181
- * would widen another tenant's role). Pair with {@link assertCanAssignRoles}. Root bypasses.
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(actor: Principal, roleUuid: string, opts?: OperationRoleOptions): Promise<void>;
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":"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"}
1
+ {"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
@@ -87,19 +87,14 @@ class GrantCeiling {
87
87
  }
88
88
  this.rootIds = rootIds ?? [];
89
89
  }
90
- /** The D1 administer rule over a binding read: used inside and nowhere outside (404 / 403). */
91
- static checkAdministrable(b) {
92
- if (!b.inside)
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
- /** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
379
- async roleBinding(actor, roleUuid) {
380
- return this.facts('role binding', () => (0, role_binding_js_1.readRoleBinding)(this.pool, roleUuid, subtreeOf(actor), this.schemas));
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
- * Assert the actor may ADMINISTER the role — edit its grants, rename or delete it (#1, D1
384
- * interim rule): the role is bound inside the actor's subtree AND bound nowhere outside it. A
385
- * role not bound in the subtree is "not found" for the actor (no existence leak); a role also
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 assertRoleAdministrable(actor, roleUuid, opts) {
385
+ async roleOwnedInReach(actor, roleUuid) {
386
+ const role = await this.roleOwner(roleUuid);
389
387
  if (actor.isRoot === true)
390
- return;
391
- if (operationCovers(actor, [roleUuid], opts))
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 POST someone to the role (#11): the role is bound within the actor's
397
- * subtree or not bound at all — never a role also used outside it (a shared role's assignment
398
- * would widen another tenant's role). Pair with {@link assertCanAssignRoles}. Root bypasses.
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 assertRoleAssignable(actor, roleUuid, opts) {
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.checkAssignable(await this.roleBinding(actor, roleUuid));
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 binding — where a GLOBAL role (`shield_app.roles`) is in use, relative to a caller's subtree.
2
+ * Role ownership reads — where a role (`<app>.roles`) belongs, relative to a caller's subtree.
3
3
  *
4
- * Roles are a global catalog, so one `role_permission` edit changes every tenant that uses the role
5
- * (review finding #1). Until roles are tenant-owned (D1 follow-up), "may this caller administer the
6
- * role" is decided by WHERE the role is bound:
7
- * - a human binding: a `membership_posting` on a `company_membership` of company X;
8
- * - a machine binding: a `service_account` of tenant X whose `sa_role_ids` holds the role.
9
- * `inside` = some binding's node is in the caller's subtree; `outside` = some binding's node is not.
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 `subtreeExists` read predicate below and the
12
- * write rule), role update/delete, and posting creation, via {@link GrantCeiling}. All node ids and
13
- * the role id are `?`-bound (never interpolated).
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
- /** Where a role is bound relative to a set of subtree nodes. */
18
- export interface RoleBinding {
19
- readonly inside: boolean;
20
- readonly outside: boolean;
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 shared EXISTS-subtree fragment for a non-root caller's READS (Task 4.2): a role row `r`
24
- * qualifies only if it is bound (`membership_posting → company_membership`) to a node in the
25
- * subtree. The caller splices `params` where `sql` sits in the surrounding query.
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 subtreeExists(nodeIds: readonly string[], schemas: IdentitySchemaNames): {
30
+ export declare function ownedInSubtree(nodeIds: readonly string[]): {
28
31
  sql: string;
29
32
  params: string[];
30
33
  };
31
- /**
32
- * Read where `roleUuid` is bound relative to `nodeIds` (human postings + service accounts) in ONE
33
- * query. An empty `nodeIds` is treated as "nothing is inside" (every binding is outside) — fail
34
- * closed, never an `IN ()` syntax error.
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;AAE3D,gEAAgE;AAChE,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,mBAAmB,GAC3B;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAQnC;AASD;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,CAAC,EAAE,SAAS,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,WAAW,CAAC,CAwBtB"}
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"}