mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 09:05:08 +00:00
d53bb9a2f7
The base php:*-fpm image creates /var/www/html owned by its own www-data (uid 82) and mode 1777, so that an image can run as an arbitrary user. This image replaces www-data with a fixed uid 1000 and copies the release in with COPY --chown — which re-owns what it copies into the directory, never the directory itself. It was left world-writable, sticky, and owned by a uid the container no longer has. fs.protected_symlinks — on by default on Ubuntu, Debian and most current distributions — then refuses to let a non-root process follow a symlink in such a directory, and .env is exactly that: the entrypoint keeps it on the storage volume so a generated APP_KEY survives container replacement, and links it into place. So every request 503'd with "ProjectSend is not configured yet" while `docker exec ... cat .env`, run as root, printed the file back perfectly (#1615). Three changes, each independently sufficient for the reported case, and deliberately so — this failure is silent and its symptom points away from its cause: - the image owns /var/www/html as the runtime user, at mode 755; - the entrypoint owns the symlink it creates, so it stays followable even if that directory's mode ever drifts back; - preflight distinguishes "no .env" from ".env is there and cannot be read", instead of reporting the second as the first and sending the operator off to create a file they already have. Verified by building the production image before and after: every request 503s beforehand, with /var/www/html at uid 82 mode 1777 and www-data denied on the symlink while root reads it; afterwards /up answers 200, the container reports healthy, and / redirects to /setup. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
111 lines
4.5 KiB
PHP
111 lines
4.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
// Load the guard without letting it auto-run against the real environment.
|
|
define('PROJECTSEND_PREFLIGHT_TEST', true);
|
|
require_once __DIR__.'/../../bootstrap/preflight.php';
|
|
|
|
/**
|
|
* A fixture directory, optionally with a .env whose APP_KEY line is $appKey
|
|
* (null = no APP_KEY line at all; '' = present but empty).
|
|
*/
|
|
function preflightFixture(?string $appKey, bool $withEnvFile): string
|
|
{
|
|
$dir = sys_get_temp_dir().'/preflight-'.bin2hex(random_bytes(6));
|
|
mkdir($dir);
|
|
|
|
if ($withEnvFile) {
|
|
$lines = ['APP_ENV=production'];
|
|
if ($appKey !== null) {
|
|
$lines[] = "APP_KEY=$appKey";
|
|
}
|
|
$lines[] = 'DB_CONNECTION=mysql';
|
|
file_put_contents($dir.'/.env', implode("\n", $lines)."\n");
|
|
}
|
|
|
|
return $dir;
|
|
}
|
|
|
|
beforeEach(function (): void {
|
|
// The real environment must not leak into these checks.
|
|
putenv('APP_KEY');
|
|
unset($_SERVER['APP_KEY'], $_ENV['APP_KEY']);
|
|
});
|
|
|
|
it('passes when APP_KEY comes from the environment, with no .env file', function (): void {
|
|
putenv('APP_KEY=base64:'.base64_encode(random_bytes(32)));
|
|
|
|
expect(projectsend_preflight_failure(preflightFixture(null, false)))->toBeNull();
|
|
});
|
|
|
|
it('passes when APP_KEY is set in the .env file', function (): void {
|
|
$dir = preflightFixture('base64:'.base64_encode(random_bytes(32)), true);
|
|
|
|
expect(projectsend_preflight_failure($dir))->toBeNull();
|
|
});
|
|
|
|
it('fails when there is no .env file and no APP_KEY in the environment', function (): void {
|
|
$failure = projectsend_preflight_failure(preflightFixture(null, false));
|
|
|
|
expect($failure)->not->toBeNull()
|
|
->and($failure[1])->toContain('No <code>.env</code> file was found')
|
|
->and(projectsend_preflight_command($failure[2]))->toContain('cp .env.example .env')
|
|
->and(projectsend_preflight_command($failure[2]))->toContain('php artisan key:generate');
|
|
});
|
|
|
|
it('fails when a .env exists but its APP_KEY is empty', function (): void {
|
|
$failure = projectsend_preflight_failure(preflightFixture('', true));
|
|
|
|
expect($failure)->not->toBeNull()
|
|
->and($failure[1])->toContain('APP_KEY</code> is empty')
|
|
->and(projectsend_preflight_command($failure[2]))->toContain('php artisan key:generate')
|
|
->and(projectsend_preflight_command($failure[2]))->not->toContain('cp .env.example');
|
|
});
|
|
|
|
it('fails when a .env exists with no APP_KEY line at all', function (): void {
|
|
// No APP_KEY line is the same "empty key" case to the operator.
|
|
expect(projectsend_preflight_failure(preflightFixture(null, true)))->not->toBeNull();
|
|
});
|
|
|
|
it('says so when .env is a symlink it cannot follow, rather than that none exists', function (): void {
|
|
// The official image symlinks .env into storage/. When that link cannot
|
|
// be followed — a missing target, or a directory whose permissions stop
|
|
// the web server user traversing it — every stat says "no such file",
|
|
// and "copy .env.example" is then exactly the wrong thing to be told.
|
|
$dir = preflightFixture(null, false);
|
|
symlink($dir.'/storage/.env', $dir.'/.env');
|
|
|
|
$failure = projectsend_preflight_failure($dir);
|
|
|
|
expect($failure)->not->toBeNull()
|
|
->and($failure[0])->toBe('ProjectSend cannot read its configuration')
|
|
->and($failure[1])->toContain('cannot read it')
|
|
->and(projectsend_preflight_command($failure[2]))->toContain('ls -ln .env')
|
|
->and(projectsend_preflight_command($failure[2]))->not->toContain('cp .env.example');
|
|
});
|
|
|
|
it('says so when .env exists but its permissions deny the reader', function (): void {
|
|
$dir = preflightFixture('base64:'.base64_encode(random_bytes(32)), true);
|
|
chmod($dir.'/.env', 0000);
|
|
clearstatcache();
|
|
|
|
$failure = projectsend_preflight_failure($dir);
|
|
|
|
expect($failure)->not->toBeNull()
|
|
->and($failure[0])->toBe('ProjectSend cannot read its configuration');
|
|
})->skip(fn (): bool => function_exists('posix_geteuid') && posix_geteuid() === 0, 'root can read anything, so the mode says nothing');
|
|
|
|
it('reads a quoted APP_KEY the way Dotenv would', function (): void {
|
|
$dir = preflightFixture('"base64:'.base64_encode(random_bytes(32)).'"', true);
|
|
|
|
expect(projectsend_preflight_failure($dir))->toBeNull();
|
|
});
|
|
|
|
it('lets the real environment override an empty .env key', function (): void {
|
|
// Docker injects APP_KEY without ever writing it into the file.
|
|
putenv('APP_KEY=base64:'.base64_encode(random_bytes(32)));
|
|
|
|
expect(projectsend_preflight_failure(preflightFixture('', true)))->toBeNull();
|
|
});
|