mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-12 15:53:46 -04:00
fix(databases): let REDIS_PASSWORD override the KeyDB and Dragonfly password
The stored password is the default. Before, a local REDIS_PASSWORD was rewritten to the stored password, and a shared one replaced the stored password, so the old password was lost. - A local, shared, or remote secret REDIS_PASSWORD sets the server password; the stored password and the variable are not changed - Removing the variable, or an empty value, uses the stored password - Connection URLs show the password in use; for a remote secret they show the reference, not the secret - Databases created before this release keep the v4.3.23 behavior
This commit is contained in:
@@ -250,22 +250,16 @@ class StartDragonfly
|
||||
{
|
||||
$environment_variables = collect();
|
||||
$this->resolvedRedisPassword = (string) $this->database->dragonfly_password;
|
||||
$passwordVariable = $this->database->serverPasswordEnvironmentVariable();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$usesSecretManager = $this->database->environmentVariableUsesSecretManager($env);
|
||||
if ($env->key === 'REDIS_PASSWORD' && ! $env->is_shared && ! $usesSecretManager) {
|
||||
$env->update(['value' => $this->database->dragonfly_password]);
|
||||
}
|
||||
$rawValue = (string) $this->database->resolveSecretManagerEnvironmentVariableValue($env);
|
||||
$resolvedValue = (string) $this->database->formatEnvironmentVariableValue($env, $rawValue);
|
||||
// Credentials below are placed directly in the compose file (healthcheck, command).
|
||||
$composeFileValue = $this->database->formatComposeFileValue($env, $rawValue);
|
||||
$environment_variables->push($env->key.'='.$resolvedValue);
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
if ($env->is_shared && ! $usesSecretManager) {
|
||||
$this->database->update(['dragonfly_password' => $rawValue]);
|
||||
}
|
||||
$this->resolvedRedisPassword = $composeFileValue;
|
||||
$this->redisPasswordFromSecretManager = $usesSecretManager;
|
||||
// A REDIS_PASSWORD variable overrides the stored password, which stays unchanged.
|
||||
if ($passwordVariable?->is($env) && $rawValue !== '') {
|
||||
// Credentials below are placed directly in the compose file (healthcheck, command).
|
||||
$this->resolvedRedisPassword = $this->database->formatComposeFileValue($env, $rawValue);
|
||||
$this->redisPasswordFromSecretManager = $this->database->environmentVariableUsesSecretManager($env);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -250,22 +250,16 @@ class StartKeydb
|
||||
{
|
||||
$environment_variables = collect();
|
||||
$this->resolvedRedisPassword = (string) $this->database->keydb_password;
|
||||
$passwordVariable = $this->database->serverPasswordEnvironmentVariable();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$usesSecretManager = $this->database->environmentVariableUsesSecretManager($env);
|
||||
if ($env->key === 'REDIS_PASSWORD' && ! $env->is_shared && ! $usesSecretManager) {
|
||||
$env->update(['value' => $this->database->keydb_password]);
|
||||
}
|
||||
$rawValue = (string) $this->database->resolveSecretManagerEnvironmentVariableValue($env);
|
||||
$resolvedValue = (string) $this->database->formatEnvironmentVariableValue($env, $rawValue);
|
||||
// Credentials below are placed directly in the compose file (healthcheck, command).
|
||||
$composeFileValue = $this->database->formatComposeFileValue($env, $rawValue);
|
||||
$environment_variables->push($env->key.'='.$resolvedValue);
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
if ($env->is_shared && ! $usesSecretManager) {
|
||||
$this->database->update(['keydb_password' => $rawValue]);
|
||||
}
|
||||
$this->resolvedRedisPassword = $composeFileValue;
|
||||
$this->redisPasswordFromSecretManager = $usesSecretManager;
|
||||
// A REDIS_PASSWORD variable overrides the stored password, which stays unchanged.
|
||||
if ($passwordVariable?->is($env) && $rawValue !== '') {
|
||||
// Credentials below are placed directly in the compose file (healthcheck, command).
|
||||
$this->resolvedRedisPassword = $this->database->formatComposeFileValue($env, $rawValue);
|
||||
$this->redisPasswordFromSecretManager = $this->database->environmentVariableUsesSecretManager($env);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -312,7 +312,7 @@ class StandaloneDragonfly extends BaseModel
|
||||
get: function () {
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$port = $this->enable_ssl ? 6380 : 6379;
|
||||
$encodedPass = rawurlencode($this->dragonfly_password);
|
||||
$encodedPass = rawurlencode($this->connectionPassword());
|
||||
$url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
@@ -334,7 +334,7 @@ class StandaloneDragonfly extends BaseModel
|
||||
return null;
|
||||
}
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$encodedPass = rawurlencode($this->dragonfly_password);
|
||||
$encodedPass = rawurlencode($this->connectionPassword());
|
||||
$url = "{$scheme}://:{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
@@ -369,6 +369,47 @@ class StandaloneDragonfly extends BaseModel
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
/**
|
||||
* The REDIS_PASSWORD variable that sets the server password instead of the stored password.
|
||||
* Databases created before this release (legacy_password_quoting) keep their v4.3.23 server
|
||||
* password, the stored one, unless the variable reads a remote secret.
|
||||
*/
|
||||
public function serverPasswordEnvironmentVariable(): ?EnvironmentVariable
|
||||
{
|
||||
$environmentVariable = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
|
||||
|
||||
if (! $environmentVariable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->legacy_password_quoting && ! $this->environmentVariableUsesSecretManager($environmentVariable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $environmentVariable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server password for connection URLs. A remote secret is not fetched here, so the URL
|
||||
* shows its reference instead of a stale or wrong password.
|
||||
*/
|
||||
public function connectionPassword(): string
|
||||
{
|
||||
$environmentVariable = $this->serverPasswordEnvironmentVariable();
|
||||
|
||||
if ($environmentVariable) {
|
||||
$password = $this->environmentVariableUsesSecretManager($environmentVariable)
|
||||
? (string) $environmentVariable->value
|
||||
: (string) $this->resolveSecretManagerEnvironmentVariableValue($environmentVariable);
|
||||
|
||||
if ($password !== '') {
|
||||
return $password;
|
||||
}
|
||||
}
|
||||
|
||||
return (string) $this->dragonfly_password;
|
||||
}
|
||||
|
||||
public function persistentStorages()
|
||||
{
|
||||
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
||||
|
||||
@@ -313,7 +313,7 @@ class StandaloneKeydb extends BaseModel
|
||||
get: function () {
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$port = $this->enable_ssl ? 6380 : 6379;
|
||||
$encodedPass = rawurlencode($this->keydb_password);
|
||||
$encodedPass = rawurlencode($this->connectionPassword());
|
||||
$url = "{$scheme}://:{$encodedPass}@{$this->uuid}:{$port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
@@ -335,7 +335,7 @@ class StandaloneKeydb extends BaseModel
|
||||
return null;
|
||||
}
|
||||
$scheme = $this->enable_ssl ? 'rediss' : 'redis';
|
||||
$encodedPass = rawurlencode($this->keydb_password);
|
||||
$encodedPass = rawurlencode($this->connectionPassword());
|
||||
$url = "{$scheme}://:{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
||||
|
||||
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
||||
@@ -370,6 +370,47 @@ class StandaloneKeydb extends BaseModel
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
/**
|
||||
* The REDIS_PASSWORD variable that sets the server password instead of the stored password.
|
||||
* Databases created before this release (legacy_password_quoting) keep their v4.3.23 server
|
||||
* password, the stored one, unless the variable reads a remote secret.
|
||||
*/
|
||||
public function serverPasswordEnvironmentVariable(): ?EnvironmentVariable
|
||||
{
|
||||
$environmentVariable = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
|
||||
|
||||
if (! $environmentVariable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->legacy_password_quoting && ! $this->environmentVariableUsesSecretManager($environmentVariable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $environmentVariable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server password for connection URLs. A remote secret is not fetched here, so the URL
|
||||
* shows its reference instead of a stale or wrong password.
|
||||
*/
|
||||
public function connectionPassword(): string
|
||||
{
|
||||
$environmentVariable = $this->serverPasswordEnvironmentVariable();
|
||||
|
||||
if ($environmentVariable) {
|
||||
$password = $this->environmentVariableUsesSecretManager($environmentVariable)
|
||||
? (string) $environmentVariable->value
|
||||
: (string) $this->resolveSecretManagerEnvironmentVariableValue($environmentVariable);
|
||||
|
||||
if ($password !== '') {
|
||||
return $password;
|
||||
}
|
||||
}
|
||||
|
||||
return (string) $this->keydb_password;
|
||||
}
|
||||
|
||||
public function persistentStorages()
|
||||
{
|
||||
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
||||
|
||||
@@ -197,30 +197,58 @@ it('quotes shell control characters in the password of a database created after
|
||||
'pipe' => 'x|y',
|
||||
]);
|
||||
|
||||
it('runs with the stored password and syncs a plain REDIS_PASSWORD variable to it', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
it('runs with a plain REDIS_PASSWORD variable instead of the stored password without changing either', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
$database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']);
|
||||
$env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'envpass']);
|
||||
|
||||
$start::run($database->fresh(), new Activity);
|
||||
|
||||
$service = legacyPasswordQuotingStartedService($this->executor, $database->uuid);
|
||||
expect($service['command'])->toBe(sprintf($commandFormat, "'envpass'"))
|
||||
->and($service['healthcheck'][3])->toBe('envpass')
|
||||
->and($service['environment'])->toContain('REDIS_PASSWORD=envpass')
|
||||
->and($env->fresh()->value)->toBe('envpass')
|
||||
->and($database->fresh()->{$passwordColumn})->toBe('storedpass');
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
it('runs with the stored password again after the REDIS_PASSWORD variable is removed', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
$database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']);
|
||||
$env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'envpass']);
|
||||
$start::run($database->fresh(), new Activity);
|
||||
|
||||
$env->delete();
|
||||
$start::run($database->fresh(), new Activity);
|
||||
|
||||
$service = legacyPasswordQuotingStartedService($this->executor, $database->uuid);
|
||||
expect($service['command'])->toBe(sprintf($commandFormat, "'storedpass'"))
|
||||
->and($service['healthcheck'][3])->toBe('storedpass')
|
||||
->and($service['environment'])->toContain('REDIS_PASSWORD=storedpass')
|
||||
->and($env->fresh()->value)->toBe('storedpass')
|
||||
->and($database->fresh()->{$passwordColumn})->toBe('storedpass');
|
||||
->and($service['environment'])->toContain('REDIS_PASSWORD=storedpass');
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
it('runs with the stored password when the REDIS_PASSWORD variable is empty', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
$database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']);
|
||||
$database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '']);
|
||||
|
||||
$start::run($database->fresh(), new Activity);
|
||||
|
||||
$service = legacyPasswordQuotingStartedService($this->executor, $database->uuid);
|
||||
expect($service['command'])->toBe(sprintf($commandFormat, "'storedpass'"))
|
||||
->and($service['healthcheck'][3])->toBe('storedpass');
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
it('keeps the exact v4.3.23 command of a legacy database with a different plain REDIS_PASSWORD variable', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
$database = $create($this->environment->id, $this->destination, [$passwordColumn => 'stored;pass']);
|
||||
DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]);
|
||||
$database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'envpass']);
|
||||
$env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'envpass']);
|
||||
|
||||
$start::run($database->fresh(), new Activity);
|
||||
|
||||
$service = legacyPasswordQuotingStartedService($this->executor, $database->uuid);
|
||||
expect($service['command'])->toBe(sprintf($commandFormat, 'stored;pass'))
|
||||
->and($service['healthcheck'][3])->toBe('stored;pass')
|
||||
->and($service['environment'])->toContain('REDIS_PASSWORD=envpass')
|
||||
->and($env->fresh()->value)->toBe('envpass')
|
||||
->and($database->fresh()->{$passwordColumn})->toBe('stored;pass')
|
||||
->and($database->fresh()->legacy_password_quoting)->toBeTrue();
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
@@ -246,7 +274,7 @@ it('runs with a remote secret REDIS_PASSWORD without storing it', function (stri
|
||||
->and($database->fresh()->{$passwordColumn})->toBe('storedpass');
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
it('copies a shared REDIS_PASSWORD variable into the stored password', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
it('runs with a shared REDIS_PASSWORD variable without storing it', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
SharedEnvironmentVariable::query()->create([
|
||||
'key' => 'REDIS_PASSWORD',
|
||||
'value' => 'sharedpass',
|
||||
@@ -262,5 +290,67 @@ it('copies a shared REDIS_PASSWORD variable into the stored password', function
|
||||
expect($service['command'])->toBe(sprintf($commandFormat, "'sharedpass'"))
|
||||
->and($service['healthcheck'][3])->toBe('sharedpass')
|
||||
->and($env->fresh()->value)->toBe('{{team.REDIS_PASSWORD}}')
|
||||
->and($database->fresh()->{$passwordColumn})->toBe('sharedpass');
|
||||
->and($database->fresh()->{$passwordColumn})->toBe('storedpass');
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
it('keeps the stored password of a legacy database with a shared REDIS_PASSWORD variable', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
SharedEnvironmentVariable::query()->create([
|
||||
'key' => 'REDIS_PASSWORD',
|
||||
'value' => 'sharedpass',
|
||||
'type' => 'team',
|
||||
'team_id' => $this->environment->project->team_id,
|
||||
]);
|
||||
$database = $create($this->environment->id, $this->destination, [$passwordColumn => 'stored;pass']);
|
||||
DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]);
|
||||
$database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '{{team.REDIS_PASSWORD}}']);
|
||||
|
||||
$start::run($database->fresh(), new Activity);
|
||||
|
||||
$service = legacyPasswordQuotingStartedService($this->executor, $database->uuid);
|
||||
expect($service['command'])->toBe(sprintf($commandFormat, 'stored;pass'))
|
||||
->and($service['healthcheck'][3])->toBe('stored;pass')
|
||||
->and($database->fresh()->{$passwordColumn})->toBe('stored;pass')
|
||||
->and($database->fresh()->legacy_password_quoting)->toBeTrue();
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
it('shows the password the server uses in the connection URLs', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
SharedEnvironmentVariable::query()->create([
|
||||
'key' => 'REDIS_PASSWORD',
|
||||
'value' => 'shared/pass',
|
||||
'type' => 'team',
|
||||
'team_id' => $this->environment->project->team_id,
|
||||
]);
|
||||
$database = $create($this->environment->id, $this->destination, [$passwordColumn => 'stored/pass', 'is_public' => true, 'public_port' => 16379]);
|
||||
expect($database->fresh()->internal_db_url)->toContain(':stored%2Fpass@')
|
||||
->and($database->fresh()->external_db_url)->toContain(':stored%2Fpass@');
|
||||
|
||||
$env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'env/pass']);
|
||||
expect($database->fresh()->internal_db_url)->toContain(':env%2Fpass@')
|
||||
->and($database->fresh()->external_db_url)->toContain(':env%2Fpass@');
|
||||
|
||||
$env->update(['value' => '{{team.REDIS_PASSWORD}}']);
|
||||
expect($database->fresh()->internal_db_url)->toContain(':shared%2Fpass@')
|
||||
->and($database->fresh()->external_db_url)->toContain(':shared%2Fpass@');
|
||||
|
||||
DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]);
|
||||
expect($database->fresh()->internal_db_url)->toContain(':stored%2Fpass@')
|
||||
->and($database->fresh()->external_db_url)->toContain(':stored%2Fpass@');
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
it('shows the unresolved remote secret reference in the connection URL without fetching it', function (string $create, string $start, string $passwordColumn, string $commandFormat) {
|
||||
Http::fake();
|
||||
$token = IntegrationToken::query()->create([
|
||||
'team_id' => $this->environment->project->team_id,
|
||||
'provider' => 'doppler',
|
||||
'name' => 'Doppler',
|
||||
'token' => 'the-secret-token',
|
||||
'capabilities' => ['secrets'],
|
||||
]);
|
||||
$database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']);
|
||||
$database->secretManagerLink()->create(['integration_token_id' => $token->id]);
|
||||
$database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '{{vault.REDIS_PASSWORD}}']);
|
||||
|
||||
expect($database->fresh()->internal_db_url)->toContain(':'.rawurlencode('{{vault.REDIS_PASSWORD}}').'@')
|
||||
->not->toContain('storedpass');
|
||||
Http::assertNothingSent();
|
||||
})->with('legacy-password-quoting-engines');
|
||||
|
||||
Reference in New Issue
Block a user