@7365admin1/core 3.65.1 → 3.65.2
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.js +30 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +30 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/console-permission.test.mjs +3 -2
- package/test/e2e/member-by-user-reach.e2e.test.mjs +135 -0
- package/test/resident-ownership-scope.test.mjs +8 -4
package/package.json
CHANGED
|
@@ -211,11 +211,12 @@ function gateCalls() {
|
|
|
211
211
|
return calls;
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
test("every console gate call site names a module - all
|
|
214
|
+
test("every console gate call site names a module - all 49 of them", () => {
|
|
215
215
|
const calls = gateCalls();
|
|
216
216
|
// 48 since the module gate added `organization.controller.ts previewOrgModules`
|
|
217
217
|
// and `member.controller.ts getModulesByUserIdType` (self-or-staff, `members`).
|
|
218
|
-
|
|
218
|
+
// 49: `member.controller.ts getAllByUser` (self-or-staff, `members`).
|
|
219
|
+
assert.equal(calls.length, 49, `gate call sites moved: ${calls.length}`);
|
|
219
220
|
|
|
220
221
|
const ungoverned = calls.filter(
|
|
221
222
|
([, call]) =>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// End-to-end proof that `GET /api/members/users/:id` — every membership a
|
|
2
|
+
// person holds, across every client — is no longer readable by anybody.
|
|
3
|
+
//
|
|
4
|
+
// It was `requireAuth` and nothing more, so any signed-in account could read
|
|
5
|
+
// which organisations, apps and sites any other person works for. Now:
|
|
6
|
+
// - the person themselves and Seven365 staff get the whole list;
|
|
7
|
+
// - anybody else gets only the rows in organisations they reach — filtered,
|
|
8
|
+
// never refused, so no screen gets a 401 and no mobile app signs out.
|
|
9
|
+
//
|
|
10
|
+
// Consumers exercised (every call site in the org):
|
|
11
|
+
// - web-app-account / web-app-org layouts/default.vue, web-app-main
|
|
12
|
+
// pages/sign-in.vue: the signed-in user's OWN id (case 1).
|
|
13
|
+
// - layer-common ServiceProviderMain.vue (e-mail check + view dialog): a
|
|
14
|
+
// client admin reading a PROVIDER's id (case 3).
|
|
15
|
+
// - layer-common InvitationClientForm.vue (web-app-org super-admin client
|
|
16
|
+
// list): Seven365 staff reading anybody (case 2).
|
|
17
|
+
//
|
|
18
|
+
// Everything runs against the harness's throwaway in-process MongoDB. Nothing
|
|
19
|
+
// real is touched; assertions compare ids and counts only.
|
|
20
|
+
|
|
21
|
+
import { after, before, describe, it } from "node:test";
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import { ObjectId } from "mongodb";
|
|
24
|
+
|
|
25
|
+
import { startHarness } from "./harness.mjs";
|
|
26
|
+
|
|
27
|
+
const PASSWORD = "Reach-Passw0rd!";
|
|
28
|
+
const PROVIDER = "provider@e2e.example.com"; // works on client A's and client B's sites
|
|
29
|
+
const ADMIN_A = "admin-a@e2e.example.com"; // client A's own admin
|
|
30
|
+
const STRANGER = "stranger@e2e.example.com"; // a resident of client C only
|
|
31
|
+
const RESUBMIT = "resubmit@e2e.example.com"; // applicant at client A, asked to resubmit
|
|
32
|
+
const STAFF = "sevenstaff@e2e.example.com";
|
|
33
|
+
|
|
34
|
+
describe("GET /members/users/:id is self/staff in full, otherwise filtered to reach", { concurrency: 1 }, () => {
|
|
35
|
+
let h;
|
|
36
|
+
const id = {};
|
|
37
|
+
const sid = {};
|
|
38
|
+
|
|
39
|
+
before(async () => {
|
|
40
|
+
h = await startHarness();
|
|
41
|
+
Object.assign(id, await seed(h));
|
|
42
|
+
for (const [k, email] of Object.entries({ PROVIDER, ADMIN_A, STRANGER, RESUBMIT, STAFF })) {
|
|
43
|
+
sid[k] = await h.login(email, PASSWORD);
|
|
44
|
+
}
|
|
45
|
+
}, { timeout: 300000 });
|
|
46
|
+
|
|
47
|
+
after(async () => {
|
|
48
|
+
if (h) await h.stop();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const read = async (who, userId) => {
|
|
52
|
+
const res = await h.api(`/members/users/${userId}`, { sid: sid[who] });
|
|
53
|
+
assert.equal(res.status, 200, JSON.stringify(res.body));
|
|
54
|
+
assert.equal(res.body.total, res.body.items.length);
|
|
55
|
+
return res.body.items.map((m) => String(m.org ?? "")).sort();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it("1. the person reads all of their own memberships", async () => {
|
|
59
|
+
assert.deepEqual(await read("PROVIDER", id.provider), [id.orgA, id.orgB, id.orgSP].map(String).sort());
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("2. Seven365 staff read anybody's in full (the console's client form)", async () => {
|
|
63
|
+
assert.deepEqual(await read("STAFF", id.provider), [id.orgA, id.orgB, id.orgSP].map(String).sort());
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("3. a client admin sees only the provider's row in their own client", async () => {
|
|
67
|
+
// ServiceProviderMain's e-mail check still finds the row at this org/site.
|
|
68
|
+
const res = await h.api(`/members/users/${id.provider}`, { sid: sid.ADMIN_A });
|
|
69
|
+
assert.equal(res.status, 200);
|
|
70
|
+
assert.deepEqual(res.body.items.map((m) => String(m.org)), [String(id.orgA)]);
|
|
71
|
+
assert.equal(res.body.items[0].type, "cleaning");
|
|
72
|
+
assert.equal(String(res.body.items[0].siteId), String(id.siteA));
|
|
73
|
+
const text = JSON.stringify(res.body);
|
|
74
|
+
assert.ok(!text.includes(String(id.orgB)) && !text.includes(String(id.orgSP)), "no other client leaks");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("4. a stranger gets an empty list, not a 401", async () => {
|
|
78
|
+
assert.deepEqual(await read("STRANGER", id.provider), []);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("5. a resubmit applicant with an active resident row reaches nothing", async () => {
|
|
82
|
+
assert.deepEqual(await read("RESUBMIT", id.provider), []);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("6. a Seven365 staff row (no org) is never in a client's reach", async () => {
|
|
86
|
+
assert.deepEqual(await read("ADMIN_A", id.staff), []);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("7. the same person's cached list is still whole for them afterwards", async () => {
|
|
90
|
+
// The filter must never be written into the per-person cache.
|
|
91
|
+
assert.equal((await read("PROVIDER", id.provider)).length, 3);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
async function seed(h) {
|
|
96
|
+
const now = new Date().toISOString();
|
|
97
|
+
const [orgA, orgB, orgC, orgSP, siteA, siteB] = Array.from({ length: 6 }, () => new ObjectId());
|
|
98
|
+
const [staffRole, roleA, roleB, roleC, roleSP] = Array.from({ length: 5 }, () => new ObjectId());
|
|
99
|
+
|
|
100
|
+
const org = (_id, name) => ({ _id, name, email: `${name.replace(/\W/g, "").toLowerCase()}@e2e.example.com`, type: "org", nature: "property_management_agency", status: "active", createdAt: now });
|
|
101
|
+
await h.db.collection("organizations").insertMany([org(orgA, "Client A"), org(orgB, "Client B"), org(orgC, "Client C"), org(orgSP, "Clean Co")]);
|
|
102
|
+
await h.db.collection("sites").insertMany([
|
|
103
|
+
{ _id: siteA, name: "Site A", org: orgA, orgId: orgA, status: "active" },
|
|
104
|
+
{ _id: siteB, name: "Site B", org: orgB, orgId: orgB, status: "active" },
|
|
105
|
+
]);
|
|
106
|
+
await h.db.collection("roles").insertMany([
|
|
107
|
+
{ _id: staffRole, name: "Super Admin", type: "admin", default: true, permissions: [], status: "active" },
|
|
108
|
+
{ _id: roleA, name: "Admin", org: orgA, permissions: ["*"], status: "active" },
|
|
109
|
+
{ _id: roleB, name: "Admin", org: orgB, permissions: ["*"], status: "active" },
|
|
110
|
+
{ _id: roleC, name: "Resident", org: orgC, permissions: [], status: "active" },
|
|
111
|
+
{ _id: roleSP, name: "Owner", org: orgSP, permissions: ["*"], status: "active" },
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const people = [
|
|
115
|
+
[PROVIDER, "active"], [ADMIN_A, "active"], [STRANGER, "active"], [RESUBMIT, "resubmit"], [STAFF, "active"],
|
|
116
|
+
];
|
|
117
|
+
const users = await h.db.collection("users").insertMany(
|
|
118
|
+
people.map(([email, status]) => ({ email, name: email.split("@")[0], status, createdAt: now })),
|
|
119
|
+
);
|
|
120
|
+
const hashed = await h.hashPassword(PASSWORD);
|
|
121
|
+
await h.db.collection("users").updateMany({ email: { $in: people.map(([e]) => e) } }, { $set: { password: hashed } });
|
|
122
|
+
const [provider, adminA, stranger, resubmit, staff] = people.map((_, i) => users.insertedIds[i]);
|
|
123
|
+
|
|
124
|
+
await h.db.collection("members").insertMany([
|
|
125
|
+
{ user: provider, org: orgSP, type: "organization", role: roleSP, status: "active" },
|
|
126
|
+
{ user: provider, org: orgA, siteId: siteA, type: "cleaning", role: roleA, status: "active" },
|
|
127
|
+
{ user: provider, org: orgB, siteId: siteB, type: "cleaning", role: roleB, status: "active" },
|
|
128
|
+
{ user: adminA, org: orgA, type: "organization", role: roleA, status: "active" },
|
|
129
|
+
{ user: stranger, org: orgC, type: "resident", role: roleC, status: "active" },
|
|
130
|
+
{ user: resubmit, org: orgA, type: "resident", role: roleA, status: "active" },
|
|
131
|
+
{ user: staff, type: "admin", role: staffRole, status: "active" },
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
return { orgA, orgB, orgC, orgSP, siteA, siteB, provider, adminA, stranger, resubmit, staff };
|
|
135
|
+
}
|
|
@@ -69,11 +69,15 @@ test("onboarding completion is STOPPED, not tightened, and says so", () => {
|
|
|
69
69
|
assert.match(body, /DELIBERATELY LEFT AT ORG REACH/);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
test("the all-clients membership read is
|
|
72
|
+
test("the all-clients membership read is whole for self/staff, filtered for others", () => {
|
|
73
73
|
// `GET /api/members/users/:id` is how layer-common's ServiceProviderMain looks
|
|
74
|
-
// somebody else up by e-mail
|
|
75
|
-
//
|
|
76
|
-
|
|
74
|
+
// somebody else up by e-mail, so it is FILTERED to reached orgs, not refused.
|
|
75
|
+
// Behaviour: test/e2e/member-by-user-reach.e2e.test.mjs.
|
|
76
|
+
const body = fn(member, "getAllByUser");
|
|
77
|
+
assert.match(body, /requireSelfOrPlatformStaff\(req, userId/);
|
|
78
|
+
assert.match(body, /membershipsInReach\(req, data\.items\)/);
|
|
79
|
+
assert.match(fn(member, "membershipsInReach"), /requireOrgReach\(req, org\)/);
|
|
80
|
+
assert.match(fn(member, "membershipsInReach"), /callerAccountApproved/);
|
|
77
81
|
});
|
|
78
82
|
|
|
79
83
|
/* ------------------------------------------------------------- D3.14 */
|