@7365admin1/core 3.32.2-staging.67 → 3.32.2-staging.68
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/.changeset/camera-list-site-scoping.md +42 -0
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/test/camera-write-gate.test.mjs +25 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
"@7365admin1/core": patch
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Scope `GET /site-cameras` to the caller's own site.
|
|
6
|
+
|
|
7
|
+
The site was an OPTIONAL query parameter and nothing on the handler looked at
|
|
8
|
+
the session beyond `requireAuth`. So any signed-in account, in any organisation,
|
|
9
|
+
could page the whole estate's cameras - on staging that is all 15 active IP
|
|
10
|
+
cameras across 3 owner organisations, each record carrying its `host` and its
|
|
11
|
+
`username`. It is the plainest cross-tenant read left in the camera module.
|
|
12
|
+
|
|
13
|
+
`site` is now required, and the caller is authorised against it with
|
|
14
|
+
`entitleSite`: membership of the site, of its owning organisation, or an ACTIVE
|
|
15
|
+
`customer.sites` engagement with it. "Not yours" and "does not exist" answer
|
|
16
|
+
identically, so site ids cannot be enumerated from the difference.
|
|
17
|
+
|
|
18
|
+
**Every caller in the estate already sends `site`** - checked across every
|
|
19
|
+
repository, not assumed:
|
|
20
|
+
|
|
21
|
+
| caller | sends |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `iservice365-mobile-app-security` `pages/cctv/index.vue` (live) | `site`, `type: ip`, `page` |
|
|
24
|
+
| `iservice365-mobile-app-security` `useAnprEnabled` (live) | `site`, `type: anpr` |
|
|
25
|
+
| `iservice365-layer-common` `CameraMain.vue` (Site Settings) | `site`, `type`, `page` |
|
|
26
|
+
| `iservice365-layer-common` `CameraWall.vue` | `site`, `type: ip`, `page` |
|
|
27
|
+
| `iservice365-web-app-security` `VirtualPatrolRoutesForm.vue` | `site`, `type: ip`, `page: 1` |
|
|
28
|
+
| `isecure365-mobile-app` `site.service.ts` `getAnprCameras` | `site`, `type: anpr` |
|
|
29
|
+
|
|
30
|
+
So no running client breaks.
|
|
31
|
+
|
|
32
|
+
Deliberately NOT gated on a camera permission. This is the same list the site's
|
|
33
|
+
own Settings panel and the guard's app have always read; on staging only 25 of
|
|
34
|
+
the 118 people entitled to a site with a camera hold a camera-view permission,
|
|
35
|
+
so a permission gate here would strand people who are legitimately at the site.
|
|
36
|
+
Restricting the read to the caller's own sites is the fix; who among them may
|
|
37
|
+
look is a separate decision that needs its own measurement.
|
|
38
|
+
|
|
39
|
+
The repository's 15-minute cache key already carries the site, so a cached page
|
|
40
|
+
can only be served to callers authorised for that same site. No caller id was
|
|
41
|
+
added to the key - it would make the cache per-person for an answer that is
|
|
42
|
+
identical per site.
|
package/dist/index.js
CHANGED
|
@@ -34228,7 +34228,7 @@ function useSiteCameraController() {
|
|
|
34228
34228
|
deleteById: _deleteById,
|
|
34229
34229
|
findOne: _findOne
|
|
34230
34230
|
} = useSiteCameraRepo();
|
|
34231
|
-
const { authorizeSite } = useCameraViewService();
|
|
34231
|
+
const { authorizeSite, entitleSite } = useCameraViewService();
|
|
34232
34232
|
function callerId(req) {
|
|
34233
34233
|
const user = req.user;
|
|
34234
34234
|
return user?.user?.toString?.() || user?._id?.toString?.();
|
|
@@ -34300,10 +34300,9 @@ function useSiteCameraController() {
|
|
|
34300
34300
|
}
|
|
34301
34301
|
async function getAll(req, res, next) {
|
|
34302
34302
|
const query2 = req.query;
|
|
34303
|
-
const allowedTypes = ["ip", "exit", "entry", "both"];
|
|
34304
34303
|
const validation = import_joi49.default.object({
|
|
34305
34304
|
type: import_joi49.default.string().optional(),
|
|
34306
|
-
site: import_joi49.default.string().
|
|
34305
|
+
site: import_joi49.default.string().hex().length(24).required(),
|
|
34307
34306
|
page: import_joi49.default.number().min(1).optional(),
|
|
34308
34307
|
limit: import_joi49.default.number().min(10).optional()
|
|
34309
34308
|
});
|
|
@@ -34313,10 +34312,18 @@ function useSiteCameraController() {
|
|
|
34313
34312
|
return;
|
|
34314
34313
|
}
|
|
34315
34314
|
const type = value.type ?? null;
|
|
34316
|
-
const site = value.site
|
|
34315
|
+
const site = value.site;
|
|
34317
34316
|
const page = value.page ?? 1;
|
|
34318
34317
|
const limit = value.limit ? parseInt(value.limit) : 10;
|
|
34319
34318
|
try {
|
|
34319
|
+
try {
|
|
34320
|
+
await entitleSite({ siteId: site, userId: callerId(req) });
|
|
34321
|
+
} catch (error2) {
|
|
34322
|
+
if (error2 instanceof import_node_server_utils95.NotFoundError) {
|
|
34323
|
+
throw new import_node_server_utils95.NotFoundError("Site not found.");
|
|
34324
|
+
}
|
|
34325
|
+
throw error2;
|
|
34326
|
+
}
|
|
34320
34327
|
const siteCameras = await _getAll({ type, site, page, limit });
|
|
34321
34328
|
res.json(siteCameras);
|
|
34322
34329
|
return;
|