mirror of
https://github.com/snipe/snipe-it.git
synced 2026-08-18 11:15:42 +00:00
144 lines
6.0 KiB
PHP
144 lines
6.0 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:
|
|
*
|
|
* - Floater mode ON: the resulting user would become a system-wide
|
|
* floater (sees everything). That's the privilege-escalation vector
|
|
* flagged in #19200 — only superusers (and users already able to grant
|
|
* floater status) may do this deliberately.
|
|
* - Floater mode OFF (strict FMCS): the resulting user would land with
|
|
* an empty pivot and be immediately invisible to the creator's own
|
|
* scope. That's the visibility bug flagged in #19192 — only superusers
|
|
* (whose scope reaches nulls) may do this.
|
|
*
|
|
* Applies to both 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) {
|
|
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)) {
|
|
$settings = Setting::getSettings();
|
|
$actor = auth()->user();
|
|
$creatorIsSuper = (bool) $actor?->isSuperUser();
|
|
$creatorHasCompanies = (bool) $actor?->companies()->exists();
|
|
$strictFmcs = $settings->full_multiple_companies_support && ! $settings->null_company_is_floater;
|
|
|
|
// Strict-FMCS #19192 gate — hits before the older floater
|
|
// gate so its more specific error message wins when both
|
|
// apply. Skips uncompanied actors because they legitimately
|
|
// work in the null pseudo-company namespace under strict
|
|
// mode; forcing them to add memberships they don't have
|
|
// would lock them out of their normal workflow.
|
|
if ($strictFmcs && ! $creatorIsSuper && $creatorHasCompanies) {
|
|
$validator->errors()->add('company_ids', trans('validation.fmcs_company', ['attribute' => trans('general.company')]));
|
|
|
|
return;
|
|
}
|
|
|
|
// Original #19200 floater-grant gate.
|
|
if (! $actor?->canGrantFloaterStatus()) {
|
|
$validator->errors()->add('company_ids', trans('admin/users/general.cannot_make_floater'));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|