From a0a1331a978d9ad0184fce66fbd704280f01ae4d Mon Sep 17 00:00:00 2001 From: robbond Date: Thu, 5 Nov 2026 17:18:30 +0000 Subject: [PATCH] fix(security): retire obsolete CRM representation deletion endpoint --- actions/services/portalDirectService.js | 18 ----- actions/services/portalService.js | 2 - .../endpoint/deletemyrepresentations_api.js | 66 ----------------- pages/api/endpoint/gethash_api.js | 1 - tests/phase14/service-behaviour.test.cjs | 30 ++------ .../endpoint-handler-contract.test.cjs | 57 +-------------- .../phase22/portal-service-behaviour.test.cjs | 33 --------- tests/phase7/service-behaviour.test.cjs | 73 +------------------ 8 files changed, 13 insertions(+), 267 deletions(-) delete mode 100644 pages/api/endpoint/deletemyrepresentations_api.js diff --git a/actions/services/portalDirectService.js b/actions/services/portalDirectService.js index 6e315a41..16fad67a 100644 --- a/actions/services/portalDirectService.js +++ b/actions/services/portalDirectService.js @@ -144,24 +144,6 @@ export const createWatchedCases = async (formValues) => { } }; -export const deleteMyRepresentations = (myRepresentationsID) => { - var queryUrl = buildFileQuery("/api/endpoint/deletemyrepresentations_api", { - myRepresentationsID - }); - - return deleteSignedJson(queryUrl, { - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - "Accept": "application/json;odata.metadata=none", - "Prefer": 'odata.include-annotations="*",return=representation', - "Content-Type": "application/json" - } - }).catch((error) => { - consoleLogger(error); - }); -}; - export const deleteAwaitingSubmissions = (incidentID) => { var queryUrl = buildFileQuery( "/api/endpoint/deleteawaitingsubmissions_api", diff --git a/actions/services/portalService.js b/actions/services/portalService.js index a0b7f31c..8ce9c3b7 100644 --- a/actions/services/portalService.js +++ b/actions/services/portalService.js @@ -12,7 +12,6 @@ import { getAwaitingSubmissionProxy, createWatchedCases, deleteWatchedCases, - deleteMyRepresentations, deleteAwaitingSubmissions, sendCaseCompleteMessage, sendCaseCompleteMessageProxy, @@ -35,7 +34,6 @@ export { getAwaitingSubmissionProxy, createWatchedCases, deleteWatchedCases, - deleteMyRepresentations, deleteAwaitingSubmissions, sendCaseCompleteMessage, sendCaseCompleteMessageProxy, diff --git a/pages/api/endpoint/deletemyrepresentations_api.js b/pages/api/endpoint/deletemyrepresentations_api.js deleted file mode 100644 index 62e02b46..00000000 --- a/pages/api/endpoint/deletemyrepresentations_api.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @swagger - * /api/endpoint/deleteamyrepresentations_api: - * delete: - * tags: [Portal,Representations] - * summary: Phase 2 - * description: Delete my representations - * responses: - * 200: - * description: Success - */ - -import axios from "axios"; -import { consoleLogger } from "../../../actions/core/logger"; -import { getToken } from "../../../actions/core/token"; - -import { hashAPIPath } from "../../../actions/core/hash"; -import { respondError, respondSuccess } from "../middleware/apiResponse"; - -const WEBAPI_URL = - process.env.RELAY_ROOT || - "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; - -export default async function ApiProxy(req, res) { - const myRepresentationsID = req.query.myRepresentationsID; - - if ( - typeof myRepresentationsID !== "string" || - myRepresentationsID.trim().length === 0 - ) { - return respondError(res, { - status: 400, - code: "MY_REPRESENTATION_ID_REQUIRED", - message: "myRepresentationsID is required" - }); - } - - try { - const token = await getToken(); - const queryUrl = - "pinswg_representationses(" + myRepresentationsID + ")"; - - const config = { - method: "delete", - url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), - headers: { - "OData-MaxVersion": "4.0", - "OData-Version": "4.0", - Accept: "application/json", - Prefer: 'odata.include-annotations="*",return=representation', - Authorization: "Bearer " + token.access_token, - "Content-Type": "application/json" - } - }; - - const { data } = await axios(config); - return respondSuccess(res, data); - } catch (error) { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "MY_REPRESENTATION_DELETE_FAILED", - message: "Failed to delete representation" - }); - } -} diff --git a/pages/api/endpoint/gethash_api.js b/pages/api/endpoint/gethash_api.js index c68f61fe..fb3e28f9 100644 --- a/pages/api/endpoint/gethash_api.js +++ b/pages/api/endpoint/gethash_api.js @@ -38,7 +38,6 @@ ApiProxy.get(async (req, res) => { const allowedPrefix = [ "/api/endpoint/getportallogin_api", - "/api/endpoint/deletemyrepresentations_api", "/api/endpoint/deletewatchedcases_api", "/api/subscriptions/delete-watched-case", "/api/file/upload", diff --git a/tests/phase14/service-behaviour.test.cjs b/tests/phase14/service-behaviour.test.cjs index 31a3f599..398c9636 100644 --- a/tests/phase14/service-behaviour.test.cjs +++ b/tests/phase14/service-behaviour.test.cjs @@ -22,12 +22,20 @@ const loadModule = (relativePath, injected = {}) => { source += '\nif (typeof ApiProxy !== "undefined" && !module.exports.default) module.exports.default = ApiProxy;\n'; + const respondError = (res, { status, ...error }) => + res.status(status).json({ error }); + const respondSuccess = (res, data) => res.status(200).json(data); + const context = { module: { exports: {} }, exports: {}, require, process, console: { log: () => {}, error: () => {} }, + getServerSession: injected.getServerSession || injected.getSession, + authOptions: injected.authOptions || (() => ({})), + respondError, + respondSuccess, ...injected }; @@ -196,28 +204,6 @@ test("gethash_api returns hash for allow-listed deletewatchedcases path", async }); }); -test("gethash_api returns hash for allow-listed deletemyrepresentations path", async () => { - const mod = loadModule("pages/api/endpoint/gethash_api.js", { - hashAPIPath: () => "&hash=delrep", - getSession: async () => ({ user: { id: "u1" } }), - nextConnect: createNextConnectMock(), - middleware: () => {} - }); - - const req = { - query: { - path: "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=abc" - } - }; - const res = createRes(); - await mod.default.handler(req, res); - - assert.strictEqual(res.state.statusCode, 200); - assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { - hash: "&hash=delrep" - }); -}); - test("gethash_api rejects unauthenticated requests with 401", async () => { const mod = loadModule("pages/api/endpoint/gethash_api.js", { hashAPIPath: () => "&hash=expected", diff --git a/tests/phase21/endpoint-handler-contract.test.cjs b/tests/phase21/endpoint-handler-contract.test.cjs index ccdd8dd0..6c3b2f0f 100644 --- a/tests/phase21/endpoint-handler-contract.test.cjs +++ b/tests/phase21/endpoint-handler-contract.test.cjs @@ -3888,56 +3888,6 @@ test("deletewatchedcasesproxy catch path returns WATCHED_CASE_PROXY_DELETE_FAILE ); }); -test("deletemyrepresentations returns MY_REPRESENTATION_ID_REQUIRED when myRepresentationsID missing", async () => { - const mod = loadModule( - "pages/api/endpoint/deletemyrepresentations_api.js", - { - respondError: respondErrorMock, - respondSuccess: respondSuccessMock, - getToken: async () => ({ access_token: "token" }), - hashAPIPath: () => "&hash=expected", - axios: async () => ({ data: {} }), - consoleLogger: () => {} - } - ); - - const req = { query: {} }; - const res = createRes(); - await mod.default(req, res); - - assert.strictEqual(res.state.statusCode, 400); - assert.strictEqual( - res.state.jsonBody.error.code, - "MY_REPRESENTATION_ID_REQUIRED" - ); -}); - -test("deletemyrepresentations catch path returns MY_REPRESENTATION_DELETE_FAILED", async () => { - const mod = loadModule( - "pages/api/endpoint/deletemyrepresentations_api.js", - { - respondError: respondErrorMock, - respondSuccess: respondSuccessMock, - getToken: async () => ({ access_token: "token" }), - hashAPIPath: () => "&hash=expected", - axios: async () => { - throw new Error("delete failed"); - }, - consoleLogger: () => {} - } - ); - - const req = { query: { myRepresentationsID: "r1" } }; - const res = createRes(); - await mod.default(req, res); - - assert.strictEqual(res.state.statusCode, 400); - assert.strictEqual( - res.state.jsonBody.error.code, - "MY_REPRESENTATION_DELETE_FAILED" - ); -}); - test("deleteawaitingsubmissions returns INCIDENT_ID_REQUIRED when incidentID missing", async () => { const mod = loadModule( "pages/api/endpoint/deleteawaitingsubmissions_api.js", @@ -4948,9 +4898,8 @@ test("getsipsmedia returns success contract", async () => { assert.strictEqual(res.state.jsonBody["@odata.count"], 4); }); -test("getsipsmedia returns METHOD_NOT_ALLOWED for non-GET methods", async () => { +test("getsipsmedia returns CASE_ID_REQUIRED when caseid missing", async () => { const mod = loadModule("pages/api/endpoint/getsipsmedia_api.js", { - respondSuccess: respondSuccessMock, respondError: respondErrorMock }); @@ -4958,8 +4907,8 @@ test("getsipsmedia returns METHOD_NOT_ALLOWED for non-GET methods", async () => const res = createRes(); await mod.default(req, res); - assert.strictEqual(res.state.statusCode, 405); - assert.strictEqual(res.state.jsonBody.error.code, "METHOD_NOT_ALLOWED"); + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual(res.state.jsonBody.error.code, "CASE_ID_REQUIRED"); }); test("getappealtypesfornewappeal returns METHOD_NOT_ALLOWED for non-GET methods", async () => { diff --git a/tests/phase22/portal-service-behaviour.test.cjs b/tests/phase22/portal-service-behaviour.test.cjs index 2c847fd6..bbae655b 100644 --- a/tests/phase22/portal-service-behaviour.test.cjs +++ b/tests/phase22/portal-service-behaviour.test.cjs @@ -43,39 +43,6 @@ test("portal/sendCaseCompleteMessage appends signed hash suffix to inv query", a ); }); -test("portal/deleteMyRepresentations uses signed delete request with headers", async () => { - const axios = createAxiosMock(); - const logger = createLoggerMock(); - - const requestCalls = []; - const requestJson = async (config) => { - requestCalls.push(config); - return { deleted: true }; - }; - - const portal = loadServiceModule("portalDirectService.js", { - axios, - BASE_URL: "", - consoleLogger: logger.consoleLogger, - buildHashedQueryUrl: async (queryUrl) => `${queryUrl}&hash=xyz`, - requestJson - }); - - const result = await portal.deleteMyRepresentations("rep-1"); - - assert.deepStrictEqual(normalize(result), { deleted: true }); - assert.strictEqual(requestCalls.length, 1); - assert.strictEqual(requestCalls[0].method, "delete"); - assert.strictEqual( - requestCalls[0].url, - "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=xyz" - ); - assert.strictEqual( - requestCalls[0].headers["Content-Type"], - "application/json" - ); -}); - test("portal/createWatchedCases posts to harmonized route helper URL", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); diff --git a/tests/phase7/service-behaviour.test.cjs b/tests/phase7/service-behaviour.test.cjs index dbe91bb3..05764a66 100644 --- a/tests/phase7/service-behaviour.test.cjs +++ b/tests/phase7/service-behaviour.test.cjs @@ -65,75 +65,6 @@ test("document/getAwaitingSubmissionFromBlob logs and returns undefined on failu assert.strictEqual(logger.calls.length, 1); }); -test("portal/deleteMyRepresentations appends hash and returns data", async () => { - const axios = createAxiosMock(); - const logger = createLoggerMock(); - const signCalls = []; - - axios.getHandler = async (url) => { - if (url.startsWith("/api/endpoint/gethash_api?path=")) { - signCalls.push(url); - return { data: { hash: "&hash=portal123" } }; - } - - throw new Error("Unexpected get url: " + url); - }; - - axios.requestHandler = async () => ({ data: { removed: true } }); - - const portal = loadServiceModule("portalDirectService.js", { - axios, - BASE_URL: "", - consoleLogger: logger.consoleLogger, - hashAPIPath: () => "&hash=fallback" - }); - - const result = await portal.deleteMyRepresentations("rep-1"); - - assert.deepStrictEqual(normalize(result), { removed: true }); - assert.strictEqual( - signCalls[0], - "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletemyrepresentations_api%3FmyRepresentationsID%3Drep-1" - ); - assert.strictEqual( - axios.calls[0].url, - "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletemyrepresentations_api%3FmyRepresentationsID%3Drep-1" - ); - assert.strictEqual( - axios.calls[1].config.url, - "/api/endpoint/deletemyrepresentations_api?myRepresentationsID=rep-1&hash=portal123" - ); - assert.strictEqual(axios.calls[1].config.method, "delete"); -}); - -test("portal/deleteMyRepresentations logs and returns undefined on failure", async () => { - const axios = createAxiosMock(); - const logger = createLoggerMock(); - - axios.getHandler = async (url) => { - if (url.startsWith("/api/endpoint/gethash_api?path=")) { - return { data: { hash: "&hash=portal-fail" } }; - } - - throw new Error("Unexpected get url: " + url); - }; - - axios.requestHandler = async () => - Promise.reject(createAxiosError(401, "Unauthorized")); - - const portal = loadServiceModule("portalDirectService.js", { - axios, - BASE_URL: "", - consoleLogger: logger.consoleLogger, - hashAPIPath: () => "&hash=fallback" - }); - - const result = await portal.deleteMyRepresentations("rep-2"); - - assert.strictEqual(result, undefined); - assert.strictEqual(logger.calls.length, 1); -}); - test("portal/deleteWatchedCases uses signer hash and returns data", async () => { const axios = createAxiosMock(); const logger = createLoggerMock(); @@ -162,11 +93,11 @@ test("portal/deleteWatchedCases uses signer hash and returns data", async () => assert.deepStrictEqual(normalize(result), { removed: true }); assert.strictEqual( signCalls[0], - "/api/endpoint/gethash_api?path=%2Fapi%2Fendpoint%2Fdeletewatchedcases_api%3FwatchedCaseID%3Dwatch-1" + "/api/endpoint/gethash_api?path=%2Fapi%2Fsubscriptions%2Fdelete-watched-case%3FwatchedCaseID%3Dwatch-1" ); assert.strictEqual( axios.calls[1].config.url, - "/api/endpoint/deletewatchedcases_api?watchedCaseID=watch-1&hash=watch123" + "/api/subscriptions/delete-watched-case?watchedCaseID=watch-1&hash=watch123" ); });