feat: proper logging to dedicated audit channel

This commit is contained in:
Björn Fromme
2026-08-12 10:34:41 +02:00
parent 898a7c7505
commit 74ebefcf65
4 changed files with 49 additions and 15 deletions
+24 -12
View File
@@ -2,7 +2,7 @@ monolog:
channels: channels:
- deprecation - deprecation
- bpn - bpn
- myep - audit
handlers: handlers:
main: main:
@@ -11,7 +11,7 @@ monolog:
handler: exceptions handler: exceptions
excluded_http_codes: [404, 405] excluded_http_codes: [404, 405]
buffer_size: 50 buffer_size: 50
channels: ["!bpn", "!myep"] channels: ["!bpn", "!audit"]
exceptions: exceptions:
type: rotating_file type: rotating_file
path: "%kernel.logs_dir%/framework.%kernel.environment%.log" path: "%kernel.logs_dir%/framework.%kernel.environment%.log"
@@ -23,14 +23,14 @@ monolog:
path: '%kernel.logs_dir%/bpn.%kernel.environment%.log' path: '%kernel.logs_dir%/bpn.%kernel.environment%.log'
max_files: 5 max_files: 5
level: info level: info
myep: audit:
channels: [ "myep" ] channels: [ "audit" ]
type: rotating_file type: rotating_file
path: '%kernel.logs_dir%/myep.%kernel.environment%.log' path: '%kernel.logs_dir%/audit.%kernel.environment%.log'
max_files: 5 max_files: 5
level: info level: info
database: database:
channels: ["myep"] channels: ["audit"]
type: service type: service
id: App\Logging\DatabaseHandler id: App\Logging\DatabaseHandler
console: console:
@@ -38,6 +38,18 @@ monolog:
process_psr_3_messages: false process_psr_3_messages: false
channels: ["!event", "!bpn", "!doctrine", "!console"] 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: when@test:
monolog: monolog:
handlers: handlers:
@@ -61,7 +73,7 @@ when@prod:
handler: exceptions handler: exceptions
excluded_http_codes: [404, 405] excluded_http_codes: [404, 405]
buffer_size: 50 buffer_size: 50
channels: ["!bpn", "!myep"] channels: ["!bpn", "!audit"]
exceptions: exceptions:
type: rotating_file type: rotating_file
path: "%kernel.logs_dir%/framework.%kernel.environment%.log" path: "%kernel.logs_dir%/framework.%kernel.environment%.log"
@@ -73,17 +85,17 @@ when@prod:
path: '%kernel.logs_dir%/bpn.%kernel.environment%.log' path: '%kernel.logs_dir%/bpn.%kernel.environment%.log'
max_files: 5 max_files: 5
level: info level: info
myep: audit:
channels: [ "myep" ] channels: [ "audit" ]
type: rotating_file type: rotating_file
path: '%kernel.logs_dir%/myep.%kernel.environment%.log' path: '%kernel.logs_dir%/audit.%kernel.environment%.log'
max_files: 5 max_files: 5
level: info level: info
database: database:
channels: ["myep"] channels: ["audit"]
type: service type: service
id: App\Logging\DatabaseHandler id: App\Logging\DatabaseHandler
console: console:
type: console type: console
process_psr_3_messages: false process_psr_3_messages: false
channels: ["!event", "!doctrine", "!bpn", "!myep"] channels: ["!event", "!doctrine", "!bpn", "!audit"]
+4 -1
View File
@@ -43,7 +43,10 @@ services:
autoconfigure: true autoconfigure: true
bind: bind:
$tempDir: '%kernel.project_dir%/temp' $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%' $houses: '%houses%'
$xmlExport: '@xml_export.storage' $xmlExport: '@xml_export.storage'
$xmlDump: '@xml_dump.storage' $xmlDump: '@xml_dump.storage'
+13
View File
@@ -49,6 +49,9 @@ class MyEpAuthenticator extends AbstractAuthenticator
try { try {
$accessToken = $this->client->fetchAccessToken($request); $accessToken = $this->client->fetchAccessToken($request);
} catch (AuthorizationRequestException|IdentityProviderException $e) { } catch (AuthorizationRequestException|IdentityProviderException $e) {
$this->logger->error('Login via MyE&P failed due to unobtainable access token', [
'exception' => $e,
]);
throw new CustomUserMessageAuthenticationException('Invalid token'); throw new CustomUserMessageAuthenticationException('Invalid token');
} }
@@ -110,11 +113,21 @@ class MyEpAuthenticator extends AbstractAuthenticator
{ {
// User is expected to have at least one role // User is expected to have at least one role
if (false === isset($userinfo['roles']) || 0 === count($userinfo['roles'])) { 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; return null;
} }
// User is expected to have at least one of the roles teamer, manager, house manager or admin // 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'])) { 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; return null;
} }
+8 -2
View File
@@ -6,6 +6,7 @@ use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException; use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\GenericProvider; use League\OAuth2\Client\Provider\GenericProvider;
use League\OAuth2\Client\Token\AccessTokenInterface; use League\OAuth2\Client\Token\AccessTokenInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -14,8 +15,11 @@ class MyEpClient
{ {
private array $config; 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); $this->config = $this->resolveConfig($options);
} }
@@ -26,6 +30,7 @@ class MyEpClient
public function fetchAccessToken(Request $request): AccessTokenInterface public function fetchAccessToken(Request $request): AccessTokenInterface
{ {
if (null === $code = $request->query->get('code')) { if (null === $code = $request->query->get('code')) {
$this->logger->error('OAuth2 login request missing code');
throw new AuthorizationRequestException('Missing code', 400, $request); throw new AuthorizationRequestException('Missing code', 400, $request);
} }
@@ -36,6 +41,7 @@ class MyEpClient
|| $request->query->get('state') !== $session->get('oauth2state') || $request->query->get('state') !== $session->get('oauth2state')
) { ) {
$session->remove('oauth2state'); $session->remove('oauth2state');
$this->logger->error('OAuth2 login request missing state or mismatch');
throw new AuthorizationRequestException('Missing state or mismatch', 400, $request); throw new AuthorizationRequestException('Missing state or mismatch', 400, $request);
} }