Skip to content

Commit fe02177

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 fe02177

File tree

6 files changed

+88
-0
lines changed

6 files changed

+88
-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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
private readonly bool $isPgSQL,
18+
) {
19+
parent::__construct($driver);
20+
}
21+
22+
/**
23+
* {@inheritDoc}
24+
*/
25+
public function connect(
26+
#[SensitiveParameter]
27+
array $params,
28+
): ConnectionInterface {
29+
$connection = parent::connect($params);
30+
31+
if ($this->isPgSQL) {
32+
$connection->exec('SET ROLE ' . $connection->quote($this->role));
33+
}
34+
35+
return $connection;
36+
}
37+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\Middleware as MiddlewareInterface;
9+
use RuntimeException;
10+
11+
use function getenv;
12+
13+
class SetRoleMiddleware implements MiddlewareInterface
14+
{
15+
public function wrap(DriverInterface $driver): DriverInterface
16+
{
17+
$isPgSQL = $driver instanceof DriverInterface\PDO\PgSQL\Driver;
18+
if (
19+
!$isPgSQL
20+
&& !$driver instanceof DriverInterface\PDO\SQLite\Driver
21+
) {
22+
throw new RuntimeException('Expected DBAL Driver to be PDO PgSQL, but got ' . $driver::class);
23+
}
24+
25+
$role = getenv('DOCTRINE_ROLE');
26+
if (false === $role) {
27+
throw new RuntimeException('Required DOCTRINE_ROLE not set...');
28+
}
29+
30+
return new Driver($driver, $role, $isPgSQL);
31+
}
32+
}

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)