feat: proper logging to dedicated audit channel
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user