users: track logins, signups, and other user events.

Add tracking of certain important user actions. These events include:

* Logins
* Logouts
* Failed login attempts
* Account creations
* Account deletions
* Password reset requests
* Password changes
* Email address changes

This is similar to the mod actions log, except for account activity
related to a single user.

The information tracked includes the user, the event type (login,
logout, etc), the timestamp, the user's IP address, IP geolocation
information, the user's browser user agent, and the user's session ID
from their session cookie. This information is visible to mods only.

This is done with three models. The UserEvent model tracks the event
type (login, logout, password change, etc) and the user. The UserEvent
is tied to a UserSession, which contains the user's IP address and
browser metadata. Finally, the IpGeolocation model contains the
geolocation information for IPs, including the city, country, ISP, and
whether the IP is a proxy.

This tracking will be used for a few purposes:

* Letting users view their account history, to detect things like logins
  from unrecognized IPs, failed logins attempts, password changes, etc.
* Rate limiting failed login attempts.
* Detecting sockpuppet accounts using their login history.
* Detecting unauthorized account sharing.
This commit is contained in:
evazion
2021-01-07 20:06:59 -06:00
parent 94e125709c
commit 65adcd09c2
39 changed files with 856 additions and 28 deletions

View File

@@ -89,6 +89,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_equal("abc@ogres.net", @user.reload.email_address.address)
assert_equal(false, @user.email_address.is_verified)
assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default"
assert_equal(true, @user.user_events.email_change.exists?)
end
should "create a new address" do
@@ -102,6 +103,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_equal("abc@ogres.net", @user.reload.email_address.address)
assert_equal(false, @user.reload.email_address.is_verified)
assert_enqueued_email_with UserMailer, :email_change_confirmation, args: [@user], queue: "default"
assert_equal(true, @user.user_events.email_change.exists?)
end
end
@@ -112,6 +114,7 @@ class EmailsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal("bob@ogres.net", @user.reload.email_address.address)
assert_no_emails
assert_equal(false, @user.user_events.email_change.exists?)
end
end
end

View File

@@ -18,18 +18,22 @@ module Maintenance
context "#destroy" do
should "delete the user when given the correct password" do
delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "password" }}
assert_redirected_to posts_path
assert_equal(true, @user.reload.is_deleted?)
assert_equal("Your account has been deactivated", flash[:notice])
assert_nil(session[:user_id])
assert_equal(true, @user.user_events.user_deletion.exists?)
end
should "not delete the user when given an incorrect password" do
delete_auth maintenance_user_deletion_path, @user, params: { user: { password: "hunter2" }}
assert_redirected_to maintenance_user_deletion_path
assert_equal(false, @user.reload.is_deleted?)
assert_equal("Password is incorrect", flash[:notice])
assert_equal(@user.id, session[:user_id])
assert_equal(false, @user.user_events.user_deletion.exists?)
end
end
end

View File

@@ -16,6 +16,7 @@ class PasswordResetsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to new_session_path
assert_enqueued_email_with UserMailer, :password_reset, args: [@user], queue: "default"
assert_equal(true, @user.user_events.password_reset.exists?)
end
should "should fail if the user doesn't have a verified email address" do

View File

@@ -20,6 +20,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to @user
assert_equal(false, @user.reload.authenticate_password("12345"))
assert_equal(@user, @user.authenticate_password("abcde"))
assert_equal(true, @user.user_events.password_change.exists?)
end
should "update the password when given a valid login key" do
@@ -29,6 +30,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to @user
assert_equal(false, @user.reload.authenticate_password("12345"))
assert_equal(@user, @user.authenticate_password("abcde"))
assert_equal(true, @user.user_events.password_change.exists?)
end
should "allow the site owner to change the password of other users" do
@@ -55,6 +57,7 @@ class PasswordsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal(@user, @user.reload.authenticate_password("12345"))
assert_equal(false, @user.authenticate_password("abcde"))
assert_equal(false, @user.user_events.password_change.exists?)
end
should "not update the password when password confirmation fails for the new password" do

View File

@@ -20,11 +20,20 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
assert_redirected_to posts_path
assert_equal(@user.id, session[:user_id])
assert_not_nil(@user.reload.last_ip_addr)
assert_equal(true, @user.user_events.login.exists?)
end
should "not log the user in when given an incorrect password" do
post session_path, params: { name: @user.name, password: "wrong"}
assert_response 401
assert_nil(nil, session[:user_id])
assert_equal(true, @user.user_events.failed_login.exists?)
end
should "not log the user in when given an incorrect username" do
post session_path, params: { name: "dne", password: "password" }
assert_response 401
assert_nil(nil, session[:user_id])
end
@@ -66,11 +75,18 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
end
context "destroy action" do
should "clear the session" do
setup do
delete_auth session_path, @user
end
should "clear the session" do
assert_redirected_to posts_path
assert_nil(session[:user_id])
end
should "generate a logout event" do
assert_equal(true, @user.user_events.logout.exists?)
end
end
context "sign_out action" do

View File

@@ -260,6 +260,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(User.last, User.last.authenticate_password("xxxxx1"))
assert_nil(User.last.email_address)
assert_no_enqueued_emails
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "create a user with a valid email" do
@@ -270,6 +271,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(User.last, User.last.authenticate_password("xxxxx1"))
assert_equal("webmaster@danbooru.donmai.us", User.last.email_address.address)
assert_enqueued_email_with UserMailer, :welcome_user, args: [User.last], queue: "default"
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "not create a user with an invalid email" do
@@ -307,6 +309,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.is_member?)
assert_equal(false, User.last.is_restricted?)
assert_equal(false, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "mark accounts created by already logged in users as restricted" do
@@ -318,6 +321,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, User.last.is_member?)
assert_equal(true, User.last.is_restricted?)
assert_equal(true, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "mark users signing up from proxies as restricted" do
@@ -330,6 +334,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, User.last.is_member?)
assert_equal(true, User.last.is_restricted?)
assert_equal(true, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "mark users signing up from a partial banned IP as restricted" do
@@ -344,6 +349,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.requires_verification)
assert_equal(1, @ip_ban.reload.hit_count)
assert(@ip_ban.last_hit_at > 1.minute.ago)
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "not mark users signing up from non-proxies as restricted" do
@@ -356,6 +362,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.is_member?)
assert_equal(false, User.last.is_restricted?)
assert_equal(false, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "mark accounts registered from an IPv4 address recently used for another account as restricted" do
@@ -368,6 +375,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(false, User.last.is_member?)
assert_equal(true, User.last.is_restricted?)
assert_equal(true, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end
should "not mark users signing up from localhost as restricted" do
@@ -379,6 +387,7 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal(true, User.last.is_member?)
assert_equal(false, User.last.is_restricted?)
assert_equal(false, User.last.requires_verification)
assert_equal(true, User.last.user_events.user_creation.exists?)
end
end
end