Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ public function __construct(private Request $request, CaseApiRepository $caseRep
*/
public function getAllCases(CaseListRequest $request): JsonResponse
{
// Users are always allowed to view cases scoped to themselves
// Any broader query requires the `view-all_cases` permission.
// Admins pass through via Gate::before in AuthServiceProvider.
$authUser = Auth::user();
$requestedUserId = $request->filled('userId') ? (int) $request->input('userId') : null;
$isViewingOwnCases = $requestedUserId !== null && $requestedUserId === $authUser->id;

if (!$isViewingOwnCases && !$authUser->can('view-all_cases')) {
abort(403);
}

$query = $this->caseRepository->getAllCases($request);

return $this->paginateResponse($query);
Expand Down
13 changes: 12 additions & 1 deletion ProcessMaker/Http/Controllers/CasesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,21 @@ class CasesController extends Controller
/**
* Get the list of requests.
*
* @param string|null $type One of `all|in_progress|completed` (constrained by the route).
*
* @return \Illuminate\View\View|\Illuminate\Contracts\View
*/
public function index()
public function index($type = null)
{
// The "All cases" tab exposes every case in the platform regardless
// of the user's relationship to it, so it is gated by the
// `view-all_cases` permission. The other tabs (My cases, In progress,
// Completed) are scoped to the user and need no gate.
// Admins bypass this check via Gate::before in AuthServiceProvider.
if ($type === 'all' && !Auth::user()->can('view-all_cases')) {
abort(403);
}

$manager = app(ScreenBuilderManager::class);
event(new ScreenBuilderStarting($manager, 'FORM'));
$currentUser = Auth::user()->only(['id', 'username', 'fullname', 'firstname', 'lastname', 'avatar']);
Expand Down
13 changes: 13 additions & 0 deletions tests/Feature/Api/V1_1/CaseControllerSearchTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Gate;
use ProcessMaker\Models\Permission;
use ProcessMaker\Models\User;
use ProcessMaker\Repositories\CaseUtils;
use Tests\Feature\Shared\RequestHelper;
Expand All @@ -16,6 +18,17 @@ public function setUp(): void
parent::setUp();

$this->user = CaseControllerTest::createUser('user_a');

// These tests intentionally exercise the unscoped `get_all_cases`
// endpoint, which now requires `view-all_cases`. Grant the
// non-admin test user the permission so each `apiCall` reaches
// the search logic rather than being short-circuited with a 403.
Permission::firstOrCreate(
['name' => 'view-all_cases'],
['title' => 'View All Cases'],
);
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));
$this->user->giveDirectPermission('view-all_cases');
}

public function tearDown(): void
Expand Down
82 changes: 82 additions & 0 deletions tests/Feature/Api/V1_1/CaseControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Tests\Feature\Api\V1_1;

use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use ProcessMaker\Constants\CaseStatusConstants;
use ProcessMaker\Models\CaseParticipated;
Expand Down Expand Up @@ -476,6 +477,87 @@ public function test_get_my_cases_counters_ok(): void
$response->assertJsonFragment(['totalMyRequest' => 5]);
}

public function test_get_all_cases_forbidden_without_view_all_cases_permission(): void
{
// Ensure the permission row exists and register it as a Laravel Gate
// so $user->can('view-all_cases') is enforceable in tests.
Permission::firstOrCreate(
['name' => 'view-all_cases'],
['title' => 'View All Cases'],
);
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));

$nonAdmin = User::factory()->create([
'is_administrator' => false,
]);

self::createCasesStartedForUser($nonAdmin->id, 3);

// Unscoped request (the "All cases" tab) — denied without the permission.
$response = $this->actingAs($nonAdmin, 'api')
->json('GET', route('api.1.1.cases.all_cases'));
$response->assertStatus(403);

// Granting the permission restores access to the unscoped query.
$nonAdmin->giveDirectPermission('view-all_cases');

$response = $this->actingAs($nonAdmin, 'api')
->json('GET', route('api.1.1.cases.all_cases'));
$response->assertStatus(200);
$response->assertJsonCount(3, 'data');
}

public function test_get_all_cases_allows_user_to_view_their_own_cases_without_permission(): void
{
// The endpoint is shared by the "My cases" tab, which scopes the
// query to the authenticated user. That self-scoped path must work
// even when the user lacks `view-all_cases`.
$nonAdmin = User::factory()->create([
'is_administrator' => false,
]);

$ownCases = self::createCasesStartedForUser($nonAdmin->id, 4);
$otherUser = self::createUser('other_user');
self::createCasesStartedForUser($otherUser->id, 6);

$response = $this->actingAs($nonAdmin, 'api')
->json('GET', route('api.1.1.cases.all_cases', ['userId' => $nonAdmin->id]));

$response->assertStatus(200);
$response->assertJsonCount($ownCases->count(), 'data');
$response->assertJsonMissing(['user_id' => $otherUser->id]);
}

public function test_get_all_cases_forbids_user_from_viewing_another_users_cases_without_permission(): void
{
Permission::firstOrCreate(
['name' => 'view-all_cases'],
['title' => 'View All Cases'],
);
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));

$nonAdmin = User::factory()->create([
'is_administrator' => false,
]);
$otherUser = self::createUser('other_user');
self::createCasesStartedForUser($otherUser->id, 2);

// Passing another user's id must require `view-all_cases`; otherwise
// any authenticated user could iterate userIds to enumerate the
// entire platform.
$response = $this->actingAs($nonAdmin, 'api')
->json('GET', route('api.1.1.cases.all_cases', ['userId' => $otherUser->id]));
$response->assertStatus(403);

// With the permission, the same request succeeds.
$nonAdmin->giveDirectPermission('view-all_cases');

$response = $this->actingAs($nonAdmin, 'api')
->json('GET', route('api.1.1.cases.all_cases', ['userId' => $otherUser->id]));
$response->assertStatus(200);
$response->assertJsonCount(2, 'data');
}

public function test_get_all_cases_participants(): void
{
$userA = $this->createUser('user_a');
Expand Down
56 changes: 56 additions & 0 deletions tests/Feature/CasesControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
namespace Tests\Feature;

use Carbon\Carbon;
use Illuminate\Support\Facades\Gate;
use ProcessMaker\Http\Controllers\CasesController;
use ProcessMaker\Models\Permission;
use ProcessMaker\Models\Process;
use ProcessMaker\Models\ProcessRequest;
use ProcessMaker\Models\ProcessRequestToken;
Expand Down Expand Up @@ -60,6 +62,60 @@ public function testShowCaseWithUserWithoutParticipation()
$response->assertStatus(403);
}

public function testCasesAllPageReturns403WithoutViewAllCasesPermission()
{
Permission::firstOrCreate(
['name' => 'view-all_cases'],
['title' => 'View All Cases'],
);
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));

$user = User::factory()->create(['is_administrator' => false]);
$this->actingAs($user);

$response = $this->get(route('cases-main.index', ['type' => 'all']));

$response->assertStatus(403);
// Confirms the standard ProcessMaker "Not Authorized" page renders
// rather than the cases shell with an empty list.
$response->assertSee('Not Authorized');
}

public function testCasesAllPageReturns200WithViewAllCasesPermission()
{
Permission::firstOrCreate(
['name' => 'view-all_cases'],
['title' => 'View All Cases'],
);
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));

$user = User::factory()->create(['is_administrator' => false]);
$user->giveDirectPermission('view-all_cases');
$this->actingAs($user);

$response = $this->get(route('cases-main.index', ['type' => 'all']));

$response->assertStatus(200);
$response->assertViewIs('cases.casesMain');
}

public function testCasesOtherTabsRemainAccessibleWithoutViewAllCasesPermission()
{
$user = User::factory()->create(['is_administrator' => false]);
$this->actingAs($user);

foreach (['in_progress', 'completed'] as $type) {
$response = $this->get(route('cases-main.index', ['type' => $type]));
$response->assertStatus(200);
$response->assertViewIs('cases.casesMain');
}

// Default `/cases` landing should also work for everyone.
$response = $this->get(route('cases-main.index'));
$response->assertStatus(200);
$response->assertViewIs('cases.casesMain');
}

public function testShowCaseWithUserAdmin()
{
// Create user admin
Expand Down
Loading