116 lines
4.9 KiB
PHP
116 lines
4.9 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Admin\Finance;
|
|
use App\Models\ExportTarget;
|
|
use App\Services\Deployment\UpdateChannel;
|
|
use Illuminate\Support\Facades\File;
|
|
use Livewire\Livewire;
|
|
|
|
/**
|
|
* Minting the key a NAS collects the archive with, from the console.
|
|
*
|
|
* Everything it replaces was done by hand across three machines: generate a
|
|
* keypair, install the public half restricted to one directory, carry the
|
|
* private half over. None of it can happen in the panel — the keys, the home
|
|
* directory and rrsync are all on the host, and the panel is www-data inside a
|
|
* container. So it asks, exactly as it does for an update.
|
|
*/
|
|
beforeEach(function () {
|
|
File::deleteDirectory(storage_path('app/deploy'));
|
|
|
|
$this->target = ExportTarget::create([
|
|
'name' => 'Übergabe', 'driver' => ExportTarget::LOCAL,
|
|
'path' => '/opt/clupilot/storage/archive', 'layout' => ExportTarget::BY_DAY, 'active' => true,
|
|
]);
|
|
});
|
|
|
|
it('asks the host for a key, and names the directory it may read', function () {
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Finance::class)
|
|
->call('createPullAccess', $this->target->uuid)
|
|
->assertSet('waitingForKey', true);
|
|
|
|
$request = json_decode(File::get(storage_path('app/deploy/update-request.json')), true);
|
|
|
|
expect($request['kind'])->toBe('archive-key')
|
|
// The path travels with the request: the panel is the only side that
|
|
// knows which destination the button was pressed on.
|
|
->and($request['archive_path'])->toBe('/opt/clupilot/storage/archive');
|
|
});
|
|
|
|
it('refuses for a destination that is not on this host', function () {
|
|
// Only a directory here has a home directory and an authorized_keys to put
|
|
// anything into. Anywhere else is somebody else's machine.
|
|
$remote = ExportTarget::create([
|
|
'name' => 'Anbieter', 'driver' => ExportTarget::SFTP, 'path' => '/rechnungen',
|
|
'host' => 'backup.example.com', 'port' => 22, 'username' => 'u1', 'active' => true,
|
|
]);
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Finance::class)
|
|
->call('createPullAccess', $remote->uuid)
|
|
->assertSet('waitingForKey', false);
|
|
|
|
expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse();
|
|
});
|
|
|
|
it('shows the minted key once and takes it off the disk in the same breath', function () {
|
|
// A private key that stays on disk after somebody has seen it is a private
|
|
// key on disk.
|
|
File::ensureDirectoryExists(storage_path('app/deploy'));
|
|
File::put(storage_path('app/deploy/archive-key.json'), json_encode([
|
|
'created_at' => now()->toIso8601String(),
|
|
'label' => 'clupilot-archiv',
|
|
'path' => '/opt/clupilot/storage/archive',
|
|
'private_key' => "-----BEGIN OPENSSH PRIVATE KEY-----\nAAAA\n-----END OPENSSH PRIVATE KEY-----",
|
|
]));
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Finance::class)
|
|
->set('waitingForKey', true)
|
|
->call('collectArchiveKey')
|
|
->assertSet('waitingForKey', false)
|
|
->assertSee('BEGIN OPENSSH PRIVATE KEY');
|
|
|
|
expect(File::exists(storage_path('app/deploy/archive-key.json')))->toBeFalse();
|
|
});
|
|
|
|
it('never writes the key into the database', function () {
|
|
// It is shown to be copied into a NAS and nowhere else. Storing it "for
|
|
// convenience" would put a working credential into every backup of this
|
|
// database.
|
|
File::ensureDirectoryExists(storage_path('app/deploy'));
|
|
File::put(storage_path('app/deploy/archive-key.json'), json_encode([
|
|
'created_at' => now()->toIso8601String(), 'label' => 'x',
|
|
'path' => '/opt/clupilot/storage/archive', 'private_key' => 'GEHEIM-XYZ',
|
|
]));
|
|
|
|
Livewire::actingAs(operator('Owner'), 'operator')
|
|
->test(Finance::class)
|
|
->set('waitingForKey', true)
|
|
->call('collectArchiveKey');
|
|
|
|
foreach (ExportTarget::all() as $target) {
|
|
expect(json_encode($target->getAttributes()))->not->toContain('GEHEIM-XYZ');
|
|
}
|
|
|
|
expect(json_encode(App\Models\ExportTarget::query()->get()->toArray()))->not->toContain('GEHEIM-XYZ');
|
|
});
|
|
|
|
it('locks the key to one directory, read-only, with no shell', function () {
|
|
// The agent's half, which no PHP test can reach. rrsync rather than pinning
|
|
// the exact rsync option string: that string differs between rsync versions
|
|
// and breaks silently, as a refusal with no reason given.
|
|
$agent = File::get(base_path('deploy/update-agent.sh'));
|
|
|
|
expect($agent)->toContain('archive-key')
|
|
->and($agent)->toContain('/usr/bin/rrsync -ro')
|
|
->and($agent)->toContain(',restrict ')
|
|
// Refuses rather than issuing an UNrestricted key when rrsync is absent.
|
|
->and($agent)->toContain('rrsync_missing');
|
|
|
|
// And the installer puts rsync on the host, because sshd starts
|
|
// `rsync --server` HERE when the NAS connects.
|
|
expect(File::get(base_path('deploy/install-agent.sh')))->toContain('install -y -qq rsync');
|
|
});
|