mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-29 01:54:37 +08:00
143 lines
6.2 KiB
PHP
143 lines
6.2 KiB
PHP
<?php
|
|
|
|
use App\Actions\Server\StartSentinel;
|
|
use App\Console\Commands\ReencryptWithAppKey;
|
|
use App\Models\Application;
|
|
use App\Models\EnvironmentVariable;
|
|
use App\Models\GitlabApp;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Server;
|
|
use App\Models\ServerSetting;
|
|
use App\Models\Team;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Symfony\Component\Finder\Finder;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
function useAppKeys(string $key, array $previousKeys = []): void
|
|
{
|
|
config(['app.key' => $key, 'app.previous_keys' => $previousKeys]);
|
|
app()->forgetInstance('encrypter');
|
|
Crypt::clearResolvedInstance('encrypter');
|
|
}
|
|
|
|
function newAppKey(): string
|
|
{
|
|
return 'base64:'.base64_encode(random_bytes(32));
|
|
}
|
|
|
|
beforeEach(function () {
|
|
Queue::fake();
|
|
$this->oldKey = newAppKey();
|
|
$this->newKey = newAppKey();
|
|
useAppKeys($this->oldKey);
|
|
|
|
InstanceSettings::forceCreate(['id' => 0, 'smtp_password' => 'instance-smtp-password']);
|
|
$this->team = Team::factory()->create();
|
|
$this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
|
|
$this->server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]);
|
|
$this->server->settings->forceFill(['is_sentinel_enabled' => true])->saveQuietly();
|
|
$this->environmentVariable = EnvironmentVariable::create([
|
|
'key' => 'DATABASE_URL',
|
|
'value' => 'postgres://user:secret@db:5432/app',
|
|
'resourceable_type' => Application::class,
|
|
'resourceable_id' => Application::factory()->create()->id,
|
|
]);
|
|
$this->gitlabApp = GitlabApp::create([
|
|
'name' => 'GitLab',
|
|
'api_url' => 'https://gitlab.example.com/api/v4',
|
|
'html_url' => 'https://gitlab.example.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'webhook_token' => 'secret-webhook-token',
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
$this->oldSentinelToken = $this->server->settings->sentinel_token;
|
|
$this->privateKeyValue = $this->privateKey->private_key;
|
|
});
|
|
|
|
it('re-encrypts stored secrets so they decrypt without the previous key', function () {
|
|
useAppKeys($this->newKey, [$this->oldKey]);
|
|
|
|
$this->artisan('app-key:reencrypt --force')
|
|
->expectsOutputToContain('Regenerated 1 Sentinel tokens.')
|
|
->assertSuccessful();
|
|
StartSentinel::assertPushed();
|
|
|
|
useAppKeys($this->newKey);
|
|
|
|
expect(InstanceSettings::query()->find(0)->smtp_password)->toBe('instance-smtp-password')
|
|
->and($this->privateKey->fresh()->private_key)->toBe($this->privateKeyValue)
|
|
->and($this->environmentVariable->fresh()->value)->toBe('postgres://user:secret@db:5432/app')
|
|
->and($this->gitlabApp->fresh()->webhook_token)->toBe('secret-webhook-token');
|
|
|
|
$sentinelToken = $this->server->settings->fresh()->sentinel_token;
|
|
expect($sentinelToken)->not->toBe($this->oldSentinelToken)
|
|
->and(json_decode(decrypt($sentinelToken), true))->toBe(['server_uuid' => $this->server->uuid]);
|
|
});
|
|
|
|
it('changes nothing when every secret already uses the current key', function () {
|
|
useAppKeys($this->newKey, [$this->oldKey]);
|
|
$this->artisan('app-key:reencrypt --force')->assertSuccessful();
|
|
$rawPrivateKey = DB::table('private_keys')->where('id', $this->privateKey->id)->value('private_key');
|
|
|
|
$this->artisan('app-key:reencrypt --force')
|
|
->expectsOutputToContain('Re-encrypted 0 values.')
|
|
->expectsOutputToContain('Regenerated 0 Sentinel tokens.')
|
|
->assertSuccessful();
|
|
|
|
expect(DB::table('private_keys')->where('id', $this->privateKey->id)->value('private_key'))->toBe($rawPrivateKey);
|
|
});
|
|
|
|
it('reports old-key values in a dry run without changing them', function () {
|
|
$rawPrivateKey = DB::table('private_keys')->where('id', $this->privateKey->id)->value('private_key');
|
|
useAppKeys($this->newKey, [$this->oldKey]);
|
|
|
|
$this->artisan('app-key:reencrypt --dry-run')
|
|
->expectsOutputToContain('values and 1 Sentinel tokens use an old key. Nothing was changed.')
|
|
->assertSuccessful();
|
|
|
|
expect(DB::table('private_keys')->where('id', $this->privateKey->id)->value('private_key'))->toBe($rawPrivateKey)
|
|
->and($this->server->settings->fresh()->sentinel_token)->toBe($this->oldSentinelToken);
|
|
StartSentinel::assertNotPushed();
|
|
});
|
|
|
|
it('keeps legacy plaintext values as they are', function () {
|
|
DB::table('gitlab_apps')->where('id', $this->gitlabApp->id)->update(['webhook_token' => 'legacy-plaintext-token']);
|
|
useAppKeys($this->newKey, [$this->oldKey]);
|
|
|
|
$this->artisan('app-key:reencrypt --force')->assertSuccessful();
|
|
|
|
expect(DB::table('gitlab_apps')->where('id', $this->gitlabApp->id)->value('webhook_token'))->toBe('legacy-plaintext-token');
|
|
});
|
|
|
|
it('fails and keeps values that no configured key can decrypt', function () {
|
|
$rawPrivateKey = DB::table('private_keys')->where('id', $this->privateKey->id)->value('private_key');
|
|
useAppKeys($this->newKey);
|
|
|
|
$this->artisan('app-key:reencrypt --force')
|
|
->expectsOutputToContain('cannot be decrypted with APP_KEY or APP_PREVIOUS_KEYS')
|
|
->assertFailed();
|
|
|
|
expect(DB::table('private_keys')->where('id', $this->privateKey->id)->value('private_key'))->toBe($rawPrivateKey);
|
|
});
|
|
|
|
it('knows every model that encrypts data without an encrypted cast', function () {
|
|
// Reviewed: EnvironmentVariable encrypts its "value" column, which also has an encrypted cast.
|
|
// ServerSetting encrypts the Sentinel token, which the command regenerates.
|
|
$reviewed = [EnvironmentVariable::class, ServerSetting::class];
|
|
|
|
$manualEncryption = collect(Finder::create()->files()->in(app_path('Models'))->name('*.php'))
|
|
->filter(fn ($file) => preg_match('/\bCrypt::|(?<![\w>:$])(en|de)crypt\(/', $file->getContents()) === 1)
|
|
->map(fn ($file) => 'App\\Models\\'.str_replace(['/', '.php'], ['\\', ''], $file->getRelativePathname()))
|
|
->values()
|
|
->all();
|
|
|
|
expect(array_diff($manualEncryption, [...array_keys(ReencryptWithAppKey::ACCESSOR_ENCRYPTED_COLUMNS), ...$reviewed]))
|
|
->toBe([], 'Add the encrypted columns of these models to ReencryptWithAppKey::ACCESSOR_ENCRYPTED_COLUMNS.');
|
|
});
|