debian-mirror-gitlab/app/models/active_session.rb

310 lines
9.4 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2021-03-11 19:13:27 +05:30
# Backing store for GitLab session data.
#
# The raw session information is stored by the Rails session store
# (config/initializers/session_store.rb). These entries are accessible by the
2022-01-26 12:08:38 +05:30
# rack_key_name class method and constitute the base of the session data
2021-03-11 19:13:27 +05:30
# entries. All other entries in the session store can be traced back to these
# entries.
#
# After a user logs in (config/initializers/warden.rb) a further entry is made
# in Redis. This entry holds a record of the user's logged in session. These
# are accessible with the key_name(user_id, session_id) class method. These
# entries will expire. Lookups to these entries are lazilly cleaned on future
# user access.
#
# There is a reference to all sessions that belong to a specific user. A
# user may login through multiple browsers/devices and thus record multiple
# login sessions. These are accessible through the lookup_key_name(user_id)
# class method.
#
2018-10-15 14:42:47 +05:30
class ActiveSession
include ActiveModel::Model
2022-01-26 12:08:38 +05:30
include ::Gitlab::Redis::SessionsStoreHelper
2018-10-15 14:42:47 +05:30
2019-09-30 21:07:59 +05:30
SESSION_BATCH_SIZE = 200
2020-01-01 13:55:28 +05:30
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
2019-09-30 21:07:59 +05:30
2022-01-26 12:08:38 +05:30
attr_accessor :ip_address, :browser, :os,
:device_name, :device_type,
:is_impersonated, :session_id, :session_private_id
attr_reader :created_at, :updated_at
def created_at=(time)
@created_at = time.is_a?(String) ? Time.zone.parse(time) : time
end
def updated_at=(time)
@updated_at = time.is_a?(String) ? Time.zone.parse(time) : time
end
2018-10-15 14:42:47 +05:30
2020-10-04 03:57:07 +05:30
def current?(rack_session)
return false if session_private_id.nil? || rack_session.id.nil?
2018-10-15 14:42:47 +05:30
2020-04-15 14:45:12 +05:30
# Rack v2.0.8+ added private_id, which uses the hash of the
# public_id to avoid timing attacks.
2020-10-04 03:57:07 +05:30
session_private_id == rack_session.id.private_id
2018-10-15 14:42:47 +05:30
end
2022-01-26 12:08:38 +05:30
def eql?(other)
other.is_a?(self.class) && id == other.id
end
alias_method :==, :eql?
def id
session_private_id.presence || session_id
end
def ids
[session_private_id, session_id].compact
end
2018-10-15 14:42:47 +05:30
def human_device_type
device_type&.titleize
end
def self.set(user, request)
2022-01-26 12:08:38 +05:30
redis_store_class.with do |redis|
2020-10-04 03:57:07 +05:30
session_private_id = request.session.id.private_id
2018-10-15 14:42:47 +05:30
client = DeviceDetector.new(request.user_agent)
timestamp = Time.current
2022-01-26 12:08:38 +05:30
expiry = Settings.gitlab['session_expire_delay'] * 60
2018-10-15 14:42:47 +05:30
active_user_session = new(
2020-06-23 00:09:42 +05:30
ip_address: request.remote_ip,
2018-10-15 14:42:47 +05:30
browser: client.name,
os: client.os_name,
device_name: client.device_name,
device_type: client.device_type,
created_at: user.current_sign_in_at || timestamp,
updated_at: timestamp,
2020-10-04 03:57:07 +05:30
session_private_id: session_private_id,
2019-03-13 22:55:13 +05:30
is_impersonated: request.session[:impersonator_id].present?
2018-10-15 14:42:47 +05:30
)
redis.pipelined do
redis.setex(
2020-10-04 03:57:07 +05:30
key_name(user.id, session_private_id),
2022-01-26 12:08:38 +05:30
expiry,
active_user_session.dump
)
# Deprecated legacy format - temporary to support mixed deployments
redis.setex(
key_name_v1(user.id, session_private_id),
expiry,
2018-10-15 14:42:47 +05:30
Marshal.dump(active_user_session)
)
redis.sadd(
lookup_key_name(user.id),
2020-10-04 03:57:07 +05:30
session_private_id
2018-10-15 14:42:47 +05:30
)
end
end
end
def self.list(user)
2022-01-26 12:08:38 +05:30
redis_store_class.with do |redis|
2020-04-15 14:45:12 +05:30
cleaned_up_lookup_entries(redis, user).map do |raw_session|
load_raw_session(raw_session)
2018-10-15 14:42:47 +05:30
end
end
end
2020-10-04 03:57:07 +05:30
def self.cleanup(user)
2022-01-26 12:08:38 +05:30
redis_store_class.with do |redis|
2020-10-04 03:57:07 +05:30
clean_up_old_sessions(redis, user)
cleaned_up_lookup_entries(redis, user)
2020-01-01 13:55:28 +05:30
end
end
2018-10-15 14:42:47 +05:30
2020-01-01 13:55:28 +05:30
def self.destroy_sessions(redis, user, session_ids)
2022-01-26 12:08:38 +05:30
return if session_ids.empty?
2020-10-04 03:57:07 +05:30
key_names = session_ids.map { |session_id| key_name(user.id, session_id) }
2022-01-26 12:08:38 +05:30
key_names += session_ids.map { |session_id| key_name_v1(user.id, session_id) }
2018-10-15 14:42:47 +05:30
2020-10-04 03:57:07 +05:30
redis.srem(lookup_key_name(user.id), session_ids)
2020-07-28 23:09:34 +05:30
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(key_names)
redis.del(rack_session_keys(session_ids))
end
2018-10-15 14:42:47 +05:30
end
2021-03-05 16:19:46 +05:30
def self.destroy_session(user, session_id)
2020-10-04 03:57:07 +05:30
return unless session_id
2022-01-26 12:08:38 +05:30
redis_store_class.with do |redis|
2021-03-05 16:19:46 +05:30
destroy_sessions(redis, user, [session_id].compact)
2018-10-15 14:42:47 +05:30
end
end
2020-10-04 03:57:07 +05:30
def self.destroy_all_but_current(user, current_rack_session)
sessions = not_impersonated(user)
sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session
2020-09-03 11:15:55 +05:30
2022-01-26 12:08:38 +05:30
redis_store_class.with do |redis|
session_ids = sessions.flat_map(&:ids)
2020-10-04 03:57:07 +05:30
destroy_sessions(redis, user, session_ids) if session_ids.any?
2020-09-03 11:15:55 +05:30
end
end
2022-01-26 12:08:38 +05:30
private_class_method def self.not_impersonated(user)
2020-09-03 11:15:55 +05:30
list(user).reject(&:is_impersonated)
end
2022-01-26 12:08:38 +05:30
private_class_method def self.rack_key_name(session_id)
"#{Gitlab::Redis::Sessions::SESSION_NAMESPACE}:#{session_id}"
2021-03-11 19:13:27 +05:30
end
2018-10-15 14:42:47 +05:30
def self.key_name(user_id, session_id = '*')
2022-01-26 12:08:38 +05:30
"#{Gitlab::Redis::Sessions::USER_SESSIONS_NAMESPACE}::v2:#{user_id}:#{session_id}"
end
# Deprecated
def self.key_name_v1(user_id, session_id = '*')
"#{Gitlab::Redis::Sessions::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
2018-10-15 14:42:47 +05:30
end
def self.lookup_key_name(user_id)
2022-01-26 12:08:38 +05:30
"#{Gitlab::Redis::Sessions::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
2018-10-15 14:42:47 +05:30
end
2019-09-04 21:01:54 +05:30
def self.list_sessions(user)
2019-10-12 21:52:04 +05:30
sessions_from_ids(session_ids_for_user(user.id))
2019-09-04 21:01:54 +05:30
end
2018-10-15 14:42:47 +05:30
2020-04-15 14:45:12 +05:30
# Lists the relevant session IDs for the user.
#
2020-10-04 03:57:07 +05:30
# Returns an array of strings
2019-10-12 21:52:04 +05:30
def self.session_ids_for_user(user_id)
2022-01-26 12:08:38 +05:30
redis_store_class.with do |redis|
2020-10-04 03:57:07 +05:30
redis.smembers(lookup_key_name(user_id))
2019-09-04 21:01:54 +05:30
end
end
2018-10-15 14:42:47 +05:30
2020-05-24 23:13:21 +05:30
# Lists the session Hash objects for the given session IDs.
2020-04-15 14:45:12 +05:30
#
2020-10-04 03:57:07 +05:30
# session_ids - An array of strings
2020-04-15 14:45:12 +05:30
#
# Returns an array of ActiveSession objects
2019-09-04 21:01:54 +05:30
def self.sessions_from_ids(session_ids)
return [] if session_ids.empty?
2018-10-15 14:42:47 +05:30
2022-01-26 12:08:38 +05:30
redis_store_class.with do |redis|
2020-04-15 14:45:12 +05:30
session_keys = rack_session_keys(session_ids)
2018-10-15 14:42:47 +05:30
2019-09-30 21:07:59 +05:30
session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch|
2020-07-28 23:09:34 +05:30
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.mget(session_keys_batch).compact.map do |raw_session|
load_raw_session(raw_session)
end
2019-09-30 21:07:59 +05:30
end
2019-09-04 21:01:54 +05:30
end
end
end
2022-01-26 12:08:38 +05:30
def dump
"v2:#{Gitlab::Json.dump(self)}"
end
# Private:
2020-04-15 14:45:12 +05:30
# raw_session - Raw bytes from Redis
#
2022-01-26 12:08:38 +05:30
# Returns an instance of this class
private_class_method def self.load_raw_session(raw_session)
return unless raw_session
if raw_session.start_with?('v2:')
session_data = Gitlab::Json.parse(raw_session[3..]).symbolize_keys
new(**session_data)
else
# Deprecated legacy format. To be removed in 15.0
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/30516
# Explanation of why this Marshal.load call is OK:
# https://gitlab.com/gitlab-com/gl-security/appsec/appsec-reviews/-/issues/124#note_744576714
# rubocop:disable Security/MarshalLoad
Marshal.load(raw_session)
# rubocop:enable Security/MarshalLoad
end
2020-10-04 03:57:07 +05:30
end
2020-04-15 14:45:12 +05:30
2022-01-26 12:08:38 +05:30
private_class_method def self.rack_session_keys(rack_session_ids)
rack_session_ids.map { |session_id| rack_key_name(session_id) }
2020-04-15 14:45:12 +05:30
end
2022-01-26 12:08:38 +05:30
private_class_method def self.raw_active_session_entries(redis, session_ids, user_id)
return {} if session_ids.empty?
found = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
session_ids.zip(redis.mget(entry_keys)).to_h
end
2019-09-04 21:01:54 +05:30
2022-01-26 12:08:38 +05:30
found.compact!
missing = session_ids - found.keys
return found if missing.empty?
2020-01-01 13:55:28 +05:30
2022-01-26 12:08:38 +05:30
fallbacks = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
entry_keys = missing.map { |session_id| key_name_v1(user_id, session_id) }
missing.zip(redis.mget(entry_keys)).to_h
2020-07-28 23:09:34 +05:30
end
2022-01-26 12:08:38 +05:30
fallbacks.merge(found.compact)
2020-01-01 13:55:28 +05:30
end
2019-09-04 21:01:54 +05:30
2022-01-26 12:08:38 +05:30
private_class_method def self.active_session_entries(session_ids, user_id, redis)
2020-01-01 13:55:28 +05:30
return [] if session_ids.empty?
2022-01-26 12:08:38 +05:30
raw_active_session_entries(redis, session_ids, user_id)
.values
.compact
.map { load_raw_session(_1) }
2019-09-04 21:01:54 +05:30
end
2022-01-26 12:08:38 +05:30
private_class_method def self.clean_up_old_sessions(redis, user)
2020-01-01 13:55:28 +05:30
session_ids = session_ids_for_user(user.id)
return if session_ids.count <= ALLOWED_NUMBER_OF_ACTIVE_SESSIONS
sessions = active_session_entries(session_ids, user.id, redis)
2022-01-26 12:08:38 +05:30
sessions.sort_by!(&:updated_at).reverse!
# remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
destroyable_session_ids = sessions
.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
.flat_map(&:ids)
destroy_sessions(redis, user, destroyable_session_ids)
2020-01-01 13:55:28 +05:30
end
2020-04-15 14:45:12 +05:30
# Cleans up the lookup set by removing any session IDs that are no longer present.
#
# Returns an array of marshalled ActiveModel objects that are still active.
2022-01-26 12:08:38 +05:30
# Records removed keys in the optional `removed` argument array.
def self.cleaned_up_lookup_entries(redis, user, removed = [])
lookup_key = lookup_key_name(user.id)
2019-10-12 21:52:04 +05:30
session_ids = session_ids_for_user(user.id)
2022-01-26 12:08:38 +05:30
session_ids_and_entries = raw_active_session_entries(redis, session_ids, user.id)
2018-10-15 14:42:47 +05:30
# remove expired keys.
# only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually.
2022-01-26 12:08:38 +05:30
redis.pipelined do |pipeline|
session_ids_and_entries.each do |session_id, entry|
next if entry
pipeline.srem(lookup_key, session_id)
removed << session_id
2019-10-12 21:52:04 +05:30
end
2018-10-15 14:42:47 +05:30
end
2022-01-26 12:08:38 +05:30
session_ids_and_entries.values.compact
2018-10-15 14:42:47 +05:30
end
end