diff --git a/app/Actions/Projects/BuildProjectBacklogPayload.php b/app/Actions/Projects/BuildProjectBacklogPayload.php index eb71b01..e7b8622 100644 --- a/app/Actions/Projects/BuildProjectBacklogPayload.php +++ b/app/Actions/Projects/BuildProjectBacklogPayload.php @@ -59,9 +59,24 @@ class BuildProjectBacklogPayload )->resolve(), 'users' => UserResource::collection( User::query() + ->whereIn('id', $this->visibleUserIds($project)) ->orderBy('name') ->get(), )->resolve(), ]; } + + /** + * @return array + */ + private function visibleUserIds(Project $project): array + { + return User::query() + ->get() + ->filter(fn (User $candidate): bool => $this->permissionService->allows($candidate, Permission::ProjectRead, $project)) + ->pluck('id') + ->map(fn (mixed $userId): int => (int) $userId) + ->values() + ->all(); + } } diff --git a/app/Actions/Projects/BuildProjectTicketDetailPayload.php b/app/Actions/Projects/BuildProjectTicketDetailPayload.php index 26b4511..56a014b 100644 --- a/app/Actions/Projects/BuildProjectTicketDetailPayload.php +++ b/app/Actions/Projects/BuildProjectTicketDetailPayload.php @@ -116,9 +116,24 @@ class BuildProjectTicketDetailPayload 'ticketTypes' => TicketTypeResource::collection($project->ticketTypes()->get())->resolve(), 'users' => UserResource::collection( User::query() + ->whereIn('id', $this->visibleUserIds($project)) ->orderBy('name') ->get(), )->resolve(), ]; } + + /** + * @return array + */ + private function visibleUserIds(Project $project): array + { + return User::query() + ->get() + ->filter(fn (User $candidate): bool => $this->permissionService->allows($candidate, Permission::ProjectRead, $project)) + ->pluck('id') + ->map(fn (mixed $userId): int => (int) $userId) + ->values() + ->all(); + } } diff --git a/tests/Feature/Api/SettingsApiTest.php b/tests/Feature/Api/SettingsApiTest.php index b5ec766..686338d 100644 --- a/tests/Feature/Api/SettingsApiTest.php +++ b/tests/Feature/Api/SettingsApiTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Api; use App\Actions\ProjectTicketMetadata\InitializeProjectTicketMetadata; use App\Models\Project; +use App\Models\Team; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; @@ -149,6 +150,43 @@ class SettingsApiTest extends TestCase ->assertNoContent(); } + public function test_project_payloads_only_return_users_who_can_view_the_project(): void + { + [$user, $project] = $this->projectContext(); + $viewer = User::factory()->create(['name' => 'Visible User']); + User::factory()->create(['name' => 'Hidden User']); + $teamViewer = User::factory()->create(['name' => 'Team Viewer']); + $team = Team::factory()->create(); + + $this->syncProjectPermissions($project, $viewer, ['view']); + $team->users()->attach($teamViewer); + $this->syncProjectPermissions($project, $team, ['view']); + + $ticket = $project->tickets()->firstOrCreate([ + 'sequence_number' => 1, + ], [ + 'ticket_key' => 'CORE-1', + 'ticket_priority_id' => $project->ticketPriorities()->firstOrFail()->id, + 'ticket_status_id' => $project->ticketStatuses()->firstOrFail()->id, + 'ticket_type_id' => $project->ticketTypes()->where('is_parent', false)->firstOrFail()->id, + 'title' => 'Scoped users ticket', + ]); + + $this->actingAs($user) + ->getJson(route('api.projects.backlog', $project)) + ->assertOk() + ->assertJsonPath('data.users.0.name', 'Team Viewer') + ->assertJsonPath('data.users.1.name', 'Visible User') + ->assertJsonCount(2, 'data.users'); + + $this->actingAs($user) + ->getJson(route('api.projects.tickets.show', [$project, $ticket])) + ->assertOk() + ->assertJsonPath('data.users.0.name', 'Team Viewer') + ->assertJsonPath('data.users.1.name', 'Visible User') + ->assertJsonCount(2, 'data.users'); + } + public function test_project_repositories_and_user_git_connections_are_available(): void { Http::preventStrayRequests();