Commit 7e7cf0b8 authored by Alex Kalderimis's avatar Alex Kalderimis

Use Gitlab::Json to serialize sessions

This changes session serialization, shifting from the use
of Marshal (which has security implications) to the safer use of JSON.

In order to support existing active sessions in the old format, we
lookup sessions by both the old and new keys, and fallback to the old
parsing if the stored value is not JSON.

Changelog: security
parent b990ab0b
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# #
# The raw session information is stored by the Rails session store # The raw session information is stored by the Rails session store
# (config/initializers/session_store.rb). These entries are accessible by the # (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. All other entries in the session store can be traced back to these
# entries. # entries.
# #
...@@ -26,11 +26,20 @@ class ActiveSession ...@@ -26,11 +26,20 @@ class ActiveSession
SESSION_BATCH_SIZE = 200 SESSION_BATCH_SIZE = 200
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100 ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
attr_accessor :created_at, :updated_at, attr_accessor :ip_address, :browser, :os,
:ip_address, :browser, :os,
:device_name, :device_type, :device_name, :device_type,
:is_impersonated, :session_id, :session_private_id :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) def current?(rack_session)
return false if session_private_id.nil? || rack_session.id.nil? return false if session_private_id.nil? || rack_session.id.nil?
...@@ -39,6 +48,15 @@ class ActiveSession ...@@ -39,6 +48,15 @@ class ActiveSession
session_private_id == rack_session.id.private_id session_private_id == rack_session.id.private_id
end 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 human_device_type def human_device_type
device_type&.titleize device_type&.titleize
end end
...@@ -65,7 +83,7 @@ class ActiveSession ...@@ -65,7 +83,7 @@ class ActiveSession
redis.setex( redis.setex(
key_name(user.id, session_private_id), key_name(user.id, session_private_id),
Settings.gitlab['session_expire_delay'] * 60, Settings.gitlab['session_expire_delay'] * 60,
Marshal.dump(active_user_session) active_user_session.dump
) )
redis.sadd( redis.sadd(
...@@ -92,7 +110,10 @@ class ActiveSession ...@@ -92,7 +110,10 @@ class ActiveSession
end end
def self.destroy_sessions(redis, user, session_ids) 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(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) redis.srem(lookup_key_name(user.id), session_ids)
...@@ -115,7 +136,7 @@ class ActiveSession ...@@ -115,7 +136,7 @@ class ActiveSession
sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session
redis_store_class.with do |redis| redis_store_class.with do |redis|
session_ids = (sessions.map(&:session_id) | sessions.map(&:session_private_id)).compact session_ids = sessions.map(&:id).compact
destroy_sessions(redis, user, session_ids) if session_ids.any? destroy_sessions(redis, user, session_ids) if session_ids.any?
end end
end end
...@@ -129,6 +150,11 @@ class ActiveSession ...@@ -129,6 +150,11 @@ class ActiveSession
end end
def self.key_name(user_id, session_id = '*') def self.key_name(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}" "#{Gitlab::Redis::Sessions::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end end
...@@ -170,41 +196,57 @@ class ActiveSession ...@@ -170,41 +196,57 @@ class ActiveSession
end end
end end
# Deserializes a session Hash object from Redis. def dump
# "v1:#{Gitlab::Json.dump(self)}"
end
# raw_session - Raw bytes from Redis # raw_session - Raw bytes from Redis
# #
# Returns an ActiveSession object # Returns an instance of this class
def self.load_raw_session(raw_session) def self.load_raw_session(raw_session)
# rubocop:disable Security/MarshalLoad return unless raw_session
if raw_session.start_with?('v1:')
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: # Explanation of why this Marshal.load call is OK:
# https://gitlab.com/gitlab-com/gl-security/appsec/appsec-reviews/-/issues/124#note_744576714 # https://gitlab.com/gitlab-com/gl-security/appsec/appsec-reviews/-/issues/124#note_744576714
# rubocop:disable Security/MarshalLoad
Marshal.load(raw_session) Marshal.load(raw_session)
# rubocop:enable Security/MarshalLoad # rubocop:enable Security/MarshalLoad
end end
end
def self.rack_session_keys(rack_session_ids) def self.rack_session_keys(rack_session_ids)
rack_session_ids.map { |session_id| rack_key_name(session_id)} rack_session_ids.map { |session_id| rack_key_name(session_id) }
end end
def self.raw_active_session_entries(redis, session_ids, user_id) def self.raw_active_session_entries(redis, session_ids, user_id)
return [] if session_ids.empty? 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) } entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
session_ids.zip(redis.mget(entry_keys)).to_h
end
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do fallbacks = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.mget(entry_keys) entry_keys = session_ids.map { |session_id| key_name_v1(user_id, session_id) }
session_ids.zip(redis.mget(entry_keys)).to_h
end end
fallbacks.merge(found.compact)
end end
def self.active_session_entries(session_ids, user_id, redis) def self.active_session_entries(session_ids, user_id, redis)
return [] if session_ids.empty? return [] if session_ids.empty?
entry_keys = raw_active_session_entries(redis, session_ids, user_id) raw_active_session_entries(redis, session_ids, user_id)
.values
entry_keys.compact.map do |raw_session| .compact
load_raw_session(raw_session) .map { load_raw_session(_1) }
end
end end
def self.clean_up_old_sessions(redis, user) def self.clean_up_old_sessions(redis, user)
...@@ -212,12 +254,15 @@ class ActiveSession ...@@ -212,12 +254,15 @@ class ActiveSession
return if session_ids.count <= ALLOWED_NUMBER_OF_ACTIVE_SESSIONS 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 = active_session_entries(session_ids, user.id, redis)
sessions.sort_by! {|session| session.updated_at }.reverse! sessions.sort_by!(&: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 # remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any? destroyable_session_ids = sessions
.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
.flat_map { |session| [session.session_id, session.session_private_id].compact }
destroy_sessions(redis, user, destroyable_session_ids)
end end
# Cleans up the lookup set by removing any session IDs that are no longer present. # Cleans up the lookup set by removing any session IDs that are no longer present.
...@@ -225,18 +270,17 @@ class ActiveSession ...@@ -225,18 +270,17 @@ class ActiveSession
# Returns an array of marshalled ActiveModel objects that are still active. # Returns an array of marshalled ActiveModel objects that are still active.
def self.cleaned_up_lookup_entries(redis, user) def self.cleaned_up_lookup_entries(redis, user)
session_ids = session_ids_for_user(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. # remove expired keys.
# only the single key entries are automatically expired by redis, the # only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually. # lookup entries in the set need to be removed manually.
session_ids_and_entries = session_ids.zip(entries)
redis.pipelined do |pipeline| redis.pipelined do |pipeline|
session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry| session_ids_and_entries.each do |session_id, entry|
pipeline.srem(lookup_key_name(user.id), session_id) pipeline.srem(lookup_key_name(user.id), session_id) unless entry
end end
end end
entries.compact session_ids_and_entries.values.compact
end end
end end
...@@ -127,8 +127,7 @@ namespace :gitlab do ...@@ -127,8 +127,7 @@ namespace :gitlab do
lookup_key_count = redis.scard(key) lookup_key_count = redis.scard(key)
session_ids = ActiveSession.session_ids_for_user(user_id) session_ids = ActiveSession.session_ids_for_user(user_id)
entries = ActiveSession.raw_active_session_entries(redis, session_ids, user_id) session_ids_and_entries = ActiveSession.raw_active_session_entries(redis, session_ids, user_id)
session_ids_and_entries = session_ids.zip(entries)
inactive_session_ids = session_ids_and_entries.map do |session_id, session| inactive_session_ids = session_ids_and_entries.map do |session_id, session|
session_id if session.nil? session_id if session.nil?
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
let(:lookup_key) { described_class.lookup_key_name(user.id) }
let(:user) do let(:user) do
create(:user).tap do |user| create(:user).tap do |user|
user.current_sign_in_at = Time.current user.current_sign_in_at = Time.current
...@@ -43,46 +44,82 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -43,46 +44,82 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
end end
describe '.list' do describe '.list' do
def make_session(id)
described_class.new(session_id: id)
end
it 'returns all sessions by user' do it 'returns all sessions by user' do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' })) # Some deprecated sessions
redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' })) redis.set(described_class.key_name_v1(user.id, "6919a6f1bb119dd7396fadc38fd18d0d"), Marshal.dump(make_session('a')))
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '') redis.set(described_class.key_name_v1(user.id, "59822c7d9fcdfa03725eff41782ad97d"), Marshal.dump(make_session('b')))
# Some new sessions
redis.set(described_class.key_name(user.id, 'some-unique-id-x'), make_session('c').dump)
redis.set(described_class.key_name(user.id, 'some-unique-id-y'), make_session('d').dump)
# Some red herrings
redis.set(described_class.key_name(9999, "5c8611e4f9c69645ad1a1492f4131358"), 'irrelevant')
redis.set(described_class.key_name_v1(9999, "5c8611e4f9c69645ad1a1492f4131358"), 'irrelevant')
redis.sadd( redis.sadd(
"session:lookup:user:gitlab:#{user.id}", lookup_key,
%w[ %w[
6919a6f1bb119dd7396fadc38fd18d0d 6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d 59822c7d9fcdfa03725eff41782ad97d
some-unique-id-x
some-unique-id-y
] ]
) )
end end
expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }] expect(described_class.list(user)).to contain_exactly(
have_attributes(session_id: 'a'),
have_attributes(session_id: 'b'),
have_attributes(session_id: 'c'),
have_attributes(session_id: 'd')
)
end end
shared_examples 'ignoring obsolete entries' do
let(:session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
let(:session) { described_class.new(session_id: 'a') }
it 'does not return obsolete entries and cleans them up' do it 'does not return obsolete entries and cleans them up' do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' })) redis.set(session_key, serialized_session)
redis.sadd( redis.sadd(
"session:lookup:user:gitlab:#{user.id}", lookup_key,
%w[ [
6919a6f1bb119dd7396fadc38fd18d0d session_id,
59822c7d9fcdfa03725eff41782ad97d '59822c7d9fcdfa03725eff41782ad97d'
] ]
) )
end end
expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }] expect(ActiveSession.list(user)).to contain_exactly(session)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d'] expect(redis.sscan_each(lookup_key)).to contain_exactly session_id
end
end end
end end
it 'returns an empty array if the use does not have any active session' do context 'when the current session is in the old format' do
expect(ActiveSession.list(user)).to eq [] let(:session_key) { described_class.key_name_v1(user.id, session_id) }
let(:serialized_session) { Marshal.dump(session) }
it_behaves_like 'ignoring obsolete entries'
end
context 'when the current session is in the new format' do
let(:session_key) { described_class.key_name(user.id, session_id) }
let(:serialized_session) { session.dump }
it_behaves_like 'ignoring obsolete entries'
end
it 'returns an empty array if the user does not have any active session' do
expect(ActiveSession.list(user)).to be_empty
end end
end end
...@@ -107,13 +144,11 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -107,13 +144,11 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
describe '.session_ids_for_user' do describe '.session_ids_for_user' do
it 'uses the user lookup table to return session ids' do it 'uses the user lookup table to return session ids' do
session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids) redis.sadd(lookup_key, %w[a b c])
end end
expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to eq(session_ids) expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to match_array(%w[a b c])
end end
end end
...@@ -151,52 +186,56 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -151,52 +186,56 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
it 'sets a new redis entry for the user session and a lookup entry' do it 'sets a new redis entry for the user session and a lookup entry' do
ActiveSession.set(user, request) ActiveSession.set(user, request)
session_id = "2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae"
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
expect(redis.scan_each.to_a).to include( expect(redis.scan_each.to_a).to include(
"session:user:gitlab:#{user.id}:2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae", described_class.key_name(user.id, session_id),
"session:lookup:user:gitlab:#{user.id}" lookup_key
) )
end end
end end
it 'adds timestamps and information from the request' do it 'adds timestamps and information from the request' do
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do time = Time.zone.parse('2018-03-12 09:06')
ActiveSession.set(user, request)
session = ActiveSession.list(user) travel_to(time) do
described_class.set(user, request)
expect(session.count).to eq 1 sessions = described_class.list(user)
expect(session.first).to have_attributes(
expect(sessions).to contain_exactly have_attributes(
ip_address: '127.0.0.1', ip_address: '127.0.0.1',
browser: 'Mobile Safari', browser: 'Mobile Safari',
os: 'iOS', os: 'iOS',
device_name: 'iPhone 6', device_name: 'iPhone 6',
device_type: 'smartphone', device_type: 'smartphone',
created_at: Time.zone.parse('2018-03-12 09:06'), created_at: eq(time),
updated_at: Time.zone.parse('2018-03-12 09:06') updated_at: eq(time)
) )
end end
end end
it 'keeps the created_at from the login on consecutive requests' do it 'keeps the created_at from the login on consecutive requests' do
now = Time.zone.parse('2018-03-12 09:06') created_at = Time.zone.parse('2018-03-12 09:06')
updated_at = created_at + 1.minute
Timecop.freeze(now) do travel_to(created_at) do
ActiveSession.set(user, request) ActiveSession.set(user, request)
end
Timecop.freeze(now + 1.minute) do travel_to(updated_at) do
ActiveSession.set(user, request) ActiveSession.set(user, request)
session = ActiveSession.list(user) session = ActiveSession.list(user)
expect(session.first).to have_attributes( expect(session.first).to have_attributes(
created_at: Time.zone.parse('2018-03-12 09:06'), created_at: eq(created_at),
updated_at: Time.zone.parse('2018-03-12 09:07') updated_at: eq(updated_at)
) )
end end
end end
end end
end
describe '.destroy_session' do describe '.destroy_session' do
shared_examples 'removes all session data' do shared_examples 'removes all session data' do
...@@ -206,10 +245,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -206,10 +245,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
# Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88 # Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88
redis.set("session:gitlab:#{rack_session.private_id}", '') redis.set("session:gitlab:#{rack_session.private_id}", '')
redis.set(described_class.key_name(user.id, active_session_lookup_key), redis.set(session_key, serialized_session)
Marshal.dump(active_session)) redis.sadd(lookup_key, active_session_lookup_key)
redis.sadd(described_class.lookup_key_name(user.id),
active_session_lookup_key)
end end
end end
...@@ -225,7 +262,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -225,7 +262,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
subject subject
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty expect(redis.scan_each(match: lookup_key).to_a).to be_empty
end end
end end
...@@ -253,8 +290,20 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -253,8 +290,20 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
let(:active_session) { ActiveSession.new(session_private_id: rack_session.private_id) } let(:active_session) { ActiveSession.new(session_private_id: rack_session.private_id) }
let(:active_session_lookup_key) { rack_session.private_id } let(:active_session_lookup_key) { rack_session.private_id }
context 'when using old session key serialization' do
let(:session_key) { described_class.key_name_v1(user.id, active_session_lookup_key) }
let(:serialized_session) { Marshal.dump(active_session) }
include_examples 'removes all session data' include_examples 'removes all session data'
end end
context 'when using new session key serialization' do
let(:session_key) { described_class.key_name(user.id, active_session_lookup_key) }
let(:serialized_session) { active_session.dump }
include_examples 'removes all session data'
end
end
end end
end end
...@@ -265,7 +314,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -265,7 +314,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
ActiveSession.destroy_all_but_current(user, nil) ActiveSession.destroy_all_but_current(user, nil)
end end
context 'with user sessions' do shared_examples 'with user sessions' do
let(:current_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' } let(:current_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
before do before do
...@@ -274,10 +323,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -274,10 +323,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
[current_session_id, '59822c7d9fcdfa03725eff41782ad97d'].each do |session_public_id| [current_session_id, '59822c7d9fcdfa03725eff41782ad97d'].each do |session_public_id|
session_private_id = Rack::Session::SessionId.new(session_public_id).private_id session_private_id = Rack::Session::SessionId.new(session_public_id).private_id
active_session = ActiveSession.new(session_private_id: session_private_id) active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(described_class.key_name(user.id, session_private_id), redis.set(key_name(user.id, session_private_id), dump_session(active_session))
Marshal.dump(active_session)) redis.sadd(lookup_key, session_private_id)
redis.sadd(described_class.lookup_key_name(user.id),
session_private_id)
end end
# setup for unrelated user # setup for unrelated user
...@@ -285,10 +332,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -285,10 +332,8 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
session_private_id = Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358').private_id session_private_id = Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358').private_id
active_session = ActiveSession.new(session_private_id: session_private_id) active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(described_class.key_name(unrelated_user_id, session_private_id), redis.set(key_name(unrelated_user_id, session_private_id), dump_session(active_session))
Marshal.dump(active_session)) redis.sadd(described_class.lookup_key_name(unrelated_user_id), session_private_id)
redis.sadd(described_class.lookup_key_name(unrelated_user_id),
session_private_id)
end end
end end
...@@ -304,18 +349,16 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -304,18 +349,16 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
ActiveSession.destroy_all_but_current(user, request.session) ActiveSession.destroy_all_but_current(user, request.session)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
expect( expect(redis.smembers(lookup_key)).to contain_exactly session_private_id
redis.smembers(described_class.lookup_key_name(user.id))
).to eq([session_private_id])
end end
end end
it 'does not remove impersonated sessions' do it 'does not remove impersonated sessions' do
impersonated_session_id = '6919a6f1bb119dd7396fadc38fd18eee' impersonated_session_id = '6919a6f1bb119dd7396fadc38fd18eee'
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
redis.set(described_class.key_name(user.id, impersonated_session_id), redis.set(key_name(user.id, impersonated_session_id),
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new(impersonated_session_id), is_impersonated: true))) dump_session(ActiveSession.new(session_id: Rack::Session::SessionId.new(impersonated_session_id), is_impersonated: true)))
redis.sadd(described_class.lookup_key_name(user.id), impersonated_session_id) redis.sadd(lookup_key, impersonated_session_id)
end end
expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2) expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2)
...@@ -323,6 +366,30 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -323,6 +366,30 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1) expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
end end
end end
context 'with legacy sessions' do
def key_name(user_id, id)
described_class.key_name_v1(user_id, id)
end
def dump_session(session)
Marshal.dump(session)
end
it_behaves_like 'with user sessions'
end
context 'with new sessions' do
def key_name(user_id, id)
described_class.key_name(user_id, id)
end
def dump_session(session)
session.dump
end
it_behaves_like 'with user sessions'
end
end end
describe '.cleanup' do describe '.cleanup' do
...@@ -330,17 +397,22 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -330,17 +397,22 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5) stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
end end
shared_examples 'cleaning up' do
context 'when removing obsolete sessions' do
let(:current_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
it 'removes obsolete lookup entries' do it 'removes obsolete lookup entries' do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '') redis.set(session_key, '')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d') redis.sadd(lookup_key, current_session_id)
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') redis.sadd(lookup_key, '59822c7d9fcdfa03725eff41782ad97d')
end end
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d'] expect(redis.smembers(lookup_key)).to contain_exactly current_session_id
end
end end
end end
...@@ -354,15 +426,12 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -354,15 +426,12 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
before do before do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
(1..max_number_of_sessions_plus_two).each do |number| max_number_of_sessions_plus_two.times do |number|
redis.set( redis.set(
"session:user:gitlab:#{user.id}:#{number}", key_name(user.id, number),
Marshal.dump(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago)) dump_session(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago))
)
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
"#{number}"
) )
redis.sadd(lookup_key, number.to_s)
end end
end end
end end
...@@ -371,10 +440,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -371,10 +440,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a sessions = described_class.list(user)
expect(sessions.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS) expect(sessions.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
expect(sessions).not_to include("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}", "session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}") expect(sessions).not_to include(
have_attributes(session_id: max_number_of_sessions_plus_one),
have_attributes(session_id: max_number_of_sessions_plus_two)
)
end end
end end
...@@ -382,7 +454,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -382,7 +454,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}") lookup_entries = redis.smembers(lookup_key)
expect(lookup_entries.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS) expect(lookup_entries.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
expect(lookup_entries).not_to include(max_number_of_sessions_plus_one.to_s, max_number_of_sessions_plus_two.to_s) expect(lookup_entries).not_to include(max_number_of_sessions_plus_one.to_s, max_number_of_sessions_plus_two.to_s)
...@@ -391,16 +463,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -391,16 +463,13 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
it 'removes obsolete lookup entries even without active session' do it 'removes obsolete lookup entries even without active session' do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
redis.sadd( redis.sadd(lookup_key, "#{max_number_of_sessions_plus_two + 1}")
"session:lookup:user:gitlab:#{user.id}",
"#{max_number_of_sessions_plus_two + 1}"
)
end end
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}") lookup_entries = redis.smembers(lookup_key)
expect(lookup_entries.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS) expect(lookup_entries.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
expect(lookup_entries).not_to include( expect(lookup_entries).not_to include(
...@@ -415,25 +484,24 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -415,25 +484,24 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
before do before do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
((max_number_of_sessions_plus_two - 4)..max_number_of_sessions_plus_two).each do |number| ((max_number_of_sessions_plus_two - 4)..max_number_of_sessions_plus_two).each do |number|
redis.del("session:user:gitlab:#{user.id}:#{number}") redis.del(key_name(user.id, number))
end end
end end
end end
it 'does not remove active session entries, but removes lookup entries' do it 'does not remove active session entries, but removes lookup entries' do
lookup_entries_before_cleanup = Gitlab::Redis::Sessions.with do |redis| lookup_entries_before_cleanup = Gitlab::Redis::Sessions.with do |redis|
redis.smembers("session:lookup:user:gitlab:#{user.id}") redis.smembers(lookup_key)
end end
sessions_before_cleanup = Gitlab::Redis::Sessions.with do |redis| sessions_before_cleanup = described_class.list(user)
redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
end
ActiveSession.cleanup(user) described_class.cleanup(user)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}") lookup_entries = redis.smembers(lookup_key)
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a sessions = described_class.list(user)
expect(sessions.count).to eq(sessions_before_cleanup.count) expect(sessions.count).to eq(sessions_before_cleanup.count)
expect(lookup_entries.count).to be < lookup_entries_before_cleanup.count expect(lookup_entries.count).to be < lookup_entries_before_cleanup.count
end end
...@@ -449,29 +517,56 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do ...@@ -449,29 +517,56 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
(1..max_number_of_sessions_plus_two).each do |number| (1..max_number_of_sessions_plus_two).each do |number|
redis.set( redis.set(
"session:user:gitlab:#{user.id}:#{number}", key_name(user.id, number),
Marshal.dump(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago)) dump_session(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago))
)
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
"#{number}"
) )
redis.sadd(lookup_key, number.to_s)
end end
end end
end end
it 'removes obsolete active sessions entries' do it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user) described_class.cleanup(user)
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a sessions = described_class.list(user)
expect(sessions.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS) expect(sessions.count).to eq(described_class::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
expect(sessions).not_to( expect(sessions).not_to include(
include("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}", key_name(user.id, max_number_of_sessions_plus_one),
"session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}")) key_name(user.id, max_number_of_sessions_plus_two)
)
end
end end
end end
end end
context 'with legacy sessions' do
let(:session_key) { described_class.key_name_v1(user.id, current_session_id) }
def key_name(user_id, session_id)
described_class.key_name_v1(user_id, session_id)
end
def dump_session(session)
Marshal.dump(session)
end
it_behaves_like 'cleaning up'
end
context 'with new sessions' do
let(:session_key) { described_class.key_name(user.id, current_session_id) }
def key_name(user_id, session_id)
described_class.key_name(user_id, session_id)
end
def dump_session(session)
session.dump
end
it_behaves_like 'cleaning up'
end
end end
end end
...@@ -174,9 +174,9 @@ RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout do ...@@ -174,9 +174,9 @@ RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout do
before do before do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{existing_session_id}", redis.set(ActiveSession.key_name(user.id, existing_session_id),
Marshal.dump(true)) Gitlab::Json.dump(ActiveSession.new(session_id: 'x')))
redis.sadd("session:lookup:user:gitlab:#{user.id}", (1..10).to_a) redis.sadd(ActiveSession.lookup_key_name(user.id), (1..10).to_a)
end end
end end
...@@ -186,10 +186,10 @@ RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout do ...@@ -186,10 +186,10 @@ RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout do
it 'removes expired active session lookup keys' do it 'removes expired active session lookup keys' do
Gitlab::Redis::Sessions.with do |redis| Gitlab::Redis::Sessions.with do |redis|
lookup_key = "session:lookup:user:gitlab:#{user.id}" lookup_key = ActiveSession.lookup_key_name(user.id)
expect { subject }.to change { redis.scard(lookup_key) }.from(10).to(1) expect { subject }.to change { redis.scard(lookup_key) }.from(10).to(1)
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to( expect(redis.smembers(lookup_key)).to contain_exactly existing_session_id
eql([existing_session_id]))
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment