Skip to content

Commit 76d4a6f

Browse files
committed
Merge remote-tracking branch 'origin/permissionPerformance' into feature/FOUR-19876
2 parents ca0c63b + 65f0fa9 commit 76d4a6f

37 files changed

Lines changed: 2903 additions & 22 deletions
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace ProcessMaker\Contracts;
4+
5+
interface PermissionCacheInterface
6+
{
7+
/**
8+
* Get cached permissions for a user
9+
*/
10+
public function getUserPermissions(int $userId): ?array;
11+
12+
/**
13+
* Cache user permissions
14+
*/
15+
public function cacheUserPermissions(int $userId, array $permissions): void;
16+
17+
/**
18+
* Get cached permissions for a group
19+
*/
20+
public function getGroupPermissions(int $groupId): ?array;
21+
22+
/**
23+
* Cache group permissions
24+
*/
25+
public function cacheGroupPermissions(int $groupId, array $permissions): void;
26+
27+
/**
28+
* Invalidate user permissions cache
29+
*/
30+
public function invalidateUserPermissions(int $userId): void;
31+
32+
/**
33+
* Invalidate group permissions cache
34+
*/
35+
public function invalidateGroupPermissions(int $groupId): void;
36+
37+
/**
38+
* Clear all permission caches
39+
*/
40+
public function clearAll(): void;
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace ProcessMaker\Contracts;
4+
5+
interface PermissionRepositoryInterface
6+
{
7+
/**
8+
* Get all permissions for a user (direct + group permissions)
9+
*/
10+
public function getUserPermissions(int $userId): array;
11+
12+
/**
13+
* Get direct user permissions
14+
*/
15+
public function getDirectUserPermissions(int $userId): array;
16+
17+
/**
18+
* Get group permissions for a user
19+
*/
20+
public function getGroupPermissions(int $userId): array;
21+
22+
/**
23+
* Check if user has a specific permission
24+
*/
25+
public function userHasPermission(int $userId, string $permission): bool;
26+
27+
/**
28+
* Get permissions for a specific group
29+
*/
30+
public function getGroupPermissionsById(int $groupId): array;
31+
32+
/**
33+
* Get nested group permissions (recursive)
34+
*/
35+
public function getNestedGroupPermissions(int $groupId): array;
36+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace ProcessMaker\Contracts;
4+
5+
interface PermissionStrategyInterface
6+
{
7+
/**
8+
* Check if user has permission using this strategy
9+
*/
10+
public function hasPermission(int $userId, string $permission): bool;
11+
12+
/**
13+
* Get strategy name for identification
14+
*/
15+
public function getStrategyName(): string;
16+
17+
/**
18+
* Check if this strategy can handle the permission check
19+
*/
20+
public function canHandle(string $permission): bool;
21+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
namespace ProcessMaker\Events;
4+
5+
use Illuminate\Foundation\Events\Dispatchable;
6+
use Illuminate\Queue\SerializesModels;
7+
use ProcessMaker\Models\Group;
8+
use ProcessMaker\Models\GroupMember;
9+
10+
class GroupMembershipChanged
11+
{
12+
use Dispatchable, SerializesModels;
13+
14+
public ?Group $group;
15+
16+
public ?Group $parentGroup;
17+
18+
public string $action; // 'added', 'removed', 'updated'
19+
20+
public ?GroupMember $groupMember;
21+
22+
/**
23+
* Create a new event instance.
24+
*/
25+
public function __construct(Group $group, ?Group $parentGroup, string $action, ?GroupMember $groupMember = null)
26+
{
27+
$this->group = $group;
28+
$this->parentGroup = $parentGroup;
29+
$this->action = $action;
30+
$this->groupMember = $groupMember;
31+
}
32+
33+
/**
34+
* Get the group that was affected
35+
*/
36+
public function getGroup(): ?Group
37+
{
38+
return $this->group;
39+
}
40+
41+
/**
42+
* Get the parent group (if any)
43+
*/
44+
public function getParentGroup(): ?Group
45+
{
46+
return $this->parentGroup;
47+
}
48+
49+
/**
50+
* Get the action performed
51+
*/
52+
public function getAction(): string
53+
{
54+
return $this->action;
55+
}
56+
57+
/**
58+
* Get the group member record
59+
*/
60+
public function getGroupMember(): ?GroupMember
61+
{
62+
return $this->groupMember;
63+
}
64+
65+
/**
66+
* Check if this is a removal action
67+
*/
68+
public function isRemoval(): bool
69+
{
70+
return $this->action === 'removed';
71+
}
72+
73+
/**
74+
* Check if this is an addition action
75+
*/
76+
public function isAddition(): bool
77+
{
78+
return $this->action === 'added';
79+
}
80+
81+
/**
82+
* Check if this is an update action
83+
*/
84+
public function isUpdate(): bool
85+
{
86+
return $this->action === 'updated';
87+
}
88+
}

ProcessMaker/Events/PermissionUpdated.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,24 @@ public function getEventName(): string
148148
{
149149
return 'PermissionUpdated';
150150
}
151+
152+
/**
153+
* Get the user ID
154+
*
155+
* @return string|null
156+
*/
157+
public function getUserId(): ?string
158+
{
159+
return $this->userId;
160+
}
161+
162+
/**
163+
* Get the group ID
164+
*
165+
* @return string|null
166+
*/
167+
public function getGroupId(): ?string
168+
{
169+
return $this->groupId;
170+
}
151171
}

ProcessMaker/Http/Controllers/Api/PermissionController.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,9 @@ public function update(Request $request)
122122
//Sync the entity's permissions with the database
123123
$entity->permissions()->sync($permissions->pluck('id')->toArray());
124124

125-
// Clear user permissions cache and rebuild
126-
$this->clearAndRebuildCache($entity);
125+
// The PermissionUpdated event will automatically trigger cache invalidation
126+
// via the InvalidatePermissionCacheOnUpdate listener
127127

128128
return response([], 204);
129129
}
130-
131-
private function clearAndRebuildCache($user)
132-
{
133-
// Rebuild and update the permissions cache
134-
$permissions = $user->permissions()->pluck('name')->toArray();
135-
Cache::put("user_{$user->id}_permissions", $permissions, 86400);
136-
}
137130
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace ProcessMaker\Listeners;
4+
5+
use Illuminate\Support\Facades\Log;
6+
use ProcessMaker\Events\GroupMembershipChanged;
7+
use ProcessMaker\Models\Group;
8+
use ProcessMaker\Models\User;
9+
use ProcessMaker\Services\PermissionServiceManager;
10+
11+
class InvalidatePermissionCacheOnGroupHierarchyChange
12+
{
13+
private PermissionServiceManager $permissionService;
14+
15+
public function __construct(PermissionServiceManager $permissionService)
16+
{
17+
$this->permissionService = $permissionService;
18+
}
19+
20+
/**
21+
* Handle the event.
22+
*/
23+
public function handle(GroupMembershipChanged $event): void
24+
{
25+
try {
26+
$group = $event->getGroup();
27+
$action = $event->getAction();
28+
29+
// All actions (added, removed, updated) require the same cache invalidation logic
30+
// because they all affect the permission hierarchy for the group and its descendants
31+
$this->permissionService->invalidateAll();
32+
33+
Log::info("Successfully invalidated permission cache for group hierarchy change: {$action} for group {$group->id}");
34+
} catch (\Exception $e) {
35+
Log::error('Failed to invalidate permission cache on group hierarchy change', [
36+
'error' => $e->getMessage(),
37+
'trace' => $e->getTraceAsString(),
38+
'group_id' => $event->getGroup()->id ?? 'unknown',
39+
'action' => $event->getAction(),
40+
]);
41+
throw $e;
42+
}
43+
}
44+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace ProcessMaker\Listeners;
4+
5+
use Illuminate\Support\Facades\Log;
6+
use ProcessMaker\Events\PermissionUpdated;
7+
use ProcessMaker\Services\PermissionServiceManager;
8+
9+
class InvalidatePermissionCacheOnUpdate
10+
{
11+
private PermissionServiceManager $permissionService;
12+
13+
public function __construct(PermissionServiceManager $permissionService)
14+
{
15+
$this->permissionService = $permissionService;
16+
}
17+
18+
/**
19+
* Handle the event.
20+
*/
21+
public function handle(PermissionUpdated $event): void
22+
{
23+
try {
24+
// Invalidate cache for user if user permissions were updated
25+
if ($event->getUserId()) {
26+
$this->permissionService->invalidateUserCache((int) $event->getUserId());
27+
}
28+
29+
// Invalidate cache for group if group permissions were updated
30+
if ($event->getGroupId()) {
31+
$this->permissionService->invalidateAll();
32+
}
33+
} catch (\Exception $e) {
34+
Log::error('Failed to invalidate permission cache', [
35+
'error' => $e->getMessage(),
36+
'trace' => $e->getTraceAsString(),
37+
'userId' => $event->getUserId(),
38+
'groupId' => $event->getGroupId(),
39+
]);
40+
throw $e; // Re-throw to ensure error is properly handled
41+
}
42+
}
43+
}

ProcessMaker/Managers/TaskSchedulerManager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ public function scheduleTasks()
195195
$task->save();
196196
}
197197
break;
198-
case 'SCHEDULED_JOB':
198+
case 'SCHEDULED_JOB':
199199
$this->executeScheduledJob($config);
200200
$task->last_execution = $today->format('Y-m-d H:i:s');
201201
$task->save();

ProcessMaker/Models/GroupMember.php

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

33
namespace ProcessMaker\Models;
44

5+
use ProcessMaker\Observers\GroupMemberObserver;
6+
57
/**
68
* Represents a group Members definition.
79
*
@@ -83,6 +85,30 @@ class GroupMember extends ProcessMakerModel
8385
'group_id', 'member_id', 'member_type',
8486
];
8587

88+
/**
89+
* Disable soft deletes for this model since the table doesn't have deleted_at column
90+
*/
91+
public function getDeletedAtColumn()
92+
{
93+
return null;
94+
}
95+
96+
/**
97+
* Disable soft deletes for this model
98+
*/
99+
public static function bootSoftDeletes()
100+
{
101+
// Do nothing - disable soft deletes
102+
}
103+
104+
/**
105+
* Override the query builder to not use soft deletes
106+
*/
107+
public function newEloquentBuilder($query)
108+
{
109+
return new \Illuminate\Database\Eloquent\Builder($query);
110+
}
111+
86112
public static function rules()
87113
{
88114
return [
@@ -101,4 +127,14 @@ public function group()
101127
{
102128
return $this->belongsTo(Group::class);
103129
}
130+
131+
/**
132+
* Boot the model and register observers
133+
*/
134+
protected static function boot()
135+
{
136+
parent::boot();
137+
138+
static::observe(GroupMemberObserver::class);
139+
}
104140
}

0 commit comments

Comments
 (0)