Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/database-mysql/src/Connection/MySqlConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ public function connect(): void
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
];

if ($this->config->sslRootCert !== null) {
$options[Pdo\Mysql::ATTR_SSL_CA] = $this->config->sslRootCert;
$options[Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT] = $this->config->sslVerifyServerCert;
}

if ($this->config->sslCert !== null) {
$options[Pdo\Mysql::ATTR_SSL_CERT] = $this->config->sslCert;
}

if ($this->config->sslKey !== null) {
$options[Pdo\Mysql::ATTR_SSL_KEY] = $this->config->sslKey;
}

try {
$this->pdo = $this->createPdo(
$this->getDsn(),
Expand Down
124 changes: 116 additions & 8 deletions packages/database-mysql/tests/Connection/MySqlConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,42 @@ function createTestDatabaseConfig(
string $database = 'test',
string $username = 'root',
string $password = '',
?string $sslCa = null,
bool $sslVerifyServerCert = false,
?string $sslCert = null,
?string $sslKey = null,
): DatabaseConfig {
$tempDir = sys_get_temp_dir() . '/marko_mysql_test_' . uniqid();
mkdir($tempDir . '/config', recursive: true);

$configArray = [
'driver' => 'mysql',
'host' => $host,
'port' => $port,
'database' => $database,
'username' => $username,
'password' => $password,
];

if ($sslCa !== null) {
$configArray['ssl_ca'] = $sslCa;
}

if ($sslVerifyServerCert) {
$configArray['ssl_verify_server_cert'] = true;
}

if ($sslCert !== null) {
$configArray['ssl_cert'] = $sslCert;
}

if ($sslKey !== null) {
$configArray['ssl_key'] = $sslKey;
}

file_put_contents(
$tempDir . '/config/database.php',
'<?php return ' . var_export([
'driver' => 'mysql',
'host' => $host,
'port' => $port,
'database' => $database,
'username' => $username,
'password' => $password,
], true) . ';',
'<?php return ' . var_export($configArray, true) . ';',
);

$paths = new ProjectPaths($tempDir);
Expand All @@ -47,6 +70,41 @@ function createTestDatabaseConfig(
return $config;
}

/**
* Connect a MySqlConnection and return the PDO options it passed.
*
* @return array<int, mixed>
*/
function connectAndCapturePdoOptions(DatabaseConfig $config): array
{
$capturedOptions = [];

$connection = new class ($config, $capturedOptions) extends MySqlConnection
{
public function __construct(
DatabaseConfig $config,
private array &$capturedOptions,
) {
parent::__construct($config);
}

protected function createPdo(
string $dsn,
string $username,
string $password,
array $options,
): PDO {
$this->capturedOptions = $options;

return new PDO('sqlite::memory:');
}
};

$connection->connect();

return $capturedOptions;
}

describe('MySqlConnection', function (): void {
it('implements ConnectionInterface', function (): void {
$config = createTestDatabaseConfig();
Expand Down Expand Up @@ -540,6 +598,56 @@ protected function createPdo(
expect($result)->toBe('success');
});

it('passes SSL CA cert in PDO options when configured', function (): void {
$options = connectAndCapturePdoOptions(createTestDatabaseConfig(sslCa: '/path/to/ca.pem'));

expect($options[Pdo\Mysql::ATTR_SSL_CA])->toBe('/path/to/ca.pem');
});

it('sets SSL verify server cert when configured', function (): void {
$options = connectAndCapturePdoOptions(
createTestDatabaseConfig(sslCa: '/path/to/ca.pem', sslVerifyServerCert: true),
);

expect($options[Pdo\Mysql::ATTR_SSL_CA])->toBe('/path/to/ca.pem')
->and($options[Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT])->toBeTrue();
});

it('defaults SSL verify server cert to true when ssl_ca is set', function (): void {
$options = connectAndCapturePdoOptions(createTestDatabaseConfig(sslCa: '/path/to/ca.pem'));

expect($options[Pdo\Mysql::ATTR_SSL_VERIFY_SERVER_CERT])->toBeTrue();
});

it('passes SSL client cert in PDO options when configured', function (): void {
$options = connectAndCapturePdoOptions(
createTestDatabaseConfig(sslCert: '/path/to/client-cert.pem', sslKey: '/path/to/client-key.pem'),
);

expect($options[Pdo\Mysql::ATTR_SSL_CERT])->toBe('/path/to/client-cert.pem');
});

it('passes SSL client key in PDO options when configured', function (): void {
$options = connectAndCapturePdoOptions(
createTestDatabaseConfig(sslCert: '/path/to/client-cert.pem', sslKey: '/path/to/client-key.pem'),
);

expect($options[Pdo\Mysql::ATTR_SSL_KEY])->toBe('/path/to/client-key.pem');
});

it('omits SSL client cert and key from PDO options when not configured', function (): void {
$options = connectAndCapturePdoOptions(createTestDatabaseConfig());

expect($options)->not->toHaveKey(Pdo\Mysql::ATTR_SSL_CERT)
->and($options)->not->toHaveKey(Pdo\Mysql::ATTR_SSL_KEY);
});

it('omits SSL CA cert from PDO options when not configured', function (): void {
$options = connectAndCapturePdoOptions(createTestDatabaseConfig());

expect($options)->not->toHaveKey(Pdo\Mysql::ATTR_SSL_CA);
});

it('prevents nested transactions (throws exception)', function (): void {
$config = createTestDatabaseConfig();
$connection = new class ($config) extends MySqlConnection
Expand Down
26 changes: 23 additions & 3 deletions packages/database-pgsql/src/Connection/PgSqlConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,25 @@ protected function createPdo(

private function buildDsn(): string
{
return "pgsql:host={$this->config->host};port={$this->config->port};dbname={$this->config->database}";
$dsn = "pgsql:host={$this->config->host};port={$this->config->port};dbname={$this->config->database}";

if ($this->config->sslMode !== null) {
$dsn .= ";sslmode={$this->config->sslMode}";
}

if ($this->config->sslRootCert !== null) {
$dsn .= ";sslrootcert={$this->config->sslRootCert}";
}

if ($this->config->sslCert !== null) {
$dsn .= ";sslcert={$this->config->sslCert}";
}

if ($this->config->sslKey !== null) {
$dsn .= ";sslkey={$this->config->sslKey}";
}

return $dsn;
}

/**
Expand Down Expand Up @@ -163,8 +181,10 @@ public function prepare(
/**
* @param array<int|string, mixed> $bindings
*/
private function bindValues(PDOStatement $statement, array $bindings): void
{
private function bindValues(
PDOStatement $statement,
array $bindings,
): void {
foreach ($bindings as $key => $value) {
$param = is_int($key) ? $key + 1 : $key;
$type = match (true) {
Expand Down
113 changes: 105 additions & 8 deletions packages/database-pgsql/tests/Connection/PgSqlConnectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,42 @@ function createTestPgSqlConfig(
string $database = 'test',
string $username = 'user',
string $password = 'pass',
?string $sslmode = null,
?string $sslCa = null,
?string $sslCert = null,
?string $sslKey = null,
): DatabaseConfig {
$tempDir = sys_get_temp_dir() . '/marko_pgsql_test_' . uniqid();
mkdir($tempDir . '/config', recursive: true);

$configArray = [
'driver' => 'pgsql',
'host' => $host,
'port' => $port,
'database' => $database,
'username' => $username,
'password' => $password,
];

if ($sslmode !== null) {
$configArray['sslmode'] = $sslmode;
}

if ($sslCa !== null) {
$configArray['ssl_ca'] = $sslCa;
}

if ($sslCert !== null) {
$configArray['ssl_cert'] = $sslCert;
}

if ($sslKey !== null) {
$configArray['ssl_key'] = $sslKey;
}

file_put_contents(
$tempDir . '/config/database.php',
'<?php return ' . var_export([
'driver' => 'pgsql',
'host' => $host,
'port' => $port,
'database' => $database,
'username' => $username,
'password' => $password,
], true) . ';',
'<?php return ' . var_export($configArray, true) . ';',
);

$paths = new ProjectPaths($tempDir);
Expand Down Expand Up @@ -569,6 +592,80 @@ protected function createPdo(
expect($result)->toBe('success');
});

it('includes sslmode in DSN when configured', function (): void {
$config = createTestPgSqlConfig(
host: 'db.example.com',
port: 5432,
database: 'myapp',
sslmode: 'require',
);
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->toBe('pgsql:host=db.example.com;port=5432;dbname=myapp;sslmode=require');
});

it('omits sslmode from DSN when not configured', function (): void {
$config = createTestPgSqlConfig(
host: 'db.example.com',
port: 5432,
database: 'myapp',
);
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->not->toContain('sslmode');
});

it('includes sslrootcert in DSN when configured', function (): void {
$config = createTestPgSqlConfig(
host: 'db.example.com',
port: 5432,
database: 'myapp',
sslCa: '/path/to/ca.pem',
);
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->toContain('sslrootcert=/path/to/ca.pem');
});

it('omits sslrootcert from DSN when not configured', function (): void {
$config = createTestPgSqlConfig(
host: 'db.example.com',
port: 5432,
database: 'myapp',
);
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->not->toContain('sslrootcert');
});

it('includes sslcert in DSN when configured', function (): void {
$config = createTestPgSqlConfig(sslCert: '/path/to/client-cert.pem', sslKey: '/path/to/client-key.pem');
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->toContain('sslcert=/path/to/client-cert.pem');
});

it('omits sslcert from DSN when not configured', function (): void {
$config = createTestPgSqlConfig();
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->not->toContain('sslcert');
});

it('includes sslkey in DSN when configured', function (): void {
$config = createTestPgSqlConfig(sslCert: '/path/to/client-cert.pem', sslKey: '/path/to/client-key.pem');
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->toContain('sslkey=/path/to/client-key.pem');
});

it('omits sslkey from DSN when not configured', function (): void {
$config = createTestPgSqlConfig();
$connection = new PgSqlConnection($config);

expect($connection->getDsn())->not->toContain('sslkey');
});

it('prevents nested transactions (throws exception)', function (): void {
$config = createTestPgSqlConfig();
$connection = new class ($config) extends PgSqlConnection
Expand Down
23 changes: 23 additions & 0 deletions packages/database/src/Config/DatabaseConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@

public string $password;

public ?string $sslMode;

public ?string $sslRootCert;

public bool $sslVerifyServerCert;

public ?string $sslCert;

public ?string $sslKey;

/**
* @throws ConfigurationException
*/
Expand Down Expand Up @@ -52,5 +62,18 @@ public function __construct(
$this->database = $config['database'];
$this->username = $config['username'];
$this->password = $config['password'];
$this->sslMode = $config['sslmode'] ?? null;
$this->sslRootCert = $config['ssl_ca'] ?? null;
$this->sslVerifyServerCert = $config['ssl_verify_server_cert'] ?? ($this->sslRootCert !== null);
$this->sslCert = $config['ssl_cert'] ?? null;
$this->sslKey = $config['ssl_key'] ?? null;

if ($this->sslCert !== null && $this->sslKey === null) {
throw ConfigurationException::incompleteSslKeyPair('ssl_cert', 'ssl_key');
}

if ($this->sslKey !== null && $this->sslCert === null) {
throw ConfigurationException::incompleteSslKeyPair('ssl_key', 'ssl_cert');
}
}
}
11 changes: 11 additions & 0 deletions packages/database/src/Exceptions/ConfigurationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,15 @@ public static function missingRequiredKey(
suggestion: "Add the '$key' key to your config/database.php file",
);
}

public static function incompleteSslKeyPair(
string $present,
string $missing,
): self {
return new self(
message: "SSL configuration key '$present' is set but '$missing' is missing",
context: 'While validating database SSL configuration',
suggestion: "When using client certificate authentication, both 'ssl_cert' and 'ssl_key' must be provided together",
);
}
}
Loading