@7365admin1/core 3.56.0 → 3.58.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 +57 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3992 -3935
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1676 -1619
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/e2e/harness.mjs +2 -0
- package/test/e2e/org-default-roles.e2e.test.mjs +320 -0
- package/test/e2e/role-template-restore.e2e.test.mjs +369 -0
- package/test/org-default-roles.test.mjs +218 -0
- package/test/role-scope-separation.test.mjs +149 -0
- package/test/role-scope.test.mjs +15 -3
|
@@ -5,6 +5,7 @@ import { readFileSync } from "node:fs";
|
|
|
5
5
|
import {
|
|
6
6
|
PLATFORM_ONLY_PERMISSIONS,
|
|
7
7
|
platformPermissionsAdded,
|
|
8
|
+
permissionsMeanEverything,
|
|
8
9
|
refuseSharedClientTemplate,
|
|
9
10
|
requireNoPlatformGrant,
|
|
10
11
|
roleScope,
|
|
@@ -193,3 +194,151 @@ test("the lockout check's copy of the platform list cannot drift", () => {
|
|
|
193
194
|
|
|
194
195
|
assert.deepEqual(onDisk, [...PLATFORM_ONLY_PERMISSIONS].sort());
|
|
195
196
|
});
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* ---------------------------------------------------------------------------
|
|
201
|
+
* THE RESTORE PATH
|
|
202
|
+
*
|
|
203
|
+
* 3.56.0 refused every write to a shared template, including the one that
|
|
204
|
+
* repairs the outage. Production "Org Owner" still holds the 34 strings and 119
|
|
205
|
+
* members are still locked out, so a platform admin needs a way BACK to
|
|
206
|
+
* allow-all — and no way to anything else.
|
|
207
|
+
*
|
|
208
|
+
* `BROKEN` is the live production state: the same template, after the save.
|
|
209
|
+
* ---------------------------------------------------------------------------
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
const BROKEN = { type: "organization", org: "", permissions: CONSOLE_SAVE };
|
|
213
|
+
|
|
214
|
+
test("RESTORE: a platform admin may set a shared template to [\"*\"]", () => {
|
|
215
|
+
assert.doesNotThrow(() => refuseSharedClientTemplate(BROKEN, ["*"]));
|
|
216
|
+
assert.equal(
|
|
217
|
+
refuseSharedClientTemplate(BROKEN, ["*"]),
|
|
218
|
+
true,
|
|
219
|
+
"must report the restore so the controller can record it",
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("RESTORE: an empty list is the same restore and is also permitted", () => {
|
|
224
|
+
// `[]` and `["*"]` are the two live spellings of everything.
|
|
225
|
+
assert.equal(refuseSharedClientTemplate(BROKEN, []), true);
|
|
226
|
+
assert.equal(permissionsMeanEverything([]), true);
|
|
227
|
+
assert.equal(permissionsMeanEverything(["*"]), true);
|
|
228
|
+
assert.equal(permissionsMeanEverything(["*", "members:view-members"]), true);
|
|
229
|
+
assert.equal(permissionsMeanEverything(["members:view-members"]), false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("THE OUTAGE STAYS IMPOSSIBLE: the 34 console strings are still refused", () => {
|
|
233
|
+
/*
|
|
234
|
+
* The one case a literal "superset-or-equal" rule would have let through.
|
|
235
|
+
* Production holds exactly this list TODAY, so an equal-set write is the
|
|
236
|
+
* outage being re-saved, not a widening. Refused from both states.
|
|
237
|
+
*/
|
|
238
|
+
assert.throws(
|
|
239
|
+
() => refuseSharedClientTemplate(BROKEN, CONSOLE_SAVE),
|
|
240
|
+
/shared by every client/,
|
|
241
|
+
);
|
|
242
|
+
assert.throws(
|
|
243
|
+
() => refuseSharedClientTemplate(ORG_OWNER, CONSOLE_SAVE),
|
|
244
|
+
/shared by every client/,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("NARROWING: a strict subset of what the template holds is refused", () => {
|
|
249
|
+
assert.throws(
|
|
250
|
+
() => refuseSharedClientTemplate(BROKEN, CONSOLE_SAVE.slice(0, 10)),
|
|
251
|
+
/shared by every client/,
|
|
252
|
+
);
|
|
253
|
+
// and a single-string list, the narrowest write there is
|
|
254
|
+
assert.throws(
|
|
255
|
+
() => refuseSharedClientTemplate(BROKEN, ["members:view-members"]),
|
|
256
|
+
/shared by every client/,
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("a strict SUPERSET is refused too — nothing may author a template's list", () => {
|
|
261
|
+
// Deliberately tighter than "superset-or-equal": no editor draws the client
|
|
262
|
+
// catalogue for an org-less role, so a hand-built list is a guess.
|
|
263
|
+
assert.throws(
|
|
264
|
+
() =>
|
|
265
|
+
refuseSharedClientTemplate(BROKEN, [
|
|
266
|
+
...CONSOLE_SAVE,
|
|
267
|
+
"members:view-members",
|
|
268
|
+
]),
|
|
269
|
+
/shared by every client/,
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("a DELETE presents no list, so the refusal is unchanged", () => {
|
|
274
|
+
// `deleteRole`/`deleteWithReassignments` call the gate with two arguments.
|
|
275
|
+
assert.throws(() => refuseSharedClientTemplate(BROKEN), /shared by every client/);
|
|
276
|
+
assert.throws(
|
|
277
|
+
() => refuseSharedClientTemplate(BROKEN, null),
|
|
278
|
+
/shared by every client/,
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("POSITIVE CONTROL: an org-scoped role is untouched in BOTH directions", () => {
|
|
283
|
+
const client = {
|
|
284
|
+
type: "organization",
|
|
285
|
+
org: "6512ab",
|
|
286
|
+
permissions: ["members:view-members", "roles-and-permissions:add-role"],
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
// widening
|
|
290
|
+
assert.equal(refuseSharedClientTemplate(client, ["*"]), false);
|
|
291
|
+
assert.equal(refuseSharedClientTemplate(client, []), false);
|
|
292
|
+
// narrowing — a client's own editor takes modules away every day
|
|
293
|
+
assert.equal(refuseSharedClientTemplate(client, ["members:view-members"]), false);
|
|
294
|
+
assert.equal(refuseSharedClientTemplate(client, []), false);
|
|
295
|
+
// and a delete
|
|
296
|
+
assert.equal(refuseSharedClientTemplate(client), false);
|
|
297
|
+
|
|
298
|
+
// the platform's own role, same both ways
|
|
299
|
+
const platform = { type: "admin", org: "", permissions: CONSOLE_SAVE };
|
|
300
|
+
assert.equal(refuseSharedClientTemplate(platform, ["*"]), false);
|
|
301
|
+
assert.equal(refuseSharedClientTemplate(platform, CONSOLE_SAVE.slice(0, 3)), false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("POSITIVE CONTROL: the refusal still bites — the exception is not a hole", () => {
|
|
305
|
+
/*
|
|
306
|
+
* If the restore arm were reachable for any list, every assertion above would
|
|
307
|
+
* pass vacuously. Count what is actually refused out of a realistic spread.
|
|
308
|
+
*/
|
|
309
|
+
const writes = [
|
|
310
|
+
CONSOLE_SAVE,
|
|
311
|
+
CONSOLE_SAVE.slice(0, 10),
|
|
312
|
+
["members:view-members"],
|
|
313
|
+
[...CONSOLE_SAVE, "members:view-members"],
|
|
314
|
+
["organizations:see-all-organizations"],
|
|
315
|
+
undefined,
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
let refused = 0;
|
|
319
|
+
for (const next of writes) {
|
|
320
|
+
try {
|
|
321
|
+
refuseSharedClientTemplate(BROKEN, next);
|
|
322
|
+
} catch {
|
|
323
|
+
refused += 1;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
assert.equal(refused, 6, "every non-restore write must still be refused");
|
|
328
|
+
assert.equal(refuseSharedClientTemplate(BROKEN, ["*"]), true);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("a client editor STILL cannot grant a platform resource after all this", () => {
|
|
332
|
+
// The other half of the guard is untouched by the restore path.
|
|
333
|
+
assert.throws(
|
|
334
|
+
() =>
|
|
335
|
+
requireNoPlatformGrant(
|
|
336
|
+
{ type: "organization", org: "6512ab", permissions: [] },
|
|
337
|
+
["organizations:see-all-organizations"],
|
|
338
|
+
),
|
|
339
|
+
/Seven365 console permissions/,
|
|
340
|
+
);
|
|
341
|
+
// and the restore itself carries no platform string, so it passes this gate
|
|
342
|
+
assert.doesNotThrow(() => requireNoPlatformGrant(BROKEN, ["*"]));
|
|
343
|
+
assert.doesNotThrow(() => requireNoPlatformGrant(BROKEN, []));
|
|
344
|
+
});
|
package/test/role-scope.test.mjs
CHANGED
|
@@ -185,14 +185,26 @@ test("the write gate refuses a shared client template before anything else", ()
|
|
|
185
185
|
const helper = controller.slice(
|
|
186
186
|
controller.indexOf("async function requireRoleWrite"),
|
|
187
187
|
);
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
188
|
+
// 900, not 500: the gate carries a comment explaining why the restore is
|
|
189
|
+
// reported only after the console check.
|
|
190
|
+
const body = helper.slice(0, 900);
|
|
191
|
+
|
|
192
|
+
// Matched without the closing paren on purpose: the gate now hands the
|
|
193
|
+
// proposed permission list in as a second argument (the restore path), and
|
|
194
|
+
// the property this test exists to hold is the ORDER, not the arity.
|
|
195
|
+
const refuse = body.indexOf("refuseSharedClientTemplate(role");
|
|
191
196
|
const fallthrough = body.indexOf("requireRoleOrg(req");
|
|
192
197
|
|
|
193
198
|
assert.ok(refuse !== -1, "requireRoleWrite must refuse shared templates");
|
|
194
199
|
assert.ok(fallthrough !== -1, "and must still apply the org rule to the rest");
|
|
195
200
|
assert.ok(refuse < fallthrough, "the refusal must come first");
|
|
201
|
+
|
|
202
|
+
// The restore is only reachable AFTER the console gate has run, so a
|
|
203
|
+
// non-staff caller can never take it.
|
|
204
|
+
assert.ok(
|
|
205
|
+
body.indexOf("return isTemplateRestore") > fallthrough,
|
|
206
|
+
"a restore must not be reported before the authorization check",
|
|
207
|
+
);
|
|
196
208
|
});
|
|
197
209
|
|
|
198
210
|
test("the permission-writing paths refuse a platform grant", () => {
|