Files
danbooru/app/logical/session_loader.rb
evazion 65adcd09c2 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.
2021-01-08 22:34:37 -06:00

138 lines
3.8 KiB
Ruby

class SessionLoader
class AuthenticationFailure < StandardError; end
attr_reader :session, :request, :params
def initialize(request)
@request = request
@session = request.session
@params = request.parameters
end
def login(name, password)
user = User.find_by_name(name)
if user.present? && user.authenticate_password(password)
session[:user_id] = user.id
UserEvent.build_from_request(user, :login, request)
user.last_logged_in_at = Time.now
user.last_ip_addr = request.remote_ip
user.save!
user
elsif user.nil?
nil # username incorrect
else
UserEvent.create_from_request!(user, :failed_login, request)
nil # password incorrect
end
end
def logout
session.delete(:user_id)
UserEvent.create_from_request!(CurrentUser.user, :logout, request)
end
def load
CurrentUser.user = User.anonymous
CurrentUser.ip_addr = request.remote_ip
if has_api_authentication?
load_session_for_api
elsif params[:signed_user_id]
load_param_user(params[:signed_user_id])
elsif session[:user_id]
load_session_user
end
set_statement_timeout
update_last_logged_in_at
update_last_ip_addr
set_time_zone
set_country
set_safe_mode
initialize_session_cookies
CurrentUser.user.unban! if CurrentUser.user.ban_expired?
ensure
DanbooruLogger.add_session_attributes(request, session, CurrentUser.user)
end
def has_api_authentication?
request.authorization.present? || params[:login].present? || params[:api_key].present?
end
private
def set_statement_timeout
timeout = CurrentUser.user.statement_timeout
ActiveRecord::Base.connection.execute("set statement_timeout = #{timeout}")
end
def load_session_for_api
if request.authorization
authenticate_basic_auth
elsif params[:login].present? && params[:api_key].present?
authenticate_api_key(params[:login], params[:api_key])
else
raise AuthenticationFailure
end
end
def authenticate_basic_auth
credentials = ::Base64.decode64(request.authorization.split(' ', 2).last || '')
login, api_key = credentials.split(/:/, 2)
DanbooruLogger.add_attributes("request.params", login: login)
authenticate_api_key(login, api_key)
end
def authenticate_api_key(name, api_key)
user = User.find_by_name(name)&.authenticate_api_key(api_key)
raise AuthenticationFailure if user.blank?
CurrentUser.user = user
end
# XXX use rails 6.1 signed ids (https://github.com/rails/rails/blob/6-1-stable/activerecord/CHANGELOG.md)
def load_param_user(signed_user_id)
session[:user_id] = Danbooru::MessageVerifier.new(:login).verify(signed_user_id)
load_session_user
end
def load_session_user
user = User.find_by_id(session[:user_id])
CurrentUser.user = user if user
end
def update_last_logged_in_at
return if CurrentUser.is_anonymous?
return if CurrentUser.last_logged_in_at && CurrentUser.last_logged_in_at > 1.hour.ago
CurrentUser.user.update_attribute(:last_logged_in_at, Time.now)
end
def update_last_ip_addr
return if CurrentUser.is_anonymous?
return if CurrentUser.user.last_ip_addr == @request.remote_ip
CurrentUser.user.update_attribute(:last_ip_addr, @request.remote_ip)
end
def set_time_zone
Time.zone = CurrentUser.user.time_zone
end
# Depends on Cloudflare
# https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation
def set_country
CurrentUser.country = request.headers["CF-IPCountry"]
end
def set_safe_mode
safe_mode = request.host.match?(/safebooru/i) || params[:safe_mode].to_s.truthy? || CurrentUser.user.enable_safe_mode?
CurrentUser.safe_mode = safe_mode
end
def initialize_session_cookies
session.options[:expire_after] = 20.years
session[:started_at] ||= Time.now.utc.to_s
end
end