109 lines
4.8 KiB
PHP
109 lines
4.8 KiB
PHP
<?php
|
|
|
|
use App\Support\NextcloudOcc;
|
|
use Illuminate\Support\Facades\File;
|
|
|
|
/**
|
|
* Nothing runs inside a container without saying who it is running as.
|
|
*
|
|
* `docker compose exec` is root unless told otherwise, and the deployment
|
|
* scripts relied on that default. When `artisan optimize` failed during a
|
|
* deployment it wrote its error into storage/logs/laravel.log AS ROOT — and
|
|
* from that moment the application could not append to its own log. Monolog
|
|
* threw on every attempt, and a throw while logging is a 500 on every page that
|
|
* logs, with nothing written down to say why.
|
|
*
|
|
* It surfaced days later as a 500 on the VPN config download, that being one of
|
|
* the few pages writing a log line on its way through, and was chased through
|
|
* the VPN code, the encryption key and the session before anyone looked at the
|
|
* owner of a file.
|
|
*
|
|
* docker/entrypoint.sh had it right all along: it drops to www-data for exactly
|
|
* these commands. The rule is only that the user is named — `-u root` is a fine
|
|
* answer where root is what is wanted (the ownership repair in update.sh), as
|
|
* long as somebody chose it.
|
|
*/
|
|
it('never runs a command in the app container as whoever docker felt like', function () {
|
|
$offenders = [];
|
|
|
|
foreach (File::glob(base_path('deploy/*.sh')) as $path) {
|
|
foreach (preg_split('/\R/', File::get($path)) ?: [] as $i => $line) {
|
|
if (! str_contains($line, 'docker compose exec')) {
|
|
continue;
|
|
}
|
|
|
|
// Only the app container. The gateway and the queues are other
|
|
// images with other users, and none of them own this checkout.
|
|
if (! preg_match('/docker compose exec\b[^|]*?\bapp\b/', $line)) {
|
|
continue;
|
|
}
|
|
|
|
if (preg_match('/\s-u\s+\S+/', $line)) {
|
|
continue;
|
|
}
|
|
|
|
// Help text counts. Telling an operator to run it as root is how
|
|
// the file ends up owned by root in the first place.
|
|
$offenders[] = basename($path).':'.($i + 1).' — '.trim($line);
|
|
}
|
|
}
|
|
|
|
expect($offenders)->toBe([]);
|
|
});
|
|
|
|
it('never runs occ in a customer guest as whoever docker felt like either', function () {
|
|
// The same default, the same lesson, a different subsystem — and it was learned
|
|
// here second. `docker compose exec` is root, the official Nextcloud image's
|
|
// console.php exits 1 unless the caller owns config/config.php (www-data), and
|
|
// the prefix was pasted into five separate files, so every occ call in the
|
|
// product was failing on every instance. The ones that go through
|
|
// CustomerStep::guest() turned that into a retry and then a failed run for a
|
|
// machine that was otherwise finished.
|
|
//
|
|
// So there is one builder, and this refuses a second: nothing in app/ may spell
|
|
// the invocation out by hand, which is the only way a sixth call site can be
|
|
// stopped from getting it wrong again.
|
|
$offenders = [];
|
|
|
|
foreach (File::allFiles(app_path()) as $file) {
|
|
if ($file->getExtension() !== 'php'
|
|
|| $file->getRelativePathname() === 'Support/NextcloudOcc.php') {
|
|
continue;
|
|
}
|
|
|
|
if (str_contains(File::get($file->getRealPath()), 'docker compose exec')) {
|
|
$offenders[] = $file->getRelativePathname();
|
|
}
|
|
}
|
|
|
|
expect($offenders)->toBe([])
|
|
// And the one builder names the user Nextcloud insists on.
|
|
->and(NextcloudOcc::command('status'))
|
|
->toContain('docker compose exec -T -u www-data ');
|
|
});
|
|
|
|
it('repairs ownership before it needs it, not after', function () {
|
|
// A server already carrying the damage has to heal on its next deployment.
|
|
// Nothing else ever will: a root-owned log file stays root-owned, and the
|
|
// page that trips over it is nowhere near the deployment that caused it.
|
|
$update = File::get(base_path('deploy/update.sh'));
|
|
|
|
expect($update)->toContain('normalise_ownership')
|
|
// Every directory the deployment writes into, and node_modules by name:
|
|
// the first version sampled the top-level owner and skipped the
|
|
// recursion when it matched, which is how a root-owned
|
|
// node_modules/.vite-temp under a www-data-owned node_modules failed
|
|
// the build in maintenance mode.
|
|
->and($update)->toContain('! -user www-data -exec chown www-data:www-data')
|
|
->and($update)->toContain('node_modules');
|
|
|
|
// Before the first unprivileged step, or it cannot help: composer and npm
|
|
// would already have failed on a root-owned vendor directory.
|
|
$repair = strpos($update, "\nnormalise_ownership\n");
|
|
$maintenance = strpos($update, 'php artisan down --retry=60');
|
|
|
|
expect($repair)->not->toBeFalse()
|
|
->and($maintenance)->not->toBeFalse()
|
|
->and($repair)->toBeLessThan($maintenance);
|
|
});
|