Merge remote-tracking branch 'origin/develop'

Signed-off-by: snipe <snipe@snipe.net>

# Conflicts:
#	public/css/build/app.css
#	public/css/build/overrides.css
#	public/css/dist/all.css
#	public/mix-manifest.json
This commit is contained in:
snipe
2025-07-22 14:34:10 +01:00
12 changed files with 491 additions and 223 deletions

View File

@@ -23,6 +23,7 @@ use App\Notifications\CurrentInventory;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
@@ -480,8 +481,25 @@ class UsersController extends Controller
return response()->json(Helper::formatStandardApiResponse('error', null, 'You cannot be your own manager'));
}
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
// check for permissions related fields and pull them out if the current user cannot edit them
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
if ($request->filled('username')) {
$user->username = $request->input('username');
}
if ($request->filled('email')) {
$user->email = $request->input('email');
}
if ($request->filled('activated')) {
$user->activated = $request->input('activated');
}
}
// We need to use has() instead of filled()

View File

@@ -13,14 +13,8 @@ use App\Models\Company;
use App\Models\Group;
use App\Models\Setting;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Storage;
use Redirect;
use Str;
use Symfony\Component\HttpFoundation\StreamedResponse;
use App\Notifications\CurrentInventory;
@@ -129,7 +123,7 @@ class UsersController extends Controller
}
$user->permissions = json_encode($permissions_array);
// we have to invoke the
// we have to invoke the form request here to handle image uploads
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');
session()->put(['redirect_option' => $request->get('redirect_option')]);
@@ -235,20 +229,14 @@ class UsersController extends Controller
}
}
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
// Update the user fields
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->first_name = $request->input('first_name');
$user->last_name = $request->input('last_name');
$user->two_factor_optin = $request->input('two_factor_optin') ?: 0;
$user->locale = $request->input('locale');
$user->employee_num = $request->input('employee_num');
$user->activated = $request->input('activated', 0);
$user->jobtitle = $request->input('jobtitle', null);
$user->phone = $request->input('phone');
$user->location_id = $request->input('location_id', null);
@@ -260,8 +248,6 @@ class UsersController extends Controller
$user->city = $request->input('city', null);
$user->state = $request->input('state', null);
$user->country = $request->input('country', null);
// if a user is editing themselves we should always keep activated true
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
$user->zip = $request->input('zip', null);
$user->remote = $request->input('remote', 0);
$user->vip = $request->input('vip', 0);
@@ -270,30 +256,49 @@ class UsersController extends Controller
$user->end_date = $request->input('end_date', null);
$user->autoassign_licenses = $request->input('autoassign_licenses', 0);
// Set this here so that we can overwrite it later if the user is an admin or superadmin
$user->activated = $request->input('activated', auth()->user()->is($user) ? 1 : $user->activated);
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $request->input('location_id', null)]);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
// check for permissions related fields and only set them if the user has permission to edit them
if (auth()->user()->can('canEditAuthFields', $user) && auth()->user()->can('editableOnDemo')) {
$user->username = trim($request->input('username'));
$user->email = trim($request->input('email'));
$user->activated = $request->input('activated', $request->user()->is($user) ? 1 : 0);
// Do we want to update the user password?
if ($request->filled('password')) {
$user->password = bcrypt($request->input('password'));
}
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! auth()->user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Only save groups if the user is a superuser
if (auth()->user()->isSuperUser()) {
$user->groups()->sync($request->input('groups'));
}
}
// Update the location of any assets checked out to this user
Asset::where('assigned_type', User::class)
->where('assigned_to', $user->id)
->update(['location_id' => $user->location_id]);
$permissions_array = $request->input('permission');
// Strip out the superuser permission if the user isn't a superadmin
if (! auth()->user()->isSuperUser()) {
unset($permissions_array['superuser']);
$permissions_array['superuser'] = $orig_superuser;
}
$user->permissions = json_encode($permissions_array);
// Handle uploaded avatar
app(ImageUploadRequest::class)->handleImages($user, 600, 'avatar', 'avatars', 'avatar');

View File

@@ -7,6 +7,7 @@ use App\Models\Department;
use App\Models\Setting;
use App\Models\User;
use App\Notifications\WelcomeNotification;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Password;
@@ -81,6 +82,7 @@ class UserImporter extends ItemImporter
$this->item['username'] = $user_formatted_array['username'];
}
// Check if a numeric ID was passed. If it does, use that above all else.
if ((array_key_exists('id', $this->item) && ($this->item['id'] != "") && (is_numeric($this->item['id'])))) {
$user = User::find($this->item['id']);
@@ -90,12 +92,25 @@ class UserImporter extends ItemImporter
if ($user) {
// If the user does not want to update existing values, only add new ones, bail out
if (! $this->updating) {
Log::debug('A matching User '.$this->item['name'].' already exists. ');
return;
}
$this->log('Updating User');
// Todo - check that this works
if (!Gate::allows('canEditAuthFields', $user)) {
unset($user->username);
unset($user->email);
unset($user->password);
unset($user->activated);
}
$user->update($this->sanitizeItemForUpdating($user));
// Why do we have to do this twice? Update should
$user->save();
// Update the location of any assets checked out to this user
@@ -116,8 +131,12 @@ class UserImporter extends ItemImporter
$this->log('No matching user, creating one');
$user = new User();
$user->created_by = auth()->id();
$user->fill($this->sanitizeItemForStoring($user));
// TODO - check for gate here I guess
if ($user->save()) {
$this->log('User '.$this->item['name'].' was created');
@@ -143,6 +162,7 @@ class UserImporter extends ItemImporter
$this->logError($user, 'User');
}
/**
* Fetch an existing department, or create new if it doesn't exist
*

View File

@@ -203,11 +203,19 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
{
$user_groups = $this->groups;
if (($this->permissions == '') && (count($user_groups) == 0)) {
return false;
}
$user_permissions = json_decode($this->permissions, true);
$user_permissions = $this->permissions;
if (is_object($this->permissions)) {
$user_permissions = json_decode(json_encode($this->permissions), true);
}
if (is_string($this->permissions)) {
$user_permissions = json_decode($this->permissions, true);
}
$is_user_section_permissions_set = ($user_permissions != '') && array_key_exists($section, $user_permissions);
//If the user is explicitly granted, return true
@@ -261,6 +269,18 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo
return $this->checkPermissionSection('superuser');
}
/**
* Checks if the user is an admin
*
* @author A. Gianotto <snipe@snipe.net>
* @since [v8.1.18]
* @return bool
*/
public function isAdmin()
{
return $this->checkPermissionSection('admin');
}
/**
* Checks if the user can edit their own profile

View File

@@ -101,13 +101,21 @@ class AuthServiceProvider extends ServiceProvider
* This is where we set the superadmin permission to allow superadmins to be able to do everything within the system.
*
*/
Gate::before(function ($user) {
Gate::before(function ($user, $ability) {
// Disallow even superadmins to edit non-editable things when in demo mode.
// (We have to do this to prevent jerks from trying to break the demo by editing things they shouldn't.)
if (($ability == 'editableOnDemo') && (config('app.lock_passwords'))) {
return false;
}
if ($user->isSuperUser()) {
return true;
}
});
/**
* GENERAL GATES
*
@@ -115,6 +123,45 @@ class AuthServiceProvider extends ServiceProvider
* use in our controllers to determine if a user has access to a certain area.
*/
Gate::define('canEditAuthFields', function ($user, $item) {
if ($item instanceof User) {
// if they can only edit users, deny them if the user is admin or superadmin
if (($user->hasAccess('users.edit')) && (!$user->isAdmin()) && (!$user->isAdmin())) {
if ($item->isAdmin() || $item->isSuperUser()) {
return false;
}
return true;
}
// if they are an admin, deny them only if the user is a superadmin
if ($user->hasAccess('admin')) {
if ($item->isSuperUser()) {
return false;
}
return true;
}
return false;
}
return false;
});
/**
* Define the demo mode gate so we have an easy way to use @can and Gate::allows()
*/
Gate::define('editableOnDemo', function () {
if (config('app.lock_passwords')) {
return false;
}
return true;
});
Gate::define('admin', function ($user) {
if ($user->hasAccess('admin')) {
return true;
@@ -249,5 +296,6 @@ class AuthServiceProvider extends ServiceProvider
return $user->canEditProfile();
});
}
}

View File

@@ -1165,6 +1165,11 @@ input[type="radio"]:checked::before {
display: table-row !important;
}
.form-control-static {
padding-top: 0px;
}
td.text-right.text-padding-number-cell {
padding-right: 30px !important;
white-space: nowrap;

View File

@@ -17,7 +17,7 @@ return array(
'last_login' => 'Last Login',
'last_name' => 'Last Name',
'location' => 'Location',
'lock_passwords' => 'Login details cannot be changed on this installation.',
'lock_passwords' => 'Login details cannot be changed on the demo.',
'manager' => 'Manager',
'managed_locations' => 'Managed Locations',
'managed_users' => 'Managed Users',

View File

@@ -4,6 +4,7 @@ return [
'2FA_reset' => '2FA reset',
'accessories' => 'Accessories',
'activated' => 'Activated',
'login_status' => 'Login Status',
'accepted_date' => 'Date Accepted',
'accessory' => 'Accessory',
'accessory_report' => 'Accessory Report',
@@ -316,6 +317,7 @@ return [
'upload_filetypes_help' => 'Allowed filetypes are: :allowed_filetypes. Max upload size allowed is :size.',
'uploaded' => 'Uploaded',
'user' => 'User',
'password' => 'Password',
'accepted' => 'accepted',
'declined' => 'declined',
'declined_note' => 'Declined Notes',
@@ -476,7 +478,7 @@ return [
'update_existing_values' => 'Update Existing Values?',
'auto_incrementing_asset_tags_disabled_so_tags_required' => 'Generating auto-incrementing asset tags is disabled so all rows need to have the "Asset Tag" column populated.',
'auto_incrementing_asset_tags_enabled_so_now_assets_will_be_created' => 'Note: Generating auto-incrementing asset tags is enabled so assets will be created for rows that do not have "Asset Tag" populated. Rows that do have "Asset Tag" populated will be updated with the provided information.',
'send_welcome_email_to_users' => ' Send Welcome Email for new Users?',
'send_welcome_email_to_users' => ' Send Welcome Email for new Users? Note that only users with a valid email address and who are marked as activated in your import file will received a welcome.',
'send_email' => 'Send Email',
'call' => 'Call number',
'back_before_importing' => 'Backup before importing?',

View File

@@ -78,7 +78,9 @@
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<li class="active"><a href="#info" data-toggle="tab">{{ trans('general.information') }} </a></li>
<li><a href="#permissions" data-toggle="tab">{{ trans('general.permissions') }} </a></li>
@can('admin')
<li><a href="#permissions" data-toggle="tab">{{ trans('general.permissions') }} </a></li>
@endcan
</ul>
<div class="tab-content">
@@ -93,138 +95,168 @@
<!-- Username -->
<div class="form-group {{ $errors->has('username') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="username">{{ trans('admin/users/table.username') }}</label>
<label class="col-md-3 control-label" for="username">
{{ trans('admin/users/table.username') }}
</label>
<div class="col-md-6">
@if ($user->ldap_import!='1' || str_contains(Route::currentRouteName(), 'clone'))
<input
class="form-control"
type="text"
name="username"
id="username"
value="{{ old('username', $user->username) }}"
autocomplete="off"
maxlength="191"
readonly
{{ (Helper::checkIfRequired($user, 'username')) ? ' required' : '' }}
onfocus="this.removeAttribute('readonly');"
{{ ((config('app.lock_passwords') && ($user->id)) ? ' disabled' : '') }}
>
<input type="hidden" name="username" value="{{ old('username', $user->username) }}">
<!-- if the user is not managed by LDAP, or this is a clone operation, allow editing of the username -->
@if ($user->ldap_import!='1' || str_contains(Route::currentRouteName(), 'clone'))
<input class="form-control" type="text" name="username" id="username" value="{{ old('username', $user->username) }}" autocomplete="off" maxlength="191" {{ (Helper::checkIfRequired($user, 'username')) ? ' required' : '' }} onfocus="this.removeAttribute('readonly');" readonly {{ (!Gate::allows('canEditAuthFields', $user)) || ((!Gate::allows('editableOnDemo')) && ($user->id)) ? ' disabled' : '' }}>
@else
@else
<!-- insert the old username so we don't break validation -->
{{ trans('general.managed_ldap') }}
<input type="hidden" name="username" value="{{ old('username', $user->username) }}">
@endif
</div>
<!-- insert the old username so we don't break validation -->
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.managed_ldap') }}
</p>
<input type="hidden" name="username" value="{{ old('username', $user->username) }}">
@endif
@cannot('canEditAuthFields', $user)
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.action_permission_generic', ['action' => trans('general.edit'), 'item_type' => trans('general.username')]) }}
</p>
@endcannot
</div> <!--/col-md-6-->
@if (config('app.lock_passwords') && ($user->id))
<!-- disallow changing existing usernames on the demo -->
<div class="col-md-8 col-md-offset-3">
<p class="text-warning"><x-icon type="lock" /> {{ trans('general.feature_disabled') }}</p>
</div>
@endif
@if (!Gate::allows('editableOnDemo') && ($user->id))
<!-- disallow changing existing usernames on the demo -->
<div class="col-md-8 col-md-offset-3">
<p class="text-warning">
<x-icon type="locked" />
{{ trans('admin/users/table.lock_passwords') }}
</p>
</div>
@endif
@if ($errors->first('username'))
<div class="col-md-8 col-md-offset-3">
{!! $errors->first('username', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
@endif
@if ($errors->first('username'))
<div class="col-md-8 col-md-offset-3">
{!! $errors->first('username', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
@endif
</div>
<!-- Password -->
<div class="form-group {{ $errors->has('password') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="password">
{{ trans('admin/users/table.password') }}
</label>
<div class="col-md-6">
@if ($user->ldap_import!='1' || str_contains(Route::currentRouteName(), 'clone') )
<input
type="password"
name="password"
class="form-control"
id="password"
value=""
maxlength="500"
autocomplete="off"
readonly
{{ ((Helper::checkIfRequired($user, 'password')) && (!$user->id)) ? ' required' : '' }}
onfocus="this.removeAttribute('readonly');"
{{ ((config('app.lock_passwords') && ($user->id)) ? ' disabled' : '') }}>
@else
{{ trans('general.managed_ldap') }}
@endif
<span id="generated-password"></span>
{!! $errors->first('password', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@if ($user->ldap_import!='1' || str_contains(Route::currentRouteName(), 'clone') )
<input type="password" name="password" class="form-control" id="password" value="" maxlength="500" autocomplete="off" onfocus="this.removeAttribute('readonly');" readonly {{ ((Helper::checkIfRequired($user, 'password')) && (!$user->id)) ? ' required' : '' }}{{ (!Gate::allows('canEditAuthFields', $user)) || ((!Gate::allows('editableOnDemo') && ($user->id))) ? ' disabled' : '' }}>
<span id="generated-password"></span>
{!! $errors->first('password', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
@else
<p class="form-control-static">
{{ trans('general.managed_ldap') }}
</p>
@endif
@cannot('canEditAuthFields', $user)
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.action_permission_generic', ['action' => trans('general.edit'), 'item_type' => trans('general.password')]) }}
</p>
@endcan
@if (!Gate::allows('editableOnDemo') && ($user->id))
<p class="text-warning">
<x-icon type="locked" />
{{ trans('admin/users/table.lock_passwords') }}
</p>
@endif
</div>
<div class="col-md-2">
@if ($user->ldap_import!='1')
@if (Gate::allows('editableOnDemo') && (Gate::allows('canEditAuthFields', $user)) && ($user->ldap_import!='1'))
<a href="#" class="left" id="genPassword">{{ trans('general.generate') }}</a>
@endif
</div>
</div>
@if (($user->ldap_import!='1') || str_contains(Route::currentRouteName(), 'clone'))
<!-- Password Confirm -->
<div class="form-group {{ $errors->has('password_confirmation') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="password_confirmation">
{{ trans('admin/users/table.password_confirm') }}
</label>
<div class="col-md-6">
<input type="password" name="password_confirmation" id="password_confirm" class="form-control" value="" maxlength="500" autocomplete="off" aria-label="password_confirmation" {{ (!$user->id) ? ' required' : '' }} onfocus="this.removeAttribute('readonly');" readonly {{ (!Gate::allows('canEditAuthFields', $user)) || ((!Gate::allows('editableOnDemo')) && ($user->id)) ? ' disabled' : '' }}>
@if ($user->ldap_import!='1' || str_contains(Route::currentRouteName(), 'clone'))
<!-- Password Confirm -->
<div class="form-group {{ $errors->has('password_confirmation') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="password_confirmation">
{{ trans('admin/users/table.password_confirm') }}
</label>
<div class="col-md-6">
<input
type="password"
name="password_confirmation"
id="password_confirm"
class="form-control"
value=""
maxlength="500"
autocomplete="off"
aria-label="password_confirmation"
readonly
{{ (!$user->id) ? ' required' : '' }}
onfocus="this.removeAttribute('readonly');"
{{ ((config('app.lock_passwords') && ($user->id)) ? ' disabled' : '') }}
>
@if (config('app.lock_passwords') && ($user->id))
<p class="help-block">{{ trans('admin/users/table.lock_passwords') }}</p>
@endif
{!! $errors->first('password_confirmation', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
@cannot('canEditAuthFields', $user)
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.action_permission_generic', ['action' => trans('general.edit'), 'item_type' => trans('general.password')]) }}
</p>
@endcan
@if (!Gate::allows('editableOnDemo') && ($user->id))
<p class="text-warning">
<x-icon type="locked" />
{{ trans('admin/users/table.lock_passwords') }}
</p>
@endif
{!! $errors->first('password_confirmation', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
@endif
<!-- Activation Status (Can the user login?) -->
<div class="form-group {{ $errors->has('activated') ? 'has-error' : '' }}">
<div class="col-md-9 col-md-offset-3">
<!-- checkbox($name, $value = 1, $checked = null, $options = array() -->
@if (config('app.lock_passwords'))
<!-- disallow changes to the user's login status -->
@if ((!Gate::allows('canEditAuthFields', $user)) || ($user->id == auth()->user()->id) || ($user->id))
<!-- demo mode - disallow changes -->
<label class="form-control form-control--disabled">
<input type="checkbox" value="1" name="activated" class="disabled" {{ (old('activated', $user->activated)) == '1' ? ' checked="checked"' : '' }} disabled="disabled" aria-label="activated">
{{ trans('admin/users/general.activated_help_text') }}
</label>
<p class="text-warning"><x-icon type="lock" /> {{ trans('general.feature_disabled') }}</p>
@elseif ($user->id === Auth::user()->id)
<!-- disallow the user from editing their own login status -->
<label class="form-control form-control--disabled">
<input type="checkbox" name="activated" value="1" checked disabled aria-label="activated">
<input type="checkbox" value="1" name="activated" class="disabled" {{ (old('activated', $user->activated)) == '1' ? ' checked="checked"' : '' }} disabled aria-label="activated">
{{ trans('admin/users/general.activated_help_text') }}
</label>
<p class="text-warning">{{ trans('admin/users/general.activated_disabled_help_text') }}</p>
@cannot('canEditAuthFields', $user)
<!-- authed user is an admin or regular user and is trying to edit someone higher -->
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.action_permission_generic', ['action' => trans('general.edit'), 'item_type' => trans('general.login_status')]) }}
</p>
@endcannot
@if ((auth()->user()->cannot('editableOnDemo')) && ($user->id))
<!-- app is locked -->
<p class="text-warning">
<x-icon type="locked" />
{{ trans('admin/users/table.lock_passwords') }}
</p>
@endif
@if ($user->id == auth()->user()->id)
<!-- disallow editing activation on your own account -->
<p class="help-block">
<x-icon type="locked" />
{{ trans('admin/users/general.activated_disabled_help_text') }}
</p>
@endcannot
@else
<!-- everything is normal - as you were -->
<label class="form-control">
<input type="checkbox" value="1" name="activated"{{ ((old('activated') == '1') || ($user->activated) == '1') ? ' checked="checked"' : '' }} aria-label="activated" id="activated">
{{ trans('admin/users/general.activated_help_text') }}
</label>
@endif
</div>
</div>
@@ -232,22 +264,28 @@
<div class="form-group {{ $errors->has('email') ? 'has-error' : '' }}">
<label class="col-md-3 control-label" for="email">{{ trans('admin/users/table.email') }} </label>
<div class="col-md-6">
<input
class="form-control"
type="text"
name="email"
id="email"
maxlength="191"
value="{{ old('email', $user->email) }}"
{{ ((config('app.lock_passwords') && ($user->id)) ? ' disabled' : '') }}
autocomplete="off"
readonly
{{ (Helper::checkIfRequired($user, 'email')) ? ' required' : '' }}
onfocus="this.removeAttribute('readonly');">
@if (config('app.lock_passwords') && ($user->id))
<p class="help-block">{{ trans('admin/users/table.lock_passwords') }}</p>
@endif
{!! $errors->first('email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
<input class="form-control" type="email" name="email" id="email" maxlength="191" value="{{ old('email', $user->email) }}" autocomplete="off"
readonly onfocus="this.removeAttribute('readonly');" {{ (Helper::checkIfRequired($user, 'email')) ? ' required' : '' }}{{ (!Gate::allows('canEditAuthFields', $user)) || ((!Gate::allows('editableOnDemo') && ($user->id))) ? ' disabled' : '' }}>
@cannot('canEditAuthFields', $user)
<!-- authed user is an admin or regular user and is trying to edit someone higher -->
<p class="help-block">
<x-icon type="locked" />
{{ trans('general.action_permission_generic', ['action' => trans('general.edit'), 'item_type' => trans('general.email')]) }}
</p>
@endcannot
@if (!Gate::allows('editableOnDemo') && ($user->id))
<p class="text-warning">
<x-icon type="locked" />
{{ trans('admin/users/table.lock_passwords') }}
</p>
@endif
{!! $errors->first('email', '<span class="alert-msg" aria-hidden="true">:message</span>') !!}
</div>
</div>
@@ -274,8 +312,19 @@
<!-- everything here should be what is considered optional -->
<br>
<!-- Company -->
@if (\App\Models\Company::canManageUsersCompanies())
@if ((Gate::allows('canEditAuthFields', $user)) && (\App\Models\Company::canManageUsersCompanies()))
@include ('partials.forms.edit.company-select', ['translated_name' => trans('general.select_company'), 'fieldname' => 'company_id'])
@else
@if ($user->company)
<div class="form-group">
<label class="col-md-3 control-label" for="locale">{{ trans('general.company') }}</label>
<div class="col-md-6">
<p class="form-control-static">
{{ $user->company ? $user->company->name : '' }}
</p>
</div>
</div>
@endif
@endif
@@ -457,7 +506,7 @@
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
@if (config('app.lock_passwords'))
@if (!Gate::allows('editableOnDemo'))
<label class="form-control form-control--disabled" for="two_factor_optin">
<input type="checkbox" value="1" name="two_factor_optin" {{ (old('two_factor_optin', $user->two_factor_optin)) == '1' ? ' checked="checked"' : '' }} aria-label="two_factor_optin" disabled>
@@ -470,7 +519,9 @@
<input type="checkbox" value="1" name="two_factor_optin" {{ (old('two_factor_optin', $user->two_factor_optin)) == '1' ? ' checked="checked"' : '' }} aria-label="two_factor_optin">
{{ trans('admin/settings/general.two_factor') }}
</label>
<p class="help-block">{{ trans('admin/users/general.two_factor_admin_optin_help') }}</p>
<p class="help-block">
{{ trans('admin/users/general.two_factor_admin_optin_help') }}
</p>
@endif
@@ -488,7 +539,9 @@
<span id="two_factor_resetstatus"></span>
</div>
<div class="col-md-8 col-md-offset-3 two_factor_resetrow">
<p class="help-block">{{ trans('admin/settings/general.two_factor_reset_help') }}</p>
<p class="help-block">
{{ trans('admin/settings/general.two_factor_reset_help') }}
</p>
</div>
</div>
@endif
@@ -497,11 +550,13 @@
<!-- Groups -->
<div class="form-group{{ $errors->has('groups') ? ' has-error' : '' }}">
<label class="col-md-3 control-label" for="groups[]"> {{ trans('general.groups') }}</label>
<label class="col-md-3 control-label" for="groups[]">
{{ trans('general.groups') }}
</label>
<div class="col-md-6">
@if ($groups->count())
@if ((Config::get('app.lock_passwords') || (!Auth::user()->isSuperUser())))
@if ((!Gate::allows('editableOnDemo') || (!Auth::user()->isSuperUser())))
@if (count($userGroups->keys()) > 0)
<ul>
@@ -511,27 +566,30 @@
</ul>
@endif
<span class="help-block">{{ trans('admin/users/general.group_memberships_helpblock') }}</span>
@else
<div class="controls">
<select
name="groups[]"
aria-label="groups[]"
id="groups[]"
multiple="multiple"
class="form-control">
<p class="help-block">
<x-icon type="locked" />
{{ trans('admin/users/general.group_memberships_helpblock') }}
</p>
@else
<div class="controls">
<select
name="groups[]"
aria-label="groups[]"
id="groups[]"
multiple="multiple"
class="form-control">
@foreach ($groups as $id => $group)
<option value="{{ $id }}"
{{ ($userGroups->keys()->contains($id) ? ' selected="selected"' : '') }}>
{{ $group }}
</option>
@endforeach
</select>
@foreach ($groups as $id => $group)
<option value="{{ $id }}"
{{ ($userGroups->keys()->contains($id) ? ' selected="selected"' : '') }}>
{{ $group }}
</option>
@endforeach
</select>
<span class="help-block">
{{ trans('admin/users/table.groupnotes') }}
</span>
<p class="help-block">
{{ trans('admin/users/table.groupnotes') }}
</p>
</div>
@endif
@else
@@ -552,6 +610,7 @@
</div>
</div><!-- /.tab-pane -->
@can('admin')
<div class="tab-pane" id="permissions">
<div class="col-md-12">
@if (!Auth::user()->isSuperUser())
@@ -575,6 +634,7 @@
@include('partials.forms.edit.permissions-base')
</table>
</div><!-- /.tab-pane -->
@endcan
</div><!-- /.tab-content -->
<x-redirect_submit_options
index_route="users.index"

View File

@@ -164,15 +164,6 @@
<div class="tab-pane active" id="details">
<div class="row">
@if ($user->deleted_at!='')
<div class="col-md-12">
<div class="callout callout-warning">
<i class="icon fas fa-exclamation-triangle"></i>
{{ trans('admin/users/message.user_deleted_warning') }}
</div>
</div>
@endif
<div class="info-stack-container">
<!-- Start button column -->
<div class="col-md-3 col-xs-12 col-sm-push-9 info-stack">
@@ -241,7 +232,7 @@
@endcan
@can('update', $user)
@if (($user->activated == '1') && ($user->ldap_import == '0'))
@if ((($user->deleted_at=='')) && ($user->activated == '1') && ($user->ldap_import == '0'))
<div class="col-md-12" style="padding-top: 5px;">
@if (($user->email != '') && ($user->activated=='1'))
<form action="{{ route('users.password',['userId'=> $user->id]) }}" method="POST">
@@ -271,7 +262,7 @@
@endcan
@can('delete', $user)
@can('update', $user)
@if ($user->deleted_at=='')
<div class="col-md-12" style="padding-top: 30px;">
@if ($user->isDeletable())
@@ -987,38 +978,20 @@
<div class="table-responsive">
<table
data-cookie-id-table="usersHistoryTable"
data-id-table="usersHistoryTable"
data-side-pagination="server"
data-sort-order="desc"
id="usersHistoryTable"
class="table table-striped snipe-table"
data-url="{{ route('api.activity.index', ['target_id' => $user->id, 'target_type' => 'user']) }}"
data-export-options='{
"fileName": "export-{{ str_slug($user->present()->fullName ) }}-history-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'>
<thead>
<tr>
<th data-field="icon" style="width: 40px;" class="hidden-xs" data-formatter="iconFormatter">Icon</th>
<th data-field="created_at" data-formatter="dateDisplayFormatter" data-sortable="true">{{ trans('general.date') }}</th>
<th data-field="item" data-formatter="polymorphicItemFormatter">{{ trans('general.item') }}</th>
<th data-field="action_type">{{ trans('general.action') }}</th>
<th data-field="target" data-formatter="polymorphicItemFormatter">{{ trans('general.target') }}</th>
<th data-field="note">{{ trans('general.notes') }}</th>
@if ($snipeSettings->require_accept_signature=='1')
<th data-field="signature_file" data-visible="false" data-formatter="imageFormatter">{{ trans('general.signature') }}</th>
@endif
<th data-field="item.serial" data-visible="false">{{ trans('admin/hardware/table.serial') }}</th>
<th data-field="admin" data-formatter="usersLinkObjFormatter">{{ trans('general.created_by') }}</th>
<th data-field="remote_ip" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_ip') }}</th>
<th data-field="user_agent" data-visible="false" data-sortable="true">{{ trans('admin/settings/general.login_user_agent') }}</th>
<th data-field="action_source" data-visible="false" data-sortable="true">{{ trans('general.action_source') }}</th>
</tr>
</thead>
</table>
<table
data-columns="{{ \App\Presenters\HistoryPresenter::dataTableLayout() }}"
class="table table-striped snipe-table"
data-cookie-id-table="UserHistoryTable"
data-id-table="UserHistoryTable"
id="UserHistoryTable"
data-side-pagination="server"
data-sort-order="desc"
data-export-options='{
"fileName": "export-{{ str_slug($user->name) }}-history-{{ date('Y-m-d') }}",
"ignoreColumn": ["actions","image","change","checkbox","checkincheckout","icon"]
}'
data-url="{{ route('api.activity.index', ['target_id' => $user->id, 'item_type' => User::class]) }}">
</table>
</div>
</div><!-- /.tab-pane -->

View File

@@ -162,7 +162,7 @@ class UpdateUserTest extends TestCase
public function testApiUsersCanBeActivatedWithNumber()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => 0]);
$this->actingAsForApi($admin)
@@ -175,7 +175,7 @@ class UpdateUserTest extends TestCase
public function testApiUsersCanBeActivatedWithBooleanTrue()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => false]);
$this->actingAsForApi($admin)
@@ -188,7 +188,7 @@ class UpdateUserTest extends TestCase
public function testApiUsersCanBeDeactivatedWithNumber()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => true]);
$this->actingAsForApi($admin)
@@ -201,7 +201,7 @@ class UpdateUserTest extends TestCase
public function testApiUsersCanBeDeactivatedWithBooleanFalse()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => true]);
$this->actingAsForApi($admin)
@@ -212,6 +212,33 @@ class UpdateUserTest extends TestCase
$this->assertEquals(0, $user->refresh()->activated);
}
public function testEditingUsersCannotEditEscalationFieldsForAdmins()
{
$hashed_original = Hash::make('!!094850394680980380kfejlskjfl');
$hashed_new = Hash::make('!ABCDEFGIJKL123!!!');
$admin = User::factory()->editUsers()->create();
$user = User::factory()->admin()->create(['username' => 'brandnewuser', 'email'=> 'brandnewemail@example.org', 'password' => $hashed_original, 'activated' => 1]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'username' => 'brandnewuser',
'email' => 'brandnewemail@example.org',
'activated' => 1,
'password' => $hashed_original,
]);
$this->actingAsForApi($admin)
->patch(route('api.users.update', $user), [
'username' => 'testnewusername',
'email' => 'testnewemail@example.org',
'activated' => 0,
'password' => $hashed_new,
]);
$this->assertEquals(0, $user->refresh()->activated);
}
public function testUsersScopedToCompanyDuringUpdateWhenMultipleFullCompanySupportEnabled()
{
$this->settings->enableMultipleFullCompanySupport();

View File

@@ -7,17 +7,26 @@ use App\Models\Company;
use App\Models\User;
use Error;
use Tests\TestCase;
use Illuminate\Support\Facades\Hash;
class UpdateUserTest extends TestCase
{
public function testRequiresPermission()
{
$this->actingAs(User::factory()->create())
->get(route('users.edit', User::factory()->create()->id))
->assertForbidden();
}
public function testPageRenders()
{
$this->actingAs(User::factory()->superuser()->create())
$this->actingAs(User::factory()->editUsers()->create())
->get(route('users.edit', User::factory()->create()->id))
->assertOk();
}
public function testCannotViewEditPageForSoftDeletedUser()
public function testCanViewEditPageForSoftDeletedUser()
{
$user = User::factory()->trashed()->create();
@@ -28,7 +37,7 @@ class UpdateUserTest extends TestCase
public function testUsersCanBeActivatedWithNumber()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => 0]);
$this->actingAs($admin)
@@ -43,7 +52,7 @@ class UpdateUserTest extends TestCase
public function testUsersCanBeActivatedWithBooleanTrue()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => false]);
$this->actingAs($admin)
@@ -58,7 +67,7 @@ class UpdateUserTest extends TestCase
public function testUsersCanBeDeactivatedWithNumber()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => true]);
$this->actingAs($admin)
@@ -73,7 +82,7 @@ class UpdateUserTest extends TestCase
public function testUsersCanBeDeactivatedWithBooleanFalse()
{
$admin = User::factory()->superuser()->create();
$admin = User::factory()->editUsers()->create();
$user = User::factory()->create(['activated' => true]);
$this->actingAs($admin)
@@ -88,7 +97,7 @@ class UpdateUserTest extends TestCase
public function testUsersUpdatingThemselvesDoNotDeactivateTheirAccount()
{
$admin = User::factory()->superuser()->create(['activated' => true]);
$admin = User::factory()->editUsers()->create(['activated' => true]);
$this->actingAs($admin)
->put(route('users.update', $admin), [
@@ -99,6 +108,87 @@ class UpdateUserTest extends TestCase
$this->assertEquals(1, $admin->refresh()->activated);
}
public function testEditingUsersCannotEditEscalationFieldsForAdmins()
{
$admin = User::factory()->editUsers()->create(['activated' => true]);
$hashed_original = Hash::make('!!094850394680980380kfejlskjfl');
$hashed_new = Hash::make('!ABCDEFGIJKL123!!!');
$user = User::factory()->admin()->create(['username' => 'brandnewuser', 'email'=> 'brandnewemail@example.org', 'password' => $hashed_original, 'activated' => true]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'username' => 'brandnewuser',
'email' => 'brandnewemail@example.org',
'activated' => 1,
'password' => $hashed_original,
]);
$this->actingAs($admin)
->put(route('users.update', $user), [
'username' => 'testnewusername',
'email' => 'testnewemail@example.org',
'activated' => 0,
'password' => 'super-secret',
]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'activated' => $user->activated,
'password' => $hashed_original,
]);
$this->assertEquals('brandnewuser', $user->refresh()->username);
$this->assertEquals('brandnewemail@example.org', $user->refresh()->email);
$this->assertEquals(1, $user->refresh()->activated);
$this->assertNotEquals(Hash::check('super-secret', $user->password), $user->refresh()->password);
$this->assertNotEquals('testnewusername', $user->refresh()->username);
$this->assertNotEquals('testnewemail@example.org', $user->refresh()->email);
$this->assertNotEquals(0, $user->refresh()->activated);
$this->assertNotEquals(Hash::check('super-secret', $user->password), $user->refresh()->password);
}
public function testAdminUsersCannotEditFieldsForSuperAdmins()
{
$admin = User::factory()->admin()->create(['activated' => true]);
$hashed_original = Hash::make('my-awesome-password');
$user = User::factory()->superuser()->create(['username' => 'brandnewuser', 'email'=> 'brandnewemail@example.org', 'password' => $hashed_original, 'activated' => true]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'username' => 'brandnewuser',
'email' => 'brandnewemail@example.org',
'activated' => 1,
'password' => $hashed_original,
]);
$this->actingAs($admin)
->put(route('users.update', $user), [
'username' => 'testnewusername',
'email' => 'testnewemail@example.org',
'activated' => 0,
'password' => 'super-secret-new-password',
]);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'username' => $user->username,
'email' => $user->email,
'activated' => $user->activated,
'password' => $hashed_original,
]);
$this->assertEquals('brandnewuser', $user->refresh()->username);
$this->assertEquals('brandnewemail@example.org', $user->refresh()->email);
$this->assertEquals(1, $user->refresh()->activated);
$this->assertTrue(Hash::check('my-awesome-password', $user->password), $user->refresh()->password);
$this->assertNotEquals('testnewusername', $user->refresh()->username);
$this->assertNotEquals('testnewemail@example.org', $user->refresh()->email);
$this->assertNotTrue(Hash::check('super-secret-new-password', $user->password), $user->refresh()->password);
}
public function testMultiCompanyUserCannotBeMovedIfHasAssetInDifferentCompany()
{
$this->settings->enableMultipleFullCompanySupport();
@@ -188,7 +278,7 @@ class UpdateUserTest extends TestCase
$user->delete();
$response = $this->actingAs(User::factory()->editUsers()->create())
->put(route('users.update', $id), [
->put(route('users.update', $user), [
'first_name' => 'test',
'username' => 'test',
'company_id' => $companyB->id,