@7365admin1/core 3.32.2-staging.110 → 3.32.2-staging.111

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.
@@ -0,0 +1,35 @@
1
+ ---
2
+ "@7365admin1/core": patch
3
+ ---
4
+
5
+ HID Amico: authorise the card, PIN, discovery and visitor-QR endpoints
6
+
7
+ Ten HID Amico handlers required a session and nothing else. The reader id in
8
+ the URL was the only thing selecting the device, and `GET /readers` returns
9
+ every reader in the estate when no `site` is supplied, so any signed-in account
10
+ could act on any reader at any site of any organisation — assign or delete a
11
+ physical access card, set or clear a keypad PIN, put a reader into card
12
+ enrolment, list a user's stored card values, or revoke a visitor's QR pass.
13
+
14
+ Each of the ten now authorises before it does anything:
15
+
16
+ - `listUserCards`, `assignUserCard`, `enrollUserCard`, `cancelUserCardEnrollment`,
17
+ `deleteUserCard` and `revokeVisitorQr` require `site-settings:manage-entry-pass`
18
+ at the site of the **stored** reader.
19
+ - `getUserPinStatus`, `setUserPin` and `deleteUserPin` allow either that same
20
+ administrator, or the person whose own device user it is — entitlement to the
21
+ reader's site plus an ownership check on `hidUserId`, so the resident and MA
22
+ mobile apps' "Access PIN" screen keeps working while a neighbour's PIN cannot
23
+ be rewritten.
24
+ - `discoverReader` now takes the `site` the reader is being added to and
25
+ authorises it, and the address it may be pointed at is constrained: an
26
+ `http(s)` origin only, no credentials, no path, and never the API host itself
27
+ or the instance-metadata range. `HID_DISCOVERY_ALLOWED_HOSTS` optionally pins
28
+ it to named hosts; unset, nothing changes operationally.
29
+
30
+ A reader that is not yours answers exactly like a reader that does not exist, so
31
+ reader ids cannot be enumerated by watching the difference.
32
+
33
+ **Consumer change required:** `POST /access-management/hid/readers/discover` now
34
+ requires `site` in the body. `HidReaderForm.vue` in `@7365admin1/layer-common` is
35
+ the only caller and must be updated to send it.
package/dist/index.d.ts CHANGED
@@ -3630,6 +3630,13 @@ declare function useCameraViewService(): {
3630
3630
  * different, narrower right and passes its own list.
3631
3631
  */
3632
3632
  permissions?: Array<string>;
3633
+ /**
3634
+ * What to say when the permission is missing. The rule this function
3635
+ * applies — membership of the site, then the granting role's permissions —
3636
+ * is not camera-specific, and the HID module now asks the same question of
3637
+ * the same data; only the sentence a refused caller reads differs.
3638
+ */
3639
+ message?: string;
3633
3640
  }) => Promise<{
3634
3641
  site: mongodb.WithId<bson.Document> | null;
3635
3642
  siteObjectId: ObjectId;
@@ -9554,6 +9561,13 @@ declare const schemaHidAmicoLogQuery: Joi.ObjectSchema<any>;
9554
9561
  declare const schemaHidAmicoIdentityQuery: Joi.ObjectSchema<any>;
9555
9562
  declare const schemaHidAmicoSync: Joi.ObjectSchema<any>;
9556
9563
  declare const schemaHidAmicoVisitorQr: Joi.ObjectSchema<any>;
9564
+ /**
9565
+ * Discovery is the only HID call that names no reader, so before this there was
9566
+ * nothing on the request to authorise against. `site` is the site the reader is
9567
+ * being added to — the same field `schemaHidAmicoReader` already requires when
9568
+ * the reader is actually created — and it is what the caller's entitlement is
9569
+ * checked against.
9570
+ */
9557
9571
  declare const schemaDiscoverHidAmicoReader: Joi.ObjectSchema<any>;
9558
9572
  declare const schemaHidAmicoVisitorImageParams: Joi.ObjectSchema<any>;
9559
9573
  declare const schemaHidAmicoUserPinParams: Joi.ObjectSchema<any>;
@@ -9818,6 +9832,15 @@ type HidObjectMutation = {
9818
9832
  where?: UnknownRecord;
9819
9833
  };
9820
9834
  declare function useHidAmicoService(): {
9835
+ authorizeReader: (readerId: string, userId: string, options?: {
9836
+ includePassword?: boolean;
9837
+ }) => Promise<THidAmicoReader>;
9838
+ authorizeReaderUser: (readerId: string, hidUserIdValue: string | number, userId: string) => Promise<THidAmicoReader>;
9839
+ authorizeSiteForCaller: (siteId: string, userId: string) => Promise<{
9840
+ site: mongodb.WithId<bson.Document> | null;
9841
+ siteObjectId: bson.ObjectId;
9842
+ db: mongodb.Db;
9843
+ }>;
9821
9844
  listReaders: (value: {
9822
9845
  site?: string;
9823
9846
  page?: number;
package/dist/index.js CHANGED
@@ -37328,7 +37328,7 @@ function useCameraViewService() {
37328
37328
  const required = params.permissions ?? CAMERA_VIEW_PERMISSIONS;
37329
37329
  if (!hasAnyPermission(permissions, required)) {
37330
37330
  throw new import_node_server_utils107.UnauthorizedError(
37331
- required === CAMERA_VIEW_PERMISSIONS ? "You do not have permission to view these cameras." : "You do not have permission to set up cameras on this site."
37331
+ params.message ?? (required === CAMERA_VIEW_PERMISSIONS ? "You do not have permission to view these cameras." : "You do not have permission to set up cameras on this site.")
37332
37332
  );
37333
37333
  }
37334
37334
  return { site, siteObjectId, db };
@@ -41655,6 +41655,7 @@ var schemaHidAmicoVisitorQr = import_joi63.default.object({
41655
41655
  qrFormat: import_joi63.default.string().valid("0", "1", "2").optional()
41656
41656
  });
41657
41657
  var schemaDiscoverHidAmicoReader = import_joi63.default.object({
41658
+ site: objectIdSchema2.required(),
41658
41659
  baseUrl: import_joi63.default.string().uri({ scheme: ["http", "https"] }).required(),
41659
41660
  username: import_joi63.default.string().trim().required(),
41660
41661
  password: import_joi63.default.string().required()
@@ -42546,6 +42547,82 @@ function useHidAmicoRepo() {
42546
42547
  };
42547
42548
  }
42548
42549
 
42550
+ // src/utils/hid-amico-authz.util.ts
42551
+ var HID_MANAGE_PERMISSIONS = ["site-settings:manage-entry-pass"];
42552
+ var HID_DISCOVERY_ALLOWED_SCHEMES = ["http:", "https:"];
42553
+ function isBlockedDiscoveryHost(hostname) {
42554
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
42555
+ if (host === "localhost" || host.endsWith(".localhost"))
42556
+ return true;
42557
+ if (host.includes(":")) {
42558
+ if (host === "::" || host === "::1")
42559
+ return true;
42560
+ if (/^fe[89ab]/.test(host) || /^fe[c-f]/.test(host))
42561
+ return true;
42562
+ const mapped = host.match(/^::ffff:([0-9a-f:.]+)$/);
42563
+ if (mapped) {
42564
+ const value = mapped[1];
42565
+ if (value.includes("."))
42566
+ return isBlockedDiscoveryHost(value);
42567
+ const [high, low] = value.split(":");
42568
+ const octets2 = [
42569
+ parseInt(high.padStart(4, "0").slice(0, 2), 16),
42570
+ parseInt(high.padStart(4, "0").slice(2), 16),
42571
+ parseInt((low ?? "0").padStart(4, "0").slice(0, 2), 16),
42572
+ parseInt((low ?? "0").padStart(4, "0").slice(2), 16)
42573
+ ];
42574
+ if (octets2.every((octet) => Number.isInteger(octet))) {
42575
+ return isBlockedDiscoveryHost(octets2.join("."));
42576
+ }
42577
+ }
42578
+ return false;
42579
+ }
42580
+ const octets = host.split(".");
42581
+ if (octets.length !== 4 || !octets.every((octet) => /^\d{1,3}$/.test(octet))) {
42582
+ return false;
42583
+ }
42584
+ const [first, second] = octets.map(Number);
42585
+ if (first === 0)
42586
+ return true;
42587
+ if (first === 127)
42588
+ return true;
42589
+ if (first === 169 && second === 254)
42590
+ return true;
42591
+ if (first >= 224)
42592
+ return true;
42593
+ return false;
42594
+ }
42595
+ function discoveryAllowedHosts(raw = process.env.HID_DISCOVERY_ALLOWED_HOSTS) {
42596
+ return String(raw || "").split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean);
42597
+ }
42598
+ function discoveryUrlRefusalReason(baseUrl, allowedHosts = discoveryAllowedHosts()) {
42599
+ if (typeof baseUrl !== "string" || !baseUrl.trim()) {
42600
+ return "A reader address is required.";
42601
+ }
42602
+ let url;
42603
+ try {
42604
+ url = new URL(baseUrl.trim());
42605
+ } catch {
42606
+ return "The reader address is not a valid URL.";
42607
+ }
42608
+ if (!HID_DISCOVERY_ALLOWED_SCHEMES.includes(url.protocol)) {
42609
+ return "The reader address must be an http or https URL.";
42610
+ }
42611
+ if (url.username || url.password) {
42612
+ return "The reader address must not carry credentials.";
42613
+ }
42614
+ if (url.pathname && url.pathname !== "/" || url.search || url.hash) {
42615
+ return "The reader address must be a host and optional port only.";
42616
+ }
42617
+ if (isBlockedDiscoveryHost(url.hostname)) {
42618
+ return "The reader address is not a permitted reader location.";
42619
+ }
42620
+ if (allowedHosts.length && !allowedHosts.includes(url.hostname.toLowerCase())) {
42621
+ return "The reader address is not a permitted reader location.";
42622
+ }
42623
+ return null;
42624
+ }
42625
+
42549
42626
  // src/utils/hid-card.util.ts
42550
42627
  var HID_CARD_VALUE_FACTOR = 2 ** 32;
42551
42628
  function requireSafeNonNegativeInteger(value, label) {
@@ -42590,6 +42667,7 @@ function resolveHidPhysicalCardValue(input) {
42590
42667
  // src/services/hid-amico.service.ts
42591
42668
  var PASSWORD_PREFIX = "v1";
42592
42669
  var remoteCardEnrollmentReaders = /* @__PURE__ */ new Set();
42670
+ var HID_READER_NOT_FOUND = "HID Amico reader not found.";
42593
42671
  var PJSIP_CONFIGURATION_KEYS = [
42594
42672
  "enabled",
42595
42673
  "server_ip",
@@ -43635,6 +43713,58 @@ function useHidAmicoService() {
43635
43713
  const visitorRepo = useVisitorTransactionRepo();
43636
43714
  const memberRepo = useMemberRepo();
43637
43715
  const userRepo = useUserRepo();
43716
+ async function authorizeSiteForCaller(siteId, userId) {
43717
+ return useCameraViewService().authorizeSite({
43718
+ siteId,
43719
+ userId,
43720
+ permissions: HID_MANAGE_PERMISSIONS,
43721
+ message: "You do not have permission to manage HID readers on this site."
43722
+ });
43723
+ }
43724
+ async function authorizeReader(readerId, userId, options = {}) {
43725
+ let reader;
43726
+ try {
43727
+ reader = await repo.getById(readerId, options);
43728
+ } catch {
43729
+ throw new import_node_server_utils126.NotFoundError(HID_READER_NOT_FOUND);
43730
+ }
43731
+ try {
43732
+ await authorizeSiteForCaller(String(reader.site || ""), userId);
43733
+ } catch (error) {
43734
+ if (error instanceof import_node_server_utils126.UnauthorizedError)
43735
+ throw error;
43736
+ throw new import_node_server_utils126.NotFoundError(HID_READER_NOT_FOUND);
43737
+ }
43738
+ return reader;
43739
+ }
43740
+ async function entitleReaderForSelf(readerId, userId) {
43741
+ let reader;
43742
+ try {
43743
+ reader = await repo.getById(readerId);
43744
+ } catch {
43745
+ throw new import_node_server_utils126.NotFoundError(HID_READER_NOT_FOUND);
43746
+ }
43747
+ try {
43748
+ await useCameraViewService().entitleSite({
43749
+ siteId: String(reader.site || ""),
43750
+ userId
43751
+ });
43752
+ } catch {
43753
+ throw new import_node_server_utils126.NotFoundError(HID_READER_NOT_FOUND);
43754
+ }
43755
+ return reader;
43756
+ }
43757
+ async function callersOwnHidUserId(readerId, userId) {
43758
+ const identity = await repo.findProfileIdentity(readerId, userId);
43759
+ return identity?.hidUserId ? Number(identity.hidUserId) : createStableHidId(`profile:${readerId}:${userId}`);
43760
+ }
43761
+ async function authorizeReaderUser(readerId, hidUserIdValue, userId) {
43762
+ const reader = await entitleReaderForSelf(readerId, userId);
43763
+ const own = await callersOwnHidUserId(readerId, userId);
43764
+ if (Number(hidUserIdValue) === own)
43765
+ return reader;
43766
+ return authorizeReader(readerId, userId);
43767
+ }
43638
43768
  async function getActiveReader(id) {
43639
43769
  const reader = await repo.getById(id, { includePassword: true });
43640
43770
  if (reader.enabled === false || reader.status === "inactive" || reader.status === "deleted") {
@@ -43769,6 +43899,9 @@ function useHidAmicoService() {
43769
43899
  };
43770
43900
  }
43771
43901
  async function discoverReader(value) {
43902
+ const refusal = discoveryUrlRefusalReason(value.baseUrl);
43903
+ if (refusal)
43904
+ throw new import_node_server_utils126.BadRequestError(refusal);
43772
43905
  const client = new HidAmicoClient({
43773
43906
  ...value,
43774
43907
  baseUrl: value.baseUrl.replace(/\/+$/, "")
@@ -45323,6 +45456,9 @@ function useHidAmicoService() {
45323
45456
  return sipAccountResponse(account);
45324
45457
  }
45325
45458
  return {
45459
+ authorizeReader,
45460
+ authorizeReaderUser,
45461
+ authorizeSiteForCaller,
45326
45462
  listReaders,
45327
45463
  discoverReader,
45328
45464
  createReader,
@@ -81257,6 +81393,9 @@ function getAuthenticatedUserId(req) {
81257
81393
  }
81258
81394
  function useHidAmicoController() {
81259
81395
  const service = useHidAmicoService();
81396
+ const authorizeReader = (req, readerId) => service.authorizeReader(readerId, getAuthenticatedUserId(req));
81397
+ const authorizeReaderUser = (req, readerId, hidUserId) => service.authorizeReaderUser(readerId, hidUserId, getAuthenticatedUserId(req));
81398
+ const authorizeSite = (req, siteId) => service.authorizeSiteForCaller(siteId, getAuthenticatedUserId(req));
81260
81399
  async function listReaders(req, res, next) {
81261
81400
  const { error, value } = schemaHidAmicoReaderListQuery.validate(req.query);
81262
81401
  if (error) {
@@ -81575,6 +81714,7 @@ function useHidAmicoController() {
81575
81714
  return;
81576
81715
  }
81577
81716
  try {
81717
+ await authorizeSite(req, value.site);
81578
81718
  res.json({ data: await service.discoverReader(value) });
81579
81719
  } catch (error2) {
81580
81720
  next(error2);
@@ -81587,6 +81727,7 @@ function useHidAmicoController() {
81587
81727
  return;
81588
81728
  }
81589
81729
  try {
81730
+ await authorizeReaderUser(req, value.readerId, value.hidUserId);
81590
81731
  res.json({ data: await service.getUserPinStatus(value.readerId, value.hidUserId) });
81591
81732
  } catch (error2) {
81592
81733
  next(error2);
@@ -81599,6 +81740,7 @@ function useHidAmicoController() {
81599
81740
  return;
81600
81741
  }
81601
81742
  try {
81743
+ await authorizeReader(req, value.readerId);
81602
81744
  res.json({ data: await service.listUserCards(value.readerId, value.hidUserId) });
81603
81745
  } catch (requestError) {
81604
81746
  next(requestError);
@@ -81612,6 +81754,7 @@ function useHidAmicoController() {
81612
81754
  return;
81613
81755
  }
81614
81756
  try {
81757
+ await authorizeReader(req, params.value.readerId);
81615
81758
  res.status(201).json({
81616
81759
  data: await service.assignUserCard(
81617
81760
  params.value.readerId,
@@ -81631,6 +81774,7 @@ function useHidAmicoController() {
81631
81774
  return;
81632
81775
  }
81633
81776
  try {
81777
+ await authorizeReader(req, params.value.readerId);
81634
81778
  res.status(201).json({
81635
81779
  data: await service.enrollUserCard(
81636
81780
  params.value.readerId,
@@ -81650,6 +81794,7 @@ function useHidAmicoController() {
81650
81794
  return;
81651
81795
  }
81652
81796
  try {
81797
+ await authorizeReader(req, value.readerId);
81653
81798
  res.json({ data: await service.cancelUserCardEnrollment(value.readerId) });
81654
81799
  } catch (requestError) {
81655
81800
  next(requestError);
@@ -81662,6 +81807,7 @@ function useHidAmicoController() {
81662
81807
  return;
81663
81808
  }
81664
81809
  try {
81810
+ await authorizeReader(req, value.readerId);
81665
81811
  res.json({
81666
81812
  data: await service.deleteUserCard(
81667
81813
  value.readerId,
@@ -81681,6 +81827,7 @@ function useHidAmicoController() {
81681
81827
  return;
81682
81828
  }
81683
81829
  try {
81830
+ await authorizeReaderUser(req, params.value.readerId, params.value.hidUserId);
81684
81831
  res.json({
81685
81832
  data: await service.setUserPin(
81686
81833
  params.value.readerId,
@@ -81699,6 +81846,7 @@ function useHidAmicoController() {
81699
81846
  return;
81700
81847
  }
81701
81848
  try {
81849
+ await authorizeReaderUser(req, value.readerId, value.hidUserId);
81702
81850
  res.json({ data: await service.deleteUserPin(value.readerId, value.hidUserId) });
81703
81851
  } catch (error2) {
81704
81852
  next(error2);
@@ -81711,6 +81859,7 @@ function useHidAmicoController() {
81711
81859
  return;
81712
81860
  }
81713
81861
  try {
81862
+ await authorizeReader(req, value.readerId);
81714
81863
  res.json({
81715
81864
  data: await service.revokeVisitorQr(value.readerId, value.visitorId)
81716
81865
  });