@7365admin1/core 3.53.1 → 3.53.3
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 +21 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +209 -76
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +270 -134
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/test/console-audit.test.mjs +4 -4
- package/test/console-permission.test.mjs +273 -0
- package/test/e2e/console-permission.e2e.test.mjs +286 -0
- package/test/file-delete-scope.test.mjs +64 -0
- package/test/member-scope.test.mjs +4 -4
- package/test/org-suspension.test.mjs +22 -7
- package/test/resident-ownership-scope.test.mjs +1 -1
- package/test/role-member-org-scope.test.mjs +1 -1
- package/test/role-scope.test.mjs +4 -4
- package/test/staff-console-authz.test.mjs +25 -25
- package/test/terms-acceptance-list.test.mjs +2 -2
|
@@ -47,7 +47,7 @@ test("the type=admin bypass is closed, and closed to staff only", () => {
|
|
|
47
47
|
|
|
48
48
|
assert.match(
|
|
49
49
|
body,
|
|
50
|
-
/if \(type === "admin"\)[\s\S]{0,120}await
|
|
50
|
+
/if \(type === "admin"\)[\s\S]{0,120}await requireConsolePermission\(req/,
|
|
51
51
|
"type=admin must be staff-only, not merely allowed through",
|
|
52
52
|
);
|
|
53
53
|
});
|
|
@@ -68,14 +68,14 @@ test("asking about your own memberships stays open; about somebody else's does n
|
|
|
68
68
|
const body = fn(controller, "getAll");
|
|
69
69
|
assert.match(
|
|
70
70
|
body,
|
|
71
|
-
/else if \(user && user !== callerId\(req\)\)[\s\S]{0,600}await
|
|
71
|
+
/else if \(user && user !== callerId\(req\)\)[\s\S]{0,600}await requireConsolePermission\(req/,
|
|
72
72
|
);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
test("the scope decision happens before the query runs", () => {
|
|
76
76
|
const body = fn(controller, "getAll");
|
|
77
77
|
const gate = Math.min(
|
|
78
|
-
...["
|
|
78
|
+
...["requireConsolePermission", "requireOrgAccess"]
|
|
79
79
|
.map((s) => body.indexOf(s))
|
|
80
80
|
.filter((i) => i !== -1),
|
|
81
81
|
);
|
|
@@ -107,6 +107,6 @@ test("a membership with no organisation is platform-staff only", () => {
|
|
|
107
107
|
// read as protection while passing everyone — and it would pass them on
|
|
108
108
|
// precisely the most privileged row in the collection.
|
|
109
109
|
const helper = controller.slice(controller.indexOf("async function requireMemberOrg"));
|
|
110
|
-
assert.match(helper.slice(0, 500), /if \(!orgId\)[\s\S]{0,120}
|
|
110
|
+
assert.match(helper.slice(0, 500), /if \(!orgId\)[\s\S]{0,120}requireConsolePermission\(req/);
|
|
111
111
|
assert.match(helper.slice(0, 500), /requireOrgAccess\(req, orgId\)/);
|
|
112
112
|
});
|
|
@@ -162,8 +162,8 @@ test("suspending and reactivating a client is OWNER-only, not staff", () => {
|
|
|
162
162
|
assert.ok(at !== -1, "organization.controller no longer defines updateStatus");
|
|
163
163
|
const body = ORG_CONTROLLER.slice(at);
|
|
164
164
|
|
|
165
|
-
assert.match(body, /await requirePlatformOwner\(req
|
|
166
|
-
const guard = body.
|
|
165
|
+
assert.match(body, /await requirePlatformOwner\(req/);
|
|
166
|
+
const guard = body.search(/await requirePlatformOwner\(req/);
|
|
167
167
|
const write = body.indexOf("await _updateStatusById(");
|
|
168
168
|
assert.ok(guard !== -1 && write !== -1 && guard < write);
|
|
169
169
|
|
|
@@ -181,11 +181,26 @@ test("suspending and reactivating a client is OWNER-only, not staff", () => {
|
|
|
181
181
|
test("the owner is told apart from ordinary staff by a marker no caller can set", () => {
|
|
182
182
|
assert.match(SUPER_ADMIN, /export async function isPlatformOwner\(/);
|
|
183
183
|
|
|
184
|
-
// The staff tier is unchanged: role type only.
|
|
184
|
+
// The staff tier is unchanged: role type only. Both tiers now read the SAME
|
|
185
|
+
// resolved role document (`resolveStaffRole`, added for the console module
|
|
186
|
+
// gate) instead of each running its own copy of the two queries — so the
|
|
187
|
+
// admin-type test is asserted where it now lives.
|
|
188
|
+
const resolver = fn(SUPER_ADMIN, "export async function resolveStaffRole(");
|
|
185
189
|
const staff = fn(SUPER_ADMIN, "export async function isSuperAdmin(");
|
|
186
190
|
const owner = fn(SUPER_ADMIN, "export async function isPlatformOwner(");
|
|
187
191
|
|
|
188
|
-
assert.match(
|
|
192
|
+
assert.match(resolver, /role\?\.type === "admin"/);
|
|
193
|
+
assert.match(resolver, /type: "admin"/, "the staff MEMBERSHIP test is gone");
|
|
194
|
+
// `default` appears in the resolver's RETURN TYPE — it hands the field to
|
|
195
|
+
// `isPlatformOwner`. What must never appear is a test ON it: the resolver
|
|
196
|
+
// returns every staff role, and only the owner tier reads the marker.
|
|
197
|
+
assert.doesNotMatch(
|
|
198
|
+
resolver,
|
|
199
|
+
/default === true|default: true/,
|
|
200
|
+
"the shared resolver must not have quietly become the owner gate",
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
assert.match(staff, /resolveStaffRole\(userId\)\) !== null/);
|
|
189
204
|
assert.doesNotMatch(
|
|
190
205
|
staff,
|
|
191
206
|
/default/,
|
|
@@ -193,8 +208,8 @@ test("the owner is told apart from ordinary staff by a marker no caller can set"
|
|
|
193
208
|
);
|
|
194
209
|
|
|
195
210
|
// The owner tier requires BOTH: an admin-typed role AND roles.default.
|
|
196
|
-
assert.match(owner, /
|
|
197
|
-
assert.match(owner,
|
|
211
|
+
assert.match(owner, /resolveStaffRole\(userId\)/);
|
|
212
|
+
assert.match(owner, /\.default === true/);
|
|
198
213
|
});
|
|
199
214
|
|
|
200
215
|
test("roles.default cannot be set through the role API", () => {
|
|
@@ -210,7 +225,7 @@ test("roles.default cannot be set through the role API", () => {
|
|
|
210
225
|
|
|
211
226
|
test("the owner guard is beside the staff guard, not a second mechanism", () => {
|
|
212
227
|
assert.match(CONSOLE_AUTHZ, /export async function requirePlatformOwner\(/);
|
|
213
|
-
assert.match(CONSOLE_AUTHZ, /isPlatformOwner,
|
|
228
|
+
assert.match(CONSOLE_AUTHZ, /isPlatformOwner, resolveStaffRole/);
|
|
214
229
|
// Same session-only resolution as the staff gate.
|
|
215
230
|
assert.match(
|
|
216
231
|
CONSOLE_AUTHZ,
|
|
@@ -46,7 +46,7 @@ test("the three 'my memberships' reads compare the URL id to the session", () =>
|
|
|
46
46
|
const body = fn(member, name);
|
|
47
47
|
assert.match(
|
|
48
48
|
body,
|
|
49
|
-
new RegExp(`requireSelfOrPlatformStaff\\(req, ${param}
|
|
49
|
+
new RegExp(`requireSelfOrPlatformStaff\\(req, ${param}[,)]`),
|
|
50
50
|
`${name} no longer scopes the user id in the URL to the caller`,
|
|
51
51
|
);
|
|
52
52
|
// The guard has to run BEFORE the read, not after it.
|
|
@@ -68,7 +68,7 @@ test("no org still means the org-less platform roles, not everybody's", () => {
|
|
|
68
68
|
const body = fn(roleController, "getRoles");
|
|
69
69
|
assert.doesNotMatch(
|
|
70
70
|
body,
|
|
71
|
-
/if \(!org\)[\s\S]{0,80}
|
|
71
|
+
/if \(!org\)[\s\S]{0,80}requireConsolePermission/,
|
|
72
72
|
"an org-less role list must stay reachable",
|
|
73
73
|
);
|
|
74
74
|
});
|
package/test/role-scope.test.mjs
CHANGED
|
@@ -95,10 +95,10 @@ test("createRole refuses a platform role from a non-staff caller", () => {
|
|
|
95
95
|
const body = fn(controller, "createRole");
|
|
96
96
|
assert.match(
|
|
97
97
|
body,
|
|
98
|
-
/if \(payload\.type === PLATFORM_STAFF_ROLE_TYPE\)[\s\S]{0,200}await
|
|
98
|
+
/if \(payload\.type === PLATFORM_STAFF_ROLE_TYPE\)[\s\S]{0,200}await requireConsolePermission\(req/,
|
|
99
99
|
);
|
|
100
100
|
|
|
101
|
-
const gate = body.
|
|
101
|
+
const gate = body.search(/requireConsolePermission\(/);
|
|
102
102
|
const write = body.indexOf("_createRole(");
|
|
103
103
|
assert.ok(gate !== -1 && write !== -1 && gate < write,
|
|
104
104
|
"the caller is checked before the role is written");
|
|
@@ -108,7 +108,7 @@ test("/api/roles/v2 is gated too — the same hole with /v2 on the URL", () => {
|
|
|
108
108
|
const body = fn(controllerV2, "createRole");
|
|
109
109
|
assert.match(
|
|
110
110
|
body,
|
|
111
|
-
/if \(value\.type === PLATFORM_STAFF_ROLE_TYPE\)[\s\S]{0,200}await
|
|
111
|
+
/if \(value\.type === PLATFORM_STAFF_ROLE_TYPE\)[\s\S]{0,200}await requireConsolePermission\(req/,
|
|
112
112
|
);
|
|
113
113
|
});
|
|
114
114
|
|
|
@@ -159,7 +159,7 @@ test("an org-less role is platform-staff only", () => {
|
|
|
159
159
|
// "no organisation to check, therefore allowed", the gate would be worse than
|
|
160
160
|
// none — it would read as protection while passing everyone.
|
|
161
161
|
const helper = controller.slice(controller.indexOf("async function requireRoleOrg"));
|
|
162
|
-
assert.match(helper.slice(0, 500), /if \(!orgId\)[\s\S]{0,120}
|
|
162
|
+
assert.match(helper.slice(0, 500), /if \(!orgId\)[\s\S]{0,120}requireConsolePermission\(req/);
|
|
163
163
|
assert.match(helper.slice(0, 500), /requireOrgAccess\(req, orgId\)/);
|
|
164
164
|
});
|
|
165
165
|
|
|
@@ -93,7 +93,7 @@ function body(file, name) {
|
|
|
93
93
|
*/
|
|
94
94
|
const GUARDED = [
|
|
95
95
|
// [file, handler, guard, the call it protects]
|
|
96
|
-
["organization.controller.ts", "add", "
|
|
96
|
+
["organization.controller.ts", "add", "requireConsolePermission", "_add("],
|
|
97
97
|
// REACH, not membership. Step 1 of the client onboarding wizard is this
|
|
98
98
|
// write and the owner has no `members` row yet - theirs is created at the
|
|
99
99
|
// last step. `requireOrgAccess` here 401'd every new client on the first
|
|
@@ -108,14 +108,14 @@ const GUARDED = [
|
|
|
108
108
|
"requirePlatformOwner",
|
|
109
109
|
"_updateStatusById(",
|
|
110
110
|
],
|
|
111
|
-
["organization-v2.controller.ts", "getAll", "
|
|
111
|
+
["organization-v2.controller.ts", "getAll", "requireConsolePermission", "_getAll("],
|
|
112
112
|
[
|
|
113
113
|
"organization-v2.controller.ts",
|
|
114
114
|
"getOrganizationsWithSubscription",
|
|
115
|
-
"
|
|
115
|
+
"requireConsolePermission",
|
|
116
116
|
"_getOrganizationsWithSubscription(",
|
|
117
117
|
],
|
|
118
|
-
["subscription.controller.ts", "add", "
|
|
118
|
+
["subscription.controller.ts", "add", "requireConsolePermission", "_add("],
|
|
119
119
|
[
|
|
120
120
|
"subscription.controller.ts",
|
|
121
121
|
"updateSubscriptionSeats",
|
|
@@ -128,66 +128,66 @@ const GUARDED = [
|
|
|
128
128
|
[
|
|
129
129
|
"subscription.controller.ts",
|
|
130
130
|
"createConsoleSubscription",
|
|
131
|
-
"
|
|
131
|
+
"requireConsolePermission",
|
|
132
132
|
"_add(",
|
|
133
133
|
],
|
|
134
134
|
[
|
|
135
135
|
"subscription.controller.ts",
|
|
136
136
|
"updateConsoleSubscription",
|
|
137
|
-
"
|
|
137
|
+
"requireConsolePermission",
|
|
138
138
|
"_updateConsoleFieldsById(",
|
|
139
139
|
],
|
|
140
140
|
[
|
|
141
141
|
"subscription.controller.ts",
|
|
142
142
|
"getConsoleSubscription",
|
|
143
|
-
"
|
|
143
|
+
"requireConsolePermission",
|
|
144
144
|
"_getByOrgId(",
|
|
145
145
|
],
|
|
146
|
-
["subscription-plan.controller.ts", "add", "
|
|
147
|
-
["subscription-plan.controller.ts", "update", "
|
|
146
|
+
["subscription-plan.controller.ts", "add", "requireConsolePermission", "_add("],
|
|
147
|
+
["subscription-plan.controller.ts", "update", "requireConsolePermission", "_update("],
|
|
148
148
|
[
|
|
149
149
|
"subscription-plan.controller.ts",
|
|
150
150
|
"updateStatus",
|
|
151
|
-
"
|
|
151
|
+
"requireConsolePermission",
|
|
152
152
|
"_updateStatusById(",
|
|
153
153
|
],
|
|
154
|
-
["promo-code.controller.ts", "add", "
|
|
154
|
+
["promo-code.controller.ts", "add", "requireConsolePermission", "_add("],
|
|
155
155
|
// Editing, disabling and removing a code are the same power as minting one:
|
|
156
156
|
// a discount is money off our own invoices whichever way it is written.
|
|
157
|
-
["promo-code.controller.ts", "update", "
|
|
157
|
+
["promo-code.controller.ts", "update", "requireConsolePermission", "_updateById("],
|
|
158
158
|
[
|
|
159
159
|
"promo-code.controller.ts",
|
|
160
160
|
"updateStatus",
|
|
161
|
-
"
|
|
161
|
+
"requireConsolePermission",
|
|
162
162
|
"_updateStatusById(",
|
|
163
163
|
],
|
|
164
164
|
[
|
|
165
165
|
"promo-code.controller.ts",
|
|
166
166
|
"remove",
|
|
167
|
-
"
|
|
167
|
+
"requireConsolePermission",
|
|
168
168
|
"_softDeleteById(",
|
|
169
169
|
],
|
|
170
|
-
["platform-terms.controller.ts", "add", "
|
|
170
|
+
["platform-terms.controller.ts", "add", "requireConsolePermission", "_add("],
|
|
171
171
|
// The acceptance record as a list - every account on the platform and
|
|
172
172
|
// whether it has consented. The fourth cross-tenant read, gated like the
|
|
173
173
|
// others; the per-user `getStatus` below cannot answer it.
|
|
174
174
|
[
|
|
175
175
|
"platform-terms.controller.ts",
|
|
176
176
|
"getAcceptance",
|
|
177
|
-
"
|
|
177
|
+
"requireConsolePermission",
|
|
178
178
|
"_listAcceptance(",
|
|
179
179
|
],
|
|
180
180
|
// The console's own history. A read, and gated like the two cross-tenant
|
|
181
181
|
// reads above: it is every client's commercial history in one list.
|
|
182
|
-
["console-audit.controller.ts", "getAll", "
|
|
182
|
+
["console-audit.controller.ts", "getAll", "requireConsolePermission", "_list("],
|
|
183
183
|
// Every account on the platform in one list - not filtered by org or site,
|
|
184
184
|
// so it spans every client's residents, guards and cleaners at once. The
|
|
185
185
|
// third cross-tenant read, gated like the other two.
|
|
186
|
-
["user.controller.ts", "getUsers", "
|
|
186
|
+
["user.controller.ts", "getUsers", "requireConsolePermission", "_getUsers("],
|
|
187
187
|
// /api/users/v2 is a separate mount onto a separate controller with its own
|
|
188
188
|
// copy of the same unfiltered list. Gating only v1 leaves the gate
|
|
189
189
|
// bypassable by adding /v2 to the URL.
|
|
190
|
-
["user-v2.controller.ts", "getUsers", "
|
|
190
|
+
["user-v2.controller.ts", "getUsers", "requireConsolePermission", "_getUsers("],
|
|
191
191
|
// The user is named in the URL, so without a check any signed-in account
|
|
192
192
|
// could read - or record - somebody else's acceptance by changing the id.
|
|
193
193
|
[
|
|
@@ -310,7 +310,7 @@ test("platform Terms are signed by the session, never by the request body", () =
|
|
|
310
310
|
"the author is back to being whatever the caller typed",
|
|
311
311
|
);
|
|
312
312
|
|
|
313
|
-
const guard = b.
|
|
313
|
+
const guard = b.search(/await requireConsolePermission\(req/);
|
|
314
314
|
assert.ok(guard !== -1 && guard < b.indexOf("createdBy:"));
|
|
315
315
|
});
|
|
316
316
|
|
|
@@ -327,7 +327,7 @@ test("a promo code is removed, never deleted", () => {
|
|
|
327
327
|
test("the code a staff member removed is attributed to them, not to the body", () => {
|
|
328
328
|
const b = body("promo-code.controller.ts", "remove");
|
|
329
329
|
|
|
330
|
-
assert.match(b, /const staffId = await
|
|
330
|
+
assert.match(b, /const staffId = await requireConsolePermission\(req, "promo-codes", "delete-promo-code"\)/);
|
|
331
331
|
assert.match(b, /_softDeleteById\(req\.params\.id, staffId\)/);
|
|
332
332
|
});
|
|
333
333
|
|
|
@@ -365,14 +365,14 @@ test("the customer checkout is NOT staff-gated", () => {
|
|
|
365
365
|
// purpose; this test exists so nobody closes it by reflex in a later sweep.
|
|
366
366
|
const b = body("subscription.controller.ts", "createOrgSubscription");
|
|
367
367
|
|
|
368
|
-
assert.doesNotMatch(b, /requirePlatformStaff/);
|
|
368
|
+
assert.doesNotMatch(b, /requirePlatformStaff|requireConsolePermission/);
|
|
369
369
|
assert.doesNotMatch(b, /requireOrgAccess/);
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
test("self-signup can still create its organisation", () => {
|
|
373
373
|
const b = body("organization.controller.ts", "addOnboardingOrg");
|
|
374
374
|
|
|
375
|
-
assert.doesNotMatch(b, /requirePlatformStaff/);
|
|
375
|
+
assert.doesNotMatch(b, /requirePlatformStaff|requireConsolePermission/);
|
|
376
376
|
assert.doesNotMatch(b, /requireOrgAccess/);
|
|
377
377
|
});
|
|
378
378
|
|
|
@@ -396,7 +396,7 @@ test("gating the platform-wide user list does not gate a person's own profile",
|
|
|
396
396
|
for (const file of ALSO_READ) {
|
|
397
397
|
assert.doesNotMatch(
|
|
398
398
|
body(file, name),
|
|
399
|
-
/requirePlatformStaff|requirePlatformOwner/,
|
|
399
|
+
/requirePlatformStaff|requirePlatformOwner|requireConsolePermission/,
|
|
400
400
|
`${file} ${name} is staff-gated — that locks users out of their own ` +
|
|
401
401
|
`account. Only the platform-wide list is a staff read.`,
|
|
402
402
|
);
|
|
@@ -414,7 +414,7 @@ test("no new endpoint arrives on this surface without a decision", () => {
|
|
|
414
414
|
assert.ok(
|
|
415
415
|
guarded.includes(name) || open.includes(name),
|
|
416
416
|
`${file} ${name} is new and is in neither list — gate it with ` +
|
|
417
|
-
`
|
|
417
|
+
`requireConsolePermission/requireOrgAccess, or add it to OPEN_BY_DESIGN ` +
|
|
418
418
|
`with the reason it stays open`,
|
|
419
419
|
);
|
|
420
420
|
}
|
|
@@ -47,11 +47,11 @@ test("the acceptance list is staff-gated, from the session, before it reads", ()
|
|
|
47
47
|
|
|
48
48
|
assert.match(
|
|
49
49
|
handler,
|
|
50
|
-
/await
|
|
50
|
+
/await requireConsolePermission\(req, "platform-terms", "see-platform-terms"\)/,
|
|
51
51
|
"the acceptance list is not behind the platform staff gate",
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
-
const gate = handler.
|
|
54
|
+
const gate = handler.search(/requireConsolePermission\(req/);
|
|
55
55
|
const reads = handler.indexOf("_listAcceptance(");
|
|
56
56
|
|
|
57
57
|
assert.ok(reads !== -1, "getAcceptance no longer reads the list");
|