ConfigController.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Application\Admin;
  4. use Psr\Http\Message\ResponseInterface;
  5. use Psr\Http\Message\ServerRequestInterface;
  6. /**
  7. * `GET /api/v1/admin/config` — effective config the api is using,
  8. * with secrets masked.
  9. *
  10. * Masking rules per SPEC §M12.5:
  11. * - `***` for: INTERNAL_JOB_TOKEN, MAXMIND_LICENSE_KEY,
  12. * IPINFO_TOKEN, DB_MYSQL_PASSWORD, APP_SECRET
  13. * - first 8 + `...` for: UI_SERVICE_TOKEN
  14. * - plain values for everything else
  15. *
  16. * Returns config grouped by section so the UI can render it without
  17. * inventing categorisation. RBAC: Admin only — viewers and operators
  18. * see no_access.
  19. */
  20. final class ConfigController
  21. {
  22. use AdminControllerSupport;
  23. /**
  24. * @param array<string, mixed> $settings Effective settings array (the same array that built the container).
  25. */
  26. public function __construct(private readonly array $settings)
  27. {
  28. }
  29. public function show(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
  30. {
  31. return self::json($response, 200, [
  32. 'sections' => $this->sections(),
  33. ]);
  34. }
  35. /**
  36. * @return array<string, array<string, mixed>>
  37. */
  38. private function sections(): array
  39. {
  40. $db = $this->settings['db'] ?? [];
  41. $geoip = $this->settings['geoip'] ?? [];
  42. return [
  43. 'app' => [
  44. 'APP_ENV' => $this->settings['app_env'] ?? null,
  45. 'LOG_LEVEL' => $this->levelName(),
  46. 'APP_SECRET' => self::mask((string) ($this->settings['app_secret'] ?? '')),
  47. 'UI_ORIGIN' => $this->settings['ui_origin'] ?? null,
  48. ],
  49. 'database' => [
  50. 'DB_DRIVER' => $db['driver'] ?? null,
  51. 'DB_SQLITE_PATH' => $db['sqlite_path'] ?? null,
  52. 'DB_MYSQL_HOST' => $db['mysql_host'] ?? null,
  53. 'DB_MYSQL_PORT' => $db['mysql_port'] ?? null,
  54. 'DB_MYSQL_DATABASE' => $db['mysql_database'] ?? null,
  55. 'DB_MYSQL_USERNAME' => $db['mysql_username'] ?? null,
  56. 'DB_MYSQL_PASSWORD' => self::mask((string) ($db['mysql_password'] ?? '')),
  57. ],
  58. 'auth' => [
  59. 'UI_SERVICE_TOKEN' => self::previewToken((string) ($this->settings['ui_service_token'] ?? '')),
  60. 'INTERNAL_JOB_TOKEN' => self::mask((string) ($this->settings['internal_job_token'] ?? '')),
  61. 'OIDC_DEFAULT_ROLE' => $this->oidcDefaultRoleName(),
  62. ],
  63. 'reputation' => [
  64. 'SCORE_REPORT_HARD_CUTOFF_DAYS' => $this->settings['score_hard_cutoff_days'] ?? null,
  65. 'SCORE_RECOMPUTE_INTERVAL_SECONDS' => $this->settings['score_recompute_interval_seconds'] ?? null,
  66. 'API_RATE_LIMIT_PER_SECOND' => $this->settings['rate_limit_per_second'] ?? null,
  67. 'CIDR_EVALUATOR_TTL_SECONDS' => $this->settings['cidr_evaluator_ttl_seconds'] ?? null,
  68. 'BLOCKLIST_CACHE_TTL_SECONDS' => $this->settings['blocklist_cache_ttl_seconds'] ?? null,
  69. ],
  70. 'jobs' => [
  71. 'JOB_RECOMPUTE_MAX_RUNTIME_SECONDS' => $this->settings['job_recompute_max_runtime_seconds'] ?? null,
  72. 'JOB_RECOMPUTE_MAX_ROWS_PER_TICK' => $this->settings['job_recompute_max_rows_per_tick'] ?? null,
  73. 'JOB_AUDIT_RETENTION_DAYS' => $this->settings['job_audit_retention_days'] ?? null,
  74. 'JOB_GEOIP_REFRESH_INTERVAL_DAYS' => $geoip['refresh_interval_days'] ?? null,
  75. ],
  76. 'geoip' => [
  77. 'GEOIP_ENABLED' => $geoip['enabled'] ?? null,
  78. 'GEOIP_PROVIDER' => $geoip['provider'] ?? null,
  79. 'GEOIP_COUNTRY_DB' => $geoip['country_db'] ?? null,
  80. 'GEOIP_ASN_DB' => $geoip['asn_db'] ?? null,
  81. 'MAXMIND_LICENSE_KEY' => self::mask((string) ($geoip['maxmind_license_key'] ?? '')),
  82. 'IPINFO_TOKEN' => self::mask((string) ($geoip['ipinfo_token'] ?? '')),
  83. ],
  84. ];
  85. }
  86. private static function mask(string $value): string
  87. {
  88. return $value === '' ? '' : '***';
  89. }
  90. /**
  91. * Token preview: empty stays empty so misconfiguration is visible;
  92. * present values show first 8 + ellipsis.
  93. */
  94. private static function previewToken(string $value): string
  95. {
  96. if ($value === '') {
  97. return '';
  98. }
  99. return substr($value, 0, 8) . '...';
  100. }
  101. private function levelName(): ?string
  102. {
  103. $level = $this->settings['log_level'] ?? null;
  104. if ($level instanceof \Monolog\Level) {
  105. return $level->getName();
  106. }
  107. return is_string($level) ? $level : null;
  108. }
  109. private function oidcDefaultRoleName(): ?string
  110. {
  111. $role = $this->settings['oidc_default_role'] ?? null;
  112. if ($role === null) {
  113. return 'none';
  114. }
  115. if ($role instanceof \App\Domain\Auth\Role) {
  116. return $role->value;
  117. }
  118. return is_string($role) ? $role : null;
  119. }
  120. }