Optimierung 3.6: Zugriffs-Gates und getrennte Impersonation-Sperre

- Gate::define('access-admin'|'access-customer') als Single Source of Truth im
  AppServiceProvider (auch via @can/Policies nutzbar). EnsureUserIsAdmin und
  EnsureUserIsCustomer pruefen jetzt ueber diese Gates.
- Impersonation-Redirect aus EnsureUserIsAdmin in die eigene Middleware
  BlockAdminDuringImpersonation ausgelagert (Sitzungs-Bedingung statt
  Rollenfrage); laeuft vor der Zugriffspruefung und ist zusaetzlich als
  persistente Livewire-Middleware registriert.
- Routen-Alias 'can:' bewusst NICHT verwendet: die Admin-Middleware ist
  persistente Livewire-Middleware und braucht eine konkrete Klasse; der
  parametrisierte Alias kann dort nicht stehen. EnsureApiUserIsActive bleibt
  eigenstaendig (is_active-Pruefung mit getesteter JSON-Antwort, kein Gate).
- Tests: Auth/AccessGatesTest; UserImpersonationTest bleibt gruen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-19 11:20:28 +00:00
parent b255d90721
commit 31e01e46b6
7 changed files with 104 additions and 16 deletions

View file

@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use App\Actions\Admin\UserImpersonation;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Sperrt den Admin-Bereich, solange eine Benutzer-Impersonation läuft. GET/HEAD
* werden auf das Kunden-Dashboard umgeleitet (der Admin agiert in der
* Kunden-Sicht), schreibende Zugriffe werden hart abgewiesen.
*
* Bewusst von der reinen Zugriffsprüfung ({@see EnsureUserIsAdmin}) getrennt:
* Impersonation ist eine Sitzungs-Bedingung, keine Rollenfrage.
*/
class BlockAdminDuringImpersonation
{
public function __construct(private readonly UserImpersonation $impersonation) {}
public function handle(Request $request, Closure $next): Response
{
if ($this->impersonation->isActive()) {
if ($request->isMethod('GET') || $request->isMethod('HEAD')) {
return redirect()->route('me.dashboard');
}
abort(403, 'Während der Benutzer-Impersonation ist der Admin-Bereich gesperrt.');
}
return $next($request);
}
}

View file

@ -2,26 +2,22 @@
namespace App\Http\Middleware;
use App\Actions\Admin\UserImpersonation;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\Response;
/**
* Reine Zugriffsprüfung auf den Admin-Bereich über das `access-admin`-Gate. Die
* Impersonation-Sperre liegt in {@see BlockAdminDuringImpersonation} und läuft
* davor. Bleibt eine konkrete Klasse (statt `can:access-admin`), weil sie als
* persistente Livewire-Middleware registriert ist (AppServiceProvider).
*/
class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (app(UserImpersonation::class)->isActive()) {
if ($request->isMethod('GET') || $request->isMethod('HEAD')) {
return redirect()->route('me.dashboard');
}
abort(403, 'Während der Benutzer-Impersonation ist der Admin-Bereich gesperrt.');
}
if (! $user?->canAccessAdmin()) {
if (Gate::denies('access-admin')) {
abort(403, 'Kein Zugriff auf den Admin-Bereich.');
}

View file

@ -4,13 +4,18 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Symfony\Component\HttpFoundation\Response;
/**
* Zugriffsprüfung auf das Kundenportal („Mein Bereich") über das
* `access-customer`-Gate (Rollen admin/editor/customer, aktiv).
*/
class EnsureUserIsCustomer
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->user()?->canAccessCustomer()) {
if (Gate::denies('access-customer')) {
abort(403, 'Kein Zugriff auf das Kundenportal.');
}

View file

@ -4,6 +4,7 @@ namespace App\Providers;
use App\Contracts\NewsletterSyncClient;
use App\Helpers\ThemeHelper;
use App\Http\Middleware\BlockAdminDuringImpersonation;
use App\Http\Middleware\EnsureUserIsAdmin;
use App\Http\Middleware\LogSlowAdminRequests;
use App\Http\Middleware\SetEdition;
@ -28,6 +29,7 @@ use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Laravel\Cashier\Cashier;
@ -67,6 +69,11 @@ class AppServiceProvider extends ServiceProvider
// Konsole, Mails).
URL::defaults(['edition' => SetEdition::DEFAULT_EDITION]);
// Zugriffs-Gates als Single Source of Truth (auch in Blade via @can und in
// Policies nutzbar). Dieselbe Definition, die die Panel-Middleware prüfen.
Gate::define('access-admin', fn (User $user): bool => $user->canAccessAdmin());
Gate::define('access-customer', fn (User $user): bool => $user->canAccessCustomer());
// Registrierung → Verifizierungsmail; bestätigter Klick → Aktivierung.
Event::listen(Registered::class, SendEmailVerificationNotification::class);
Event::listen(Verified::class, ActivateUserAfterVerification::class);
@ -94,6 +101,7 @@ class AppServiceProvider extends ServiceProvider
DB::listen(fn (QueryExecuted $query) => app(AdminRequestPerformanceMetrics::class)->record($query));
Livewire::addPersistentMiddleware([
BlockAdminDuringImpersonation::class,
EnsureUserIsAdmin::class,
LogSlowAdminRequests::class,
]);