Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions app/Filament/Admin/Pages/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use App\Extensions\Avatar\AvatarService;
use App\Extensions\Captcha\CaptchaService;
use App\Extensions\OAuth\OAuthService;
use App\Facades\Activity;
use App\Notifications\MailTested;
use App\Traits\EnvironmentWriterTrait;
use App\Traits\Filament\CanCustomizeHeaderActions;
Expand Down Expand Up @@ -979,8 +980,16 @@ public function save(): void
return $value;
}, $data);

$changes = $this->buildSettingsDiff($data);

$this->writeToEnvironment($data);

if ($changes !== []) {
Activity::event('settings:update')
->property('changes', $changes)
->log();
}

Artisan::call('queue:restart');

$this->redirect($this->getUrl());
Expand All @@ -998,6 +1007,47 @@ public function save(): void
}
}

/**
* Old-to-new pairs for every env key actually changing, secrets masked.
* Snapshotted through env() before the write, since the loaded environment
* still holds the previous values at that point.
*
* @param array<string, mixed> $data
* @return array<string, array{old: string|null, new: string|null}>
*/
private function buildSettingsDiff(array $data): array
{
$changes = [];

foreach ($data as $key => $value) {
$old = env($key);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if (is_bool($old)) {
$old = $old ? 'true' : 'false';
}

$old = is_null($old) ? null : (string) $old;
$new = match (true) {
is_null($value) => null,
is_array($value) => implode(',', $value),
default => (string) $value,
};

if ($old === $new) {
continue;
}

if (Str::is(['*SECRET*', '*PASSWORD*', '*TOKEN*', '*_KEY'], $key)) {
$old = is_null($old) || $old === '' ? $old : '********';
$new = is_null($new) || $new === '' ? $new : '********';
}

$changes[$key] = ['old' => $old, 'new' => $new];
}

return $changes;
}

/** @return array<Action|ActionGroup> */
protected function getDefaultHeaderActions(): array
{
Expand Down
3 changes: 3 additions & 0 deletions lang/en/activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
'delete' => 'Disabled two-factor auth',
],
],
'settings' => [
'update' => 'Updated <b>:count</b> panel setting|Updated <b>:count</b> panel settings',
],
'server' => [
'console' => [
'command' => 'Executed "<b>:command</b>" on the server',
Expand Down
42 changes: 42 additions & 0 deletions tests/Filament/Admin/SettingsTest.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<?php

use App\Events\ActivityLogged;
use App\Filament\Admin\Pages\Settings;
use App\Models\Role;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Event;

use function Pest\Livewire\livewire;

Expand All @@ -26,6 +28,46 @@
livewire(Settings::class)->assertForbidden();
});

it('logs a settings:update event with a redacted diff', function () {
$path = sys_get_temp_dir().'/pelican-settings-test-'.getmypid();
@mkdir($path);
file_put_contents("$path/.env", '');
app()->useEnvironmentPath($path);

try {
livewire(Settings::class)
->fillForm([
'APP_NAME' => 'Audited Panel',
'FILAMENT_AVATAR_PROVIDER' => 'gravatar',
// smtp keeps the MAIL_PASSWORD field visible so it lands in the diff
'MAIL_MAILER' => 'smtp',
'MAIL_HOST' => 'localhost',
'MAIL_PORT' => 2525,
'MAIL_SCHEME' => 'smtp',
'MAIL_PASSWORD' => 'supersecretpw',
'GUZZLE_CONNECT_TIMEOUT' => 5,
])
->call('save')
->assertHasNoErrors();

$this->assertActivityLogged('settings:update');
$this->assertActivityActor('settings:update', $this->admin);
Event::assertDispatched(ActivityLogged::class, function (ActivityLogged $e) {
if (!$e->is('settings:update')) {
return false;
}
$changes = $e->model->properties['changes'];

return $changes['APP_NAME']['new'] === 'Audited Panel'
&& $changes['MAIL_PASSWORD']['new'] === '********'
&& !str_contains(json_encode($e->model->properties), 'supersecretpw');
});
} finally {
@unlink("$path/.env");
@rmdir($path);
}
});

it('persists saved settings to the environment file', function () {
// Point the environment file at a scratch copy so the real .env is untouched.
$path = sys_get_temp_dir().'/pelican-settings-test-'.getmypid();
Expand Down
Loading