Skip to content

Commit f80419f

Browse files
tomuddingrinkp
andcommitted
feat: add DBAL middleware to ensure proper role is used for all queries
Initially, we attempted to use the `wrapperClass` option to handle setting the database role. However, we 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. We 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 us 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, we 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 for each database is defined. However, validation of the actual value is done by PostgreSQL itself (it will complain if the role does not exist). Co-Authored-By: Rink <rinkp@users.noreply.github.com>
1 parent f2c4154 commit f80419f

File tree

6 files changed

+142
-0
lines changed

6 files changed

+142
-0
lines changed

.env.dist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ DOCTRINE_DEFAULT_HOST=postgresql
77
DOCTRINE_DEFAULT_PORT=5432
88
DOCTRINE_DEFAULT_USER=gewisdb
99
DOCTRINE_DEFAULT_PASSWORD=gewisdb
10+
DOCTRINE_DEFAULT_ROLE=gewisdb
1011
DOCTRINE_DEFAULT_DATABASE=gewisdb
1112
DOCTRINE_REPORT_HOST=postgresql
1213
DOCTRINE_REPORT_PORT=5432
1314
DOCTRINE_REPORT_USER=gewisdb
1415
DOCTRINE_REPORT_PASSWORD=gewisdb
16+
DOCTRINE_REPORT_ROLE=gewisdb
1517
DOCTRINE_REPORT_DATABASE=gewisdb_report
1618

1719
# Laminas settings

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: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
use function implode;
13+
14+
class Driver extends AbstractDriverMiddleware
15+
{
16+
/**
17+
* @param array<non-empty-string, non-empty-string> $roles
18+
*/
19+
public function __construct(
20+
DriverInterface $driver,
21+
private readonly array $roles,
22+
private readonly bool $isPgSQL,
23+
) {
24+
parent::__construct($driver);
25+
}
26+
27+
/**
28+
* {@inheritDoc}
29+
*/
30+
public function connect(
31+
#[SensitiveParameter]
32+
array $params,
33+
): ConnectionInterface {
34+
$connection = parent::connect($params);
35+
36+
if (
37+
$this->isPgSQL
38+
&& isset($params['host'], $params['port'], $params['dbname'])
39+
) {
40+
$role = $this->roles[implode(':', [$params['host'], $params['port'], $params['dbname']])];
41+
42+
$connection->exec('SET ROLE ' . $connection->quote($role));
43+
}
44+
45+
return $connection;
46+
}
47+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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+
use function implode;
13+
14+
class SetRoleMiddleware implements MiddlewareInterface
15+
{
16+
public function wrap(DriverInterface $driver): DriverInterface
17+
{
18+
$isPgSQL = $driver instanceof DriverInterface\PDO\PgSQL\Driver;
19+
if (
20+
!$isPgSQL
21+
&& !$driver instanceof DriverInterface\PDO\SQLite\Driver
22+
) {
23+
throw new RuntimeException('Expected DBAL Driver to be PDO PgSQL/Sqlite, but got ' . $driver::class);
24+
}
25+
26+
$roleDefaultHost = getenv('DOCTRINE_DEFAULT_HOST');
27+
$roleDefaultPort = getenv('DOCTRINE_DEFAULT_PORT');
28+
$roleDefaultDB = getenv('DOCTRINE_DEFAULT_DATABASE');
29+
$roleDefaultRole = getenv('DOCTRINE_DEFAULT_ROLE');
30+
31+
$roleReportHost = getenv('DOCTRINE_REPORT_HOST');
32+
$roleReportPort = getenv('DOCTRINE_REPORT_PORT');
33+
$roleReportDB = getenv('DOCTRINE_REPORT_DATABASE');
34+
$roleReportRole = getenv('DOCTRINE_REPORT_ROLE');
35+
36+
if (
37+
false === $roleDefaultHost
38+
|| false === $roleDefaultPort
39+
|| false === $roleDefaultDB
40+
|| false === $roleDefaultRole
41+
) {
42+
throw new RuntimeException('Required `DOCTRINE_DEFAULT_*` environment variables not set...');
43+
}
44+
45+
if (
46+
false === $roleReportHost
47+
|| false === $roleReportPort
48+
|| false === $roleReportDB
49+
|| false === $roleReportRole
50+
) {
51+
throw new RuntimeException('Required `DOCTRINE_REPORT_*` environment variables not set...');
52+
}
53+
54+
$roles = [
55+
implode(
56+
':',
57+
[
58+
$roleDefaultHost,
59+
$roleDefaultPort,
60+
$roleDefaultDB,
61+
],
62+
) => $roleDefaultRole,
63+
implode(
64+
':',
65+
[
66+
$roleReportHost,
67+
$roleReportPort,
68+
$roleReportDB,
69+
],
70+
) => $roleReportRole,
71+
];
72+
73+
return new Driver($driver, $roles, $isPgSQL);
74+
}
75+
}

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)