@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
|
@@ -7,18 +7,19 @@
|
|
|
7
7
|
* diffs current-vs-desired and applies the minimal change — delete only what's removed,
|
|
8
8
|
* create only what's added, keep the rest untouched.
|
|
9
9
|
*
|
|
10
|
-
* ──
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
10
|
+
* ── Authority (security hardening #11) ─────────────────────────────────────────────────
|
|
11
|
+
* A posting IS a role assignment, so before ANY write the actor (the request principal) must:
|
|
12
|
+
* - reach the membership (a walled `records.get` → null ⇒ 404; the engine's ADR-0034 create
|
|
13
|
+
* wall re-checks the posting's membership company inside the transaction);
|
|
14
|
+
* - for every ADDED role: the role is bound within the actor's subtree or not bound at all
|
|
15
|
+
* (`GrantCeiling.assertRoleAssignable` — a role also used elsewhere is never assignable),
|
|
16
|
+
* and the role's keys are all held by the actor (`assertCanAssignRoles`, ADR-0029 applied to
|
|
17
|
+
* assignment). Removals need no ceiling: self-demotion stays allowed.
|
|
18
|
+
*
|
|
19
|
+
* ── Atomicity (KI #16) ────────────────────────────────────────────────────────────────
|
|
20
|
+
* The diff runs inside ONE `records.transaction` (ADR-0034): creates FIRST, then deletes, so a
|
|
21
|
+
* failure at any point rolls back every posting write of the replace (no partial set, no
|
|
22
|
+
* window where a removed role is gone but its replacement was refused).
|
|
22
23
|
*/
|
|
23
24
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
24
25
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
@@ -37,8 +38,18 @@ exports.PostingService = void 0;
|
|
|
37
38
|
const common_1 = require("@nestjs/common");
|
|
38
39
|
const engine_1 = require("@astram/engine");
|
|
39
40
|
const posting_version_bump_js_1 = require("./posting-version-bump.js");
|
|
41
|
+
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
42
|
+
const identity_errors_js_1 = require("../shared/identity-errors.js");
|
|
40
43
|
/** The field keys whose values are effective-window epoch-ms bounds (normalized on write). */
|
|
41
44
|
const WINDOW_KEYS = new Set(['effectiveFrom', 'effectiveUntil']);
|
|
45
|
+
/** The ONLY keys a client posting entry may set (review F4) — never `membershipUuid`. */
|
|
46
|
+
const POSTING_KEYS = [
|
|
47
|
+
'roleUuid',
|
|
48
|
+
'departmentUuid',
|
|
49
|
+
'branchUuid',
|
|
50
|
+
'effectiveFrom',
|
|
51
|
+
'effectiveUntil',
|
|
52
|
+
];
|
|
42
53
|
/**
|
|
43
54
|
* Normalize an effective-window bound to epoch-ms (a `number`), or `null` if empty/unparseable.
|
|
44
55
|
* The membership_posting object declares these fields as `number` (BIGINT epoch-ms) so the
|
|
@@ -79,7 +90,13 @@ function naturalKey(p) {
|
|
|
79
90
|
*/
|
|
80
91
|
function stripEmpty(input) {
|
|
81
92
|
const out = {};
|
|
82
|
-
|
|
93
|
+
// Security review F4: only the posting's own fields are taken from the client entry — a
|
|
94
|
+
// client `membershipUuid` (or `createdBy`, or any unknown key) never reaches the engine; the
|
|
95
|
+
// caller sets `membershipUuid` from the PATH.
|
|
96
|
+
for (const k of POSTING_KEYS) {
|
|
97
|
+
if (!(k in input))
|
|
98
|
+
continue;
|
|
99
|
+
const v = input[k];
|
|
83
100
|
if (WINDOW_KEYS.has(k)) {
|
|
84
101
|
const ms = toEpochMs(v);
|
|
85
102
|
if (ms !== null)
|
|
@@ -93,9 +110,14 @@ function stripEmpty(input) {
|
|
|
93
110
|
}
|
|
94
111
|
let PostingService = class PostingService {
|
|
95
112
|
records;
|
|
113
|
+
ceiling;
|
|
96
114
|
versionBump;
|
|
97
|
-
constructor(records,
|
|
115
|
+
constructor(records,
|
|
116
|
+
// The role-assignment ceiling + binding rule (security hardening #11). REQUIRED: a missing
|
|
117
|
+
// ceiling must fail DI at boot, never silently skip the check.
|
|
118
|
+
ceiling, versionBump) {
|
|
98
119
|
this.records = records;
|
|
120
|
+
this.ceiling = ceiling;
|
|
99
121
|
this.versionBump = versionBump;
|
|
100
122
|
}
|
|
101
123
|
/** List a membership's postings (tenant-scoped by the pipe). */
|
|
@@ -106,45 +128,75 @@ let PostingService = class PostingService {
|
|
|
106
128
|
/**
|
|
107
129
|
* Replace a membership's full posting set with `desired`, applying the minimal diff.
|
|
108
130
|
* Compares current vs desired by the natural key `roleUuid|departmentUuid|branchUuid`:
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* kept postings are never delete targets, so the membership never lands empty).
|
|
131
|
+
* creates added rows, deletes removed rows, leaves kept rows untouched. Every authority check
|
|
132
|
+
* runs BEFORE any write; the writes run in one transaction (see the file header).
|
|
112
133
|
*/
|
|
113
|
-
async replacePostings(membershipUuid, desired) {
|
|
114
|
-
|
|
134
|
+
async replacePostings(actor, membershipUuid, desired) {
|
|
135
|
+
for (const d of desired) {
|
|
136
|
+
if (typeof d?.roleUuid !== 'string' || d.roleUuid.length === 0) {
|
|
137
|
+
throw new identity_errors_js_1.IdentityBadRequestError('Every assignment needs a role.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 1) Reach: the membership must be visible through the subtree read wall (else 404).
|
|
141
|
+
const membership = await this.records.get('company_membership', membershipUuid);
|
|
142
|
+
if (membership === null)
|
|
143
|
+
throw new identity_errors_js_1.IdentityNotFoundError('Membership not found.');
|
|
144
|
+
const tenantUuid = typeof membership.fields?.companyUuid === 'string' && membership.fields.companyUuid.length > 0
|
|
145
|
+
? membership.fields.companyUuid
|
|
146
|
+
: null;
|
|
147
|
+
const current = (await this.records.list('membership_posting', { where: { membershipUuid } }))
|
|
148
|
+
.rows;
|
|
115
149
|
const desiredKeys = new Set(desired.map(naturalKey));
|
|
116
150
|
const currentKeys = new Set(current.map((r) => naturalKey(r.fields)));
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
const removed = current.filter((r) => !desiredKeys.has(naturalKey(r.fields)));
|
|
152
|
+
const seenAdded = new Set();
|
|
153
|
+
const added = desired.filter((d) => {
|
|
154
|
+
const k = naturalKey(d);
|
|
155
|
+
if (currentKeys.has(k) || seenAdded.has(k))
|
|
156
|
+
return false;
|
|
157
|
+
seenAdded.add(k);
|
|
158
|
+
return true;
|
|
159
|
+
});
|
|
160
|
+
// 2) Authority for every ADDED role, before any write: binding rule, then the ceiling.
|
|
161
|
+
const addedRoles = [...new Set(added.map((d) => d.roleUuid))];
|
|
162
|
+
for (const roleUuid of addedRoles) {
|
|
163
|
+
await this.ceiling.assertRoleAssignable(actor, roleUuid);
|
|
127
164
|
}
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
changed = true;
|
|
136
|
-
}
|
|
165
|
+
await this.ceiling.assertCanAssignRoles(actor, addedRoles);
|
|
166
|
+
// Removing a PEER's postings strips their authority: the actor must dominate the member
|
|
167
|
+
// (security review). Removing your OWN postings (self-demotion) stays allowed.
|
|
168
|
+
const memberUuid = typeof membership.fields?.userUuid === 'string' ? membership.fields.userUuid : '';
|
|
169
|
+
if (removed.length > 0 && memberUuid !== actor.id) {
|
|
170
|
+
await this.ceiling.assertDominates(actor, memberUuid);
|
|
137
171
|
}
|
|
172
|
+
if (added.length === 0 && removed.length === 0)
|
|
173
|
+
return; // no-op replace: no write, no bump
|
|
174
|
+
// 3) Write atomically: creates first, then deletes (KI #16). Only whitelisted fields are sent,
|
|
175
|
+
// and `membershipUuid` always comes from the path (review F4).
|
|
176
|
+
await this.records.transaction(async (tx) => {
|
|
177
|
+
// Review F2: serialise against a concurrent grant/posting of the same role. Lock each
|
|
178
|
+
// added role row on THIS transaction (grant administration locks the same row), then
|
|
179
|
+
// re-run the binding rule: a binding another tenant committed while we waited is now
|
|
180
|
+
// visible, and a role that became shared is refused before the INSERT (rolled back).
|
|
181
|
+
for (const roleUuid of addedRoles) {
|
|
182
|
+
await tx.lockForUpdate('role', roleUuid);
|
|
183
|
+
await this.ceiling.assertRoleAssignable(actor, roleUuid);
|
|
184
|
+
}
|
|
185
|
+
for (const d of added) {
|
|
186
|
+
await tx.create('membership_posting', { fields: { ...stripEmpty(d), membershipUuid } });
|
|
187
|
+
}
|
|
188
|
+
for (const r of removed) {
|
|
189
|
+
await tx.delete('membership_posting', r.id);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
138
192
|
// ADR-0011 STALENESS FIX: a posting add/remove changes the effective roles (→ coarse
|
|
139
193
|
// grants) of the membership's user in that company, so bump the tenant's permission
|
|
140
194
|
// version — every live session in that tenant then reloads its cached facts on the next
|
|
141
|
-
// request (a role gain/loss lands within the version bound). Only when
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
if (changed && this.versionBump) {
|
|
147
|
-
const tenantUuid = await this.resolveMembershipTenant(membershipUuid);
|
|
195
|
+
// request (a role gain/loss lands within the version bound). Only when the bump collaborator
|
|
196
|
+
// is wired (absent in unit tests → no-op). Best-effort + fail-safe: the posting write already
|
|
197
|
+
// committed, so a bump failure must NOT surface as a 500 that hides a successful assignment
|
|
198
|
+
// change — the cache is instead invalidated by the next legit bump / the facts' natural TTL.
|
|
199
|
+
if (this.versionBump) {
|
|
148
200
|
if (tenantUuid) {
|
|
149
201
|
try {
|
|
150
202
|
await this.versionBump.bumpTenant(tenantUuid);
|
|
@@ -155,27 +207,12 @@ let PostingService = class PostingService {
|
|
|
155
207
|
}
|
|
156
208
|
}
|
|
157
209
|
}
|
|
158
|
-
/**
|
|
159
|
-
* Resolve the tenant (company uuid) a membership belongs to — the tenant whose permission
|
|
160
|
-
* version a posting change must bump. Reads the parent `company_membership` through the
|
|
161
|
-
* pipe (tenant-scoped, no raw SQL), returning `companyUuid` or `null` if unresolvable
|
|
162
|
-
* (then no bump — fail-safe; never guesses a tenant).
|
|
163
|
-
*/
|
|
164
|
-
async resolveMembershipTenant(membershipUuid) {
|
|
165
|
-
try {
|
|
166
|
-
const m = await this.records.get('company_membership', membershipUuid);
|
|
167
|
-
const companyUuid = m?.fields?.companyUuid;
|
|
168
|
-
return typeof companyUuid === 'string' && companyUuid.length > 0 ? companyUuid : null;
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
210
|
};
|
|
175
211
|
exports.PostingService = PostingService;
|
|
176
212
|
exports.PostingService = PostingService = __decorate([
|
|
177
213
|
(0, common_1.Injectable)(),
|
|
178
|
-
__param(
|
|
179
|
-
__param(
|
|
180
|
-
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
214
|
+
__param(2, (0, common_1.Optional)()),
|
|
215
|
+
__param(2, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
|
|
216
|
+
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
217
|
+
grant_ceiling_js_1.GrantCeiling, Object])
|
|
181
218
|
], PostingService);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-catalog.controller.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"org-catalog.controller.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.controller.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAU7D,qBAEa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,iBAAiB;IAEvD,kFAAkF;IAG5E,sBAAsB,CAAuB,WAAW,EAAE,MAAM;;;IAKtE,+EAA+E;IAGzE,mBAAmB,CAAuB,WAAW,EAAE,MAAM;;;CAIpE"}
|
|
@@ -14,6 +14,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.OrgCatalogController = void 0;
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
17
18
|
const guard_1 = require("@astram/guard");
|
|
18
19
|
const org_catalog_service_js_1 = require("./org-catalog.service.js");
|
|
19
20
|
// ── Company-scoped catalogs (dept/branch option lists for the posting editor) ──
|
|
@@ -58,5 +59,6 @@ __decorate([
|
|
|
58
59
|
], OrgCatalogController.prototype, "listCompanyBranches", null);
|
|
59
60
|
exports.OrgCatalogController = OrgCatalogController = __decorate([
|
|
60
61
|
(0, common_1.Controller)('identity'),
|
|
62
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
61
63
|
__metadata("design:paramtypes", [org_catalog_service_js_1.OrgCatalogService])
|
|
62
64
|
], OrgCatalogController);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,qBACa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,kFAAkF;IAClF,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"org-catalog.service.d.ts","sourceRoot":"","sources":["../../../src/features/org-structure/org-catalog.service.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAE7D,qBACa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,cAAc;IAEpD,kFAAkF;IAClF,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IASnE,+EAA+E;IAC/E,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAIhE,qFAAqF;YACvE,kBAAkB;CAoBjC"}
|
|
@@ -41,7 +41,10 @@ let OrgCatalogService = class OrgCatalogService {
|
|
|
41
41
|
continue;
|
|
42
42
|
const view = await this.records.get(catalogTypeKey, refUuid);
|
|
43
43
|
const name = view?.fields?.name;
|
|
44
|
-
options.push({
|
|
44
|
+
options.push({
|
|
45
|
+
uuid: refUuid,
|
|
46
|
+
name: typeof name === 'string' && name.length > 0 ? name : '(unknown)',
|
|
47
|
+
});
|
|
45
48
|
}
|
|
46
49
|
return options;
|
|
47
50
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.controller.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy.controller.d.ts","sourceRoot":"","sources":["../../../src/features/policy/policy.controller.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,aAAa,EAId,MAAM,qBAAqB,CAAC;AAE7B,qFAAqF;AACrF,UAAU,aAAa;IACrB,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,qBAEa,gBAAgB;IACf,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,aAAa;IAEpD;;;;;;;;OAQG;IAGG,MAAM,CAAmB,OAAO,CAAC,EAAE,MAAM;IAY/C;;;OAGG;IAGG,IAAI,CAAQ,GAAG,EAAE,aAAa,EAAoB,OAAO,CAAC,EAAE,MAAM;;;IAQxE;;;;;OAKG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAU,IAAI,EAAE,YAAY;;;IAWlE;;;;OAIG;IAGG,MAAM,CAAQ,GAAG,EAAE,aAAa,EAAe,EAAE,EAAE,MAAM;;;CAShE"}
|
|
@@ -33,6 +33,7 @@ exports.PolicyController = void 0;
|
|
|
33
33
|
*/
|
|
34
34
|
const common_1 = require("@nestjs/common");
|
|
35
35
|
const guard_1 = require("@astram/guard");
|
|
36
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
36
37
|
const policy_service_js_1 = require("./policy.service.js");
|
|
37
38
|
let PolicyController = class PolicyController {
|
|
38
39
|
policies;
|
|
@@ -146,5 +147,6 @@ __decorate([
|
|
|
146
147
|
], PolicyController.prototype, "delete", null);
|
|
147
148
|
exports.PolicyController = PolicyController = __decorate([
|
|
148
149
|
(0, common_1.Controller)('identity'),
|
|
150
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
149
151
|
__metadata("design:paramtypes", [policy_service_js_1.PolicyService])
|
|
150
152
|
], PolicyController);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RoleService } from './role.service.js';
|
|
2
2
|
import { type ListQueryDto } from '../shared/list-query.js';
|
|
3
|
+
import type { AuthedRequest } from '../shared/http.js';
|
|
3
4
|
export declare class RoleController {
|
|
4
5
|
private readonly roles;
|
|
5
6
|
constructor(roles: RoleService);
|
|
@@ -24,13 +25,13 @@ export declare class RoleController {
|
|
|
24
25
|
* administrator, may read roles). */
|
|
25
26
|
getRole(uuid: string): Promise<import("packages/engine/dist/types.js").RecordView | null>;
|
|
26
27
|
/** Update a role — gated on `identity:role:update`. */
|
|
27
|
-
updateRole(uuid: string, body: {
|
|
28
|
+
updateRole(req: AuthedRequest, uuid: string, body: {
|
|
28
29
|
name?: string;
|
|
29
30
|
}): Promise<{
|
|
30
31
|
success: boolean;
|
|
31
32
|
}>;
|
|
32
33
|
/** Delete a role — gated on `identity:role:delete`. */
|
|
33
|
-
deleteRole(uuid: string): Promise<{
|
|
34
|
+
deleteRole(req: AuthedRequest, uuid: string): Promise<{
|
|
34
35
|
success: boolean;
|
|
35
36
|
}>;
|
|
36
37
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"role.controller.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.controller.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAOvD,qBAEa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,WAAW;IAE/C;;;;OAIG;IAGG,SAAS,CAAU,KAAK,EAAE,YAAY;;;;;;IAM5C,uDAAuD;IAGjD,UAAU,CAAS,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAKhD;0CACsC;IAGtC,OAAO,CAAgB,IAAI,EAAE,MAAM;IAInC,uDAAuD;IAGjD,UAAU,CACP,GAAG,EAAE,aAAa,EACV,IAAI,EAAE,MAAM,EACnB,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE;;;IAMjC,uDAAuD;IAGjD,UAAU,CAAQ,GAAG,EAAE,aAAa,EAAiB,IAAI,EAAE,MAAM;;;CAIxE"}
|
|
@@ -17,9 +17,11 @@ const common_1 = require("@nestjs/common");
|
|
|
17
17
|
const guard_1 = require("@astram/guard");
|
|
18
18
|
const role_service_js_1 = require("./role.service.js");
|
|
19
19
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
20
|
+
const identity_error_interceptor_js_1 = require("../shared/identity-error.interceptor.js");
|
|
20
21
|
// Role management is gated on `identity:role:<action>` grants. `role` is CONTROL-plane
|
|
21
22
|
// for ALL actions — including read: the capped root is the identity administrator (the
|
|
22
|
-
// identity-administrator model), so it may list/read roles AND manage them.
|
|
23
|
+
// identity-administrator model), so it may list/read roles AND manage them. Update/delete
|
|
24
|
+
// also pass the role-binding rule (a role used outside the caller's subtree is root-only).
|
|
23
25
|
let RoleController = class RoleController {
|
|
24
26
|
roles;
|
|
25
27
|
constructor(roles) {
|
|
@@ -46,13 +48,13 @@ let RoleController = class RoleController {
|
|
|
46
48
|
return this.roles.getRole(uuid);
|
|
47
49
|
}
|
|
48
50
|
/** Update a role — gated on `identity:role:update`. */
|
|
49
|
-
async updateRole(uuid, body) {
|
|
50
|
-
await this.roles.updateRole(uuid, body
|
|
51
|
+
async updateRole(req, uuid, body) {
|
|
52
|
+
await this.roles.updateRole(req.principal, uuid, { name: body?.name });
|
|
51
53
|
return { success: true };
|
|
52
54
|
}
|
|
53
55
|
/** Delete a role — gated on `identity:role:delete`. */
|
|
54
|
-
async deleteRole(uuid) {
|
|
55
|
-
await this.roles.deleteRole(uuid);
|
|
56
|
+
async deleteRole(req, uuid) {
|
|
57
|
+
await this.roles.deleteRole(req.principal, uuid);
|
|
56
58
|
return { success: true };
|
|
57
59
|
}
|
|
58
60
|
};
|
|
@@ -84,21 +86,24 @@ __decorate([
|
|
|
84
86
|
__decorate([
|
|
85
87
|
(0, common_1.Put)('roles/:uuid'),
|
|
86
88
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'update' }),
|
|
87
|
-
__param(0, (0, common_1.
|
|
88
|
-
__param(1, (0, common_1.
|
|
89
|
+
__param(0, (0, common_1.Req)()),
|
|
90
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
91
|
+
__param(2, (0, common_1.Body)()),
|
|
89
92
|
__metadata("design:type", Function),
|
|
90
|
-
__metadata("design:paramtypes", [String, Object]),
|
|
93
|
+
__metadata("design:paramtypes", [Object, String, Object]),
|
|
91
94
|
__metadata("design:returntype", Promise)
|
|
92
95
|
], RoleController.prototype, "updateRole", null);
|
|
93
96
|
__decorate([
|
|
94
97
|
(0, common_1.Delete)('roles/:uuid'),
|
|
95
98
|
(0, guard_1.Classified)({ typeKey: 'role', action: 'delete' }),
|
|
96
|
-
__param(0, (0, common_1.
|
|
99
|
+
__param(0, (0, common_1.Req)()),
|
|
100
|
+
__param(1, (0, common_1.Param)('uuid')),
|
|
97
101
|
__metadata("design:type", Function),
|
|
98
|
-
__metadata("design:paramtypes", [String]),
|
|
102
|
+
__metadata("design:paramtypes", [Object, String]),
|
|
99
103
|
__metadata("design:returntype", Promise)
|
|
100
104
|
], RoleController.prototype, "deleteRole", null);
|
|
101
105
|
exports.RoleController = RoleController = __decorate([
|
|
102
106
|
(0, common_1.Controller)('identity'),
|
|
107
|
+
(0, common_1.UseInterceptors)(identity_error_interceptor_js_1.IdentityErrorInterceptor),
|
|
103
108
|
__metadata("design:paramtypes", [role_service_js_1.RoleService])
|
|
104
109
|
], RoleController);
|
|
@@ -1,24 +1,55 @@
|
|
|
1
1
|
import { RecordsService, type RecordId } from '@astram/engine';
|
|
2
|
+
import type { Principal } from '@astram/core';
|
|
2
3
|
import { type ListParams } from '../shared/list-query.js';
|
|
4
|
+
import { GrantCeiling } from '../authz/grant-ceiling.js';
|
|
5
|
+
import { GrantAdminService } from '../authz/grant-admin.service.js';
|
|
6
|
+
import { type PostingVersionBump } from '../membership/posting-version-bump.js';
|
|
3
7
|
/**
|
|
4
8
|
* RoleService — role CRUD on the paved road, via the framework's RecordsService
|
|
5
|
-
* facade. The facade supplies the acting identity (the request principal)
|
|
6
|
-
*
|
|
9
|
+
* facade. The facade supplies the acting identity (the request principal) for the pipe;
|
|
10
|
+
* no raw SQL, no RECORD_ENGINE.
|
|
11
|
+
*
|
|
12
|
+
* Roles are a GLOBAL catalog, so renaming or deleting one changes every tenant that uses it (a
|
|
13
|
+
* delete cascades its postings). Security hardening #1 (D1 interim rule): update/delete first pass
|
|
14
|
+
* `GrantCeiling.assertRoleAdministrable` — the role must be bound inside the actor's subtree and
|
|
15
|
+
* nowhere outside it (a shared role is root-only). The engine's write wall still applies after.
|
|
7
16
|
*/
|
|
8
17
|
export declare class RoleService {
|
|
9
18
|
private readonly records;
|
|
10
|
-
|
|
19
|
+
private readonly ceiling;
|
|
20
|
+
private readonly versionBump?;
|
|
21
|
+
private readonly grants?;
|
|
22
|
+
constructor(records: RecordsService, ceiling: GrantCeiling, versionBump?: (PostingVersionBump | null) | undefined, grants?: (GrantAdminService | null) | undefined);
|
|
11
23
|
/** Create a role. */
|
|
12
24
|
createRole(name: string): Promise<RecordId>;
|
|
13
25
|
/** List roles (tenant-scoped by the pipe), with paging/search/sort. */
|
|
14
26
|
listRoles(params?: ListParams): Promise<import("@astram/engine").RecordPage>;
|
|
15
27
|
/** Read a single role. */
|
|
16
28
|
getRole(uuid: string): Promise<import("@astram/engine").RecordView | null>;
|
|
17
|
-
/**
|
|
18
|
-
updateRole(uuid: string, patch: {
|
|
19
|
-
name?:
|
|
29
|
+
/** Rename a role — only a role the actor may administer (binding rule). Patch whitelisted. */
|
|
30
|
+
updateRole(actor: Principal, uuid: string, patch: {
|
|
31
|
+
name?: unknown;
|
|
20
32
|
}): Promise<void>;
|
|
21
|
-
/**
|
|
22
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Delete a role — only a role the actor may administer (binding rule) AND whose every holder the
|
|
35
|
+
* actor dominates (Phase 8 #2: the delete cascades the holders' postings, stripping their
|
|
36
|
+
* authority). In ONE transaction: lock the role row (the posting and grant paths lock the same
|
|
37
|
+
* row first), re-check the binding rule and holder dominance against what is committed now, read
|
|
38
|
+
* the holder tenants (before the cascade removes the postings), then delete. After the commit,
|
|
39
|
+
* every holder tenant's permission version is bumped so cached roles drop at the next request.
|
|
40
|
+
* The bump runs after the commit, never before: an earlier bump would let a session reload the
|
|
41
|
+
* pre-delete roles under the new version. A bump failure throws (503) rather than leave holders'
|
|
42
|
+
* cached grants silently stale.
|
|
43
|
+
*
|
|
44
|
+
* The role's `role_permission` rows (shield_authz, no FK) and its place in every service
|
|
45
|
+
* account's `sa_role_ids` (read live by the API-key store) do not cascade, so before the delete,
|
|
46
|
+
* still under the role lock, `GrantAdminService.purgeRole` removes both in its own committed
|
|
47
|
+
* authz transaction — checking first that the actor may change each holding account, and
|
|
48
|
+
* bumping every tenant that used the role (account tenants included). Order is fail-closed: a
|
|
49
|
+
* failure after the purge leaves a role with no grants and no agents, never live grants or an
|
|
50
|
+
* agent carrying a deleted role. A re-created role gets a fresh uuid (the engine mints it), so
|
|
51
|
+
* it can never pick up anything left behind.
|
|
52
|
+
*/
|
|
53
|
+
deleteRole(actor: Principal, uuid: string): Promise<void>;
|
|
23
54
|
}
|
|
24
55
|
//# sourceMappingURL=role.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"role.service.d.ts","sourceRoot":"","sources":["../../../src/features/role/role.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,uCAAuC,CAAC;AAK/C;;;;;;;;;GASG;AACH,qBACa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAKxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAC;IAK7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAXP,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,YAAY,EAKrB,WAAW,CAAC,GAAE,kBAAkB,GAAG,IAAI,aAAA,EAKvC,MAAM,CAAC,GAAE,iBAAiB,GAAG,IAAI,aAAA;IAGpD,qBAAqB;IACrB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAI3C,uEAAuE;IACvE,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU;IAI7B,0BAA0B;IAC1B,OAAO,CAAC,IAAI,EAAE,MAAM;IAIpB,8FAA8F;IACxF,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAO1F;;;;;;;;;;;;;;;;;;;OAmBG;IACG,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAoBhE"}
|
|
@@ -8,22 +8,40 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
8
8
|
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
9
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
10
|
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
exports.RoleService = void 0;
|
|
13
16
|
const common_1 = require("@nestjs/common");
|
|
14
17
|
const engine_1 = require("@astram/engine");
|
|
18
|
+
const core_1 = require("@astram/core");
|
|
15
19
|
const list_query_js_1 = require("../shared/list-query.js");
|
|
20
|
+
const grant_ceiling_js_1 = require("../authz/grant-ceiling.js");
|
|
21
|
+
const grant_admin_service_js_1 = require("../authz/grant-admin.service.js");
|
|
22
|
+
const posting_version_bump_js_1 = require("../membership/posting-version-bump.js");
|
|
16
23
|
const ROLE_SEARCH_FIELDS = ['name'];
|
|
17
24
|
const ROLE_SORT_FIELDS = ['name', 'createdAt'];
|
|
18
25
|
/**
|
|
19
26
|
* RoleService — role CRUD on the paved road, via the framework's RecordsService
|
|
20
|
-
* facade. The facade supplies the acting identity (the request principal)
|
|
21
|
-
*
|
|
27
|
+
* facade. The facade supplies the acting identity (the request principal) for the pipe;
|
|
28
|
+
* no raw SQL, no RECORD_ENGINE.
|
|
29
|
+
*
|
|
30
|
+
* Roles are a GLOBAL catalog, so renaming or deleting one changes every tenant that uses it (a
|
|
31
|
+
* delete cascades its postings). Security hardening #1 (D1 interim rule): update/delete first pass
|
|
32
|
+
* `GrantCeiling.assertRoleAdministrable` — the role must be bound inside the actor's subtree and
|
|
33
|
+
* nowhere outside it (a shared role is root-only). The engine's write wall still applies after.
|
|
22
34
|
*/
|
|
23
35
|
let RoleService = class RoleService {
|
|
24
36
|
records;
|
|
25
|
-
|
|
37
|
+
ceiling;
|
|
38
|
+
versionBump;
|
|
39
|
+
grants;
|
|
40
|
+
constructor(records, ceiling, versionBump, grants) {
|
|
26
41
|
this.records = records;
|
|
42
|
+
this.ceiling = ceiling;
|
|
43
|
+
this.versionBump = versionBump;
|
|
44
|
+
this.grants = grants;
|
|
27
45
|
}
|
|
28
46
|
/** Create a role. */
|
|
29
47
|
createRole(name) {
|
|
@@ -37,17 +55,66 @@ let RoleService = class RoleService {
|
|
|
37
55
|
getRole(uuid) {
|
|
38
56
|
return this.records.get('role', uuid);
|
|
39
57
|
}
|
|
40
|
-
/**
|
|
41
|
-
updateRole(uuid, patch) {
|
|
42
|
-
|
|
58
|
+
/** Rename a role — only a role the actor may administer (binding rule). Patch whitelisted. */
|
|
59
|
+
async updateRole(actor, uuid, patch) {
|
|
60
|
+
await this.ceiling.assertRoleAdministrable(actor, uuid);
|
|
61
|
+
const fields = {};
|
|
62
|
+
if (typeof patch?.name === 'string')
|
|
63
|
+
fields.name = patch.name;
|
|
64
|
+
await this.records.update('role', uuid, { fields });
|
|
43
65
|
}
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Delete a role — only a role the actor may administer (binding rule) AND whose every holder the
|
|
68
|
+
* actor dominates (Phase 8 #2: the delete cascades the holders' postings, stripping their
|
|
69
|
+
* authority). In ONE transaction: lock the role row (the posting and grant paths lock the same
|
|
70
|
+
* row first), re-check the binding rule and holder dominance against what is committed now, read
|
|
71
|
+
* the holder tenants (before the cascade removes the postings), then delete. After the commit,
|
|
72
|
+
* every holder tenant's permission version is bumped so cached roles drop at the next request.
|
|
73
|
+
* The bump runs after the commit, never before: an earlier bump would let a session reload the
|
|
74
|
+
* pre-delete roles under the new version. A bump failure throws (503) rather than leave holders'
|
|
75
|
+
* cached grants silently stale.
|
|
76
|
+
*
|
|
77
|
+
* The role's `role_permission` rows (shield_authz, no FK) and its place in every service
|
|
78
|
+
* account's `sa_role_ids` (read live by the API-key store) do not cascade, so before the delete,
|
|
79
|
+
* still under the role lock, `GrantAdminService.purgeRole` removes both in its own committed
|
|
80
|
+
* authz transaction — checking first that the actor may change each holding account, and
|
|
81
|
+
* bumping every tenant that used the role (account tenants included). Order is fail-closed: a
|
|
82
|
+
* failure after the purge leaves a role with no grants and no agents, never live grants or an
|
|
83
|
+
* agent carrying a deleted role. A re-created role gets a fresh uuid (the engine mints it), so
|
|
84
|
+
* it can never pick up anything left behind.
|
|
85
|
+
*/
|
|
86
|
+
async deleteRole(actor, uuid) {
|
|
87
|
+
await this.ceiling.assertRoleAdministrable(actor, uuid);
|
|
88
|
+
const grants = this.grants;
|
|
89
|
+
if (!grants)
|
|
90
|
+
throw new core_1.AuthorizationUnavailableError('role grant purge is not configured');
|
|
91
|
+
const tenants = await this.records.transaction(async (tx) => {
|
|
92
|
+
await tx.lockForUpdate('role', uuid);
|
|
93
|
+
await this.ceiling.assertRoleAdministrable(actor, uuid);
|
|
94
|
+
await this.ceiling.assertDominatesRoleHolders(actor, uuid);
|
|
95
|
+
const affected = (await this.versionBump?.tenantsForRole?.(uuid)) ?? [];
|
|
96
|
+
await grants.purgeRole(uuid, actor);
|
|
97
|
+
await tx.delete('role', uuid);
|
|
98
|
+
return affected;
|
|
99
|
+
});
|
|
100
|
+
if (!this.versionBump)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
for (const tenant of tenants)
|
|
104
|
+
await this.versionBump.bumpTenant(tenant);
|
|
105
|
+
}
|
|
106
|
+
catch (cause) {
|
|
107
|
+
throw new core_1.AuthorizationUnavailableError('permission version bump failed', cause);
|
|
108
|
+
}
|
|
47
109
|
}
|
|
48
110
|
};
|
|
49
111
|
exports.RoleService = RoleService;
|
|
50
112
|
exports.RoleService = RoleService = __decorate([
|
|
51
113
|
(0, common_1.Injectable)(),
|
|
52
|
-
|
|
114
|
+
__param(2, (0, common_1.Optional)()),
|
|
115
|
+
__param(2, (0, common_1.Inject)(posting_version_bump_js_1.POSTING_VERSION_BUMP)),
|
|
116
|
+
__param(3, (0, common_1.Optional)()),
|
|
117
|
+
__param(3, (0, common_1.Inject)(grant_admin_service_js_1.GrantAdminService)),
|
|
118
|
+
__metadata("design:paramtypes", [engine_1.RecordsService,
|
|
119
|
+
grant_ceiling_js_1.GrantCeiling, Object, Object])
|
|
53
120
|
], RoleService);
|