3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 11:15:42 +00:00
Files
snipe-it/app/Http/Requests/SaveUserRequest.php
2026-06-26 15:16:23 +01:00

115 lines
4.5 KiB
PHP

<?php
namespace App\Http\Requests;
use App\Models\Company;
use App\Models\Setting;
use App\Rules\UserCannotSwitchCompaniesIfItemsAssigned;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
class SaveUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
public function response(array $errors)
{
return $this->redirector->back()->withInput()->withErrors($errors, $this->errorBag);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
$rules = [
'department_id' => 'nullable|integer|exists:departments,id',
'manager_id' => 'nullable|integer|exists:users,id',
'company_id' => ['nullable', 'integer', 'exists:companies,id'],
'company_ids' => 'nullable|array',
'company_ids.*' => 'integer|exists:companies,id',
'location_id' => 'nullable|integer|exists:locations,id|fmcs_location',
];
switch ($this->method()) {
// Brand new user
case 'POST':
$rules['first_name'] = 'required|string|min:1';
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
if ($this->input('ldap_import') == false) {
$rules['password'] = Setting::passwordComplexityRulesSaving('store').'|confirmed';
}
break;
// Save all fields
case 'PUT':
$rules['first_name'] = 'required|string|min:1';
$rules['username'] = 'required_unless:ldap_import,1|string|min:1';
$rules['password'] = Setting::passwordComplexityRulesSaving('update').'|confirmed';
$rules['company_id'] = ['nullable', 'integer', 'exists:companies,id', new UserCannotSwitchCompaniesIfItemsAssigned];
break;
// Save only what's passed
case 'PATCH':
$rules['password'] = Setting::passwordComplexityRulesSaving('update');
$rules['company_id'] = ['nullable', 'integer', 'exists:companies,id', new UserCannotSwitchCompaniesIfItemsAssigned];
break;
default:
break;
}
return $rules;
}
/**
* Block non-superusers from saving a user whose resulting company set would
* be empty *while floater mode is enabled* — that combination promotes the
* target user to a system-wide floater (sees everything), and is the
* privilege-escalation vector flagged in #19200. Superusers can still make
* floaters intentionally. Applies to web and API store/update.
*/
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator) {
// Console commands (artisan/seeders/queue workers) need to be able
// to manage users without going through the HTTP floater gate.
// We deliberately check runningInConsole() (rather than "no auth
// user") so a future unauthenticated HTTP endpoint can never
// sneak past the gate — flagged in PR review for #19200. Exclude
// the PHPUnit/Pest runner, which also reports as "in console" but
// is using the HTTP stack and must see the gate fire.
$inActualConsole = app()->runningInConsole() && ! app()->runningUnitTests();
if ($inActualConsole || auth()->user()?->canGrantFloaterStatus()) {
return;
}
// Mirror the controller's resolution: prefer company_ids[], fall
// back to legacy company_id, intval, drop empties.
$submitted = (array) ($this->input('company_ids') ?? ($this->filled('company_id') ? [$this->input('company_id')] : []));
$submitted = array_filter(array_map('intval', $submitted));
// Filter to companies the actor can actually assign (matches the
// controller's syncCompaniesWithLogging(Company::getIdsForCurrentUser(...))
// step). If nothing survives the filter, the save would clear the
// user's pivot.
$effective = Company::getIdsForCurrentUser($submitted);
if (empty($effective)) {
$validator->errors()->add('company_ids', trans('admin/users/general.cannot_make_floater'));
}
});
}
}