diff --git a/composer.json b/composer.json index f54c7e207eb445ac4c688c257c5e767958605d9c..26db40563e1610572b5371216720570ba80011ff 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "require": { "php": ">=7.3", "ext-fileinfo": "*", + "ext-intl": "*", "ext-json": "*", "api-platform/core": "^2.6.8 <2.7.0", "doctrine/annotations": "^1.13", @@ -22,6 +23,7 @@ "symfony/lock": "^5.4", "symfony/messenger": "^5.4", "symfony/mime": "^5.4", + "symfony/monolog-bundle": "^3.8", "symfony/process": "^5.4", "symfony/security-bundle": "^5.4", "symfony/security-core": "^5.4", @@ -29,8 +31,7 @@ "symfony/twig-bundle": "^5.4", "symfony/uid": "^5.4", "symfony/validator": "^5.4", - "symfony/yaml": "^5.4", - "ext-intl": "*" + "symfony/yaml": "^5.4" }, "require-dev": { "brainmaestro/composer-git-hooks": "^2.8.5", diff --git a/composer.lock b/composer.lock index 6428f0182e186fea5a25b80429b0522f11633918..31ef39348b525c7a2b50fe5e5cfb0e4d367700aa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6ddd4889d724c8beabfe6b3b58bf35d7", + "content-hash": "e544dddb07b8f681111a5f8765b36d29", "packages": [ { "name": "api-platform/core", @@ -992,6 +992,108 @@ }, "time": "2022-10-02T13:13:18+00:00" }, + { + "name": "monolog/monolog", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f259e2b15fb95494c83f52d3caad003bbf5ffaa1", + "reference": "f259e2b15fb95494c83f52d3caad003bbf5ffaa1", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^0.12.91", + "phpunit/phpunit": "^8.5.14", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.9.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2023-02-06T13:44:46+00:00" + }, { "name": "nelmio/cors-bundle", "version": "2.3.1", @@ -3311,6 +3413,171 @@ ], "time": "2023-01-09T05:43:46+00:00" }, + { + "name": "symfony/monolog-bridge", + "version": "v5.4.19", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bridge.git", + "reference": "6b2732feb1335d588a902ca744cae9812cc0e7d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/6b2732feb1335d588a902ca744cae9812cc0e7d3", + "reference": "6b2732feb1335d588a902ca744cae9812cc0e7d3", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.25.1|^2", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/http-kernel": "^5.3|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3" + }, + "conflict": { + "symfony/console": "<4.4", + "symfony/http-foundation": "<5.3" + }, + "require-dev": { + "symfony/console": "^4.4|^5.0|^6.0", + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/mailer": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/security-core": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" + }, + "suggest": { + "symfony/console": "For the possibility to show log messages in console commands depending on verbosity settings.", + "symfony/http-kernel": "For using the debugging handlers together with the response life cycle of the HTTP kernel.", + "symfony/var-dumper": "For using the debugging handlers like the console handler or the log server handler." + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\Monolog\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides integration for Monolog with various Symfony components", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/monolog-bridge/tree/v5.4.19" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-01-01T08:32:19+00:00" + }, + { + "name": "symfony/monolog-bundle", + "version": "v3.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/monolog-bundle.git", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "reference": "a41bbcdc1105603b6d73a7d9a43a3788f8e0fb7d", + "shasum": "" + }, + "require": { + "monolog/monolog": "^1.22 || ^2.0 || ^3.0", + "php": ">=7.1.3", + "symfony/config": "~4.4 || ^5.0 || ^6.0", + "symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0", + "symfony/http-kernel": "~4.4 || ^5.0 || ^6.0", + "symfony/monolog-bridge": "~4.4 || ^5.0 || ^6.0" + }, + "require-dev": { + "symfony/console": "~4.4 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.2 || ^6.0", + "symfony/yaml": "~4.4 || ^5.0 || ^6.0" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bundle\\MonologBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony MonologBundle", + "homepage": "https://symfony.com", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/symfony/monolog-bundle/issues", + "source": "https://github.com/symfony/monolog-bundle/tree/v3.8.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-10T14:24:36+00:00" + }, { "name": "symfony/password-hasher", "version": "v5.4.19", @@ -9892,8 +10159,8 @@ "platform": { "php": ">=7.3", "ext-fileinfo": "*", - "ext-json": "*", - "ext-intl": "*" + "ext-intl": "*", + "ext-json": "*" }, "platform-dev": [], "platform-overrides": { diff --git a/src/DependencyInjection/DbpRelayCoreExtension.php b/src/DependencyInjection/DbpRelayCoreExtension.php index 10ad237b8ae8c7ddca0578aa63ecb42639b212a9..d4217808b9dadac49e8775cc69d30aa61e48eead 100644 --- a/src/DependencyInjection/DbpRelayCoreExtension.php +++ b/src/DependencyInjection/DbpRelayCoreExtension.php @@ -7,6 +7,7 @@ namespace Dbp\Relay\CoreBundle\DependencyInjection; use Dbp\Relay\CoreBundle\Auth\ProxyAuthenticator; use Dbp\Relay\CoreBundle\Cron\CronManager; use Dbp\Relay\CoreBundle\DB\MigrateCommand; +use Dbp\Relay\CoreBundle\Logging\LoggingProcessor; use Dbp\Relay\CoreBundle\Queue\TestMessage; use Dbp\Relay\CoreBundle\Queue\Utils as QueueUtils; use Symfony\Component\Cache\Adapter\FilesystemAdapter; @@ -46,11 +47,35 @@ class DbpRelayCoreExtension extends ConfigurableExtension implements PrependExte $entityManagers = $container->getParameter('dbp_api.entity_managers'); } $definition->addMethodCall('setEntityManagers', [$entityManagers]); + + $definition = $container->getDefinition(LoggingProcessor::class); + $definition->addMethodCall('setMaskConfig', [self::getLoggingChannels($container)]); + } + + /** + * Gives a mapping of all register logging channels. + * + * @return array<string,bool> + */ + private static function getLoggingChannels(ContainerBuilder $container): array + { + $channels = []; + if ($container->hasParameter('dbp_api.logging_channels')) { + $data = $container->getParameter('dbp_api.logging_channels'); + + foreach ($data as $entry) { + $name = $entry[0]; + $mask = $entry[1]; + $channels[$name] = $mask; + } + } + + return $channels; } public function prepend(ContainerBuilder $container) { - foreach (['api_platform', 'nelmio_cors', 'twig', 'security', 'framework'] as $extKey) { + foreach (['api_platform', 'nelmio_cors', 'twig', 'security', 'framework', 'monolog'] as $extKey) { if (!$container->hasExtension($extKey)) { throw new \Exception("'".$this->getAlias()."' requires the '$extKey' bundle to be loaded"); } @@ -187,6 +212,11 @@ class DbpRelayCoreExtension extends ConfigurableExtension implements PrependExte ]), ]); + // Register extra bundle logging channels + $container->loadFromExtension('monolog', [ + 'channels' => array_keys(self::getLoggingChannels($container)), + ]); + $routing = [ TestMessage::class => QueueUtils::QUEUE_TRANSPORT_NAME, ]; diff --git a/src/Extension/ExtensionTrait.php b/src/Extension/ExtensionTrait.php index ab65fba0115923f1060d78b0769abc7dc1df07e6..4288f1375f6bf220b70351e9e62c4729da725882 100644 --- a/src/Extension/ExtensionTrait.php +++ b/src/Extension/ExtensionTrait.php @@ -110,4 +110,17 @@ trait ExtensionTrait { $this->addQueueMessageClass($container, $messageClass); } + + /** + * Registers a new channel with monolog. + * + * @param $mask - If false potential secrets and PII won't be masked from the logs. For example for audit logs. + */ + public function registerLoggingChannel(ContainerBuilder $container, string $channelName, bool $mask = true): void + { + $this->ensureInPrepend($container); + $this->extendArrayParameter( + $container, 'dbp_api.logging_channels', [[$channelName, $mask]] + ); + } } diff --git a/src/Logging/LoggingProcessor.php b/src/Logging/LoggingProcessor.php index a2268da8302d2a97d99cfef2961eb2a0d49279df..0b8eeb21de737c7b4fef1deec478665b7871990c 100644 --- a/src/Logging/LoggingProcessor.php +++ b/src/Logging/LoggingProcessor.php @@ -14,12 +14,25 @@ final class LoggingProcessor private $userDataProvider; private $requestStack; + /** + * @var array<string,bool> + */ + private $maskConfig; + public function __construct(UserSessionInterface $userDataProvider, RequestStack $requestStack) { $this->userDataProvider = $userDataProvider; $this->requestStack = $requestStack; } + /** + * @param array<string,bool> $maskConfig + */ + public function setMaskConfig(array $maskConfig): void + { + $this->maskConfig = $maskConfig; + } + private function maskUserId(array &$record) { try { @@ -36,11 +49,13 @@ final class LoggingProcessor public function __invoke(array $record) { - // Try to avoid information leaks (users should still not log sensitive information though...) - $record['message'] = CoreTools::filterErrorMessage($record['message']); + if ($this->maskConfig[$record['channel']] ?? true) { + // Try to avoid information leaks (users should still not log sensitive information though...) + $record['message'] = CoreTools::filterErrorMessage($record['message']); - // Mask the user identifier - $this->maskUserId($record); + // Mask the user identifier + $this->maskUserId($record); + } // Add a session ID (the same during multiple requests for the same user session) $record['context']['relay-session-id'] = $this->userDataProvider->getSessionLoggingId(); diff --git a/tests/ExtensionTraitTest.php b/tests/ExtensionTraitTest.php index dd41ceaa81e6b29ff348e7eb1adc9995071e9c90..36401138042456368c86b550960b65481738948b 100644 --- a/tests/ExtensionTraitTest.php +++ b/tests/ExtensionTraitTest.php @@ -30,6 +30,8 @@ class ExtensionTraitTest extends TestCase $this->assertTrue($params->has('dbp_api.allow_headers')); $this->registerEntityManager($builder, 'some_entity_manager'); $this->assertTrue($params->has('dbp_api.entity_managers')); + $this->registerLoggingChannel($builder, 'mychannel', false); + $this->assertTrue($params->has('dbp_api.logging_channels')); } public function testCalledTooLate() diff --git a/tests/Kernel.php b/tests/Kernel.php index 2d515e97207be803ab1dc0ee0ade72e13c3fce65..5f944ad399c0389099cd9ce494150149c3f611dc 100644 --- a/tests/Kernel.php +++ b/tests/Kernel.php @@ -9,6 +9,7 @@ use Dbp\Relay\CoreBundle\DbpRelayCoreBundle; use Nelmio\CorsBundle\NelmioCorsBundle; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Bundle\MonologBundle\MonologBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; @@ -25,6 +26,7 @@ class Kernel extends BaseKernel yield new SecurityBundle(); yield new TwigBundle(); yield new NelmioCorsBundle(); + yield new MonologBundle(); yield new ApiPlatformBundle(); yield new DbpRelayCoreBundle(); } diff --git a/tests/Logging/LoggingProcessorTest.php b/tests/Logging/LoggingProcessorTest.php index ec0a6ae3fea6c3fd8593d54fc5b3b55a892bda90..01cba8c6aaaa871913a66a653f7a3afd83ef2e3e 100644 --- a/tests/Logging/LoggingProcessorTest.php +++ b/tests/Logging/LoggingProcessorTest.php @@ -16,9 +16,9 @@ class LoggingProcessorTest extends WebTestCase { $processor = new LoggingProcessor(new TestUserSession('some-random-user-id'), new RequestStack()); - $record = ['message' => 'http://foo.bar?token=secret']; + $record = ['message' => 'http://foo.bar?token=secret', 'channel' => 'app']; $record = $processor->__invoke($record); - $this->assertSame(['message' => 'http://foo.bar?token=hidden', 'context' => ['relay-session-id' => 'logging-id']], $record); + $this->assertSame(['message' => 'http://foo.bar?token=hidden', 'channel' => 'app', 'context' => ['relay-session-id' => 'logging-id']], $record); } public function testRequestId() @@ -27,7 +27,7 @@ class LoggingProcessorTest extends WebTestCase $stack->push(new Request()); $processor = new LoggingProcessor(new TestUserSession('some-random-user-id'), $stack); - $record = ['message' => 'foo']; + $record = ['message' => 'foo', 'channel' => 'app']; $processed = $processor->__invoke($record); $this->assertArrayHasKey('relay-request-id', $processed['context']); $processed2 = $processor->__invoke($record); @@ -37,9 +37,9 @@ class LoggingProcessorTest extends WebTestCase public function testSessionId() { $processor = new LoggingProcessor(new TestUserSession('log'), new RequestStack()); - $record = ['message' => 'foobar']; + $record = ['message' => 'foobar', 'channel' => 'app']; $record = $processor->__invoke($record); - $this->assertSame(['message' => 'foobar', 'context' => ['relay-session-id' => 'logging-id']], $record); + $this->assertSame(['message' => 'foobar', 'channel' => 'app', 'context' => ['relay-session-id' => 'logging-id']], $record); } public function testRoute() @@ -49,7 +49,7 @@ class LoggingProcessorTest extends WebTestCase $request->attributes->set('_route', 'some_route'); $stack->push($request); $processor = new LoggingProcessor(new TestUserSession('log'), $stack); - $record = ['message' => 'foobar']; + $record = ['message' => 'foobar', 'channel' => 'app']; $record = $processor->__invoke($record); $this->assertSame('some_route', $record['context']['relay-route']); } @@ -62,17 +62,38 @@ class LoggingProcessorTest extends WebTestCase 'message' => 'hello some-random-user-id!', 'extra' => ['foo' => 'some-random-user-id'], 'context' => ['foo' => 'some-random-user-id'], + 'channel' => 'app', ]; $record = $processor->__invoke($record); $this->assertSame([ 'message' => 'hello *****!', 'extra' => ['foo' => '*****'], - 'context' => ['foo' => '*****', 'relay-session-id' => 'logging-id'], ], $record); + 'context' => ['foo' => '*****', 'relay-session-id' => 'logging-id'], + 'channel' => 'app', ], $record); // Don't mask when contained in a word $processor = new LoggingProcessor(new TestUserSession('log'), new RequestStack()); - $record = ['message' => 'logging log']; + $record = ['message' => 'logging log', 'channel' => 'app']; $record = $processor->__invoke($record); - $this->assertSame(['message' => 'logging *****', 'context' => ['relay-session-id' => 'logging-id']], $record); + $this->assertSame(['message' => 'logging *****', 'channel' => 'app', 'context' => ['relay-session-id' => 'logging-id']], $record); + } + + public function testNoMasking() + { + $processor = new LoggingProcessor(new TestUserSession('some-random-user-id'), new RequestStack()); + $record = [ + 'message' => 'hello some-random-user-id!', + 'channel' => 'mychannel', + ]; + $result = $processor->__invoke($record); + $this->assertSame('hello *****!', $result['message']); + + $processor->setMaskConfig(['mychannel' => true]); + $result = $processor->__invoke($record); + $this->assertSame('hello *****!', $result['message']); + + $processor->setMaskConfig(['mychannel' => false]); + $result = $processor->__invoke($record); + $this->assertSame('hello some-random-user-id!', $result['message']); } }