Skip to content

Commit f8c96ed

Browse files
committed
Merge branch 'develop' into test-custom-env-vars-fx
2 parents 5152d43 + 1d2cc90 commit f8c96ed

10 files changed

Lines changed: 181 additions & 66 deletions

File tree

ProcessMaker/Http/Controllers/Api/V1_1/CaseController.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ public function __construct(private Request $request, CaseApiRepository $caseRep
4343
*/
4444
public function getAllCases(CaseListRequest $request): JsonResponse
4545
{
46+
// Users are always allowed to view cases scoped to themselves
47+
// Any broader query requires the `view-all_cases` permission.
48+
// Admins pass through via Gate::before in AuthServiceProvider.
49+
$authUser = Auth::user();
50+
$requestedUserId = $request->filled('userId') ? (int) $request->input('userId') : null;
51+
$isViewingOwnCases = $requestedUserId !== null && $requestedUserId === $authUser->id;
52+
53+
if (!$isViewingOwnCases && !$authUser->can('view-all_cases')) {
54+
abort(403);
55+
}
56+
4657
$query = $this->caseRepository->getAllCases($request);
4758

4859
return $this->paginateResponse($query);

ProcessMaker/Http/Controllers/CasesController.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,21 @@ class CasesController extends Controller
2222
/**
2323
* Get the list of requests.
2424
*
25+
* @param string|null $type One of `all|in_progress|completed` (constrained by the route).
26+
*
2527
* @return \Illuminate\View\View|\Illuminate\Contracts\View
2628
*/
27-
public function index()
29+
public function index($type = null)
2830
{
31+
// The "All cases" tab exposes every case in the platform regardless
32+
// of the user's relationship to it, so it is gated by the
33+
// `view-all_cases` permission. The other tabs (My cases, In progress,
34+
// Completed) are scoped to the user and need no gate.
35+
// Admins bypass this check via Gate::before in AuthServiceProvider.
36+
if ($type === 'all' && !Auth::user()->can('view-all_cases')) {
37+
abort(403);
38+
}
39+
2940
$manager = app(ScreenBuilderManager::class);
3041
event(new ScreenBuilderStarting($manager, 'FORM'));
3142
$currentUser = Auth::user()->only(['id', 'username', 'fullname', 'firstname', 'lastname', 'avatar']);

composer.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "processmaker/processmaker",
3-
"version": "2026.8.2",
3+
"version": "2026.9.1",
44
"description": "BPM PHP Software",
55
"keywords": [
66
"php bpm processmaker"
@@ -113,7 +113,7 @@
113113
"Gmail"
114114
],
115115
"processmaker": {
116-
"build": "90d83217",
116+
"build": "5c91234c",
117117
"cicd-enabled": true,
118118
"custom": {
119119
"package-ellucian-ethos": "1.19.10",
@@ -250,4 +250,4 @@
250250
"ignore": []
251251
}
252252
}
253-
}
253+
}

database/migrations/2026_05_19_102552_update_passport_schema.php

Lines changed: 0 additions & 57 deletions
This file was deleted.

database/seeders/UserSeeder.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ public function run(ClientRepository $clients)
5555
]);
5656

5757
// Create client so we can generate tokens
58-
$personalAccessClient = $clients->createPersonalAccessGrantClient('PmApi');
59-
$personalAccessClient->save();
58+
$clients->createPersonalAccessGrantClient('PmApi');
6059

6160
// Create client OAuth (for 3-legged auth) - Authorization Code Grant for Swagger UI
6261
$clients->createAuthorizationCodeGrantClient(

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@processmaker/processmaker",
3-
"version": "2026.8.2",
3+
"version": "2026.9.1",
44
"description": "ProcessMaker 4",
55
"author": "DevOps <devops@processmaker.com>",
66
"license": "ISC",

tests/Feature/Api/V1_1/CaseControllerSearchTest.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Tests\Feature\Api\V1_1;
44

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

1820
$this->user = CaseControllerTest::createUser('user_a');
21+
22+
// These tests intentionally exercise the unscoped `get_all_cases`
23+
// endpoint, which now requires `view-all_cases`. Grant the
24+
// non-admin test user the permission so each `apiCall` reaches
25+
// the search logic rather than being short-circuited with a 403.
26+
Permission::firstOrCreate(
27+
['name' => 'view-all_cases'],
28+
['title' => 'View All Cases'],
29+
);
30+
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));
31+
$this->user->giveDirectPermission('view-all_cases');
1932
}
2033

2134
public function tearDown(): void

tests/Feature/Api/V1_1/CaseControllerTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Tests\Feature\Api\V1_1;
44

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

480+
public function test_get_all_cases_forbidden_without_view_all_cases_permission(): void
481+
{
482+
// Ensure the permission row exists and register it as a Laravel Gate
483+
// so $user->can('view-all_cases') is enforceable in tests.
484+
Permission::firstOrCreate(
485+
['name' => 'view-all_cases'],
486+
['title' => 'View All Cases'],
487+
);
488+
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));
489+
490+
$nonAdmin = User::factory()->create([
491+
'is_administrator' => false,
492+
]);
493+
494+
self::createCasesStartedForUser($nonAdmin->id, 3);
495+
496+
// Unscoped request (the "All cases" tab) — denied without the permission.
497+
$response = $this->actingAs($nonAdmin, 'api')
498+
->json('GET', route('api.1.1.cases.all_cases'));
499+
$response->assertStatus(403);
500+
501+
// Granting the permission restores access to the unscoped query.
502+
$nonAdmin->giveDirectPermission('view-all_cases');
503+
504+
$response = $this->actingAs($nonAdmin, 'api')
505+
->json('GET', route('api.1.1.cases.all_cases'));
506+
$response->assertStatus(200);
507+
$response->assertJsonCount(3, 'data');
508+
}
509+
510+
public function test_get_all_cases_allows_user_to_view_their_own_cases_without_permission(): void
511+
{
512+
// The endpoint is shared by the "My cases" tab, which scopes the
513+
// query to the authenticated user. That self-scoped path must work
514+
// even when the user lacks `view-all_cases`.
515+
$nonAdmin = User::factory()->create([
516+
'is_administrator' => false,
517+
]);
518+
519+
$ownCases = self::createCasesStartedForUser($nonAdmin->id, 4);
520+
$otherUser = self::createUser('other_user');
521+
self::createCasesStartedForUser($otherUser->id, 6);
522+
523+
$response = $this->actingAs($nonAdmin, 'api')
524+
->json('GET', route('api.1.1.cases.all_cases', ['userId' => $nonAdmin->id]));
525+
526+
$response->assertStatus(200);
527+
$response->assertJsonCount($ownCases->count(), 'data');
528+
$response->assertJsonMissing(['user_id' => $otherUser->id]);
529+
}
530+
531+
public function test_get_all_cases_forbids_user_from_viewing_another_users_cases_without_permission(): void
532+
{
533+
Permission::firstOrCreate(
534+
['name' => 'view-all_cases'],
535+
['title' => 'View All Cases'],
536+
);
537+
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));
538+
539+
$nonAdmin = User::factory()->create([
540+
'is_administrator' => false,
541+
]);
542+
$otherUser = self::createUser('other_user');
543+
self::createCasesStartedForUser($otherUser->id, 2);
544+
545+
// Passing another user's id must require `view-all_cases`; otherwise
546+
// any authenticated user could iterate userIds to enumerate the
547+
// entire platform.
548+
$response = $this->actingAs($nonAdmin, 'api')
549+
->json('GET', route('api.1.1.cases.all_cases', ['userId' => $otherUser->id]));
550+
$response->assertStatus(403);
551+
552+
// With the permission, the same request succeeds.
553+
$nonAdmin->giveDirectPermission('view-all_cases');
554+
555+
$response = $this->actingAs($nonAdmin, 'api')
556+
->json('GET', route('api.1.1.cases.all_cases', ['userId' => $otherUser->id]));
557+
$response->assertStatus(200);
558+
$response->assertJsonCount(2, 'data');
559+
}
560+
479561
public function test_get_all_cases_participants(): void
480562
{
481563
$userA = $this->createUser('user_a');

tests/Feature/CasesControllerTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Tests\Feature;
44

55
use Carbon\Carbon;
6+
use Illuminate\Support\Facades\Gate;
67
use ProcessMaker\Http\Controllers\CasesController;
8+
use ProcessMaker\Models\Permission;
79
use ProcessMaker\Models\Process;
810
use ProcessMaker\Models\ProcessRequest;
911
use ProcessMaker\Models\ProcessRequestToken;
@@ -60,6 +62,60 @@ public function testShowCaseWithUserWithoutParticipation()
6062
$response->assertStatus(403);
6163
}
6264

65+
public function testCasesAllPageReturns403WithoutViewAllCasesPermission()
66+
{
67+
Permission::firstOrCreate(
68+
['name' => 'view-all_cases'],
69+
['title' => 'View All Cases'],
70+
);
71+
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));
72+
73+
$user = User::factory()->create(['is_administrator' => false]);
74+
$this->actingAs($user);
75+
76+
$response = $this->get(route('cases-main.index', ['type' => 'all']));
77+
78+
$response->assertStatus(403);
79+
// Confirms the standard ProcessMaker "Not Authorized" page renders
80+
// rather than the cases shell with an empty list.
81+
$response->assertSee('Not Authorized');
82+
}
83+
84+
public function testCasesAllPageReturns200WithViewAllCasesPermission()
85+
{
86+
Permission::firstOrCreate(
87+
['name' => 'view-all_cases'],
88+
['title' => 'View All Cases'],
89+
);
90+
Gate::define('view-all_cases', fn ($user) => $user->hasPermission('view-all_cases'));
91+
92+
$user = User::factory()->create(['is_administrator' => false]);
93+
$user->giveDirectPermission('view-all_cases');
94+
$this->actingAs($user);
95+
96+
$response = $this->get(route('cases-main.index', ['type' => 'all']));
97+
98+
$response->assertStatus(200);
99+
$response->assertViewIs('cases.casesMain');
100+
}
101+
102+
public function testCasesOtherTabsRemainAccessibleWithoutViewAllCasesPermission()
103+
{
104+
$user = User::factory()->create(['is_administrator' => false]);
105+
$this->actingAs($user);
106+
107+
foreach (['in_progress', 'completed'] as $type) {
108+
$response = $this->get(route('cases-main.index', ['type' => $type]));
109+
$response->assertStatus(200);
110+
$response->assertViewIs('cases.casesMain');
111+
}
112+
113+
// Default `/cases` landing should also work for everyone.
114+
$response = $this->get(route('cases-main.index'));
115+
$response->assertStatus(200);
116+
$response->assertViewIs('cases.casesMain');
117+
}
118+
63119
public function testShowCaseWithUserAdmin()
64120
{
65121
// Create user admin

0 commit comments

Comments
 (0)