@7365admin1/core 3.130.0 → 3.131.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/CHANGELOG.md +6 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +113 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +113 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/member-direct-grantor.e2e.test.mjs +137 -0
- package/test/e2e/org-members-people.e2e.test.mjs +137 -0
- package/test/e2e/privilege-escalation.e2e.test.mjs +12 -6
- package/test/role-member-org-scope.test.mjs +4 -3
package/dist/index.mjs
CHANGED
|
@@ -3703,6 +3703,63 @@ function useMemberRepo() {
|
|
|
3703
3703
|
throw error;
|
|
3704
3704
|
}
|
|
3705
3705
|
}
|
|
3706
|
+
async function peoplePage(query2, page2, limit, rowStages) {
|
|
3707
|
+
const noSite = { $in: ["", null] };
|
|
3708
|
+
const orgWide = await collection.distinct("user", {
|
|
3709
|
+
org: query2.org,
|
|
3710
|
+
type: query2.type,
|
|
3711
|
+
siteId: noSite,
|
|
3712
|
+
status: { $ne: "deleted" }
|
|
3713
|
+
});
|
|
3714
|
+
const { type: orgType, siteId: pinned, ...rest } = query2;
|
|
3715
|
+
const limitedRow = {
|
|
3716
|
+
siteId: pinned ?? { $nin: ["", null] },
|
|
3717
|
+
user: { $nin: orgWide }
|
|
3718
|
+
};
|
|
3719
|
+
const head = [
|
|
3720
|
+
// `$text` (search) may only sit in the first stage.
|
|
3721
|
+
{ $match: { ...rest, type: { $nin: ["resident", "admin"] } } },
|
|
3722
|
+
{ $match: pinned ? limitedRow : { $or: [{ type: orgType }, limitedRow] } },
|
|
3723
|
+
{ $sort: { _id: -1 } },
|
|
3724
|
+
{
|
|
3725
|
+
$group: {
|
|
3726
|
+
_id: { $ifNull: ["$user", "$_id"] },
|
|
3727
|
+
doc: { $first: "$$ROOT" },
|
|
3728
|
+
orgWide: {
|
|
3729
|
+
$max: { $and: [{ $eq: ["$type", orgType] }, { $in: [{ $ifNull: ["$siteId", ""] }, ["", null]] }] }
|
|
3730
|
+
},
|
|
3731
|
+
limitedTo: { $push: { siteId: "$siteId", siteName: "$siteName", role: "$role" } }
|
|
3732
|
+
}
|
|
3733
|
+
},
|
|
3734
|
+
{
|
|
3735
|
+
$replaceRoot: {
|
|
3736
|
+
newRoot: { $mergeObjects: ["$doc", { limitedTo: { $cond: ["$orgWide", [], "$limitedTo"] } }] }
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
];
|
|
3740
|
+
const items = await collection.aggregate([...head, { $sort: { _id: -1 } }, { $skip: page2 * limit }, { $limit: limit }, ...rowStages]).toArray();
|
|
3741
|
+
const [counted] = await collection.aggregate([...head, { $count: "n" }]).toArray();
|
|
3742
|
+
const ids2 = (key) => [...new Set(items.flatMap((i) => (i.limitedTo ?? []).map((l) => l[key]?.toString?.() ?? "")))].filter((id) => ObjectId14.isValid(id)).map((id) => new ObjectId14(id));
|
|
3743
|
+
const [sites, roles] = await Promise.all([
|
|
3744
|
+
db2.collection("sites").find({ _id: { $in: ids2("siteId") } }, { projection: { name: 1 } }).toArray(),
|
|
3745
|
+
db2.collection("roles").find({ _id: { $in: ids2("role") } }, { projection: { name: 1 } }).toArray()
|
|
3746
|
+
]);
|
|
3747
|
+
const nameOf = (list) => new Map(list.map((d) => [d._id.toString(), d.name ?? ""]));
|
|
3748
|
+
const siteName = nameOf(sites);
|
|
3749
|
+
const roleName = nameOf(roles);
|
|
3750
|
+
for (const item of items) {
|
|
3751
|
+
const bySite = /* @__PURE__ */ new Map();
|
|
3752
|
+
for (const l of item.limitedTo ?? []) {
|
|
3753
|
+
const site = l.siteId?.toString?.() ?? "";
|
|
3754
|
+
const role = roleName.get(l.role?.toString?.() ?? "") ?? "";
|
|
3755
|
+
const entry = bySite.get(site) ?? { siteId: site, siteName: siteName.get(site) || l.siteName || "", roleName: "" };
|
|
3756
|
+
entry.roleName = [entry.roleName, role].filter(Boolean).join(", ");
|
|
3757
|
+
bySite.set(site, entry);
|
|
3758
|
+
}
|
|
3759
|
+
item.limitedTo = [...bySite.values()].sort((a, b) => a.siteName.localeCompare(b.siteName));
|
|
3760
|
+
}
|
|
3761
|
+
return { items, length: counted?.n ?? 0 };
|
|
3762
|
+
}
|
|
3706
3763
|
async function getAll({
|
|
3707
3764
|
search = "",
|
|
3708
3765
|
page: page2 = 1,
|
|
@@ -3713,7 +3770,8 @@ function useMemberRepo() {
|
|
|
3713
3770
|
siteIds,
|
|
3714
3771
|
type = "",
|
|
3715
3772
|
status = "",
|
|
3716
|
-
roleState = ""
|
|
3773
|
+
roleState = "",
|
|
3774
|
+
people = false
|
|
3717
3775
|
}) {
|
|
3718
3776
|
limit = limit > 0 ? limit : 10;
|
|
3719
3777
|
search = search || "";
|
|
@@ -3772,6 +3830,9 @@ function useMemberRepo() {
|
|
|
3772
3830
|
query2.$or = await otherSiteRoleClauses2(query2.org);
|
|
3773
3831
|
cacheOptions.roleState = roleState;
|
|
3774
3832
|
}
|
|
3833
|
+
const peopleMode = people && type === "organization" && !!query2.org && !siteId && !roleState;
|
|
3834
|
+
if (peopleMode)
|
|
3835
|
+
cacheOptions.people = 1;
|
|
3775
3836
|
const cacheKey = makeCacheKey5(namespace_collection3, cacheOptions);
|
|
3776
3837
|
const cachedData = await getCache(cacheKey);
|
|
3777
3838
|
if (cachedData) {
|
|
@@ -3779,15 +3840,7 @@ function useMemberRepo() {
|
|
|
3779
3840
|
return cachedData;
|
|
3780
3841
|
}
|
|
3781
3842
|
try {
|
|
3782
|
-
const
|
|
3783
|
-
{ $match: query2 },
|
|
3784
|
-
{ $sort: { _id: -1 } },
|
|
3785
|
-
// `paginate()` only LABELS the range ("11-20 of 42"); it does not
|
|
3786
|
-
// slice. Without this $skip every page returned page 1's rows under
|
|
3787
|
-
// a later page's heading. `page` is already zero-based above, and
|
|
3788
|
-
// this is the same $skip/$limit pair the two getOrgs* reads use.
|
|
3789
|
-
{ $skip: page2 * limit },
|
|
3790
|
-
{ $limit: limit },
|
|
3843
|
+
const rowStages = [
|
|
3791
3844
|
{
|
|
3792
3845
|
$lookup: {
|
|
3793
3846
|
from: "roles",
|
|
@@ -3861,11 +3914,29 @@ function useMemberRepo() {
|
|
|
3861
3914
|
dateInvited: { $ifNull: ["$dateInvited", "$createdAt"] },
|
|
3862
3915
|
siteId: 1,
|
|
3863
3916
|
siteName: 1,
|
|
3864
|
-
type: 1
|
|
3917
|
+
type: 1,
|
|
3918
|
+
limitedTo: 1
|
|
3865
3919
|
}
|
|
3866
3920
|
}
|
|
3867
|
-
]
|
|
3868
|
-
|
|
3921
|
+
];
|
|
3922
|
+
let items;
|
|
3923
|
+
let length;
|
|
3924
|
+
if (peopleMode) {
|
|
3925
|
+
({ items, length } = await peoplePage(query2, page2, limit, rowStages));
|
|
3926
|
+
} else {
|
|
3927
|
+
items = await collection.aggregate([
|
|
3928
|
+
{ $match: query2 },
|
|
3929
|
+
{ $sort: { _id: -1 } },
|
|
3930
|
+
// `paginate()` only LABELS the range ("11-20 of 42"); it does not
|
|
3931
|
+
// slice. Without this $skip every page returned page 1's rows under
|
|
3932
|
+
// a later page's heading. `page` is already zero-based above, and
|
|
3933
|
+
// this is the same $skip/$limit pair the two getOrgs* reads use.
|
|
3934
|
+
{ $skip: page2 * limit },
|
|
3935
|
+
{ $limit: limit },
|
|
3936
|
+
...rowStages
|
|
3937
|
+
]).toArray();
|
|
3938
|
+
length = await collection.countDocuments(query2);
|
|
3939
|
+
}
|
|
3869
3940
|
const data = { ...paginate5(items, page2, limit, length), total: length };
|
|
3870
3941
|
setCache(cacheKey, data, 15 * 60).then(() => {
|
|
3871
3942
|
logger8.info(`Cache set for key: ${cacheKey}`);
|
|
@@ -23028,6 +23099,29 @@ async function requireNotOwnerUnlessOwner(req, orgId, targetUser) {
|
|
|
23028
23099
|
return;
|
|
23029
23100
|
throw new UnauthorizedError3("Only the organisation's owner can change the owner's access.");
|
|
23030
23101
|
}
|
|
23102
|
+
async function requireMemberGrantor(req, input) {
|
|
23103
|
+
const { orgId, targetUser, roleId } = input;
|
|
23104
|
+
const siteId = input.siteId?.toString() ?? "";
|
|
23105
|
+
const id = await requireSiteAccessEditor(req, orgId);
|
|
23106
|
+
await requireNotOwnerUnlessOwner(req, orgId, targetUser);
|
|
23107
|
+
if (staffMayBypass(await resolveInviteActor(id)))
|
|
23108
|
+
return;
|
|
23109
|
+
if (siteId)
|
|
23110
|
+
await requireSiteServedByOrg(siteId, orgId);
|
|
23111
|
+
if (await hasOrgOwnership(id, orgId))
|
|
23112
|
+
return;
|
|
23113
|
+
const db2 = useAtlas36.getDb();
|
|
23114
|
+
if (!db2 || !ObjectId54.isValid(roleId))
|
|
23115
|
+
return;
|
|
23116
|
+
const role = await db2.collection("roles").findOne({ _id: new ObjectId54(roleId) }, { projection: { permissions: 1 } });
|
|
23117
|
+
if (isOwnerRole(role)) {
|
|
23118
|
+
const rows = await db2.collection("members").find({ user: new ObjectId54(id), org: new ObjectId54(orgId), status: "active" }, { projection: { role: 1 } }).toArray();
|
|
23119
|
+
const held = rows.map((m) => m.role?.toString?.() ?? "").filter((r) => ObjectId54.isValid(r)).map((r) => new ObjectId54(r));
|
|
23120
|
+
const holdsOwner = held.length ? await db2.collection("roles").countDocuments({ _id: { $in: held }, permissions: "*", status: "active" }) : 0;
|
|
23121
|
+
if (!holdsOwner)
|
|
23122
|
+
throw new UnauthorizedError3("Only the organisation's owner can give an owner role.");
|
|
23123
|
+
}
|
|
23124
|
+
}
|
|
23031
23125
|
async function requireSiteReachInOrg(req, siteId, orgId) {
|
|
23032
23126
|
if (await hasOrgOwnership(callerId(req), orgId)) {
|
|
23033
23127
|
await requireSiteServedByOrg(siteId, orgId);
|
|
@@ -28311,7 +28405,9 @@ function useMemberController() {
|
|
|
28311
28405
|
siteId: Joi29.string().hex().optional().allow("", null),
|
|
28312
28406
|
type: Joi29.string().required(),
|
|
28313
28407
|
status: Joi29.string().required(),
|
|
28314
|
-
roleState: Joi29.string().valid("other-site").optional().allow("", null)
|
|
28408
|
+
roleState: Joi29.string().valid("other-site").optional().allow("", null),
|
|
28409
|
+
// Organization app Members: every person, site-limited ones included.
|
|
28410
|
+
people: Joi29.boolean().truthy("1").falsy("0").optional().allow("", null)
|
|
28315
28411
|
});
|
|
28316
28412
|
const query2 = { ...req.query };
|
|
28317
28413
|
const { error } = validation.validate(query2);
|
|
@@ -28366,7 +28462,8 @@ function useMemberController() {
|
|
|
28366
28462
|
siteIds,
|
|
28367
28463
|
type,
|
|
28368
28464
|
status,
|
|
28369
|
-
roleState: req.query.roleState ?? ""
|
|
28465
|
+
roleState: req.query.roleState ?? "",
|
|
28466
|
+
people: ["1", "true"].includes(String(req.query.people ?? ""))
|
|
28370
28467
|
});
|
|
28371
28468
|
res.json(items);
|
|
28372
28469
|
return;
|
|
@@ -28507,7 +28604,7 @@ function useMemberController() {
|
|
|
28507
28604
|
await requireOrgReach(req, orgId);
|
|
28508
28605
|
await requireSelfEnrolRole(req, orgId, roleId);
|
|
28509
28606
|
} else {
|
|
28510
|
-
await
|
|
28607
|
+
await requireMemberGrantor(req, { orgId, targetUser: userId, roleId, siteId });
|
|
28511
28608
|
}
|
|
28512
28609
|
const data = await _createMemberDirect({ userId, orgId, roleId, app, siteId, siteName, onboardingRequired, callerId: callerId(req) });
|
|
28513
28610
|
res.status(201).json(data);
|