security: Rechteausweitung schließen & Admin-Panel feingranular gaten (K1, H1, H2, M8)
- users:manage/roles:manage auf allen User-/Rollen-Masken erzwungen; is_super_admin aus $fillable entfernt und nur per autorisiertem forceFill setzbar; zentraler Gate::before-Super-Admin-Kurzschluss (K1/M8). - Nicht-Super-Admins können Super-Admin-Konten nicht mehr bearbeiten (Fix#2); Impersonation mit Rangprüfung, keine Token-Erstellung während Impersonation (H1). - Destruktive/verwaltende Admin-Aktionen (CRM/Billing/Compliance/Struktur) hängen an neuen admin-only Permissions; Editor behält reine Content-Rechte (H2). - Kanonischer Permission-Katalog (App\Authorization\Permission) als Single Source of Truth für Seeder und Monitoring. Deployment: `php artisan db:seed --class=RolesAndPermissionsSeeder` auf allen Umgebungen ausführen (neue Permissions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
39e13d6d84
commit
77f8f9cb35
34 changed files with 748 additions and 100 deletions
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Admin\UserImpersonation;
|
||||
use App\Services\Api\ApiAccessEligibilityService;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Attributes\Layout;
|
||||
|
|
@ -19,6 +20,8 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
|
||||
public ?string $eligibilityMessage = null;
|
||||
|
||||
private const IMPERSONATION_DENIAL_MESSAGE = 'Während der Benutzer-Impersonation können keine API-Tokens erstellt oder widerrufen werden.';
|
||||
|
||||
private const ABILITIES = [
|
||||
'press-releases:read' => 'Pressemitteilungen lesen',
|
||||
'press-releases:write' => 'Pressemitteilungen erstellen und bearbeiten',
|
||||
|
|
@ -29,6 +32,8 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
|
||||
public function createToken(): void
|
||||
{
|
||||
$this->ensureNotImpersonating();
|
||||
|
||||
$eligibility = app(ApiAccessEligibilityService::class);
|
||||
$denialReason = $eligibility->denialReason(auth()->user());
|
||||
|
||||
|
|
@ -59,6 +64,8 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
|
||||
public function revokeToken(int $tokenId): void
|
||||
{
|
||||
$this->ensureNotImpersonating();
|
||||
|
||||
auth()->user()
|
||||
->tokens()
|
||||
->whereKey($tokenId)
|
||||
|
|
@ -72,17 +79,30 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
{
|
||||
$eligibility = app(ApiAccessEligibilityService::class);
|
||||
$denialReason = $eligibility->denialReason(auth()->user());
|
||||
$isImpersonating = app(UserImpersonation::class)->isActive();
|
||||
|
||||
return [
|
||||
'abilityOptions' => self::ABILITIES,
|
||||
'canCreateApiToken' => $denialReason === null,
|
||||
'apiTokenDenialReason' => $denialReason,
|
||||
'canCreateApiToken' => $denialReason === null && ! $isImpersonating,
|
||||
'canRevokeApiTokens' => ! $isImpersonating,
|
||||
'apiTokenDenialReason' => $isImpersonating
|
||||
? __(self::IMPERSONATION_DENIAL_MESSAGE)
|
||||
: $denialReason,
|
||||
'tokens' => auth()->user()
|
||||
->tokens()
|
||||
->latest()
|
||||
->get(['id', 'name', 'abilities', 'last_used_at', 'created_at']),
|
||||
];
|
||||
}
|
||||
|
||||
private function ensureNotImpersonating(): void
|
||||
{
|
||||
abort_if(
|
||||
app(UserImpersonation::class)->isActive(),
|
||||
403,
|
||||
__(self::IMPERSONATION_DENIAL_MESSAGE),
|
||||
);
|
||||
}
|
||||
}; ?>
|
||||
|
||||
<div class="space-y-8">
|
||||
|
|
@ -223,6 +243,7 @@ new #[Layout('components.layouts.app'), Title('API-Tokens')] class extends Compo
|
|||
icon="trash"
|
||||
wire:click="revokeToken({{ $token->id }})"
|
||||
wire:confirm="{{ __('Diesen API-Token wirklich widerrufen?') }}"
|
||||
:disabled="! $canRevokeApiTokens"
|
||||
>
|
||||
{{ __('Widerrufen') }}
|
||||
</flux:button>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue