diff --git a/core/components/minishop3/config/routes/manager.php b/core/components/minishop3/config/routes/manager.php index 71aa8978..fe98d79d 100644 --- a/core/components/minishop3/config/routes/manager.php +++ b/core/components/minishop3/config/routes/manager.php @@ -49,6 +49,7 @@ ]); })->middleware(new AuthMiddleware($modx, 'mgr')); + // Config reads: any authenticated mgr (product forms load page-fields without settings perm) $router->group('/config', function($router) use ($modx) { $router->get('/page-fields/{page_key}', function($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); @@ -58,6 +59,14 @@ $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); return $controller->getAllPageFields($params); }); + $router->get('/sections/{page_key}', function($params) use ($modx) { + $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); + return $controller->getSections($params); + }); + }); + + // Config writes: mssetting_save — same gate as model-fields / extra-fields writes (#381) + $router->group('/config', function($router) use ($modx) { $router->put('/page-fields/{page_key}', function($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); return $controller->updatePageFields($params); @@ -66,10 +75,6 @@ $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); return $controller->deleteFieldOverride($params); }); - $router->get('/sections/{page_key}', function($params) use ($modx) { - $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); - return $controller->getSections($params); - }); $router->put('/sections/{page_key}', function($params) use ($modx) { $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); return $controller->updateSections($params); @@ -78,8 +83,9 @@ $controller = new \MiniShop3\Controllers\Api\ConfigController($modx); return $controller->deleteSection($params); }); - - }); + }, [ + new PermissionMiddleware($modx, 'mssetting_save') + ]); $router->group('/models', function($router) use ($modx) { $router->get('/{alias}/fields', function($params) use ($modx) { diff --git a/core/components/minishop3/tests/ConfigRoutePermissionsTest.php b/core/components/minishop3/tests/ConfigRoutePermissionsTest.php new file mode 100644 index 00000000..90fec8d4 --- /dev/null +++ b/core/components/minishop3/tests/ConfigRoutePermissionsTest.php @@ -0,0 +1,113 @@ +getValue($middleware); + $found = $permission; + } + + return $found; +}; + +$_SERVER['HTTP_MODAUTH'] = 'test-modauth-token'; + +$modx = new modX(); +$router = new Router($modx); +$router->loadRoutes(dirname(__DIR__) . '/config/routes/manager.php'); +$router->build(); + +$routesProperty = new ReflectionProperty(Router::class, 'routes'); +$registered = $routesProperty->getValue($router); + +$expected = [ + 'GET /api/mgr/config/page-fields/{page_key}' => null, + 'GET /api/mgr/config/page-fields/{page_key}/all' => null, + 'GET /api/mgr/config/sections/{page_key}' => null, + 'PUT /api/mgr/config/page-fields/{page_key}' => 'mssetting_save', + 'DELETE /api/mgr/config/page-fields/{page_key}/{field_name}' => 'mssetting_save', + 'PUT /api/mgr/config/sections/{page_key}' => 'mssetting_save', + 'DELETE /api/mgr/config/sections/{page_key}/{section_key}' => 'mssetting_save', +]; + +$actual = []; +foreach ($registered as $route) { + $pattern = $route['pattern'] ?? ''; + if (!str_starts_with($pattern, '/api/mgr/config')) { + continue; + } + $method = is_array($route['method'] ?? null) + ? implode('|', $route['method']) + : (string) ($route['method'] ?? ''); + $key = strtoupper($method) . ' ' . $pattern; + $actual[$key] = $permissionFromMiddlewares($route['middlewares'] ?? []); +} + +foreach ($expected as $routeKey => $permission) { + $assertSame($permission, $actual[$routeKey] ?? null, "route {$routeKey}"); +} + +$unknown = array_diff(array_keys($actual), array_keys($expected)); +if ($unknown !== []) { + $fail('unexpected config routes: ' . implode(', ', $unknown)); +} + +$assertDenied = static function ( + Router $router, + string $method, + string $uri, + string $label +) use ($assertSame, $fail): void { + $response = $router->dispatch($uri, $method); + $data = $response->getData(); + $assertSame(403, $response->getStatusCode(), "{$label}: status"); + $assertSame(false, $data['success'] ?? null, "{$label}: success"); + if (!str_contains((string) ($data['message'] ?? ''), 'mssetting_save')) { + $fail("{$label}: message should mention mssetting_save"); + } +}; + +// Runtime: no mssetting_save → write 403; GET still reaches handler (not 403 from permission) +$modx->setPermissions([]); +$assertDenied($router, 'PUT', '/api/mgr/config/page-fields/order', 'PUT page-fields'); +$assertDenied($router, 'DELETE', '/api/mgr/config/sections/order/main', 'DELETE section'); +$assertDenied($router, 'PUT', '/api/mgr/config/sections/order', 'PUT sections'); +$assertDenied($router, 'DELETE', '/api/mgr/config/page-fields/order/title', 'DELETE field override'); + +fwrite(STDOUT, "OK ConfigRoutePermissionsTest\n"); +exit(0); diff --git a/core/components/minishop3/tests/stubs/ModxStub.php b/core/components/minishop3/tests/stubs/ModxStub.php new file mode 100644 index 00000000..04b54029 --- /dev/null +++ b/core/components/minishop3/tests/stubs/ModxStub.php @@ -0,0 +1,59 @@ + */ + private array $permissions = []; + + public function __construct() + { + $this->context = new class { + public function get(string $key): string + { + return $key === 'key' ? 'mgr' : ''; + } + }; + + $this->user = new class { + public function isAuthenticated(string $context): bool + { + return $context === 'mgr'; + } + + public function getUserToken(string $contextKey): string + { + return 'test-modauth-token'; + } + }; + } + + /** + * @param list $permissions + */ + public function setPermissions(array $permissions): void + { + $this->permissions = array_fill_keys($permissions, true); + } + + public function hasPermission(string $permission): bool + { + return !empty($this->permissions[$permission]); + } + + public function log($level, $message): void + { + } +}