@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/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// POST /api/members/direct enrolling SOMEBODY ELSE (follow-up of Site access,
|
|
2
|
+
// 2026-09-29): it needed only org membership, so a member confined to ONE site
|
|
3
|
+
// could create another person's membership anywhere in the organisation with
|
|
4
|
+
// any of its roles. Now the Site access editor rule applies (core
|
|
5
|
+
// `requireMemberGrantor`): the owner, cross-client staff, or an org-WIDE member
|
|
6
|
+
// who may assign roles - never a site-confined member - and never beyond what
|
|
7
|
+
// the organisation can give at a site it serves, nor an owner role the caller
|
|
8
|
+
// does not hold, nor the owner's own access.
|
|
9
|
+
//
|
|
10
|
+
// Nothing outside the harness is touched. Run with: yarn test:e2e
|
|
11
|
+
|
|
12
|
+
import { after, before, describe, it } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { ObjectId } from "mongodb";
|
|
15
|
+
|
|
16
|
+
import { startHarness } from "./harness.mjs";
|
|
17
|
+
|
|
18
|
+
const PASSWORD = "DirectGrant-Passw0rd!";
|
|
19
|
+
const SEC = "security_agency";
|
|
20
|
+
const U = {
|
|
21
|
+
owner: "mdg-owner@e2e.example.com",
|
|
22
|
+
orgAdmin: "mdg-org-admin@e2e.example.com",
|
|
23
|
+
assigner: "mdg-assigner@e2e.example.com",
|
|
24
|
+
viewer: "mdg-viewer@e2e.example.com",
|
|
25
|
+
siteAdmin: "mdg-site-admin@e2e.example.com",
|
|
26
|
+
support: "mdg-support@e2e.example.com",
|
|
27
|
+
n1: "mdg-n1@e2e.example.com",
|
|
28
|
+
n2: "mdg-n2@e2e.example.com",
|
|
29
|
+
n3: "mdg-n3@e2e.example.com",
|
|
30
|
+
n4: "mdg-n4@e2e.example.com",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("members/direct: enrolling somebody else", { concurrency: 1 }, () => {
|
|
34
|
+
let h;
|
|
35
|
+
const sid = {};
|
|
36
|
+
const id = {};
|
|
37
|
+
|
|
38
|
+
before(async () => {
|
|
39
|
+
h = await startHarness();
|
|
40
|
+
Object.assign(id, await seed(h));
|
|
41
|
+
for (const k of ["owner", "orgAdmin", "assigner", "viewer", "siteAdmin"]) sid[k] = await h.login(U[k], PASSWORD);
|
|
42
|
+
}, { timeout: 300000 });
|
|
43
|
+
|
|
44
|
+
after(async () => {
|
|
45
|
+
if (h) await h.stop();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const refused = (res) => [401, 403].includes(res.status);
|
|
49
|
+
const refused404 = (res) => [400, 401, 403, 404].includes(res.status);
|
|
50
|
+
const direct = (who, target, roleId, siteId = "", app = SEC) =>
|
|
51
|
+
h.api("/members/direct", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
sid: sid[who],
|
|
54
|
+
body: { userId: String(id.users[target]), orgId: String(id.org), roleId: String(roleId), app, siteId: siteId ? String(siteId) : "", siteName: "" },
|
|
55
|
+
});
|
|
56
|
+
const rows = (target) => h.db.collection("members").countDocuments({ user: id.users[target] });
|
|
57
|
+
|
|
58
|
+
it("1. ESCALATION: a site-confined member cannot enrol anybody - at their own site, another site, or org-wide", async () => {
|
|
59
|
+
assert.ok(refused(await direct("siteAdmin", "n1", id.X, id.A)));
|
|
60
|
+
assert.ok(refused(await direct("siteAdmin", "n1", id.Y, id.B)));
|
|
61
|
+
assert.ok(refused(await direct("siteAdmin", "n1", id.orgRole, "", "organization")));
|
|
62
|
+
assert.equal(await rows("n1"), 0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("2. an org-wide member without Assign Role permission cannot enrol anybody", async () => {
|
|
66
|
+
assert.ok(refused(await direct("viewer", "n1", id.X, id.A)));
|
|
67
|
+
assert.equal(await rows("n1"), 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("3. the owner enrols a colleague at a site, and the Seven365 support account org-wide with the owner role", async () => {
|
|
71
|
+
const res = await direct("owner", "n1", id.X, id.A);
|
|
72
|
+
assert.equal(res.status, 201, JSON.stringify(res.body));
|
|
73
|
+
const sup = await direct("owner", "support", id.ownerRole, "", "organization");
|
|
74
|
+
assert.equal(sup.status, 201, JSON.stringify(sup.body));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("4. an org-wide admin enrols a colleague with that site's role", async () => {
|
|
78
|
+
const res = await direct("orgAdmin", "n2", id.Y, id.B);
|
|
79
|
+
assert.equal(res.status, 201, JSON.stringify(res.body));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("5. the admin cannot go beyond the organisation: another site's role, another client's role or site", async () => {
|
|
83
|
+
assert.equal((await direct("orgAdmin", "n3", id.X, id.B)).status, 400, "role of site A at site B");
|
|
84
|
+
assert.equal((await direct("orgAdmin", "n3", id.foreignRole, id.A)).status, 400, "another client's role");
|
|
85
|
+
{ const r = await direct("orgAdmin", "n3", id.foreignRole, id.foreign); assert.ok([400, 401, 403, 404].includes(r.status), `another client site: ${r.status} ${JSON.stringify(r.body)}`); }
|
|
86
|
+
assert.ok(refused404(await direct("owner", "n3", id.X, id.foreign)), "not even the owner at another client's site");
|
|
87
|
+
assert.equal(await rows("n3"), 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("6. an owner role only from somebody who holds one; the owner's access never from an admin", async () => {
|
|
91
|
+
assert.ok(refused(await direct("assigner", "n4", id.ownerRole, "", "organization")));
|
|
92
|
+
assert.equal(await rows("n4"), 0);
|
|
93
|
+
assert.equal((await direct("assigner", "n4", id.Y, id.B)).status, 201, "a site role is fine");
|
|
94
|
+
assert.ok(refused(await direct("orgAdmin", "owner", id.Z, id.C)));
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
async function seed(h) {
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
const org = new ObjectId(), other = new ObjectId();
|
|
101
|
+
const A = new ObjectId(), B = new ObjectId(), C = new ObjectId(), foreign = new ObjectId();
|
|
102
|
+
const X = new ObjectId(), Y = new ObjectId(), Z = new ObjectId(), orgRole = new ObjectId(), ownerRole = new ObjectId();
|
|
103
|
+
const assignRole = new ObjectId(), viewRole = new ObjectId(), foreignRole = new ObjectId();
|
|
104
|
+
|
|
105
|
+
await h.db.collection("organizations").insertMany([
|
|
106
|
+
{ _id: org, name: "Aigle MDG", email: "aigle-mdg@e2e.example.com", type: "org", nature: SEC, status: "active", createdAt: now },
|
|
107
|
+
{ _id: other, name: "Other MDG", email: "other-mdg@e2e.example.com", type: "org", nature: SEC, status: "active", createdAt: now },
|
|
108
|
+
]);
|
|
109
|
+
const site = (_id, name, o) => ({ _id, name, org: o, orgId: o, status: "active", createdAt: now });
|
|
110
|
+
await h.db.collection("sites").insertMany([site(A, "One Pearl MDG", org), site(B, "The Loft MDG", org), site(C, "Third MDG", org), site(foreign, "Foreign MDG", other)]);
|
|
111
|
+
await h.db.collection("roles").insertMany([
|
|
112
|
+
{ _id: X, name: "X at A", type: SEC, org, site: A, permissions: ["*"], status: "active" },
|
|
113
|
+
{ _id: Y, name: "Y at B", type: SEC, org, site: B, permissions: ["dashboard:view"], status: "active" },
|
|
114
|
+
{ _id: Z, name: "Z at C", type: SEC, org, site: C, permissions: ["dashboard:view"], status: "active" },
|
|
115
|
+
{ _id: orgRole, name: "Org Admin", type: "organization", org, site: "", permissions: ["*"], status: "active" },
|
|
116
|
+
{ _id: ownerRole, name: "owner", type: "organization", org, site: "", permissions: ["*"], default: true, status: "active" },
|
|
117
|
+
{ _id: assignRole, name: "Assigner", type: "organization", org, site: "", permissions: ["members:assign-member-role"], status: "active" },
|
|
118
|
+
{ _id: viewRole, name: "Viewer", type: "organization", org, site: "", permissions: ["dashboard:view"], status: "active" },
|
|
119
|
+
{ _id: foreignRole, name: "Foreign", type: SEC, org: other, site: foreign, permissions: ["*"], status: "active" },
|
|
120
|
+
]);
|
|
121
|
+
const users = {};
|
|
122
|
+
for (const [k, email] of Object.entries(U)) {
|
|
123
|
+
const { insertedId } = await h.db.collection("users").insertOne({ email, name: k, status: "active", createdAt: now });
|
|
124
|
+
await h.db.collection("users").updateOne({ _id: insertedId }, { $set: { password: await h.hashPassword(PASSWORD) } });
|
|
125
|
+
users[k] = insertedId;
|
|
126
|
+
}
|
|
127
|
+
await h.db.collection("organizations").updateOne({ _id: org }, { $set: { createdBy: users.owner } });
|
|
128
|
+
const m = (k, role, type = "organization", siteId = "") => ({ user: users[k], email: U[k], org, type, role, siteId, status: "active", deletedAt: "", createdAt: now });
|
|
129
|
+
await h.db.collection("members").insertMany([
|
|
130
|
+
m("owner", ownerRole),
|
|
131
|
+
m("orgAdmin", orgRole),
|
|
132
|
+
m("assigner", assignRole),
|
|
133
|
+
m("viewer", viewRole),
|
|
134
|
+
m("siteAdmin", X, SEC, A),
|
|
135
|
+
]);
|
|
136
|
+
return { org, other, A, B, C, foreign, X, Y, Z, orgRole, ownerRole, foreignRole, users };
|
|
137
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Organization app -> Members lists EVERY person of the organisation (owner
|
|
2
|
+
// report 2026-09-29, Aigle / Kanesan): after the owner removed Kanesan's
|
|
3
|
+
// organisation access and limited him to sites, he vanished from the list, so
|
|
4
|
+
// Site access could not be opened again. `GET /members?type=organization&
|
|
5
|
+
// people=1` now returns the organisation-wide rows as before PLUS one row per
|
|
6
|
+
// site-limited person carrying `limitedTo` (site + role). Residents, Seven365
|
|
7
|
+
// staff, other organisations and removed rows never appear. Without `people`
|
|
8
|
+
// the answer is unchanged.
|
|
9
|
+
//
|
|
10
|
+
// Nothing outside the harness is touched. Run with: yarn test:e2e
|
|
11
|
+
|
|
12
|
+
import { after, before, describe, it } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { ObjectId } from "mongodb";
|
|
15
|
+
|
|
16
|
+
import { startHarness } from "./harness.mjs";
|
|
17
|
+
|
|
18
|
+
const PASSWORD = "OrgPeople-Passw0rd!";
|
|
19
|
+
const SEC = "security_agency";
|
|
20
|
+
const U = {
|
|
21
|
+
owner: "mop-owner@e2e.example.com",
|
|
22
|
+
admin: "mop-admin@e2e.example.com",
|
|
23
|
+
kanesan: "mop-kanesan@e2e.example.com",
|
|
24
|
+
both: "mop-both@e2e.example.com",
|
|
25
|
+
siteGuard: "mop-guard@e2e.example.com",
|
|
26
|
+
pinnedOrg: "mop-pinned-org@e2e.example.com",
|
|
27
|
+
resident: "mop-resident@e2e.example.com",
|
|
28
|
+
gone: "mop-gone@e2e.example.com",
|
|
29
|
+
outsider: "mop-outsider@e2e.example.com",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
describe("org Members: every person, site-limited ones included", { concurrency: 1 }, () => {
|
|
33
|
+
let h;
|
|
34
|
+
const sid = {};
|
|
35
|
+
const id = {};
|
|
36
|
+
|
|
37
|
+
before(async () => {
|
|
38
|
+
h = await startHarness();
|
|
39
|
+
Object.assign(id, await seed(h));
|
|
40
|
+
for (const k of ["owner", "siteGuard"]) sid[k] = await h.login(U[k], PASSWORD);
|
|
41
|
+
}, { timeout: 300000 });
|
|
42
|
+
|
|
43
|
+
after(async () => {
|
|
44
|
+
if (h) await h.stop();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const list = (who, extra = "&people=1", status = "active") =>
|
|
48
|
+
h.api(`/members?org=${id.org}&type=organization&status=${status}&page=1&limit=50${extra}`, { sid: sid[who] });
|
|
49
|
+
const byEmail = (res) => Object.fromEntries(res.body.items.map((i) => [i.email, i]));
|
|
50
|
+
|
|
51
|
+
it("1. the owner sees every person once: org-wide people with no limit, Kanesan limited to A and B", async () => {
|
|
52
|
+
const res = await list("owner");
|
|
53
|
+
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
54
|
+
const rows = byEmail(res);
|
|
55
|
+
assert.deepEqual(Object.keys(rows).sort(), [U.admin, U.both, U.kanesan, U.owner, U.siteGuard, U.pinnedOrg].sort());
|
|
56
|
+
assert.equal(res.body.items.length, 6, "one row per person");
|
|
57
|
+
assert.equal(res.body.total, 6);
|
|
58
|
+
assert.deepEqual(rows[U.pinnedOrg].limitedTo.map((l) => l.siteName), ["The Loft MOP"], "an organisation row pinned to a site is limited");
|
|
59
|
+
assert.deepEqual(rows[U.owner].limitedTo, []);
|
|
60
|
+
assert.deepEqual(rows[U.both].limitedTo, [], "an org-wide person with site rows too is 'All sites'");
|
|
61
|
+
assert.deepEqual(
|
|
62
|
+
rows[U.kanesan].limitedTo.map((l) => `${l.siteName}:${l.roleName}`),
|
|
63
|
+
["One Pearl MOP:X at A", "The Loft MOP:Y at B"],
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("2. residents, removed rows, suspended-only and other organisations never appear", async () => {
|
|
68
|
+
const rows = byEmail(await list("owner"));
|
|
69
|
+
for (const e of [U.resident, U.gone, U.outsider]) assert.ok(!rows[e], e);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("3. without people=1 the list is unchanged (organisation rows only)", async () => {
|
|
73
|
+
const rows = byEmail(await list("owner", ""));
|
|
74
|
+
assert.deepEqual(Object.keys(rows).sort(), [U.admin, U.both, U.owner, U.pinnedOrg].sort());
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("4. paging and totals count people, not rows", async () => {
|
|
78
|
+
const res = await h.api(`/members?org=${id.org}&type=organization&status=active&page=2&limit=2&people=1`, { sid: sid.owner });
|
|
79
|
+
assert.equal(res.status, 200);
|
|
80
|
+
assert.equal(res.body.items.length, 2);
|
|
81
|
+
assert.equal(res.body.total, 6);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("5. a site-confined member sees only people limited to their own site", async () => {
|
|
85
|
+
const res = await list("siteGuard");
|
|
86
|
+
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
87
|
+
const rows = byEmail(res);
|
|
88
|
+
assert.deepEqual(Object.keys(rows).sort(), [U.kanesan, U.siteGuard].sort());
|
|
89
|
+
assert.deepEqual(rows[U.kanesan].limitedTo.map((l) => l.siteName), ["One Pearl MOP"], "never another site's name");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
async function seed(h) {
|
|
94
|
+
const now = new Date().toISOString();
|
|
95
|
+
const org = new ObjectId(), other = new ObjectId();
|
|
96
|
+
const A = new ObjectId(), B = new ObjectId();
|
|
97
|
+
const X = new ObjectId(), Y = new ObjectId(), orgRole = new ObjectId(), ownerRole = new ObjectId();
|
|
98
|
+
|
|
99
|
+
await h.db.collection("organizations").insertMany([
|
|
100
|
+
{ _id: org, name: "Aigle MOP", email: "aigle-mop@e2e.example.com", type: "org", nature: SEC, status: "active", createdAt: now },
|
|
101
|
+
{ _id: other, name: "Other MOP", email: "other-mop@e2e.example.com", type: "org", nature: SEC, status: "active", createdAt: now },
|
|
102
|
+
]);
|
|
103
|
+
await h.db.collection("sites").insertMany([
|
|
104
|
+
{ _id: A, name: "One Pearl MOP", org, orgId: org, status: "active", createdAt: now },
|
|
105
|
+
{ _id: B, name: "The Loft MOP", org, orgId: org, status: "active", createdAt: now },
|
|
106
|
+
]);
|
|
107
|
+
await h.db.collection("roles").insertMany([
|
|
108
|
+
{ _id: X, name: "X at A", type: SEC, org, site: A, permissions: ["dashboard:view"], status: "active" },
|
|
109
|
+
{ _id: Y, name: "Y at B", type: SEC, org, site: B, permissions: ["dashboard:view"], status: "active" },
|
|
110
|
+
{ _id: orgRole, name: "Org Admin", type: "organization", org, site: "", permissions: ["*"], status: "active" },
|
|
111
|
+
{ _id: ownerRole, name: "owner", type: "organization", org, site: "", permissions: ["*"], status: "active" },
|
|
112
|
+
]);
|
|
113
|
+
const users = {};
|
|
114
|
+
for (const [k, email] of Object.entries(U)) {
|
|
115
|
+
const { insertedId } = await h.db.collection("users").insertOne({ email, name: k, status: "active", createdAt: now });
|
|
116
|
+
await h.db.collection("users").updateOne({ _id: insertedId }, { $set: { password: await h.hashPassword(PASSWORD) } });
|
|
117
|
+
users[k] = insertedId;
|
|
118
|
+
}
|
|
119
|
+
await h.db.collection("organizations").updateOne({ _id: org }, { $set: { createdBy: users.owner } });
|
|
120
|
+
const m = (k, role, type = "organization", siteId = "", status = "active", o = org) =>
|
|
121
|
+
({ _id: new ObjectId(), user: users[k], email: U[k], name: k, org: o, type, role, siteId, siteName: "", status, deletedAt: "", createdAt: now });
|
|
122
|
+
await h.db.collection("members").insertMany([
|
|
123
|
+
m("owner", ownerRole),
|
|
124
|
+
m("admin", orgRole),
|
|
125
|
+
m("kanesan", orgRole, "organization", "", "deleted"),
|
|
126
|
+
m("kanesan", X, SEC, A),
|
|
127
|
+
m("kanesan", Y, SEC, B),
|
|
128
|
+
m("both", orgRole),
|
|
129
|
+
m("both", X, SEC, A),
|
|
130
|
+
m("siteGuard", X, SEC, A),
|
|
131
|
+
m("pinnedOrg", orgRole, "organization", B),
|
|
132
|
+
m("resident", X, "resident", A),
|
|
133
|
+
m("gone", X, SEC, A, "deleted"),
|
|
134
|
+
m("outsider", X, SEC, A, "active", other),
|
|
135
|
+
]);
|
|
136
|
+
return { org, A, B, users };
|
|
137
|
+
}
|
|
@@ -253,7 +253,7 @@ describe("privilege escalation to platform staff, end to end", { concurrency: 1
|
|
|
253
253
|
assert.equal(res.status, 201, JSON.stringify(res.body));
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
-
record("10. the
|
|
256
|
+
record("10. CLOSED 2026-09-29: a resident cannot enrol somebody ELSE (the live sign-in flows enrol the signed-in user themselves)", async () => {
|
|
257
257
|
// web-app-main pages/sign-in.vue:347 and web-app-org
|
|
258
258
|
// onboarding/getting-started/index.vue:357 both post
|
|
259
259
|
// `app: "organization"` for an org the caller is not yet a member of. That
|
|
@@ -276,10 +276,12 @@ describe("privilege escalation to platform staff, end to end", { concurrency: 1
|
|
|
276
276
|
},
|
|
277
277
|
});
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
// The Site access editor rule (`requireMemberGrantor`): a resident is none of
|
|
280
|
+
// owner, cross-client staff or org-wide admin.
|
|
281
|
+
assert.equal(res.status, 401, JSON.stringify(res.body));
|
|
280
282
|
});
|
|
281
283
|
|
|
282
|
-
record("11.
|
|
284
|
+
record("11. CLOSED 2026-09-29: nor with a service-provider membership", async () => {
|
|
283
285
|
const fresh = await h.db.collection("users").insertOne({
|
|
284
286
|
email: "guard@e2e.example.com",
|
|
285
287
|
name: "guard",
|
|
@@ -298,10 +300,12 @@ describe("privilege escalation to platform staff, end to end", { concurrency: 1
|
|
|
298
300
|
},
|
|
299
301
|
});
|
|
300
302
|
|
|
301
|
-
|
|
303
|
+
// The Site access editor rule (`requireMemberGrantor`): a resident is none of
|
|
304
|
+
// owner, cross-client staff or org-wide admin.
|
|
305
|
+
assert.equal(res.status, 401, JSON.stringify(res.body));
|
|
302
306
|
});
|
|
303
307
|
|
|
304
|
-
record("12.
|
|
308
|
+
record("12. CLOSED 2026-09-29: nor with an org id sent as `app`", async () => {
|
|
305
309
|
// `web-app-org pages/org/[organization]/invitations/invite.vue:122` and
|
|
306
310
|
// `web-app-hygiene pages/[org]/[site]/invitations/invite.vue:135` send the
|
|
307
311
|
// ORGANISATION ID as `app`. It is a defect in those two screens, but it is
|
|
@@ -325,7 +329,9 @@ describe("privilege escalation to platform staff, end to end", { concurrency: 1
|
|
|
325
329
|
},
|
|
326
330
|
});
|
|
327
331
|
|
|
328
|
-
|
|
332
|
+
// The Site access editor rule (`requireMemberGrantor`): a resident is none of
|
|
333
|
+
// owner, cross-client staff or org-wide admin.
|
|
334
|
+
assert.equal(res.status, 401, JSON.stringify(res.body));
|
|
329
335
|
});
|
|
330
336
|
});
|
|
331
337
|
|
|
@@ -85,12 +85,13 @@ test("the org a NEW role is filed under is checked, on both mounts", () => {
|
|
|
85
85
|
);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
-
test("enrolling yourself needs reach; enrolling anybody else needs
|
|
88
|
+
test("enrolling yourself needs reach; enrolling anybody else needs the Site access editor rule", () => {
|
|
89
89
|
const body = fn(memberController, "createMemberDirect");
|
|
90
90
|
assert.match(
|
|
91
91
|
body,
|
|
92
|
-
// ...and yourself only with a role you may give yourself (requireSelfEnrolRole)
|
|
93
|
-
|
|
92
|
+
// ...and yourself only with a role you may give yourself (requireSelfEnrolRole);
|
|
93
|
+
// somebody else only as owner / staff / org-wide admin (2026-09-29).
|
|
94
|
+
/if \(userId === callerId\(req\)\)[\s\S]{0,120}await requireOrgReach\(req, orgId\)[\s\S]{0,120}await requireSelfEnrolRole\(req, orgId, roleId\)[\s\S]{0,40}else[\s\S]{0,400}await requireMemberGrantor\(req, \{ orgId, targetUser: userId, roleId, siteId \}\)/,
|
|
94
95
|
);
|
|
95
96
|
});
|
|
96
97
|
|