diff --git a/app/Policies/CloudInitScriptPolicy.php b/app/Policies/CloudInitScriptPolicy.php index 0be4f26629..d57081a8e6 100644 --- a/app/Policies/CloudInitScriptPolicy.php +++ b/app/Policies/CloudInitScriptPolicy.php @@ -8,7 +8,7 @@ use App\Models\User; class CloudInitScriptPolicy { /** - * Determine whether the user can view any models. + * Listing and creating happen on the session team's security pages. */ public function viewAny(User $user): bool { @@ -16,11 +16,11 @@ class CloudInitScriptPolicy } /** - * Determine whether the user can view the model. + * A loaded resource is checked against its own team: the session team can differ. */ public function view(User $user, CloudInitScript $cloudInitScript): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudInitScript->team_id); } /** @@ -36,7 +36,7 @@ class CloudInitScriptPolicy */ public function update(User $user, CloudInitScript $cloudInitScript): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudInitScript->team_id); } /** @@ -44,7 +44,7 @@ class CloudInitScriptPolicy */ public function delete(User $user, CloudInitScript $cloudInitScript): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudInitScript->team_id); } /** @@ -52,7 +52,7 @@ class CloudInitScriptPolicy */ public function restore(User $user, CloudInitScript $cloudInitScript): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudInitScript->team_id); } /** @@ -60,6 +60,6 @@ class CloudInitScriptPolicy */ public function forceDelete(User $user, CloudInitScript $cloudInitScript): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudInitScript->team_id); } } diff --git a/app/Policies/CloudProviderTokenPolicy.php b/app/Policies/CloudProviderTokenPolicy.php index b7b108ba88..6c22ee40c2 100644 --- a/app/Policies/CloudProviderTokenPolicy.php +++ b/app/Policies/CloudProviderTokenPolicy.php @@ -8,7 +8,7 @@ use App\Models\User; class CloudProviderTokenPolicy { /** - * Determine whether the user can view any models. + * Listing and creating happen on the session team's security pages. */ public function viewAny(User $user): bool { @@ -16,11 +16,11 @@ class CloudProviderTokenPolicy } /** - * Determine whether the user can view the model. + * A loaded resource is checked against its own team: the session team can differ. */ public function view(User $user, CloudProviderToken $cloudProviderToken): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudProviderToken->team_id); } /** @@ -36,7 +36,7 @@ class CloudProviderTokenPolicy */ public function update(User $user, CloudProviderToken $cloudProviderToken): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudProviderToken->team_id); } /** @@ -44,7 +44,7 @@ class CloudProviderTokenPolicy */ public function delete(User $user, CloudProviderToken $cloudProviderToken): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudProviderToken->team_id); } /** @@ -52,7 +52,7 @@ class CloudProviderTokenPolicy */ public function restore(User $user, CloudProviderToken $cloudProviderToken): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudProviderToken->team_id); } /** @@ -60,6 +60,6 @@ class CloudProviderTokenPolicy */ public function forceDelete(User $user, CloudProviderToken $cloudProviderToken): bool { - return $user->isAdmin(); + return $user->isAdminOfTeam((int) $cloudProviderToken->team_id); } } diff --git a/tests/Feature/Security/CloudResourcePolicyResourceTeamTest.php b/tests/Feature/Security/CloudResourcePolicyResourceTeamTest.php new file mode 100644 index 0000000000..f3e9ddff7a --- /dev/null +++ b/tests/Feature/Security/CloudResourcePolicyResourceTeamTest.php @@ -0,0 +1,115 @@ +withoutDefer(); + if (! InstanceSettings::query()->whereKey(0)->exists()) { + $settings = new InstanceSettings; + $settings->id = 0; + $settings->save(); + } + Once::flush(); + + $this->resourceTeam = Team::factory()->create(); + $this->otherTeam = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->otherTeam->members()->attach($this->user->id, ['role' => 'owner']); + + $this->cloudToken = CloudProviderToken::factory()->create([ + 'team_id' => $this->resourceTeam->id, + 'name' => 'Production Hetzner', + ]); + $this->cloudInitScript = CloudInitScript::query()->create([ + 'team_id' => $this->resourceTeam->id, + 'name' => 'Bootstrap', + 'script' => "#cloud-config\nruncmd:\n - echo original\n", + ]); + + $this->actingAs($this->user); + session(['currentTeam' => $this->otherTeam]); +}); + +function switchCloudResourcePolicySessionTeam(Team $team): void +{ + session(['currentTeam' => $team]); + Cache::flush(); + Once::flush(); +} + +function setCloudResourcePolicyRole(Team $team, User $user, string $role): void +{ + $team->members()->syncWithoutDetaching([$user->id => ['role' => $role]]); + $team->members()->updateExistingPivot($user->id, ['role' => $role]); + $user->unsetRelation('teams'); + Cache::flush(); + Once::flush(); +} + +it('lets an admin of the resource team manage cloud resources while the session team is another team', function (string $ability) { + setCloudResourcePolicyRole($this->resourceTeam, $this->user, 'admin'); + $user = $this->user->fresh(); + + expect($user->can($ability, $this->cloudToken))->toBeTrue() + ->and($user->can($ability, $this->cloudInitScript))->toBeTrue(); +})->with(['view', 'update', 'delete']); + +it('denies a member of the resource team who owns the session team', function (string $ability) { + setCloudResourcePolicyRole($this->resourceTeam, $this->user, 'member'); + $user = $this->user->fresh(); + + expect($user->can($ability, $this->cloudToken))->toBeFalse() + ->and($user->can($ability, $this->cloudInitScript))->toBeFalse(); +})->with(['view', 'update', 'delete']); + +it('denies an outsider even when the session points to the resource team', function (string $ability) { + switchCloudResourcePolicySessionTeam($this->resourceTeam); + $user = $this->user->fresh(); + + expect($user->can($ability, $this->cloudToken))->toBeFalse() + ->and($user->can($ability, $this->cloudInitScript))->toBeFalse(); +})->with(['view', 'update', 'delete']); + +it('blocks a demoted cloud token editor after a session switch to a team they own', function (string $action) { + setCloudResourcePolicyRole($this->resourceTeam, $this->user, 'admin'); + switchCloudResourcePolicySessionTeam($this->resourceTeam); + + $component = Livewire::test(CloudProviderTokenShow::class, ['cloud_token_uuid' => $this->cloudToken->uuid]); + + setCloudResourcePolicyRole($this->resourceTeam, $this->user, 'member'); + switchCloudResourcePolicySessionTeam($this->otherTeam); + + $component->set('name', 'Hijacked')->call($action)->assertForbidden(); + + $fresh = CloudProviderToken::query()->find($this->cloudToken->id); + expect($fresh)->not->toBeNull() + ->and($fresh->name)->toBe('Production Hetzner'); +})->with(['save', 'delete']); + +it('blocks a demoted cloud-init script editor after a session switch to a team they own', function (string $action) { + setCloudResourcePolicyRole($this->resourceTeam, $this->user, 'admin'); + switchCloudResourcePolicySessionTeam($this->resourceTeam); + + $component = Livewire::test(CloudInitScriptShow::class, ['cloud_init_script_uuid' => $this->cloudInitScript->uuid]); + + setCloudResourcePolicyRole($this->resourceTeam, $this->user, 'member'); + switchCloudResourcePolicySessionTeam($this->otherTeam); + + $component->set('name', 'Hijacked')->call($action)->assertForbidden(); + + $fresh = CloudInitScript::query()->find($this->cloudInitScript->id); + expect($fresh)->not->toBeNull() + ->and($fresh->name)->toBe('Bootstrap'); +})->with(['save', 'delete']);