Fix self-serve lane command customer authorization

This commit is contained in:
Jeppe B
2026-06-02 09:15:24 +02:00
parent ec1988715d
commit 46bdeded78
2 changed files with 102 additions and 20 deletions
@@ -460,11 +460,6 @@ class moduleSelfServeRoute
self::requireType($commandParam, self::type_string());
// Get the lane and command
$lane = $selfserve->lane($lane_id);
// Require access to the lane's department to prevent cross-department command execution
if (empty($lane->department_lane) || empty($lane->department_lane->department)) {
$response->error('Lane department not found', 404);
}
self::requireDepartmentAccess((string)$lane->department_lane->department->value());
// If the user has the bypass permission, set the lane to bypass customer number validation
if (self::hasPermission('modules_selfserve_lane_command_bypass_customer_number_validation')) {
$lane->setBypassCustomerNumberValidation(true);
@@ -482,7 +477,7 @@ class moduleSelfServeRoute
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_start',
false
true
);
break;
case selfserve_lane_command::STOP:
@@ -495,34 +490,36 @@ class moduleSelfServeRoute
);
break;
case selfserve_lane_command::RESERVE:
$this->requireSelfServeLaneCommandPermission(
$this->requireOperatorLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_reserve',
false
'modules_selfserve_lane_command_execute_reserve'
);
break;
case selfserve_lane_command::RELEASE:
$this->requireSelfServeLaneCommandPermission(
$this->requireOperatorLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_release',
false
'modules_selfserve_lane_command_execute_release'
);
break;
case selfserve_lane_command::RESET:
$this->requireSelfServeLaneCommandPermission(
$this->requireOperatorLaneCommandPermission(
$lane,
$customer_number,
'modules_selfserve_lane_command_execute_reset',
false
'modules_selfserve_lane_command_execute_reset'
);
break;
case selfserve_lane_command::OPEN_PROPERTY_ACCESS_GATE:
self::requirePermission('modules_selfserve_lane_command_execute_open_property_access_gate');
$this->requirePropertyGateCommandPermission(
'modules_selfserve_lane_command_execute_open_property_access_gate',
$lane,
$customer_number
);
break;
case selfserve_lane_command::OPEN_PROPERTY_EXIT_GATE:
self::requirePermission('modules_selfserve_lane_command_execute_open_property_exit_gate');
$this->requirePropertyGateCommandPermission(
'modules_selfserve_lane_command_execute_open_property_exit_gate',
$lane,
$customer_number
);
break;
}
// Execute the command
@@ -1435,6 +1432,18 @@ class moduleSelfServeRoute
);
}
private function requireOperatorLaneCommandPermission(selfserve_lane $lane, string $command_permission): void
{
if (empty($lane->department_lane) || empty($lane->department_lane->department)) {
global $response;
$response->error('Lane department not found', 404);
}
self::requireDepartmentAccess((string)$lane->department_lane->department->value());
self::requirePermission('modules_selfserve_lane_command_execute');
self::requirePermission($command_permission);
}
private function requirePropertyGateCommandPermission(string $permission, selfserve_lane $lane, int $customer_number): void
{
$elevated_permissions = [
@@ -465,3 +465,76 @@ it('keeps read-only self-serve preview and summary refreshes from touching relay
expect($vehicleConditionsRoute)->toContain('$this->getWashFlow()->synchronizeSession($lane, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);');
expect($vehicleConditionsRoute)->toContain('$this->getWashFlow()->synchronizeSession($lane_id, $reg, $customer_id, $activate_machine, $vehicle_type_id, $sync_relay_state);');
});
it('classifies self-serve lane command route authorization by customer product behavior', function (): void {
$moduleSelfServeRoute = file_get_contents(app_path('routes/moduleSelfServeRoute.php'));
$productDocs = file_get_contents(app_path('modules/selfserve/selfserve.md'));
$washFlow = file_get_contents(app_path('modules/selfserve/classes/selfserve_wash_flow.php'));
expect($moduleSelfServeRoute)->not->toBeFalse();
expect($productDocs)->not->toBeFalse();
expect($washFlow)->not->toBeFalse();
// Product flow confirms the customer UI sends STOP, while route docs expose START and property gates
// as customer-scoped lane commands. RESERVE, RELEASE, and RESET remain absent from customer flow/docs.
expect($productDocs)->toContain('Customer->>API: POST /modules/self-serve/lane/command (STOP)');
expect($productDocs)->toContain('customer `list_own_department_selfserve_vehicle_conditions` for scoped `START`, scoped `STOP`, and property gate commands');
expect($productDocs)->toContain('Customer `START` requires an enabled self-serve lane. Customer `STOP` and property gate commands require the customer\'s active wash in the lane department.');
expect($washFlow)->toContain('$payload[\'command\'] = $relayRole === \'PROPERTY_ENTRANCE\' ? \'OPEN_PROPERTY_ACCESS_GATE\' : \'OPEN_PROPERTY_EXIT_GATE\'');
expect($washFlow)->toContain('$signalType = \'studio_action_gate_open\'');
$commandCases = selfserve_lane_command_route_cases($moduleSelfServeRoute);
expect($commandCases['START'])
->toContain('requireSelfServeLaneCommandPermission')
->toContain("'modules_selfserve_lane_command_execute_start'")
->toContain("true\n );")
->not->toContain('requireDepartmentAccess')
->not->toContain('requireOperatorLaneCommandPermission');
expect($commandCases['STOP'])
->toContain('requireSelfServeLaneCommandPermission')
->toContain("'modules_selfserve_lane_command_execute_stop'")
->toContain("true,\n true")
->not->toContain('requireDepartmentAccess')
->not->toContain('requireOperatorLaneCommandPermission');
expect($commandCases['OPEN_PROPERTY_ACCESS_GATE'])
->toContain('requirePropertyGateCommandPermission')
->toContain("'modules_selfserve_lane_command_execute_open_property_access_gate'")
->not->toContain('self::requirePermission')
->not->toContain('requireDepartmentAccess');
expect($commandCases['OPEN_PROPERTY_EXIT_GATE'])
->toContain('requirePropertyGateCommandPermission')
->toContain("'modules_selfserve_lane_command_execute_open_property_exit_gate'")
->not->toContain('self::requirePermission')
->not->toContain('requireDepartmentAccess');
foreach (['RESERVE', 'RELEASE', 'RESET'] as $operatorOnlyCommand) {
expect($commandCases[$operatorOnlyCommand])
->toContain('requireOperatorLaneCommandPermission')
->not->toContain('requireSelfServeLaneCommandPermission')
->not->toContain('requirePropertyGateCommandPermission');
}
});
/**
* @return array<string,string>
*/
function selfserve_lane_command_route_cases(string $moduleSelfServeRoute): array
{
$cases = [];
preg_match_all(
'/case selfserve_lane_command::([A-Z_]+):(.*?)(?=\n\s*case selfserve_lane_command::|\n\s*}\n\s*\/\/ Execute the command)/s',
$moduleSelfServeRoute,
$matches,
PREG_SET_ORDER
);
foreach ($matches as $match) {
$cases[$match[1]] = $match[2];
}
return $cases;
}