CluPilotCloud/tests/Feature/Billing/ArchivePullAccessTest.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');
});