diff --git a/app/Http/Controllers/Api/UsersController.php b/app/Http/Controllers/Api/UsersController.php index afbe423b53..20d3f8805b 100644 --- a/app/Http/Controllers/Api/UsersController.php +++ b/app/Http/Controllers/Api/UsersController.php @@ -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() diff --git a/app/Http/Controllers/Users/UsersController.php b/app/Http/Controllers/Users/UsersController.php index 943d7e240b..8221fc4bd8 100755 --- a/app/Http/Controllers/Users/UsersController.php +++ b/app/Http/Controllers/Users/UsersController.php @@ -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'); diff --git a/app/Importer/UserImporter.php b/app/Importer/UserImporter.php index 2d5cc3d730..31f56602d3 100644 --- a/app/Importer/UserImporter.php +++ b/app/Importer/UserImporter.php @@ -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 * diff --git a/app/Models/User.php b/app/Models/User.php index 416c97bc57..3d436b07e2 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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 + * @since [v8.1.18] + * @return bool + */ + public function isAdmin() + { + return $this->checkPermissionSection('admin'); + } + /** * Checks if the user can edit their own profile diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 899df2ef14..114100f288 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -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(); }); + } } diff --git a/public/css/build/app.css b/public/css/build/app.css index f139c052cf..b6c2073aeb 100644 --- a/public/css/build/app.css +++ b/public/css/build/app.css @@ -1429,6 +1429,9 @@ input[type="radio"]:checked::before { .bootstrap-table .fixed-table-container .table tbody tr .card-view { display: table-row !important; } +.form-control-static { + padding-top: 0px; +} td.text-right.text-padding-number-cell { padding-right: 30px !important; white-space: nowrap; diff --git a/public/css/build/overrides.css b/public/css/build/overrides.css index aa8e28acbb..34a882ff8b 100644 --- a/public/css/build/overrides.css +++ b/public/css/build/overrides.css @@ -1050,6 +1050,9 @@ input[type="radio"]:checked::before { .bootstrap-table .fixed-table-container .table tbody tr .card-view { display: table-row !important; } +.form-control-static { + padding-top: 0px; +} td.text-right.text-padding-number-cell { padding-right: 30px !important; white-space: nowrap; diff --git a/public/css/dist/all.css b/public/css/dist/all.css index 5d809f514e..0924af427a 100644 --- a/public/css/dist/all.css +++ b/public/css/dist/all.css @@ -22764,6 +22764,9 @@ input[type="radio"]:checked::before { .bootstrap-table .fixed-table-container .table tbody tr .card-view { display: table-row !important; } +.form-control-static { + padding-top: 0px; +} td.text-right.text-padding-number-cell { padding-right: 30px !important; white-space: nowrap; @@ -24337,6 +24340,9 @@ input[type="radio"]:checked::before { .bootstrap-table .fixed-table-container .table tbody tr .card-view { display: table-row !important; } +.form-control-static { + padding-top: 0px; +} td.text-right.text-padding-number-cell { padding-right: 30px !important; white-space: nowrap; diff --git a/public/mix-manifest.json b/public/mix-manifest.json index e4dee94e3d..37f6ae70bb 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -2,8 +2,8 @@ "/js/build/app.js": "/js/build/app.js?id=19253af36b58ed3fb6770c7bb944f079", "/css/dist/skins/skin-black-dark.css": "/css/dist/skins/skin-black-dark.css?id=78bfb1c7b5782df4fb0ac7e36f80f847", "/css/dist/skins/_all-skins.css": "/css/dist/skins/_all-skins.css?id=503d0b09e157a22f555e3670d1ec9bb5", - "/css/build/overrides.css": "/css/build/overrides.css?id=2bfc7b71d951c5ac026dbc034f7373b1", - "/css/build/app.css": "/css/build/app.css?id=4b4c2f1225d59efa7a22b76f7bbe39d8", + "/css/build/overrides.css": "/css/build/overrides.css?id=f2822e504d454229b662569ee39c579d", + "/css/build/app.css": "/css/build/app.css?id=945d335018cdbccc366d239b6c7d67d6", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=4ea0068716c1bb2434d87a16d51b98c9", "/css/dist/skins/skin-yellow.css": "/css/dist/skins/skin-yellow.css?id=7b315b9612b8fde8f9c5b0ddb6bba690", "/css/dist/skins/skin-yellow-dark.css": "/css/dist/skins/skin-yellow-dark.css?id=f6b2e7fa795596ac4754500c9c30eacc", @@ -19,7 +19,7 @@ "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=a82b065847bf3cd5d713c04ee8dc86c6", "/css/dist/skins/skin-blue-dark.css": "/css/dist/skins/skin-blue-dark.css?id=7aacfabbafd138c5af6420609f97820d", "/css/dist/skins/skin-black.css": "/css/dist/skins/skin-black.css?id=76482123f6c70e866d6b971ba91de7bb", - "/css/dist/all.css": "/css/dist/all.css?id=12e96a25d0dc3ee39e9d3ce97044fbbf", + "/css/dist/all.css": "/css/dist/all.css?id=8867854784ed3c7ec07d84940894fd47", "/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/js/select2/i18n/af.js": "/js/select2/i18n/af.js?id=4f6fcd73488ce79fae1b7a90aceaecde", diff --git a/resources/assets/less/overrides.less b/resources/assets/less/overrides.less index e072e4b1e4..ab932ddfbf 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -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; diff --git a/resources/lang/en-US/admin/users/table.php b/resources/lang/en-US/admin/users/table.php index 094cdbc412..969770b213 100644 --- a/resources/lang/en-US/admin/users/table.php +++ b/resources/lang/en-US/admin/users/table.php @@ -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', diff --git a/resources/lang/en-US/general.php b/resources/lang/en-US/general.php index 433fdd1106..1588669742 100644 --- a/resources/lang/en-US/general.php +++ b/resources/lang/en-US/general.php @@ -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?', diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 8c1e34d1a0..69723a1696 100755 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -78,7 +78,9 @@