@objectstack/plugin-sharing 4.0.1
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/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +15 -0
- package/LICENSE +202 -0
- package/dist/index.d.mts +290 -0
- package/dist/index.d.ts +290 -0
- package/dist/index.js +980 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +942 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +37 -0
- package/src/department-graph.ts +178 -0
- package/src/index.ts +46 -0
- package/src/rule-hooks.ts +64 -0
- package/src/sharing-plugin.ts +211 -0
- package/src/sharing-rule-service.ts +438 -0
- package/src/sharing-rule.test.ts +348 -0
- package/src/sharing-service.test.ts +355 -0
- package/src/sharing-service.ts +283 -0
- package/src/team-graph.ts +158 -0
- package/tsconfig.json +10 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { SysRecordShare as SysRecordShare2, SysSharingRule as SysSharingRule2 } from "@objectstack/platform-objects/security";
|
|
3
|
+
import { SysDepartment as SysDepartment2, SysDepartmentMember as SysDepartmentMember2 } from "@objectstack/platform-objects/identity";
|
|
4
|
+
|
|
5
|
+
// src/sharing-service.ts
|
|
6
|
+
function makeShareId() {
|
|
7
|
+
const g = globalThis;
|
|
8
|
+
if (g.crypto?.randomUUID) return `shr_${g.crypto.randomUUID()}`;
|
|
9
|
+
return `shr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
10
|
+
}
|
|
11
|
+
var SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
|
|
12
|
+
var OWNER_FIELD = "owner_id";
|
|
13
|
+
function effectiveSharingModel(schema) {
|
|
14
|
+
const m = schema?.sharingModel ?? schema?.security?.sharingModel;
|
|
15
|
+
if (m === "private") return "private";
|
|
16
|
+
if (m === "read") return "read";
|
|
17
|
+
return "public";
|
|
18
|
+
}
|
|
19
|
+
function hasOwnerField(schema) {
|
|
20
|
+
return Boolean(schema?.fields && OWNER_FIELD in schema.fields);
|
|
21
|
+
}
|
|
22
|
+
var SharingService = class {
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.engine = options.engine;
|
|
25
|
+
this.bypassObjects = /* @__PURE__ */ new Set([
|
|
26
|
+
"sys_record_share",
|
|
27
|
+
"sys_user",
|
|
28
|
+
"sys_organization",
|
|
29
|
+
"sys_member",
|
|
30
|
+
"sys_role",
|
|
31
|
+
"sys_permission_set",
|
|
32
|
+
"sys_user_permission_set",
|
|
33
|
+
"sys_role_permission_set",
|
|
34
|
+
...options.bypassObjects ?? []
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build a `FilterCondition` restricting `find` to records the caller
|
|
39
|
+
* may see. Returns `null` when no filter should be applied.
|
|
40
|
+
*/
|
|
41
|
+
async buildReadFilter(object, context) {
|
|
42
|
+
if (this.shouldBypass(object, context)) return null;
|
|
43
|
+
const schema = this.engine.getSchema?.(object);
|
|
44
|
+
if (!schema) return null;
|
|
45
|
+
if (effectiveSharingModel(schema) !== "private") return null;
|
|
46
|
+
if (!hasOwnerField(schema)) return null;
|
|
47
|
+
if (!context.userId) {
|
|
48
|
+
return { id: "__deny_all__" };
|
|
49
|
+
}
|
|
50
|
+
const grants = await this.engine.find("sys_record_share", {
|
|
51
|
+
filter: {
|
|
52
|
+
object_name: object,
|
|
53
|
+
recipient_type: "user",
|
|
54
|
+
recipient_id: context.userId
|
|
55
|
+
},
|
|
56
|
+
fields: ["record_id", "access_level"],
|
|
57
|
+
limit: 5e3,
|
|
58
|
+
context: SYSTEM_CTX
|
|
59
|
+
});
|
|
60
|
+
const grantedIds = Array.isArray(grants) ? grants.map((g) => String(g.record_id)).filter(Boolean) : [];
|
|
61
|
+
if (grantedIds.length === 0) {
|
|
62
|
+
return { [OWNER_FIELD]: context.userId };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
$or: [
|
|
66
|
+
{ [OWNER_FIELD]: context.userId },
|
|
67
|
+
{ id: { $in: grantedIds } }
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Return `true` if the caller may edit `(object, recordId)`. Always
|
|
73
|
+
* `true` for system context, public objects, and objects without an
|
|
74
|
+
* owner field.
|
|
75
|
+
*/
|
|
76
|
+
async canEdit(object, recordId, context) {
|
|
77
|
+
if (this.shouldBypass(object, context)) return true;
|
|
78
|
+
const schema = this.engine.getSchema?.(object);
|
|
79
|
+
if (!schema) return true;
|
|
80
|
+
const model = effectiveSharingModel(schema);
|
|
81
|
+
if (model === "public") return true;
|
|
82
|
+
if (!hasOwnerField(schema)) return true;
|
|
83
|
+
if (!context.userId) return false;
|
|
84
|
+
const own = await this.engine.find(object, {
|
|
85
|
+
filter: { id: recordId },
|
|
86
|
+
fields: ["id", OWNER_FIELD],
|
|
87
|
+
limit: 1,
|
|
88
|
+
context: SYSTEM_CTX
|
|
89
|
+
});
|
|
90
|
+
const owner = Array.isArray(own) && own[0] ? own[0][OWNER_FIELD] : void 0;
|
|
91
|
+
if (owner && String(owner) === String(context.userId)) return true;
|
|
92
|
+
const editGrants = await this.engine.find("sys_record_share", {
|
|
93
|
+
filter: {
|
|
94
|
+
object_name: object,
|
|
95
|
+
record_id: recordId,
|
|
96
|
+
recipient_type: "user",
|
|
97
|
+
recipient_id: context.userId,
|
|
98
|
+
access_level: { $in: ["edit", "full"] }
|
|
99
|
+
},
|
|
100
|
+
fields: ["id"],
|
|
101
|
+
limit: 1,
|
|
102
|
+
context: SYSTEM_CTX
|
|
103
|
+
});
|
|
104
|
+
return Array.isArray(editGrants) && editGrants.length > 0;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Upsert a share row. Returning the existing row when an identical
|
|
108
|
+
* grant already exists keeps the REST endpoint idempotent.
|
|
109
|
+
*/
|
|
110
|
+
async grant(input, context) {
|
|
111
|
+
if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
|
|
112
|
+
if (!input.recordId) throw new Error("VALIDATION_FAILED: recordId is required");
|
|
113
|
+
if (!input.recipientId) throw new Error("VALIDATION_FAILED: recipientId is required");
|
|
114
|
+
const recipientType = input.recipientType ?? "user";
|
|
115
|
+
const accessLevel = input.accessLevel ?? "read";
|
|
116
|
+
const source = input.source ?? "manual";
|
|
117
|
+
const existing = await this.engine.find("sys_record_share", {
|
|
118
|
+
filter: {
|
|
119
|
+
object_name: input.object,
|
|
120
|
+
record_id: input.recordId,
|
|
121
|
+
recipient_type: recipientType,
|
|
122
|
+
recipient_id: input.recipientId
|
|
123
|
+
},
|
|
124
|
+
limit: 1,
|
|
125
|
+
context: SYSTEM_CTX
|
|
126
|
+
});
|
|
127
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
128
|
+
if (Array.isArray(existing) && existing[0]) {
|
|
129
|
+
const row2 = existing[0];
|
|
130
|
+
const patch = {
|
|
131
|
+
id: row2.id,
|
|
132
|
+
access_level: accessLevel,
|
|
133
|
+
source,
|
|
134
|
+
source_id: input.sourceId ?? row2.source_id ?? null,
|
|
135
|
+
reason: input.reason ?? row2.reason ?? null,
|
|
136
|
+
updated_at: now
|
|
137
|
+
};
|
|
138
|
+
await this.engine.update("sys_record_share", patch, { context: SYSTEM_CTX });
|
|
139
|
+
return { ...row2, ...patch };
|
|
140
|
+
}
|
|
141
|
+
const id = makeShareId();
|
|
142
|
+
const row = {
|
|
143
|
+
id,
|
|
144
|
+
object_name: input.object,
|
|
145
|
+
record_id: input.recordId,
|
|
146
|
+
recipient_type: recipientType,
|
|
147
|
+
recipient_id: input.recipientId,
|
|
148
|
+
access_level: accessLevel,
|
|
149
|
+
source,
|
|
150
|
+
source_id: input.sourceId ?? null,
|
|
151
|
+
granted_by: context.userId ?? null,
|
|
152
|
+
reason: input.reason ?? null,
|
|
153
|
+
created_at: now,
|
|
154
|
+
updated_at: now
|
|
155
|
+
};
|
|
156
|
+
await this.engine.insert("sys_record_share", row, { context: SYSTEM_CTX });
|
|
157
|
+
return row;
|
|
158
|
+
}
|
|
159
|
+
/** Delete a share row by id. No-op when not found. */
|
|
160
|
+
async revoke(shareId, _context) {
|
|
161
|
+
if (!shareId) throw new Error("VALIDATION_FAILED: shareId is required");
|
|
162
|
+
await this.engine.delete("sys_record_share", {
|
|
163
|
+
where: { id: shareId },
|
|
164
|
+
context: SYSTEM_CTX
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** List share rows for `(object, recordId)`. */
|
|
168
|
+
async listShares(object, recordId, _context) {
|
|
169
|
+
const rows = await this.engine.find("sys_record_share", {
|
|
170
|
+
filter: { object_name: object, record_id: recordId },
|
|
171
|
+
orderBy: [{ field: "created_at", direction: "desc" }],
|
|
172
|
+
limit: 500,
|
|
173
|
+
context: SYSTEM_CTX
|
|
174
|
+
});
|
|
175
|
+
return Array.isArray(rows) ? rows : [];
|
|
176
|
+
}
|
|
177
|
+
// ── helpers ──────────────────────────────────────────────────────
|
|
178
|
+
shouldBypass(object, context) {
|
|
179
|
+
if (context?.isSystem) return true;
|
|
180
|
+
if (this.bypassObjects.has(object)) return true;
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/team-graph.ts
|
|
186
|
+
var SYSTEM_CTX2 = { isSystem: true, roles: [], permissions: [] };
|
|
187
|
+
var TeamGraphService = class {
|
|
188
|
+
constructor(opts) {
|
|
189
|
+
var _a, _b, _c;
|
|
190
|
+
this.engine = opts.engine;
|
|
191
|
+
this.organizationId = opts.organizationId ?? null;
|
|
192
|
+
this.cache = opts.cache ?? {};
|
|
193
|
+
(_a = this.cache).expandUsers ?? (_a.expandUsers = /* @__PURE__ */ new Map());
|
|
194
|
+
(_b = this.cache).expandRole ?? (_b.expandRole = /* @__PURE__ */ new Map());
|
|
195
|
+
(_c = this.cache).manager ?? (_c.manager = /* @__PURE__ */ new Map());
|
|
196
|
+
}
|
|
197
|
+
async expandUsers(teamId) {
|
|
198
|
+
if (!teamId) return [];
|
|
199
|
+
const cached = this.cache.expandUsers.get(teamId);
|
|
200
|
+
if (cached) return cached;
|
|
201
|
+
let rows = [];
|
|
202
|
+
try {
|
|
203
|
+
rows = await this.engine.find("sys_team_member", {
|
|
204
|
+
filter: { team_id: teamId },
|
|
205
|
+
fields: ["user_id"],
|
|
206
|
+
limit: 1e4,
|
|
207
|
+
context: SYSTEM_CTX2
|
|
208
|
+
});
|
|
209
|
+
} catch {
|
|
210
|
+
rows = [];
|
|
211
|
+
}
|
|
212
|
+
const users = Array.from(new Set((rows ?? []).map((r) => String(r.user_id ?? "")).filter(Boolean)));
|
|
213
|
+
this.cache.expandUsers.set(teamId, users);
|
|
214
|
+
return users;
|
|
215
|
+
}
|
|
216
|
+
async expandRoleUsers(roleName, organizationId) {
|
|
217
|
+
if (!roleName) return [];
|
|
218
|
+
const key = `${organizationId ?? this.organizationId ?? "*"}::${roleName}`;
|
|
219
|
+
const cached = this.cache.expandRole.get(key);
|
|
220
|
+
if (cached) return cached;
|
|
221
|
+
const filter = { role: roleName };
|
|
222
|
+
const org = organizationId ?? this.organizationId;
|
|
223
|
+
if (org) filter.organization_id = org;
|
|
224
|
+
let rows = [];
|
|
225
|
+
try {
|
|
226
|
+
rows = await this.engine.find("sys_member", {
|
|
227
|
+
filter,
|
|
228
|
+
fields: ["user_id"],
|
|
229
|
+
limit: 1e4,
|
|
230
|
+
context: SYSTEM_CTX2
|
|
231
|
+
});
|
|
232
|
+
} catch {
|
|
233
|
+
rows = [];
|
|
234
|
+
}
|
|
235
|
+
const users = Array.from(new Set((rows ?? []).map((r) => String(r.user_id ?? "")).filter(Boolean)));
|
|
236
|
+
this.cache.expandRole.set(key, users);
|
|
237
|
+
return users;
|
|
238
|
+
}
|
|
239
|
+
async managerOf(userId, _organizationId) {
|
|
240
|
+
if (!userId) return null;
|
|
241
|
+
if (this.cache.manager.has(userId)) return this.cache.manager.get(userId) ?? null;
|
|
242
|
+
let row = null;
|
|
243
|
+
try {
|
|
244
|
+
const rows = await this.engine.find("sys_user", {
|
|
245
|
+
filter: { id: userId },
|
|
246
|
+
fields: ["id", "manager_id"],
|
|
247
|
+
limit: 1,
|
|
248
|
+
context: SYSTEM_CTX2
|
|
249
|
+
});
|
|
250
|
+
row = Array.isArray(rows) ? rows[0] : null;
|
|
251
|
+
} catch {
|
|
252
|
+
row = null;
|
|
253
|
+
}
|
|
254
|
+
const mgr = row?.manager_id ? String(row.manager_id) : null;
|
|
255
|
+
this.cache.manager.set(userId, mgr);
|
|
256
|
+
return mgr;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
async function expandPrincipal(input, ctx) {
|
|
260
|
+
const t = input.type;
|
|
261
|
+
const v = String(input.value ?? "");
|
|
262
|
+
if (!v) return [];
|
|
263
|
+
if (t === "user") return [v];
|
|
264
|
+
if (t === "team") return ctx.team.expandUsers(v);
|
|
265
|
+
if (t === "department" || t === "dept") {
|
|
266
|
+
if (ctx.dept) return ctx.dept.expandUsers(v);
|
|
267
|
+
return [`${t}:${v}`];
|
|
268
|
+
}
|
|
269
|
+
if (t === "role") return ctx.team.expandRoleUsers(v, ctx.organizationId ?? void 0);
|
|
270
|
+
if (t === "field" && input.record) {
|
|
271
|
+
const fv = input.record[v];
|
|
272
|
+
return fv ? [String(fv)] : [];
|
|
273
|
+
}
|
|
274
|
+
if (t === "manager" && input.record) {
|
|
275
|
+
const subject = input.record[v] ?? input.record.owner_id;
|
|
276
|
+
if (!subject) return [];
|
|
277
|
+
const mgr = await ctx.team.managerOf(String(subject), ctx.organizationId ?? void 0);
|
|
278
|
+
return mgr ? [mgr] : [];
|
|
279
|
+
}
|
|
280
|
+
return [`${t}:${v}`];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/department-graph.ts
|
|
284
|
+
var SYSTEM_CTX3 = { isSystem: true, roles: [], permissions: [] };
|
|
285
|
+
var DepartmentGraphService = class {
|
|
286
|
+
constructor(opts) {
|
|
287
|
+
var _a, _b, _c;
|
|
288
|
+
this.engine = opts.engine;
|
|
289
|
+
this.organizationId = opts.organizationId ?? null;
|
|
290
|
+
this.cache = opts.cache ?? {};
|
|
291
|
+
(_a = this.cache).descendants ?? (_a.descendants = /* @__PURE__ */ new Map());
|
|
292
|
+
(_b = this.cache).expandUsers ?? (_b.expandUsers = /* @__PURE__ */ new Map());
|
|
293
|
+
(_c = this.cache).head ?? (_c.head = /* @__PURE__ */ new Map());
|
|
294
|
+
this.teamGraph = opts.teamGraph;
|
|
295
|
+
}
|
|
296
|
+
async descendants(departmentId) {
|
|
297
|
+
if (!departmentId) return [];
|
|
298
|
+
const cached = this.cache.descendants.get(departmentId);
|
|
299
|
+
if (cached) return cached;
|
|
300
|
+
let seedActive = true;
|
|
301
|
+
try {
|
|
302
|
+
const seedRows = await this.engine.find("sys_department", {
|
|
303
|
+
filter: this.orgScope({ id: departmentId }),
|
|
304
|
+
fields: ["id", "active"],
|
|
305
|
+
limit: 1,
|
|
306
|
+
context: SYSTEM_CTX3
|
|
307
|
+
});
|
|
308
|
+
const seedRow = Array.isArray(seedRows) ? seedRows[0] : null;
|
|
309
|
+
if (!seedRow) seedActive = false;
|
|
310
|
+
else if (seedRow.active === false) seedActive = false;
|
|
311
|
+
} catch {
|
|
312
|
+
seedActive = false;
|
|
313
|
+
}
|
|
314
|
+
if (!seedActive) {
|
|
315
|
+
this.cache.descendants.set(departmentId, []);
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
const seen = /* @__PURE__ */ new Set([departmentId]);
|
|
319
|
+
const queue = [departmentId];
|
|
320
|
+
while (queue.length) {
|
|
321
|
+
const parent = queue.shift();
|
|
322
|
+
let children = [];
|
|
323
|
+
try {
|
|
324
|
+
children = await this.engine.find("sys_department", {
|
|
325
|
+
filter: this.orgScope({ parent_department_id: parent, active: { $ne: false } }),
|
|
326
|
+
fields: ["id"],
|
|
327
|
+
limit: 1e3,
|
|
328
|
+
context: SYSTEM_CTX3
|
|
329
|
+
});
|
|
330
|
+
} catch {
|
|
331
|
+
children = [];
|
|
332
|
+
}
|
|
333
|
+
for (const c of children ?? []) {
|
|
334
|
+
const cid = String(c.id ?? "");
|
|
335
|
+
if (cid && !seen.has(cid)) {
|
|
336
|
+
seen.add(cid);
|
|
337
|
+
queue.push(cid);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const out = Array.from(seen);
|
|
342
|
+
this.cache.descendants.set(departmentId, out);
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
async expandUsers(departmentId) {
|
|
346
|
+
if (!departmentId) return [];
|
|
347
|
+
const cached = this.cache.expandUsers.get(departmentId);
|
|
348
|
+
if (cached) return cached;
|
|
349
|
+
const depts = await this.descendants(departmentId);
|
|
350
|
+
if (depts.length === 0) return [];
|
|
351
|
+
let rows = [];
|
|
352
|
+
try {
|
|
353
|
+
rows = await this.engine.find("sys_department_member", {
|
|
354
|
+
filter: { department_id: { $in: depts } },
|
|
355
|
+
fields: ["user_id"],
|
|
356
|
+
limit: 1e4,
|
|
357
|
+
context: SYSTEM_CTX3
|
|
358
|
+
});
|
|
359
|
+
} catch {
|
|
360
|
+
rows = [];
|
|
361
|
+
}
|
|
362
|
+
const users = Array.from(
|
|
363
|
+
new Set((rows ?? []).map((r) => String(r.user_id ?? "")).filter(Boolean))
|
|
364
|
+
);
|
|
365
|
+
this.cache.expandUsers.set(departmentId, users);
|
|
366
|
+
return users;
|
|
367
|
+
}
|
|
368
|
+
async headOf(departmentId) {
|
|
369
|
+
if (!departmentId) return null;
|
|
370
|
+
if (this.cache.head.has(departmentId)) return this.cache.head.get(departmentId) ?? null;
|
|
371
|
+
let row = null;
|
|
372
|
+
try {
|
|
373
|
+
const rows = await this.engine.find("sys_department", {
|
|
374
|
+
filter: { id: departmentId },
|
|
375
|
+
fields: ["id", "manager_user_id"],
|
|
376
|
+
limit: 1,
|
|
377
|
+
context: SYSTEM_CTX3
|
|
378
|
+
});
|
|
379
|
+
row = Array.isArray(rows) ? rows[0] : null;
|
|
380
|
+
} catch {
|
|
381
|
+
row = null;
|
|
382
|
+
}
|
|
383
|
+
const head = row?.manager_user_id ? String(row.manager_user_id) : null;
|
|
384
|
+
this.cache.head.set(departmentId, head);
|
|
385
|
+
return head;
|
|
386
|
+
}
|
|
387
|
+
async managerOf(userId, organizationId) {
|
|
388
|
+
if (this.teamGraph) return this.teamGraph.managerOf(userId, organizationId);
|
|
389
|
+
if (!userId) return null;
|
|
390
|
+
try {
|
|
391
|
+
const rows = await this.engine.find("sys_user", {
|
|
392
|
+
filter: { id: userId },
|
|
393
|
+
fields: ["id", "manager_id"],
|
|
394
|
+
limit: 1,
|
|
395
|
+
context: SYSTEM_CTX3
|
|
396
|
+
});
|
|
397
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
398
|
+
return row?.manager_id ? String(row.manager_id) : null;
|
|
399
|
+
} catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
orgScope(filter) {
|
|
404
|
+
if (this.organizationId) return { ...filter, organization_id: this.organizationId };
|
|
405
|
+
return filter;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// src/sharing-rule-service.ts
|
|
410
|
+
var SYSTEM_CTX4 = { isSystem: true, roles: [], permissions: [] };
|
|
411
|
+
function uid(prefix) {
|
|
412
|
+
const g = globalThis;
|
|
413
|
+
if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
|
|
414
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
415
|
+
}
|
|
416
|
+
function parseCriteria(raw) {
|
|
417
|
+
if (raw == null || raw === "") return void 0;
|
|
418
|
+
if (typeof raw === "string") {
|
|
419
|
+
const trimmed = raw.trim();
|
|
420
|
+
if (!trimmed) return void 0;
|
|
421
|
+
try {
|
|
422
|
+
return JSON.parse(trimmed);
|
|
423
|
+
} catch {
|
|
424
|
+
return void 0;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return raw;
|
|
428
|
+
}
|
|
429
|
+
function rowFromRule(row) {
|
|
430
|
+
return {
|
|
431
|
+
id: row.id,
|
|
432
|
+
organization_id: row.organization_id ?? null,
|
|
433
|
+
name: row.name,
|
|
434
|
+
label: row.label,
|
|
435
|
+
description: row.description ?? null,
|
|
436
|
+
object_name: row.object_name,
|
|
437
|
+
criteria: parseCriteria(row.criteria_json),
|
|
438
|
+
recipient_type: row.recipient_type,
|
|
439
|
+
recipient_id: row.recipient_id,
|
|
440
|
+
access_level: row.access_level,
|
|
441
|
+
active: row.active !== false,
|
|
442
|
+
created_at: row.created_at ?? void 0,
|
|
443
|
+
updated_at: row.updated_at ?? void 0
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
var SharingRuleService = class {
|
|
447
|
+
constructor(opts) {
|
|
448
|
+
this.engine = opts.engine;
|
|
449
|
+
this.sharing = opts.sharing;
|
|
450
|
+
this.logger = opts.logger;
|
|
451
|
+
}
|
|
452
|
+
async defineRule(input, context) {
|
|
453
|
+
if (!input.name) throw new Error("VALIDATION_FAILED: name is required");
|
|
454
|
+
if (!input.label) throw new Error("VALIDATION_FAILED: label is required");
|
|
455
|
+
if (!input.object) throw new Error("VALIDATION_FAILED: object is required");
|
|
456
|
+
if (!input.recipientType) throw new Error("VALIDATION_FAILED: recipientType is required");
|
|
457
|
+
if (!input.recipientId) throw new Error("VALIDATION_FAILED: recipientId is required");
|
|
458
|
+
const orgId = context?.organizationId ?? context?.tenantId ?? null;
|
|
459
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
460
|
+
const accessLevel = input.accessLevel ?? "read";
|
|
461
|
+
const active = input.active !== false;
|
|
462
|
+
const criteriaJson = input.criteria == null ? null : typeof input.criteria === "string" ? input.criteria : JSON.stringify(input.criteria);
|
|
463
|
+
const existing = await this.engine.find("sys_sharing_rule", {
|
|
464
|
+
filter: orgId ? { name: input.name, organization_id: orgId } : { name: input.name },
|
|
465
|
+
limit: 1,
|
|
466
|
+
context: SYSTEM_CTX4
|
|
467
|
+
});
|
|
468
|
+
if (Array.isArray(existing) && existing[0]) {
|
|
469
|
+
const row = existing[0];
|
|
470
|
+
const patch = {
|
|
471
|
+
id: row.id,
|
|
472
|
+
label: input.label,
|
|
473
|
+
description: input.description ?? null,
|
|
474
|
+
object_name: input.object,
|
|
475
|
+
criteria_json: criteriaJson,
|
|
476
|
+
recipient_type: input.recipientType,
|
|
477
|
+
recipient_id: input.recipientId,
|
|
478
|
+
access_level: accessLevel,
|
|
479
|
+
active,
|
|
480
|
+
updated_at: now
|
|
481
|
+
};
|
|
482
|
+
await this.engine.update("sys_sharing_rule", patch, { context: SYSTEM_CTX4 });
|
|
483
|
+
return rowFromRule({ ...row, ...patch });
|
|
484
|
+
}
|
|
485
|
+
const newRow = {
|
|
486
|
+
id: uid("srule"),
|
|
487
|
+
organization_id: orgId,
|
|
488
|
+
name: input.name,
|
|
489
|
+
label: input.label,
|
|
490
|
+
description: input.description ?? null,
|
|
491
|
+
object_name: input.object,
|
|
492
|
+
criteria_json: criteriaJson,
|
|
493
|
+
recipient_type: input.recipientType,
|
|
494
|
+
recipient_id: input.recipientId,
|
|
495
|
+
access_level: accessLevel,
|
|
496
|
+
active,
|
|
497
|
+
created_at: now,
|
|
498
|
+
updated_at: now
|
|
499
|
+
};
|
|
500
|
+
await this.engine.insert("sys_sharing_rule", newRow, { context: SYSTEM_CTX4 });
|
|
501
|
+
return rowFromRule(newRow);
|
|
502
|
+
}
|
|
503
|
+
async listRules(filter, context) {
|
|
504
|
+
const where = {};
|
|
505
|
+
if (filter.object) where.object_name = filter.object;
|
|
506
|
+
if (filter.activeOnly) where.active = true;
|
|
507
|
+
const orgId = context?.organizationId ?? context?.tenantId;
|
|
508
|
+
if (orgId) where.organization_id = orgId;
|
|
509
|
+
const rows = await this.engine.find("sys_sharing_rule", {
|
|
510
|
+
filter: where,
|
|
511
|
+
orderBy: [{ field: "name", direction: "asc" }],
|
|
512
|
+
limit: 1e3,
|
|
513
|
+
context: SYSTEM_CTX4
|
|
514
|
+
});
|
|
515
|
+
return Array.isArray(rows) ? rows.map(rowFromRule) : [];
|
|
516
|
+
}
|
|
517
|
+
async getRule(idOrName, context) {
|
|
518
|
+
if (!idOrName) return null;
|
|
519
|
+
const orgId = context?.organizationId ?? context?.tenantId;
|
|
520
|
+
const byId = await this.engine.find("sys_sharing_rule", {
|
|
521
|
+
filter: { id: idOrName },
|
|
522
|
+
limit: 1,
|
|
523
|
+
context: SYSTEM_CTX4
|
|
524
|
+
});
|
|
525
|
+
if (Array.isArray(byId) && byId[0]) return rowFromRule(byId[0]);
|
|
526
|
+
const byName = await this.engine.find("sys_sharing_rule", {
|
|
527
|
+
filter: orgId ? { name: idOrName, organization_id: orgId } : { name: idOrName },
|
|
528
|
+
limit: 1,
|
|
529
|
+
context: SYSTEM_CTX4
|
|
530
|
+
});
|
|
531
|
+
if (Array.isArray(byName) && byName[0]) return rowFromRule(byName[0]);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
async deleteRule(idOrName, context) {
|
|
535
|
+
const row = await this.getRule(idOrName, context);
|
|
536
|
+
if (!row) return;
|
|
537
|
+
await this.engine.delete("sys_record_share", {
|
|
538
|
+
where: { source: "rule", source_id: row.id },
|
|
539
|
+
context: SYSTEM_CTX4
|
|
540
|
+
});
|
|
541
|
+
await this.engine.delete("sys_sharing_rule", {
|
|
542
|
+
where: { id: row.id },
|
|
543
|
+
context: SYSTEM_CTX4
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async evaluateRule(idOrName, context) {
|
|
547
|
+
const rule = await this.getRule(idOrName, context);
|
|
548
|
+
if (!rule) throw new Error("RULE_NOT_FOUND");
|
|
549
|
+
if (!rule.active) {
|
|
550
|
+
const revoked = await this.purgeRuleGrants(rule.id);
|
|
551
|
+
return { ruleId: rule.id, matchedRecords: 0, expandedUsers: 0, grantsCreated: 0, grantsUpdated: 0, grantsRevoked: revoked };
|
|
552
|
+
}
|
|
553
|
+
const matches = await this.findMatchingRecords(rule);
|
|
554
|
+
const users = await this.expandRecipient(rule);
|
|
555
|
+
return this.reconcile(rule, matches, users);
|
|
556
|
+
}
|
|
557
|
+
async evaluateAllForRecord(object, recordId, context) {
|
|
558
|
+
const rules = await this.listRules({ object, activeOnly: true }, context);
|
|
559
|
+
if (rules.length === 0) return [];
|
|
560
|
+
const results = [];
|
|
561
|
+
for (const rule of rules) {
|
|
562
|
+
const match = await this.recordMatches(rule, recordId);
|
|
563
|
+
const users = match ? await this.expandRecipient(rule) : [];
|
|
564
|
+
results.push(await this.reconcileForRecord(rule, recordId, match, users));
|
|
565
|
+
}
|
|
566
|
+
return results;
|
|
567
|
+
}
|
|
568
|
+
// ── internals ─────────────────────────────────────────────────────
|
|
569
|
+
async findMatchingRecords(rule) {
|
|
570
|
+
const filter = rule.criteria ?? {};
|
|
571
|
+
try {
|
|
572
|
+
const rows = await this.engine.find(rule.object_name, {
|
|
573
|
+
filter,
|
|
574
|
+
fields: ["id"],
|
|
575
|
+
limit: 5e3,
|
|
576
|
+
context: SYSTEM_CTX4
|
|
577
|
+
});
|
|
578
|
+
return Array.isArray(rows) ? rows.map((r) => String(r.id)).filter(Boolean) : [];
|
|
579
|
+
} catch (err) {
|
|
580
|
+
this.logger?.warn?.("[sharing-rule] criteria query failed", { rule: rule.name, error: err?.message });
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async recordMatches(rule, recordId) {
|
|
585
|
+
const filter = { ...rule.criteria ?? {}, id: recordId };
|
|
586
|
+
try {
|
|
587
|
+
const rows = await this.engine.find(rule.object_name, {
|
|
588
|
+
filter,
|
|
589
|
+
fields: ["id"],
|
|
590
|
+
limit: 1,
|
|
591
|
+
context: SYSTEM_CTX4
|
|
592
|
+
});
|
|
593
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async expandRecipient(rule) {
|
|
599
|
+
const team = new TeamGraphService({
|
|
600
|
+
engine: this.engine,
|
|
601
|
+
organizationId: rule.organization_id ?? null
|
|
602
|
+
});
|
|
603
|
+
if (rule.recipient_type === "user") return [rule.recipient_id];
|
|
604
|
+
if (rule.recipient_type === "team") return team.expandUsers(rule.recipient_id);
|
|
605
|
+
if (rule.recipient_type === "department") {
|
|
606
|
+
const dept = new DepartmentGraphService({
|
|
607
|
+
engine: this.engine,
|
|
608
|
+
organizationId: rule.organization_id ?? null,
|
|
609
|
+
teamGraph: team
|
|
610
|
+
});
|
|
611
|
+
return dept.expandUsers(rule.recipient_id);
|
|
612
|
+
}
|
|
613
|
+
if (rule.recipient_type === "role") return team.expandRoleUsers(rule.recipient_id, rule.organization_id ?? void 0);
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
async reconcile(rule, matchedIds, users) {
|
|
617
|
+
const existing = await this.engine.find("sys_record_share", {
|
|
618
|
+
filter: { source: "rule", source_id: rule.id },
|
|
619
|
+
fields: ["id", "record_id", "recipient_id", "access_level"],
|
|
620
|
+
limit: 1e5,
|
|
621
|
+
context: SYSTEM_CTX4
|
|
622
|
+
});
|
|
623
|
+
const desired = /* @__PURE__ */ new Map();
|
|
624
|
+
for (const rid of matchedIds) {
|
|
625
|
+
for (const uId of users) desired.set(`${rid}::${uId}`, { record_id: rid, recipient_id: uId });
|
|
626
|
+
}
|
|
627
|
+
const existingMap = /* @__PURE__ */ new Map();
|
|
628
|
+
for (const row of existing ?? []) existingMap.set(`${row.record_id}::${row.recipient_id}`, row);
|
|
629
|
+
let created = 0;
|
|
630
|
+
let updated = 0;
|
|
631
|
+
let revoked = 0;
|
|
632
|
+
for (const [k, want] of desired.entries()) {
|
|
633
|
+
const cur = existingMap.get(k);
|
|
634
|
+
if (cur) {
|
|
635
|
+
if (cur.access_level !== rule.access_level) {
|
|
636
|
+
await this.sharing.grant(
|
|
637
|
+
{
|
|
638
|
+
object: rule.object_name,
|
|
639
|
+
recordId: want.record_id,
|
|
640
|
+
recipientType: "user",
|
|
641
|
+
recipientId: want.recipient_id,
|
|
642
|
+
accessLevel: rule.access_level,
|
|
643
|
+
source: "rule",
|
|
644
|
+
sourceId: rule.id,
|
|
645
|
+
reason: `rule:${rule.name}`
|
|
646
|
+
},
|
|
647
|
+
SYSTEM_CTX4
|
|
648
|
+
);
|
|
649
|
+
updated += 1;
|
|
650
|
+
}
|
|
651
|
+
existingMap.delete(k);
|
|
652
|
+
} else {
|
|
653
|
+
await this.sharing.grant(
|
|
654
|
+
{
|
|
655
|
+
object: rule.object_name,
|
|
656
|
+
recordId: want.record_id,
|
|
657
|
+
recipientType: "user",
|
|
658
|
+
recipientId: want.recipient_id,
|
|
659
|
+
accessLevel: rule.access_level,
|
|
660
|
+
source: "rule",
|
|
661
|
+
sourceId: rule.id,
|
|
662
|
+
reason: `rule:${rule.name}`
|
|
663
|
+
},
|
|
664
|
+
SYSTEM_CTX4
|
|
665
|
+
);
|
|
666
|
+
created += 1;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
for (const [, stale] of existingMap.entries()) {
|
|
670
|
+
await this.sharing.revoke(stale.id, SYSTEM_CTX4);
|
|
671
|
+
revoked += 1;
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
ruleId: rule.id,
|
|
675
|
+
matchedRecords: matchedIds.length,
|
|
676
|
+
expandedUsers: users.length,
|
|
677
|
+
grantsCreated: created,
|
|
678
|
+
grantsUpdated: updated,
|
|
679
|
+
grantsRevoked: revoked
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
async reconcileForRecord(rule, recordId, match, users) {
|
|
683
|
+
const existing = await this.engine.find("sys_record_share", {
|
|
684
|
+
filter: { source: "rule", source_id: rule.id, record_id: recordId },
|
|
685
|
+
fields: ["id", "record_id", "recipient_id", "access_level"],
|
|
686
|
+
limit: 1e3,
|
|
687
|
+
context: SYSTEM_CTX4
|
|
688
|
+
});
|
|
689
|
+
const existingMap = /* @__PURE__ */ new Map();
|
|
690
|
+
for (const row of existing ?? []) existingMap.set(String(row.recipient_id), row);
|
|
691
|
+
let created = 0;
|
|
692
|
+
let updated = 0;
|
|
693
|
+
let revoked = 0;
|
|
694
|
+
if (match) {
|
|
695
|
+
for (const userId of users) {
|
|
696
|
+
const cur = existingMap.get(userId);
|
|
697
|
+
if (cur) {
|
|
698
|
+
if (cur.access_level !== rule.access_level) {
|
|
699
|
+
await this.sharing.grant(
|
|
700
|
+
{
|
|
701
|
+
object: rule.object_name,
|
|
702
|
+
recordId,
|
|
703
|
+
recipientType: "user",
|
|
704
|
+
recipientId: userId,
|
|
705
|
+
accessLevel: rule.access_level,
|
|
706
|
+
source: "rule",
|
|
707
|
+
sourceId: rule.id,
|
|
708
|
+
reason: `rule:${rule.name}`
|
|
709
|
+
},
|
|
710
|
+
SYSTEM_CTX4
|
|
711
|
+
);
|
|
712
|
+
updated += 1;
|
|
713
|
+
}
|
|
714
|
+
existingMap.delete(userId);
|
|
715
|
+
} else {
|
|
716
|
+
await this.sharing.grant(
|
|
717
|
+
{
|
|
718
|
+
object: rule.object_name,
|
|
719
|
+
recordId,
|
|
720
|
+
recipientType: "user",
|
|
721
|
+
recipientId: userId,
|
|
722
|
+
accessLevel: rule.access_level,
|
|
723
|
+
source: "rule",
|
|
724
|
+
sourceId: rule.id,
|
|
725
|
+
reason: `rule:${rule.name}`
|
|
726
|
+
},
|
|
727
|
+
SYSTEM_CTX4
|
|
728
|
+
);
|
|
729
|
+
created += 1;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
for (const [, stale] of existingMap.entries()) {
|
|
734
|
+
await this.sharing.revoke(stale.id, SYSTEM_CTX4);
|
|
735
|
+
revoked += 1;
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
ruleId: rule.id,
|
|
739
|
+
matchedRecords: match ? 1 : 0,
|
|
740
|
+
expandedUsers: users.length,
|
|
741
|
+
grantsCreated: created,
|
|
742
|
+
grantsUpdated: updated,
|
|
743
|
+
grantsRevoked: revoked
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
async purgeRuleGrants(ruleId) {
|
|
747
|
+
const existing = await this.engine.find("sys_record_share", {
|
|
748
|
+
filter: { source: "rule", source_id: ruleId },
|
|
749
|
+
fields: ["id"],
|
|
750
|
+
limit: 1e5,
|
|
751
|
+
context: SYSTEM_CTX4
|
|
752
|
+
});
|
|
753
|
+
let revoked = 0;
|
|
754
|
+
for (const row of existing ?? []) {
|
|
755
|
+
await this.sharing.revoke(row.id, SYSTEM_CTX4);
|
|
756
|
+
revoked += 1;
|
|
757
|
+
}
|
|
758
|
+
return revoked;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// src/rule-hooks.ts
|
|
763
|
+
var SYSTEM_CTX5 = { isSystem: true, roles: [], permissions: [] };
|
|
764
|
+
var SHARING_RULE_HOOK_PACKAGE = "plugin-sharing:rules";
|
|
765
|
+
function bindRuleHooks(engine, service, rules, logger) {
|
|
766
|
+
const objects = /* @__PURE__ */ new Set();
|
|
767
|
+
for (const r of rules) {
|
|
768
|
+
if (r.active === false) continue;
|
|
769
|
+
if (r.object_name) objects.add(r.object_name);
|
|
770
|
+
}
|
|
771
|
+
for (const objectName of objects) {
|
|
772
|
+
const handler = async (ctx) => {
|
|
773
|
+
if (ctx?.session?.isSystem) return;
|
|
774
|
+
try {
|
|
775
|
+
const data = ctx?.result ?? ctx?.input?.data ?? {};
|
|
776
|
+
const id = String(data?.id ?? ctx?.input?.id ?? "");
|
|
777
|
+
if (!id) return;
|
|
778
|
+
await service.evaluateAllForRecord(objectName, id, SYSTEM_CTX5);
|
|
779
|
+
} catch (err) {
|
|
780
|
+
logger?.warn?.("[sharing-rule] hook evaluation failed", { object: objectName, error: err?.message });
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
engine.registerHook("afterInsert", handler, { object: objectName, packageId: SHARING_RULE_HOOK_PACKAGE, priority: 180 });
|
|
784
|
+
engine.registerHook("afterUpdate", handler, { object: objectName, packageId: SHARING_RULE_HOOK_PACKAGE, priority: 180 });
|
|
785
|
+
}
|
|
786
|
+
logger?.info?.("[sharing-rule] hooks bound", { objects: Array.from(objects), ruleCount: rules.length });
|
|
787
|
+
}
|
|
788
|
+
function unbindAllRuleHooks(engine) {
|
|
789
|
+
return engine.unregisterHooksByPackage(SHARING_RULE_HOOK_PACKAGE);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// src/sharing-plugin.ts
|
|
793
|
+
import { SysRecordShare, SysSharingRule } from "@objectstack/platform-objects/security";
|
|
794
|
+
import { SysDepartment, SysDepartmentMember } from "@objectstack/platform-objects/identity";
|
|
795
|
+
var SharingServicePlugin = class {
|
|
796
|
+
constructor(options = {}) {
|
|
797
|
+
this.name = "com.objectstack.service.sharing";
|
|
798
|
+
this.version = "1.0.0";
|
|
799
|
+
this.type = "standard";
|
|
800
|
+
this.dependencies = ["com.objectstack.engine.objectql"];
|
|
801
|
+
this.options = options;
|
|
802
|
+
}
|
|
803
|
+
async init(ctx) {
|
|
804
|
+
ctx.getService("manifest").register({
|
|
805
|
+
id: "com.objectstack.service.sharing",
|
|
806
|
+
name: "Sharing Service",
|
|
807
|
+
version: "1.0.0",
|
|
808
|
+
type: "plugin",
|
|
809
|
+
scope: "system",
|
|
810
|
+
defaultDatasource: "cloud",
|
|
811
|
+
namespace: "sys",
|
|
812
|
+
objects: [SysRecordShare, SysSharingRule, SysDepartment, SysDepartmentMember]
|
|
813
|
+
});
|
|
814
|
+
ctx.logger.info("SharingServicePlugin: schema registered");
|
|
815
|
+
}
|
|
816
|
+
async start(ctx) {
|
|
817
|
+
ctx.hook("kernel:ready", async () => {
|
|
818
|
+
let engine = null;
|
|
819
|
+
try {
|
|
820
|
+
engine = ctx.getService("objectql");
|
|
821
|
+
} catch {
|
|
822
|
+
try {
|
|
823
|
+
engine = ctx.getService("data");
|
|
824
|
+
} catch {
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (!engine) {
|
|
828
|
+
ctx.logger.warn("SharingServicePlugin: no ObjectQL engine \u2014 service NOT registered");
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
this.service = new SharingService({
|
|
832
|
+
engine,
|
|
833
|
+
bypassObjects: this.options.bypassObjects
|
|
834
|
+
});
|
|
835
|
+
ctx.registerService("sharing", this.service);
|
|
836
|
+
if (this.options.enforce === false) {
|
|
837
|
+
ctx.logger.info("SharingServicePlugin: enforcement disabled (enforce=false)");
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const mw = buildSharingMiddleware(this.service);
|
|
841
|
+
if (typeof engine.registerMiddleware === "function") {
|
|
842
|
+
engine.registerMiddleware(mw, { object: "*" });
|
|
843
|
+
ctx.logger.info("SharingServicePlugin: enforcement middleware installed");
|
|
844
|
+
} else {
|
|
845
|
+
ctx.logger.warn("SharingServicePlugin: engine has no registerMiddleware \u2014 enforcement not applied");
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
this.ruleService = new SharingRuleService({
|
|
849
|
+
engine,
|
|
850
|
+
sharing: this.service,
|
|
851
|
+
logger: ctx.logger
|
|
852
|
+
});
|
|
853
|
+
ctx.registerService("sharingRules", this.ruleService);
|
|
854
|
+
if (typeof engine.registerHook === "function" && typeof engine.unregisterHooksByPackage === "function") {
|
|
855
|
+
const rules = await this.ruleService.listRules({ activeOnly: true }, { isSystem: true });
|
|
856
|
+
unbindAllRuleHooks(engine);
|
|
857
|
+
bindRuleHooks(engine, this.ruleService, rules, ctx.logger);
|
|
858
|
+
} else {
|
|
859
|
+
ctx.logger.warn("SharingServicePlugin: engine has no hook API \u2014 sharing rule auto-evaluation disabled");
|
|
860
|
+
}
|
|
861
|
+
} catch (err) {
|
|
862
|
+
ctx.logger.warn("SharingServicePlugin: sharing-rule subsystem not started", { error: err?.message });
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
function buildSharingMiddleware(service) {
|
|
868
|
+
return async function sharingMiddleware(ctx, next) {
|
|
869
|
+
const op = ctx.operation;
|
|
870
|
+
const exec = ctx.context;
|
|
871
|
+
if (op === "find" || op === "findOne" || op === "count" || op === "aggregate") {
|
|
872
|
+
const filter = await service.buildReadFilter(ctx.object, exec ?? {});
|
|
873
|
+
if (filter) {
|
|
874
|
+
const ast = ctx.ast ?? {};
|
|
875
|
+
ast.where = composeAnd(ast.where, filter);
|
|
876
|
+
ast.filter = composeAnd(ast.filter, filter);
|
|
877
|
+
ctx.ast = ast;
|
|
878
|
+
}
|
|
879
|
+
return next();
|
|
880
|
+
}
|
|
881
|
+
if (op === "update" || op === "delete") {
|
|
882
|
+
const data = ctx.data;
|
|
883
|
+
const options = ctx.options;
|
|
884
|
+
const id = inferTargetId(data, options);
|
|
885
|
+
if (id != null) {
|
|
886
|
+
const ok = await service.canEdit(ctx.object, String(id), exec ?? {});
|
|
887
|
+
if (!ok) {
|
|
888
|
+
const err = new Error(
|
|
889
|
+
`FORBIDDEN: insufficient privileges to ${op} ${ctx.object} ${id}`
|
|
890
|
+
);
|
|
891
|
+
err.code = "FORBIDDEN";
|
|
892
|
+
err.status = 403;
|
|
893
|
+
throw err;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return next();
|
|
897
|
+
}
|
|
898
|
+
return next();
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
function composeAnd(existing, addition) {
|
|
902
|
+
if (existing == null) return addition;
|
|
903
|
+
if (addition == null) return existing;
|
|
904
|
+
if (typeof existing === "object" && existing !== null && !Array.isArray(existing) && typeof addition === "object" && addition !== null && !Array.isArray(addition)) {
|
|
905
|
+
const ex = existing;
|
|
906
|
+
if (Array.isArray(ex.$and)) {
|
|
907
|
+
return { $and: [...ex.$and, addition] };
|
|
908
|
+
}
|
|
909
|
+
return { $and: [existing, addition] };
|
|
910
|
+
}
|
|
911
|
+
return { $and: [existing, addition] };
|
|
912
|
+
}
|
|
913
|
+
function inferTargetId(data, options) {
|
|
914
|
+
if (data && typeof data === "object" && data.id != null) return data.id;
|
|
915
|
+
if (options && typeof options === "object") {
|
|
916
|
+
if (options.id != null) return options.id;
|
|
917
|
+
if (options.where && typeof options.where === "object" && options.where.id != null) {
|
|
918
|
+
return options.where.id;
|
|
919
|
+
}
|
|
920
|
+
if (options.filter && typeof options.filter === "object" && options.filter.id != null) {
|
|
921
|
+
return options.filter.id;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return void 0;
|
|
925
|
+
}
|
|
926
|
+
export {
|
|
927
|
+
DepartmentGraphService,
|
|
928
|
+
SHARING_RULE_HOOK_PACKAGE,
|
|
929
|
+
SharingRuleService,
|
|
930
|
+
SharingService,
|
|
931
|
+
SharingServicePlugin,
|
|
932
|
+
SysDepartment2 as SysDepartment,
|
|
933
|
+
SysDepartmentMember2 as SysDepartmentMember,
|
|
934
|
+
SysRecordShare2 as SysRecordShare,
|
|
935
|
+
SysSharingRule2 as SysSharingRule,
|
|
936
|
+
TeamGraphService,
|
|
937
|
+
bindRuleHooks,
|
|
938
|
+
buildSharingMiddleware,
|
|
939
|
+
expandPrincipal,
|
|
940
|
+
unbindAllRuleHooks
|
|
941
|
+
};
|
|
942
|
+
//# sourceMappingURL=index.mjs.map
|