@astram/identity 0.4.0 → 0.6.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 +16 -0
- package/dist/features/authz/db-permission-facts.adapter.d.ts.map +1 -1
- package/dist/features/authz/db-permission-facts.adapter.js +46 -2
- 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 +20 -18
- package/dist/features/authz/grant-admin.service.d.ts +72 -16
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +232 -86
- package/dist/features/authz/grant-ceiling.d.ts +100 -1
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +264 -2
- 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 +52 -1
- package/dist/features/authz/permission-version.bumper.d.ts.map +1 -1
- package/dist/features/authz/permission-version.bumper.js +119 -20
- package/dist/features/authz/record-authorization.d.ts +12 -0
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +49 -0
- 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 +108 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -0
- package/dist/features/company/company-hierarchy.service.js +277 -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 +2 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +72 -5
- 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 +12 -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 +39 -8
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +77 -10
- package/dist/features/scope/scope-closure.service.d.ts +54 -4
- package/dist/features/scope/scope-closure.service.d.ts.map +1 -1
- package/dist/features/scope/scope-closure.service.js +237 -85
- package/dist/features/service-account/service-account.controller.d.ts +3 -2
- package/dist/features/service-account/service-account.controller.d.ts.map +1 -1
- package/dist/features/service-account/service-account.controller.js +29 -15
- package/dist/features/service-account/service-account.service.d.ts +29 -7
- package/dist/features/service-account/service-account.service.d.ts.map +1 -1
- package/dist/features/service-account/service-account.service.js +73 -12
- package/dist/features/service-account/service-account.store.d.ts +15 -0
- package/dist/features/service-account/service-account.store.d.ts.map +1 -1
- package/dist/features/service-account/service-account.store.js +29 -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 +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/internal.d.ts +10 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +13 -0
- 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 +13 -3
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +3 -0
- package/dist/user.object.d.ts.map +1 -1
- package/dist/user.object.js +3 -0
- package/migrations/1710000000019-AddSessionMustReenroll.cjs +35 -0
- package/package.json +15 -4
- package/dist/auth-facts-keys.js.map +0 -1
- package/dist/branch.object.js.map +0 -1
- package/dist/company-branch.object.js.map +0 -1
- package/dist/company-department.object.js.map +0 -1
- package/dist/company-membership.object.js.map +0 -1
- package/dist/company.object.js.map +0 -1
- package/dist/cross-tenant-grant.object.js.map +0 -1
- package/dist/department.object.js.map +0 -1
- package/dist/features/access/access-admin.access.js.map +0 -1
- package/dist/features/access/company-admin.access.js.map +0 -1
- package/dist/features/access/index.js.map +0 -1
- package/dist/features/access/user-management.access.js.map +0 -1
- package/dist/features/audit/audit-read.service.js.map +0 -1
- package/dist/features/audit/audit.controller.js.map +0 -1
- package/dist/features/audit/directory-label-resolver.js.map +0 -1
- package/dist/features/audit/mariadb-audit-reader.js.map +0 -1
- package/dist/features/authz/access-bundle-reconcile.job.js.map +0 -1
- package/dist/features/authz/access-bundle-store.js.map +0 -1
- package/dist/features/authz/catalog-bootstrap.provider.js.map +0 -1
- package/dist/features/authz/catalog-reconcile.job.js.map +0 -1
- package/dist/features/authz/db-permission-facts.adapter.js.map +0 -1
- package/dist/features/authz/grant-admin.controller.js.map +0 -1
- package/dist/features/authz/grant-admin.service.js.map +0 -1
- package/dist/features/authz/grant-ceiling.js.map +0 -1
- package/dist/features/authz/mariadb-catalog-store.js.map +0 -1
- package/dist/features/authz/permission-version.bumper.js.map +0 -1
- package/dist/features/authz/record-authorization.js.map +0 -1
- package/dist/features/authz/session-facts.reader.js.map +0 -1
- package/dist/features/company/company.controller.js.map +0 -1
- package/dist/features/company/company.service.js.map +0 -1
- package/dist/features/cross-tenant/cross-tenant-grant-read.service.js.map +0 -1
- package/dist/features/cross-tenant/cross-tenant-grant.controller.js.map +0 -1
- package/dist/features/cross-tenant/grant-authorize.js.map +0 -1
- package/dist/features/identity-feature.module.js.map +0 -1
- package/dist/features/membership/membership-checker.js.map +0 -1
- package/dist/features/membership/membership.controller.js.map +0 -1
- package/dist/features/membership/membership.service.js.map +0 -1
- package/dist/features/membership/posting-version-bump.js.map +0 -1
- package/dist/features/membership/posting.service.js.map +0 -1
- package/dist/features/org-structure/org-catalog.controller.js.map +0 -1
- package/dist/features/org-structure/org-catalog.service.js.map +0 -1
- package/dist/features/policy/policy.controller.js.map +0 -1
- package/dist/features/policy/policy.service.js.map +0 -1
- package/dist/features/role/role.controller.js.map +0 -1
- package/dist/features/role/role.service.js.map +0 -1
- package/dist/features/scope/scope-closure.service.js.map +0 -1
- package/dist/features/scope/scope-subtree.resolver.js.map +0 -1
- package/dist/features/share/share.controller.js.map +0 -1
- package/dist/features/share/share.service.js.map +0 -1
- package/dist/features/shared/http.js.map +0 -1
- package/dist/features/shared/list-query.js.map +0 -1
- package/dist/features/shared/named-option.js.map +0 -1
- package/dist/features/shared/record-resolver.js.map +0 -1
- package/dist/features/user/user.controller.js.map +0 -1
- package/dist/features/user/user.service.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/membership-posting.object.js.map +0 -1
- package/dist/migrations-dir.js.map +0 -1
- package/dist/objects.js.map +0 -1
- package/dist/record-share.object.js.map +0 -1
- package/dist/role.object.js.map +0 -1
- package/dist/schema.js.map +0 -1
- package/dist/shared-resource.object.js.map +0 -1
- package/dist/user.object.js.map +0 -1
|
@@ -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,
|
|
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,22 +4,28 @@ 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
/** The grant-admin actions that REMOVE authority from a role's holders (Phase 8 #2). */
|
|
13
|
+
const REVOKE_ACTIONS = new Set([
|
|
14
|
+
'role.permission_revoked',
|
|
15
|
+
'role.bundle_revoked',
|
|
16
|
+
]);
|
|
17
|
+
/** A JSON-array column (the driver returns an array OR a string) → its string items; else []. */
|
|
18
|
+
function jsonIdArray(v) {
|
|
19
|
+
let arr = v;
|
|
20
|
+
if (typeof v === 'string') {
|
|
21
|
+
try {
|
|
22
|
+
arr = JSON.parse(v);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
arr = [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return Array.isArray(arr) ? arr.filter((x) => typeof x === 'string') : [];
|
|
23
29
|
}
|
|
24
30
|
/** A granted key was not in the catalog and is not a recognized wildcard. The
|
|
25
31
|
* controller maps this to HTTP 400 (distinct from a store outage → 503). */
|
|
@@ -34,17 +40,22 @@ class GrantAdminService {
|
|
|
34
40
|
pool;
|
|
35
41
|
bumper;
|
|
36
42
|
ceiling;
|
|
43
|
+
audit;
|
|
37
44
|
bundles;
|
|
38
45
|
/**
|
|
39
|
-
* @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold")
|
|
40
|
-
*
|
|
41
|
-
* (legacy wiring / tests that don't
|
|
42
|
-
* identity module ALWAYS wires it, so production always
|
|
46
|
+
* @param ceiling The ADR-0029 self-escalation ceiling ("only grant what you hold") plus the
|
|
47
|
+
* security-hardening role-binding rule. Enforced on grant/revoke/grantBundle/revokeBundle for a
|
|
48
|
+
* non-root actor. OPTIONAL for back-compat: when absent (legacy wiring / tests that don't
|
|
49
|
+
* exercise it), neither check runs — the identity module ALWAYS wires it, so production always
|
|
50
|
+
* enforces. Root bypasses regardless.
|
|
51
|
+
* @param audit The grant-administration audit sink (D6): written on the grant's own transaction
|
|
52
|
+
* connection, fail-closed. The identity module ALWAYS wires the engine `MariaDbAuditSink`.
|
|
43
53
|
*/
|
|
44
|
-
constructor(pool, bumper, bundleStore, ceiling) {
|
|
54
|
+
constructor(pool, bumper, bundleStore, ceiling, audit) {
|
|
45
55
|
this.pool = pool;
|
|
46
56
|
this.bumper = bumper;
|
|
47
57
|
this.ceiling = ceiling;
|
|
58
|
+
this.audit = audit;
|
|
48
59
|
this.bundles = bundleStore ?? new access_bundle_store_js_1.AccessBundleStore(pool);
|
|
49
60
|
}
|
|
50
61
|
/** All grantable exact keys (from the catalog) + the computed wildcard set. */
|
|
@@ -60,8 +71,11 @@ class GrantAdminService {
|
|
|
60
71
|
throw new core_1.AuthorizationUnavailableError('permission catalog unavailable', cause);
|
|
61
72
|
}
|
|
62
73
|
const catalog = rows.map((r) => ({
|
|
63
|
-
key: r.perm_key,
|
|
64
|
-
|
|
74
|
+
key: r.perm_key,
|
|
75
|
+
label: r.perm_label,
|
|
76
|
+
module: r.perm_module,
|
|
77
|
+
typeKey: r.perm_type_key,
|
|
78
|
+
action: r.perm_action,
|
|
65
79
|
}));
|
|
66
80
|
// Wildcards: module-wide + one per distinct type key.
|
|
67
81
|
const modules = [...new Set(catalog.map((c) => c.module))];
|
|
@@ -106,52 +120,206 @@ class GrantAdminService {
|
|
|
106
120
|
throw new BadKeyError(key);
|
|
107
121
|
}
|
|
108
122
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
123
|
+
* Security hardening #1 (D1 interim rule) + ADR-0029, in this order, BEFORE any write: the role
|
|
124
|
+
* must be administrable by the actor (bound inside its subtree and nowhere outside — a shared
|
|
125
|
+
* role is root-only), then every key must be covered by the actor's own effective keys ("only
|
|
126
|
+
* grant — or revoke — what you hold"). Root bypasses both. No ceiling wired (legacy/tests) ⇒
|
|
127
|
+
* neither check runs; the identity module always wires it.
|
|
114
128
|
*/
|
|
115
|
-
async
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await this.ceiling
|
|
119
|
-
|
|
129
|
+
async assertMayChangeGrants(roleUuid, keys, actor) {
|
|
130
|
+
if (!this.ceiling)
|
|
131
|
+
return;
|
|
132
|
+
await this.ceiling.assertRoleAdministrable(actor, roleUuid);
|
|
133
|
+
await this.ceiling.assertCanGrant(actor, keys);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Run one grant-administration write in ONE transaction: the `role_permission` statements, the
|
|
137
|
+
* permission-version bump of every tenant using the role, and the audit row(s) (D6 — audited in
|
|
138
|
+
* the SAME transaction, fail-closed: an audit failure rolls the grant back). Store failures map
|
|
139
|
+
* to AuthorizationUnavailableError (503).
|
|
140
|
+
*/
|
|
141
|
+
async writeGrants(action, roleUuid, keys, actor, statement, extra = {}) {
|
|
120
142
|
const conn = await this.pool.getConnection();
|
|
121
143
|
try {
|
|
122
144
|
await conn.beginTransaction();
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
WHERE
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
// Security review F2: serialise against a concurrent posting of this role (in any tenant).
|
|
146
|
+
// Lock the role row FIRST — the posting path locks the same row before it inserts — then
|
|
147
|
+
// re-run the binding rule on THIS connection, so a binding committed by a writer we waited
|
|
148
|
+
// for is seen, and a role that became shared in the meantime is refused (rolled back).
|
|
149
|
+
await conn.query(`SELECT r_uuid FROM \`${APP}\`.\`roles\` WHERE r_uuid = ? FOR UPDATE`, [
|
|
150
|
+
roleUuid,
|
|
151
|
+
]);
|
|
152
|
+
if (this.ceiling && actor.isRoot !== true) {
|
|
153
|
+
grant_ceiling_js_1.GrantCeiling.checkAdministrable(await (0, role_binding_js_1.readRoleBinding)(conn, roleUuid, (0, grant_ceiling_js_1.subtreeOf)(actor)));
|
|
154
|
+
// Phase 8 #2: a revocation strips authority from every holder of the role, so the actor
|
|
155
|
+
// must dominate each of them — checked under the role lock (no holder can appear unseen).
|
|
156
|
+
if (REVOKE_ACTIONS.has(action)) {
|
|
157
|
+
await this.ceiling.assertDominatesRoleHolders(actor, roleUuid);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
for (const key of keys)
|
|
161
|
+
await statement(conn, key);
|
|
162
|
+
const bumped = await this.bumper.bumpForRole(roleUuid, conn);
|
|
163
|
+
await this.auditGrantChange(conn, action, roleUuid, keys, actor, bumped, extra);
|
|
131
164
|
await conn.commit();
|
|
165
|
+
// ADR-0035: the committed versions reach Redis now (gate 5 reads them there).
|
|
166
|
+
await this.bumper.publishCommitted(conn);
|
|
132
167
|
}
|
|
133
168
|
catch (cause) {
|
|
134
169
|
await conn.rollback();
|
|
135
|
-
|
|
170
|
+
this.bumper.discardPending(conn);
|
|
171
|
+
if (cause instanceof BadKeyError ||
|
|
172
|
+
cause instanceof identity_errors_js_1.IdentityForbiddenError ||
|
|
173
|
+
cause instanceof identity_errors_js_1.IdentityNotFoundError) {
|
|
136
174
|
throw cause;
|
|
137
|
-
|
|
175
|
+
}
|
|
176
|
+
throw new core_1.AuthorizationUnavailableError(`${action} failed`, cause);
|
|
138
177
|
}
|
|
139
178
|
finally {
|
|
140
179
|
await conn.release();
|
|
141
180
|
}
|
|
142
181
|
}
|
|
143
|
-
/**
|
|
144
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Write the grant-administration audit row(s) on the transaction connection (D6). One row per
|
|
184
|
+
* distinct tenant among the actor's tenant and every tenant the change bumped, so each affected
|
|
185
|
+
* tenant's auditor sees it. Names, never ids, in the detail (ADR-0012): the role name and the
|
|
186
|
+
* bumped tenants' names; the viewer resolves the actor and row tenant itself. No sink wired
|
|
187
|
+
* (legacy/tests) ⇒ nothing is written.
|
|
188
|
+
*/
|
|
189
|
+
async auditGrantChange(conn, action, roleUuid, keys, actor, bumpedTenants, extra) {
|
|
190
|
+
if (!this.audit)
|
|
191
|
+
return;
|
|
192
|
+
const query = (sql, params) => conn.query(sql, params);
|
|
193
|
+
const roleRows = (await query(`SELECT r_name FROM \`${APP}\`.\`roles\` WHERE r_uuid = ? LIMIT 1`, [roleUuid]));
|
|
194
|
+
const tenants = [...new Set(bumpedTenants)];
|
|
195
|
+
const nameOf = new Map();
|
|
196
|
+
if (tenants.length > 0) {
|
|
197
|
+
const ph = tenants.map(() => '?').join(', ');
|
|
198
|
+
const rows = (await query(`SELECT c_uuid, c_name FROM \`${APP}\`.\`companies\` WHERE c_uuid IN (${ph})`, tenants));
|
|
199
|
+
for (const r of rows)
|
|
200
|
+
nameOf.set(r.c_uuid, r.c_name);
|
|
201
|
+
}
|
|
202
|
+
const labelOf = (t) => nameOf.get(t) ?? '(deleted organisation)';
|
|
203
|
+
const bumpedSet = new Set(tenants);
|
|
204
|
+
// Security review F1: each row is filed under ONE tenant and names ONLY that tenant — a
|
|
205
|
+
// tenant's auditor must never learn which OTHER tenants use the same (shared) role. The one
|
|
206
|
+
// exception is root's own row, filed under a tenant the change did NOT bump, which may carry
|
|
207
|
+
// the full list (root administers across tenants).
|
|
208
|
+
const tenantsFor = (rowTenant) => {
|
|
209
|
+
if (bumpedSet.has(rowTenant))
|
|
210
|
+
return [labelOf(rowTenant)];
|
|
211
|
+
return actor.isRoot === true ? tenants.map(labelOf) : [];
|
|
212
|
+
};
|
|
213
|
+
const occurredAtMs = Date.now();
|
|
214
|
+
for (const tenantId of new Set([actor.tenantId, ...tenants])) {
|
|
215
|
+
const tenantNames = tenantsFor(tenantId);
|
|
216
|
+
await this.audit.record({
|
|
217
|
+
type: 'admin_action',
|
|
218
|
+
occurredAtMs,
|
|
219
|
+
actorId: actor.id,
|
|
220
|
+
tenantId,
|
|
221
|
+
action: { key: action },
|
|
222
|
+
attributes: {
|
|
223
|
+
eventType: 'role_grant',
|
|
224
|
+
role: roleRows[0]?.r_name ?? '(deleted role)',
|
|
225
|
+
keys: [...keys],
|
|
226
|
+
tenants: tenantNames,
|
|
227
|
+
...extra,
|
|
228
|
+
},
|
|
229
|
+
}, query);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Grant a key to a role (idempotent) + bump the version + audit, atomically. The `grantor`
|
|
234
|
+
* Principal stamps `rp_granted_at`/`rp_granted_by` on a NEWLY-inserted row (a duplicate is a
|
|
235
|
+
* no-op and its existing stamp is left as-is). Order: catalog validation (400) → role binding
|
|
236
|
+
* (404/403) → ceiling (403) → write. Root bypasses the binding rule and the ceiling.
|
|
237
|
+
*/
|
|
238
|
+
async grant(roleUuid, key, grantor) {
|
|
239
|
+
await this.assertValidKey(key); // throws BadKeyError (400) before touching the store
|
|
240
|
+
await this.assertMayChangeGrants(roleUuid, [key], grantor);
|
|
241
|
+
const actorUuid = grantor.id;
|
|
242
|
+
await this.writeGrants('role.permission_granted', roleUuid, [key], grantor, async (conn, k) => {
|
|
243
|
+
// Idempotent insert — UNIQUE (rp_role_uuid, rp_perm_key); a duplicate is a no-op.
|
|
244
|
+
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`role_permission\`
|
|
245
|
+
(rp_uuid, rp_role_uuid, rp_perm_key, rp_granted_at, rp_granted_by)
|
|
246
|
+
SELECT UUID(), ?, ?, UTC_TIMESTAMP(), ?
|
|
247
|
+
WHERE NOT EXISTS (
|
|
248
|
+
SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?
|
|
249
|
+
)`, [roleUuid, k, actorUuid ?? null, roleUuid, k]);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Revoke a key from a role (no-op if absent) + bump + audit, atomically. Security hardening #1:
|
|
254
|
+
* the actor must be able to administer the role (binding rule) and must hold the key itself —
|
|
255
|
+
* "only revoke what you hold" — so a delegated admin can no longer strip a role of authority it
|
|
256
|
+
* never had. Phase 8 #2: it must also dominate every user holding the role (checked under the
|
|
257
|
+
* role lock in `writeGrants`). Root bypasses.
|
|
258
|
+
*/
|
|
259
|
+
async revoke(roleUuid, key, actor) {
|
|
260
|
+
await this.assertMayChangeGrants(roleUuid, [key], actor);
|
|
261
|
+
await this.writeGrants('role.permission_revoked', roleUuid, [key], actor, async (conn, k) => {
|
|
262
|
+
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, k]);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Remove a role that is being DELETED from authorization (Phase 8 #2 follow-up): every
|
|
267
|
+
* `role_permission` row of the role, and the role from every service account's `sa_role_ids`
|
|
268
|
+
* (guard's API-key store reads that column live, so a stale entry would keep the deleted role's
|
|
269
|
+
* keys on the agent). ONE transaction on the shared authz pool — the same pool, bump, audit and
|
|
270
|
+
* error mapping as {@link revoke}:
|
|
271
|
+
* 1. lock the holding accounts (`FOR UPDATE`), then check the actor may strip the role from
|
|
272
|
+
* each (`GrantCeiling.assertMayStripRoleFromServiceAccounts`, root bypasses);
|
|
273
|
+
* 2. bump every tenant using the role — postings AND the accounts' tenants — before the strip,
|
|
274
|
+
* so the account tenants are still found;
|
|
275
|
+
* 3. delete the role's grants; rewrite each account's role list without it (edit stamp moved
|
|
276
|
+
* forward, so a concurrent preserve-merge edit conflicts instead of re-adding it);
|
|
277
|
+
* 4. audit as `role.permission_revoked` (`roleDeleted: true`), commit, publish the versions.
|
|
278
|
+
* Returns the bumped tenants.
|
|
279
|
+
*
|
|
280
|
+
* The CALLER (`RoleService.deleteRole`) holds the role row lock on its own engine connection and
|
|
281
|
+
* has already run the binding rule and user-holder dominance; this method therefore does NOT
|
|
282
|
+
* lock the role row (that would wait on the caller). It commits BEFORE the caller deletes the
|
|
283
|
+
* role, so a failure in between leaves a role with no grants and no agents: fewer permissions,
|
|
284
|
+
* never live grants for a deleted role (fail closed).
|
|
285
|
+
*/
|
|
286
|
+
async purgeRole(roleUuid, actor) {
|
|
145
287
|
const conn = await this.pool.getConnection();
|
|
146
288
|
try {
|
|
147
289
|
await conn.beginTransaction();
|
|
148
|
-
await conn.query(`
|
|
149
|
-
|
|
290
|
+
const accounts = (await conn.query(`SELECT sa_id, sa_tenant_id, sa_role_ids, sa_updated_at_ms
|
|
291
|
+
FROM \`${APP}\`.\`service_account\`
|
|
292
|
+
WHERE JSON_CONTAINS(sa_role_ids, JSON_QUOTE(?))
|
|
293
|
+
FOR UPDATE`, [roleUuid]));
|
|
294
|
+
if (this.ceiling && actor.isRoot !== true) {
|
|
295
|
+
await this.ceiling.assertMayStripRoleFromServiceAccounts(actor, roleUuid, accounts.map((a) => a.sa_tenant_id));
|
|
296
|
+
}
|
|
297
|
+
const keys = (await conn.query(`SELECT rp_perm_key FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? ORDER BY rp_perm_key`, [roleUuid])).map((r) => r.rp_perm_key);
|
|
298
|
+
const bumped = await this.bumper.bumpForRole(roleUuid, conn);
|
|
299
|
+
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ?`, [
|
|
300
|
+
roleUuid,
|
|
301
|
+
]);
|
|
302
|
+
const nowMs = Date.now();
|
|
303
|
+
for (const a of accounts) {
|
|
304
|
+
const remaining = jsonIdArray(a.sa_role_ids).filter((id) => id !== roleUuid);
|
|
305
|
+
const previous = a.sa_updated_at_ms === null ? 0 : Number(a.sa_updated_at_ms);
|
|
306
|
+
await conn.query(`UPDATE \`${APP}\`.\`service_account\` SET sa_role_ids = ?, sa_updated_at_ms = ? WHERE sa_id = ?`, [JSON.stringify(remaining), Math.max(nowMs, previous + 1), a.sa_id]);
|
|
307
|
+
}
|
|
308
|
+
await this.auditGrantChange(conn, 'role.permission_revoked', roleUuid, keys, actor, bumped, {
|
|
309
|
+
roleDeleted: true,
|
|
310
|
+
serviceAccountsUpdated: accounts.length,
|
|
311
|
+
});
|
|
150
312
|
await conn.commit();
|
|
313
|
+
await this.bumper.publishCommitted(conn);
|
|
314
|
+
return bumped;
|
|
151
315
|
}
|
|
152
316
|
catch (cause) {
|
|
153
317
|
await conn.rollback();
|
|
154
|
-
|
|
318
|
+
this.bumper.discardPending(conn);
|
|
319
|
+
if (cause instanceof identity_errors_js_1.IdentityForbiddenError || cause instanceof identity_errors_js_1.IdentityNotFoundError) {
|
|
320
|
+
throw cause;
|
|
321
|
+
}
|
|
322
|
+
throw new core_1.AuthorizationUnavailableError('role grant purge failed', cause);
|
|
155
323
|
}
|
|
156
324
|
finally {
|
|
157
325
|
await conn.release();
|
|
@@ -177,57 +345,33 @@ class GrantAdminService {
|
|
|
177
345
|
*/
|
|
178
346
|
async grantBundle(roleUuid, bundleKey, grantor) {
|
|
179
347
|
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
180
|
-
// ADR-0029 ceiling: a non-root grantor must hold EVERY atomic key the bundle
|
|
181
|
-
//
|
|
182
|
-
await this.
|
|
348
|
+
// Binding rule + ADR-0029 ceiling: a non-root grantor must hold EVERY atomic key the bundle
|
|
349
|
+
// expands to — checked before any write (no partial bundle escalation).
|
|
350
|
+
await this.assertMayChangeGrants(roleUuid, [...keys], grantor);
|
|
183
351
|
const actorUuid = grantor.id;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
await conn.
|
|
187
|
-
for (const key of keys) {
|
|
188
|
-
// Idempotent — UNIQUE (rp_role_uuid, rp_perm_key); a duplicate is a no-op.
|
|
189
|
-
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`role_permission\`
|
|
352
|
+
await this.writeGrants('role.bundle_granted', roleUuid, keys, grantor, async (conn, key) => {
|
|
353
|
+
// Idempotent — UNIQUE (rp_role_uuid, rp_perm_key); a duplicate is a no-op.
|
|
354
|
+
await conn.query(`INSERT INTO \`${AUTHZ}\`.\`role_permission\`
|
|
190
355
|
(rp_uuid, rp_role_uuid, rp_perm_key, rp_granted_at, rp_granted_by)
|
|
191
356
|
SELECT UUID(), ?, ?, UTC_TIMESTAMP(), ?
|
|
192
357
|
WHERE NOT EXISTS (
|
|
193
358
|
SELECT 1 FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?
|
|
194
359
|
)`, [roleUuid, key, actorUuid ?? null, roleUuid, key]);
|
|
195
|
-
|
|
196
|
-
await this.bumper.bumpForRole(roleUuid, conn);
|
|
197
|
-
await conn.commit();
|
|
198
|
-
}
|
|
199
|
-
catch (cause) {
|
|
200
|
-
await conn.rollback();
|
|
201
|
-
throw new core_1.AuthorizationUnavailableError('grant bundle failed', cause);
|
|
202
|
-
}
|
|
203
|
-
finally {
|
|
204
|
-
await conn.release();
|
|
205
|
-
}
|
|
360
|
+
}, { bundle: bundleKey });
|
|
206
361
|
}
|
|
207
362
|
/**
|
|
208
363
|
* Revoke a bundle from a role: resolve → DELETE each of the bundle's atomic keys, in
|
|
209
|
-
* ONE transaction with ONE version bump. Caveat (design §5.1): this removes exactly the
|
|
364
|
+
* ONE transaction with ONE version bump + audit. Caveat (design §5.1): this removes exactly the
|
|
210
365
|
* bundle's keys even if a manual grant or another bundle shares one — the model is
|
|
211
|
-
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`.
|
|
366
|
+
* key-based, not bundle-membership-based. Unknown bundle → `BadKeyError`. Security hardening
|
|
367
|
+
* #1: the binding rule + "only revoke what you hold" apply (root bypasses).
|
|
212
368
|
*/
|
|
213
|
-
async revokeBundle(roleUuid, bundleKey) {
|
|
369
|
+
async revokeBundle(roleUuid, bundleKey, actor) {
|
|
214
370
|
const keys = await this.resolveBundleKeysOrThrow(bundleKey);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await conn.
|
|
218
|
-
|
|
219
|
-
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, key]);
|
|
220
|
-
}
|
|
221
|
-
await this.bumper.bumpForRole(roleUuid, conn);
|
|
222
|
-
await conn.commit();
|
|
223
|
-
}
|
|
224
|
-
catch (cause) {
|
|
225
|
-
await conn.rollback();
|
|
226
|
-
throw new core_1.AuthorizationUnavailableError('revoke bundle failed', cause);
|
|
227
|
-
}
|
|
228
|
-
finally {
|
|
229
|
-
await conn.release();
|
|
230
|
-
}
|
|
371
|
+
await this.assertMayChangeGrants(roleUuid, [...keys], actor);
|
|
372
|
+
await this.writeGrants('role.bundle_revoked', roleUuid, keys, actor, async (conn, key) => {
|
|
373
|
+
await conn.query(`DELETE FROM \`${AUTHZ}\`.\`role_permission\` WHERE rp_role_uuid = ? AND rp_perm_key = ?`, [roleUuid, key]);
|
|
374
|
+
}, { bundle: bundleKey });
|
|
231
375
|
}
|
|
232
376
|
/** Resolve a bundle key → its atomic keys; throw BadKeyError when unknown/orphaned. */
|
|
233
377
|
async resolveBundleKeysOrThrow(bundleKey) {
|
|
@@ -270,7 +414,9 @@ class GrantAdminService {
|
|
|
270
414
|
// subtree (the EXISTS predicate); root spans all tenants (no predicate). The scope clause
|
|
271
415
|
// sits FIRST in the WHERE (before the LIKE), so its ?-bound node params lead the arg list —
|
|
272
416
|
// identical on BOTH the count and page queries so they can never disagree.
|
|
273
|
-
const scope = subtreeScope.isRoot
|
|
417
|
+
const scope = subtreeScope.isRoot
|
|
418
|
+
? { sql: '', params: [] }
|
|
419
|
+
: (0, role_binding_js_1.subtreeExists)(subtreeScope.nodeIds);
|
|
274
420
|
// Distinct granted roles (a role appears iff it has ≥1 role_permission row), filtered
|
|
275
421
|
// by name search. Bind the LIKE term as a param (wildcards escaped).
|
|
276
422
|
const likeClause = search ? `AND r.r_name LIKE ?` : '';
|
|
@@ -14,10 +14,13 @@
|
|
|
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
18
|
/** The grantor exceeds their own authority — cannot grant a key they do not hold (→ 403). */
|
|
18
19
|
export declare class GrantCeilingError extends Error {
|
|
19
|
-
constructor(key: string);
|
|
20
|
+
constructor(key: string, message?: string);
|
|
20
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[];
|
|
21
24
|
/**
|
|
22
25
|
* Does `effective` (the grantor's held EXACT keys) cover `granted`?
|
|
23
26
|
*
|
|
@@ -40,9 +43,27 @@ export declare class GrantCeiling {
|
|
|
40
43
|
private readonly registry;
|
|
41
44
|
/** Injectable clock (epoch-ms) for the posting effective-window read; defaults to Date.now. */
|
|
42
45
|
private readonly now;
|
|
46
|
+
private readonly rootIds;
|
|
43
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,
|
|
44
55
|
/** Injectable clock (epoch-ms) for the posting effective-window read; defaults to Date.now. */
|
|
45
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;
|
|
46
67
|
/**
|
|
47
68
|
* The grantor's EFFECTIVE exact keys: the keys granted to the grantor's roles (active
|
|
48
69
|
* membership's in-effect postings in the grantor's tenant), wildcards expanded against the
|
|
@@ -56,5 +77,83 @@ export declare class GrantCeiling {
|
|
|
56
77
|
* `GrantCeilingError` on the FIRST key that exceeds the grantor's authority.
|
|
57
78
|
*/
|
|
58
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; a role key the registry does not know
|
|
85
|
+
* counts as not held (Phase 8 #4). Root bypasses. Store outage → 503.
|
|
86
|
+
*/
|
|
87
|
+
assertCanAssignRoles(actor: Principal, roleIds: readonly string[]): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* The first of a TARGET's raw keys (a role's `role_permission` rows) the actor does not cover,
|
|
90
|
+
* or `undefined` when all are covered. Phase 8 #4: judged per RAW key with `coversKey`, so a key
|
|
91
|
+
* the registry does not know — an exact key that is not registered, or a wildcard expanding to
|
|
92
|
+
* nothing — counts as NOT held and denies. Expanding first would silently drop it and could
|
|
93
|
+
* pass vacuously (fail open) when this registry is narrower than the decision path's.
|
|
94
|
+
*/
|
|
95
|
+
private firstUncovered;
|
|
96
|
+
/**
|
|
97
|
+
* Assert the actor DOMINATES the target user (#3, D2): every membership the target holds (in any
|
|
98
|
+
* tenant, active or not) is inside the actor's subtree, AND every key the target could hold
|
|
99
|
+
* through any of its postings (any membership, any window — potential authority, not just what is
|
|
100
|
+
* in effect now) is held by the actor. A platform root target is never dominated by a non-root.
|
|
101
|
+
* Root actors bypass. Store outage → 503.
|
|
102
|
+
*/
|
|
103
|
+
assertDominates(actor: Principal, targetUserId: string): Promise<void>;
|
|
104
|
+
/**
|
|
105
|
+
* Assert the actor DOMINATES every user who holds the role (Phase 8 #2): deleting a role, or
|
|
106
|
+
* revoking a key from it, strips authority from each holder, so it needs the same
|
|
107
|
+
* {@link assertDominates} the actor would need to change any one of them directly. Holders are
|
|
108
|
+
* the users with a posting of the role in any membership and any window (potential authority).
|
|
109
|
+
* The actor itself is skipped: giving up your own authority stays allowed (self-demotion).
|
|
110
|
+
* Service-account holders: a revoke is covered by the binding rule (`assertRoleAdministrable`:
|
|
111
|
+
* no binding outside the actor's subtree) plus "only revoke what you hold"; a role DELETE also
|
|
112
|
+
* strips the role from each account and checks {@link assertMayStripRoleFromServiceAccounts}.
|
|
113
|
+
* Root bypasses. Store outage → 503.
|
|
114
|
+
*
|
|
115
|
+
* Callers serialise against a concurrent posting by locking the role row first (the posting
|
|
116
|
+
* path locks the same row before it inserts), then call this — so no holder appears unseen.
|
|
117
|
+
*/
|
|
118
|
+
assertDominatesRoleHolders(actor: Principal, roleUuid: string): Promise<void>;
|
|
119
|
+
/**
|
|
120
|
+
* Assert the actor may REMOVE `roleUuid` from every service account holding it (Phase 8 #2
|
|
121
|
+
* follow-up: a role delete strips the role from each holding account). Judged by the SAME rules
|
|
122
|
+
* as editing the account directly (`ServiceAccountService.update`, ADR-0029 ceiling via
|
|
123
|
+
* {@link ceilingOf}): each account's tenant is inside the actor's subtree, and the role is one
|
|
124
|
+
* the actor holds itself — a non-root admin's edit preserves (cannot strip) a role it does not
|
|
125
|
+
* hold. `accountTenantIds` are the holding accounts' tenants, read by the caller under a row lock.
|
|
126
|
+
* Root bypasses. Refusals name no account. Store outage → 503.
|
|
127
|
+
*/
|
|
128
|
+
assertMayStripRoleFromServiceAccounts(actor: Principal, roleUuid: string, accountTenantIds: readonly string[]): Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Assert the actor may RETIRE (disable + revoke the keys of) each of these service accounts
|
|
131
|
+
* (Phase 8 #3: a company delete retires the deleted company's own agents). Judged by the SAME
|
|
132
|
+
* ADR-0029 ceiling an admin needs to have made the agent ({@link ceilingOf}): the account's
|
|
133
|
+
* tenant and every node of its scope are inside the actor's subtree, and every role it holds is
|
|
134
|
+
* one the actor holds itself — so a sub-admin can never switch off an agent more powerful than
|
|
135
|
+
* itself by deleting its company. `accounts` are read by the caller under a row lock. Root
|
|
136
|
+
* bypasses. Refusals name no account. Store outage → 503.
|
|
137
|
+
*/
|
|
138
|
+
assertMayRetireServiceAccounts(actor: Principal, accounts: ReadonlyArray<{
|
|
139
|
+
readonly tenantId: string;
|
|
140
|
+
readonly scopeSubtreeNodeIds: readonly string[];
|
|
141
|
+
readonly roleIds: readonly string[];
|
|
142
|
+
}>): Promise<void>;
|
|
143
|
+
/** Where `roleUuid` is bound relative to the actor's subtree (postings + service accounts). */
|
|
144
|
+
roleBinding(actor: Principal, roleUuid: string): Promise<RoleBinding>;
|
|
145
|
+
/**
|
|
146
|
+
* Assert the actor may ADMINISTER the role — edit its grants, rename or delete it (#1, D1
|
|
147
|
+
* interim rule): the role is bound inside the actor's subtree AND bound nowhere outside it. A
|
|
148
|
+
* role not bound in the subtree is "not found" for the actor (no existence leak); a role also
|
|
149
|
+
* used outside (a shared role) is root-only. Root bypasses.
|
|
150
|
+
*/
|
|
151
|
+
assertRoleAdministrable(actor: Principal, roleUuid: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Assert the actor may POST someone to the role (#11): the role is bound within the actor's
|
|
154
|
+
* subtree or not bound at all — never a role also used outside it (a shared role's assignment
|
|
155
|
+
* would widen another tenant's role). Pair with {@link assertCanAssignRoles}. Root bypasses.
|
|
156
|
+
*/
|
|
157
|
+
assertRoleAssignable(actor: Principal, roleUuid: string): Promise<void>;
|
|
59
158
|
}
|
|
60
159
|
//# 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;
|
|
1
|
+
{"version":3,"file":"grant-ceiling.d.ts","sourceRoot":"","sources":["../../../src/features/authz/grant-ceiling.ts"],"names":[],"mappings":"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"}
|