Skip to content

Commit 5fa9d53

Browse files
committed
feat: add DBAL middleware to ensure proper role is used for all queries
Initially, I attempted to use the `wrapperClass` option to handle setting the database role. However, I encountered issues because DBAL checks for the exact `Connection` class type and not the interface, making it difficult to extend the `Connection` class as needed. I also considered overwriting the PgSQL-specific connection class (PDO variant) to set the role upon connection. Unfortunately, this was not an option because the class is declared as `final`, preventing me from extending it. The next potential solution was to use an `EventSubscriber` to set the role after the connection was established (using `postConnect`). However, this approach is already deprecated in our version of DBAL and completely removed in the next major release, rendering it unsuitable for us (maintainability). Ultimately, I implemented the `SET ROLE` functionality using DBAL's middleware. By wrapping the driver, and manually creating the `Connection` we can perform the `SET ROLE` query before the connection is used by the application. Runtime checks exist to ensure that the role (`DOCTRINE_ROLE`) is defined. However, validation of the actual value is done by PostgreSQL itself (it will complain if the role does not exist).
1 parent c3a4f55 commit 5fa9d53

File tree

6 files changed

+77
-0
lines changed

6 files changed

+77
-0
lines changed

.env.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CHECKER_MEMBERSHIP_API_ENDPOINT=https://tue-lookup.test.gewis.nl/user/
33
CHECKER_MEMBERSHIP_API_KEY=c2VjcmV0
44
CHECKER_MEMBERSHIP_API_MAX_TOTAL_REQUESTS=200
55
CHECKER_MEMBERSHIP_API_MAX_MANUAL_REQUESTS=20
6+
DOCTRINE_ROLE=gewisdb
67
DOCTRINE_DEFAULT_HOST=postgresql
78
DOCTRINE_DEFAULT_PORT=5432
89
DOCTRINE_DEFAULT_USER=gewisdb

config/autoload/doctrine.local.development.php.dist

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
declare(strict_types=1);
44

5+
use Application\Extensions\Doctrine\Middleware\SetRoleMiddleware;
56
use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PgSQLDriver;
67
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
78

@@ -82,6 +83,9 @@ return [
8283
// to use the default chained driver. The retrieved service name will
8384
// be `doctrine.driver.$thisSetting`
8485
'driver' => 'orm_default',
86+
'middlewares' => [
87+
SetRoleMiddleware::class,
88+
],
8589

8690
// Generate proxies automatically (turn off for production)
8791
'generate_proxies' => true,
@@ -128,6 +132,9 @@ return [
128132
// to use the default chained driver. The retrieved service name will
129133
// be `doctrine.driver.$thisSetting`
130134
'driver' => 'orm_report',
135+
'middlewares' => [
136+
SetRoleMiddleware::class,
137+
],
131138

132139
// Generate proxies automatically (turn off for production)
133140
'generate_proxies' => true,

config/autoload/doctrine.local.production.php.dist

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
declare(strict_types=1);
44

5+
use Application\Extensions\Doctrine\Middleware\SetRoleMiddleware;
56
use Doctrine\DBAL\Driver\PDO\PgSQL\Driver as PgSQLDriver;
67
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
78

@@ -82,6 +83,9 @@ return [
8283
// to use the default chained driver. The retrieved service name will
8384
// be `doctrine.driver.$thisSetting`
8485
'driver' => 'orm_default',
86+
'middlewares' => [
87+
SetRoleMiddleware::class,
88+
],
8589

8690
// Generate proxies automatically (turn off for production)
8791
'generate_proxies' => false,
@@ -128,6 +132,9 @@ return [
128132
// to use the default chained driver. The retrieved service name will
129133
// be `doctrine.driver.$thisSetting`
130134
'driver' => 'orm_report',
135+
'middlewares' => [
136+
SetRoleMiddleware::class,
137+
],
131138

132139
// Generate proxies automatically (turn off for production)
133140
'generate_proxies' => false,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Application\Extensions\Doctrine\Middleware;
6+
7+
use Doctrine\DBAL\Driver as DriverInterface;
8+
use Doctrine\DBAL\Driver\Connection as ConnectionInterface;
9+
use Doctrine\DBAL\Driver\Middleware\AbstractDriverMiddleware;
10+
use SensitiveParameter;
11+
12+
class Driver extends AbstractDriverMiddleware
13+
{
14+
public function __construct(
15+
DriverInterface $driver,
16+
private readonly string $role,
17+
) {
18+
parent::__construct($driver);
19+
}
20+
21+
/**
22+
* {@inheritDoc}
23+
*/
24+
public function connect(
25+
#[SensitiveParameter]
26+
array $params,
27+
): ConnectionInterface {
28+
$connection = parent::connect($params);
29+
$connection->exec('SET ROLE ' . $connection->quote($this->role));
30+
31+
return $connection;
32+
}
33+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Application\Extensions\Doctrine\Middleware;
6+
use Doctrine\DBAL\Driver as DriverInterface;
7+
use Doctrine\DBAL\Driver\Middleware as MiddlewareInterface;
8+
use RuntimeException;
9+
10+
class SetRoleMiddleware implements MiddlewareInterface
11+
{
12+
public function wrap(DriverInterface $driver): DriverInterface
13+
{
14+
if (!$driver instanceof DriverInterface\PDO\PgSQL\Driver) {
15+
throw new RuntimeException('Expected DBAL Driver to be PDO PgSQL, but got ' . get_class($driver));
16+
}
17+
18+
$role = getenv('DOCTRINE_ROLE');
19+
if (false === $role) {
20+
throw new RuntimeException('Required DOCTRINE_ROLE not set...');
21+
}
22+
23+
return new Driver($driver, $role);
24+
}
25+
}

module/Application/src/Module.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Application;
66

7+
use Application\Extensions\Doctrine\Middleware\SetRoleMiddleware;
78
use Application\Mapper\ConfigItem as ConfigItemMapper;
89
use Application\Mapper\Factory\ConfigItemFactory as ConfigItemMapperFactory;
910
use Application\Service\Config as ConfigService;
@@ -123,6 +124,9 @@ public function getConfig(): array
123124
public function getServiceConfig(): array
124125
{
125126
return [
127+
'invokables' => [
128+
SetRoleMiddleware::class => SetRoleMiddleware::class,
129+
],
126130
'factories' => [
127131
ConfigItemMapper::class => ConfigItemMapperFactory::class,
128132
ConfigService::class => ConfigServiceFactory::class,

0 commit comments

Comments
 (0)