@7365admin1/core 3.71.0 → 3.72.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 +2 -0
- package/dist/index.js +5453 -5325
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1078 -950
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/e2e/client-invite-modules.e2e.test.mjs +119 -2
- package/test/org-default-roles.test.mjs +22 -0
- package/test/org-modules.test.mjs +26 -0
package/package.json
CHANGED
|
@@ -25,6 +25,10 @@ const STAFF = "sevenadmin-cim@e2e.example.com";
|
|
|
25
25
|
const OUTSIDER = "outsider-cim@e2e.example.com";
|
|
26
26
|
const CLIENT = "client-cim@e2e.example.com";
|
|
27
27
|
const OTHER_NATURE = "othernature-cim@e2e.example.com";
|
|
28
|
+
const ROLE_CLIENT = "roleclient-cim@e2e.example.com";
|
|
29
|
+
|
|
30
|
+
// The owner role Seven365 chooses with the modules (owner, 2026-09-15).
|
|
31
|
+
const OWNER_PERMISSIONS = ["visitor-mgmt:view-visitors", "visitor-mgmt:add-visitor", "members:view-members"];
|
|
28
32
|
|
|
29
33
|
const MODULES = ["visitor-mgmt", "members", "roles", "invitations", "sites"];
|
|
30
34
|
|
|
@@ -39,7 +43,7 @@ describe("Invite Client chooses modules and the owner gets the client's own role
|
|
|
39
43
|
before(async () => {
|
|
40
44
|
h = await startHarness();
|
|
41
45
|
Object.assign(id, await seed(h));
|
|
42
|
-
for (const email of [STAFF, OUTSIDER, CLIENT, OTHER_NATURE]) sid[email] = await h.login(email, PASSWORD);
|
|
46
|
+
for (const email of [STAFF, OUTSIDER, CLIENT, OTHER_NATURE, ROLE_CLIENT]) sid[email] = await h.login(email, PASSWORD);
|
|
43
47
|
}, { timeout: 300000 });
|
|
44
48
|
|
|
45
49
|
after(async () => {
|
|
@@ -136,6 +140,119 @@ describe("Invite Client chooses modules and the owner gets the client's own role
|
|
|
136
140
|
0,
|
|
137
141
|
);
|
|
138
142
|
});
|
|
143
|
+
it("permissions are refused outside the modules, without modules, or empty", async () => {
|
|
144
|
+
const outside = await invite(STAFF, {
|
|
145
|
+
email: "outside-cim@e2e.example.com", app: ["security_agency"], modules: MODULES,
|
|
146
|
+
permissions: ["patrol-mgmt:view-patrols"],
|
|
147
|
+
});
|
|
148
|
+
assert.equal(outside.status, 400, JSON.stringify(outside.body));
|
|
149
|
+
|
|
150
|
+
const noModules = await invite(STAFF, {
|
|
151
|
+
email: "nomodules-cim@e2e.example.com", app: ["security_agency"], permissions: OWNER_PERMISSIONS,
|
|
152
|
+
});
|
|
153
|
+
assert.equal(noModules.status, 400, JSON.stringify(noModules.body));
|
|
154
|
+
|
|
155
|
+
const empty = await invite(STAFF, {
|
|
156
|
+
email: "empty-cim@e2e.example.com", app: ["security_agency"], modules: MODULES, permissions: [],
|
|
157
|
+
});
|
|
158
|
+
assert.equal(empty.status, 400, JSON.stringify(empty.body));
|
|
159
|
+
|
|
160
|
+
for (const email of ["outside-cim@e2e.example.com", "nomodules-cim@e2e.example.com", "empty-cim@e2e.example.com"]) {
|
|
161
|
+
assert.equal(await inviteOf(email), null, "nothing was written for " + email);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("a re-invite replaces the chosen permissions on the pending invitation", async () => {
|
|
166
|
+
const first = await invite(STAFF, { email: ROLE_CLIENT, app: ["security_agency"], modules: MODULES, permissions: ["members:view-members"] });
|
|
167
|
+
assert.equal(first.status, 201, JSON.stringify(first.body));
|
|
168
|
+
const again = await invite(STAFF, { email: ROLE_CLIENT, app: ["security_agency"], modules: MODULES, permissions: [...OWNER_PERMISSIONS, "members:view-members"] });
|
|
169
|
+
assert.equal(again.status, 201, JSON.stringify(again.body));
|
|
170
|
+
|
|
171
|
+
const rows = await h.db.collection("verifications").find({ type: "user-invite", email: ROLE_CLIENT }).toArray();
|
|
172
|
+
assert.equal(rows.length, 1, "one pending invitation");
|
|
173
|
+
assert.deepEqual(rows[0].metadata.permissions, OWNER_PERMISSIONS, "the latest choice, once each");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("onboarding gives the owner the application role Seven365 chose, and the owner can take it", async () => {
|
|
177
|
+
const row = await inviteOf(ROLE_CLIENT);
|
|
178
|
+
const res = await h.api("/organizations/onboarding", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
sid: sid[ROLE_CLIENT],
|
|
181
|
+
body: { name: "CIM Role Client", type: "org", nature: "security_agency", email: ROLE_CLIENT, inviteId: row._id.toString() },
|
|
182
|
+
});
|
|
183
|
+
assert.equal(res.status, 201, JSON.stringify(res.body));
|
|
184
|
+
|
|
185
|
+
const org = await h.db.collection("organizations").findOne({ name: "CIM Role Client" });
|
|
186
|
+
assert.ok(org.invitedRole, "the chosen role is recorded for the finish");
|
|
187
|
+
|
|
188
|
+
const appRole = await h.db.collection("roles").findOne({ _id: org.invitedRole });
|
|
189
|
+
assert.equal(String(appRole.org), org._id.toString(), "the client's own role");
|
|
190
|
+
assert.equal(appRole.type, "security_agency");
|
|
191
|
+
assert.equal(appRole.name, "owner");
|
|
192
|
+
assert.deepEqual(appRole.permissions, OWNER_PERMISSIONS, "exactly what was ticked");
|
|
193
|
+
|
|
194
|
+
// Running the organisation is never narrowed.
|
|
195
|
+
const orgMember = await h.db.collection("members").findOne({ org: org._id, type: "organization" });
|
|
196
|
+
const orgRole = await h.db.collection("roles").findOne({ _id: orgMember.role });
|
|
197
|
+
assert.deepEqual(orgRole.permissions, ["*"]);
|
|
198
|
+
|
|
199
|
+
// The finish writes the application membership with that role (web-app-org onboarding-finish).
|
|
200
|
+
const site = new ObjectId();
|
|
201
|
+
const owner = await h.db.collection("users").findOne({ email: ROLE_CLIENT });
|
|
202
|
+
const member = await h.api("/members/direct", {
|
|
203
|
+
method: "POST",
|
|
204
|
+
sid: sid[ROLE_CLIENT],
|
|
205
|
+
body: { userId: owner._id.toString(), orgId: org._id.toString(), roleId: org.invitedRole.toString(), app: "security_agency", siteId: site.toString(), siteName: "HQ" },
|
|
206
|
+
});
|
|
207
|
+
assert.equal(member.status, 201, JSON.stringify(member.body));
|
|
208
|
+
const appMember = await h.db.collection("members").findOne({ org: org._id, type: "security_agency", user: owner._id });
|
|
209
|
+
assert.equal(String(appMember.role), String(org.invitedRole));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("the client cannot widen its application roles past what Seven365 chose; Seven365 can", async () => {
|
|
213
|
+
const org = await h.db.collection("organizations").findOne({ name: "CIM Role Client" });
|
|
214
|
+
const roleId = org.invitedRole.toString();
|
|
215
|
+
const patch = (who, permissions) =>
|
|
216
|
+
h.api(`/roles/permissions/id/${roleId}`, { method: "PATCH", sid: sid[who], body: { permissions } });
|
|
217
|
+
const roleNow = async () => (await h.db.collection("roles").findOne({ _id: org.invitedRole })).permissions;
|
|
218
|
+
|
|
219
|
+
const wider = await patch(ROLE_CLIENT, [...OWNER_PERMISSIONS, "visitor-mgmt:delete-visitor"]);
|
|
220
|
+
assert.ok([401, 403].includes(wider.status), `owner widening refused, got ${wider.status} ${JSON.stringify(wider.body)}`);
|
|
221
|
+
assert.deepEqual(await roleNow(), OWNER_PERMISSIONS, "nothing was written");
|
|
222
|
+
|
|
223
|
+
const runOrg = await patch(ROLE_CLIENT, [...OWNER_PERMISSIONS, "members:delete-member"]);
|
|
224
|
+
assert.equal(runOrg.status, 200, "running the organisation is not capped: " + JSON.stringify(runOrg.body));
|
|
225
|
+
|
|
226
|
+
const staffRole = await h.api("/roles", {
|
|
227
|
+
method: "POST", sid: sid[ROLE_CLIENT],
|
|
228
|
+
body: { name: "Guard lead", type: "security_agency", org: org._id.toString(), permissions: ["*"] },
|
|
229
|
+
});
|
|
230
|
+
assert.equal(staffRole.status, 201, JSON.stringify(staffRole.body));
|
|
231
|
+
const created = await h.db.collection("roles").findOne({ org: org._id, name: "Guard lead" });
|
|
232
|
+
assert.ok(!created.permissions.includes("*"), "an everything role becomes the grant");
|
|
233
|
+
assert.ok(created.permissions.includes("visitor-mgmt:view-visitors"));
|
|
234
|
+
assert.ok(!created.permissions.includes("visitor-mgmt:delete-visitor"));
|
|
235
|
+
|
|
236
|
+
const widerStaffRole = await h.api("/roles", {
|
|
237
|
+
method: "POST", sid: sid[ROLE_CLIENT],
|
|
238
|
+
body: { name: "Too wide", type: "security_agency", org: org._id.toString(), permissions: ["visitor-mgmt:delete-visitor"] },
|
|
239
|
+
});
|
|
240
|
+
assert.ok([401, 403].includes(widerStaffRole.status), `got ${widerStaffRole.status}`);
|
|
241
|
+
|
|
242
|
+
const bySeven365 = await patch(STAFF, [...OWNER_PERMISSIONS, "members:delete-member", "visitor-mgmt:delete-visitor"]);
|
|
243
|
+
assert.equal(bySeven365.status, 200, JSON.stringify(bySeven365.body));
|
|
244
|
+
assert.ok((await roleNow()).includes("visitor-mgmt:delete-visitor"), "Seven365 widened the grant");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("a client invited before this keeps its roles uncapped", async () => {
|
|
248
|
+
const org = await h.db.collection("organizations").findOne({ name: "CIM Security Client" });
|
|
249
|
+
const member = await h.db.collection("members").findOne({ org: org._id, type: "organization" });
|
|
250
|
+
const res = await h.api("/roles", {
|
|
251
|
+
method: "POST", sid: sid[CLIENT],
|
|
252
|
+
body: { name: "Anything", type: "security_agency", org: org._id.toString(), permissions: ["visitor-mgmt:delete-visitor"] },
|
|
253
|
+
});
|
|
254
|
+
assert.equal(res.status, 201, JSON.stringify(res.body) + " member " + Boolean(member));
|
|
255
|
+
});
|
|
139
256
|
});
|
|
140
257
|
|
|
141
258
|
async function seed(h) {
|
|
@@ -153,7 +270,7 @@ async function seed(h) {
|
|
|
153
270
|
]);
|
|
154
271
|
|
|
155
272
|
const users = await h.db.collection("users").insertMany(
|
|
156
|
-
[STAFF, OUTSIDER, CLIENT, OTHER_NATURE].map((email) => ({ email, name: email.split("@")[0], status: "active", createdAt: now })),
|
|
273
|
+
[STAFF, OUTSIDER, CLIENT, OTHER_NATURE, ROLE_CLIENT].map((email) => ({ email, name: email.split("@")[0], status: "active", createdAt: now })),
|
|
157
274
|
);
|
|
158
275
|
const hashed = await h.hashPassword(PASSWORD);
|
|
159
276
|
await h.db.collection("users").updateMany({}, { $set: { password: hashed } });
|
|
@@ -259,3 +259,25 @@ test("positive control: the guard DOES refuse the shapes this seeding avoids", (
|
|
|
259
259
|
"a platform grant on a client role is still refused",
|
|
260
260
|
);
|
|
261
261
|
});
|
|
262
|
+
|
|
263
|
+
// Owner, 2026-09-15: the permissions Seven365 chose on Invite Client land on the
|
|
264
|
+
// NATURE-typed owner role only; the organisation owner role keeps "*".
|
|
265
|
+
test("chosen owner permissions narrow only the application owner role", () => {
|
|
266
|
+
const chosen = ["visitor-mgmt:view-visitors", "members:view-members"];
|
|
267
|
+
const roles = defaultOwnerRoles(ORG, "security_agency", chosen);
|
|
268
|
+
const owner = (type) => roles.find((r) => r.type === type && r.name === "owner");
|
|
269
|
+
|
|
270
|
+
assert.deepEqual(owner("security_agency").permissions, chosen);
|
|
271
|
+
assert.deepEqual(owner("organization").permissions, ["*"]);
|
|
272
|
+
// An empty list would mean EVERYTHING, so it is ignored rather than stored.
|
|
273
|
+
assert.deepEqual(
|
|
274
|
+
defaultOwnerRoles(ORG, "security_agency", []).find((r) => r.type === "security_agency" && r.name === "owner").permissions,
|
|
275
|
+
["*"],
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("the owner-permissions kill switch", async () => {
|
|
280
|
+
const { clientInviteOwnerPermissionsOn } = await import("./.build/utils/org-default-roles.util.mjs");
|
|
281
|
+
assert.equal(clientInviteOwnerPermissionsOn({}), true);
|
|
282
|
+
assert.equal(clientInviteOwnerPermissionsOn({ CLIENT_INVITE_OWNER_PERMISSIONS: "off" }), false);
|
|
283
|
+
});
|
|
@@ -257,3 +257,29 @@ test("a real list now offers every spelling and the always-included set", () =>
|
|
|
257
257
|
["members", "visitor-mgmt", "workOrder"],
|
|
258
258
|
);
|
|
259
259
|
});
|
|
260
|
+
|
|
261
|
+
// Owner, 2026-09-15: the owner role chosen on Invite Client must stay inside the
|
|
262
|
+
// chosen modules.
|
|
263
|
+
test("permissions outside the chosen modules are named", async () => {
|
|
264
|
+
const { normalisePermissions, permissionsOutsideModules } = await import("./.build/utils/org-modules.util.mjs");
|
|
265
|
+
const chosen = normalisePermissions([" visitor-mgmt:view-visitors", "visitor-mgmt:view-visitors", "*", "patrol-mgmt:view", "members:view-members", 7]);
|
|
266
|
+
assert.deepEqual(chosen, ["visitor-mgmt:view-visitors", "patrol-mgmt:view", "members:view-members"]);
|
|
267
|
+
// members is always allowed; visitorManagement is a spelling of visitor-mgmt.
|
|
268
|
+
assert.deepEqual(permissionsOutsideModules(chosen, ["visitorManagement"]), ["patrol-mgmt:view"]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("roles stay inside the owner grant Seven365 chose", async () => {
|
|
272
|
+
const { boundToOwnerGrant } = await import("./.build/utils/org-modules.util.mjs");
|
|
273
|
+
const ceiling = ["visitor-mgmt:view-visitors", "visitor-mgmt:add-visitor"];
|
|
274
|
+
|
|
275
|
+
assert.deepEqual(boundToOwnerGrant(null, ["x:y"]), { permissions: ["x:y"], refused: [] }, "no ceiling, no change");
|
|
276
|
+
assert.deepEqual(boundToOwnerGrant(ceiling, ["*"]).permissions, ceiling, "everything becomes the grant");
|
|
277
|
+
assert.deepEqual(boundToOwnerGrant(ceiling, []).permissions, ceiling, "an empty list means everything too");
|
|
278
|
+
assert.deepEqual(boundToOwnerGrant(ceiling, ["visitor-mgmt:delete-visitor"]).refused, ["visitor-mgmt:delete-visitor"]);
|
|
279
|
+
assert.deepEqual(boundToOwnerGrant(ceiling, ["members:delete-member", "visitor-mgmt:add-visitor"]).refused, [], "running the organisation is not capped");
|
|
280
|
+
assert.deepEqual(
|
|
281
|
+
boundToOwnerGrant(ceiling, ["visitor-mgmt:delete-visitor"], ["visitor-mgmt:delete-visitor"]).refused,
|
|
282
|
+
[],
|
|
283
|
+
"a string already held is never refused",
|
|
284
|
+
);
|
|
285
|
+
});
|