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

@@ -26,6 +26,7 @@ class EmailsController < ApplicationController
@user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy
if @user.authenticate_password(params[:user][:password])
UserEvent.build_from_request(@user, :email_change, request)
@user.update(email_address_attributes: { address: params[:user][:email] })
else
@user.errors.add(:base, "Password was incorrect")

View File

@@ -17,7 +17,7 @@ class IpAddressesController < ApplicationController
def show
@ip_address = authorize IpAddress.new(ip_addr: params[:id])
@ip_info = @ip_address.lookup.info
@ip_info = @ip_address.lookup.response
respond_with(@ip_info)
end
end

View File

@@ -0,0 +1,9 @@
class IpGeolocationsController < ApplicationController
respond_to :html, :json, :xml
def index
@ip_geolocations = authorize IpGeolocation.visible(CurrentUser.user).paginated_search(params, count_pages: true)
respond_with(@ip_geolocations)
end
end

View File

@@ -7,7 +7,7 @@ module Maintenance
end
def destroy
deletion = UserDeletion.new(CurrentUser.user, params.dig(:user, :password))
deletion = UserDeletion.new(CurrentUser.user, params.dig(:user, :password), request)
deletion.delete!
if deletion.errors.none?

View File

@@ -9,6 +9,7 @@ class PasswordResetsController < ApplicationController
redirect_to password_reset_path
elsif @user.can_receive_email?(require_verification: false)
UserMailer.password_reset(@user).deliver_later
UserEvent.create_from_request!(@user, :password_reset, request)
flash[:notice] = "Password reset email sent. Check your email"
respond_with(@user, location: new_session_path)
else

View File

@@ -10,6 +10,7 @@ class PasswordsController < ApplicationController
@user = authorize User.find(params[:user_id]), policy_class: PasswordPolicy
if @user.authenticate_password(params[:user][:old_password]) || @user.authenticate_login_key(params[:user][:signed_user_id]) || CurrentUser.user.is_owner?
UserEvent.build_from_request(@user, :password_change, request)
@user.update(password: params[:user][:password], password_confirmation: params[:user][:password_confirmation])
else
@user.errors.add(:base, "Incorrect password")

View File

@@ -20,7 +20,7 @@ class SessionsController < ApplicationController
end
def destroy
session.delete(:user_id)
SessionLoader.new(request).logout
redirect_to(posts_path, :notice => "You are now logged out")
end

View File

@@ -0,0 +1,10 @@
class UserEventsController < ApplicationController
respond_to :html, :json, :xml
def index
@user_events = authorize UserEvent.visible(CurrentUser.user).paginated_search(params, count_pages: true)
@user_events = @user_events.includes(:user, user_session: [:ip_geolocation]) if request.format.html?
respond_with(@user_events)
end
end

View File

@@ -0,0 +1,9 @@
class UserSessionsController < ApplicationController
respond_to :html, :json, :xml
def index
@user_sessions = authorize UserSession.visible(CurrentUser.user).paginated_search(params, count_pages: true)
respond_with(@user_sessions)
end
end

View File

@@ -70,6 +70,8 @@ class UsersController < ApplicationController
password_confirmation: params[:user][:password_confirmation]
)
UserEvent.build_from_request(@user, :user_creation, request)
if params[:user][:email].present?
@user.email_address = EmailAddress.new(address: params[:user][:email])
end