fix(security): retire obsolete CRM representation deletion endpoint
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user