fix(security): console and account checks now survive a Livewire action
Livewire re-applies only the middleware on its persistent list when an action posts to /livewire/update. Its defaults cover auth; ours were not on the list — only RestrictAdminHost had been added. So EnsureAdmin and EnsureCustomerActive guarded the PAGE and not the actions, and the page is not where the actions run: a signed-in non-operator who could reach the admin host could drive console components, and a suspended customer could keep driving the portal. Found by Codex while reviewing the domain-separation design, and confirmed against Livewire's own default list in vendor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/portal-design tested-20260726-1819-5c2e481
parent
5d64da1cb3
commit
5c2e481985
|
|
@ -17,6 +17,8 @@ use App\Services\Traefik\SshTraefikWriter;
|
|||
use App\Services\Traefik\TraefikWriter;
|
||||
use App\Services\Wireguard\LocalWireguardHub;
|
||||
use App\Services\Wireguard\WireguardHub;
|
||||
use App\Http\Middleware\EnsureAdmin;
|
||||
use App\Http\Middleware\EnsureCustomerActive;
|
||||
use App\Http\Middleware\RestrictAdminHost;
|
||||
use App\Mail\MaintenanceCancelledMail;
|
||||
use App\Models\MaintenanceNotification;
|
||||
|
|
@ -57,7 +59,17 @@ class AppServiceProvider extends ServiceProvider
|
|||
// path-based guard would skip. Marking the host restriction persistent
|
||||
// makes Livewire re-apply it from the component's original route, so an
|
||||
// admin action cannot be driven through a public hostname.
|
||||
Livewire::addPersistentMiddleware(RestrictAdminHost::class);
|
||||
// Livewire re-applies only the middleware on this list when an action
|
||||
// posts to /livewire/update. Its own defaults cover `auth` — but not
|
||||
// ours. Without these two, a signed-in NON-operator could drive console
|
||||
// components, and a suspended customer could keep driving the portal:
|
||||
// the page would never load for them, but the page is not where the
|
||||
// actions run.
|
||||
Livewire::addPersistentMiddleware([
|
||||
RestrictAdminHost::class,
|
||||
EnsureAdmin::class,
|
||||
EnsureCustomerActive::class,
|
||||
]);
|
||||
|
||||
// Send-time guard for maintenance mail (X-CP-Notification carries the
|
||||
// ledger id). Deliberately READ-ONLY — it never marks the row sent or
|
||||
|
|
|
|||
|
|
@ -44,3 +44,16 @@ it('lets Support retry provisioning but not manage datacenters', function () {
|
|||
// ...but cannot manage datacenters.
|
||||
Livewire::actingAs(operator('Support'))->test(Datacenters::class)->call('toggle', 'x')->assertForbidden();
|
||||
});
|
||||
|
||||
it('replays the operator and account checks on every Livewire action', function () {
|
||||
// Livewire re-applies only the middleware on this list when an action posts
|
||||
// to /livewire/update, and its defaults cover auth but not ours. Without
|
||||
// these, the console page would refuse a non-operator while the console's
|
||||
// actions still answered them — the page is not where the actions run.
|
||||
$persistent = app(\Livewire\Mechanisms\PersistentMiddleware\PersistentMiddleware::class)
|
||||
->getPersistentMiddleware();
|
||||
|
||||
expect($persistent)->toContain(\App\Http\Middleware\EnsureAdmin::class)
|
||||
->and($persistent)->toContain(\App\Http\Middleware\EnsureCustomerActive::class)
|
||||
->and($persistent)->toContain(\App\Http\Middleware\RestrictAdminHost::class);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue