diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index 45f7e467f..45af53950 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -45,9 +45,16 @@ class InviteLink extends Component try { $this->authorize('manageInvitations', currentTeam()); $this->validate(); - if (auth()->user()->role() === 'admin' && $this->role === 'owner') { + + // Prevent privilege escalation: users cannot invite someone with higher privileges + $userRole = auth()->user()->role(); + if ($userRole === 'member' && in_array($this->role, ['admin', 'owner'])) { + throw new \Exception('Members cannot invite admins or owners.'); + } + if ($userRole === 'admin' && $this->role === 'owner') { throw new \Exception('Admins cannot invite owners.'); } + $this->email = strtolower($this->email); $member_emails = currentTeam()->members()->get()->pluck('email'); diff --git a/app/Policies/TeamPolicy.php b/app/Policies/TeamPolicy.php index b7ef48943..849e23751 100644 --- a/app/Policies/TeamPolicy.php +++ b/app/Policies/TeamPolicy.php @@ -42,8 +42,7 @@ class TeamPolicy return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -56,8 +55,7 @@ class TeamPolicy return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -70,8 +68,7 @@ class TeamPolicy return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -84,8 +81,7 @@ class TeamPolicy return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } /** @@ -98,7 +94,6 @@ class TeamPolicy return false; } - // return $user->isAdmin() || $user->isOwner(); - return true; + return $user->isAdmin() || $user->isOwner(); } } diff --git a/tests/Feature/TeamInvitationPrivilegeEscalationTest.php b/tests/Feature/TeamInvitationPrivilegeEscalationTest.php new file mode 100644 index 000000000..9e011965a --- /dev/null +++ b/tests/Feature/TeamInvitationPrivilegeEscalationTest.php @@ -0,0 +1,176 @@ +team = Team::factory()->create(); + + $this->owner = User::factory()->create(); + $this->admin = User::factory()->create(); + $this->member = User::factory()->create(); + + $this->team->members()->attach($this->owner->id, ['role' => 'owner']); + $this->team->members()->attach($this->admin->id, ['role' => 'admin']); + $this->team->members()->attach($this->member->id, ['role' => 'member']); +}); + +describe('privilege escalation prevention', function () { + test('member cannot invite admin (SECURITY FIX)', function () { + // Login as member + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as admin + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaLink') + ->assertDispatched('error'); + }); + + test('member cannot invite owner (SECURITY FIX)', function () { + // Login as member + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as owner + Livewire::test(InviteLink::class) + ->set('email', 'newowner@example.com') + ->set('role', 'owner') + ->call('viaLink') + ->assertDispatched('error'); + }); + + test('admin cannot invite owner', function () { + // Login as admin + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as owner + Livewire::test(InviteLink::class) + ->set('email', 'newowner@example.com') + ->set('role', 'owner') + ->call('viaLink') + ->assertDispatched('error'); + }); + + test('admin can invite member', function () { + // Login as admin + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + // Invite someone as member + Livewire::test(InviteLink::class) + ->set('email', 'newmember@example.com') + ->set('role', 'member') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newmember@example.com', + 'role' => 'member', + 'team_id' => $this->team->id, + ]); + }); + + test('admin can invite admin', function () { + // Login as admin + $this->actingAs($this->admin); + session(['currentTeam' => $this->team]); + + // Invite someone as admin + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newadmin@example.com', + 'role' => 'admin', + 'team_id' => $this->team->id, + ]); + }); + + test('owner can invite member', function () { + // Login as owner + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + // Invite someone as member + Livewire::test(InviteLink::class) + ->set('email', 'newmember@example.com') + ->set('role', 'member') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newmember@example.com', + 'role' => 'member', + 'team_id' => $this->team->id, + ]); + }); + + test('owner can invite admin', function () { + // Login as owner + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + // Invite someone as admin + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newadmin@example.com', + 'role' => 'admin', + 'team_id' => $this->team->id, + ]); + }); + + test('owner can invite owner', function () { + // Login as owner + $this->actingAs($this->owner); + session(['currentTeam' => $this->team]); + + // Invite someone as owner + Livewire::test(InviteLink::class) + ->set('email', 'newowner@example.com') + ->set('role', 'owner') + ->call('viaLink') + ->assertDispatched('success'); + + // Verify invitation was created + $this->assertDatabaseHas('team_invitations', [ + 'email' => 'newowner@example.com', + 'role' => 'owner', + 'team_id' => $this->team->id, + ]); + }); + + test('member cannot bypass policy by calling viaEmail', function () { + // Login as member + $this->actingAs($this->member); + session(['currentTeam' => $this->team]); + + // Attempt to invite someone as admin via email + Livewire::test(InviteLink::class) + ->set('email', 'newadmin@example.com') + ->set('role', 'admin') + ->call('viaEmail') + ->assertDispatched('error'); + }); +}); diff --git a/tests/Feature/TeamPolicyTest.php b/tests/Feature/TeamPolicyTest.php new file mode 100644 index 000000000..10abd8adf --- /dev/null +++ b/tests/Feature/TeamPolicyTest.php @@ -0,0 +1,136 @@ +team = Team::factory()->create(); + + $this->owner = User::factory()->create(); + $this->admin = User::factory()->create(); + $this->member = User::factory()->create(); + + $this->team->members()->attach($this->owner->id, ['role' => 'owner']); + $this->team->members()->attach($this->admin->id, ['role' => 'admin']); + $this->team->members()->attach($this->member->id, ['role' => 'member']); +}); + +describe('update permission', function () { + test('owner can update team', function () { + expect($this->owner->can('update', $this->team))->toBeTrue(); + }); + + test('admin can update team', function () { + expect($this->admin->can('update', $this->team))->toBeTrue(); + }); + + test('member cannot update team', function () { + expect($this->member->can('update', $this->team))->toBeFalse(); + }); + + test('non-team member cannot update team', function () { + $outsider = User::factory()->create(); + expect($outsider->can('update', $this->team))->toBeFalse(); + }); +}); + +describe('delete permission', function () { + test('owner can delete team', function () { + expect($this->owner->can('delete', $this->team))->toBeTrue(); + }); + + test('admin can delete team', function () { + expect($this->admin->can('delete', $this->team))->toBeTrue(); + }); + + test('member cannot delete team', function () { + expect($this->member->can('delete', $this->team))->toBeFalse(); + }); + + test('non-team member cannot delete team', function () { + $outsider = User::factory()->create(); + expect($outsider->can('delete', $this->team))->toBeFalse(); + }); +}); + +describe('manageMembers permission', function () { + test('owner can manage members', function () { + expect($this->owner->can('manageMembers', $this->team))->toBeTrue(); + }); + + test('admin can manage members', function () { + expect($this->admin->can('manageMembers', $this->team))->toBeTrue(); + }); + + test('member cannot manage members', function () { + expect($this->member->can('manageMembers', $this->team))->toBeFalse(); + }); + + test('non-team member cannot manage members', function () { + $outsider = User::factory()->create(); + expect($outsider->can('manageMembers', $this->team))->toBeFalse(); + }); +}); + +describe('viewAdmin permission', function () { + test('owner can view admin panel', function () { + expect($this->owner->can('viewAdmin', $this->team))->toBeTrue(); + }); + + test('admin can view admin panel', function () { + expect($this->admin->can('viewAdmin', $this->team))->toBeTrue(); + }); + + test('member cannot view admin panel', function () { + expect($this->member->can('viewAdmin', $this->team))->toBeFalse(); + }); + + test('non-team member cannot view admin panel', function () { + $outsider = User::factory()->create(); + expect($outsider->can('viewAdmin', $this->team))->toBeFalse(); + }); +}); + +describe('manageInvitations permission (privilege escalation fix)', function () { + test('owner can manage invitations', function () { + expect($this->owner->can('manageInvitations', $this->team))->toBeTrue(); + }); + + test('admin can manage invitations', function () { + expect($this->admin->can('manageInvitations', $this->team))->toBeTrue(); + }); + + test('member cannot manage invitations (SECURITY FIX)', function () { + // This test verifies the privilege escalation vulnerability is fixed + // Previously, members could see and manage admin invitations + expect($this->member->can('manageInvitations', $this->team))->toBeFalse(); + }); + + test('non-team member cannot manage invitations', function () { + $outsider = User::factory()->create(); + expect($outsider->can('manageInvitations', $this->team))->toBeFalse(); + }); +}); + +describe('view permission', function () { + test('owner can view team', function () { + expect($this->owner->can('view', $this->team))->toBeTrue(); + }); + + test('admin can view team', function () { + expect($this->admin->can('view', $this->team))->toBeTrue(); + }); + + test('member can view team', function () { + expect($this->member->can('view', $this->team))->toBeTrue(); + }); + + test('non-team member cannot view team', function () { + $outsider = User::factory()->create(); + expect($outsider->can('view', $this->team))->toBeFalse(); + }); +});