Merge pull request #1697 from denkfabrik-li/fix/assigned-clients-authority

Nobody hands out reach they do not hold either

Two resolutions against branches that landed first. #1678 and this one
each add a constructor property and an import to StaffAccounts, so both
are kept. And #1702's merge note called this one exactly: its
"converting an account to staff cannot hand out clients either" case
promoted a stranger client, which #1702 now refuses at 404 before
validation runs. Pointed at a client the actor holds, as that note
proposed, so the request reaches the assigned_clients rule the case is
actually about.
This commit is contained in:
ignacionelson
2026-08-26 22:35:35 -03:00
6 changed files with 276 additions and 10 deletions
@@ -118,7 +118,10 @@ class UsersController extends Controller
'role_id' => ['required', 'integer', Rule::in($this->accounts->assignableRoleIds($this->actor()))],
'password' => ['required', 'confirmed', Password::defaults()],
'assigned_clients' => ['array'],
'assigned_clients.*' => ['integer', Rule::exists('users', 'id')->where('type', UserType::Client->value)],
// Reach, not a label: see StaffAccounts::assignableClientIds.
// The list is client-typed already, so this is one rule where
// an exists() plus a type filter used to be two.
'assigned_clients.*' => ['integer', Rule::in($this->accounts->assignableClientIds($this->actor()))],
]);
$user = $this->accounts->create([
@@ -178,7 +181,10 @@ class UsersController extends Controller
'active' => ['required', 'boolean'],
'password' => ['nullable', 'confirmed', Password::defaults()],
'assigned_clients' => ['array'],
'assigned_clients.*' => ['integer', Rule::exists('users', 'id')->where('type', UserType::Client->value)],
// Reach, not a label: see StaffAccounts::assignableClientIds.
// The list is client-typed already, so this is one rule where
// an exists() plus a type filter used to be two.
'assigned_clients.*' => ['integer', Rule::in($this->accounts->assignableClientIds($this->actor()))],
]);
// Deactivating yourself is refused here rather than in StaffAccounts
@@ -272,13 +278,15 @@ class UsersController extends Controller
}
/**
* The client roster, for the assigned-clients picker.
* The client roster, for the assigned-clients picker narrowed to
* what this actor may actually hand out, the same way roleOptions()
* is narrowed to the roles they may grant.
*
* @return array<int, array{id: int, name: string}>
*/
private function clientOptions(): array
{
return User::query()->where('type', UserType::Client)->orderBy('name')->get()
return User::query()->whereIn('id', $this->accounts->assignableClientIds($this->actor()))->orderBy('name')->get()
->map(fn (User $client): array => ['id' => $client->id, 'name' => $client->name])
->values()->all();
}