From e4fa0b463ca29a2c7a18e411da8f8170420d5fa6 Mon Sep 17 00:00:00 2001 From: snipe Date: Tue, 7 Jul 2026 21:46:59 +0100 Subject: [PATCH] Clean up --- .../Users/ImpersonateController.php | 5 + .../Controllers/Users/UsersController.php | 36 +++++ app/Models/User.php | 16 +++ .../lang/en-US/admin/settings/general.php | 1 + resources/lang/en-US/admin/users/general.php | 2 + resources/lang/en-US/admin/users/message.php | 1 + .../views/modals/confirm-action.blade.php | 6 +- .../partials/impersonation-banner.blade.php | 4 +- resources/views/users/view.blade.php | 80 ++++------- routes/web/users.php | 8 ++ tests/Feature/Users/ImpersonateUserTest.php | 39 ++++++ tests/Feature/Users/Ui/TwoFactorResetTest.php | 125 ++++++++++++++++++ 12 files changed, 267 insertions(+), 56 deletions(-) create mode 100644 tests/Feature/Users/Ui/TwoFactorResetTest.php diff --git a/app/Http/Controllers/Users/ImpersonateController.php b/app/Http/Controllers/Users/ImpersonateController.php index d894696b14..f4bfd44f8c 100644 --- a/app/Http/Controllers/Users/ImpersonateController.php +++ b/app/Http/Controllers/Users/ImpersonateController.php @@ -29,6 +29,11 @@ class ImpersonateController extends Controller ->with('error', trans('admin/users/message.impersonate.cannot_impersonate_self')); } + if ($user->isSuperUser()) { + return redirect()->route('users.show', $user) + ->with('error', trans('admin/users/message.impersonate.cannot_impersonate_superuser')); + } + if ($user->deleted_at !== null || $user->activated != 1) { return redirect()->route('users.show', $user) ->with('error', trans('admin/users/message.impersonate.target_not_active')); diff --git a/app/Http/Controllers/Users/UsersController.php b/app/Http/Controllers/Users/UsersController.php index 2373c272cc..1e32fa913f 100755 --- a/app/Http/Controllers/Users/UsersController.php +++ b/app/Http/Controllers/Users/UsersController.php @@ -834,4 +834,40 @@ class UsersController extends Controller return redirect()->back()->with('error', trans('general.pwd_reset_not_sent')); } + + public function twoFactorReset(User $user): RedirectResponse + { + $this->authorize('update', $user); + + if (! $user->twoFactorResettable()) { + return redirect()->back()->with('error', trans('general.unauthorized')); + } + + if (! auth()->user()->can('canEditAuthFields', $user) || ! auth()->user()->can('editableOnDemo')) { + return redirect()->back()->with('error', trans('general.unauthorized')); + } + + try { + $user->two_factor_secret = null; + $user->two_factor_enrolled = 0; + $user->saveQuietly(); + + $log = new Actionlog; + $log->target_type = User::class; + $log->target_id = $user->id; + $log->item_type = User::class; + $log->item_id = $user->id; + $log->created_at = date('Y-m-d H:i:s'); + $log->created_by = auth()->id(); + $log->logaction('2FA reset'); + + return redirect()->route('users.show', $user) + ->with('success', trans('admin/settings/general.two_factor_reset_success')); + } catch (\Exception $e) { + Log::error($e); + + return redirect()->route('users.show', $user) + ->with('error', trans('admin/settings/general.two_factor_reset_error')); + } + } } diff --git a/app/Models/User.php b/app/Models/User.php index 4c973f9523..8c58a431a1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -563,6 +563,22 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo return in_array(mb_strtolower((string) $this->username), $allowed, true); } + public function mayImpersonate(User $target): bool + { + return $this->canImpersonate() + && $target->id !== $this->id + && ! $target->isSuperUser() + && $target->deleted_at === null + && $target->activated == 1; + } + + public function twoFactorResettable(): bool + { + return $this->activated == '1' + && $this->two_factor_active_and_enrolled() + && ! in_array(Setting::getSettings()->two_factor_enabled, ['0', '', null], true); + } + /** * Checks if the user is an admin * diff --git a/resources/lang/en-US/admin/settings/general.php b/resources/lang/en-US/admin/settings/general.php index d7e3b5dd62..e7388e3b58 100644 --- a/resources/lang/en-US/admin/settings/general.php +++ b/resources/lang/en-US/admin/settings/general.php @@ -306,6 +306,7 @@ return [ 'two_factor_enabled_text' => 'Enable Two Factor', 'two_factor_reset' => 'Reset Two-Factor Secret', 'two_factor_reset_help' => 'This will force the user to enroll their device with their authenticator app again. This can be useful if their currently enrolled device is lost or stolen. ', + 'two_factor_reset_confirm' => 'You are about to reset the two-factor secret for :name. They will need to re-enroll their device the next time they log in. Continue?', 'two_factor_reset_success' => 'Two factor device successfully reset', 'two_factor_reset_error' => 'Two factor device reset failed', 'two_factor_enabled_warning' => 'Enabling two-factor if it is not currently enabled will immediately force you to authenticate with a Google Auth enrolled device. You will have the ability to enroll your device if one is not currently enrolled.', diff --git a/resources/lang/en-US/admin/users/general.php b/resources/lang/en-US/admin/users/general.php index 813317334c..953cbf8764 100644 --- a/resources/lang/en-US/admin/users/general.php +++ b/resources/lang/en-US/admin/users/general.php @@ -66,6 +66,8 @@ return [ 'cannot_edit_privileged_user_companies' => 'Only an admin or super admin can modify the company assignments of an admin or super admin user.', 'cannot_manage_companies_without_membership' => 'Full multiple company support with floater mode is enabled, so you must be assigned to at least one company before you can manage another user\'s company assignments.', 'impersonate_user' => 'Login as :name', + 'impersonate_confirm_title' => 'Confirm Impersonation', + 'impersonate_confirm_body' => 'You are about to log in as :name. Any action you take will be recorded as if :name performed it. Continue?', 'impersonating_banner_title' => 'Impersonating:', 'impersonating_banner_text' => 'You are currently logged in as :name. Anything you do will be recorded as if :name did it. Your real account is :impersonator.', 'impersonating_stop_link' => 'Switch back to :name', diff --git a/resources/lang/en-US/admin/users/message.php b/resources/lang/en-US/admin/users/message.php index 767ca91a40..84e798c30b 100644 --- a/resources/lang/en-US/admin/users/message.php +++ b/resources/lang/en-US/admin/users/message.php @@ -24,6 +24,7 @@ return [ 'started' => 'You are now logged in as :name.', 'stopped' => 'You are back to your own account.', 'cannot_impersonate_self' => 'You cannot log in as yourself.', + 'cannot_impersonate_superuser' => 'You cannot log in as another superuser.', 'target_not_active' => 'That user is deactivated or deleted and cannot be logged in as.', 'impersonator_missing' => 'The original account for this impersonation session no longer exists. Please log in again.', ], diff --git a/resources/views/modals/confirm-action.blade.php b/resources/views/modals/confirm-action.blade.php index 135a9799ed..ba2508a081 100644 --- a/resources/views/modals/confirm-action.blade.php +++ b/resources/views/modals/confirm-action.blade.php @@ -1,5 +1,5 @@ -