refactor: move admin route into middleware group and harden authorization
Move the admin panel route into the existing auth middleware group and replace client-side redirects with server-side abort calls in the Livewire component. Extract shared authorization logic into reusable private methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e39678aea5
commit
aea201fcba
3 changed files with 141 additions and 11 deletions
|
|
@ -6,7 +6,6 @@
|
|||
use App\Models\User;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Livewire\Component;
|
||||
|
||||
class Index extends Component
|
||||
|
|
@ -22,16 +21,15 @@ class Index extends Component
|
|||
public function mount()
|
||||
{
|
||||
if (! isCloud() && ! isDev()) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
if (Auth::id() !== 0 && ! session('impersonating')) {
|
||||
return redirect()->route('dashboard');
|
||||
abort(403);
|
||||
}
|
||||
$this->authorizeAdminAccess();
|
||||
$this->getSubscribers();
|
||||
}
|
||||
|
||||
public function back()
|
||||
{
|
||||
$this->authorizeAdminAccess();
|
||||
if (session('impersonating')) {
|
||||
session()->forget('impersonating');
|
||||
$user = User::find(0);
|
||||
|
|
@ -45,6 +43,7 @@ public function back()
|
|||
|
||||
public function submitSearch()
|
||||
{
|
||||
$this->authorizeAdminAccess();
|
||||
if ($this->search !== '') {
|
||||
$this->foundUsers = User::where(function ($query) {
|
||||
$query->where('name', 'like', "%{$this->search}%")
|
||||
|
|
@ -61,19 +60,33 @@ public function getSubscribers()
|
|||
|
||||
public function switchUser(int $user_id)
|
||||
{
|
||||
if (Auth::id() !== 0) {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
$this->authorizeRootOnly();
|
||||
session(['impersonating' => true]);
|
||||
$user = User::find($user_id);
|
||||
if (! $user) {
|
||||
abort(404);
|
||||
}
|
||||
$team_to_switch_to = $user->teams->first();
|
||||
// Cache::forget("team:{$user->id}");
|
||||
Auth::login($user);
|
||||
refreshSession($team_to_switch_to);
|
||||
|
||||
return redirect(request()->header('Referer'));
|
||||
}
|
||||
|
||||
private function authorizeAdminAccess(): void
|
||||
{
|
||||
if (! Auth::check() || (Auth::id() !== 0 && ! session('impersonating'))) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
private function authorizeRootOnly(): void
|
||||
{
|
||||
if (! Auth::check() || Auth::id() !== 0) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.admin.index');
|
||||
|
|
|
|||
|
|
@ -90,8 +90,6 @@
|
|||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
Route::get('/admin', AdminIndex::class)->name('admin.index');
|
||||
|
||||
Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot')->middleware('throttle:forgot-password');
|
||||
Route::get('/realtime', [Controller::class, 'realtime_test'])->middleware('auth');
|
||||
Route::get('/verify', [Controller::class, 'verify'])->middleware('auth')->name('verify.email');
|
||||
|
|
@ -109,6 +107,7 @@
|
|||
});
|
||||
|
||||
Route::get('/', Dashboard::class)->name('dashboard');
|
||||
Route::get('/admin', AdminIndex::class)->name('admin.index');
|
||||
Route::get('/onboarding', BoardingIndex::class)->name('onboarding');
|
||||
|
||||
Route::get('/subscription', SubscriptionShow::class)->name('subscription.show');
|
||||
|
|
|
|||
118
tests/Feature/AdminAccessAuthorizationTest.php
Normal file
118
tests/Feature/AdminAccessAuthorizationTest.php
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Admin\Index as AdminIndex;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
test('unauthenticated user cannot access admin route', function () {
|
||||
$response = $this->get('/admin');
|
||||
|
||||
$response->assertRedirect('/login');
|
||||
});
|
||||
|
||||
test('authenticated non-root user gets 403 on admin page', function () {
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => ['id' => $team->id]]);
|
||||
|
||||
Livewire::test(AdminIndex::class)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('root user can access admin page in cloud mode', function () {
|
||||
config()->set('constants.coolify.self_hosted', false);
|
||||
|
||||
$rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]);
|
||||
$rootUser = User::factory()->create(['id' => 0]);
|
||||
$rootTeam->members()->attach($rootUser->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($rootUser);
|
||||
session(['currentTeam' => ['id' => $rootTeam->id]]);
|
||||
|
||||
Livewire::test(AdminIndex::class)
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
test('root user gets 403 on admin page in self-hosted non-dev mode', function () {
|
||||
config()->set('constants.coolify.self_hosted', true);
|
||||
config()->set('app.env', 'production');
|
||||
|
||||
$rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]);
|
||||
$rootUser = User::factory()->create(['id' => 0]);
|
||||
$rootTeam->members()->attach($rootUser->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($rootUser);
|
||||
session(['currentTeam' => ['id' => $rootTeam->id]]);
|
||||
|
||||
Livewire::test(AdminIndex::class)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('submitSearch requires admin authorization', function () {
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => ['id' => $team->id]]);
|
||||
|
||||
Livewire::test(AdminIndex::class)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('switchUser requires root user id 0', function () {
|
||||
config()->set('constants.coolify.self_hosted', false);
|
||||
|
||||
$rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]);
|
||||
$rootUser = User::factory()->create(['id' => 0]);
|
||||
$rootTeam->members()->attach($rootUser->id, ['role' => 'admin']);
|
||||
|
||||
$targetUser = User::factory()->create();
|
||||
$targetTeam = Team::factory()->create();
|
||||
$targetTeam->members()->attach($targetUser->id, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($rootUser);
|
||||
session(['currentTeam' => ['id' => $rootTeam->id]]);
|
||||
|
||||
Livewire::test(AdminIndex::class)
|
||||
->assertOk()
|
||||
->call('switchUser', $targetUser->id)
|
||||
->assertRedirect();
|
||||
});
|
||||
|
||||
test('switchUser rejects non-root user', function () {
|
||||
config()->set('constants.coolify.self_hosted', false);
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user->id, ['role' => 'admin']);
|
||||
|
||||
// Must set impersonating session to bypass mount() check
|
||||
$this->actingAs($user);
|
||||
session([
|
||||
'currentTeam' => ['id' => $team->id],
|
||||
'impersonating' => true,
|
||||
]);
|
||||
|
||||
Livewire::test(AdminIndex::class)
|
||||
->call('switchUser', 999)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin route has auth middleware applied', function () {
|
||||
$route = collect(app('router')->getRoutes()->getRoutesByName())
|
||||
->get('admin.index');
|
||||
|
||||
expect($route)->not->toBeNull();
|
||||
|
||||
$middleware = $route->gatherMiddleware();
|
||||
|
||||
expect($middleware)->toContain('auth');
|
||||
});
|
||||
Loading…
Reference in a new issue