From 74ebefcf65bb4194a9e141c9bd13464acf8683e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 12 Aug 2026 10:34:41 +0200 Subject: [PATCH] feat: proper logging to dedicated audit channel --- config/packages/monolog.yaml | 36 ++++++++++++++++++++---------- config/services.yaml | 5 ++++- src/Security/MyEpAuthenticator.php | 13 +++++++++++ src/Security/OAuth2/MyEpClient.php | 10 +++++++-- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 5fbad28..75b5a93 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -2,7 +2,7 @@ monolog: channels: - deprecation - bpn - - myep + - audit handlers: main: @@ -11,7 +11,7 @@ monolog: handler: exceptions excluded_http_codes: [404, 405] buffer_size: 50 - channels: ["!bpn", "!myep"] + channels: ["!bpn", "!audit"] exceptions: type: rotating_file path: "%kernel.logs_dir%/framework.%kernel.environment%.log" @@ -23,14 +23,14 @@ monolog: path: '%kernel.logs_dir%/bpn.%kernel.environment%.log' max_files: 5 level: info - myep: - channels: [ "myep" ] + audit: + channels: [ "audit" ] type: rotating_file - path: '%kernel.logs_dir%/myep.%kernel.environment%.log' + path: '%kernel.logs_dir%/audit.%kernel.environment%.log' max_files: 5 level: info database: - channels: ["myep"] + channels: ["audit"] type: service id: App\Logging\DatabaseHandler console: @@ -38,6 +38,18 @@ monolog: process_psr_3_messages: false channels: ["!event", "!bpn", "!doctrine", "!console"] +when@dev: + monolog: + handlers: + # Single catch-all file so everything is visible in one place during + # development. The handlers above are error-level or channel-bound, + # which makes ordinary app output invisible locally. + dev: + type: stream + path: '%kernel.logs_dir%/%kernel.environment%.log' + level: debug + channels: ["!event", "!doctrine"] + when@test: monolog: handlers: @@ -61,7 +73,7 @@ when@prod: handler: exceptions excluded_http_codes: [404, 405] buffer_size: 50 - channels: ["!bpn", "!myep"] + channels: ["!bpn", "!audit"] exceptions: type: rotating_file path: "%kernel.logs_dir%/framework.%kernel.environment%.log" @@ -73,17 +85,17 @@ when@prod: path: '%kernel.logs_dir%/bpn.%kernel.environment%.log' max_files: 5 level: info - myep: - channels: [ "myep" ] + audit: + channels: [ "audit" ] type: rotating_file - path: '%kernel.logs_dir%/myep.%kernel.environment%.log' + path: '%kernel.logs_dir%/audit.%kernel.environment%.log' max_files: 5 level: info database: - channels: ["myep"] + channels: ["audit"] type: service id: App\Logging\DatabaseHandler console: type: console process_psr_3_messages: false - channels: ["!event", "!doctrine", "!bpn", "!myep"] + channels: ["!event", "!doctrine", "!bpn", "!audit"] diff --git a/config/services.yaml b/config/services.yaml index 2368909..7c033dc 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -43,7 +43,10 @@ services: autoconfigure: true bind: $tempDir: '%kernel.project_dir%/temp' - $logger: '@monolog.logger.myep' + # Application audit channel, not a MyE&P one: this is what feeds the + # log table via App\Logging\DatabaseHandler. Unbinding a service from + # it drops its records from that table. + $logger: '@monolog.logger.audit' $houses: '%houses%' $xmlExport: '@xml_export.storage' $xmlDump: '@xml_dump.storage' diff --git a/src/Security/MyEpAuthenticator.php b/src/Security/MyEpAuthenticator.php index 2349333..33406ba 100644 --- a/src/Security/MyEpAuthenticator.php +++ b/src/Security/MyEpAuthenticator.php @@ -49,6 +49,9 @@ class MyEpAuthenticator extends AbstractAuthenticator try { $accessToken = $this->client->fetchAccessToken($request); } catch (AuthorizationRequestException|IdentityProviderException $e) { + $this->logger->error('Login via MyE&P failed due to unobtainable access token', [ + 'exception' => $e, + ]); throw new CustomUserMessageAuthenticationException('Invalid token'); } @@ -110,11 +113,21 @@ class MyEpAuthenticator extends AbstractAuthenticator { // User is expected to have at least one role if (false === isset($userinfo['roles']) || 0 === count($userinfo['roles'])) { + // Claim keys only, the payload itself carries the full profile + $this->logger->warning('Login via MyE&P failed due to missing roles claim', [ + 'claims' => array_keys($userinfo), + ]); + return null; } // User is expected to have at least one of the roles teamer, manager, house manager or admin if ([] === array_intersect(self::ELIGIBLE_ROLES, $userinfo['roles'])) { + $this->logger->warning('Login via MyE&P failed due to lack of an eligible role', [ + 'roles' => $userinfo['roles'], + 'eligible_roles' => self::ELIGIBLE_ROLES, + ]); + return null; } diff --git a/src/Security/OAuth2/MyEpClient.php b/src/Security/OAuth2/MyEpClient.php index c904015..4b60911 100644 --- a/src/Security/OAuth2/MyEpClient.php +++ b/src/Security/OAuth2/MyEpClient.php @@ -6,6 +6,7 @@ use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Provider\GenericProvider; use League\OAuth2\Client\Token\AccessTokenInterface; +use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -14,8 +15,11 @@ class MyEpClient { private array $config; - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, array $options) - { + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + private readonly LoggerInterface $logger, + array $options, + ) { $this->config = $this->resolveConfig($options); } @@ -26,6 +30,7 @@ class MyEpClient public function fetchAccessToken(Request $request): AccessTokenInterface { if (null === $code = $request->query->get('code')) { + $this->logger->error('OAuth2 login request missing code'); throw new AuthorizationRequestException('Missing code', 400, $request); } @@ -36,6 +41,7 @@ class MyEpClient || $request->query->get('state') !== $session->get('oauth2state') ) { $session->remove('oauth2state'); + $this->logger->error('OAuth2 login request missing state or mismatch'); throw new AuthorizationRequestException('Missing state or mismatch', 400, $request); }