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:
Jeppe B
2026-06-01 21:08:43 +02:00
committed by GitHub
2 changed files with 77 additions and 3 deletions
@@ -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();
});
});