Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/models/active_session.rb')
-rw-r--r--app/models/active_session.rb174
1 files changed, 122 insertions, 52 deletions
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index a0e74c7f48e..0094d98fb73 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -4,7 +4,7 @@
#
# The raw session information is stored by the Rails session store
# (config/initializers/session_store.rb). These entries are accessible by the
-# rack_key_name class method and consistute the base of the session data
+# rack_key_name class method and constitute the base of the session data
# entries. All other entries in the session store can be traced back to these
# entries.
#
@@ -21,14 +21,24 @@
#
class ActiveSession
include ActiveModel::Model
+ include ::Gitlab::Redis::SessionsStoreHelper
SESSION_BATCH_SIZE = 200
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
- attr_accessor :created_at, :updated_at,
- :ip_address, :browser, :os,
- :device_name, :device_type,
- :is_impersonated, :session_id, :session_private_id
+ 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
def current?(rack_session)
return false if session_private_id.nil? || rack_session.id.nil?
@@ -38,15 +48,29 @@ class ActiveSession
session_private_id == rack_session.id.private_id
end
+ 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
+
def human_device_type
device_type&.titleize
end
def self.set(user, request)
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
session_private_id = request.session.id.private_id
client = DeviceDetector.new(request.user_agent)
timestamp = Time.current
+ expiry = Settings.gitlab['session_expire_delay'] * 60
active_user_session = new(
ip_address: request.remote_ip,
@@ -63,7 +87,14 @@ class ActiveSession
redis.pipelined do
redis.setex(
key_name(user.id, session_private_id),
- Settings.gitlab['session_expire_delay'] * 60,
+ expiry,
+ active_user_session.dump
+ )
+
+ # Deprecated legacy format - temporary to support mixed deployments
+ redis.setex(
+ key_name_v1(user.id, session_private_id),
+ expiry,
Marshal.dump(active_user_session)
)
@@ -76,7 +107,7 @@ class ActiveSession
end
def self.list(user)
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
cleaned_up_lookup_entries(redis, user).map do |raw_session|
load_raw_session(raw_session)
end
@@ -84,14 +115,17 @@ class ActiveSession
end
def self.cleanup(user)
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
clean_up_old_sessions(redis, user)
cleaned_up_lookup_entries(redis, user)
end
end
def self.destroy_sessions(redis, user, session_ids)
+ return if session_ids.empty?
+
key_names = session_ids.map { |session_id| key_name(user.id, session_id) }
+ key_names += session_ids.map { |session_id| key_name_v1(user.id, session_id) }
redis.srem(lookup_key_name(user.id), session_ids)
@@ -104,7 +138,7 @@ class ActiveSession
def self.destroy_session(user, session_id)
return unless session_id
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
destroy_sessions(redis, user, [session_id].compact)
end
end
@@ -113,26 +147,31 @@ class ActiveSession
sessions = not_impersonated(user)
sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session
- Gitlab::Redis::SharedState.with do |redis|
- session_ids = (sessions.map(&:session_id) | sessions.map(&:session_private_id)).compact
+ redis_store_class.with do |redis|
+ session_ids = sessions.flat_map(&:ids)
destroy_sessions(redis, user, session_ids) if session_ids.any?
end
end
- def self.not_impersonated(user)
+ private_class_method def self.not_impersonated(user)
list(user).reject(&:is_impersonated)
end
- def self.rack_key_name(session_id)
- "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}"
+ private_class_method def self.rack_key_name(session_id)
+ "#{Gitlab::Redis::Sessions::SESSION_NAMESPACE}:#{session_id}"
end
def self.key_name(user_id, session_id = '*')
- "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
+ "#{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}"
end
def self.lookup_key_name(user_id)
- "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
+ "#{Gitlab::Redis::Sessions::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
end
def self.list_sessions(user)
@@ -143,7 +182,7 @@ class ActiveSession
#
# Returns an array of strings
def self.session_ids_for_user(user_id)
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
redis.smembers(lookup_key_name(user_id))
end
end
@@ -156,7 +195,7 @@ class ActiveSession
def self.sessions_from_ids(session_ids)
return [] if session_ids.empty?
- Gitlab::Redis::SharedState.with do |redis|
+ redis_store_class.with do |redis|
session_keys = rack_session_keys(session_ids)
session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch|
@@ -169,71 +208,102 @@ class ActiveSession
end
end
- # Deserializes a session Hash object from Redis.
- #
+ def dump
+ "v2:#{Gitlab::Json.dump(self)}"
+ end
+
+ # Private:
+
# raw_session - Raw bytes from Redis
#
- # Returns an ActiveSession object
- def self.load_raw_session(raw_session)
- # rubocop:disable Security/MarshalLoad
- Marshal.load(raw_session)
- # rubocop:enable Security/MarshalLoad
+ # 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
end
- def self.rack_session_keys(rack_session_ids)
- rack_session_ids.map { |session_id| rack_key_name(session_id)}
+ private_class_method def self.rack_session_keys(rack_session_ids)
+ rack_session_ids.map { |session_id| rack_key_name(session_id) }
end
- def self.raw_active_session_entries(redis, session_ids, user_id)
- return [] if session_ids.empty?
+ 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
- entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+ found.compact!
+ missing = session_ids - found.keys
+ return found if missing.empty?
- Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
- redis.mget(entry_keys)
+ 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
end
+
+ fallbacks.merge(found.compact)
end
- def self.active_session_entries(session_ids, user_id, redis)
+ private_class_method 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|
- load_raw_session(raw_session)
- end
+ raw_active_session_entries(redis, session_ids, user_id)
+ .values
+ .compact
+ .map { load_raw_session(_1) }
end
- def self.clean_up_old_sessions(redis, user)
+ private_class_method 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!
- destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
- destroyable_session_ids = destroyable_sessions.flat_map { |session| [session.session_id, session.session_private_id] }.compact
- destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
+ 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)
end
# 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.
- def self.cleaned_up_lookup_entries(redis, user)
+ # 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)
session_ids = session_ids_for_user(user.id)
- entries = raw_active_session_entries(redis, session_ids, user.id)
+ session_ids_and_entries = raw_active_session_entries(redis, session_ids, user.id)
# remove expired keys.
# only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually.
- session_ids_and_entries = session_ids.zip(entries)
- 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)
+ 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
end
end
- entries.compact
+ session_ids_and_entries.values.compact
end
end