Skip to content

Commit 56a7785

Browse files
Merge branch 'develop' into epic/FOUR-26611
2 parents 11943c9 + 1b626f6 commit 56a7785

141 files changed

Lines changed: 7530 additions & 1744 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎ProcessMaker/Application.php‎

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace ProcessMaker;
44

55
use Igaster\LaravelTheme\Facades\Theme;
6+
use Illuminate\Container\Container;
67
use Illuminate\Filesystem\Filesystem;
78
use Illuminate\Foundation\Application as IlluminateApplication;
89
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
@@ -12,8 +13,8 @@
1213
use Illuminate\Support\Facades\App;
1314
use Illuminate\Support\Facades\Auth;
1415
use Illuminate\Support\Facades\Config;
16+
use ProcessMaker\Console\Kernel;
1517
use ProcessMaker\Multitenancy\Tenant;
16-
use ProcessMaker\Multitenancy\TenantBootstrapper;
1718

1819
/**
1920
* Class Application.
@@ -101,15 +102,4 @@ public function registerConfiguredProviders()
101102

102103
parent::registerConfiguredProviders();
103104
}
104-
105-
public function bootstrapWith(array $bootstrappers)
106-
{
107-
// Insert TenantBootstrapper after LoadEnvironmentVariables
108-
if ($bootstrappers[0] !== LoadEnvironmentVariables::class) {
109-
throw new \Exception('LoadEnvironmentVariables is not the first bootstrapper. Did a laravel upgrade change this?');
110-
}
111-
array_splice($bootstrappers, 1, 0, [TenantBootstrapper::class]);
112-
113-
return parent::bootstrapWith($bootstrappers);
114-
}
115105
}

‎ProcessMaker/AssignmentRules/ProcessManagerAssigned.php‎

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
use ProcessMaker\Exception\ThereIsNoProcessManagerAssignedException;
77
use ProcessMaker\Models\Process;
88
use ProcessMaker\Models\ProcessRequest;
9+
use ProcessMaker\Models\ProcessRequestToken;
10+
use ProcessMaker\Models\User;
911
use ProcessMaker\Nayra\Contracts\Bpmn\ActivityInterface;
1012
use ProcessMaker\Nayra\Contracts\Bpmn\TokenInterface;
1113

@@ -24,16 +26,62 @@ class ProcessManagerAssigned implements AssignmentRuleInterface
2426
* @param TokenInterface $token
2527
* @param Process $process
2628
* @param ProcessRequest $request
27-
* @return int
29+
* @return int|null
2830
* @throws ThereIsNoProcessManagerAssignedException
2931
*/
3032
public function getNextUser(ActivityInterface $task, TokenInterface $token, Process $process, ProcessRequest $request)
3133
{
32-
$user_id = $request->processVersion->manager_id;
34+
// review for multiple managers
35+
$managers = $request->processVersion->manager_id;
36+
$user_id = $this->getNextManagerAssigned($managers, $task, $request);
3337
if (!$user_id) {
3438
throw new ThereIsNoProcessManagerAssignedException($task);
3539
}
3640

3741
return $user_id;
3842
}
43+
44+
/**
45+
* Get the round robin manager using a true round robin algorithm
46+
*
47+
* @param array $managers
48+
* @param ActivityInterface $task
49+
* @param ProcessRequest $request
50+
* @return int|null
51+
*/
52+
private function getNextManagerAssigned($managers, $task, $request)
53+
{
54+
// Validate input
55+
if (empty($managers) || !is_array($managers)) {
56+
return null;
57+
}
58+
59+
// If only one manager, return it
60+
if (count($managers) === 1) {
61+
return $managers[0];
62+
}
63+
64+
// get the last manager assigned to the task across all requests
65+
$last = ProcessRequestToken::where('process_id', $request->process_id)
66+
->where('element_id', $task->getId())
67+
->whereIn('user_id', $managers)
68+
->orderBy('created_at', 'desc')
69+
->first();
70+
71+
$user_id = $last ? $last->user_id : null;
72+
73+
sort($managers);
74+
75+
$key = array_search($user_id, $managers);
76+
if ($key === false) {
77+
// If no previous manager found, start with the first manager
78+
$key = 0;
79+
} else {
80+
// Move to the next manager in the round-robin
81+
$key = ($key + 1) % count($managers);
82+
}
83+
$user_id = $managers[$key];
84+
85+
return $user_id;
86+
}
3987
}

‎ProcessMaker/Console/Commands/TenantsVerify.php‎

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class TenantsVerify extends Command
1919
*
2020
* @var string
2121
*/
22-
protected $signature = 'tenants:verify';
22+
protected $signature = 'tenants:verify {--json : Output the results as JSON}';
2323

2424
/**
2525
* The console command description.
@@ -33,20 +33,29 @@ class TenantsVerify extends Command
3333
*
3434
* @return int
3535
*/
36+
private $jsonData = [];
37+
3638
public function handle()
3739
{
40+
if (!config('app.multitenancy')) {
41+
$this->info('Multitenancy is disabled');
42+
43+
return;
44+
}
45+
46+
$errors = [];
3847
$currentTenant = null;
3948
if (app()->has('currentTenant')) {
4049
$currentTenant = app('currentTenant');
4150
}
4251

43-
if (config('app.multitenancy') && !$currentTenant) {
52+
if (!$currentTenant) {
4453
$this->error('Multitenancy enabled but no current tenant found.');
4554

4655
return;
4756
}
4857

49-
$this->info('Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE'));
58+
\Log::warning('TenantsVerify: Current Tenant ID: ' . ($currentTenant?->id ?? 'NONE'));
5059

5160
$paths = [
5261
['Storage Path', storage_path()],
@@ -55,32 +64,44 @@ public function handle()
5564
];
5665

5766
// Display paths in a nice table
58-
$this->table(['Path', 'Value'], $paths);
67+
$this->infoTable(['Path', 'Value'], $paths);
5968

6069
$configs = [
61-
'app.key',
62-
'app.url',
63-
'app.instance',
64-
'cache.prefix',
65-
'database.redis.options.prefix',
66-
'cache.stores.cache_settings.prefix',
67-
'script-runner-microservice.callback',
68-
'database.connections.processmaker.database',
69-
'logging.channels.daily.path',
70-
'filesystems.disks.public.root',
71-
'filesystems.disks.local.root',
72-
'filesystems.disks.lang.root',
70+
'app.key' => null,
71+
'app.url' => null,
72+
'app.instance' => null,
73+
'cache.prefix' => 'tenant_{tenant_id}:',
74+
'database.redis.options.prefix' => null,
75+
'cache.stores.cache_settings.prefix' => 'tenant_{tenant_id}:settings',
76+
'script-runner-microservice.callback' => null,
77+
'database.connections.processmaker.database' => null,
78+
'logging.channels.daily.path' => base_path() . '/storage/tenant_{tenant_id}/logs/processmaker.log',
79+
'filesystems.disks.public.root' => base_path() . '/storage/tenant_{tenant_id}/app/public',
80+
'filesystems.disks.local.root' => base_path() . '/storage/tenant_{tenant_id}/app',
81+
'filesystems.disks.lang.root' => base_path() . '/resources/lang/tenant_{tenant_id}',
7382
];
7483

75-
$configs = array_map(function ($config) {
84+
$configs = array_map(function ($config) use ($configs, $currentTenant, &$errors) {
85+
$ok = '';
86+
if ($configs[$config] !== null) {
87+
$expected = str_replace('{tenant_id}', $currentTenant->id, $configs[$config]);
88+
if (config($config) === $expected) {
89+
$ok = '✓';
90+
} else {
91+
$ok = '✗';
92+
$errors[] = 'Expected: ' . $expected . ' != Actual: ' . config($config);
93+
}
94+
}
95+
7696
return [
7797
$config,
7898
config($config),
99+
$ok,
79100
];
80-
}, $configs);
101+
}, array_keys($configs));
81102

82103
// Display configs in a nice table
83-
$this->table(['Config', 'Value'], $configs);
104+
$this->infoTable(['Config', 'Value', 'OK'], $configs);
84105

85106
$env = EnvironmentVariable::first();
86107
if (!$env) {
@@ -102,10 +123,62 @@ public function handle()
102123
['Tenant Config Is Cached', File::exists(app()->getCachedConfigPath()) ? 'Yes' : 'No'],
103124
['First username (database check)', User::first()?->username ?? 'No users found'],
104125
['Decrypted check', substr($decrypted, 0, 50)],
105-
['Original App URL (landlord)', $currentTenant?->getOriginalValue('APP_URL') ?? config('app.url')],
126+
// ['Original App URL (landlord)', $currentTenant?->getOriginalValue('APP_URL') ?? config('app.url')],
127+
['config("app.url")', config('app.url')],
128+
['getenv("APP_URL")', getenv('APP_URL')],
129+
['env("APP_URL")', env('APP_URL')],
130+
['$_SERVER["APP_URL"]', $_SERVER['APP_URL'] ?? 'NOT SET'],
131+
['$_ENV["APP_URL"]', $_ENV['APP_URL'] ?? 'NOT SET'],
132+
['Current PID', getmypid()],
106133
];
107134

108135
// Display other in a nice table
109-
$this->table(['Other', 'Value'], $other);
136+
$this->infoTable(['Other', 'Value'], $other);
137+
138+
$checkUrls = [
139+
'config("app.url")' => config('app.url'),
140+
'getenv("APP_URL")' => getenv('APP_URL'),
141+
'env("APP_URL")' => env('APP_URL'),
142+
'$_SERVER["APP_URL"]' => $_SERVER['APP_URL'] ?? 'NOT SET',
143+
'$_ENV["APP_URL"]' => $_ENV['APP_URL'] ?? 'NOT SET',
144+
];
145+
146+
foreach ($checkUrls as $key => $value) {
147+
if ($value !== $currentTenant?->config['app.url']) {
148+
$errors[] = 'Expected: ' . $key . ' to be ' . $currentTenant?->config['app.url'] . ' but got ' . $value;
149+
}
150+
}
151+
152+
$this->finish($errors);
153+
}
154+
155+
private function finish($errors)
156+
{
157+
if (count($errors) > 0) {
158+
$this->error('Errors found');
159+
} else {
160+
$this->info('No errors found');
161+
}
162+
163+
if ($this->option('json')) {
164+
$this->jsonData['Errors'] = $errors;
165+
$this->line(json_encode($this->jsonData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
166+
}
167+
}
168+
169+
private function infoTable($headers, $rows)
170+
{
171+
if ($this->option('json')) {
172+
$section = [];
173+
foreach ($rows as $row) {
174+
$section[$row[0]] = $row[1];
175+
}
176+
$this->jsonData[$headers[0]] = $section;
177+
} else {
178+
foreach ($rows as $row) {
179+
\Log::warning($row[0] . ': ' . $row[1]);
180+
}
181+
$this->table($headers, $rows);
182+
}
110183
}
111184
}

‎ProcessMaker/Console/Kernel.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ protected function schedule(Schedule $schedule)
8888
$schedule->command('metrics:clear')->cron("*/{$clearInterval} * * * *");
8989
break;
9090
}
91+
92+
// 5 minutes is recommended in https://laravel.com/docs/12.x/horizon#metrics
93+
$schedule->command('horizon:snapshot')->everyFiveMinutes();
9194
}
9295

9396
/**
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+
}

0 commit comments

Comments
 (0)