Merge pull request #250 from copenhagentruckwash/propose-fix-for-privilege-boundary-regression
Restrict studio simulation to config-version view and prevent auto-creating drafts
This commit is contained in:
@@ -517,21 +517,11 @@ class selfserve_studio_graph
|
||||
throw new \RuntimeException('lane_id is required for studio simulation.');
|
||||
}
|
||||
|
||||
$configSource = strtolower(trim((string)($payload['config_source'] ?? 'draft')));
|
||||
if (!in_array($configSource, ['draft', 'published'], true)) {
|
||||
$configSource = 'draft';
|
||||
}
|
||||
|
||||
$versioning = new selfserve_config_versioning();
|
||||
if ($configSource === 'published') {
|
||||
$version = $versioning->getPublishedV2Config($departmentId);
|
||||
$config = is_array($version['config'] ?? null) ? (array)$version['config'] : null;
|
||||
$versionId = isset($version['version_id']) ? (int)$version['version_id'] : null;
|
||||
} else {
|
||||
$version = $versioning->ensureDraftFromLegacy($departmentId, $userId, false);
|
||||
$config = is_array($version['config'] ?? null) ? (array)$version['config'] : $versioning->snapshotLegacyConfig($departmentId);
|
||||
$versionId = isset($version['id']) ? (int)$version['id'] : null;
|
||||
}
|
||||
$configContext = $this->loadSimulationConfig($departmentId, $payload, $permissions, $versioning);
|
||||
$configSource = $configContext['config_source'];
|
||||
$config = $configContext['config'];
|
||||
$versionId = $configContext['version_id'];
|
||||
|
||||
$includeHardware = filter_var($payload['include_hardware'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
$includeHardware = $includeHardware !== false;
|
||||
@@ -592,6 +582,70 @@ class selfserve_studio_graph
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @param array<string,bool> $permissions
|
||||
* @return array{config_source:string,config:array<string,mixed>|null,version_id:int|null}
|
||||
*/
|
||||
private function loadSimulationConfig(
|
||||
int $departmentId,
|
||||
array $payload,
|
||||
array $permissions,
|
||||
selfserve_config_versioning $versioning
|
||||
): array {
|
||||
$configSource = $this->resolveSimulationConfigSource($payload, $permissions);
|
||||
|
||||
if ($configSource === 'published') {
|
||||
$version = $versioning->getPublishedV2Config($departmentId);
|
||||
return [
|
||||
'config_source' => $configSource,
|
||||
'config' => is_array($version['config'] ?? null) ? (array)$version['config'] : null,
|
||||
'version_id' => isset($version['version_id']) ? (int)$version['version_id'] : null,
|
||||
];
|
||||
}
|
||||
|
||||
$draft = (new selfserve_config_versions_o())->selectLatestByDepartmentAndStatus(
|
||||
$departmentId,
|
||||
selfserve_config_versioning::STATUS_DRAFT
|
||||
);
|
||||
if (!$draft->exists()) {
|
||||
return [
|
||||
'config_source' => $configSource,
|
||||
'config' => $versioning->snapshotLegacyConfig($departmentId),
|
||||
'version_id' => null,
|
||||
];
|
||||
}
|
||||
|
||||
$config = (array)($draft->config_json->value() ?? []);
|
||||
if (!$versioning->isV2Config($config)) {
|
||||
$config = $versioning->migrateLegacyConfigToV2($config + ['department_id' => $departmentId]);
|
||||
}
|
||||
|
||||
return [
|
||||
'config_source' => $configSource,
|
||||
'config' => $config,
|
||||
'version_id' => (int)$draft->id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @param array<string,bool> $permissions
|
||||
*/
|
||||
private function resolveSimulationConfigSource(array $payload, array $permissions): string
|
||||
{
|
||||
$configSource = strtolower(trim((string)($payload['config_source'] ?? 'draft')));
|
||||
if (!in_array($configSource, ['draft', 'published'], true)) {
|
||||
$configSource = 'draft';
|
||||
}
|
||||
|
||||
if ($configSource === 'draft' && !($permissions['can_view'] ?? false)) {
|
||||
throw new \RuntimeException('Draft studio simulation requires list_department_selfserve_config_versions permission.');
|
||||
}
|
||||
|
||||
return $configSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $payload
|
||||
* @param array<string,bool> $permissions
|
||||
@@ -605,21 +659,11 @@ class selfserve_studio_graph
|
||||
?callable $progressCallback = null
|
||||
): array
|
||||
{
|
||||
$configSource = strtolower(trim((string)($payload['config_source'] ?? 'draft')));
|
||||
if (!in_array($configSource, ['draft', 'published'], true)) {
|
||||
$configSource = 'draft';
|
||||
}
|
||||
|
||||
$versioning = new selfserve_config_versioning();
|
||||
if ($configSource === 'published') {
|
||||
$version = $versioning->getPublishedV2Config($departmentId);
|
||||
$config = is_array($version['config'] ?? null) ? (array)$version['config'] : null;
|
||||
$versionId = isset($version['version_id']) ? (int)$version['version_id'] : null;
|
||||
} else {
|
||||
$version = $versioning->ensureDraftFromLegacy($departmentId, $userId, false);
|
||||
$config = is_array($version['config'] ?? null) ? (array)$version['config'] : $versioning->snapshotLegacyConfig($departmentId);
|
||||
$versionId = isset($version['id']) ? (int)$version['id'] : null;
|
||||
}
|
||||
$configContext = $this->loadSimulationConfig($departmentId, $payload, $permissions, $versioning);
|
||||
$configSource = $configContext['config_source'];
|
||||
$config = $configContext['config'];
|
||||
$versionId = $configContext['version_id'];
|
||||
|
||||
$includeHardware = filter_var($payload['include_hardware'] ?? true, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||
$includeHardware = $includeHardware !== false;
|
||||
|
||||
@@ -109,7 +109,7 @@ class departmentSelfserveStudioRoute
|
||||
|
||||
$this->post('/department/selfserve/studio/simulate', function (): void {
|
||||
global $response;
|
||||
$user = $this->requireStudioUser('list_department_selfserve_vehicle_conditions');
|
||||
$user = $this->requireStudioUser('list_department_selfserve_config_versions');
|
||||
self::requireParameters(['department', 'lane_id', 'reg']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$laneId = (int)self::getParameter('lane_id');
|
||||
@@ -129,11 +129,11 @@ class departmentSelfserveStudioRoute
|
||||
$response->error($exception->getMessage(), 422);
|
||||
}
|
||||
}, [
|
||||
'list_department_selfserve_vehicle_conditions' => 'Run the self-serve studio simulator',
|
||||
'list_department_selfserve_config_versions' => 'Run the self-serve studio simulator',
|
||||
]);
|
||||
|
||||
$this->post('/department/selfserve/studio/path-outcomes/stream', function (): void {
|
||||
$user = $this->requireStudioUser('list_department_selfserve_vehicle_conditions');
|
||||
$user = $this->requireStudioUser('list_department_selfserve_config_versions');
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
@@ -169,12 +169,12 @@ class departmentSelfserveStudioRoute
|
||||
}
|
||||
exit;
|
||||
}, [
|
||||
'list_department_selfserve_vehicle_conditions' => 'Stream grouped self-serve studio question path outcome progress',
|
||||
'list_department_selfserve_config_versions' => 'Stream grouped self-serve studio question path outcome progress',
|
||||
]);
|
||||
|
||||
$this->post('/department/selfserve/studio/path-outcomes', function (): void {
|
||||
global $response;
|
||||
$user = $this->requireStudioUser('list_department_selfserve_vehicle_conditions');
|
||||
$user = $this->requireStudioUser('list_department_selfserve_config_versions');
|
||||
self::requireParameters(['department']);
|
||||
$departmentId = (int)self::getParameter('department');
|
||||
$this->assertDepartmentAccess($user, $departmentId);
|
||||
@@ -192,7 +192,7 @@ class departmentSelfserveStudioRoute
|
||||
$response->error($exception->getMessage(), 422);
|
||||
}
|
||||
}, [
|
||||
'list_department_selfserve_vehicle_conditions' => 'Project grouped self-serve studio question path outcomes',
|
||||
'list_department_selfserve_config_versions' => 'Project grouped self-serve studio question path outcomes',
|
||||
]);
|
||||
|
||||
$this->post('/department/selfserve/studio/path-confirmations', function (): void {
|
||||
|
||||
@@ -120,7 +120,26 @@ it('wires the all-in-one self-serve studio replacement endpoints', function ():
|
||||
expect($studioRoute)->toContain('modules_shelly_config');
|
||||
expect($studioRoute)->toContain('modules_selfserve_sessions_force_stop');
|
||||
|
||||
$simulateStart = strpos($studioRoute, "/department/selfserve/studio/simulate");
|
||||
$simulateEnd = strpos($studioRoute, "/department/selfserve/studio/path-outcomes/stream", $simulateStart);
|
||||
expect($simulateStart)->not->toBeFalse();
|
||||
expect($simulateEnd)->not->toBeFalse();
|
||||
$simulateRoute = substr($studioRoute, $simulateStart, $simulateEnd - $simulateStart);
|
||||
expect($simulateRoute)->toContain("requireStudioUser('list_department_selfserve_config_versions')");
|
||||
expect($simulateRoute)->toContain("'list_department_selfserve_config_versions' => 'Run the self-serve studio simulator'");
|
||||
expect($simulateRoute)->not->toContain('list_department_selfserve_vehicle_conditions');
|
||||
|
||||
expect($studioGraph)->not->toBeFalse();
|
||||
$loadSimulationConfigStart = strpos($studioGraph, 'private function loadSimulationConfig');
|
||||
$projectPathOutcomesStart = strpos($studioGraph, 'public function projectPathOutcomes', $loadSimulationConfigStart);
|
||||
expect($loadSimulationConfigStart)->not->toBeFalse();
|
||||
expect($projectPathOutcomesStart)->not->toBeFalse();
|
||||
$loadSimulationConfig = substr($studioGraph, $loadSimulationConfigStart, $projectPathOutcomesStart - $loadSimulationConfigStart);
|
||||
expect($loadSimulationConfig)->toContain('selectLatestByDepartmentAndStatus');
|
||||
expect($loadSimulationConfig)->toContain('snapshotLegacyConfig($departmentId)');
|
||||
expect($loadSimulationConfig)->not->toContain('ensureDraftFromLegacy');
|
||||
expect($loadSimulationConfig)->toContain('Draft studio simulation requires list_department_selfserve_config_versions permission.');
|
||||
|
||||
expect($studioGraph)->toContain('department_selfserve_studio_layouts');
|
||||
expect($studioGraph)->toContain('department_selfserve_path_confirmations');
|
||||
expect($studioGraph)->toContain('buildGatewayWorkspace');
|
||||
|
||||
Reference in New Issue
Block a user