256 lines
8.4 KiB
PHP
256 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Livewire\Release\Index as ReleaseIndex;
|
|
use App\Livewire\Settings\Email;
|
|
use App\Livewire\Settings\LoginProtection;
|
|
use App\Livewire\Settings\Sessions;
|
|
use App\Models\BannedIp;
|
|
use App\Models\User;
|
|
use App\Services\SessionService;
|
|
use App\Support\Confirm\ConfirmToken;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Livewire\Livewire;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* RBAC guards on the SETTINGS + RELEASE admin surfaces. The RBAC foundation (Role enum, Gates
|
|
* manage-panel/manage-users) is committed separately; this proves the abort_unless(...) guards are
|
|
* wired at every dangerous entry point:
|
|
* - manage-panel gates LoginProtection (brute-force config / whitelist / ban clearing), Email (SMTP
|
|
* config) and the whole Release page + its actions;
|
|
* - manage-users gates the destructive GLOBAL logout (Sessions::logoutAll) while per-session
|
|
* self-revoke stays open to everyone.
|
|
* Admin (factory default) may act; operator + viewer are refused with 403.
|
|
*/
|
|
class RbacSettingsGateTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Cache::flush();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function admin(): User
|
|
{
|
|
return User::factory()->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]);
|
|
}
|
|
|
|
// ── manage-panel: LoginProtection (mount + save + unban + unbanAll + whitelistMyIp) ──
|
|
|
|
public function test_operator_cannot_mount_login_protection(): void
|
|
{
|
|
Livewire::actingAs($this->operator())
|
|
->test(LoginProtection::class)
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_viewer_cannot_mount_login_protection(): void
|
|
{
|
|
Livewire::actingAs($this->viewer())
|
|
->test(LoginProtection::class)
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_admin_can_mount_login_protection(): void
|
|
{
|
|
Livewire::actingAs($this->admin())
|
|
->test(LoginProtection::class)
|
|
->assertOk();
|
|
}
|
|
|
|
public function test_operator_cannot_save_login_protection(): void
|
|
{
|
|
// Boot as admin so the snapshot is valid, then swap to an operator and call the mutation —
|
|
// the save() method-level guard must still fire 403 (models a demotion / crafted update).
|
|
$component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();
|
|
|
|
$this->actingAs($this->operator());
|
|
|
|
$component->set('maxretry', 5)->call('save')->assertForbidden();
|
|
}
|
|
|
|
public function test_viewer_cannot_call_whitelist_my_ip(): void
|
|
{
|
|
$component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();
|
|
|
|
$this->actingAs($this->viewer());
|
|
|
|
$component->call('whitelistMyIp')->assertForbidden();
|
|
}
|
|
|
|
public function test_operator_cannot_unban(): void
|
|
{
|
|
BannedIp::create(['ip' => '203.0.113.5', 'banned_until' => now()->addHour(), 'attempts' => 10]);
|
|
|
|
$component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();
|
|
|
|
$this->actingAs($this->operator());
|
|
|
|
$component->call('unban', 'any-token')->assertForbidden();
|
|
$this->assertDatabaseCount('banned_ips', 1); // guard fired before the ban was touched
|
|
}
|
|
|
|
public function test_viewer_cannot_unban_all(): void
|
|
{
|
|
BannedIp::create(['ip' => '203.0.113.5', 'banned_until' => now()->addHour(), 'attempts' => 10]);
|
|
|
|
$component = Livewire::actingAs($this->admin())->test(LoginProtection::class)->assertOk();
|
|
|
|
$this->actingAs($this->viewer());
|
|
|
|
$component->call('unbanAll', 'any-token')->assertForbidden();
|
|
$this->assertDatabaseCount('banned_ips', 1);
|
|
}
|
|
|
|
public function test_admin_can_save_login_protection(): void
|
|
{
|
|
Livewire::actingAs($this->admin())
|
|
->test(LoginProtection::class)
|
|
->set('maxretry', 7)
|
|
->call('save')
|
|
->assertHasNoErrors(); // allowed through the guard
|
|
}
|
|
|
|
// ── manage-panel: Email (mount + save) ──────────────────────────────────────
|
|
|
|
public function test_operator_cannot_mount_email(): void
|
|
{
|
|
Livewire::actingAs($this->operator())
|
|
->test(Email::class)
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_viewer_cannot_mount_email(): void
|
|
{
|
|
Livewire::actingAs($this->viewer())
|
|
->test(Email::class)
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_operator_cannot_save_email(): void
|
|
{
|
|
$component = Livewire::actingAs($this->admin())->test(Email::class)->assertOk();
|
|
|
|
$this->actingAs($this->operator());
|
|
|
|
$component
|
|
->set('mail_host', 'smtp.example.com')
|
|
->set('mail_from_address', 'noreply@example.com')
|
|
->set('mail_from_name', 'Clusev')
|
|
->call('save')
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_admin_can_save_email(): void
|
|
{
|
|
Livewire::actingAs($this->admin())
|
|
->test(Email::class)
|
|
->set('mail_host', 'smtp.example.com')
|
|
->set('mail_from_address', 'noreply@example.com')
|
|
->set('mail_from_name', 'Clusev')
|
|
->call('save')
|
|
->assertHasNoErrors();
|
|
}
|
|
|
|
// ── manage-users: Sessions::logoutAll (destructive GLOBAL logout) ─────────────
|
|
|
|
public function test_operator_cannot_logout_all(): void
|
|
{
|
|
$component = Livewire::actingAs($this->admin())->test(Sessions::class)->assertOk();
|
|
|
|
$this->actingAs($this->operator());
|
|
|
|
$component->call('logoutAll', 'any-token')->assertForbidden();
|
|
}
|
|
|
|
public function test_viewer_cannot_logout_all(): void
|
|
{
|
|
$component = Livewire::actingAs($this->admin())->test(Sessions::class)->assertOk();
|
|
|
|
$this->actingAs($this->viewer());
|
|
|
|
$component->call('logoutAll', 'any-token')->assertForbidden();
|
|
}
|
|
|
|
public function test_admin_can_logout_all(): void
|
|
{
|
|
$admin = $this->admin();
|
|
|
|
// The global logout truncates every session — mock it so the test session survives.
|
|
$sessions = Mockery::mock(SessionService::class)->makePartial();
|
|
$sessions->shouldReceive('logoutEveryone')->once();
|
|
$sessions->shouldReceive('forUser')->andReturn([]);
|
|
$this->app->instance(SessionService::class, $sessions);
|
|
|
|
$this->actingAs($admin);
|
|
$token = ConfirmToken::issue('sessionsLogoutAll', [], 'session.logout_all', $admin->email);
|
|
ConfirmToken::confirm($token);
|
|
|
|
Livewire::actingAs($admin)
|
|
->test(Sessions::class)
|
|
->call('logoutAll', $token)
|
|
->assertRedirect(route('login')); // allowed through the guard → redirect to login
|
|
}
|
|
|
|
public function test_non_admin_self_session_revoke_is_not_forbidden(): void
|
|
{
|
|
// A non-admin managing their OWN sessions (per-session self-revoke) is NOT gated. Opening the
|
|
// "sign out other devices" confirm must not 403 for an operator.
|
|
Livewire::actingAs($this->operator())
|
|
->test(Sessions::class)
|
|
->call('confirmLogoutOthers')
|
|
->assertOk();
|
|
}
|
|
|
|
// ── manage-panel: Release\Index (flag ON, but role still enforced) ───────────
|
|
|
|
public function test_operator_cannot_mount_release_even_with_flag_on(): void
|
|
{
|
|
config()->set('clusev.release_controls', true);
|
|
|
|
Livewire::actingAs($this->operator())
|
|
->test(ReleaseIndex::class)
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_viewer_cannot_mount_release_even_with_flag_on(): void
|
|
{
|
|
config()->set('clusev.release_controls', true);
|
|
|
|
Livewire::actingAs($this->viewer())
|
|
->test(ReleaseIndex::class)
|
|
->assertForbidden();
|
|
}
|
|
|
|
public function test_admin_can_mount_release_with_flag_on(): void
|
|
{
|
|
config()->set('clusev.release_controls', true);
|
|
|
|
Livewire::actingAs($this->admin())
|
|
->test(ReleaseIndex::class)
|
|
->assertOk();
|
|
}
|
|
}
|