AppFactory.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\App;
  4. use App\Application\Admin\AllowlistController;
  5. use App\Application\Admin\ConsumersController;
  6. use App\Application\Admin\IpsController;
  7. use App\Application\Admin\ManualBlocksController;
  8. use App\Application\Admin\MeController;
  9. use App\Application\Admin\PoliciesController;
  10. use App\Application\Admin\ReportersController;
  11. use App\Application\Admin\StatsController;
  12. use App\Application\Admin\TokensController;
  13. use App\Application\Auth\AuthController;
  14. use App\Application\Internal\JobsController;
  15. use App\Application\Public\BlocklistController;
  16. use App\Application\Public\ReportController;
  17. use App\Domain\Auth\Role;
  18. use App\Infrastructure\Http\JsonErrorHandler;
  19. use App\Infrastructure\Http\Middleware\ImpersonationMiddleware;
  20. use App\Infrastructure\Http\Middleware\InternalNetworkMiddleware;
  21. use App\Infrastructure\Http\Middleware\InternalTokenMiddleware;
  22. use App\Infrastructure\Http\Middleware\RateLimitMiddleware;
  23. use App\Infrastructure\Http\Middleware\RbacMiddleware;
  24. use App\Infrastructure\Http\Middleware\TokenAuthenticationMiddleware;
  25. use Psr\Container\ContainerInterface;
  26. use Psr\Http\Message\ResponseFactoryInterface;
  27. use Psr\Http\Message\ResponseInterface;
  28. use Psr\Http\Message\ServerRequestInterface;
  29. use Psr\Log\LoggerInterface;
  30. use Slim\App;
  31. use Slim\Factory\AppFactory as SlimAppFactory;
  32. use Slim\Routing\RouteCollectorProxy;
  33. /**
  34. * Builds the configured Slim app.
  35. *
  36. * Slim middleware is LIFO. To get "TokenAuth → Impersonation → Rbac" we
  37. * add them in reverse on each route group.
  38. *
  39. * Route groups in M04:
  40. * - Public /api/v1/report TokenAuth → RateLimit → controller (kind check inside)
  41. * - Admin /api/v1/admin/{reporters,consumers,tokens} TokenAuth → Impersonation → Rbac(Admin)
  42. * - Admin /api/v1/admin/me TokenAuth → Impersonation → Rbac(Viewer)
  43. * - Auth /api/v1/auth/* TokenAuth (controller checks kind=service)
  44. */
  45. final class AppFactory
  46. {
  47. /**
  48. * @return App<ContainerInterface|null>
  49. */
  50. public static function build(ContainerInterface $container): App
  51. {
  52. SlimAppFactory::setContainer($container);
  53. $app = SlimAppFactory::create();
  54. $app->addRoutingMiddleware();
  55. $app->addBodyParsingMiddleware();
  56. /** @var array{app_env: string} $settings */
  57. $settings = $container->get('settings');
  58. $isDev = $settings['app_env'] === 'development';
  59. /** @var LoggerInterface $logger */
  60. $logger = $container->get(LoggerInterface::class);
  61. $errorMiddleware = $app->addErrorMiddleware($isDev, true, true, $logger);
  62. /** @var JsonErrorHandler $handler */
  63. $handler = $container->get(JsonErrorHandler::class);
  64. $errorMiddleware->setDefaultErrorHandler($handler);
  65. /** @var ResponseFactoryInterface $rf */
  66. $rf = $container->get(ResponseFactoryInterface::class);
  67. /** @var TokenAuthenticationMiddleware $tokenAuth */
  68. $tokenAuth = $container->get(TokenAuthenticationMiddleware::class);
  69. /** @var ImpersonationMiddleware $impersonation */
  70. $impersonation = $container->get(ImpersonationMiddleware::class);
  71. /** @var RateLimitMiddleware $rateLimit */
  72. $rateLimit = $container->get(RateLimitMiddleware::class);
  73. /** @var InternalNetworkMiddleware $internalNetwork */
  74. $internalNetwork = $container->get(InternalNetworkMiddleware::class);
  75. /** @var InternalTokenMiddleware $internalToken */
  76. $internalToken = $container->get(InternalTokenMiddleware::class);
  77. $app->get('/healthz', function (ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
  78. $response->getBody()->write((string) json_encode(['status' => 'ok']));
  79. return $response->withHeader('Content-Type', 'application/json');
  80. });
  81. // Auth API: service-token-only. No impersonation — these endpoints
  82. // exist to *produce* user_ids the ui can later impersonate. The
  83. // controller enforces kind=service on each call.
  84. $app->group('/api/v1/auth', function (RouteCollectorProxy $auth) use ($container): void {
  85. /** @var AuthController $controller */
  86. $controller = $container->get(AuthController::class);
  87. $auth->post('/users/upsert-oidc', [$controller, 'upsertOidc']);
  88. $auth->post('/users/upsert-local', [$controller, 'upsertLocal']);
  89. $auth->get('/users/{id}', function (
  90. ServerRequestInterface $request,
  91. ResponseInterface $response,
  92. array $args,
  93. ) use ($controller): ResponseInterface {
  94. /** @var array{id: string} $args */
  95. return $controller->getUser($request, $response, $args['id']);
  96. });
  97. })->add($tokenAuth);
  98. // Public API: ingest endpoint. Auth → rate limit → controller. The
  99. // controller rejects non-reporter kinds itself (uniform 401 per
  100. // SPEC).
  101. $app->group('/api/v1', function (RouteCollectorProxy $public) use ($container): void {
  102. /** @var ReportController $report */
  103. $report = $container->get(ReportController::class);
  104. $public->post('/report', $report);
  105. /** @var BlocklistController $blocklist */
  106. $blocklist = $container->get(BlocklistController::class);
  107. $public->get('/blocklist', $blocklist);
  108. })
  109. ->add($rateLimit)
  110. ->add($tokenAuth);
  111. // Admin API: token auth → impersonation → role check.
  112. $app->group('/api/v1/admin', function (RouteCollectorProxy $admin) use ($container, $rf): void {
  113. /** @var MeController $me */
  114. $me = $container->get(MeController::class);
  115. $admin->get('/me', $me)
  116. ->add(RbacMiddleware::require($rf, Role::Viewer));
  117. /** @var ReportersController $reporters */
  118. $reporters = $container->get(ReportersController::class);
  119. $admin->group('/reporters', function (RouteCollectorProxy $r) use ($reporters): void {
  120. $r->get('', [$reporters, 'list']);
  121. $r->post('', [$reporters, 'create']);
  122. $r->get('/{id}', [$reporters, 'show']);
  123. $r->patch('/{id}', [$reporters, 'update']);
  124. $r->delete('/{id}', [$reporters, 'delete']);
  125. })->add(RbacMiddleware::require($rf, Role::Admin));
  126. /** @var ConsumersController $consumers */
  127. $consumers = $container->get(ConsumersController::class);
  128. $admin->group('/consumers', function (RouteCollectorProxy $r) use ($consumers): void {
  129. $r->get('', [$consumers, 'list']);
  130. $r->post('', [$consumers, 'create']);
  131. $r->get('/{id}', [$consumers, 'show']);
  132. $r->patch('/{id}', [$consumers, 'update']);
  133. $r->delete('/{id}', [$consumers, 'delete']);
  134. })->add(RbacMiddleware::require($rf, Role::Admin));
  135. /** @var TokensController $tokens */
  136. $tokens = $container->get(TokensController::class);
  137. $admin->group('/tokens', function (RouteCollectorProxy $r) use ($tokens): void {
  138. $r->get('', [$tokens, 'list']);
  139. $r->post('', [$tokens, 'create']);
  140. $r->delete('/{id}', [$tokens, 'delete']);
  141. })->add(RbacMiddleware::require($rf, Role::Admin));
  142. // Manual blocks: list/show require Viewer, create/delete require Operator.
  143. // Per-route middleware lets us split read vs write at one URL group.
  144. /** @var ManualBlocksController $manualBlocks */
  145. $manualBlocks = $container->get(ManualBlocksController::class);
  146. $admin->get('/manual-blocks', [$manualBlocks, 'list'])
  147. ->add(RbacMiddleware::require($rf, Role::Viewer));
  148. $admin->get('/manual-blocks/{id}', [$manualBlocks, 'show'])
  149. ->add(RbacMiddleware::require($rf, Role::Viewer));
  150. $admin->post('/manual-blocks', [$manualBlocks, 'create'])
  151. ->add(RbacMiddleware::require($rf, Role::Operator));
  152. $admin->delete('/manual-blocks/{id}', [$manualBlocks, 'delete'])
  153. ->add(RbacMiddleware::require($rf, Role::Operator));
  154. /** @var AllowlistController $allowlist */
  155. $allowlist = $container->get(AllowlistController::class);
  156. $admin->get('/allowlist', [$allowlist, 'list'])
  157. ->add(RbacMiddleware::require($rf, Role::Viewer));
  158. $admin->get('/allowlist/{id}', [$allowlist, 'show'])
  159. ->add(RbacMiddleware::require($rf, Role::Viewer));
  160. $admin->post('/allowlist', [$allowlist, 'create'])
  161. ->add(RbacMiddleware::require($rf, Role::Operator));
  162. $admin->delete('/allowlist/{id}', [$allowlist, 'delete'])
  163. ->add(RbacMiddleware::require($rf, Role::Operator));
  164. // IPs: list, detail, stats — all Viewer (read-only this milestone).
  165. /** @var IpsController $ips */
  166. $ips = $container->get(IpsController::class);
  167. $admin->get('/ips', [$ips, 'list'])
  168. ->add(RbacMiddleware::require($rf, Role::Viewer));
  169. $admin->get('/ips/{ip:.+}', [$ips, 'show'])
  170. ->add(RbacMiddleware::require($rf, Role::Viewer));
  171. /** @var StatsController $stats */
  172. $stats = $container->get(StatsController::class);
  173. $admin->get('/stats/dashboard', [$stats, 'dashboard'])
  174. ->add(RbacMiddleware::require($rf, Role::Viewer));
  175. // Policies: list/show/preview = Viewer; write = Admin.
  176. /** @var PoliciesController $policies */
  177. $policies = $container->get(PoliciesController::class);
  178. $admin->get('/policies', [$policies, 'list'])
  179. ->add(RbacMiddleware::require($rf, Role::Viewer));
  180. $admin->get('/policies/{id}', [$policies, 'show'])
  181. ->add(RbacMiddleware::require($rf, Role::Viewer));
  182. $admin->get('/policies/{id}/preview', [$policies, 'preview'])
  183. ->add(RbacMiddleware::require($rf, Role::Viewer));
  184. $admin->post('/policies', [$policies, 'create'])
  185. ->add(RbacMiddleware::require($rf, Role::Admin));
  186. $admin->patch('/policies/{id}', [$policies, 'update'])
  187. ->add(RbacMiddleware::require($rf, Role::Admin));
  188. $admin->delete('/policies/{id}', [$policies, 'delete'])
  189. ->add(RbacMiddleware::require($rf, Role::Admin));
  190. })
  191. ->add($impersonation)
  192. ->add($tokenAuth);
  193. // Internal jobs API: scheduler-only. Network gate (404 outside
  194. // RFC1918) → token gate (401) → controller. Order matters:
  195. // network rejection must not leak through token-attempt logs.
  196. $app->group('/internal/jobs', function (RouteCollectorProxy $internal) use ($container): void {
  197. /** @var JobsController $jobs */
  198. $jobs = $container->get(JobsController::class);
  199. $internal->post('/recompute-scores', [$jobs, 'recomputeScores']);
  200. $internal->post('/cleanup-audit', [$jobs, 'cleanupAudit']);
  201. $internal->post('/enrich-pending', [$jobs, 'enrichPending']);
  202. $internal->post('/tick', [$jobs, 'tick']);
  203. $internal->post('/refresh-geoip', [$jobs, 'refreshGeoip']);
  204. $internal->get('/status', [$jobs, 'status']);
  205. })
  206. ->add($internalToken)
  207. ->add($internalNetwork);
  208. $app->map(
  209. ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  210. '/{routes:.+}',
  211. function (ServerRequestInterface $request, ResponseInterface $response): ResponseInterface {
  212. $response->getBody()->write((string) json_encode(['error' => 'not_found']));
  213. return $response
  214. ->withHeader('Content-Type', 'application/json')
  215. ->withStatus(404);
  216. }
  217. );
  218. return $app;
  219. }
  220. }