mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(auth): protect the root user and owners from deletion by admins
An admin of the root team could delete the root user (id 0) and the owners of the root team from the user list. - Never delete the root user here - Refuse a user whose root-team role is higher than the caller's - Refuse the caller's own account; the profile has its own flow
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Team;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
@@ -79,6 +80,10 @@ class AdminView extends Component
|
||||
return $this->dispatch('error', 'User not found');
|
||||
}
|
||||
|
||||
if ($error = $this->deletionError(auth()->user(), $user)) {
|
||||
return $this->dispatch('error', $error);
|
||||
}
|
||||
|
||||
try {
|
||||
$user->delete();
|
||||
auditLog('ui.user.deleted', [
|
||||
@@ -96,6 +101,38 @@ class AdminView extends Component
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance admins may delete other users, but never the root user, their
|
||||
* own account (use the profile instead), or a user whose role in the root
|
||||
* team is higher than their own.
|
||||
*/
|
||||
private function deletionError(User $actor, User $target): ?string
|
||||
{
|
||||
if ($target->id === 0) {
|
||||
return 'The root user cannot be deleted.';
|
||||
}
|
||||
|
||||
if ($target->id === $actor->id) {
|
||||
return 'Delete your own account from your profile.';
|
||||
}
|
||||
|
||||
$actorRole = $this->rootTeamRole($actor);
|
||||
$targetRole = $this->rootTeamRole($target);
|
||||
|
||||
if (! $actorRole || ($targetRole && $targetRole->gt($actorRole))) {
|
||||
return 'You cannot delete a user with a higher role in the root team.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function rootTeamRole(User $user): ?Role
|
||||
{
|
||||
$role = $user->teams()->where('teams.id', 0)->first()?->pivot?->role;
|
||||
|
||||
return $role ? Role::tryFrom($role) : null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
abort_unless(isInstanceAdmin(), 403);
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Team\AdminView;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::forceCreate(['id' => 0]);
|
||||
|
||||
$this->rootTeam = Team::factory()->create(['id' => 0]);
|
||||
$this->rootUser = User::factory()->create(['id' => 0]);
|
||||
|
||||
$this->rootAdmin = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($this->rootAdmin->id, ['role' => 'admin']);
|
||||
|
||||
$this->rootOwner = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($this->rootOwner->id, ['role' => 'owner']);
|
||||
});
|
||||
|
||||
function actAsRootTeamMemberForAdminViewDeletion(User $user): void
|
||||
{
|
||||
test()->actingAs($user);
|
||||
session(['currentTeam' => Team::find(0)]);
|
||||
}
|
||||
|
||||
test('a root team admin cannot delete the root user', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', 0, 'password')
|
||||
->assertDispatched('error', 'The root user cannot be deleted.');
|
||||
|
||||
expect(User::find(0))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team admin cannot delete a root team owner', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $this->rootOwner->id, 'password')
|
||||
->assertDispatched('error', 'You cannot delete a user with a higher role in the root team.');
|
||||
|
||||
expect(User::find($this->rootOwner->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team owner cannot delete the root user', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootOwner);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', 0, 'password')
|
||||
->assertDispatched('error', 'The root user cannot be deleted.');
|
||||
|
||||
expect(User::find(0))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('an instance admin cannot delete their own account from the admin view', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $this->rootAdmin->id, 'password')
|
||||
->assertDispatched('error', 'Delete your own account from your profile.');
|
||||
|
||||
expect(User::find($this->rootAdmin->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team owner can delete a root team admin and a member', function () {
|
||||
$member = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($member->id, ['role' => 'member']);
|
||||
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootOwner);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $this->rootAdmin->id, 'password')
|
||||
->assertReturned(true)
|
||||
->call('delete', $member->id, 'password')
|
||||
->assertReturned(true);
|
||||
|
||||
expect(User::find($this->rootAdmin->id))->toBeNull()
|
||||
->and(User::find($member->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team admin can delete another root team admin and a user outside the root team', function () {
|
||||
$otherAdmin = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($otherAdmin->id, ['role' => 'admin']);
|
||||
$outsider = User::factory()->create();
|
||||
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $otherAdmin->id, 'password')
|
||||
->assertReturned(true)
|
||||
->call('delete', $outsider->id, 'password')
|
||||
->assertReturned(true);
|
||||
|
||||
expect(User::find($otherAdmin->id))->toBeNull()
|
||||
->and(User::find($outsider->id))->toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user