security: retire obsolete CRM representation deletion endpoint

This commit is contained in:
2026-11-04 17:10:23 +00:00
parent 5729625229
commit d0ce82c69f
8 changed files with 24 additions and 271 deletions
-18
View File
@@ -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",
-2
View File
@@ -12,7 +12,6 @@ import {
getAwaitingSubmissionProxy,
createWatchedCases,
deleteWatchedCases,
deleteMyRepresentations,
deleteAwaitingSubmissions,
sendCaseCompleteMessage,
sendCaseCompleteMessageProxy,
@@ -35,7 +34,6 @@ export {
getAwaitingSubmissionProxy,
createWatchedCases,
deleteWatchedCases,
deleteMyRepresentations,
deleteAwaitingSubmissions,
sendCaseCompleteMessage,
sendCaseCompleteMessageProxy,
@@ -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"
});
}
}
-1
View File
@@ -38,7 +38,6 @@ ApiProxy.get(async (req, res) => {
const allowedPrefix = [
"/api/endpoint/getportallogin_api",
"/api/endpoint/deletemyrepresentations_api",
"/api/endpoint/deletewatchedcases_api",
"/api/file/upload",
"/api/file/uploadsinglefile",
+8 -22
View File
@@ -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",
@@ -1262,6 +1262,8 @@ test("getadvancedsearchpaged catch path returns ADVANCED_SEARCH_PAGED_FETCH_FAIL
relayGet: async ({ res, errorResponse }) =>
respondErrorMock(res, errorResponse),
azureHeadersPagedCustom: () => ({}),
hasOwn: (obj, key) =>
obj != null && Object.prototype.hasOwnProperty.call(obj, key),
_: { has: () => false }
});
@@ -3073,56 +3075,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",
@@ -4021,7 +3973,8 @@ test("gethash returns UNAUTHENTICATED when session missing", async () => {
respondError: respondErrorMock,
respondSuccess: respondSuccessMock,
hashAPIPath: () => "&hash=expected",
getSession: async () => null,
getServerSession: async () => null,
authOptions: () => ({}),
nextConnect: () => {
const handler = { registeredGet: null };
handler.get = (cb) => {
@@ -4045,7 +3998,10 @@ test("gethash returns INVALID_HASH_PATH when path is invalid", async () => {
respondError: respondErrorMock,
respondSuccess: respondSuccessMock,
hashAPIPath: () => "&hash=expected",
getSession: async () => ({ user: { email: "user@test.local" } }),
getServerSession: async () => ({
user: { email: "user@test.local" }
}),
authOptions: () => ({}),
nextConnect: () => {
const handler = { registeredGet: null };
handler.get = (cb) => {
@@ -4178,10 +4134,11 @@ test("getToken catch path returns TOKEN_FETCH_FAILED", async () => {
test("getsipsmedia returns success contract", async () => {
const mod = loadModule("pages/api/endpoint/getsipsmedia_api.js", {
respondSuccess: respondSuccessMock
relayGet: async ({ res }) =>
respondSuccessMock(res, { "@odata.count": 4 })
});
const req = { query: {} };
const req = { query: { caseid: "case-1" } };
const res = createRes();
await mod.default(req, res);
@@ -4189,9 +4146,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
});
@@ -4199,8 +4155,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 () => {
@@ -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();
+2 -71
View File
@@ -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"
);
});