mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-17 09:05:08 +00:00
85b1650ef0
The dashboard's System card now says so when no scanner is configured at all, not only when a configured one is failing: "Anything uploaded here — by staff, by clients, or through an upload link — is passed on unchecked", with a link to set it up. Said only where somebody can act on it. Connecting a scanner is a new capability, scanning.connect, community only — on a hosted installation the scanner is infrastructure the platform runs, so its address is not a tenant's to set and its absence is not a tenant's to fix. The two policies stay on both editions, because what to do with a file nobody could scan is a decision about somebody's own files. An edition difference through the registry, never an edition check. Also: PROJECTSEND_SCANNER_DEFAULT_ADDRESS, seeded into the settings on first boot by the command that already does this for two-factor enforcement. It is the opposite of PROJECTSEND_SCANNER_ADDRESS — a starting value rather than a policy, so a Docker install that brings up the optional scanner container arrives configured while the address and the switch stay on the settings screen. Both are seeded together or neither: an address with scanning off would look configured and check nothing. Nothing changes for an existing installation on upgrade: scanning stays off, existing files are marked "never scanned", and the scanner container is still opt-in.
124 lines
5.0 KiB
PHP
124 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Modules\Platform\Settings\Console;
|
|
|
|
use App\Modules\Identity\TwoFactor\TwoFactorEnforcement;
|
|
use App\Modules\Platform\Settings\Setting;
|
|
use App\Modules\Platform\Settings\Settings;
|
|
use App\Modules\Platform\Settings\StoredSetting;
|
|
use Illuminate\Console\Command;
|
|
|
|
/**
|
|
* Seed a setting from the environment, once, at provision.
|
|
*
|
|
* Settings live in the database because they belong to whoever runs the
|
|
* installation. A managed one has a moment before anybody runs it — the
|
|
* first boot, where the entrypoint already creates the first administrator
|
|
* from ADMIN_* — and a policy that has to exist before the account it
|
|
* protects has to be written there or not at all.
|
|
*
|
|
* The case this exists for is two-factor enforcement. A platform wants it
|
|
* on before the first seat, and the first seat is created in that same
|
|
* boot. Left to a control plane calling in afterwards, there is a window
|
|
* between the account existing and the policy covering it.
|
|
*
|
|
* ### Seeded, not overridden
|
|
*
|
|
* Writing only when nothing has been stored is the whole design. A value
|
|
* that won on every boot would take the setting away from the
|
|
* administrator it belongs to, and somebody who tightened it would find it
|
|
* loosened again by a restart. Same shape as `projectsend:admin --if-none`,
|
|
* which runs a line below this one in the entrypoint.
|
|
*
|
|
* ### Read through config, never env() directly
|
|
*
|
|
* `config:cache` stops `.env` being read at all, which is exactly how
|
|
* TRUSTED_PROXIES came to have no effect on any web request while looking
|
|
* correct in the file. Anything an operator sets has to arrive through a
|
|
* config key or it works until somebody optimises the install.
|
|
*
|
|
* ### Deliberately not general
|
|
*
|
|
* No `PROJECTSEND_SETTING_<KEY>` mechanism. Every setting reachable from
|
|
* outside is a setting whose value depends on where you look, and the
|
|
* blast radius of getting that wrong is the whole settings table. One
|
|
* named key per setting that needs it, added when it needs it.
|
|
*/
|
|
class SeedSettingsCommand extends Command
|
|
{
|
|
protected $signature = 'projectsend:seed-settings';
|
|
|
|
protected $description = 'Apply provisioning defaults from the environment to settings that have never been set';
|
|
|
|
public function handle(Settings $settings): int
|
|
{
|
|
$enforcement = config('projectsend.platform.two_factor_enforcement');
|
|
|
|
if (is_string($enforcement) && $enforcement !== '') {
|
|
$this->seedTwoFactorEnforcement($settings, $enforcement);
|
|
}
|
|
|
|
$scanner = config('projectsend.scanning.default_address');
|
|
|
|
if (is_string($scanner) && trim($scanner) !== '') {
|
|
$this->seedScanner($settings, trim($scanner));
|
|
}
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* Point a fresh installation at its scanner, and switch scanning on.
|
|
*
|
|
* For the operator who brings up the optional scanner container beside
|
|
* the application: without this they would have to find the settings
|
|
* screen and type an address the compose file already knows. Unlike
|
|
* PROJECTSEND_SCANNER_ADDRESS this leaves both the address and the
|
|
* switch editable afterwards — it is a starting value, not a policy.
|
|
*
|
|
* Both are seeded together or neither: an address with scanning off
|
|
* would look configured and check nothing, and scanning on with no
|
|
* address would hold every upload.
|
|
*/
|
|
private function seedScanner(Settings $settings, string $address): void
|
|
{
|
|
// The address, asked of the table for the reason given below: its
|
|
// default is the empty string, so get() cannot tell "never set"
|
|
// from "deliberately cleared".
|
|
if (StoredSetting::query()->where('key', Setting::VirusScannerAddress->value)->exists()) {
|
|
return;
|
|
}
|
|
|
|
$settings->set(Setting::VirusScannerAddress, $address);
|
|
$settings->set(Setting::VirusScanningEnabled, true);
|
|
|
|
$this->info("Virus scanning seeded to '{$address}' and switched on (first boot).");
|
|
}
|
|
|
|
private function seedTwoFactorEnforcement(Settings $settings, string $value): void
|
|
{
|
|
if (TwoFactorEnforcement::tryFrom($value) === null) {
|
|
// Named rather than ignored. A typo here means a tenant
|
|
// provisioned without the policy it was meant to have, and
|
|
// silence would make that indistinguishable from success.
|
|
$this->warn("PROJECTSEND_TWO_FACTOR_ENFORCEMENT='{$value}' is not one of none, staff, clients, all — leaving the setting alone.");
|
|
|
|
return;
|
|
}
|
|
|
|
// Asked of the table rather than of Settings::get(), which cannot
|
|
// tell a stored value apart from the enum's own default — and
|
|
// 'none' is that default, so get() would report the thing we are
|
|
// trying to detect the absence of.
|
|
if (StoredSetting::query()->where('key', Setting::TwoFactorEnforcement->value)->exists()) {
|
|
return;
|
|
}
|
|
|
|
$settings->set(Setting::TwoFactorEnforcement, $value);
|
|
|
|
$this->info("Two-factor enforcement seeded to '{$value}' (first boot).");
|
|
}
|
|
}
|