diff --git a/app/Actions/Projects/BuildProjectBacklogPayload.php b/app/Actions/Projects/BuildProjectBacklogPayload.php index eb71b01..bff6315 100644 --- a/app/Actions/Projects/BuildProjectBacklogPayload.php +++ b/app/Actions/Projects/BuildProjectBacklogPayload.php @@ -59,6 +59,7 @@ class BuildProjectBacklogPayload )->resolve(), 'users' => UserResource::collection( User::query() + ->visibleToProject($project) ->orderBy('name') ->get(), )->resolve(), diff --git a/app/Actions/Projects/BuildProjectTicketDetailPayload.php b/app/Actions/Projects/BuildProjectTicketDetailPayload.php index 26b4511..25257cb 100644 --- a/app/Actions/Projects/BuildProjectTicketDetailPayload.php +++ b/app/Actions/Projects/BuildProjectTicketDetailPayload.php @@ -116,6 +116,7 @@ class BuildProjectTicketDetailPayload 'ticketTypes' => TicketTypeResource::collection($project->ticketTypes()->get())->resolve(), 'users' => UserResource::collection( User::query() + ->visibleToProject($project) ->orderBy('name') ->get(), )->resolve(), diff --git a/app/Models/User.php b/app/Models/User.php index ef51f81..22fdb24 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,9 +3,12 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Actions\Permissions\PermissionService; +use App\Enums\Permission; use Database\Factories\UserFactory; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Hidden; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -69,4 +72,22 @@ class User extends Authenticatable { return $this->hasMany(Ticket::class, 'author_user_id'); } + + public function scopeVisibleToProject(Builder $query, Project $project): Builder + { + $permissionService = app(PermissionService::class); + + $allowedUserIds = User::query() + ->select('id') + ->cursor() + ->filter(fn (User $candidate) => $permissionService->allows( + $candidate, + Permission::ProjectRead, + $project + )) + ->pluck('id') + ->all(); + + return $query->whereKey($allowedUserIds); + } } diff --git a/tests/Feature/Api/SettingsApiTest.php b/tests/Feature/Api/SettingsApiTest.php index b5ec766..5f88734 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,45 @@ 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', $user->name) + ->assertJsonPath('data.users.1.name', 'Team Viewer') + ->assertJsonPath('data.users.2.name', 'Visible User') + ->assertJsonCount(3, 'data.users'); + + $this->actingAs($user) + ->getJson(route('api.projects.tickets.show', [$project, $ticket])) + ->assertOk() + ->assertJsonPath('data.users.0.name', $user->name) + ->assertJsonPath('data.users.1.name', 'Team Viewer') + ->assertJsonPath('data.users.2.name', 'Visible User') + ->assertJsonCount(3, 'data.users'); + } + public function test_project_repositories_and_user_git_connections_are_available(): void { Http::preventStrayRequests();