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

240 lines
7.5 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2018-10-15 14:42:47 +05:30
class ActiveSession
include ActiveModel::Model
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
2018-10-15 14:42:47 +05:30
attr_accessor :created_at, :updated_at,
2020-03-13 15:44:24 +05:30
:ip_address, :browser, :os,
:device_name, :device_type,
2020-04-15 14:45:12 +05:30
:is_impersonated, :session_id
2018-10-15 14:42:47 +05:30
def current?(session)
return false if session_id.nil? || session.id.nil?
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.
session_id.private_id == session.id.private_id
2018-10-15 14:42:47 +05:30
end
def human_device_type
device_type&.titleize
end
2020-04-15 14:45:12 +05:30
# This is not the same as Rack::Session::SessionId#public_id, but we
# need to preserve this for backwards compatibility.
2020-03-13 15:44:24 +05:30
def public_id
2020-04-15 14:45:12 +05:30
Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id.public_id)
2020-03-13 15:44:24 +05:30
end
2018-10-15 14:42:47 +05:30
def self.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
2020-04-15 14:45:12 +05:30
session_id = request.session.id.public_id
2018-10-15 14:42:47 +05:30
client = DeviceDetector.new(request.user_agent)
timestamp = Time.current
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,
2019-03-13 22:55:13 +05:30
session_id: session_id,
is_impersonated: request.session[:impersonator_id].present?
2018-10-15 14:42:47 +05:30
)
redis.pipelined do
redis.setex(
key_name(user.id, session_id),
Settings.gitlab['session_expire_delay'] * 60,
Marshal.dump(active_user_session)
)
redis.sadd(
lookup_key_name(user.id),
session_id
)
end
end
end
def self.list(user)
Gitlab::Redis::SharedState.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
def self.destroy(user, session_id)
2020-04-15 14:45:12 +05:30
return unless session_id
2018-10-15 14:42:47 +05:30
Gitlab::Redis::SharedState.with do |redis|
2020-01-01 13:55:28 +05:30
destroy_sessions(redis, user, [session_id])
end
end
2018-10-15 14:42:47 +05:30
2020-03-13 15:44:24 +05:30
def self.destroy_with_public_id(user, public_id)
2020-04-15 14:45:12 +05:30
decrypted_id = decrypt_public_id(public_id)
return if decrypted_id.nil?
session_id = Rack::Session::SessionId.new(decrypted_id)
destroy(user, session_id)
2020-03-13 15:44:24 +05:30
end
2020-01-01 13:55:28 +05:30
def self.destroy_sessions(redis, user, session_ids)
2020-04-15 14:45:12 +05:30
key_names = session_ids.map { |session_id| key_name(user.id, session_id.public_id) }
2018-10-15 14:42:47 +05:30
2020-04-15 14:45:12 +05:30
redis.srem(lookup_key_name(user.id), session_ids.map(&:public_id))
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
def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
2020-01-01 13:55:28 +05:30
clean_up_old_sessions(redis, user)
2019-09-04 21:01:54 +05:30
cleaned_up_lookup_entries(redis, user)
2018-10-15 14:42:47 +05:30
end
end
def self.key_name(user_id, session_id = '*')
"#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end
def self.lookup_key_name(user_id)
"#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
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.
#
# Returns an array of Rack::Session::SessionId objects
2019-10-12 21:52:04 +05:30
def self.session_ids_for_user(user_id)
2019-09-04 21:01:54 +05:30
Gitlab::Redis::SharedState.with do |redis|
2020-04-15 14:45:12 +05:30
session_ids = redis.smembers(lookup_key_name(user_id))
session_ids.map { |id| Rack::Session::SessionId.new(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
#
# session_ids - An array of Rack::Session::SessionId objects
#
# 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
2019-09-04 21:01:54 +05:30
Gitlab::Redis::SharedState.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
2020-05-24 23:13:21 +05:30
# Deserializes a session Hash object from Redis.
2020-04-15 14:45:12 +05:30
#
# raw_session - Raw bytes from Redis
#
# Returns an ActiveSession object
def self.load_raw_session(raw_session)
# rubocop:disable Security/MarshalLoad
session = Marshal.load(raw_session)
# rubocop:enable Security/MarshalLoad
# Older ActiveSession models serialize `session_id` as strings, To
# avoid breaking older sessions, we keep backwards compatibility
# with older Redis keys and initiate Rack::Session::SessionId here.
session.session_id = Rack::Session::SessionId.new(session.session_id) if session.try(:session_id).is_a?(String)
session
end
def self.rack_session_keys(session_ids)
session_ids.each_with_object([]) do |session_id, arr|
# This is a redis-rack implementation detail
# (https://github.com/redis-store/redis-rack/blob/master/lib/rack/session/redis.rb#L88)
#
# We need to delete session keys based on the legacy public key name
# and the newer private ID keys, but there's no well-defined interface
# so we have to do it directly.
arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.public_id}"
arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.private_id}"
end
end
2020-01-01 13:55:28 +05:30
def self.raw_active_session_entries(redis, session_ids, user_id)
2019-09-04 21:01:54 +05:30
return [] if session_ids.empty?
2020-01-01 13:55:28 +05:30
entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
2020-07-28 23:09:34 +05:30
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.mget(entry_keys)
end
2020-01-01 13:55:28 +05:30
end
2019-09-04 21:01:54 +05:30
2020-01-01 13:55:28 +05:30
def self.active_session_entries(session_ids, user_id, redis)
return [] if session_ids.empty?
entry_keys = raw_active_session_entries(redis, session_ids, user_id)
entry_keys.compact.map do |raw_session|
2020-04-15 14:45:12 +05:30
load_raw_session(raw_session)
2019-09-04 21:01:54 +05:30
end
end
2020-01-01 13:55:28 +05:30
def self.clean_up_old_sessions(redis, user)
session_ids = session_ids_for_user(user.id)
return if session_ids.count <= ALLOWED_NUMBER_OF_ACTIVE_SESSIONS
# remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
sessions = active_session_entries(session_ids, user.id, redis)
sessions.sort_by! {|session| session.updated_at }.reverse!
2020-03-13 15:44:24 +05:30
destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
2020-04-15 14:45:12 +05:30
destroyable_session_ids = destroyable_sessions.map { |session| session.session_id }
2020-03-13 15:44:24 +05:30
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
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.
2019-09-04 21:01:54 +05:30
def self.cleaned_up_lookup_entries(redis, user)
2019-10-12 21:52:04 +05:30
session_ids = session_ids_for_user(user.id)
2020-01-01 13:55:28 +05:30
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.
2019-09-04 21:01:54 +05:30
session_ids_and_entries = session_ids.zip(entries)
2019-10-12 21:52:04 +05:30
redis.pipelined do
session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
redis.srem(lookup_key_name(user.id), session_id)
end
2018-10-15 14:42:47 +05:30
end
2019-09-04 21:01:54 +05:30
entries.compact
2018-10-15 14:42:47 +05:30
end
2020-03-13 15:44:24 +05:30
private_class_method def self.decrypt_public_id(public_id)
2020-04-15 14:45:12 +05:30
Gitlab::CryptoHelper.aes256_gcm_decrypt(public_id)
2020-03-13 15:44:24 +05:30
rescue
nil
end
2018-10-15 14:42:47 +05:30
end