Fix-Runde 1: Host-Sperren gehen an jeden Betreiber mit hosts.manage
Koordinator-Entscheidung statt neuer Einstellung: der Empfaengerkreis ist per Definition richtig, weil es genau die Menschen sind, die eine Host-Sperre in der Konsole ueberhaupt aufheben duerfen, und er pflegt sich bei jedem Rollenwechsel von selbst mit. notifyHostManagers() nutzt Spaties eigenen Operator::permission()-Scope (dieselbe Pruefung wie authorize() an anderer Stelle, nur als Mengenabfrage) - kein Empfaenger heisst keine Mail, kein Fehler, keine Drossel-Markierung; jede Adresse einzeln in ihrem eigenen try/catch, damit ein abgelehntes Postfach nicht die uebrigen Betreiber um ihre Meldung bringt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/versandtakt
parent
2d979052d0
commit
17a07d1a68
|
|
@ -5,6 +5,7 @@ namespace App\Services\Security;
|
|||
use App\Mail\SecurityBlockMail;
|
||||
use App\Models\Host;
|
||||
use App\Models\Instance;
|
||||
use App\Models\Operator;
|
||||
use App\Models\SecurityBlock;
|
||||
use App\Support\ProvisioningSettings;
|
||||
use App\Support\Settings;
|
||||
|
|
@ -63,7 +64,7 @@ class BlockAddress
|
|||
return null;
|
||||
}
|
||||
|
||||
return $this->createBlock(
|
||||
$block = $this->createBlock(
|
||||
ip: $ip,
|
||||
attempts: $attempts,
|
||||
reason: 'host_ssh',
|
||||
|
|
@ -71,6 +72,10 @@ class BlockAddress
|
|||
hostId: $host->id,
|
||||
firewallHost: $host,
|
||||
);
|
||||
|
||||
$this->notifyHostManagers($host, $block);
|
||||
|
||||
return $block;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -80,10 +85,8 @@ class BlockAddress
|
|||
* eigenen Spalte: nach einer Stunde interessiert er niemanden mehr, und
|
||||
* eine Spalte dafür wäre außerhalb dieses einen Fensters immer bedeutungslos.
|
||||
*
|
||||
* Für Host-Sperren gibt es hier absichtlich noch kein Gegenstück: im Repo
|
||||
* findet sich kein Muster, wie eine Betreiber-Meldung ihren Empfänger
|
||||
* sucht (keine Konsolen-Rolle "bekommt Systemmails", keine Adresse dafür
|
||||
* in den Einstellungen) — das wäre eine eigene Entwurfsentscheidung.
|
||||
* Für Host-Sperren siehe `notifyHostManagers()` weiter unten — andere
|
||||
* Empfängerlogik (Betreiber statt Kunde), gleiche Drossel-Idee.
|
||||
*
|
||||
* Zustellung ist nicht die Bedingung für Schutz: die Sperre steht bereits,
|
||||
* bevor hier auch nur versucht wird zu verschicken, und die Drossel wird
|
||||
|
|
@ -112,6 +115,55 @@ class BlockAddress
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigt jeden AKTIVEN Betreiber mit `hosts.manage` — höchstens
|
||||
* einmal je Host und Stunde (`security.notified.host.<id>`, dieselbe
|
||||
* Drossel-Idee wie bei Instanzen, eigener Schlüssel).
|
||||
*
|
||||
* Nicht an eine feste Adresse und nicht an eine neue Einstellung: genau
|
||||
* diese Berechtigung entscheidet schon, wer eine Host-Sperre in der
|
||||
* Konsole überhaupt aufheben darf (siehe `HostDetail`, `HostTerminal`,
|
||||
* `ConfirmRemoveHost`, alle hinter `hosts.manage`) — eine Mail an jemand
|
||||
* anderen wäre Lärm, eine an jeden mit dieser Berechtigung ist per
|
||||
* Definition der richtige Empfängerkreis, und er pflegt sich von selbst
|
||||
* bei jedem Rollenwechsel mit (Koordinator-Entscheidung, siehe Bericht).
|
||||
*
|
||||
* Kein Empfänger ist kein Fehler: eine frische Installation ohne Admin
|
||||
* neben dem Owner, oder eine, in der `hosts.manage` gerade niemandem
|
||||
* zugewiesen ist, bekommt schlicht keine Mail — die Sperre selbst steht
|
||||
* trotzdem, unabhängig davon.
|
||||
*/
|
||||
private function notifyHostManagers(Host $host, SecurityBlock $block): void
|
||||
{
|
||||
$recipients = Operator::query()
|
||||
->whereNull('disabled_at')
|
||||
->permission('hosts.manage')
|
||||
->pluck('email');
|
||||
|
||||
if ($recipients->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$key = 'security.notified.host.'.$host->id;
|
||||
$last = Settings::get($key);
|
||||
|
||||
if ($last !== null && now()->subHour()->lt($last)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Settings::set($key, now()->toIso8601String());
|
||||
|
||||
// Einzeln umschlossen: ein Postfach, das ablehnt, darf die übrigen
|
||||
// Betreiber nicht auch noch um ihre Meldung bringen.
|
||||
foreach ($recipients as $email) {
|
||||
try {
|
||||
Mail::to($email)->queue(new SecurityBlockMail($block));
|
||||
} catch (Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Läuft für diese Adresse an diesem Subjekt schon eine Sperre? */
|
||||
private function hasActiveBlock(string $ip, ?int $instanceId, ?int $hostId): bool
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
// tests/Feature/Security/SecurityBlockMailTest.php
|
||||
|
||||
use App\Mail\SecurityBlockMail;
|
||||
use App\Models\Host;
|
||||
use App\Models\Instance;
|
||||
use App\Models\Mailbox;
|
||||
use App\Models\SecurityBlock;
|
||||
|
|
@ -67,3 +68,30 @@ it('geht standardmaessig aus dem System-Postfach und folgt der Wegwahl', functio
|
|||
|
||||
expect((new SecurityBlockMail($block))->envelope()->from->address)->toBe($info->address);
|
||||
});
|
||||
|
||||
// ---- Host-Sperren: an jeden aktiven Betreiber mit hosts.manage (Koordinator-Entscheidung, Fix-Runde 1) ----
|
||||
|
||||
it('schickt bei einer Host-Sperre an jeden Betreiber, der Hosts verwalten darf', function () {
|
||||
Mail::fake();
|
||||
// Owner und Admin haben hosts.manage (seed_roles_and_permissions), Support
|
||||
// nicht — siehe database/migrations/2026_07_25_133900_seed_roles_and_permissions.php.
|
||||
$darf = operator('Admin');
|
||||
$darfNicht = operator('Support');
|
||||
$host = Host::factory()->create();
|
||||
|
||||
app(BlockAddress::class)->forHost($host, '203.0.113.7', 12);
|
||||
|
||||
// Beide Haelften: der Zustaendige bekommt sie, der Unzustaendige nicht.
|
||||
Mail::assertQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($darf->email));
|
||||
Mail::assertNotQueued(SecurityBlockMail::class, fn ($m) => $m->hasTo($darfNicht->email));
|
||||
});
|
||||
|
||||
it('sperrt auch dann, wenn es keinen zustaendigen Betreiber gibt', function () {
|
||||
Mail::fake();
|
||||
// Keine Betreiber mit hosts.manage — kein Empfaenger, kein Fehler, aber
|
||||
// sehr wohl eine Sperre. Schutz haengt nicht an Zustellung.
|
||||
$host = Host::factory()->create();
|
||||
|
||||
expect(app(BlockAddress::class)->forHost($host, '203.0.113.7', 12))->not->toBeNull();
|
||||
Mail::assertNothingQueued();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue