Skip to content
Open
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
68 changes: 68 additions & 0 deletions ProcessMaker/Http/Controllers/Api/UserController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rule;
use ProcessMaker\Events\UserCreated;
use ProcessMaker\Events\UserDeleted;
use ProcessMaker\Events\UserGroupMembershipUpdated;
Expand Down Expand Up @@ -63,6 +64,7 @@ class UserController extends Controller
'timezone',
'datetime_format',
'status',
'delegation_user_id',
'avatar',
'preferences_2fa',
'connected_accounts',
Expand All @@ -79,6 +81,17 @@ class UserController extends Controller
'disableRecommendations',
];

/**
* Statuses a personal-profile user may set on their own account.
*
* @var array<string>
*/
private const SELF_SERVICE_STATUSES = [
'ACTIVE',
'INACTIVE',
'OUT_OF_OFFICE',
];

/**
* Display a listing of the resource.
*
Expand Down Expand Up @@ -503,8 +516,23 @@ public function update(User $user, Request $request)
$isSelfServiceUpdate = $this->authorizeSelfServiceUpdate($authenticatedUser, $user, $fields);
$rules = User::rules($user);
if ($isSelfServiceUpdate) {
$fields = $this->forgetUnassignableDelegation($fields);
if (array_key_exists('delegation_user_id', $fields)) {
$request->json()->set('delegation_user_id', $fields['delegation_user_id']);
}
$rules['meta'] = ['sometimes', 'array'];
$rules['meta.disableRecommendations'] = ['sometimes', 'boolean'];
$rules['status'] = ['required', Rule::in($this->selfServiceStatuses($user))];
$rules['delegation_user_id'] = [
'nullable',
'integer',
Rule::exists('users', 'id')->where(function ($query) {
$query->where('is_system', false)
->whereNull('deleted_at')
->where('status', 'ACTIVE');
}),
Rule::notIn([(string) $user->id]),
];
Comment thread
cursor[bot] marked this conversation as resolved.
}
$request->validate($rules);
if ($isSelfServiceUpdate) {
Expand Down Expand Up @@ -646,6 +674,46 @@ private function authorizeSelfServiceUpdate(User $authenticatedUser, User $targe
return true;
}

/**
* Statuses this user may submit. An administrator-assigned status that is
* not otherwise available stays valid so saving the profile does not
* reject the account's current value.
*
* @return array<string>
*/
private function selfServiceStatuses(User $user): array
{
$statuses = self::SELF_SERVICE_STATUSES;
if ($user->status && !in_array($user->status, $statuses, true)) {
$statuses[] = $user->status;
}

return $statuses;
}

/**
* A replacement the profile picker can no longer load should not fail the
* rest of the save. Clear missing, deleted, system, and non-ACTIVE users.
*/
private function forgetUnassignableDelegation(array $fields): array
Comment thread
mcraeteisha marked this conversation as resolved.
{
if (empty($fields['delegation_user_id'])) {
return $fields;
}

$delegate = User::withTrashed()->find($fields['delegation_user_id']);
if (
!$delegate
|| $delegate->trashed()
|| $delegate->is_system
|| $delegate->status !== 'ACTIVE'
) {
$fields['delegation_user_id'] = null;
}

return $fields;
}

/**
* Merge self-service metadata into the persisted server-managed values.
*/
Expand Down
6 changes: 4 additions & 2 deletions ProcessMaker/Http/Controllers/ProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,13 @@ function ($result, $item) {
$currentUser->preferences_2fa = $currentUser->getValid2FAPreferences();
$is2FAEnabledForGroup = $currentUser->in2FAGroupOrIndependent();

$addons = $this->getPluginAddons('edit', []);
$addons = $this->getPluginAddons('edit', ['user' => $currentUser]);
$addonsSettings = $this->getPluginAddons('edit.settings', ['user' => $currentUser]);

return view('profile.edit',
compact('currentUser', 'states', 'timezones', 'countries', 'datetimeFormats',
'status', 'enabled2FA', 'global2FAEnabled', 'is2FAEnabledForGroup', 'addons', 'ssoUser'));
'status', 'enabled2FA', 'global2FAEnabled', 'is2FAEnabledForGroup', 'addons',
'addonsSettings', 'ssoUser'));
}

/**
Expand Down
12 changes: 8 additions & 4 deletions ProcessMaker/Models/Process.php
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,7 @@ public function scalateToManagerIfEnabled($user, $activity, $token, $assignmentT
if (app()->bound('workflow.UserManager') && $assignmentProcess) {
$config = json_decode($activity->getProperty('config', '{}'), true);
$escalateToManager = $config['escalateToManager'] ?? false;
$assignee = User::find($user);
if ($escalateToManager) {
$user = WorkflowUserManager::escalateToManager($token, $user);
} else {
Expand All @@ -732,7 +733,10 @@ public function scalateToManagerIfEnabled($user, $activity, $token, $assignmentT
'process_id' => $this->id,
'request_id' => $token->getInstance()->getId(),
]);
$user = $res['assign_to'];
$assignedUserId = $res['assign_to'] ?? null;
$user = $assignee
? $assignee->acceptResolvedAssignee($assignedUserId)
: $assignedUserId;
}
}
}
Expand Down Expand Up @@ -806,9 +810,9 @@ private function getNextUserFromProcessVariable($activity, $token)
$outOfOffice = User::whereIn('id', array_unique($assignedUsers))->where('status', 'OUT_OF_OFFICE')->get();

foreach ($outOfOffice as $user) {
$delegation = $user->delegationUser()->pluck('id')->toArray();
if ($delegation) {
$users[] = $delegation[0];
$delegationId = $user->activeDelegationUserId();
if ($delegationId) {
$users[] = $delegationId;
}
}

Expand Down
41 changes: 38 additions & 3 deletions ProcessMaker/Models/ProcessRequestToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Illuminate\Support\Facades\Notification;
use Laravel\Scout\Searchable;
use Log;
use ProcessMaker\AssignmentRules\ProcessManagerAssigned;
use ProcessMaker\Casts\MillisecondsToDateCast;
use ProcessMaker\Contracts\ConditionalRedirectServiceInterface;
use ProcessMaker\Events\ActivityAssigned;
Expand Down Expand Up @@ -1323,12 +1324,43 @@ public function reassignTo($userId)
'process_id' => $this->process_id,
'request_id' => $this->process_request_id,
]);
$this->user_id = $res['assign_to'];
$assignedUserId = $res['assign_to'] ?? null;
$this->user_id = $this->resolveAssignedUserId($userId, $assignedUserId);
$this->unsetRelation('user');
}

return $this;
}

/**
* Assignee to store after the assignment process runs. When nobody can
* take the task, it falls back to the Process Manager instead of being
* left unassigned.
*
* @param mixed $requestedUserId
* @param mixed $assignedUserId
* @return mixed
*/
public function resolveAssignedUserId($requestedUserId, $assignedUserId)
{
$assignee = User::find($requestedUserId);
$resolvedUserId = $assignee
? $assignee->acceptResolvedAssignee($assignedUserId)
: $assignedUserId;

return $resolvedUserId ?: $this->processManagerId();
}

/**
* @return int
*/
private function processManagerId(): int
{
$request = $this->processRequest;

return (new ProcessManagerAssigned())->getNextUser($this->getDefinition(true), $this, $request->process, $request);
}

/**
* Returns True is the tokens belongs to a MultiInstance Task
*
Expand Down Expand Up @@ -1449,8 +1481,11 @@ public function reassign($toUserId, User $requestingUser, $comments = '')
}

// Send a notification to the user
$notification = new TaskReassignmentNotification($this);
$this->user->notify($notification);
$this->unsetRelation('user');
if ($this->user) {
$notification = new TaskReassignmentNotification($this);
$this->user->notify($notification);
}
if ($this->element_type === 'task') {
event(new ActivityAssigned($this));
}
Expand Down
31 changes: 31 additions & 0 deletions ProcessMaker/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,37 @@ public function delegationUser()
return $this->belongsTo(self::class);
}

/**
* Replacement who can take a task right now. A stored replacement can
* become inactive or out of office after it was selected.
*/
public function activeDelegationUserId(): ?int
{
$delegateId = $this->delegationUser()->where('status', 'ACTIVE')->value('id');

return $delegateId ? (int) $delegateId : null;
}

/**
* Keep a resolved assignee unless it is this user's replacement and that
* replacement cannot take a task.
*
* @param mixed $resolvedUserId
* @return mixed
*/
public function acceptResolvedAssignee($resolvedUserId)
{
if (!$resolvedUserId || (int) $resolvedUserId === (int) $this->id) {
return $resolvedUserId;
}

if ((int) $this->delegation_user_id !== (int) $resolvedUserId) {
return $resolvedUserId;
}

return $this->activeDelegationUserId() ? $resolvedUserId : null;
}

/**
* User's Manager are user associations that allow for automatic reassignment based on specific rules in the task assignment.
*
Expand Down
16 changes: 13 additions & 3 deletions ProcessMaker/Repositories/ProcessExecutionRawRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,10 @@ public function getUserByIdRaw(?int $userId): ?User
return null;
}

$row = DB::selectOne('SELECT * FROM users WHERE id = ? LIMIT 1', [$userId]);
$row = DB::selectOne(
'SELECT * FROM users WHERE id = ? AND deleted_at IS NULL LIMIT 1',
[$userId]
);
if (!$row) {
return null;
}
Expand Down Expand Up @@ -566,8 +569,15 @@ private function getNextUserFromProcessVariableRaw(

$oooPlaceholders = implode(',', array_fill(0, count($uniqueUsers), '?'));
$oooRows = DB::select(
"SELECT delegation_user_id FROM users WHERE id IN ($oooPlaceholders) AND status = ? AND delegation_user_id IS NOT NULL",
array_merge($uniqueUsers, ['OUT_OF_OFFICE'])
"SELECT delegate.id AS delegation_user_id
FROM users
INNER JOIN users AS delegate ON delegate.id = users.delegation_user_id
WHERE users.id IN ($oooPlaceholders)
AND users.status = ?
AND users.delegation_user_id IS NOT NULL
AND delegate.status = ?
AND delegate.deleted_at IS NULL",
array_merge($uniqueUsers, ['OUT_OF_OFFICE', 'ACTIVE'])
);
foreach ($oooRows as $row) {
$users[(int) $row->delegation_user_id] = (int) $row->delegation_user_id;
Expand Down
5 changes: 4 additions & 1 deletion resources/views/profile/edit.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@
'timezone',
'datetime_format',
'status',
'delegation_user_id',
'avatar',
'preferences_2fa',
'connected_accounts',
'valpassword',
];
const isSelfServiceProfile = @json(!(Auth::user()->is_administrator || Auth::user()->hasPermission('edit-users')));
let formVueInstance = new Vue({
el: '#editProfile',
mixins:addons,
Expand Down Expand Up @@ -312,7 +314,8 @@
if (this.image === false) {
this.formData.avatar = false;
}
ProcessMaker.apiClient.put('users/' + this.formData.id, this.formData)
const payload = isSelfServiceProfile ? this.profilePayload() : this.formData;
ProcessMaker.apiClient.put('users/' + this.formData.id, payload)
.then((response) => {
// reset the slack configuration error
this.slackConfigurationError = false;
Expand Down
33 changes: 33 additions & 0 deletions tests/Feature/Api/TaskAssignmentByVariableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,39 @@ public function testProcessVariableAssignmentWithObjectVariable()
$this->assertEquals($users->first()->id, $task->user_id);
}

public function testOutOfOfficeAssignmentUsesOnlyAnActiveReplacement()
{
$activeReplacement = User::factory()->create(['status' => 'ACTIVE']);
$outOfOffice = User::factory()->create([
'status' => 'OUT_OF_OFFICE',
'delegation_user_id' => $activeReplacement->id,
]);
$process = $this->createProcess('process_variable', 'usersVariable', '', '', false);

$response = $this->startTestProcess($process, ['usersVariable' => $outOfOffice->id]);
$task = ProcessRequestToken::where([
'process_request_id' => $response['id'],
'status' => 'ACTIVE',
])->firstOrFail();
$this->assertEquals($activeReplacement->id, $task->user_id);

$manager = User::factory()->create(['status' => 'ACTIVE']);
foreach (['INACTIVE', 'OUT_OF_OFFICE'] as $status) {
$unavailableReplacement = User::factory()->create(['status' => $status]);
$outOfOffice->delegation_user_id = $unavailableReplacement->id;
$outOfOffice->save();
$process = $this->createProcess('process_variable', 'usersVariable', '', '', false);
$process->manager_id = [$manager->id];
$process->save();

$response = $this->startTestProcess($process, ['usersVariable' => $outOfOffice->id]);
$assignedToReplacement = ProcessRequestToken::where('process_request_id', $response['id'])
->where('user_id', $unavailableReplacement->id)
->exists();
$this->assertFalse($assignedToReplacement);
}
}

public function testProcessVariableAssignmentWithLiteralId()
{
// Create users of a group and a user without group
Expand Down
36 changes: 36 additions & 0 deletions tests/Feature/Api/TaskAssignmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,42 @@ public function testUpdateTaskAssignment()
$this->assertEquals('ProcessMaker\Models\Group', $assignment->assignment_type);
}

/**
* An unavailable replacement is resolved to the process manager
*/
public function testUnavailableReplacementResolvesToProcessManager()
{
$manager = User::factory()->create(['status' => 'ACTIVE']);
$process = Process::factory()->create([
'status' => 'ACTIVE',
'bpmn' => file_get_contents(__DIR__ . '/processes/InvalidUserAssignment.bpmn'),
]);
$process->manager_id = $manager->id;
$process->save();
$instance = $this->startProcess($process, 'node_1');
$token = $instance->tokens()->where('status', 'ACTIVE')->firstOrFail();

$replacement = User::factory()->create(['status' => 'INACTIVE']);
$outOfOffice = User::factory()->create([
'status' => 'OUT_OF_OFFICE',
'delegation_user_id' => $replacement->id,
]);

$this->assertSame(
$manager->id,
$token->resolveAssignedUserId($outOfOffice->id, $replacement->id)
);

$activeReplacement = User::factory()->create(['status' => 'ACTIVE']);
$outOfOffice->delegation_user_id = $activeReplacement->id;
$outOfOffice->save();
$this->assertSame(
$activeReplacement->id,
$token->resolveAssignedUserId($outOfOffice->id, $activeReplacement->id)
);
$this->assertSame($manager->id, $token->resolveAssignedUserId($outOfOffice->id, null));
}

/**
* Invalid user assignment is reassigned to the process manager
*/
Expand Down
Loading
Loading