diff --git a/app/Commands/Connection/AddCommand.php b/app/Commands/Connection/AddCommand.php index 47a4be3..3cce757 100644 --- a/app/Commands/Connection/AddCommand.php +++ b/app/Commands/Connection/AddCommand.php @@ -29,7 +29,8 @@ class AddCommand extends Command {--username= : Database username} {--password= : Database password} {--production : Mark this as a production connection} - {--trust-server-certificate : Trust the server certificate (SQL Server with self-signed certs)}'; + {--trust-server-certificate : Trust the server certificate (SQL Server with self-signed certs)} + {--attr_ssl_ca : The file path to an SSL certificate authority (CA), to enable secure encrypted connections to a MySQL database}'; /** * @var string @@ -227,7 +228,20 @@ public function handle(ConfigService $config): int } } - // --- Step 10: Production flag --- + // --- Step 10: SSL certificate --- + $attrSslCa = null; + + if ($type->requiresNetworkConfig() && ($type === DatabaseConnectionType::Mysql || $type === DatabaseConnectionType::MariaDB)) { + $attrSslCaOption = $this->option('attr_ssl_ca'); + $attrSslCa = is_string($attrSslCaOption) && $attrSslCaOption !== '' ? $attrSslCaOption : null; + + if ($attrSslCa === null) { + $asked = $this->ask('SSL Certificate Authority'); + $attrSslCa = is_string($asked) ? $asked : null; + } + } + + // --- Step 11: Production flag --- $isProduction = (bool) $this->option('production'); if (! $isProduction) { @@ -239,6 +253,10 @@ public function handle(ConfigService $config): int $this->warn('This connection is marked as production. Destructive operations will require confirmation.'); } + + + + // --- Summary table --- $summaryRows = [ ['Name', $name], @@ -282,6 +300,7 @@ public function handle(ConfigService $config): int } $summaryRows[] = ['Production', $isProduction ? 'Yes' : 'No']; + $summaryRows[] = ['SSL Certificate Authority', $attrSslCa ? 'Yes' : 'No']; $this->table(['Field', 'Value'], $summaryRows); @@ -305,6 +324,7 @@ public function handle(ConfigService $config): int isProduction: $isProduction, trustServerCertificate: $trustServerCertificate, dialect: $dialect, + attrSslCa: $attrSslCa, ); try { diff --git a/app/Commands/Connection/ListCommand.php b/app/Commands/Connection/ListCommand.php index 1ff29e1..30b0204 100644 --- a/app/Commands/Connection/ListCommand.php +++ b/app/Commands/Connection/ListCommand.php @@ -45,10 +45,11 @@ public function handle(ConfigService $config): int $host, $connection->database ?? '—', $connection->isProduction ? 'Yes' : 'No', + $connection->attrSslCa ? 'Yes' : 'No', ]; } - $this->table(['Name', 'Driver', 'Host', 'Database', 'Production'], $rows); + $this->table(['Name', 'Driver', 'Host', 'Database', 'Production', 'SSL Certificate Authority'], $rows); return ExitCode::Success->value; } diff --git a/app/Commands/Connection/UpdateCommand.php b/app/Commands/Connection/UpdateCommand.php index 191816d..8d5531b 100644 --- a/app/Commands/Connection/UpdateCommand.php +++ b/app/Commands/Connection/UpdateCommand.php @@ -118,6 +118,7 @@ private function promptForFields(ConnectionData $current): ConnectionData $newType = DatabaseConnectionType::from($newTypeValue); $typeChanged = $newType !== $current->type; + $attrSslCa = null; if ($newType->requiresNetworkConfig()) { if ($typeChanged) { $host = $this->askString('Host', 'localhost'); @@ -156,6 +157,10 @@ private function promptForFields(ConnectionData $current): ConnectionData $password = Crypt::encryptString($passwordInput); } + if($newType->requiresNetworkConfig() && ($newType === DatabaseConnectionType::Mysql || $newType === DatabaseConnectionType::MariaDB)) { + $attrSslCa = $this->ask('SSL Certificate Authority', $current->attrSslCa ?? null); + } + $isProduction = $this->confirm('Is this a production connection?', $current->isProduction); $trustServerCertificate = false; @@ -177,6 +182,7 @@ private function promptForFields(ConnectionData $current): ConnectionData password: $password, isProduction: $isProduction, trustServerCertificate: $trustServerCertificate, + attrSslCa: $attrSslCa, ); } diff --git a/app/Data/ConnectionData.php b/app/Data/ConnectionData.php index 20083eb..a22b2bb 100644 --- a/app/Data/ConnectionData.php +++ b/app/Data/ConnectionData.php @@ -20,6 +20,7 @@ public function __construct( public bool $isProduction, public bool $trustServerCertificate = false, public ?DatabaseConnectionType $dialect = null, + public ?string $attrSslCa = null, ) {} /** @param array $data */ @@ -33,6 +34,7 @@ public static function fromArray(string $name, array $data): self $username = $data['username'] ?? null; $password = $data['password'] ?? null; $dialect = $data['dialect'] ?? null; + $attrSslCa = $data['attr_ssl_ca'] ?? null; return new self( name: $name, @@ -46,6 +48,7 @@ public static function fromArray(string $name, array $data): self isProduction: (bool) ($data['is_production'] ?? false), trustServerCertificate: (bool) ($data['trust_server_certificate'] ?? false), dialect: is_string($dialect) ? DatabaseConnectionType::tryFrom($dialect) : null, + attrSslCa: is_string($attrSslCa) ? $attrSslCa : null, ); } @@ -85,6 +88,10 @@ public function toArray(): array $data['trust_server_certificate'] = true; } + if ($this->attrSslCa) { + $data['attr_ssl_ca'] = $this->attrSslCa; + } + return $data; } } diff --git a/app/Services/Database/DatabaseConnectionService.php b/app/Services/Database/DatabaseConnectionService.php index 0befec6..3bfee22 100644 --- a/app/Services/Database/DatabaseConnectionService.php +++ b/app/Services/Database/DatabaseConnectionService.php @@ -8,6 +8,8 @@ use App\Enums\DatabaseConnectionType; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; +use PDO; +use Pdo\Mysql; use RuntimeException; use Throwable; @@ -92,6 +94,12 @@ public function buildConfig(ConnectionData $connection, string $password): array $config['search_path'] = $connection->schema; } + if($connection->attrSslCa) { + $config['options'] = [ + Mysql::ATTR_SSL_CA => base_path($connection->attrSslCa), + ]; + } + return $config; } diff --git a/tests/Feature/Commands/Connection/AddCommandTest.php b/tests/Feature/Commands/Connection/AddCommandTest.php index 0fa60b7..8c4b2f4 100644 --- a/tests/Feature/Commands/Connection/AddCommandTest.php +++ b/tests/Feature/Commands/Connection/AddCommandTest.php @@ -40,7 +40,9 @@ function fakeConfigReadOnly(): ConfigService '--database' => 'mydb', '--username' => 'root', '--password' => 'secret', + '--attr_ssl_ca' => '', ]) + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save this connection?', 'yes') ->assertExitCode(0); @@ -162,6 +164,7 @@ function fakeConfigReadOnly(): ConfigService '--username' => 'root', '--password' => 'secret', ]) + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save this connection?', 'no') ->expectsOutputToContain('Cancelled.') @@ -179,6 +182,7 @@ function fakeConfigReadOnly(): ConfigService ->expectsQuestion('Database name', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password', 'secret') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save this connection?', 'yes') ->assertExitCode(0); @@ -273,6 +277,7 @@ function fakeConfigReadOnly(): ConfigService '--password' => 'secret', '--production' => true, ]) + ->expectsQuestion('SSL Certificate Authority', null) ->expectsOutputToContain('This connection is marked as production.') ->expectsConfirmation('Save this connection?', 'yes') ->assertExitCode(0); @@ -326,6 +331,7 @@ function fakeConfigReadOnly(): ConfigService '--username' => 'root', '--password' => 'secret', ]) + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save this connection?', 'yes') ->expectsOutputToContain('Failed to save connection: disk full') diff --git a/tests/Feature/Commands/Connection/UpdateCommandTest.php b/tests/Feature/Commands/Connection/UpdateCommandTest.php index 6322f00..938c818 100644 --- a/tests/Feature/Commands/Connection/UpdateCommandTest.php +++ b/tests/Feature/Commands/Connection/UpdateCommandTest.php @@ -49,6 +49,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save changes?', 'yes') ->assertExitCode(0); @@ -73,6 +74,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save changes?', 'no') ->assertExitCode(0); @@ -114,6 +116,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save changes?', 'yes') ->assertExitCode(0); @@ -139,6 +142,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save changes?', 'yes') ->assertExitCode(0); @@ -179,6 +183,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save changes?', 'yes') ->expectsOutputToContain("Connection 'renamed' updated successfully.") @@ -204,6 +209,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save changes?', 'yes') ->expectsOutputToContain('disk full') @@ -234,6 +240,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsConfirmation('Save changes?', 'yes') ->assertExitCode(0); @@ -396,6 +403,7 @@ function makeUpdateConnection(string $name = 'staging'): ConnectionData ->expectsQuestion('Database', 'mydb') ->expectsQuestion('Username', 'root') ->expectsQuestion('Password (press Enter to keep current)', '') + ->expectsQuestion('SSL Certificate Authority', null) ->expectsConfirmation('Is this a production connection?', 'no') ->expectsOutputToContain("A connection named 'production' already exists.") ->assertExitCode(4);