Stop the mailbox password save from crashing without SECRETS_KEY

EditMailbox::save() now checks SecretCipher::isUsable() before touching a
typed password and reports it as a form error, instead of letting
SecretCipher::encrypt() throw all the way to Laravel's debug page. Also
switches the mail.manage migration to Role::findOrCreate, matching every
sibling capability migration, so a squashed replay can't hit
RoleDoesNotExist.
feat/mailboxes
nexxo 2026-07-28 02:22:36 +02:00
parent 4211f3dfab
commit 67a3e78cb4
3 changed files with 33 additions and 1 deletions

View File

@ -3,6 +3,7 @@
namespace App\Livewire;
use App\Models\Mailbox;
use App\Services\Secrets\SecretCipher;
use LivewireUI\Modal\ModalComponent;
/**
@ -57,6 +58,18 @@ class EditMailbox extends ModalComponent
'password' => ['nullable', 'string', 'max:255'],
]);
// A password can only be STORED where SECRETS_KEY exists to encrypt it
// under — the same condition the page's own banner already names.
// Checked here rather than left for Mailbox::setPasswordAttribute() to
// throw through: an uncaught RuntimeException would render Laravel's
// debug page, whose request-payload inspector can show the very
// password just typed. The page must say so, not crash on it.
if ($this->password !== '' && ! app(SecretCipher::class)->isUsable()) {
$this->addError('password', __('mail_settings.no_key'));
return;
}
$box = Mailbox::query()->where('uuid', $this->uuid)->firstOrFail();
$box->address = $this->address;

View File

@ -21,7 +21,7 @@ return new class extends Migration
$permission = Permission::findOrCreate('mail.manage', 'web');
foreach (['Owner', 'Admin', 'Support'] as $role) {
Role::findByName($role, 'web')->givePermissionTo($permission);
Role::findOrCreate($role, 'web')->givePermissionTo($permission);
}
app(PermissionRegistrar::class)->forgetCachedPermissions();

View File

@ -97,6 +97,25 @@ it('says so when SECRETS_KEY is missing, instead of crashing on the first passwo
->assertSee(__('mail_settings.no_key'));
});
it('fails the password save with a form error, not an exception, when SECRETS_KEY is missing', function () {
// The test above only proves the PAGE says so. Nothing proved that typing
// a password into the MODAL and saving it fails gracefully — the gap a
// reviewer found: left unfixed, SecretCipher::encrypt() throws all the way
// up, and with APP_DEBUG on, Laravel's debug page can put the just-typed
// plaintext password in its request-payload inspector. All three
// password-writing tests above run under this file's valid-key beforeEach,
// so none of them would have caught it.
$box = Mailbox::factory()->create(['key' => 'support']);
config()->set('admin_access.secrets_key', '');
Livewire::actingAs(User::factory()->operator('Owner')->create())
->test(EditMailbox::class, ['uuid' => $box->uuid])
->set('password', 'neu-und-sicher')
->call('save')
->assertHasErrors('password');
});
it('refuses to leave the system purpose empty, because it is the fallback', function () {
Mailbox::factory()->create(['key' => 'no-reply']);
Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');