Skip to content
Snippets Groups Projects
Commit e8107c17 authored by Reiter, Christoph's avatar Reiter, Christoph :snake:
Browse files

extension: add a way to register a new logging channel and configure masking

This allows a bundle to register an extension without having to create a
dummy service+tag.

Registering allows an optional parameter for disabling secret masking in the
log processor for that channel.

The main use case for this is when a bundle wants an additional audit channel
which can contain PII or similar.

Since we configure the monolog bundle directly here we need to add a dependency
on it and make sure it is loaded. This means that bundles might need to add/load
the monolog bundle in their tests now after updating to this version.
parent 0652267b
No related branches found
No related tags found
No related merge requests found
Pipeline #232529 passed
......@@ -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",
......
......@@ -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": {
......
......@@ -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,
];
......
......@@ -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]]
);
}
}
......@@ -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();
......
......@@ -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()
......
......@@ -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();
}
......
......@@ -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']);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment