debian-mirror-gitlab/spec/models/active_session_spec.rb

416 lines
15 KiB
Ruby
Raw Normal View History

2019-07-07 11:18:12 +05:30
# frozen_string_literal: true
2019-12-04 20:38:33 +05:30
require 'spec_helper'
2018-10-15 14:42:47 +05:30
RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
let(:user) do
create(:user).tap do |user|
user.current_sign_in_at = Time.current
end
end
2020-04-15 14:45:12 +05:30
let(:rack_session) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
let(:session) { instance_double(ActionDispatch::Request::Session, id: rack_session, '[]': {}) }
2018-10-15 14:42:47 +05:30
let(:request) do
double(:request, {
user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \
'(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]',
ip: '127.0.0.1',
session: session
})
end
describe '#current?' do
it 'returns true if the active session matches the current session' do
2020-04-15 14:45:12 +05:30
active_session = ActiveSession.new(session_id: rack_session)
2018-10-15 14:42:47 +05:30
expect(active_session.current?(session)).to be true
end
it 'returns false if the active session does not match the current session' do
2020-04-15 14:45:12 +05:30
active_session = ActiveSession.new(session_id: Rack::Session::SessionId.new('59822c7d9fcdfa03725eff41782ad97d'))
2018-10-15 14:42:47 +05:30
expect(active_session.current?(session)).to be false
end
it 'returns false if the session id is nil' do
active_session = ActiveSession.new(session_id: nil)
session = double(:session, id: nil)
expect(active_session.current?(session)).to be false
end
end
2020-03-13 15:44:24 +05:30
describe '#public_id' do
it 'returns an encrypted, url-encoded session id' do
2020-04-15 14:45:12 +05:30
original_session_id = Rack::Session::SessionId.new("!*'();:@&\n=+$,/?%abcd#123[4567]8")
2020-03-13 15:44:24 +05:30
active_session = ActiveSession.new(session_id: original_session_id)
2020-04-15 14:45:12 +05:30
encrypted_id = active_session.public_id
2020-03-13 15:44:24 +05:30
derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
2020-04-15 14:45:12 +05:30
expect(original_session_id.public_id).to eq derived_session_id
2020-03-13 15:44:24 +05:30
end
end
2018-10-15 14:42:47 +05:30
describe '.list' do
it 'returns all sessions by user' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' }))
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
end
it 'does not return obsolete entries and cleans them up' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
Gitlab::Redis::SharedState.with do |redis|
expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
end
it 'returns an empty array if the use does not have any active session' do
expect(ActiveSession.list(user)).to eq []
end
end
2019-09-04 21:01:54 +05:30
describe '.list_sessions' do
it 'uses the ActiveSession lookup to return original sessions' do
Gitlab::Redis::SharedState.with do |redis|
2020-04-15 14:45:12 +05:30
# 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}", Marshal.dump({ _csrf_token: 'abcd' }))
2019-09-04 21:01:54 +05:30
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
end
end
describe '.session_ids_for_user' do
it 'uses the user lookup table to return session ids' do
session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
Gitlab::Redis::SharedState.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids)
end
2020-04-15 14:45:12 +05:30
expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to eq(session_ids)
2019-09-04 21:01:54 +05:30
end
end
describe '.sessions_from_ids' do
it 'uses the ActiveSession lookup to return original sessions' do
Gitlab::Redis::SharedState.with do |redis|
2020-04-15 14:45:12 +05:30
# 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}", Marshal.dump({ _csrf_token: 'abcd' }))
2019-09-04 21:01:54 +05:30
end
2020-04-15 14:45:12 +05:30
expect(ActiveSession.sessions_from_ids([rack_session])).to eq [{ _csrf_token: 'abcd' }]
2019-09-04 21:01:54 +05:30
end
it 'avoids a redis lookup for an empty array' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
expect(ActiveSession.sessions_from_ids([])).to eq([])
end
2019-09-30 21:07:59 +05:30
it 'uses redis lookup in batches' do
stub_const('ActiveSession::SESSION_BATCH_SIZE', 1)
redis = double(:redis)
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
2020-04-15 14:45:12 +05:30
sessions = %w[session-a session-b session-c session-d]
2019-09-30 21:07:59 +05:30
mget_responses = sessions.map { |session| [Marshal.dump(session)]}
2020-04-15 14:45:12 +05:30
expect(redis).to receive(:mget).exactly(4).times.and_return(*mget_responses)
2019-09-30 21:07:59 +05:30
2020-04-15 14:45:12 +05:30
session_ids = [1, 2].map { |id| Rack::Session::SessionId.new(id.to_s) }
expect(ActiveSession.sessions_from_ids(session_ids).map(&:to_s)).to eql(sessions)
2019-09-30 21:07:59 +05:30
end
2019-09-04 21:01:54 +05:30
end
2018-10-15 14:42:47 +05:30
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' do
ActiveSession.set(user, request)
Gitlab::Redis::SharedState.with do |redis|
2020-04-22 19:07:51 +05:30
expect(redis.scan_each.to_a).to include(
2018-10-15 14:42:47 +05:30
"session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d",
"session:lookup:user:gitlab:#{user.id}"
2020-04-22 19:07:51 +05:30
)
2018-10-15 14:42:47 +05:30
end
end
it 'adds timestamps and information from the request' do
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
ActiveSession.set(user, request)
session = ActiveSession.list(user)
expect(session.count).to eq 1
expect(session.first).to have_attributes(
ip_address: '127.0.0.1',
browser: 'Mobile Safari',
os: 'iOS',
device_name: 'iPhone 6',
device_type: 'smartphone',
created_at: Time.zone.parse('2018-03-12 09:06'),
2020-03-13 15:44:24 +05:30
updated_at: Time.zone.parse('2018-03-12 09:06')
2018-10-15 14:42:47 +05:30
)
end
end
it 'keeps the created_at from the login on consecutive requests' do
now = Time.zone.parse('2018-03-12 09:06')
Timecop.freeze(now) do
ActiveSession.set(user, request)
Timecop.freeze(now + 1.minute) do
ActiveSession.set(user, request)
session = ActiveSession.list(user)
expect(session.first).to have_attributes(
created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:07')
)
end
end
end
end
describe '.destroy' do
2020-04-15 14:45:12 +05:30
it 'gracefully handles a nil session ID' do
expect(described_class).not_to receive(:destroy_sessions)
ActiveSession.destroy(user, nil)
end
2018-10-15 14:42:47 +05:30
it 'removes the entry associated with the currently killed user session' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", '')
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
end
ActiveSession.destroy(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [
"session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d",
"session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358"
]
end
end
it 'removes the lookup entry' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
end
ActiveSession.destroy(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
end
end
it 'removes the devise session' do
Gitlab::Redis::SharedState.with do |redis|
2020-04-15 14:45:12 +05:30
redis.set("session:user:gitlab:#{user.id}:#{rack_session.public_id}", '')
# 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}", '')
2018-10-15 14:42:47 +05:30
end
ActiveSession.destroy(user, request.session.id)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
end
end
end
2020-03-13 15:44:24 +05:30
describe '.destroy_with_public_id' do
it 'receives a user and public id and destroys the associated session' do
ActiveSession.set(user, request)
session = ActiveSession.list(user).first
ActiveSession.destroy_with_public_id(user, session.public_id)
total_sessions = ActiveSession.list(user).count
expect(total_sessions).to eq 0
end
it 'handles invalid input for public id' do
expect do
ActiveSession.destroy_with_public_id(user, nil)
end.not_to raise_error
expect do
ActiveSession.destroy_with_public_id(user, "")
end.not_to raise_error
expect do
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
end.not_to raise_error
end
it 'does not attempt to destroy session when given invalid input for public id' do
expect(ActiveSession).not_to receive(:destroy)
ActiveSession.destroy_with_public_id(user, nil)
ActiveSession.destroy_with_public_id(user, "")
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
end
end
2018-10-15 14:42:47 +05:30
describe '.cleanup' do
2020-01-01 13:55:28 +05:30
before do
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
end
2018-10-15 14:42:47 +05:30
it 'removes obsolete lookup entries' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
end
it 'does not bail if there are no lookup entries' do
ActiveSession.cleanup(user)
end
2020-01-01 13:55:28 +05:30
context 'cleaning up old sessions' do
let(:max_number_of_sessions_plus_one) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 1 }
let(:max_number_of_sessions_plus_two) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 2 }
before do
Gitlab::Redis::SharedState.with do |redis|
(1..max_number_of_sessions_plus_two).each do |number|
redis.set(
"session:user:gitlab:#{user.id}:#{number}",
2020-04-15 14:45:12 +05:30
Marshal.dump(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago))
2020-01-01 13:55:28 +05:30
)
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
"#{number}"
)
end
end
end
it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
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}")
end
end
it 'removes obsolete lookup entries' do
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
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)
end
end
it 'removes obsolete lookup entries even without active session' do
Gitlab::Redis::SharedState.with do |redis|
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
"#{max_number_of_sessions_plus_two + 1}"
)
end
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
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,
(max_number_of_sessions_plus_two + 1).to_s
)
end
end
context 'when the number of active sessions is lower than the limit' do
before do
Gitlab::Redis::SharedState.with do |redis|
((max_number_of_sessions_plus_two - 4)..max_number_of_sessions_plus_two).each do |number|
redis.del("session:user:gitlab:#{user.id}:#{number}")
end
end
end
it 'does not remove active session entries, but removes lookup entries' do
lookup_entries_before_cleanup = Gitlab::Redis::SharedState.with do |redis|
redis.smembers("session:lookup:user:gitlab:#{user.id}")
end
sessions_before_cleanup = Gitlab::Redis::SharedState.with do |redis|
redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
end
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
expect(sessions.count).to eq(sessions_before_cleanup.count)
expect(lookup_entries.count).to be < lookup_entries_before_cleanup.count
end
end
end
end
2018-10-15 14:42:47 +05:30
end
end