mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-28 08:26:48 +09:00
Sign out the account's other sessions when its password changes
Changing the password is how someone shuts out whoever else is signed
in to their account, but every session opened with the old password
stayed valid. Now the others end and their live connections close, as
on sign-out. The session the password was changed from stays signed in.
Changing the password outside a request, as from a console, ends every
session.
From #371. 1.5.2 has no record snapshot or response cache, so the test
that a cached session lookup doesn't outlive the password change is
left out; the rest applies as it is.
(cherry picked from commit c78c3fd7ef)
This commit is contained in:
1 parent
9474e70f5a
commit
c78f03fda7
4 files changed
+77
No files matched your search
@@ -8,6 +8,8 @@ class Users::ProfilesController < ApplicationController
|
|||||||
|
|
||||||
def update
|
def update
|
||||||
@user.update user_params
|
@user.update user_params
|
||||||
|
disconnect_remote_connections if @user.saved_change_to_password_digest?
|
||||||
|
|
||||||
redirect_to user_profile_url, notice: update_notice
|
redirect_to user_profile_url, notice: update_notice
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class User < ApplicationRecord
|
|||||||
has_secure_password validations: false
|
has_secure_password validations: false
|
||||||
|
|
||||||
after_create_commit :grant_membership_to_open_rooms
|
after_create_commit :grant_membership_to_open_rooms
|
||||||
|
after_update :sign_out_other_sessions, if: :saved_change_to_password_digest?
|
||||||
|
|
||||||
scope :ordered, -> { order("LOWER(name)") }
|
scope :ordered, -> { order("LOWER(name)") }
|
||||||
scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") }
|
scope :filtered_by, ->(query) { where("name like ?", "%#{query}%") }
|
||||||
@@ -50,6 +51,12 @@ class User < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
# A new password is how someone shuts out whoever else got into their account, so it ends the sessions opened
|
||||||
|
# with the old one. The session it was changed from, if any, stays.
|
||||||
|
def sign_out_other_sessions
|
||||||
|
sessions.excluding(Current.session).delete_all
|
||||||
|
end
|
||||||
|
|
||||||
# One statement, which SQLite runs under the write lock, so no room is closed between reading the open ones and
|
# One statement, which SQLite runs under the write lock, so no room is closed between reading the open ones and
|
||||||
# inserting. A room that Room#destroy_later closes is either closed before, and skipped, or after, and takes this
|
# inserting. A room that Room#destroy_later closes is either closed before, and skipped, or after, and takes this
|
||||||
# membership away with the others. A room that granted itself to every user when it was created is skipped too,
|
# membership away with the others. A room that granted itself to every user when it was created is skipped too,
|
||||||
|
|||||||
@@ -27,6 +27,52 @@ class Users::ProfilesControllerTest < ActionDispatch::IntegrationTest
|
|||||||
assert_equal "david@37signals.com", users(:david).email_address
|
assert_equal "david@37signals.com", users(:david).email_address
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "changing the password signs out the account's other sessions but not this one" do
|
||||||
|
other_device = open_session
|
||||||
|
other_device.post session_url, params: { email_address: users(:david).email_address, password: "secret123456" }
|
||||||
|
other_device.get user_profile_url
|
||||||
|
other_device.assert_response :success
|
||||||
|
|
||||||
|
put user_profile_url, params: { user: { password: "a new secret123456" } }
|
||||||
|
assert_redirected_to user_profile_url
|
||||||
|
|
||||||
|
other_device.get user_profile_url
|
||||||
|
other_device.assert_redirected_to new_session_url
|
||||||
|
get user_profile_url
|
||||||
|
assert_response :success
|
||||||
|
assert_equal 1, users(:david).sessions.count
|
||||||
|
end
|
||||||
|
|
||||||
|
test "changing the password closes the account's live connections" do
|
||||||
|
remote_connections = mock
|
||||||
|
remote_connections.expects(:disconnect).with(reconnect: true)
|
||||||
|
ActionCable.server.stubs(:remote_connections).returns(mock.tap { |m| m.expects(:where).with(current_user: users(:david)).returns(remote_connections) })
|
||||||
|
|
||||||
|
put user_profile_url, params: { user: { password: "a new secret123456" } }
|
||||||
|
|
||||||
|
assert_redirected_to user_profile_url
|
||||||
|
end
|
||||||
|
|
||||||
|
test "changing the password still signs out other sessions when the realtime service is unreachable" do
|
||||||
|
other_session = users(:david).sessions.first
|
||||||
|
ActionCable.server.stubs(:remote_connections).raises(RuntimeError.new("cable down"))
|
||||||
|
|
||||||
|
put user_profile_url, params: { user: { password: "a new secret123456" } }
|
||||||
|
|
||||||
|
assert_redirected_to user_profile_url
|
||||||
|
assert_not Session.exists?(other_session.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "updating the profile without a new password leaves other sessions signed in" do
|
||||||
|
ActionCable.server.expects(:remote_connections).never
|
||||||
|
|
||||||
|
assert_no_difference -> { users(:david).sessions.count } do
|
||||||
|
put user_profile_url, params: { user: { name: "David", password: "" } }
|
||||||
|
end
|
||||||
|
|
||||||
|
assert_redirected_to user_profile_url
|
||||||
|
end
|
||||||
|
|
||||||
test "updates are limited to the current user" do
|
test "updates are limited to the current user" do
|
||||||
put user_profile_url(users(:jason)), params: { user: { name: "John Doe" } }
|
put user_profile_url(users(:jason)), params: { user: { name: "John Doe" } }
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,28 @@ class UserTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "changing the password outside a request signs out every session" do
|
||||||
|
users(:david).sessions.start!(user_agent: "Other device", ip_address: "127.0.0.1")
|
||||||
|
|
||||||
|
assert_changes -> { users(:david).sessions.count }, to: 0 do
|
||||||
|
users(:david).update!(password: "a new secret123456")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "changing the password keeps the session it was changed from" do
|
||||||
|
Current.session = users(:david).sessions.start!(user_agent: "This device", ip_address: "127.0.0.1")
|
||||||
|
|
||||||
|
users(:david).update!(password: "a new secret123456")
|
||||||
|
|
||||||
|
assert_equal [ Current.session ], users(:david).sessions.reload
|
||||||
|
end
|
||||||
|
|
||||||
|
test "changing other details keeps every session" do
|
||||||
|
assert_no_changes -> { users(:david).sessions.count } do
|
||||||
|
users(:david).update!(name: "Dave", email_address: "dave@37signals.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def create_new_user
|
def create_new_user
|
||||||
User.create!(name: "User", email_address: "user@example.com", password: "secret123456")
|
User.create!(name: "User", email_address: "user@example.com", password: "secret123456")
|
||||||
|
|||||||
Reference in new issue
Block a user