Skip to content

Commit 97b618b

Browse files
authored
Merge pull request #8552 from ProcessMaker/task/FOUR-26776
Process Manager Granular Permissions
2 parents 74b55ee + af97cf6 commit 97b618b

5 files changed

Lines changed: 216 additions & 8 deletions

File tree

ProcessMaker/Http/Controllers/Api/TaskController.php

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,15 @@ public function index(Request $request, $getTotal = false, User $user = null)
146146

147147
$this->applyAdvancedFilter($query, $request);
148148

149-
$this->applyForCurrentUser($query, $user);
150-
151-
// Apply filter overdue
152-
$query->overdue($request->input('overdue'));
153-
154149
if ($request->input('processesIManage') === 'true') {
155150
$this->applyProcessManager($query, $user);
151+
} else {
152+
$this->applyForCurrentUser($query, $user);
156153
}
157154

155+
// Apply filter overdue
156+
$query->overdue($request->input('overdue'));
157+
158158
// If only the total is being requested (by a Saved Search), send it now
159159
if ($getTotal === true) {
160160
return $query->count();
@@ -168,6 +168,11 @@ public function index(Request $request, $getTotal = false, User $user = null)
168168

169169
$response = $this->applyUserFilter($response, $request, $user);
170170

171+
if ($response->total() > 0 && $request->input('processesIManage') === 'true') {
172+
// enable user manager in cache
173+
$this->enableUserManager($user);
174+
}
175+
171176
$inOverdueQuery = ProcessRequestToken::query()
172177
->whereIn('id', $response->pluck('id'))
173178
->where('due_at', '<', Carbon::now());

ProcessMaker/Http/Kernel.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ class Kernel extends HttpKernel
9191
'session_kill' => Middleware\SessionControlKill::class,
9292
'no-cache' => Middleware\NoCache::class,
9393
'admin' => Middleware\IsAdmin::class,
94+
'manager' => Middleware\IsManager::class,
9495
'etag' => Middleware\Etag\HandleEtag::class,
9596
'file_size_check' => Middleware\FileSizeCheck::class,
9697
];
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
<?php
2+
3+
namespace ProcessMaker\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Cache;
8+
use Illuminate\Support\Facades\Log;
9+
use Symfony\Component\HttpFoundation\Response;
10+
11+
class IsManager
12+
{
13+
/**
14+
* Handle an incoming request.
15+
*
16+
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
17+
*/
18+
public function handle(Request $request, Closure $next): Response
19+
{
20+
$user = $request->user();
21+
22+
if (!$user) {
23+
return abort(401, 'Unauthenticated');
24+
}
25+
26+
// if user is administrator, allow access
27+
if ($user->is_administrator) {
28+
return $next($request);
29+
}
30+
31+
if (!Cache::get("user_{$user->id}_manager")) {
32+
// if user is not manager, continue
33+
return $next($request);
34+
}
35+
36+
// get the required permissions for this specific URL
37+
$requiredPermissions = $this->getRequiredPermissionsForRequest($request);
38+
39+
if (empty($requiredPermissions)) {
40+
// if no required permissions, continue
41+
return $next($request);
42+
}
43+
44+
// simulate that the user has all the necessary permissions for this request
45+
$this->simulateRequiredPermissionsForRequest($user, $requiredPermissions);
46+
47+
try {
48+
// process the request - the internal endpoints will handle the permission validation
49+
$response = $next($request);
50+
51+
// clean up the simulated permissions after processing the request
52+
$this->cleanupSimulatedPermission($user);
53+
54+
return $response;
55+
} catch (\Exception $e) {
56+
// make sure to clean up the simulated permissions even if there is an exception
57+
$this->cleanupSimulatedPermission($user);
58+
throw $e;
59+
}
60+
}
61+
62+
/**
63+
* Simula que el usuario tiene los permisos requeridos solo para esta solicitud
64+
*/
65+
private function simulateRequiredPermissionsForRequest($user, array $requiredPermissions)
66+
{
67+
try {
68+
// get the current permissions of the user
69+
$currentPermissions = $user->loadPermissions();
70+
71+
// filter only the permissions that the user does not have
72+
$permissionsToAdd = array_diff($requiredPermissions, $currentPermissions);
73+
74+
if (empty($permissionsToAdd)) {
75+
return;
76+
}
77+
78+
// simulate the permissions by adding them temporarily to the cache of permissions
79+
$cacheKey = "user_{$user->id}_permissions";
80+
$simulatedPermissions = array_merge($currentPermissions, $permissionsToAdd);
81+
82+
// save in cache temporarily (only for this request)
83+
// use a very short time to expire quickly if not cleaned manually
84+
Cache::put($cacheKey, $simulatedPermissions, 5); // 5 segundos como fallback
85+
} catch (\Exception $e) {
86+
Log::error('IsManager middleware - Error simulating permissions: ' . $e->getMessage());
87+
}
88+
}
89+
90+
/**
91+
* clean up the simulated permissions from the cache after processing the request
92+
*/
93+
private function cleanupSimulatedPermission($user)
94+
{
95+
try {
96+
$cacheKey = "user_{$user->id}_permissions";
97+
98+
// delete the cache to force the reload of real permissions
99+
Cache::forget($cacheKey);
100+
} catch (\Exception $e) {
101+
Log::error('IsManager middleware - Error cleaning up simulated permissions: ' . $e->getMessage());
102+
}
103+
}
104+
105+
/**
106+
* get the required permissions for the current URL
107+
*/
108+
private function getRequiredPermissionsForRequest(Request $request): array
109+
{
110+
$permissions = [];
111+
112+
try {
113+
$url = $request->fullUrl();
114+
$path = $request->path();
115+
$method = $request->method();
116+
117+
// first, get permissions from middlewares of the route
118+
$middlewarePermissions = $this->getPermissionsFromMiddlewares($request);
119+
$permissions = array_merge($permissions, $middlewarePermissions);
120+
121+
// then, get permissions based on URL patterns
122+
$urlPermissions = $this->getPermissionsFromUrlPatterns($url, $path, $method);
123+
$permissions = array_merge($permissions, $urlPermissions);
124+
} catch (\Exception $e) {
125+
Log::error('IsManager middleware - Error getting required permissions: ' . $e->getMessage());
126+
}
127+
128+
return array_unique($permissions);
129+
}
130+
131+
/**
132+
* get permissions from the middlewares of the route
133+
*/
134+
private function getPermissionsFromMiddlewares(Request $request): array
135+
{
136+
$permissions = [];
137+
138+
try {
139+
// get all the middlewares of the route
140+
$middlewares = $request->route()->middleware();
141+
142+
// filter only the middlewares that contain 'can:'
143+
$permissionMiddlewares = array_filter($middlewares, function ($middleware) {
144+
return str_contains($middleware, 'can:');
145+
});
146+
147+
// extract the permissions from each middleware
148+
foreach ($permissionMiddlewares as $middleware) {
149+
// format: "can:permission" or "can:permission,model"
150+
if (preg_match('/can:([^,]+)/', $middleware, $matches)) {
151+
$permissions[] = $matches[1];
152+
}
153+
}
154+
} catch (\Exception $e) {
155+
Log::error('IsManager middleware - Error getting permissions from middlewares: ' . $e->getMessage());
156+
}
157+
158+
return $permissions;
159+
}
160+
161+
/**
162+
* get permissions based on URL patterns
163+
*/
164+
private function getPermissionsFromUrlPatterns(string $url, string $path, string $method): array
165+
{
166+
$permissions = [];
167+
168+
// for now we only support GET methods
169+
if ($method !== 'GET') {
170+
return $permissions;
171+
}
172+
173+
try {
174+
// URL patterns and their corresponding permissions
175+
$urlPatterns = [
176+
// patterns for users
177+
'/api\/.*\/users(\?.*)?$/' => 'view-users',
178+
179+
// patterns for saved searches
180+
'/api\/.*\/saved-searches\/columns(\?.*)?$/' => 'view-saved-searches-columns',
181+
];
182+
183+
// check each pattern
184+
foreach ($urlPatterns as $pattern => $permission) {
185+
if (preg_match($pattern, $url)) {
186+
$permissions[] = $permission;
187+
}
188+
}
189+
} catch (\Exception $e) {
190+
Log::error('IsManager middleware - Error getting permissions from URL patterns: ' . $e->getMessage());
191+
}
192+
193+
return $permissions;
194+
}
195+
}

ProcessMaker/Traits/TaskControllerIndexMethods.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use DB;
77
use Illuminate\Database\QueryException;
88
use Illuminate\Support\Arr;
9+
use Illuminate\Support\Facades\Cache;
910
use Illuminate\Support\Str;
1011
use ProcessMaker\Filters\Filter;
1112
use ProcessMaker\Managers\DataManager;
@@ -344,8 +345,14 @@ public function applyProcessManager($query, $user)
344345
});
345346
}
346347

348+
private function enableUserManager($user)
349+
{
350+
// enable user in cache
351+
Cache::put("user_{$user->id}_manager", true);
352+
}
353+
347354
/**
348-
* Get the ID of the default saved search for tasks.
355+
* Get the ID of the default saved search for tasks
349356
*
350357
* @return int|null
351358
*/

routes/api.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
use ProcessMaker\Http\Controllers\Auth\TwoFactorAuthController;
4646
use ProcessMaker\Http\Controllers\TestStatusController;
4747

48-
Route::middleware('auth:api', 'setlocale', 'bindings', 'sanitize')->prefix('api/1.0')->name('api.')->group(function () {
48+
Route::middleware('auth:api', 'setlocale', 'bindings', 'sanitize', 'manager')->prefix('api/1.0')->name('api.')->group(function () {
4949
// Users
5050
Route::get('users', [UserController::class, 'index'])->name('users.index'); // Permissions handled in the controller
5151
Route::get('users/{user}', [UserController::class, 'show'])->name('users.show'); // Permissions handled in the controller
@@ -448,6 +448,6 @@
448448
});
449449

450450
// Slack Connector Validation
451-
Route::post('connector-slack/validate-token', [\ProcessMaker\Packages\Connectors\Slack\Controllers\SlackController::class, 'validateToken'])->name('connector-slack.validate-token');
451+
Route::post('connector-slack/validate-token', [ProcessMaker\Packages\Connectors\Slack\Controllers\SlackController::class, 'validateToken'])->name('connector-slack.validate-token');
452452
});
453453
Route::post('devlink/bundle-updated/{bundle}/{token}', [DevLinkController::class, 'bundleUpdated'])->name('devlink.bundle-updated');

0 commit comments

Comments
 (0)