Merge pull request #65 from copenhagentruckwash/fix-privilege-escalation-in-daily-report
Guard department lane toggles by wash-lane permission
This commit is contained in:
+16
-3
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import {ref, watch} from "vue";
|
||||
import {computed, ref, watch} from "vue";
|
||||
import { BLoading } from "buefy";
|
||||
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
@@ -26,6 +26,9 @@ const washes = ref(0);
|
||||
const outsideHours = ref(createEmptyOutsideHours());
|
||||
const laneToggles = ref([]);
|
||||
const laneToggleError = ref("");
|
||||
const canManageLaneToggles = computed(() =>
|
||||
SessionUser.hasPermission("admin") || SessionUser.hasPermission("list_department_wash_lanes")
|
||||
);
|
||||
|
||||
const normalizeBoolean = (value, defaultValue = false) => {
|
||||
if (typeof value === "boolean") {
|
||||
@@ -249,6 +252,11 @@ const applyLaneUpdate = (laneId, currentLane, update, savingPatch = {}) => {
|
||||
};
|
||||
|
||||
const toggleMachineStatus = async (lane, event) => {
|
||||
if (!canManageLaneToggles.value) {
|
||||
event.target.checked = isMachineToggleChecked(lane);
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = event.target.checked;
|
||||
const previousLane = { ...lane };
|
||||
|
||||
@@ -278,6 +286,11 @@ const toggleMachineStatus = async (lane, event) => {
|
||||
};
|
||||
|
||||
const toggleDognvask = async (lane, event) => {
|
||||
if (!canManageLaneToggles.value) {
|
||||
event.target.checked = isDognvaskToggleChecked(lane);
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = event.target.checked;
|
||||
const previousLane = { ...lane };
|
||||
|
||||
@@ -335,7 +348,7 @@ watch([selected_date, selected_date_to], () => {
|
||||
type="checkbox"
|
||||
:id="machineToggleId(lane)"
|
||||
:checked="isMachineToggleChecked(lane)"
|
||||
:disabled="lane.isSavingMachineStatus"
|
||||
:disabled="!canManageLaneToggles || lane.isSavingMachineStatus"
|
||||
@change="toggleMachineStatus(lane, $event)"
|
||||
>
|
||||
<div class="status-toggle-top">
|
||||
@@ -371,7 +384,7 @@ watch([selected_date, selected_date_to], () => {
|
||||
type="checkbox"
|
||||
:id="dognvaskToggleId(lane)"
|
||||
:checked="isDognvaskToggleChecked(lane)"
|
||||
:disabled="lane.isSavingDognvask"
|
||||
:disabled="!canManageLaneToggles || lane.isSavingDognvask"
|
||||
@change="toggleDognvask(lane, $event)"
|
||||
>
|
||||
<div class="status-toggle-top">
|
||||
|
||||
@@ -8,6 +8,7 @@ const getDepartmentMock = vi.hoisted(() => vi.fn());
|
||||
const getLaneStatusTogglesMock = vi.hoisted(() => vi.fn());
|
||||
const setMachineStatusEnabledMock = vi.hoisted(() => vi.fn());
|
||||
const setLaneSelfServeEnabledMock = vi.hoisted(() => vi.fn());
|
||||
const hasPermissionMock = vi.hoisted(() => vi.fn());
|
||||
const laneHelpers = vi.hoisted(() => {
|
||||
const requiredFields = [
|
||||
"relay_in_id",
|
||||
@@ -67,6 +68,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
},
|
||||
},
|
||||
},
|
||||
hasPermission: hasPermissionMock,
|
||||
functions: {
|
||||
date: {
|
||||
isToday: () => false,
|
||||
@@ -141,6 +143,8 @@ describe("Department overview period sync behavior", () => {
|
||||
getLaneStatusTogglesMock.mockReset();
|
||||
setMachineStatusEnabledMock.mockReset();
|
||||
setLaneSelfServeEnabledMock.mockReset();
|
||||
hasPermissionMock.mockReset();
|
||||
hasPermissionMock.mockImplementation((permission) => permission === "list_department_wash_lanes");
|
||||
|
||||
getDepartmentMock.mockResolvedValue({ id: 20, name: "Dept 20" });
|
||||
getLaneStatusTogglesMock.mockResolvedValue({ data: { data: [] } });
|
||||
@@ -438,4 +442,61 @@ describe("Department overview period sync behavior", () => {
|
||||
expect(setLaneSelfServeEnabledMock).toHaveBeenCalledTimes(2);
|
||||
expect(wrapper.find("#dognvask-20-22").element.checked).toBe(false);
|
||||
});
|
||||
|
||||
it("disables lane mutation toggles for daily-report users without wash-lane permission", async () => {
|
||||
hasPermissionMock.mockImplementation((permission) => permission === "list_department_daily_reports");
|
||||
getLaneStatusTogglesMock.mockResolvedValueOnce({
|
||||
data: {
|
||||
data: [
|
||||
{
|
||||
id: 21,
|
||||
department: 20,
|
||||
name: "T1",
|
||||
status: "AVAILABLE",
|
||||
machine_status_enabled: true,
|
||||
selfserve_enabled: true,
|
||||
relay_in_id: "in-1",
|
||||
relay_out_id: "out-1",
|
||||
relay_machine_id: "machine-1",
|
||||
relay_machine_program_picker_id: "picker-1",
|
||||
relay_machine_cleaner_id: "cleaner-1",
|
||||
dynamic_image_id: 1,
|
||||
machine_type_id: 1,
|
||||
dognvask_configured: true,
|
||||
dognvask_configuration_warnings: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = mount(DepartmentDailyReportSmall, {
|
||||
props: {
|
||||
department_id: 20,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: (value) => value,
|
||||
},
|
||||
stubs: {
|
||||
BLoading: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await flushAll();
|
||||
await flushAll();
|
||||
|
||||
const machineToggle = wrapper.find("#machine-status-20-21");
|
||||
const dognvaskToggle = wrapper.find("#dognvask-20-21");
|
||||
|
||||
expect(machineToggle.element.disabled).toBe(true);
|
||||
expect(dognvaskToggle.element.disabled).toBe(true);
|
||||
|
||||
await machineToggle.trigger("change");
|
||||
await dognvaskToggle.trigger("change");
|
||||
await flushAll();
|
||||
|
||||
expect(setMachineStatusEnabledMock).not.toHaveBeenCalled();
|
||||
expect(setLaneSelfServeEnabledMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user