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
41 changes: 41 additions & 0 deletions ProcessMaker/Contracts/PermissionCacheInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace ProcessMaker\Contracts;

interface PermissionCacheInterface
{
/**
* Get cached permissions for a user
*/
public function getUserPermissions(int $userId): ?array;

/**
* Cache user permissions
*/
public function cacheUserPermissions(int $userId, array $permissions): void;

/**
* Get cached permissions for a group
*/
public function getGroupPermissions(int $groupId): ?array;

/**
* Cache group permissions
*/
public function cacheGroupPermissions(int $groupId, array $permissions): void;

/**
* Invalidate user permissions cache
*/
public function invalidateUserPermissions(int $userId): void;

/**
* Invalidate group permissions cache
*/
public function invalidateGroupPermissions(int $groupId): void;

/**
* Clear all permission caches
*/
public function clearAll(): void;
}
36 changes: 36 additions & 0 deletions ProcessMaker/Contracts/PermissionRepositoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace ProcessMaker\Contracts;

interface PermissionRepositoryInterface
{
/**
* Get all permissions for a user (direct + group permissions)
*/
public function getUserPermissions(int $userId): array;

/**
* Get direct user permissions
*/
public function getDirectUserPermissions(int $userId): array;

/**
* Get group permissions for a user
*/
public function getGroupPermissions(int $userId): array;

/**
* Check if user has a specific permission
*/
public function userHasPermission(int $userId, string $permission): bool;

/**
* Get permissions for a specific group
*/
public function getGroupPermissionsById(int $groupId): array;

/**
* Get nested group permissions (recursive)
*/
public function getNestedGroupPermissions(int $groupId): array;
}
21 changes: 21 additions & 0 deletions ProcessMaker/Contracts/PermissionStrategyInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace ProcessMaker\Contracts;

interface PermissionStrategyInterface
{
/**
* Check if user has permission using this strategy
*/
public function hasPermission(int $userId, string $permission): bool;

/**
* Get strategy name for identification
*/
public function getStrategyName(): string;

/**
* Check if this strategy can handle the permission check
*/
public function canHandle(string $permission): bool;
}
88 changes: 88 additions & 0 deletions ProcessMaker/Events/GroupMembershipChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace ProcessMaker\Events;

use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use ProcessMaker\Models\Group;
use ProcessMaker\Models\GroupMember;

class GroupMembershipChanged
{
use Dispatchable, SerializesModels;

public ?Group $group;

public ?Group $parentGroup;

public string $action; // 'added', 'removed', 'updated'

public ?GroupMember $groupMember;

/**
* Create a new event instance.
*/
public function __construct(Group $group, ?Group $parentGroup, string $action, ?GroupMember $groupMember = null)
{
$this->group = $group;
$this->parentGroup = $parentGroup;
$this->action = $action;
$this->groupMember = $groupMember;
}

/**
* Get the group that was affected
*/
public function getGroup(): ?Group
{
return $this->group;
}

/**
* Get the parent group (if any)
*/
public function getParentGroup(): ?Group
{
return $this->parentGroup;
}

/**
* Get the action performed
*/
public function getAction(): string
{
return $this->action;
}

/**
* Get the group member record
*/
public function getGroupMember(): ?GroupMember
{
return $this->groupMember;
}

/**
* Check if this is a removal action
*/
public function isRemoval(): bool
{
return $this->action === 'removed';
}

/**
* Check if this is an addition action
*/
public function isAddition(): bool
{
return $this->action === 'added';
}

/**
* Check if this is an update action
*/
public function isUpdate(): bool
{
return $this->action === 'updated';
}
}
20 changes: 20 additions & 0 deletions ProcessMaker/Events/PermissionUpdated.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,24 @@ public function getEventName(): string
{
return 'PermissionUpdated';
}

/**
* Get the user ID
*
* @return string|null
*/
public function getUserId(): ?string
{
return $this->userId;
}

/**
* Get the group ID
*
* @return string|null
*/
public function getGroupId(): ?string
{
return $this->groupId;
}
}
11 changes: 2 additions & 9 deletions ProcessMaker/Http/Controllers/Api/PermissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,9 @@ public function update(Request $request)
//Sync the entity's permissions with the database
$entity->permissions()->sync($permissions->pluck('id')->toArray());

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

return response([], 204);
}

private function clearAndRebuildCache($user)
{
// Rebuild and update the permissions cache
$permissions = $user->permissions()->pluck('name')->toArray();
Cache::put("user_{$user->id}_permissions", $permissions, 86400);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace ProcessMaker\Listeners;

use Illuminate\Support\Facades\Log;
use ProcessMaker\Events\GroupMembershipChanged;
use ProcessMaker\Models\Group;
use ProcessMaker\Models\User;
use ProcessMaker\Services\PermissionServiceManager;

class InvalidatePermissionCacheOnGroupHierarchyChange
{
private PermissionServiceManager $permissionService;

public function __construct(PermissionServiceManager $permissionService)
{
$this->permissionService = $permissionService;
}

/**
* Handle the event.
*/
public function handle(GroupMembershipChanged $event): void
{
try {
$group = $event->getGroup();
$action = $event->getAction();

// All actions (added, removed, updated) require the same cache invalidation logic
// because they all affect the permission hierarchy for the group and its descendants
$this->permissionService->invalidateAll();

Log::info("Successfully invalidated permission cache for group hierarchy change: {$action} for group {$group->id}");
} catch (\Exception $e) {
Log::error('Failed to invalidate permission cache on group hierarchy change', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'group_id' => $event->getGroup()->id ?? 'unknown',
'action' => $event->getAction(),
]);
throw $e;
}
}
}
43 changes: 43 additions & 0 deletions ProcessMaker/Listeners/InvalidatePermissionCacheOnUpdate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace ProcessMaker\Listeners;

use Illuminate\Support\Facades\Log;
use ProcessMaker\Events\PermissionUpdated;
use ProcessMaker\Services\PermissionServiceManager;

class InvalidatePermissionCacheOnUpdate
{
private PermissionServiceManager $permissionService;

public function __construct(PermissionServiceManager $permissionService)
{
$this->permissionService = $permissionService;
}

/**
* Handle the event.
*/
public function handle(PermissionUpdated $event): void
{
try {
// Invalidate cache for user if user permissions were updated
if ($event->getUserId()) {
$this->permissionService->invalidateUserCache((int) $event->getUserId());
}

// Invalidate cache for group if group permissions were updated
if ($event->getGroupId()) {
$this->permissionService->invalidateAll();
}
} catch (\Exception $e) {
Log::error('Failed to invalidate permission cache', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'userId' => $event->getUserId(),
'groupId' => $event->getGroupId(),
]);
throw $e; // Re-throw to ensure error is properly handled
}
}
}
2 changes: 1 addition & 1 deletion ProcessMaker/Managers/TaskSchedulerManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ public function scheduleTasks()
$task->save();
}
break;
case 'SCHEDULED_JOB':
case 'SCHEDULED_JOB':
$this->executeScheduledJob($config);
$task->last_execution = $today->format('Y-m-d H:i:s');
$task->save();
Expand Down
36 changes: 36 additions & 0 deletions ProcessMaker/Models/GroupMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace ProcessMaker\Models;

use ProcessMaker\Observers\GroupMemberObserver;

/**
* Represents a group Members definition.
*
Expand Down Expand Up @@ -83,6 +85,30 @@ class GroupMember extends ProcessMakerModel
'group_id', 'member_id', 'member_type',
];

/**
* Disable soft deletes for this model since the table doesn't have deleted_at column
*/
public function getDeletedAtColumn()
{
return null;
}

/**
* Disable soft deletes for this model
*/
public static function bootSoftDeletes()
{
// Do nothing - disable soft deletes
}

/**
* Override the query builder to not use soft deletes
*/
public function newEloquentBuilder($query)
{
return new \Illuminate\Database\Eloquent\Builder($query);
}

public static function rules()
{
return [
Expand All @@ -101,4 +127,14 @@ public function group()
{
return $this->belongsTo(Group::class);
}

/**
* Boot the model and register observers
*/
protected static function boot()
{
parent::boot();

static::observe(GroupMemberObserver::class);
}
}
Loading
Loading