@7365admin1/core 3.53.6 → 3.53.7
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 +18 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/verification-scope.test.mjs +96 -0
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Listing and cancelling invitations both ask who is calling.
|
|
3
|
+
*
|
|
4
|
+
* `GET /api/verifications` took `orgId` as an OPTIONAL query filter with
|
|
5
|
+
* `requireAuth` in front and nothing else, so omitting it read every client's
|
|
6
|
+
* pending invitations. `PUT /api/verifications/:id/cancel` took the id off the
|
|
7
|
+
* URL with no authorization identifier in the handler at all, so any signed-in
|
|
8
|
+
* account could cancel any other client's invitations.
|
|
9
|
+
*
|
|
10
|
+
* What is pinned here is the wiring — the part that silently rots when somebody
|
|
11
|
+
* adds a filter, or copies one of these handlers to make a v3 — plus the two
|
|
12
|
+
* rules inside the gates themselves, because the rules are what make the fix
|
|
13
|
+
* lockout-free rather than merely closed.
|
|
14
|
+
*
|
|
15
|
+
* Nothing here opens a socket or a database. Every assertion reads a file.
|
|
16
|
+
*/
|
|
17
|
+
import test from "node:test";
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
|
|
22
|
+
const read = (rel) =>
|
|
23
|
+
readFileSync(fileURLToPath(new URL(`../src/${rel}`, import.meta.url)), "utf8");
|
|
24
|
+
|
|
25
|
+
const v1 = read("controllers/verification.controller.ts");
|
|
26
|
+
const v2 = read("controllers/verification-v2.controller.ts");
|
|
27
|
+
const consoleAuthz = read("utils/console-authz.util.ts");
|
|
28
|
+
|
|
29
|
+
/** One `async function name(...)` body out of a source file. */
|
|
30
|
+
function fn(text, name) {
|
|
31
|
+
const start = text.indexOf(`async function ${name}(`);
|
|
32
|
+
assert.notEqual(start, -1, `${name} is no longer defined`);
|
|
33
|
+
const rest = text.slice(start);
|
|
34
|
+
const next = rest.slice(1).search(/\n *async function [a-zA-Z0-9_]+ ?\(/);
|
|
35
|
+
return next === -1 ? rest : rest.slice(0, next + 1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** [source, label, handler, the gate it must call, the call it guards] */
|
|
39
|
+
const GUARDED = [
|
|
40
|
+
[v1, "v1", "getVerifications", "requireVerificationListReach(", "_getVerifications("],
|
|
41
|
+
[v2, "v2", "getVerifications", "requireVerificationListReach(", "_getVerifications("],
|
|
42
|
+
[v1, "v1", "cancelUserInvitation", "requireVerificationReach(", "_cancelUserInvitation("],
|
|
43
|
+
[v2, "v2", "cancelUserInvitation", "requireVerificationReach(", "_cancelUserInvitation("],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
test("both list and both cancel handlers ask, and ask BEFORE the read or write", () => {
|
|
47
|
+
for (const [source, label, handler, gate, guarded] of GUARDED) {
|
|
48
|
+
const body = fn(source, handler);
|
|
49
|
+
|
|
50
|
+
assert.ok(
|
|
51
|
+
body.includes(gate),
|
|
52
|
+
`${label} ${handler} no longer calls ${gate}`
|
|
53
|
+
);
|
|
54
|
+
assert.ok(
|
|
55
|
+
body.indexOf(gate) < body.indexOf(guarded),
|
|
56
|
+
`${label} ${handler} calls ${guarded} before it is authorized`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("the cancel gate reads the STORED verification, never the request", () => {
|
|
62
|
+
for (const [source, label] of [[v1, "v1"], [v2, "v2"]]) {
|
|
63
|
+
const body = fn(source, "cancelUserInvitation");
|
|
64
|
+
assert.match(
|
|
65
|
+
body,
|
|
66
|
+
/requireVerificationReach\(\s*req,\s*await _getVerificationById\(otpId\)/,
|
|
67
|
+
`${label} cancel must authorize against the stored record`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("the v1 list gate is handed the caller's own orgId, not a literal", () => {
|
|
73
|
+
const body = fn(v1, "getVerifications");
|
|
74
|
+
assert.match(body, /requireVerificationListReach\(req, orgId\)/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("an omitted organisation costs Seven365 staff identity", () => {
|
|
78
|
+
const gate = fn(consoleAuthz, "requireVerificationListReach");
|
|
79
|
+
assert.ok(gate.includes("requireOrgReach(req, orgId)"), "the org branch is gone");
|
|
80
|
+
assert.ok(gate.includes("requirePlatformStaff(req)"), "the estate-wide branch is gone");
|
|
81
|
+
// The org branch must return, or a reaching member would then be asked for
|
|
82
|
+
// staff identity as well and every web app's invitations screen would 401.
|
|
83
|
+
assert.match(gate, /requireOrgReach\(req, orgId\);\s*\n\s*return;/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a verification that names no organisation behaves exactly as before", () => {
|
|
87
|
+
const gate = fn(consoleAuthz, "requireVerificationReach");
|
|
88
|
+
// `createUserInvite` writes rows with an empty organisation and they are
|
|
89
|
+
// cancelled from the staff console. Refusing them would break a working
|
|
90
|
+
// screen; this early return is the zero-lockout half of the fix.
|
|
91
|
+
assert.match(gate, /if \(!org\) return;/);
|
|
92
|
+
assert.ok(
|
|
93
|
+
gate.indexOf("if (!org) return;") < gate.indexOf("requireOrgReach"),
|
|
94
|
+
"the org-less carve-out must come before the refusal"
|
|
95
|
+
);
|
|
96
|
+
});
|