From b93e695c39555990bc59fc1385adbff9d4fafa0d Mon Sep 17 00:00:00 2001
From: Jens Kraemer <jk@jkraemer.net>
Date: Wed, 30 Sep 2026 15:44:13 +0800
Subject: [PATCH] Run User callbacks when bulk locking or unlocking users

users#bulk_lock and #bulk_unlock updated the status with update_all, which
skips the User callbacks. Locked users kept their autologin and session tokens,
which become valid again after unlocking, and no security notification was sent
when an admin was locked or unlocked. This also affects the Lock button on the
single user delete page, which posts to bulk_lock.
---
 app/controllers/users_controller.rb      |  2 +-
 test/functional/users_controller_test.rb | 48 ++++++++++++++++++++++++
 2 files changed, 49 insertions(+), 1 deletion(-)

diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 10ccb2958..c112b68d4 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -280,7 +280,7 @@ class UsersController < ApplicationController
     users = User.logged.where(id: user_ids).where.not(id: User.current)
     (render_404; return) unless users.any?
 
-    users.update_all status: status
+    users.each {|user| user.update_attribute(:status, status)}
     flash[:notice] = l(:notice_successful_update)
     redirect_to users_path
   end
diff --git a/test/functional/users_controller_test.rb b/test/functional/users_controller_test.rb
index e4e16659b..6ec7f2bb5 100644
--- a/test/functional/users_controller_test.rb
+++ b/test/functional/users_controller_test.rb
@@ -1227,6 +1227,54 @@ class UsersControllerTest < Redmine::ControllerTest
     assert User.find_by_id(9).active?
   end
 
+  def test_bulk_lock_should_destroy_autologin_and_session_tokens
+    autologin_token = Token.create!(:user_id => 2, :action => 'autologin')
+    session_token = Token.create!(:user_id => 2, :action => 'session')
+
+    post :bulk_lock, :params => {:ids => [2]}
+    assert_nil Token.find_by_id(autologin_token.id)
+    assert_nil Token.find_by_id(session_token.id)
+  end
+
+  def test_bulk_lock_should_send_security_notification_for_admins
+    user = User.find(2)
+    user.admin = true
+    user.save!
+
+    ActionMailer::Base.deliveries.clear
+    post :bulk_lock, :params => {:ids => [2]}
+
+    assert_not_nil (mail = ActionMailer::Base.deliveries.last)
+    assert_mail_body_match(
+      I18n.t(
+        :mail_body_security_notification_remove,
+        field: I18n.t(:field_admin),
+        value: 'jsmith'
+      ),
+      mail
+    )
+  end
+
+  def test_bulk_unlock_should_send_security_notification_for_admins
+    user = User.find(2)
+    user.admin = true
+    user.status = User::STATUS_LOCKED
+    user.save!
+
+    ActionMailer::Base.deliveries.clear
+    post :bulk_unlock, :params => {:ids => [2]}
+
+    assert_not_nil (mail = ActionMailer::Base.deliveries.last)
+    assert_mail_body_match(
+      I18n.t(
+        :mail_body_security_notification_add,
+        field: I18n.t(:field_admin),
+        value: 'jsmith'
+      ),
+      mail
+    )
+  end
+
   def test_bulk_lock_should_not_lock_current_user
     assert_difference 'User.status(User::STATUS_LOCKED).count', 1 do
       delete :bulk_lock, :params => {:ids => [2, 1]}
-- 
2.55.0

