diff --git a/resources/views/components/sidebar.blade.php b/resources/views/components/sidebar.blade.php index ce52cb7..198a09e 100644 --- a/resources/views/components/sidebar.blade.php +++ b/resources/views/components/sidebar.blade.php @@ -33,7 +33,9 @@ {{ __('shell.nav_services') }} {{ __('shell.nav_files') }} {{ __('shell.nav_audit') }} - {{ __('shell.nav_wireguard') }} + @can('manage-network') + {{ __('shell.nav_wireguard') }} + @endcan {{ __('shell.nav_terminal') }}

{{ __('shell.group_account') }}

diff --git a/resources/views/livewire/files/index.blade.php b/resources/views/livewire/files/index.blade.php index 604bf9d..8459934 100644 --- a/resources/views/livewire/files/index.blade.php +++ b/resources/views/livewire/files/index.blade.php @@ -49,12 +49,14 @@ {{-- Listing --}} - + @can('operate') + + @endcan {{-- Column header (wide desktop only — below xl the rows render as stacked cards) --}} @@ -132,7 +134,9 @@ {{ __('files.download') }} {{ __('common.edit') }} @endunless - {{ __('common.delete') }} + @can('operate') + {{ __('common.delete') }} + @endcan @endforeach diff --git a/resources/views/livewire/modals/file-editor.blade.php b/resources/views/livewire/modals/file-editor.blade.php index 8854a83..3cc3c1b 100644 --- a/resources/views/livewire/modals/file-editor.blade.php +++ b/resources/views/livewire/modals/file-editor.blade.php @@ -31,10 +31,12 @@
{{ __('common.close') }} @if ($loaded && ! $error && ! $tooBig && ! $binary && ! $this->isImage()) - - - {{ __('common.save') }} - + @can('operate') + + + {{ __('common.save') }} + + @endcan @endif
diff --git a/resources/views/livewire/servers/index.blade.php b/resources/views/livewire/servers/index.blade.php index 19df232..de164bc 100644 --- a/resources/views/livewire/servers/index.blade.php +++ b/resources/views/livewire/servers/index.blade.php @@ -10,10 +10,12 @@

{{ __('servers.heading') }}

- - - {{ __('servers.add_server') }} - + @can('manage-fleet') + + + {{ __('servers.add_server') }} + + @endcan {{ __('servers.online_of_total', ['online' => $online, 'total' => $total]) }}
diff --git a/resources/views/livewire/servers/show.blade.php b/resources/views/livewire/servers/show.blade.php index d334050..664fc9d 100644 --- a/resources/views/livewire/servers/show.blade.php +++ b/resources/views/livewire/servers/show.blade.php @@ -117,19 +117,21 @@ {{ $cred->username }}@{{ $server->ip }}

-
- - {{ __('common.edit') }} - - - - - {{ $cred->disabled ? __('common.unlock') : __('common.lock') }} - - - {{ __('common.delete') }} - -
+ @can('manage-fleet') +
+ + {{ __('common.edit') }} + + + + + {{ $cred->disabled ? __('common.unlock') : __('common.lock') }} + + + {{ __('common.delete') }} + +
+ @endcan @else
@@ -140,9 +142,11 @@

{{ __('servers.cred_none_title') }}

{{ __('servers.cred_none_hint') }}

- - {{ __('servers.cred_deposit') }} - + @can('manage-fleet') + + {{ __('servers.cred_deposit') }} + + @endcan @endif
@@ -307,23 +311,29 @@ @if ($supported) @if ($check['key'] === 'fail2ban') - - - + @can('manage-network') + + + + @endcan @endif @if ($check['key'] === 'ssh_password' && $check['featureOn']) {{-- Safe auto-flow: generate+install a key, verify, switch the panel credential, THEN disable. --}} - - {{ __('servers.ssh_key_provision_action') }} - + @can('manage-fleet') + + {{ __('servers.ssh_key_provision_action') }} + + @endcan @else - - {{ $check['featureOn'] ? __('common.disable') : __('common.enable') }} - + @can('manage-network') + + {{ $check['featureOn'] ? __('common.disable') : __('common.enable') }} + + @endcan @endif @endif @@ -386,12 +396,14 @@ :subtitle="$fwToolLabel . ($fwActive ? __('servers.firewall_sub_active') : __('servers.firewall_sub_inactive'))" :padded="false"> @if ($fwManageable) - - - {{ __('servers.firewall_add_rule') }} - - + @can('manage-network') + + + {{ __('servers.firewall_add_rule') }} + + + @endcan @endif @if ($fwReadError) @@ -402,10 +414,12 @@ @elseif ($fwTool === 'firewalld' && ! $fwActive)

{{ __('servers.firewalld_inactive') }}

- - {{ __('common.enable') }} - + @can('manage-network') + + {{ __('common.enable') }} + + @endcan
@else {{-- Lone firewall: defaults/state in the left column, rules list in the @@ -456,10 +470,12 @@ 'text-offline' => $ruleTone === 'offline', ])>{{ strtolower($rule['action'] ?? 'allow') }} @unless ($fwReadOnly) - - - + @can('manage-network') + + + + @endcan @endunless @empty @@ -479,17 +495,19 @@ :subtitle="$f2bActive ? __('servers.fail2ban_sub_active', ['count' => $f2bBanned]) : __('servers.fail2ban_sub_installed_inactive')" :padded="false"> @if ($f2bActive) - - - - - - {{ __('servers.fail2ban_ban_ip') }} - - + @can('manage-network') + + + + + + {{ __('servers.fail2ban_ban_ip') }} + + + @endcan @endif @if ($f2bReadError) @@ -500,10 +518,12 @@ @elseif (! $f2bActive)

{{ __('servers.fail2ban_state_installed_inactive') }}

- - {{ __('common.enable') }} - + @can('manage-network') + + {{ __('common.enable') }} + + @endcan
@else {{-- Lone fail2ban: status summary + "Gesperrte IPs ansehen" in the left @@ -522,10 +542,12 @@ · $f2bBanned > 0])>{{ __('servers.fail2ban_banned', ['count' => $f2bBanned]) }} · {{ __('servers.fail2ban_jails_count', ['count' => count($f2bJails)]) }}

- - {{ __('servers.fail2ban_view_bans') }} - + @can('manage-network') + + {{ __('servers.fail2ban_view_bans') }} + + @endcan {{-- Whitelist (ignoreip) --}} @@ -540,18 +562,22 @@ {{ $ip }} @unless (in_array($ip, ['127.0.0.1/8', '::1'], true)) - + @can('manage-network') + + @endcan @endunless @endforeach -
- - {{ __('common.add') }} -
+ @can('manage-network') +
+ + {{ __('common.add') }} +
+ @endcan @endif @@ -616,11 +642,13 @@ {{-- SSH keys --}} - - - {{ __('servers.ssh_keys_add') }} - - + @can('manage-fleet') + + + {{ __('servers.ssh_keys_add') }} + + + @endcan
@forelse ($sshKeys as $key)
@@ -634,9 +662,11 @@

{{ $key['fingerprint'] }}

- - - + @can('manage-fleet') + + + + @endcan @empty

{{ __('servers.ssh_keys_none') }}

diff --git a/resources/views/livewire/services/index.blade.php b/resources/views/livewire/services/index.blade.php index 4aa432f..330a394 100644 --- a/resources/views/livewire/services/index.blade.php +++ b/resources/views/livewire/services/index.blade.php @@ -72,11 +72,13 @@ {{ $svcLabel[$svc['status']] }} {{-- R5: wire to wire-elements/modal --}} -
- {{ __('services.start') }} - {{ __('services.stop') }} - {{ __('services.restart') }} -
+ @can('operate') +
+ {{ __('services.start') }} + {{ __('services.stop') }} + {{ __('services.restart') }} +
+ @endcan @empty diff --git a/resources/views/livewire/system/index.blade.php b/resources/views/livewire/system/index.blade.php index 59042d8..d33b587 100644 --- a/resources/views/livewire/system/index.blade.php +++ b/resources/views/livewire/system/index.blade.php @@ -39,9 +39,11 @@ {{ __('system.restart_running') }}

@else - - {{ __('system.restart_now') }} - + @can('manage-panel') + + {{ __('system.restart_now') }} + + @endcan @endif

{{ __('system.restart_watcher_hint') }}

@@ -79,9 +81,11 @@ autocapitalize="off" spellcheck="false" placeholder="{{ __('system.domain_placeholder') }}" class="h-9 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" /> - - {{ __('common.save') }} - + @can('manage-panel') + + {{ __('common.save') }} + + @endcan @error('domainInput')

{{ $message }}

@@ -94,40 +98,42 @@

{{ __('system.tls_mode_title') }}

{{-- Segmented control --}} -
- - -
+ @can('manage-panel') +
+ + +
+ @endcan {{-- Per-mode description --}}
@@ -211,12 +217,14 @@ {{ __('system.cert_status_unknown') }} @endswitch

- - - - {{ __('system.cert_button') }} - + @can('manage-panel') + + + + {{ __('system.cert_button') }} + + @endcan
@elseif ($tlsMode === 'external')
diff --git a/resources/views/livewire/terminal/index.blade.php b/resources/views/livewire/terminal/index.blade.php index 5167789..30368da 100644 --- a/resources/views/livewire/terminal/index.blade.php +++ b/resources/views/livewire/terminal/index.blade.php @@ -11,26 +11,28 @@
{{-- Clusev host — a real SSH login into the host machine (configured by the operator). --}} -
$activeKey === 'host', - 'border-transparent hover:border-line hover:bg-raised/60' => $activeKey !== 'host', - ])> - - -
+ @can('manage-fleet') +
$activeKey === 'host', + 'border-transparent hover:border-line hover:bg-raised/60' => $activeKey !== 'host', + ])> + + +
+ @endcan
{{ __('terminal.servers_heading') }}
diff --git a/resources/views/livewire/versions/index.blade.php b/resources/views/livewire/versions/index.blade.php index 2b54c25..060c1d8 100644 --- a/resources/views/livewire/versions/index.blade.php +++ b/resources/views/livewire/versions/index.blade.php @@ -79,11 +79,13 @@ {{ __('versions.update_running') }}

@elseif ($isUpdate) - - - - {{ __('versions.update_now') }} - + @can('manage-panel') + + + + {{ __('versions.update_now') }} + + @endcan @endif @if ($lastChecked) {{ __('versions.checked_at', ['time' => $lastChecked]) }} diff --git a/tests/Feature/RbacUiHidingTest.php b/tests/Feature/RbacUiHidingTest.php new file mode 100644 index 0000000..f3f6401 --- /dev/null +++ b/tests/Feature/RbacUiHidingTest.php @@ -0,0 +1,138 @@ +create(['must_change_password' => false]); + } + + private function operator(): User + { + return User::factory()->operator()->create(['must_change_password' => false]); + } + + private function viewer(): User + { + return User::factory()->viewer()->create(['must_change_password' => false]); + } + + /** An online server with a credential, selected as the active fleet server. */ + private function activeServer(): Server + { + $server = Server::create(['name' => 'box', 'ip' => '10.0.0.2', 'ssh_port' => 22, 'status' => 'online']); + $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']); + session(['active_server_id' => $server->id]); + + return $server; + } + + // ── manage-fleet: Servers\Index add-server trigger ────────────────────────── + + public function test_admin_sees_the_add_server_trigger(): void + { + $this->actingAs($this->admin()); + + Livewire::test(Servers::class) + ->assertOk() + ->assertSee(__('servers.add_server')); // "Server hinzufügen" + } + + public function test_viewer_does_not_see_the_add_server_trigger(): void + { + $this->actingAs($this->viewer()); + + Livewire::test(Servers::class) + ->assertOk() // the fleet list still renders + ->assertDontSee(__('servers.add_server')); // but the create control is gone + } + + // ── operate: Services\Index start/stop/restart action buttons ─────────────── + + /** Stubs the systemd read so the list renders with one service row. */ + private function stubServices(): void + { + $fleet = Mockery::mock(FleetService::class); + $fleet->shouldReceive('systemd')->andReturn([ + 'services' => [['name' => 'nginx.service', 'status' => 'offline', 'sub' => 'dead', 'enabled' => true, 'desc' => 'web']], + 'journal' => [], + 'cursor' => null, + ]); + app()->instance(FleetService::class, $fleet); + } + + public function test_operator_sees_a_service_action_control(): void + { + $this->actingAs($this->operator()); + $this->activeServer(); + $this->stubServices(); + + Livewire::test(Services::class) + ->call('load') + ->assertOk() + ->assertSee(__('services.restart')); // "Neustart" — a per-row action button + } + + public function test_viewer_does_not_see_a_service_action_control(): void + { + $this->actingAs($this->viewer()); + $this->activeServer(); + $this->stubServices(); + + Livewire::test(Services::class) + ->call('load') + ->assertOk() + ->assertSee('nginx.service') // the list itself is visible + ->assertDontSee(__('services.restart')); // but the action buttons are gone + } + + // ── manage-network: sidebar WireGuard nav item ────────────────────────────── + + public function test_admin_sees_the_wireguard_nav(): void + { + $this->actingAs($this->admin()); + + $this->get('/servers') + ->assertOk() + ->assertSee(__('shell.nav_wireguard')); // "WireGuard" nav item present + } + + public function test_viewer_does_not_see_the_wireguard_nav(): void + { + $this->actingAs($this->viewer()); + + $this->get('/servers') + ->assertOk() + ->assertDontSee(__('shell.nav_wireguard')); // hidden for a viewer + } +}