@7365admin1/core 3.57.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@7365admin1/core",
3
3
  "license": "MIT",
4
- "version": "3.57.0",
4
+ "version": "3.58.0",
5
5
  "author": "7365admin1",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -0,0 +1,369 @@
1
+ /**
2
+ * A platform admin can put a shared client template BACK to allow-all, and can
3
+ * still do nothing else to it.
4
+ *
5
+ * ## What this is proving
6
+ *
7
+ * The guard shipped in core 3.56.0 refuses every write to an org-less
8
+ * `type: "organization"` role — the shared CLIENT TEMPLATES that every client
9
+ * invited with them depends on. It stopped the 2026-09-07 outage recurring and
10
+ * it also stopped it being repaired: production "Org Owner"
11
+ * (`69df7c8034293c971075ab97`) still holds the 34 platform permission strings,
12
+ * 119 members are still locked out, and the guard is what refuses the one write
13
+ * that gives them their modules back.
14
+ *
15
+ * The unit half (`test/role-scope-separation.test.mjs`) proves the predicate.
16
+ * This proves it over real HTTP, through the real routes and the real
17
+ * controller, and — the assertion that matters most — that what LANDS IN THE
18
+ * DATABASE after the restore is `["*"]` and not something else.
19
+ *
20
+ * The template here is seeded holding exactly the 34 strings production holds,
21
+ * so case 2 is today's outage attempted against today's code.
22
+ *
23
+ * Everything is created and thrown away by the harness: an in-process MongoDB
24
+ * replica set, a loopback Redis, a loopback mail sink. No staging or production
25
+ * database, Redis, mailbox or endpoint is touched.
26
+ *
27
+ * Run with: yarn test:e2e
28
+ */
29
+
30
+ import { after, before, describe, it } from "node:test";
31
+ import assert from "node:assert/strict";
32
+ import { ObjectId } from "mongodb";
33
+
34
+ import { startHarness } from "./harness.mjs";
35
+
36
+ const PASSWORD = "RoleTemplateRestore-Passw0rd!";
37
+ const STAFF = "rtr-staff@e2e.example.com"; // Seven365 console
38
+ const CLIENT = "rtr-client@e2e.example.com"; // an ordinary tenant member
39
+
40
+ /** Verbatim `useAdminPermission`: the list the console save wrote. */
41
+ const CONSOLE_SAVE = [
42
+ "organizations:see-all-organizations",
43
+ "organizations:see-organization-details",
44
+ "promo-codes:create-promo-code",
45
+ "promo-codes:see-promo-code-details",
46
+ "promo-codes:edit-promo-code-details",
47
+ "promo-codes:change-promo-code-status",
48
+ "promo-codes:delete-promo-code",
49
+ "users:see-all-users",
50
+ "users:see-user-details",
51
+ "invitations:create-invitation",
52
+ "invitations:view-invitations",
53
+ "invitations:cancel-invitation",
54
+ "subscriptions:see-all-subscriptions",
55
+ "subscriptions:see-subscription-details",
56
+ "subscriptions:manage-subscription",
57
+ "sp-approvals:see-all-sp-approvals",
58
+ "sp-approvals:approve-sp",
59
+ "sp-approvals:reject-sp",
60
+ "sp-approvals:delete-sp-approval",
61
+ "marketplace-vendors:see-all-marketplace-vendors",
62
+ "marketplace-vendors:see-marketplace-vendor-details",
63
+ "platform-terms:see-platform-terms",
64
+ "platform-terms:edit-platform-terms",
65
+ "activity-history:see-activity-history",
66
+ "members:view-members",
67
+ "members:assign-member-role",
68
+ "members:suspend-member",
69
+ "members:activate-member",
70
+ "members:delete-member",
71
+ "roles-and-permissions:add-role",
72
+ "roles-and-permissions:see-all-roles",
73
+ "roles-and-permissions:see-role-details",
74
+ "roles-and-permissions:update-role",
75
+ "roles-and-permissions:delete-role",
76
+ ];
77
+
78
+ const REFUSAL = /shared by every client/;
79
+
80
+ describe("a shared client template can be restored and nothing else", { concurrency: 1 }, () => {
81
+ let h;
82
+ let id;
83
+ let staffSid;
84
+ let clientSid;
85
+ const result = [];
86
+
87
+ const record = (name, fn) =>
88
+ it(name, async () => {
89
+ try {
90
+ await fn();
91
+ result.push(["PASS", name]);
92
+ } catch (error) {
93
+ result.push(["FAIL", name]);
94
+ throw error;
95
+ }
96
+ });
97
+
98
+ before(async () => {
99
+ h = await startHarness();
100
+ id = await seed(h);
101
+ staffSid = await h.login(STAFF, PASSWORD);
102
+ clientSid = await h.login(CLIENT, PASSWORD);
103
+ }, { timeout: 300000 });
104
+
105
+ after(async () => {
106
+ if (h) await h.stop();
107
+ console.log("\n--- role template restore: per-case result ---");
108
+ for (const [verdict, name] of result) console.log(`${verdict} ${name}`);
109
+ });
110
+
111
+ const permissionsOf = async (roleId) =>
112
+ (await h.db.collection("roles").findOne({ _id: roleId }))?.permissions;
113
+
114
+ const patchPermissions = (roleId, permissions, sid) =>
115
+ h.api("/roles/permissions/id/" + roleId.toString(), {
116
+ method: "PATCH",
117
+ sid,
118
+ body: { permissions },
119
+ });
120
+
121
+ // ---- the outage, attempted -----------------------------------------------
122
+
123
+ record("1. the template starts in production's broken state — 34 strings", async () => {
124
+ assert.deepEqual(await permissionsOf(id.template), CONSOLE_SAVE);
125
+ });
126
+
127
+ record("2. THE OUTAGE: a console save of the 34 strings is still refused", async () => {
128
+ const res = await patchPermissions(id.template, CONSOLE_SAVE, staffSid);
129
+
130
+ assert.equal(res.status, 401, JSON.stringify(res.body));
131
+ assert.match(res.body?.message ?? "", REFUSAL);
132
+ assert.deepEqual(
133
+ await permissionsOf(id.template),
134
+ CONSOLE_SAVE,
135
+ "the refused write must not have touched the row",
136
+ );
137
+ });
138
+
139
+ record("3. NARROWING: a strict subset of what it holds is refused", async () => {
140
+ const res = await patchPermissions(id.template, CONSOLE_SAVE.slice(0, 10), staffSid);
141
+
142
+ assert.equal(res.status, 401, JSON.stringify(res.body));
143
+ assert.match(res.body?.message ?? "", REFUSAL);
144
+ assert.deepEqual(await permissionsOf(id.template), CONSOLE_SAVE);
145
+ });
146
+
147
+ record("4. a hand-built superset is refused too — nothing authors a template", async () => {
148
+ const res = await patchPermissions(
149
+ id.template,
150
+ [...CONSOLE_SAVE, "members:view-members-extra"],
151
+ staffSid,
152
+ );
153
+
154
+ assert.equal(res.status, 401, JSON.stringify(res.body));
155
+ assert.deepEqual(await permissionsOf(id.template), CONSOLE_SAVE);
156
+ });
157
+
158
+ // ---- the repair ----------------------------------------------------------
159
+
160
+ record("5. THE REPAIR: a platform admin sets it to allow-all and it STORES it", async () => {
161
+ const res = await patchPermissions(id.template, ["*"], staffSid);
162
+
163
+ assert.equal(res.status, 200, JSON.stringify(res.body));
164
+ assert.deepEqual(
165
+ await permissionsOf(id.template),
166
+ ["*"],
167
+ "the whole point: the row must now read allow-all",
168
+ );
169
+ });
170
+
171
+ record("6. an EMPTY list is not refused by the template guard — a different rule stops it", async () => {
172
+ /*
173
+ * MEASURED, and it is the reason the repair instruction says `["*"]`.
174
+ *
175
+ * `PATCH /roles/permissions/id/:id` refuses an empty list in the repository
176
+ * (`role.repo.ts:481` — "Permissions cannot be empty."), and has since long
177
+ * before any of this. It is a 400, not the 401 the template guard raises,
178
+ * so the guard did let it through; the storage layer is what says no.
179
+ *
180
+ * Deliberately NOT changed. Loosening it would let an empty list — which
181
+ * the permission model reads as EVERYTHING — be saved on any role by any
182
+ * client's own editor. That is a widening with a blast radius far past this
183
+ * repair, and `["*"]` restores the same thing.
184
+ */
185
+ const res = await patchPermissions(id.secondTemplate, [], staffSid);
186
+
187
+ assert.equal(res.status, 400, JSON.stringify(res.body));
188
+ assert.match(res.body?.message ?? "", /Permissions cannot be empty/);
189
+ assert.doesNotMatch(
190
+ res.body?.message ?? "",
191
+ REFUSAL,
192
+ "the template guard must NOT be what refused an empty list",
193
+ );
194
+ assert.deepEqual(await permissionsOf(id.secondTemplate), CONSOLE_SAVE);
195
+ });
196
+
197
+ record("6b. and where an empty list IS accepted, the guard permits it", async () => {
198
+ // `PATCH /roles/id/:id` stores it (`role.repo.ts:427` keeps a `[]`), so the
199
+ // guard's empty-list arm is exercised end to end and not merely asserted.
200
+ const res = await h.api("/roles/id/" + id.secondTemplate.toString(), {
201
+ method: "PATCH",
202
+ sid: staffSid,
203
+ body: { name: "Org Manager", permissions: [] },
204
+ });
205
+
206
+ assert.equal(res.status, 200, JSON.stringify(res.body));
207
+ assert.deepEqual(await permissionsOf(id.secondTemplate), []);
208
+ });
209
+
210
+ record("7. PATCH /id/:id — the other write endpoint behaves identically", async () => {
211
+ const refused = await h.api("/roles/id/" + id.thirdTemplate.toString(), {
212
+ method: "PATCH",
213
+ sid: staffSid,
214
+ body: { name: "Renamed Template", permissions: CONSOLE_SAVE },
215
+ });
216
+ assert.equal(refused.status, 401, JSON.stringify(refused.body));
217
+ assert.match(refused.body?.message ?? "", REFUSAL);
218
+
219
+ const restored = await h.api("/roles/id/" + id.thirdTemplate.toString(), {
220
+ method: "PATCH",
221
+ sid: staffSid,
222
+ body: { name: "Renamed Template", permissions: ["*"] },
223
+ });
224
+ assert.equal(restored.status, 200, JSON.stringify(restored.body));
225
+ assert.deepEqual(await permissionsOf(id.thirdTemplate), ["*"]);
226
+ });
227
+
228
+ // ---- what the exception must NOT have opened -----------------------------
229
+
230
+ record("8. a client member cannot use the restore path on a shared template", async () => {
231
+ // Reaching the restore still means passing the console gate. A tenant
232
+ // member holds no staff membership, so this must not be a way in.
233
+ const res = await patchPermissions(id.fourthTemplate, ["*"], clientSid);
234
+
235
+ assert.notEqual(res.status, 200, JSON.stringify(res.body));
236
+ assert.deepEqual(
237
+ await permissionsOf(id.fourthTemplate),
238
+ ["members:view-members"],
239
+ "a non-staff caller must not have restored anything",
240
+ );
241
+ });
242
+
243
+ record("9. an ORG-SCOPED role is unaffected — it still narrows normally", async () => {
244
+ const res = await patchPermissions(id.clientRole, ["members:view-members"], staffSid);
245
+
246
+ assert.equal(res.status, 200, JSON.stringify(res.body));
247
+ assert.deepEqual(await permissionsOf(id.clientRole), ["members:view-members"]);
248
+ });
249
+
250
+ record("10. a client editor still cannot grant a platform resource", async () => {
251
+ const res = await patchPermissions(
252
+ id.clientRole,
253
+ ["members:view-members", "organizations:see-all-organizations"],
254
+ staffSid,
255
+ );
256
+
257
+ assert.equal(res.status, 401, JSON.stringify(res.body));
258
+ assert.match(res.body?.message ?? "", /Seven365 console permissions/);
259
+ assert.deepEqual(await permissionsOf(id.clientRole), ["members:view-members"]);
260
+ });
261
+
262
+ record("11. POSITIVE CONTROL: the run really did write, and really did refuse", async () => {
263
+ // If every case above 401'd, cases 5-7 and 9 would be vacuous; if every
264
+ // case 200'd, cases 2-4 and 8 would be. Count both.
265
+ const verdicts = result.map(([v]) => v);
266
+ assert.equal(verdicts.includes("FAIL"), false, JSON.stringify(result));
267
+
268
+ for (const roleId of [id.template, id.thirdTemplate]) {
269
+ assert.deepEqual(await permissionsOf(roleId), ["*"], roleId.toString());
270
+ }
271
+ assert.deepEqual(await permissionsOf(id.secondTemplate), []);
272
+ // and one template that nothing was allowed to move
273
+ assert.deepEqual(await permissionsOf(id.fourthTemplate), ["members:view-members"]);
274
+ });
275
+ });
276
+
277
+ async function seed(h) {
278
+ const now = new Date().toISOString();
279
+ const hashed = await h.hashPassword(PASSWORD);
280
+
281
+ const staffRole = new ObjectId();
282
+ const clientOrg = new ObjectId();
283
+ const clientRole = new ObjectId();
284
+ const template = new ObjectId();
285
+ const secondTemplate = new ObjectId();
286
+ const thirdTemplate = new ObjectId();
287
+ const fourthTemplate = new ObjectId();
288
+
289
+ await h.db.collection("organizations").insertOne({
290
+ _id: clientOrg,
291
+ name: "RTR Client",
292
+ email: "rtr-org@e2e.example.com",
293
+ type: "org",
294
+ nature: "organization",
295
+ status: "active",
296
+ createdAt: now,
297
+ });
298
+
299
+ await h.db.collection("roles").insertMany([
300
+ // Seven365 staff: `type: "admin"` with `["*"]`, so the console gate passes
301
+ // and the cases measure the TEMPLATE rule, not authorization.
302
+ { _id: staffRole, name: "Staff Wildcard", type: "admin", status: "active", permissions: ["*"] },
303
+ // The client's own role — org-scoped, so the template rule never applies.
304
+ {
305
+ _id: clientRole,
306
+ name: "RTR Client Admin",
307
+ org: clientOrg,
308
+ type: "organization",
309
+ status: "active",
310
+ permissions: ["*"],
311
+ },
312
+ // Four shared templates: org-less, no `org` field at all. The first three
313
+ // hold production's broken list verbatim.
314
+ {
315
+ _id: template,
316
+ name: "Org Owner",
317
+ type: "organization",
318
+ status: "active",
319
+ default: true,
320
+ permissions: CONSOLE_SAVE,
321
+ },
322
+ {
323
+ _id: secondTemplate,
324
+ name: "Org Manager",
325
+ type: "organization",
326
+ status: "active",
327
+ permissions: CONSOLE_SAVE,
328
+ },
329
+ {
330
+ _id: thirdTemplate,
331
+ name: "Agency Owner",
332
+ type: "security_agency",
333
+ status: "active",
334
+ permissions: CONSOLE_SAVE,
335
+ },
336
+ {
337
+ _id: fourthTemplate,
338
+ name: "Org Supervisor",
339
+ type: "organization",
340
+ status: "active",
341
+ permissions: ["members:view-members"],
342
+ },
343
+ ]);
344
+
345
+ const users = await h.db.collection("users").insertMany([
346
+ { email: STAFF, password: hashed, name: "RTR Staff", status: "active", createdAt: now },
347
+ {
348
+ email: CLIENT,
349
+ password: hashed,
350
+ name: "RTR Client User",
351
+ status: "active",
352
+ defaultOrg: clientOrg.toString(),
353
+ createdAt: now,
354
+ },
355
+ ]);
356
+
357
+ await h.db.collection("members").insertMany([
358
+ { user: users.insertedIds[0], type: "admin", role: staffRole, status: "active" },
359
+ {
360
+ user: users.insertedIds[1],
361
+ org: clientOrg,
362
+ type: "organization",
363
+ role: clientRole,
364
+ status: "active",
365
+ },
366
+ ]);
367
+
368
+ return { clientOrg, clientRole, template, secondTemplate, thirdTemplate, fourthTemplate };
369
+ }
@@ -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
+ });
@@ -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
- const body = helper.slice(0, 500);
189
-
190
- const refuse = body.indexOf("refuseSharedClientTemplate(role)");
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", () => {