@astram/identity 0.3.1 → 0.5.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/company-membership.object.d.ts.map +1 -1
- package/dist/company-membership.object.js +3 -2
- package/dist/company.object.d.ts.map +1 -1
- package/dist/company.object.js +8 -3
- package/dist/features/access/index.d.ts.map +1 -1
- package/dist/features/access/index.js +1 -5
- package/dist/features/audit/audit-read.service.d.ts.map +1 -1
- package/dist/features/audit/audit-read.service.js +13 -6
- package/dist/features/audit/audit.controller.d.ts.map +1 -1
- package/dist/features/audit/audit.controller.js +2 -0
- package/dist/features/audit/mariadb-audit-reader.d.ts.map +1 -1
- package/dist/features/authz/access-bundle-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.d.ts.map +1 -1
- package/dist/features/authz/catalog-reconcile.job.js +1 -3
- package/dist/features/authz/db-permission-facts.adapter.d.ts +24 -4
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +92 -5
- package/dist/features/authz/grant-admin.controller.d.ts +8 -5
- package/dist/features/authz/grant-admin.controller.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.controller.js +22 -15
- package/dist/features/authz/grant-admin.service.d.ts +57 -11
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +163 -85
- package/dist/features/authz/grant-ceiling.d.ts +111 -0
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -0
- package/dist/features/authz/grant-ceiling.js +248 -0
- package/dist/features/authz/mariadb-catalog-store.d.ts.map +1 -1
- package/dist/features/authz/mariadb-catalog-store.js +15 -3
- package/dist/features/authz/permission-version.bumper.d.ts +45 -1
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
- package/dist/features/authz/permission-version.bumper.js +106 -16
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/role-binding.d.ts +36 -0
- package/dist/features/authz/role-binding.d.ts.map +1 -0
- package/dist/features/authz/role-binding.js +53 -0
- package/dist/features/company/company-hierarchy.service.d.ts +46 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
- package/dist/features/company/company-hierarchy.service.js +100 -0
- package/dist/features/company/company.controller.d.ts +23 -5
- package/dist/features/company/company.controller.d.ts.map +1 -1
- package/dist/features/company/company.controller.js +47 -12
- package/dist/features/company/company.service.d.ts +28 -12
- package/dist/features/company/company.service.d.ts.map +1 -1
- package/dist/features/company/company.service.js +61 -14
- package/dist/features/cross-tenant/cross-tenant-grant.controller.d.ts.map +1 -1
- package/dist/features/cross-tenant/cross-tenant-grant.controller.js +2 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +123 -6
- package/dist/features/membership/membership.controller.d.ts +6 -5
- package/dist/features/membership/membership.controller.d.ts.map +1 -1
- package/dist/features/membership/membership.controller.js +34 -22
- package/dist/features/membership/membership.service.d.ts +20 -14
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +62 -29
- package/dist/features/membership/posting-version-bump.d.ts +5 -0
- package/dist/features/membership/posting-version-bump.d.ts.map +1 -1
- package/dist/features/membership/posting.service.d.ts +20 -24
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +101 -64
- package/dist/features/org-structure/org-catalog.controller.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.controller.js +2 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +4 -1
- package/dist/features/policy/policy.controller.d.ts.map +1 -1
- package/dist/features/policy/policy.controller.js +2 -0
- package/dist/features/role/role.controller.d.ts +3 -2
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +15 -10
- package/dist/features/role/role.service.d.ts +16 -8
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +24 -10
- package/dist/features/scope/scope-closure.service.d.ts +44 -4
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
- package/dist/features/scope/scope-closure.service.js +215 -82
- package/dist/features/service-account/service-account.controller.d.ts +63 -0
- package/dist/features/service-account/service-account.controller.d.ts.map +1 -0
- package/dist/features/service-account/service-account.controller.js +295 -0
- package/dist/features/service-account/service-account.service.d.ts +209 -0
- package/dist/features/service-account/service-account.service.d.ts.map +1 -0
- package/dist/features/service-account/service-account.service.js +528 -0
- package/dist/features/service-account/service-account.store.d.ts +91 -0
- package/dist/features/service-account/service-account.store.d.ts.map +1 -0
- package/dist/features/service-account/service-account.store.js +158 -0
- package/dist/features/share/share.controller.d.ts.map +1 -1
- package/dist/features/share/share.controller.js +2 -0
- package/dist/features/share/share.service.d.ts.map +1 -1
- package/dist/features/shared/identity-error.interceptor.d.ts +24 -0
- package/dist/features/shared/identity-error.interceptor.d.ts.map +1 -0
- package/dist/features/shared/identity-error.interceptor.js +82 -0
- package/dist/features/shared/identity-errors.d.ts +23 -0
- package/dist/features/shared/identity-errors.d.ts.map +1 -0
- package/dist/features/shared/identity-errors.js +41 -0
- package/dist/features/user/session-revocation-check.d.ts +11 -0
- package/dist/features/user/session-revocation-check.d.ts.map +1 -0
- package/dist/features/user/session-revocation-check.js +21 -0
- package/dist/features/user/user.controller.d.ts +11 -6
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +35 -17
- package/dist/features/user/user.service.d.ts +55 -14
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +143 -16
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/membership-posting.object.d.ts.map +1 -1
- package/dist/membership-posting.object.js +4 -2
- package/dist/objects.d.ts.map +1 -1
- package/dist/objects.js +14 -3
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +3 -0
- package/dist/service-account.object.d.ts +31 -0
- package/dist/service-account.object.d.ts.map +1 -0
- package/dist/service-account.object.js +75 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +3 -0
- package/migrations/1710000000018-CreateServiceAccount.cjs +53 -0
- package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
- package/package.json +4 -4
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Pool } from 'mariadb';
|
|
2
|
+
import type { Principal } from '@astram/core';
|
|
3
|
+
import type { AuditPort } from '@astram/engine';
|
|
2
4
|
import { PermissionVersionBumper } from './permission-version.bumper.js';
|
|
3
5
|
import { AccessBundleStore } from './access-bundle-store.js';
|
|
6
|
+
import { GrantCeiling } from './grant-ceiling.js';
|
|
4
7
|
/**
|
|
5
8
|
* The caller's reachable subtree, threaded from `req.principal` (Task 4.2). It scopes the
|
|
6
9
|
* grant-admin reads so a caller sees ONLY roles bound within their subtree — the SAME cached
|
|
@@ -49,8 +52,19 @@ export interface RoleGrantSummary {
|
|
|
49
52
|
export declare class GrantAdminService {
|
|
50
53
|
private readonly pool;
|
|
51
54
|
private readonly bumper;
|
|
55
|
+
private readonly ceiling?;
|
|
56
|
+
private readonly audit?;
|
|
52
57
|
private readonly bundles;
|
|
53
|
-
|
|
58
|
+
/**
|
|
59
|
+
* @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold") plus the
|
|
60
|
+
* security-hardening role-binding rule. Enforced on grant/revoke/grantBundle/revokeBundle for a
|
|
61
|
+
* non-root actor. OPTIONAL for back-compat: when absent (legacy wiring / tests that don't
|
|
62
|
+
* exercise it), neither check runs — the identity module ALWAYS wires it, so production always
|
|
63
|
+
* enforces. Root bypasses regardless.
|
|
64
|
+
* @param audit The grant-administration audit sink (D6): written on the grant's own transaction
|
|
65
|
+
* connection, fail-closed. The identity module ALWAYS wires the engine `MariaDbAuditSink`.
|
|
66
|
+
*/
|
|
67
|
+
constructor(pool: Pool, bumper: PermissionVersionBumper, bundleStore?: AccessBundleStore, ceiling?: GrantCeiling | undefined, audit?: AuditPort | undefined);
|
|
54
68
|
/** All grantable exact keys (from the catalog) + the computed wildcard set. */
|
|
55
69
|
listCatalog(): Promise<{
|
|
56
70
|
catalog: CatalogRow[];
|
|
@@ -67,13 +81,42 @@ export declare class GrantAdminService {
|
|
|
67
81
|
/** Validate the key against the catalog + wildcard set. Throws BadKeyError if unknown. */
|
|
68
82
|
private assertValidKey;
|
|
69
83
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
84
|
+
* Security hardening #1 (D1 interim rule) + ADR-0029, in this order, BEFORE any write: the role
|
|
85
|
+
* must be administrable by the actor (bound inside its subtree and nowhere outside — a shared
|
|
86
|
+
* role is root-only), then every key must be covered by the actor's own effective keys ("only
|
|
87
|
+
* grant — or revoke — what you hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒
|
|
88
|
+
* neither check runs; the identity module always wires it.
|
|
89
|
+
*/
|
|
90
|
+
private assertMayChangeGrants;
|
|
91
|
+
/**
|
|
92
|
+
* Run one grant-administration write in ONE transaction: the `role_permission` statements, the
|
|
93
|
+
* permission-version bump of every tenant using the role, and the audit row(s) (D6 — audited in
|
|
94
|
+
* the SAME transaction, fail-closed: an audit failure rolls the grant back). Store failures map
|
|
95
|
+
* to AuthorizationUnavailableError (503).
|
|
96
|
+
*/
|
|
97
|
+
private writeGrants;
|
|
98
|
+
/**
|
|
99
|
+
* Write the grant-administration audit row(s) on the transaction connection (D6). One row per
|
|
100
|
+
* distinct tenant among the actor's tenant and every tenant the change bumped, so each affected
|
|
101
|
+
* tenant's auditor sees it. Names, never ids, in the detail (ADR-0012): the role name and the
|
|
102
|
+
* bumped tenants' names; the viewer resolves the actor and row tenant itself. No sink wired
|
|
103
|
+
* (legacy/tests) ⇒ nothing is written.
|
|
104
|
+
*/
|
|
105
|
+
private auditGrantChange;
|
|
106
|
+
/**
|
|
107
|
+
* Grant a key to a role (idempotent) + bump the version + audit, atomically. The `grantor`
|
|
108
|
+
* Principal stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a
|
|
109
|
+
* no-op and its existing stamp is left as-is). Order: catalog validation (400) → role binding
|
|
110
|
+
* (404/403) → ceiling (403) → write. Root bypasses the binding rule and the ceiling.
|
|
111
|
+
*/
|
|
112
|
+
grant(roleUuid: string, key: string, grantor: Principal): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Revoke a key from a role (no-op if absent) + bump + audit, atomically. Security hardening #1:
|
|
115
|
+
* the actor must be able to administer the role (binding rule) and must hold the key itself —
|
|
116
|
+
* "only revoke what you hold" — so a delegated admin can no longer strip a role of authority it
|
|
117
|
+
* never had. Root bypasses.
|
|
73
118
|
*/
|
|
74
|
-
|
|
75
|
-
/** Revoke a key from a role (no-op if absent) + bump the version, atomically. */
|
|
76
|
-
revoke(roleUuid: string, key: string): Promise<void>;
|
|
119
|
+
revoke(roleUuid: string, key: string, actor: Principal): Promise<void>;
|
|
77
120
|
/** All active bundles + their resolved atomic keys, for the Access UI. */
|
|
78
121
|
listBundles(): Promise<{
|
|
79
122
|
bundles: BundleView[];
|
|
@@ -82,15 +125,18 @@ export declare class GrantAdminService {
|
|
|
82
125
|
* Grant a bundle to a role: resolve the bundle → its atomic keys, then INSERT each
|
|
83
126
|
* into `role_permission` idempotently in ONE transaction with ONE version bump.
|
|
84
127
|
* An unknown / orphaned bundle → `BadKeyError` (mapped to 400 by the controller).
|
|
128
|
+
* ADR-0029: a non-root `grantor` must effectively hold EVERY atomic key the bundle expands
|
|
129
|
+
* to; otherwise `GrantCeilingError` (403), checked before any write. Root bypasses.
|
|
85
130
|
*/
|
|
86
|
-
grantBundle(roleUuid: string, bundleKey: string,
|
|
131
|
+
grantBundle(roleUuid: string, bundleKey: string, grantor: Principal): Promise<void>;
|
|
87
132
|
/**
|
|
88
133
|
* Revoke a bundle from a role: resolve → DELETE each of the bundle's atomic keys, in
|
|
89
|
-
* ONE transaction with ONE version bump. Caveat (design §5.1): this removes exactly the
|
|
134
|
+
* ONE transaction with ONE version bump + audit. Caveat (design §5.1): this removes exactly the
|
|
90
135
|
* bundle's keys even if a manual grant or another bundle shares one — the model is
|
|
91
|
-
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`.
|
|
136
|
+
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`. Security hardening
|
|
137
|
+
* #1: the binding rule + "only revoke what you hold" apply (root bypasses).
|
|
92
138
|
*/
|
|
93
|
-
revokeBundle(roleUuid: string, bundleKey: string): Promise<void>;
|
|
139
|
+
revokeBundle(roleUuid: string, bundleKey: string, actor: Principal): Promise<void>;
|
|
94
140
|
/** Resolve a bundle key → its atomic keys; throw BadKeyError when unknown/orphaned. */
|
|
95
141
|
private resolveBundleKeysOrThrow;
|
|
96
142
|
/**
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"grant-admin.service.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-admin.service.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,23 +4,11 @@ exports.GrantAdminService = exports.BadKeyError = void 0;
|
|
|
4
4
|
const core_1 = require("@astram/core");
|
|
5
5
|
const access_bundle_store_js_1 = require("./access-bundle-store.js");
|
|
6
6
|
const db_permission_facts_adapter_js_1 = require("./db-permission-facts.adapter.js");
|
|
7
|
+
const grant_ceiling_js_1 = require("./grant-ceiling.js");
|
|
8
|
+
const role_binding_js_1 = require("./role-binding.js");
|
|
9
|
+
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
7
10
|
const AUTHZ = 'shield_authz';
|
|
8
11
|
const APP = 'shield_app';
|
|
9
|
-
/**
|
|
10
|
-
* Build the shared EXISTS-subtree fragment for a non-root caller. A role qualifies only if it
|
|
11
|
-
* is bound (`membership_posting → company_membership`) to a node in the caller's subtree. All
|
|
12
|
-
* node ids are `?`-bound (never interpolated → injection-free); the caller splices the returned
|
|
13
|
-
* `params` at the position where `sql` sits in the surrounding query. Root / empty scope callers
|
|
14
|
-
* are handled by the caller (root ⇒ no fragment; the engine guarantees non-empty subtrees).
|
|
15
|
-
*/
|
|
16
|
-
function subtreeExists(nodeIds) {
|
|
17
|
-
const ph = nodeIds.map(() => '?').join(', ');
|
|
18
|
-
const sql = ` AND EXISTS (` +
|
|
19
|
-
`SELECT 1 FROM \`${APP}\`.\`membership_posting\` mp` +
|
|
20
|
-
` JOIN \`${APP}\`.\`company_membership\` cm ON cm.cm_uuid = mp.mp_membership_uuid` +
|
|
21
|
-
` WHERE mp.mp_role_uuid = r.r_uuid AND cm.cm_company_uuid IN (${ph}))`;
|
|
22
|
-
return { sql, params: [...nodeIds] };
|
|
23
|
-
}
|
|
24
12
|
/** A granted key was not in the catalog and is not a recognized wildcard. The
|
|
25
13
|
* controller maps this to HTTP 400 (distinct from a store outage → 503). */
|
|
26
14
|
class BadKeyError extends Error {
|
|
@@ -33,10 +21,23 @@ exports.BadKeyError = BadKeyError;
|
|
|
33
21
|
class GrantAdminService {
|
|
34
22
|
pool;
|
|
35
23
|
bumper;
|
|
24
|
+
ceiling;
|
|
25
|
+
audit;
|
|
36
26
|
bundles;
|
|
37
|
-
|
|
27
|
+
/**
|
|
28
|
+
* @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold") plus the
|
|
29
|
+
* security-hardening role-binding rule. Enforced on grant/revoke/grantBundle/revokeBundle for a
|
|
30
|
+
* non-root actor. OPTIONAL for back-compat: when absent (legacy wiring / tests that don't
|
|
31
|
+
* exercise it), neither check runs — the identity module ALWAYS wires it, so production always
|
|
32
|
+
* enforces. Root bypasses regardless.
|
|
33
|
+
* @param audit The grant-administration audit sink (D6): written on the grant's own transaction
|
|
34
|
+
* connection, fail-closed. The identity module ALWAYS wires the engine `MariaDbAuditSink`.
|
|
35
|
+
*/
|
|
36
|
+
constructor(pool, bumper, bundleStore, ceiling, audit) {
|
|
38
37
|
this.pool = pool;
|
|
39
38
|
this.bumper = bumper;
|
|
39
|
+
this.ceiling = ceiling;
|
|
40
|
+
this.audit = audit;
|
|
40
41
|
this.bundles = bundleStore ?? new access_bundle_store_js_1.AccessBundleStore(pool);
|
|
41
42
|
}
|
|
42
43
|
/** All grantable exact keys (from the catalog) + the computed wildcard set. */
|
|
@@ -52,8 +53,11 @@ class GrantAdminService {
|
|
|
52
53
|
throw new core_1.AuthorizationUnavailableError('permission catalog unavailable', cause);
|
|
53
54
|
}
|
|
54
55
|
const catalog = rows.map((r) => ({
|
|
55
|
-
key: r.perm_key,
|
|
56
|
-
|
|
56
|
+
key: r.perm_key,
|
|
57
|
+
label: r.perm_label,
|
|
58
|
+
module: r.perm_module,
|
|
59
|
+
typeKey: r.perm_type_key,
|
|
60
|
+
action: r.perm_action,
|
|
57
61
|
}));
|
|
58
62
|
// Wildcards: module-wide + one per distinct type key.
|
|
59
63
|
const modules = [...new Set(catalog.map((c) => c.module))];
|
|
@@ -98,52 +102,142 @@ class GrantAdminService {
|
|
|
98
102
|
throw new BadKeyError(key);
|
|
99
103
|
}
|
|
100
104
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
105
|
+
* Security hardening #1 (D1 interim rule) + ADR-0029, in this order, BEFORE any write: the role
|
|
106
|
+
* must be administrable by the actor (bound inside its subtree and nowhere outside — a shared
|
|
107
|
+
* role is root-only), then every key must be covered by the actor's own effective keys ("only
|
|
108
|
+
* grant — or revoke — what you hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒
|
|
109
|
+
* neither check runs; the identity module always wires it.
|
|
104
110
|
*/
|
|
105
|
-
async
|
|
106
|
-
|
|
111
|
+
async assertMayChangeGrants(roleUuid, keys, actor) {
|
|
112
|
+
if (!this.ceiling)
|
|
113
|
+
return;
|
|
114
|
+
await this.ceiling.assertRoleAdministrable(actor, roleUuid);
|
|
115
|
+
await this.ceiling.assertCanGrant(actor, keys);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run one grant-administration write in ONE transaction: the `role_permission` statements, the
|
|
119
|
+
* permission-version bump of every tenant using the role, and the audit row(s) (D6 — audited in
|
|
120
|
+
* the SAME transaction, fail-closed: an audit failure rolls the grant back). Store failures map
|
|
121
|
+
* to AuthorizationUnavailableError (503).
|
|
122
|
+
*/
|
|
123
|
+
async writeGrants(action, roleUuid, keys, actor, statement, extra = {}) {
|
|
107
124
|
const conn = await this.pool.getConnection();
|
|
108
125
|
try {
|
|
109
126
|
await conn.beginTransaction();
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
WHERE
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
127
|
+
// Security review F2: serialise against a concurrent posting of this role (in any tenant).
|
|
128
|
+
// Lock the role row FIRST — the posting path locks the same row before it inserts — then
|
|
129
|
+
// re-run the binding rule on THIS connection, so a binding committed by a writer we waited
|
|
130
|
+
// for is seen, and a role that became shared in the meantime is refused (rolled back).
|
|
131
|
+
await conn.query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_uuid = ? FOR UPDATE`, [
|
|
132
|
+
roleUuid,
|
|
133
|
+
]);
|
|
134
|
+
if (this.ceiling && actor.isRoot !== true) {
|
|
135
|
+
grant_ceiling_js_1.GrantCeiling.checkAdministrable(await (0, role_binding_js_1.readRoleBinding)(conn, roleUuid, (0, grant_ceiling_js_1.subtreeOf)(actor)));
|
|
136
|
+
}
|
|
137
|
+
for (const key of keys)
|
|
138
|
+
await statement(conn, key);
|
|
139
|
+
const bumped = await this.bumper.bumpForRole(roleUuid, conn);
|
|
140
|
+
await this.auditGrantChange(conn, action, roleUuid, keys, actor, bumped, extra);
|
|
118
141
|
await conn.commit();
|
|
142
|
+
// ADR-0035: the committed versions reach Redis now (gate 5 reads them there).
|
|
143
|
+
await this.bumper.publishCommitted(conn);
|
|
119
144
|
}
|
|
120
145
|
catch (cause) {
|
|
121
146
|
await conn.rollback();
|
|
122
|
-
|
|
147
|
+
this.bumper.discardPending(conn);
|
|
148
|
+
if (cause instanceof BadKeyError ||
|
|
149
|
+
cause instanceof identity_errors_js_1.IdentityForbiddenError ||
|
|
150
|
+
cause instanceof identity_errors_js_1.IdentityNotFoundError) {
|
|
123
151
|
throw cause;
|
|
124
|
-
|
|
152
|
+
}
|
|
153
|
+
throw new core_1.AuthorizationUnavailableError(`${action} failed`, cause);
|
|
125
154
|
}
|
|
126
155
|
finally {
|
|
127
156
|
await conn.release();
|
|
128
157
|
}
|
|
129
158
|
}
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
/**
|
|
160
|
+
* Write the grant-administration audit row(s) on the transaction connection (D6). One row per
|
|
161
|
+
* distinct tenant among the actor's tenant and every tenant the change bumped, so each affected
|
|
162
|
+
* tenant's auditor sees it. Names, never ids, in the detail (ADR-0012): the role name and the
|
|
163
|
+
* bumped tenants' names; the viewer resolves the actor and row tenant itself. No sink wired
|
|
164
|
+
* (legacy/tests) ⇒ nothing is written.
|
|
165
|
+
*/
|
|
166
|
+
async auditGrantChange(conn, action, roleUuid, keys, actor, bumpedTenants, extra) {
|
|
167
|
+
if (!this.audit)
|
|
168
|
+
return;
|
|
169
|
+
const query = (sql, params) => conn.query(sql, params);
|
|
170
|
+
const roleRows = (await query(`SELECT r_name FROM \`${APP}\`.\`roles\` WHERE r_uuid = ? LIMIT 1`, [roleUuid]));
|
|
171
|
+
const tenants = [...new Set(bumpedTenants)];
|
|
172
|
+
const nameOf = new Map();
|
|
173
|
+
if (tenants.length > 0) {
|
|
174
|
+
const ph = tenants.map(() => '?').join(', ');
|
|
175
|
+
const rows = (await query(`SELECT c_uuid, c_name FROM \`${APP}\`.\`companies\` WHERE c_uuid IN (${ph})`, tenants));
|
|
176
|
+
for (const r of rows)
|
|
177
|
+
nameOf.set(r.c_uuid, r.c_name);
|
|
142
178
|
}
|
|
143
|
-
|
|
144
|
-
|
|
179
|
+
const labelOf = (t) => nameOf.get(t) ?? '(deleted organisation)';
|
|
180
|
+
const bumpedSet = new Set(tenants);
|
|
181
|
+
// Security review F1: each row is filed under ONE tenant and names ONLY that tenant — a
|
|
182
|
+
// tenant's auditor must never learn which OTHER tenants use the same (shared) role. The one
|
|
183
|
+
// exception is root's own row, filed under a tenant the change did NOT bump, which may carry
|
|
184
|
+
// the full list (root administers across tenants).
|
|
185
|
+
const tenantsFor = (rowTenant) => {
|
|
186
|
+
if (bumpedSet.has(rowTenant))
|
|
187
|
+
return [labelOf(rowTenant)];
|
|
188
|
+
return actor.isRoot === true ? tenants.map(labelOf) : [];
|
|
189
|
+
};
|
|
190
|
+
const occurredAtMs = Date.now();
|
|
191
|
+
for (const tenantId of new Set([actor.tenantId, ...tenants])) {
|
|
192
|
+
const tenantNames = tenantsFor(tenantId);
|
|
193
|
+
await this.audit.record({
|
|
194
|
+
type: 'admin_action',
|
|
195
|
+
occurredAtMs,
|
|
196
|
+
actorId: actor.id,
|
|
197
|
+
tenantId,
|
|
198
|
+
action: { key: action },
|
|
199
|
+
attributes: {
|
|
200
|
+
eventType: 'role_grant',
|
|
201
|
+
role: roleRows[0]?.r_name ?? '(deleted role)',
|
|
202
|
+
keys: [...keys],
|
|
203
|
+
tenants: tenantNames,
|
|
204
|
+
...extra,
|
|
205
|
+
},
|
|
206
|
+
}, query);
|
|
145
207
|
}
|
|
146
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* Grant a key to a role (idempotent) + bump the version + audit, atomically. The `grantor`
|
|
211
|
+
* Principal stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a
|
|
212
|
+
* no-op and its existing stamp is left as-is). Order: catalog validation (400) → role binding
|
|
213
|
+
* (404/403) → ceiling (403) → write. Root bypasses the binding rule and the ceiling.
|
|
214
|
+
*/
|
|
215
|
+
async grant(roleUuid, key, grantor) {
|
|
216
|
+
await this.assertValidKey(key); // throws BadKeyError (400) before touching the store
|
|
217
|
+
await this.assertMayChangeGrants(roleUuid, [key], grantor);
|
|
218
|
+
const actorUuid = grantor.id;
|
|
219
|
+
await this.writeGrants('role.permission_granted', roleUuid, [key], grantor, async (conn, k) => {
|
|
220
|
+
// Idempotent insert — UNIQUE (rp_role_uuid, rp_perm_key); a duplicate is a no-op.
|
|
221
|
+
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`role_permission\`
|
|
222
|
+
(rp_uuid, rp_role_uuid, rp_perm_key, rp_granted_at, rp_granted_by)
|
|
223
|
+
SELECT UUID(), ?, ?, UTC_TIMESTAMP(), ?
|
|
224
|
+
WHERE NOT EXISTS (
|
|
225
|
+
SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?
|
|
226
|
+
)`, [roleUuid, k, actorUuid ?? null, roleUuid, k]);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Revoke a key from a role (no-op if absent) + bump + audit, atomically. Security hardening #1:
|
|
231
|
+
* the actor must be able to administer the role (binding rule) and must hold the key itself —
|
|
232
|
+
* "only revoke what you hold" — so a delegated admin can no longer strip a role of authority it
|
|
233
|
+
* never had. Root bypasses.
|
|
234
|
+
*/
|
|
235
|
+
async revoke(roleUuid, key, actor) {
|
|
236
|
+
await this.assertMayChangeGrants(roleUuid, [key], actor);
|
|
237
|
+
await this.writeGrants('role.permission_revoked', roleUuid, [key], actor, async (conn, k) => {
|
|
238
|
+
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, k]);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
147
241
|
// ── Access bundles ────────────────────────────────────────────────────────
|
|
148
242
|
/** All active bundles + their resolved atomic keys, for the Access UI. */
|
|
149
243
|
async listBundles() {
|
|
@@ -159,56 +253,38 @@ class GrantAdminService {
|
|
|
159
253
|
* Grant a bundle to a role: resolve the bundle → its atomic keys, then INSERT each
|
|
160
254
|
* into `role_permission` idempotently in ONE transaction with ONE version bump.
|
|
161
255
|
* An unknown / orphaned bundle → `BadKeyError` (mapped to 400 by the controller).
|
|
256
|
+
* ADR-0029: a non-root `grantor` must effectively hold EVERY atomic key the bundle expands
|
|
257
|
+
* to; otherwise `GrantCeilingError` (403), checked before any write. Root bypasses.
|
|
162
258
|
*/
|
|
163
|
-
async grantBundle(roleUuid, bundleKey,
|
|
259
|
+
async grantBundle(roleUuid, bundleKey, grantor) {
|
|
164
260
|
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
261
|
+
// Binding rule + ADR-0029 ceiling: a non-root grantor must hold EVERY atomic key the bundle
|
|
262
|
+
// expands to — checked before any write (no partial bundle escalation).
|
|
263
|
+
await this.assertMayChangeGrants(roleUuid, [...keys], grantor);
|
|
264
|
+
const actorUuid = grantor.id;
|
|
265
|
+
await this.writeGrants('role.bundle_granted', roleUuid, keys, grantor, async (conn, key) => {
|
|
266
|
+
// Idempotent — UNIQUE (rp_role_uuid, rp_perm_key); a duplicate is a no-op.
|
|
267
|
+
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`role_permission\`
|
|
171
268
|
(rp_uuid, rp_role_uuid, rp_perm_key, rp_granted_at, rp_granted_by)
|
|
172
269
|
SELECT UUID(), ?, ?, UTC_TIMESTAMP(), ?
|
|
173
270
|
WHERE NOT EXISTS (
|
|
174
271
|
SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?
|
|
175
272
|
)`, [roleUuid, key, actorUuid ?? null, roleUuid, key]);
|
|
176
|
-
|
|
177
|
-
await this.bumper.bumpForRole(roleUuid, conn);
|
|
178
|
-
await conn.commit();
|
|
179
|
-
}
|
|
180
|
-
catch (cause) {
|
|
181
|
-
await conn.rollback();
|
|
182
|
-
throw new core_1.AuthorizationUnavailableError('grant bundle failed', cause);
|
|
183
|
-
}
|
|
184
|
-
finally {
|
|
185
|
-
await conn.release();
|
|
186
|
-
}
|
|
273
|
+
}, { bundle: bundleKey });
|
|
187
274
|
}
|
|
188
275
|
/**
|
|
189
276
|
* Revoke a bundle from a role: resolve → DELETE each of the bundle's atomic keys, in
|
|
190
|
-
* ONE transaction with ONE version bump. Caveat (design §5.1): this removes exactly the
|
|
277
|
+
* ONE transaction with ONE version bump + audit. Caveat (design §5.1): this removes exactly the
|
|
191
278
|
* bundle's keys even if a manual grant or another bundle shares one — the model is
|
|
192
|
-
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`.
|
|
279
|
+
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`. Security hardening
|
|
280
|
+
* #1: the binding rule + "only revoke what you hold" apply (root bypasses).
|
|
193
281
|
*/
|
|
194
|
-
async revokeBundle(roleUuid, bundleKey) {
|
|
282
|
+
async revokeBundle(roleUuid, bundleKey, actor) {
|
|
195
283
|
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
await conn.
|
|
199
|
-
|
|
200
|
-
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, key]);
|
|
201
|
-
}
|
|
202
|
-
await this.bumper.bumpForRole(roleUuid, conn);
|
|
203
|
-
await conn.commit();
|
|
204
|
-
}
|
|
205
|
-
catch (cause) {
|
|
206
|
-
await conn.rollback();
|
|
207
|
-
throw new core_1.AuthorizationUnavailableError('revoke bundle failed', cause);
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
await conn.release();
|
|
211
|
-
}
|
|
284
|
+
await this.assertMayChangeGrants(roleUuid, [...keys], actor);
|
|
285
|
+
await this.writeGrants('role.bundle_revoked', roleUuid, keys, actor, async (conn, key) => {
|
|
286
|
+
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, key]);
|
|
287
|
+
}, { bundle: bundleKey });
|
|
212
288
|
}
|
|
213
289
|
/** Resolve a bundle key → its atomic keys; throw BadKeyError when unknown/orphaned. */
|
|
214
290
|
async resolveBundleKeysOrThrow(bundleKey) {
|
|
@@ -251,7 +327,9 @@ class GrantAdminService {
|
|
|
251
327
|
// subtree (the EXISTS predicate); root spans all tenants (no predicate). The scope clause
|
|
252
328
|
// sits FIRST in the WHERE (before the LIKE), so its ?-bound node params lead the arg list —
|
|
253
329
|
// identical on BOTH the count and page queries so they can never disagree.
|
|
254
|
-
const scope = subtreeScope.isRoot
|
|
330
|
+
const scope = subtreeScope.isRoot
|
|
331
|
+
? { sql: '', params: [] }
|
|
332
|
+
: (0, role_binding_js_1.subtreeExists)(subtreeScope.nodeIds);
|
|
255
333
|
// Distinct granted roles (a role appears iff it has ≥1 role_permission row), filtered
|
|
256
334
|
// by name search. Bind the LIKE term as a param (wildcards escaped).
|
|
257
335
|
const likeClause = search ? `AND r.r_name LIKE ?` : '';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grant-admin self-escalation ceiling (ADR-0029) — "only grant what you hold".
|
|
3
|
+
*
|
|
4
|
+
* A non-root grantor may only grant a role a permission key the grantor THEMSELVES effectively
|
|
5
|
+
* holds. This module provides:
|
|
6
|
+
* • `coversKey` — a PURE predicate: does an effective key set cover a granted key (exact or
|
|
7
|
+
* wildcard)? A wildcard is covered only if EVERY registered exact key it expands to is held.
|
|
8
|
+
* • `GrantCeiling` — resolves a grantor's effective keys (their roles' `role_permission` rows in
|
|
9
|
+
* their tenant, wildcards expanded) and asserts coverage; throws `GrantCeilingError` (→ 403) on
|
|
10
|
+
* violation, `AuthorizationUnavailableError` (→ 503) on a store outage (fail-closed).
|
|
11
|
+
*
|
|
12
|
+
* Reuses the SAME `expandGrantKeys` + `readRoleIdsForTenant` the object-type gate uses — one
|
|
13
|
+
* definition of "what a principal holds". Root (strict `isRoot === true`) bypasses.
|
|
14
|
+
*/
|
|
15
|
+
import type { Pool } from 'mariadb';
|
|
16
|
+
import type { ModuleRegistry, Principal } from '@astram/core';
|
|
17
|
+
import { type RoleBinding } from './role-binding.js';
|
|
18
|
+
/** The grantor exceeds their own authority — cannot grant a key they do not hold (→ 403). */
|
|
19
|
+
export declare class GrantCeilingError extends Error {
|
|
20
|
+
constructor(key: string, message?: string);
|
|
21
|
+
}
|
|
22
|
+
/** The actor's reachable subtree: the session's cached closure, else the self node (non-widening). */
|
|
23
|
+
export declare function subtreeOf(actor: Principal): readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Does `effective` (the grantor's held EXACT keys) cover `granted`?
|
|
26
|
+
*
|
|
27
|
+
* - An EXACT key is covered iff it is in `effective`.
|
|
28
|
+
* - A WILDCARD key (`module:*`, `module:type:*`, bare `module`) is covered iff EVERY registered
|
|
29
|
+
* exact key it expands to is in `effective` — you cannot grant a wildcard you don't fully hold.
|
|
30
|
+
* A wildcard that expands to ZERO registered keys is treated as NOT covered (nothing to hold ⇒
|
|
31
|
+
* no basis to grant; also prevents a typo'd wildcard from vacuously passing).
|
|
32
|
+
*
|
|
33
|
+
* `effective` is assumed already expanded to exact keys (the grantor's wildcards resolved).
|
|
34
|
+
* PURE — no I/O; `registeredKeys` is the live registry's exact-key universe.
|
|
35
|
+
*/
|
|
36
|
+
export declare function coversKey(effective: ReadonlySet<string>, registeredKeys: readonly string[], granted: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Resolves a grantor's effective keys and enforces the ADR-0029 ceiling. Constructed in the
|
|
39
|
+
* identity module over the shared authz pool + the identity registry.
|
|
40
|
+
*/
|
|
41
|
+
export declare class GrantCeiling {
|
|
42
|
+
private readonly pool;
|
|
43
|
+
private readonly registry;
|
|
44
|
+
/** Injectable clock (epoch-ms) for the posting effective-window read; defaults to Date.now. */
|
|
45
|
+
private readonly now;
|
|
46
|
+
private readonly rootIds;
|
|
47
|
+
constructor(pool: Pool, registry: ModuleRegistry,
|
|
48
|
+
/**
|
|
49
|
+
* The platform root allowlist (the SAME `rootIds` given to `ShieldModule.forRoot`) — REQUIRED,
|
|
50
|
+
* no default (security review F5): pass the array, or `null` to state explicitly that it is
|
|
51
|
+
* not configured. A root user is never dominated by a non-root actor; independently of this
|
|
52
|
+
* list, a user flagged `u_is_platform_superuser` in the database is never dominated either.
|
|
53
|
+
*/
|
|
54
|
+
rootIds: readonly string[] | null,
|
|
55
|
+
/** Injectable clock (epoch-ms) for the posting effective-window read; defaults to Date.now. */
|
|
56
|
+
now?: () => number);
|
|
57
|
+
/** The D1 administer rule over a binding read: used inside and nowhere outside (404 / 403). */
|
|
58
|
+
static checkAdministrable(b: RoleBinding): void;
|
|
59
|
+
/** The #11 assign rule over a binding read: never a role also used outside (403). */
|
|
60
|
+
static checkAssignable(b: RoleBinding): void;
|
|
61
|
+
/** The registry's exact-key universe (wildcard expansion bound). */
|
|
62
|
+
private registeredKeys;
|
|
63
|
+
/** The raw `role_permission` keys of `roleIds` (exact or wildcard). Store errors propagate. */
|
|
64
|
+
private rawKeysOfRoles;
|
|
65
|
+
/** Run a store read, mapping any non-domain failure to AuthorizationUnavailableError (503). */
|
|
66
|
+
private facts;
|
|
67
|
+
/**
|
|
68
|
+
* The grantor's EFFECTIVE exact keys: the keys granted to the grantor's roles (active
|
|
69
|
+
* membership's in-effect postings in the grantor's tenant), wildcards expanded against the
|
|
70
|
+
* live registry. Fail-closed: a store outage throws AuthorizationUnavailableError (never an
|
|
71
|
+
* empty set masking an outage).
|
|
72
|
+
*/
|
|
73
|
+
effectiveKeys(grantor: Principal): Promise<Set<string>>;
|
|
74
|
+
/**
|
|
75
|
+
* Assert the grantor may grant EVERY key in `keys`. Root (strict) bypasses. A non-root grantor
|
|
76
|
+
* must effectively hold each key (exact) or fully hold each wildcard's expansion. Throws
|
|
77
|
+
* `GrantCeilingError` on the FIRST key that exceeds the grantor's authority.
|
|
78
|
+
*/
|
|
79
|
+
assertCanGrant(grantor: Principal, keys: readonly string[]): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Assert the actor may ASSIGN every role in `roleIds` (post someone to it): the roles' effective
|
|
82
|
+
* keys (their `role_permission` rows, wildcards expanded against the registry) must all be held
|
|
83
|
+
* by the actor — the same "only grant what you hold" ceiling, applied to role assignment (#11).
|
|
84
|
+
* A role holding nothing assigns nothing and passes. Root bypasses. Store outage → 503.
|
|
85
|
+
*/
|
|
86
|
+
assertCanAssignRoles(actor: Principal, roleIds: readonly string[]): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Assert the actor DOMINATES the target user (#3, D2): every membership the target holds (in any
|
|
89
|
+
* tenant, active or not) is inside the actor's subtree, AND every key the target could hold
|
|
90
|
+
* through any of its postings (any membership, any window — potential authority, not just what is
|
|
91
|
+
* in effect now) is held by the actor. A platform root target is never dominated by a non-root.
|
|
92
|
+
* Root actors bypass. Store outage → 503.
|
|
93
|
+
*/
|
|
94
|
+
assertDominates(actor: Principal, targetUserId: string): Promise<void>;
|
|
95
|
+
/** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
|
|
96
|
+
roleBinding(actor: Principal, roleUuid: string): Promise<RoleBinding>;
|
|
97
|
+
/**
|
|
98
|
+
* Assert the actor may ADMINISTER the role — edit its grants, rename or delete it (#1, D1
|
|
99
|
+
* interim rule): the role is bound inside the actor's subtree AND bound nowhere outside it. A
|
|
100
|
+
* role not bound in the subtree is "not found" for the actor (no existence leak); a role also
|
|
101
|
+
* used outside (a shared role) is root-only. Root bypasses.
|
|
102
|
+
*/
|
|
103
|
+
assertRoleAdministrable(actor: Principal, roleUuid: string): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Assert the actor may POST someone to the role (#11): the role is bound within the actor's
|
|
106
|
+
* subtree or not bound at all — never a role also used outside it (a shared role's assignment
|
|
107
|
+
* would widen another tenant's role). Pair with {@link assertCanAssignRoles}. Root bypasses.
|
|
108
|
+
*/
|
|
109
|
+
assertRoleAssignable(actor: Principal, roleUuid: string): Promise<void>;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=grant-ceiling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9D,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAMtE,6FAA6F;AAC7F,qBAAa,iBAAkB,SAAQ,KAAK;gBAC9B,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAI1C;AAED,sGAAsG;AACtG,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,MAAM,EAAE,CAE7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CACvB,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,EAC9B,cAAc,EAAE,SAAS,MAAM,EAAE,EACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAST;AAED;;;GAGG;AACH,qBAAa,YAAY;IAIrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAQzB,+FAA+F;IAC/F,OAAO,CAAC,QAAQ,CAAC,GAAG;IAbtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;gBAGzB,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,cAAc;IACzC;;;;;OAKG;IACH,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI;IACjC,+FAA+F;IAC9E,GAAG,GAAE,MAAM,MAAiB;IAU/C,+FAA+F;IAC/F,MAAM,CAAC,kBAAkB,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI;IAS/C,qFAAqF;IACrF,MAAM,CAAC,eAAe,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI;IAQ5C,oEAAoE;IACpE,OAAO,CAAC,cAAc;IAItB,+FAA+F;YACjF,cAAc;IAU5B,+FAA+F;YACjF,KAAK;IASnB;;;;;OAKG;IACG,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAkB7D;;;;OAIG;IACG,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAahF;;;;;OAKG;IACG,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBvF;;;;;;OAMG;IACG,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwD5E,+FAA+F;IACzF,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAI3E;;;;;OAKG;IACG,uBAAuB,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKhF;;;;OAIG;IACG,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAI9E"}
|