Commit 229fd8af authored by nmilojevic1's avatar nmilojevic1

Add specs for both SharedState and Sessions store

parent e6551492
...@@ -45,22 +45,26 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -45,22 +45,26 @@ RSpec.describe Groups::DependencyProxyForContainersController do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
context 'with an active session', :clean_gitlab_redis_shared_state do shared_examples 'active session' do
let(:session_id) { '42' } context 'with an active session' do
let(:session_time) { 5.minutes.ago } let(:session_id) { '42' }
let(:stored_session) do let(:session_time) { 5.minutes.ago }
{ 'active_group_sso_sign_ins' => { saml_provider.id => session_time } } let(:stored_session) do
end { 'active_group_sso_sign_ins' => { saml_provider.id => session_time } }
end
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session)) redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id]) redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end end
end
it_behaves_like successful_example it_behaves_like successful_example
end
end end
it_behaves_like 'redis sessions store', 'active session'
end end
context 'when git check is not enforced' do context 'when git check is not enforced' do
......
...@@ -85,44 +85,52 @@ RSpec.describe 'Login' do ...@@ -85,44 +85,52 @@ RSpec.describe 'Login' do
expect(page.body).to have_link('Register now', href: new_user_registration_path) expect(page.body).to have_link('Register now', href: new_user_registration_path)
end end
describe 'with two-factor authentication required', :clean_gitlab_redis_shared_state do RSpec.shared_examples_for 'two-factor authentication' do
let_it_be(:user) { create(:user) }
let_it_be(:smartcard_identity) { create(:smartcard_identity, user: user) }
before do before do
stub_application_setting(require_two_factor_authentication: true) load Rails.root.join('config/initializers/session_store.rb')
end end
context 'with a smartcard session' do describe 'with two-factor authentication required' do
let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) } let_it_be(:user) { create(:user) }
let(:openssl_certificate) do let_it_be(:smartcard_identity) { create(:smartcard_identity, user: user) }
instance_double(OpenSSL::X509::Certificate, subject: smartcard_identity.subject, issuer: smartcard_identity.issuer)
before do
stub_application_setting(require_two_factor_authentication: true)
end end
it 'does not ask for Two-Factor Authentication' do context 'with a smartcard session' do
allow(Gitlab::Auth::Smartcard::Certificate).to receive(:store).and_return(openssl_certificate_store) let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) }
allow(OpenSSL::X509::Certificate).to receive(:new).and_return(openssl_certificate) let(:openssl_certificate) do
allow(openssl_certificate_store).to receive(:verify).and_return(true) instance_double(OpenSSL::X509::Certificate, subject: smartcard_identity.subject, issuer: smartcard_identity.issuer)
end
it 'does not ask for Two-Factor Authentication' do
allow(Gitlab::Auth::Smartcard::Certificate).to receive(:store).and_return(openssl_certificate_store)
allow(OpenSSL::X509::Certificate).to receive(:new).and_return(openssl_certificate)
allow(openssl_certificate_store).to receive(:verify).and_return(true)
# Loging using smartcard # Loging using smartcard
visit verify_certificate_smartcard_path(client_certificate: openssl_certificate) visit verify_certificate_smartcard_path(client_certificate: openssl_certificate)
visit profile_path visit profile_path
expect(page).not_to have_content('Two-Factor Authentication') expect(page).not_to have_content('Two-Factor Authentication')
end
end end
end
context 'without a smartcard session' do context 'without a smartcard session' do
it 'asks for Two-Factor Authentication' do it 'asks for Two-Factor Authentication' do
sign_in(user) sign_in(user)
visit profile_path visit profile_path
expect(page).to have_content('Two-Factor Authentication') expect(page).to have_content('Two-Factor Authentication')
end
end end
end end
end end
it_behaves_like 'redis sessions store', 'two-factor authentication'
end end
end end
end end
......
...@@ -9,208 +9,212 @@ RSpec.describe Gitlab::Auth::GroupSaml::SessionEnforcer do ...@@ -9,208 +9,212 @@ RSpec.describe Gitlab::Auth::GroupSaml::SessionEnforcer do
end end
end end
describe '#access_restricted' do RSpec.shared_examples_for 'group saml session enforcer' do
let_it_be(:saml_provider) { create(:saml_provider, enforced_sso: true) } describe '#access_restricted' do
let_it_be(:user) { create(:user) } let_it_be(:saml_provider) { create(:saml_provider, enforced_sso: true) }
let_it_be(:identity) { create(:group_saml_identity, saml_provider: saml_provider, user: user) } let_it_be(:user) { create(:user) }
let_it_be(:identity) { create(:group_saml_identity, saml_provider: saml_provider, user: user) }
let(:root_group) { saml_provider.group } let(:root_group) { saml_provider.group }
subject(:enforced?) { described_class.new(user, root_group).access_restricted? } subject(:enforced?) { described_class.new(user, root_group).access_restricted? }
before do
stub_licensed_features(group_saml: true)
end
context 'when git check is enforced' do
before do before do
allow(saml_provider).to receive(:git_check_enforced?).and_return(true) stub_licensed_features(group_saml: true)
end end
context 'with an active session', :clean_gitlab_redis_shared_state do context 'when git check is enforced' do
let(:session_id) { '42' }
let(:session_time) { 5.minutes.ago }
let(:stored_session) do
{ 'active_group_sso_sign_ins' => { saml_provider.id => session_time } }
end
before do before do
Gitlab::Redis::SharedState.with do |redis| allow(saml_provider).to receive(:git_check_enforced?).and_return(true)
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end end
it_behaves_like 'not enforced' context 'with an active session' do
let(:session_id) { '42' }
let(:session_time) { 5.minutes.ago }
let(:stored_session) do
{ 'active_group_sso_sign_ins' => { saml_provider.id => session_time } }
end
context 'with sub-group' do
before do before do
allow(group).to receive(:root_ancestor).and_return(root_group) redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end end
let(:group) { create(:group) } it_behaves_like 'not enforced'
subject(:enforced?) { described_class.new(user, group).access_restricted? } context 'with sub-group' do
before do
allow(group).to receive(:root_ancestor).and_return(root_group)
end
it_behaves_like 'not enforced' let(:group) { create(:group) }
end
context 'with expired session' do subject(:enforced?) { described_class.new(user, group).access_restricted? }
let(:session_time) { 2.days.ago }
it 'returns true' do it_behaves_like 'not enforced'
expect(enforced?).to eq(true)
end end
end
context 'with two active sessions', :clean_gitlab_redis_shared_state do context 'with expired session' do
let(:second_session_id) { '52' } let(:session_time) { 2.days.ago }
let(:second_stored_session) do
{ 'active_group_sso_sign_ins' => { create(:saml_provider, enforced_sso: true).id => session_time } }
end
before do it 'returns true' do
Gitlab::Redis::SharedState.with do |redis| expect(enforced?).to eq(true)
redis.set("session:gitlab:#{second_session_id}", Marshal.dump(second_stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id, second_session_id])
end end
end end
it_behaves_like 'not enforced' context 'with two active sessions' do
end let(:second_session_id) { '52' }
let(:second_stored_session) do
context 'with two active sessions for the same provider and one pre-sso', :clean_gitlab_redis_shared_state do { 'active_group_sso_sign_ins' => { create(:saml_provider, enforced_sso: true).id => session_time } }
let(:second_session_id) { '52' } end
let(:third_session_id) { '62' }
let(:second_stored_session) do
{ 'active_group_sso_sign_ins' => { saml_provider.id => 2.days.ago } }
end
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:gitlab:#{second_session_id}", Marshal.dump(second_stored_session)) redis.set("session:gitlab:#{second_session_id}", Marshal.dump(second_stored_session))
redis.set("session:gitlab:#{third_session_id}", Marshal.dump({})) redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id, second_session_id])
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id, second_session_id, third_session_id]) end
end end
it_behaves_like 'not enforced'
end end
it_behaves_like 'not enforced' context 'with two active sessions for the same provider and one pre-sso' do
end let(:second_session_id) { '52' }
let(:third_session_id) { '62' }
let(:second_stored_session) do
{ 'active_group_sso_sign_ins' => { saml_provider.id => 2.days.ago } }
end
context 'without enforced_sso_expiry feature flag' do before do
let(:session_time) { 2.days.ago } redis_store_class.with do |redis|
redis.set("session:gitlab:#{second_session_id}", Marshal.dump(second_stored_session))
redis.set("session:gitlab:#{third_session_id}", Marshal.dump({}))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id, second_session_id, third_session_id])
end
end
before do it_behaves_like 'not enforced'
stub_feature_flags(enforced_sso_expiry: false)
end end
it_behaves_like 'not enforced' context 'without enforced_sso_expiry feature flag' do
end let(:session_time) { 2.days.ago }
context 'without group' do
let(:root_group) { nil }
it_behaves_like 'not enforced' before do
end stub_feature_flags(enforced_sso_expiry: false)
end
context 'without saml_provider' do it_behaves_like 'not enforced'
let(:root_group) { create(:group) } end
it_behaves_like 'not enforced' context 'without group' do
end let(:root_group) { nil }
context 'with admin', :enable_admin_mode do it_behaves_like 'not enforced'
let(:user) { create(:user, :admin) } end
it_behaves_like 'not enforced' context 'without saml_provider' do
end let(:root_group) { create(:group) }
context 'with auditor' do it_behaves_like 'not enforced'
let(:user) { create(:user, :auditor) } end
it_behaves_like 'not enforced' context 'with admin', :enable_admin_mode do
end let(:user) { create(:user, :admin) }
context 'with group owner' do it_behaves_like 'not enforced'
before do
root_group.add_owner(user)
end end
it_behaves_like 'not enforced' context 'with auditor' do
end let(:user) { create(:user, :auditor) }
end
context 'without any session' do it_behaves_like 'not enforced'
it 'returns true' do end
expect(enforced?).to eq(true)
end
context 'with admin', :enable_admin_mode do context 'with group owner' do
let(:user) { create(:user, :admin) } before do
root_group.add_owner(user)
end
it_behaves_like 'not enforced' it_behaves_like 'not enforced'
end
end end
context 'with auditor' do context 'without any session' do
let(:user) { create(:user, :auditor) } it 'returns true' do
expect(enforced?).to eq(true)
end
it_behaves_like 'not enforced' context 'with admin', :enable_admin_mode do
end let(:user) { create(:user, :admin) }
context 'with group owner' do it_behaves_like 'not enforced'
before do
root_group.add_owner(user)
end end
it_behaves_like 'not enforced' context 'with auditor' do
let(:user) { create(:user, :auditor) }
it_behaves_like 'not enforced'
end
context 'when group is a subgroup' do context 'with group owner' do
before do before do
allow(group).to receive(:root_ancestor).and_return(root_group) root_group.add_owner(user)
end end
let(:group) { create(:group) } it_behaves_like 'not enforced'
subject(:enforced?) { described_class.new(user, group).access_restricted? } context 'when group is a subgroup' do
before do
allow(group).to receive(:root_ancestor).and_return(root_group)
end
it 'returns true' do let(:group) { create(:group) }
expect(enforced?).to eq(true)
subject(:enforced?) { described_class.new(user, group).access_restricted? }
it 'returns true' do
expect(enforced?).to eq(true)
end
end end
end end
end
context 'with project bot' do context 'with project bot' do
let(:user) { create(:user, :project_bot) } let(:user) { create(:user, :project_bot) }
it_behaves_like 'not enforced' it_behaves_like 'not enforced'
end
end end
end end
end
context 'when git check is not enforced' do
before do
allow(saml_provider).to receive(:git_check_enforced?).and_return(false)
end
context 'with an active session', :clean_gitlab_redis_shared_state do context 'when git check is not enforced' do
let(:session_id) { '42' } before do
let(:stored_session) do allow(saml_provider).to receive(:git_check_enforced?).and_return(false)
{ 'active_group_sso_sign_ins' => { saml_provider.id => 5.minutes.ago } }
end end
before do context 'with an active session' do
Gitlab::Redis::SharedState.with do |redis| let(:session_id) { '42' }
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session)) let(:stored_session) do
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id]) { 'active_group_sso_sign_ins' => { saml_provider.id => 5.minutes.ago } }
end end
end
it_behaves_like 'not enforced' before do
end redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
context 'without any session' do it_behaves_like 'not enforced'
it_behaves_like 'not enforced' end
context 'without any session' do
it_behaves_like 'not enforced'
end
end end
end end
end end
it_behaves_like 'redis sessions store', 'group saml session enforcer'
end end
...@@ -2,62 +2,66 @@ ...@@ -2,62 +2,66 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do RSpec.describe Gitlab::Auth::Otp::SessionEnforcer do
let_it_be(:key) { create(:key)} shared_examples_for 'otp session enforcer' do
let_it_be(:key) { create(:key)}
describe '#update_session' do describe '#update_session' do
let(:redis) { double(:redis) } let(:redis) { double(:redis) }
before do
stub_licensed_features(git_two_factor_enforcement: true)
end
it 'registers a session in Redis' do
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
session_expiry_in_seconds = Gitlab::CurrentSettings.git_two_factor_session_expiry.minutes.to_i
expect(redis).to(
receive(:setex)
.with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}",
session_expiry_in_seconds,
true)
.once)
described_class.new(key).update_session
end
context 'when licensed feature is not available' do
before do before do
stub_licensed_features(git_two_factor_enforcement: false) stub_licensed_features(git_two_factor_enforcement: true)
end end
it 'does not register a session in Redis' do it 'registers a session in Redis' do
expect(redis).not_to receive(:setex) expect(redis_store_class).to receive(:with).and_yield(redis)
session_expiry_in_seconds = Gitlab::CurrentSettings.git_two_factor_session_expiry.minutes.to_i
expect(redis).to(
receive(:setex)
.with("#{::Gitlab::Redis::Sessions::OTP_SESSIONS_NAMESPACE}:#{key.id}",
session_expiry_in_seconds,
true)
.once)
described_class.new(key).update_session described_class.new(key).update_session
end end
end
end
describe '#access_restricted?' do context 'when licensed feature is not available' do
subject { described_class.new(key).access_restricted? } before do
stub_licensed_features(git_two_factor_enforcement: false)
end
it 'does not register a session in Redis' do
expect(redis).not_to receive(:setex)
before do described_class.new(key).update_session
stub_licensed_features(git_two_factor_enforcement: true) end
end
end end
context 'with existing session' do describe '#access_restricted?' do
subject { described_class.new(key).access_restricted? }
before do before do
Gitlab::Redis::SharedState.with do |redis| stub_licensed_features(git_two_factor_enforcement: true)
redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true )
end
end end
it { is_expected.to be_falsey } context 'with existing session' do
end before do
redis_store_class.with do |redis|
redis.set("#{::Gitlab::Redis::Sessions::OTP_SESSIONS_NAMESPACE}:#{key.id}", true )
end
end
it { is_expected.to be_falsey }
end
context 'without an existing session' do context 'without an existing session' do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end
end end
end end
it_behaves_like 'redis sessions store', 'otp session enforcer'
end end
...@@ -29,22 +29,26 @@ RSpec.describe Gitlab::Auth::Smartcard::SessionEnforcer do ...@@ -29,22 +29,26 @@ RSpec.describe Gitlab::Auth::Smartcard::SessionEnforcer do
stub_smartcard_setting(enabled: true, required_for_git_access: true) stub_smartcard_setting(enabled: true, required_for_git_access: true)
end end
context 'with a smartcard session', :clean_gitlab_redis_shared_state do RSpec.shared_examples_for 'smartcard session' do
let(:session_id) { '42' } context 'with a smartcard session' do
let(:stored_session) do let(:session_id) { '42' }
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } } let(:stored_session) do
end { 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session)) redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id]) redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end end
end
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
end
end end
it_behaves_like 'redis sessions store', 'smartcard session'
context 'without any session' do context 'without any session' do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
......
...@@ -3,45 +3,49 @@ ...@@ -3,45 +3,49 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Auth::Smartcard::Session do RSpec.describe Gitlab::Auth::Smartcard::Session do
describe '#active?' do RSpec.shared_examples_for 'smartcard session' do
let(:user) { create(:user) } describe '#active?' do
let(:user) { create(:user) }
subject { described_class.new.active?(user) } subject { described_class.new.active?(user) }
context 'with a smartcard session', :clean_gitlab_redis_shared_state do context 'with a smartcard session' do
let(:session_id) { '42' } let(:session_id) { '42' }
let(:stored_session) do let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } } { 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end end
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session)) redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id]) redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end end
it { is_expected.to be_truthy }
end end
it { is_expected.to be_truthy } context 'without any session' do
it { is_expected.to be_falsey }
end
end end
context 'without any session' do describe '#update_active' do
it { is_expected.to be_falsey } let(:now) { Time.now }
end
end
describe '#update_active' do around do |example|
let(:now) { Time.now } Gitlab::Session.with_session({}) do
example.run
around do |example| end
Gitlab::Session.with_session({}) do
example.run
end end
end
it 'stores the time of last sign-in' do it 'stores the time of last sign-in' do
subject.update_active(now) subject.update_active(now)
expect(Gitlab::Session.current[:smartcard_signins]).to eq({ 'last_signin_at' => now }) expect(Gitlab::Session.current[:smartcard_signins]).to eq({ 'last_signin_at' => now })
end
end end
end end
it_behaves_like 'redis sessions store', 'smartcard session'
end end
...@@ -704,232 +704,236 @@ RSpec.describe Gitlab::GitAccess do ...@@ -704,232 +704,236 @@ RSpec.describe Gitlab::GitAccess do
end end
end end
describe '#check_smartcard_access!' do RSpec.shared_examples_for 'checks smartcard access & otp session' do
before do describe '#check_smartcard_access!' do
stub_licensed_features(smartcard_auth: true) before do
stub_smartcard_setting(enabled: true, required_for_git_access: true) stub_licensed_features(smartcard_auth: true)
stub_smartcard_setting(enabled: true, required_for_git_access: true)
project.add_developer(user)
end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do project.add_developer(user)
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end end
before do context 'user with a smartcard session' do
Gitlab::Redis::SharedState.with do |redis| let(:session_id) { '42' }
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session)) let(:stored_session) do
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id]) { 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end end
end
it 'allows pull changes' do before do
expect { pull_changes }.not_to raise_error redis_store_class.with do |redis|
end redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
it 'allows push changes' do it 'allows pull changes' do
expect { push_changes }.not_to raise_error expect { pull_changes }.not_to raise_error
end end
end
context 'user without a smartcard session' do it 'allows push changes' do
it 'does not allow pull changes' do expect { push_changes }.not_to raise_error
expect { pull_changes }.to raise_error(Gitlab::GitAccess::ForbiddenError) end
end end
it 'does not allow push changes' do context 'user without a smartcard session' do
expect { push_changes }.to raise_error(Gitlab::GitAccess::ForbiddenError) it 'does not allow pull changes' do
end expect { pull_changes }.to raise_error(Gitlab::GitAccess::ForbiddenError)
end end
context 'with the setting off' do it 'does not allow push changes' do
before do expect { push_changes }.to raise_error(Gitlab::GitAccess::ForbiddenError)
stub_smartcard_setting(required_for_git_access: false) end
end end
it 'allows pull changes' do context 'with the setting off' do
expect { pull_changes }.not_to raise_error before do
end stub_smartcard_setting(required_for_git_access: false)
end
it 'allows push changes' do it 'allows pull changes' do
expect { push_changes }.not_to raise_error expect { pull_changes }.not_to raise_error
end
it 'allows push changes' do
expect { push_changes }.not_to raise_error
end
end end
end end
end
describe '#check_otp_session!' do describe '#check_otp_session!' do
let_it_be(:user) { create(:user, :two_factor_via_otp)} let_it_be(:user) { create(:user, :two_factor_via_otp)}
let_it_be(:key) { create(:key, user: user) } let_it_be(:key) { create(:key, user: user) }
let_it_be(:actor) { key } let_it_be(:actor) { key }
let(:protocol) { 'ssh' }
before do let(:protocol) { 'ssh' }
project.add_developer(user)
stub_feature_flags(two_factor_for_cli: true)
stub_licensed_features(git_two_factor_enforcement: true)
end
context 'with an OTP session', :clean_gitlab_redis_shared_state do
before do before do
Gitlab::Redis::SharedState.with do |redis| project.add_developer(user)
redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true) stub_feature_flags(two_factor_for_cli: true)
end stub_licensed_features(git_two_factor_enforcement: true)
end end
it 'allows push and pull access' do context 'with an OTP session' do
aggregate_failures do before do
expect { push_changes }.not_to raise_error redis_store_class.with do |redis|
expect { pull_changes }.not_to raise_error redis.set("#{Gitlab::Redis::Sessions::OTP_SESSIONS_NAMESPACE}:#{key.id}", true)
end
end end
end
context 'based on the duration set by the `git_two_factor_session_expiry` setting' do it 'allows push and pull access' do
let_it_be(:git_two_factor_session_expiry) { 20 } aggregate_failures do
let_it_be(:redis_key_expiry_at) { git_two_factor_session_expiry.minutes.from_now } expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error
before do end
stub_application_setting(git_two_factor_session_expiry: git_two_factor_session_expiry)
end end
def value_of_key context 'based on the duration set by the `git_two_factor_session_expiry` setting' do
key_expired = Time.current > redis_key_expiry_at let_it_be(:git_two_factor_session_expiry) { 20 }
return if key_expired let_it_be(:redis_key_expiry_at) { git_two_factor_session_expiry.minutes.from_now }
true before do
end stub_application_setting(git_two_factor_session_expiry: git_two_factor_session_expiry)
end
def stub_redis def value_of_key
redis = double(:redis) key_expired = Time.current > redis_key_expiry_at
expect(Gitlab::Redis::SharedState).to receive(:with).at_most(:twice).and_yield(redis) return if key_expired
expect(redis).to( true
receive(:get) end
.with("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}"))
.at_most(:twice)
.and_return(value_of_key)
end
context 'at a time before the stipulated expiry' do def stub_redis
it 'allows push and pull access' do redis = double(:redis)
travel_to(10.minutes.from_now) do expect(redis_store_class).to receive(:with).at_most(:twice).and_yield(redis)
stub_redis
expect(redis).to(
receive(:get)
.with("#{Gitlab::Redis::Sessions::OTP_SESSIONS_NAMESPACE}:#{key.id}"))
.at_most(:twice)
.and_return(value_of_key)
end
aggregate_failures do context 'at a time before the stipulated expiry' do
expect { push_changes }.not_to raise_error it 'allows push and pull access' do
expect { pull_changes }.not_to raise_error travel_to(10.minutes.from_now) do
stub_redis
aggregate_failures do
expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error
end
end end
end end
end end
end
context 'at a time after the stipulated expiry' do context 'at a time after the stipulated expiry' do
it 'does not allow push and pull access' do it 'does not allow push and pull access' do
travel_to(30.minutes.from_now) do travel_to(30.minutes.from_now) do
stub_redis stub_redis
aggregate_failures do aggregate_failures do
expect { push_changes }.to raise_error(::Gitlab::GitAccess::ForbiddenError) expect { push_changes }.to raise_error(::Gitlab::GitAccess::ForbiddenError)
expect { pull_changes }.to raise_error(::Gitlab::GitAccess::ForbiddenError) expect { pull_changes }.to raise_error(::Gitlab::GitAccess::ForbiddenError)
end
end end
end end
end end
end end
end end
end
context 'without OTP session' do context 'without OTP session' do
it 'does not allow push or pull access' do it 'does not allow push or pull access' do
user = 'jane.doe' user = 'jane.doe'
host = 'fridge.ssh' host = 'fridge.ssh'
port = 42 port = 42
stub_config( stub_config(
gitlab_shell: { gitlab_shell: {
ssh_user: user, ssh_user: user,
ssh_host: host, ssh_host: host,
ssh_port: port ssh_port: port
} }
) )
error_message = "OTP verification is required to access the repository.\n\n"\ error_message = "OTP verification is required to access the repository.\n\n"\
" Use: ssh #{user}@#{host} -p #{port} 2fa_verify" " Use: ssh #{user}@#{host} -p #{port} 2fa_verify"
aggregate_failures do aggregate_failures do
expect { push_changes }.to raise_forbidden(error_message) expect { push_changes }.to raise_forbidden(error_message)
expect { pull_changes }.to raise_forbidden(error_message) expect { pull_changes }.to raise_forbidden(error_message)
end
end end
end
context 'when protocol is HTTP' do context 'when protocol is HTTP' do
let(:protocol) { 'http' } let(:protocol) { 'http' }
it 'allows push and pull access' do it 'allows push and pull access' do
aggregate_failures do aggregate_failures do
expect { push_changes }.not_to raise_error expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error expect { pull_changes }.not_to raise_error
end
end end
end end
end
context 'when actor is not an SSH key' do context 'when actor is not an SSH key' do
let(:deploy_key) { create(:deploy_key, user: user) } let(:deploy_key) { create(:deploy_key, user: user) }
let(:actor) { deploy_key } let(:actor) { deploy_key }
before do before do
deploy_key.deploy_keys_projects.create(project: project, can_push: true) deploy_key.deploy_keys_projects.create(project: project, can_push: true)
end end
it 'allows push and pull access' do it 'allows push and pull access' do
aggregate_failures do aggregate_failures do
expect { push_changes }.not_to raise_error expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error expect { pull_changes }.not_to raise_error
end
end end
end end
end
context 'when 2FA is not enabled for the user' do context 'when 2FA is not enabled for the user' do
let(:user) { create(:user)} let(:user) { create(:user)}
let(:actor) { create(:key, user: user) } let(:actor) { create(:key, user: user) }
it 'allows push and pull access' do it 'allows push and pull access' do
aggregate_failures do aggregate_failures do
expect { push_changes }.not_to raise_error expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error expect { pull_changes }.not_to raise_error
end
end end
end end
end
context 'when feature flag is disabled' do context 'when feature flag is disabled' do
before do before do
stub_feature_flags(two_factor_for_cli: false) stub_feature_flags(two_factor_for_cli: false)
end end
it 'allows push and pull access' do it 'allows push and pull access' do
aggregate_failures do aggregate_failures do
expect { push_changes }.not_to raise_error expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error expect { pull_changes }.not_to raise_error
end
end end
end end
end
context 'when licensed feature is not available' do context 'when licensed feature is not available' do
before do before do
stub_licensed_features(git_two_factor_enforcement: false) stub_licensed_features(git_two_factor_enforcement: false)
end end
it 'allows push and pull access' do it 'allows push and pull access' do
aggregate_failures do aggregate_failures do
expect { push_changes }.not_to raise_error expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error expect { pull_changes }.not_to raise_error
end
end end
end end
end end
end end
end end
it_behaves_like 'redis sessions store', 'checks smartcard access & otp session'
describe '#check_sso_session!' do describe '#check_sso_session!' do
before do before do
project.add_developer(user) project.add_developer(user)
......
...@@ -154,26 +154,30 @@ RSpec.describe API::Internal::Base do ...@@ -154,26 +154,30 @@ RSpec.describe API::Internal::Base do
project.add_developer(user) project.add_developer(user)
end end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do RSpec.shared_examples_for 'smartcard session' do
let(:session_id) { '42' } context 'user with a smartcard session' do
let(:stored_session) do let(:session_id) { '42' }
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } } let(:stored_session) do
end { 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session)) redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id]) redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end end
end
it "allows access" do it "allows access" do
subject subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end
end end
end end
it_behaves_like 'redis sessions store', 'smartcard session'
context 'user without a smartcard session' do context 'user without a smartcard session' do
it "does not allow access" do it "does not allow access" do
subject subject
......
...@@ -54,26 +54,30 @@ RSpec.describe Repositories::GitHttpController, type: :request do ...@@ -54,26 +54,30 @@ RSpec.describe Repositories::GitHttpController, type: :request do
project.add_developer(user) project.add_developer(user)
end end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do RSpec.shared_examples_for 'smartcard session' do
let(:session_id) { '42' } context 'user with a smartcard session' do
let(:stored_session) do let(:session_id) { '42' }
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } } let(:stored_session) do
end { 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session)) redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id]) redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end end
end
it "allows access" do it "allows access" do
subject subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end
end end
end end
it_behaves_like 'redis sessions store', 'smartcard session'
context 'user without a smartcard session' do context 'user without a smartcard session' do
it "does not allow access" do it "does not allow access" do
subject subject
......
...@@ -2,31 +2,43 @@ ...@@ -2,31 +2,43 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do RSpec.describe ApplicationCable::Connection do
let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') } RSpec.shared_examples_for 'ApplicationCable::Connection' do
let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
context 'when session cookie is set' do
before do
redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
end
context 'when session cookie is set' do cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
end end
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id context 'when user is logged in' do
end let(:user) { create(:user) }
let(:session_hash) { { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } }
context 'when user is logged in' do it 'sets current_user' do
let(:user) { create(:user) } connect
let(:session_hash) { { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } }
it 'sets current_user' do expect(connection.current_user).to eq(user)
connect end
expect(connection.current_user).to eq(user) context 'with a stale password' do
let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] }
let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } }
it 'sets current_user to nil' do
connect
expect(connection.current_user).to be_nil
end
end
end end
context 'with a stale password' do context 'when user is not logged in' do
let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] } let(:session_hash) { {} }
let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } }
it 'sets current_user to nil' do it 'sets current_user to nil' do
connect connect
...@@ -36,32 +48,24 @@ RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do ...@@ -36,32 +48,24 @@ RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do
end end
end end
context 'when user is not logged in' do context 'when session cookie is not set' do
let(:session_hash) { {} }
it 'sets current_user to nil' do it 'sets current_user to nil' do
connect connect
expect(connection.current_user).to be_nil expect(connection.current_user).to be_nil
end end
end end
end
context 'when session cookie is not set' do context 'when session cookie is an empty string' do
it 'sets current_user to nil' do it 'sets current_user to nil' do
connect cookies[Gitlab::Application.config.session_options[:key]] = ''
expect(connection.current_user).to be_nil
end
end
context 'when session cookie is an empty string' do
it 'sets current_user to nil' do
cookies[Gitlab::Application.config.session_options[:key]] = ''
connect connect
expect(connection.current_user).to be_nil expect(connection.current_user).to be_nil
end
end end
end end
it_behaves_like 'redis sessions store', 'ApplicationCable::Connection'
end end
...@@ -2,70 +2,74 @@ ...@@ -2,70 +2,74 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Active user sessions', :clean_gitlab_redis_shared_state do RSpec.describe 'Active user sessions' do
it 'successful login adds a new active user login' do RSpec.shared_examples_for 'active user sessions' do
now = Time.zone.parse('2018-03-12 09:06') it 'successful login adds a new active user login' do
Timecop.freeze(now) do now = Time.zone.parse('2018-03-12 09:06')
user = create(:user) Timecop.freeze(now) do
gitlab_sign_in(user) user = create(:user)
expect(current_path).to eq root_path gitlab_sign_in(user)
expect(current_path).to eq root_path
sessions = ActiveSession.list(user)
expect(sessions.count).to eq 1
# refresh the current page updates the updated_at
Timecop.freeze(now + 1.minute) do
visit current_path
sessions = ActiveSession.list(user) sessions = ActiveSession.list(user)
expect(sessions.first).to have_attributes( expect(sessions.count).to eq 1
created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:07') # refresh the current page updates the updated_at
) Timecop.freeze(now + 1.minute) do
visit current_path
sessions = ActiveSession.list(user)
expect(sessions.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 end
end
it 'successful login cleans up obsolete entries' do it 'successful login cleans up obsolete entries' do
user = create(:user) user = create(:user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end end
gitlab_sign_in(user) gitlab_sign_in(user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d' expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d'
end
end end
end
it 'sessionless login does not clean up obsolete entries' do it 'sessionless login does not clean up obsolete entries' do
user = create(:user) user = create(:user)
personal_access_token = create(:personal_access_token, user: user) personal_access_token = create(:personal_access_token, user: user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end end
visit user_path(user, :atom, private_token: personal_access_token.token) visit user_path(user, :atom, private_token: personal_access_token.token)
expect(page.status_code).to eq 200 expect(page.status_code).to eq 200
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d' expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d'
end
end end
end
it 'logout deletes the active user login' do it 'logout deletes the active user login' do
user = create(:user) user = create(:user)
gitlab_sign_in(user) gitlab_sign_in(user)
expect(current_path).to eq root_path expect(current_path).to eq root_path
expect(ActiveSession.list(user).count).to eq 1 expect(ActiveSession.list(user).count).to eq 1
gitlab_sign_out gitlab_sign_out
expect(current_path).to eq new_user_session_path expect(current_path).to eq new_user_session_path
expect(ActiveSession.list(user)).to be_empty expect(ActiveSession.list(user)).to be_empty
end
end end
it_behaves_like 'redis sessions store', 'active user sessions'
end end
...@@ -2,38 +2,42 @@ ...@@ -2,38 +2,42 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state do RSpec.describe 'Session TTLs' do
include SessionHelpers include SessionHelpers
it 'creates a session with a short TTL when login fails' do RSpec.shared_examples_for 'session ttls' do
visit new_user_session_path it 'creates a session with a short TTL when login fails' do
# The session key only gets created after a post visit new_user_session_path
fill_in 'user_login', with: 'non-existant@gitlab.org' # The session key only gets created after a post
fill_in 'user_password', with: '12345678' fill_in 'user_login', with: 'non-existant@gitlab.org'
click_button 'Sign in' fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect(page).to have_content('Invalid login or password') expect(page).to have_content('Invalid login or password')
expect_single_session_with_short_ttl expect_single_session_with_short_ttl(redis_store_class)
end end
it 'increases the TTL when the login succeeds' do it 'increases the TTL when the login succeeds' do
user = create(:user) user = create(:user)
gitlab_sign_in(user) gitlab_sign_in(user)
expect(page).to have_content(user.name) expect(page).to have_content(user.name)
expect_single_session_with_authenticated_ttl expect_single_session_with_authenticated_ttl(redis_store_class)
end end
context 'with an unauthorized project' do context 'with an unauthorized project' do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
it 'creates a session with a short TTL' do it 'creates a session with a short TTL' do
visit project_raw_path(project, 'master/README.md') visit project_raw_path(project, 'master/README.md')
expect_single_session_with_short_ttl expect_single_session_with_short_ttl(redis_store_class)
expect(page).to have_current_path(new_user_session_path) expect(page).to have_current_path(new_user_session_path)
end
end end
end end
it_behaves_like 'redis sessions store', 'session ttls'
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Login', :clean_gitlab_redis_shared_state do RSpec.describe 'Login' do
include TermsHelper include TermsHelper
include UserLoginHelper include UserLoginHelper
include SessionHelpers include SessionHelpers
...@@ -11,506 +11,540 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do ...@@ -11,506 +11,540 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
stub_authentication_activity_metrics(debug: true) stub_authentication_activity_metrics(debug: true)
end end
describe 'password reset token after successful sign in' do RSpec.shared_examples_for 'login' do
it 'invalidates password reset token' do before do
expect(authentication_metrics) load Rails.root.join('config/initializers/session_store.rb')
.to increment(:user_authenticated_counter) end
describe 'password reset token after successful sign in' do
it 'invalidates password reset token' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
user = create(:user) user = create(:user)
expect(user.reset_password_token).to be_nil expect(user.reset_password_token).to be_nil
visit new_user_password_path visit new_user_password_path
fill_in 'user_email', with: user.email fill_in 'user_email', with: user.email
click_button 'Reset password' click_button 'Reset password'
user.reload user.reload
expect(user.reset_password_token).not_to be_nil expect(user.reset_password_token).not_to be_nil
gitlab_sign_in(user) gitlab_sign_in(user)
expect(current_path).to eq root_path expect(current_path).to eq root_path
user.reload user.reload
expect(user.reset_password_token).to be_nil expect(user.reset_password_token).to be_nil
end
end end
end
describe 'initial login after setup' do describe 'initial login after setup' do
it 'allows the initial admin to create a password' do it 'allows the initial admin to create a password' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
# This behavior is dependent on there only being one user # This behavior is dependent on there only being one user
User.delete_all User.delete_all
user = create(:admin, password_automatically_set: true) user = create(:admin, password_automatically_set: true)
visit root_path visit root_path
expect(current_path).to eq edit_user_password_path expect(current_path).to eq edit_user_password_path
expect(page).to have_content('Please create a password for your new account.') expect(page).to have_content('Please create a password for your new account.')
fill_in 'user_password', with: 'password' fill_in 'user_password', with: 'password'
fill_in 'user_password_confirmation', with: 'password' fill_in 'user_password_confirmation', with: 'password'
click_button 'Change your password' click_button 'Change your password'
expect(current_path).to eq new_user_session_path expect(current_path).to eq new_user_session_path
expect(page).to have_content(I18n.t('devise.passwords.updated_not_active')) expect(page).to have_content(I18n.t('devise.passwords.updated_not_active'))
fill_in 'user_login', with: user.username fill_in 'user_login', with: user.username
fill_in 'user_password', with: 'password' fill_in 'user_password', with: 'password'
click_button 'Sign in' click_button 'Sign in'
expect_single_session_with_authenticated_ttl expect_single_session_with_authenticated_ttl(redis_store_class)
expect(current_path).to eq root_path expect(current_path).to eq root_path
end end
it 'does not show flash messages when login page' do it 'does not show flash messages when login page' do
visit root_path visit root_path
expect(page).not_to have_content('You need to sign in or sign up before continuing.') expect(page).not_to have_content('You need to sign in or sign up before continuing.')
end
end end
end
describe 'with a blocked account' do
it 'prevents the user from logging in' do
expect(authentication_metrics)
.to increment(:user_blocked_counter)
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
user = create(:user, :blocked) describe 'with a blocked account' do
it 'prevents the user from logging in' do
gitlab_sign_in(user) expect(authentication_metrics)
.to increment(:user_blocked_counter)
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
expect(page).to have_content('Your account has been blocked.') user = create(:user, :blocked)
end
it 'does not update Devise trackable attributes', :clean_gitlab_redis_shared_state do gitlab_sign_in(user)
expect(authentication_metrics)
.to increment(:user_blocked_counter)
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
user = create(:user, :blocked) expect(page).to have_content('Your account has been blocked.')
end
expect { gitlab_sign_in(user) }.not_to change { user.reload.sign_in_count } it 'does not update Devise trackable attributes' do
end expect(authentication_metrics)
end .to increment(:user_blocked_counter)
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
describe 'with an unconfirmed email address' do user = create(:user, :blocked)
let!(:user) { create(:user, confirmed_at: nil) }
let(:grace_period) { 2.days }
let(:alert_title) { 'Please confirm your email address' }
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
before do expect { gitlab_sign_in(user) }.not_to change { user.reload.sign_in_count }
stub_application_setting(send_user_confirmation_email: true) end
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
end end
context 'within the grace period' do describe 'with an unconfirmed email address' do
it 'allows to login' do let!(:user) { create(:user, confirmed_at: nil) }
expect(authentication_metrics).to increment(:user_authenticated_counter) let(:grace_period) { 2.days }
let(:alert_title) { 'Please confirm your email address' }
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
gitlab_sign_in(user) before do
stub_application_setting(send_user_confirmation_email: true)
expect(page).not_to have_content(alert_title) allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
expect(page).not_to have_content(alert_message)
expect(page).not_to have_link('Resend confirmation email', href: new_user_confirmation_path)
end end
end
context 'when the confirmation grace period is expired' do context 'within the grace period' do
it 'prevents the user from logging in and renders a resend confirmation email link', :js do it 'allows to login' do
travel_to((grace_period + 1.day).from_now) do expect(authentication_metrics).to increment(:user_authenticated_counter)
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
gitlab_sign_in(user) gitlab_sign_in(user)
expect(page).to have_content(alert_title) expect(page).not_to have_content(alert_title)
expect(page).to have_content(alert_message) expect(page).not_to have_content(alert_message)
expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path) expect(page).not_to have_link('Resend confirmation email', href: new_user_confirmation_path)
end end
end end
end
context 'when resending the confirmation email' do
it 'redirects to the "almost there" page' do
stub_feature_flags(soft_email_confirmation: false)
user = create(:user) context 'when the confirmation grace period is expired' do
it 'prevents the user from logging in and renders a resend confirmation email link', :js do
travel_to((grace_period + 1.day).from_now) do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
visit new_user_confirmation_path gitlab_sign_in(user)
fill_in 'user_email', with: user.email
click_button 'Resend'
expect(current_path).to eq users_almost_there_path expect(page).to have_content(alert_title)
expect(page).to have_content(alert_message)
expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path)
end
end
end end
end
end
describe 'with the ghost user' do
it 'disallows login' do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter)
gitlab_sign_in(User.ghost) context 'when resending the confirmation email' do
it 'redirects to the "almost there" page' do
expect(page).to have_content('Invalid login or password.') stub_feature_flags(soft_email_confirmation: false)
end
it 'does not update Devise trackable attributes', :clean_gitlab_redis_shared_state do user = create(:user)
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter)
expect { gitlab_sign_in(User.ghost) } visit new_user_confirmation_path
.not_to change { User.ghost.reload.sign_in_count } fill_in 'user_email', with: user.email
end click_button 'Resend'
end
describe 'with OneTrust authentication' do expect(current_path).to eq users_almost_there_path
before do end
stub_config(extra: { one_trust_id: SecureRandom.uuid }) end
end
it 'has proper Content-Security-Policy headers' do
visit root_path
expect(response_headers['Content-Security-Policy']).to include('https://cdn.cookielaw.org https://*.onetrust.com')
end
end
describe 'with two-factor authentication', :js do
def enter_code(code)
fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end end
context 'with valid username/password' do describe 'with the ghost user' do
let(:user) { create(:user, :two_factor) } it 'disallows login' do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter)
before do gitlab_sign_in(User.ghost)
gitlab_sign_in(user, remember: true)
expect(page).to have_content('Two-Factor Authentication') expect(page).to have_content('Invalid login or password.')
end end
it 'does not show a "You are already signed in." error message' do it 'does not update Devise trackable attributes' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_unauthenticated_counter)
.and increment(:user_two_factor_authenticated_counter) .and increment(:user_password_invalid_counter)
enter_code(user.current_otp)
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated')) expect { gitlab_sign_in(User.ghost) }
expect_single_session_with_authenticated_ttl .not_to change { User.ghost.reload.sign_in_count }
end end
end
it 'does not allow sign-in if the user password is updated before entering a one-time code' do describe 'with OneTrust authentication' do
user.update!(password: 'new_password') before do
stub_config(extra: { one_trust_id: SecureRandom.uuid })
enter_code(user.current_otp)
expect(page).to have_content('An error occurred. Please sign in again.')
end end
context 'using one-time code' do it 'has proper Content-Security-Policy headers' do
it 'allows login with valid code' do visit root_path
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
enter_code(user.current_otp)
expect_single_session_with_authenticated_ttl
expect(current_path).to eq root_path
end
it 'persists remember_me value via hidden field' do expect(response_headers['Content-Security-Policy']).to include('https://cdn.cookielaw.org https://*.onetrust.com')
field = first('input#user_remember_me', visible: false) end
end
expect(field.value).to eq '1' describe 'with two-factor authentication', :js do
end def enter_code(code)
fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end
it 'blocks login with invalid code' do context 'with valid username/password' do
# TODO invalid 2FA code does not generate any events let(:user) { create(:user, :two_factor) }
# See gitlab-org/gitlab-ce#49785
enter_code('foo') before do
gitlab_sign_in(user, remember: true)
expect(page).to have_content('Invalid two-factor code') expect(page).to have_content('Two-Factor Authentication')
end end
it 'allows login with invalid code, then valid code' do it 'does not show a "You are already signed in." error message' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter) .and increment(:user_two_factor_authenticated_counter)
enter_code('foo')
expect(page).to have_content('Invalid two-factor code')
enter_code(user.current_otp) enter_code(user.current_otp)
expect_single_session_with_authenticated_ttl expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
expect(current_path).to eq root_path expect_single_session_with_authenticated_ttl(redis_store_class)
end end
it 'triggers ActiveSession.cleanup for the user' do it 'does not allow sign-in if the user password is updated before entering a one-time code' do
expect(authentication_metrics) user.update!(password: 'new_password')
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
enter_code(user.current_otp) enter_code(user.current_otp)
end
end
context 'using backup code' do
let(:codes) { user.generate_otp_backup_codes! }
before do expect(page).to have_content('An error occurred. Please sign in again.')
expect(codes.size).to eq 10
# Ensure the generated codes get saved
user.save!(touch: false)
end end
context 'with valid code' do context 'using one-time code' do
it 'allows login' do it 'allows login with valid code' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter) .and increment(:user_two_factor_authenticated_counter)
enter_code(codes.sample) enter_code(user.current_otp)
expect_single_session_with_authenticated_ttl(redis_store_class)
expect(current_path).to eq root_path expect(current_path).to eq root_path
end end
it 'invalidates the used code' do it 'persists remember_me value via hidden field' do
expect(authentication_metrics) field = first('input#user_remember_me', visible: false)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter) expect(field.value).to eq '1'
end
expect { enter_code(codes.sample) } it 'blocks login with invalid code' do
.to change { user.reload.otp_backup_codes.size }.by(-1) # TODO invalid 2FA code does not generate any events
# See gitlab-org/gitlab-ce#49785
enter_code('foo')
expect(page).to have_content('Invalid two-factor code')
end end
it 'invalidates backup codes twice in a row' do it 'allows login with invalid code, then valid code' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter).twice .to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter).twice .and increment(:user_two_factor_authenticated_counter)
.and increment(:user_session_destroyed_counter)
random_code = codes.delete(codes.sample) enter_code('foo')
expect { enter_code(random_code) } expect(page).to have_content('Invalid two-factor code')
.to change { user.reload.otp_backup_codes.size }.by(-1)
gitlab_sign_out enter_code(user.current_otp)
gitlab_sign_in(user)
expect { enter_code(codes.sample) } expect_single_session_with_authenticated_ttl(redis_store_class)
.to change { user.reload.otp_backup_codes.size }.by(-1) expect(current_path).to eq root_path
end end
it 'triggers ActiveSession.cleanup for the user' do it 'triggers ActiveSession.cleanup for the user' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter) .and increment(:user_two_factor_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
enter_code(codes.sample) enter_code(user.current_otp)
end end
end end
context 'with invalid code' do context 'using backup code' do
it 'blocks login' do let(:codes) { user.generate_otp_backup_codes! }
# TODO, invalid two factor authentication does not increment
# metrics / counters, see gitlab-org/gitlab-ce#49785
code = codes.sample before do
expect(user.invalidate_otp_backup_code!(code)).to eq true expect(codes.size).to eq 10
# Ensure the generated codes get saved
user.save!(touch: false) user.save!(touch: false)
expect(user.reload.otp_backup_codes.size).to eq 9 end
enter_code(code) context 'with valid code' do
expect(page).to have_content('Invalid two-factor code.') it 'allows login' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
enter_code(codes.sample)
expect(current_path).to eq root_path
end
it 'invalidates the used code' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
end
it 'invalidates backup codes twice in a row' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter).twice
.and increment(:user_two_factor_authenticated_counter).twice
.and increment(:user_session_destroyed_counter)
random_code = codes.delete(codes.sample)
expect { enter_code(random_code) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
gitlab_sign_out
gitlab_sign_in(user)
expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
end
it 'triggers ActiveSession.cleanup for the user' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
enter_code(codes.sample)
end
end end
end
end
end
context 'when logging in via OAuth' do context 'with invalid code' do
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')} it 'blocks login' do
let(:mock_saml_response) do # TODO, invalid two factor authentication does not increment
File.read('spec/fixtures/authentication/saml_response.xml') # metrics / counters, see gitlab-org/gitlab-ce#49785
end
before do code = codes.sample
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], expect(user.invalidate_otp_backup_code!(code)).to eq true
providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
user.save!(touch: false)
expect(user.reload.otp_backup_codes.size).to eq 9
enter_code(code)
expect(page).to have_content('Invalid two-factor code.')
end
end
end
end end
context 'when authn_context is worth two factors' do context 'when logging in via OAuth' do
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
let(:mock_saml_response) do let(:mock_saml_response) do
File.read('spec/fixtures/authentication/saml_response.xml') File.read('spec/fixtures/authentication/saml_response.xml')
.gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
end end
it 'signs user in without prompting for second factor' do before do
# TODO, OAuth authentication does not fire events, stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
# see gitlab-org/gitlab-ce#49786 providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
end
expect(authentication_metrics) context 'when authn_context is worth two factors' do
.to increment(:user_authenticated_counter) let(:mock_saml_response) do
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original File.read('spec/fixtures/authentication/saml_response.xml')
.gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
end
sign_in_using_saml! it 'signs user in without prompting for second factor' do
# TODO, OAuth authentication does not fire events,
# see gitlab-org/gitlab-ce#49786
expect_single_session_with_authenticated_ttl expect(authentication_metrics)
expect(page).not_to have_content('Two-Factor Authentication') .to increment(:user_authenticated_counter)
expect(current_path).to eq root_path expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
sign_in_using_saml!
expect_single_session_with_authenticated_ttl(redis_store_class)
expect(page).not_to have_content('Two-Factor Authentication')
expect(current_path).to eq root_path
end
end end
end
context 'when two factor authentication is required' do context 'when two factor authentication is required' do
it 'shows 2FA prompt after OAuth login' do it 'shows 2FA prompt after OAuth login' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter) .and increment(:user_two_factor_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
sign_in_using_saml! sign_in_using_saml!
expect(page).to have_content('Two-Factor Authentication') expect(page).to have_content('Two-Factor Authentication')
enter_code(user.current_otp) enter_code(user.current_otp)
expect_single_session_with_authenticated_ttl expect_single_session_with_authenticated_ttl(redis_store_class)
expect(current_path).to eq root_path expect(current_path).to eq root_path
end
end end
end
def sign_in_using_saml! def sign_in_using_saml!
gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response) gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response)
end
end end
end end
end
describe 'without two-factor authentication' do
context 'with correct username and password' do
let(:user) { create(:user) }
it 'allows basic login' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect_single_session_with_authenticated_ttl describe 'without two-factor authentication' do
expect(current_path).to eq root_path context 'with correct username and password' do
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated')) let(:user) { create(:user) }
end
it 'does not show already signed in message when opening sign in page after login' do it 'allows basic login' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
gitlab_sign_in(user) gitlab_sign_in(user)
visit new_user_session_path
expect_single_session_with_authenticated_ttl expect_single_session_with_authenticated_ttl(redis_store_class)
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated')) expect(current_path).to eq root_path
end expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
it 'triggers ActiveSession.cleanup for the user' do it 'does not show already signed in message when opening sign in page after login' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
gitlab_sign_in(user) gitlab_sign_in(user)
end visit new_user_session_path
context 'when the users password is expired' do expect_single_session_with_authenticated_ttl(redis_store_class)
before do expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
end end
it 'asks for a new password' do it 'triggers ActiveSession.cleanup for the user' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
visit new_user_session_path gitlab_sign_in(user)
end
fill_in 'user_login', with: user.email context 'when the users password is expired' do
fill_in 'user_password', with: '12345678' before do
click_button 'Sign in' user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
end
expect(current_path).to eq(new_profile_password_path) it 'asks for a new password' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect(current_path).to eq(new_profile_password_path)
end
end end
end end
end
context 'with invalid username and password' do context 'with invalid username and password' do
let(:user) { create(:user, password: 'not-the-default') } let(:user) { create(:user, password: 'not-the-default') }
it 'blocks invalid login' do it 'blocks invalid login' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_unauthenticated_counter) .to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter) .and increment(:user_password_invalid_counter)
gitlab_sign_in(user) gitlab_sign_in(user)
expect_single_session_with_short_ttl expect_single_session_with_short_ttl(redis_store_class)
expect(page).to have_content('Invalid login or password.') expect(page).to have_content('Invalid login or password.')
end
end end
end end
end
describe 'with required two-factor authentication enabled' do describe 'with required two-factor authentication enabled' do
let(:user) { create(:user) } let(:user) { create(:user) }
# TODO: otp_grace_period_started_at # TODO: otp_grace_period_started_at
context 'global setting' do context 'global setting' do
before do
stub_application_setting(require_two_factor_authentication: true)
end
context 'with grace period defined' do
before do before do
stub_application_setting(two_factor_grace_period: 48) stub_application_setting(require_two_factor_authentication: true)
end end
context 'within the grace period' do context 'with grace period defined' do
it 'redirects to two-factor configuration page' do before do
expect(authentication_metrics) stub_application_setting(two_factor_grace_period: 48)
.to increment(:user_authenticated_counter) end
gitlab_sign_in(user) context 'within the grace period' do
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
expect(current_path).to eq profile_two_factor_auth_path gitlab_sign_in(user)
expect(page).to have_content('The global settings require you to enable Two-Factor Authentication for your account. You need to do this before ')
expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content('The global settings require you to enable Two-Factor Authentication for your account. You need to do this before ')
end
it 'allows skipping two-factor configuration', :js do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
end
end end
it 'allows skipping two-factor configuration', :js do context 'after the grace period' do
expect(authentication_metrics) let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
.to increment(:user_authenticated_counter)
gitlab_sign_in(user) it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
expect(current_path).to eq profile_two_factor_auth_path gitlab_sign_in(user)
click_link 'Configure it later'
expect(current_path).to eq root_path expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content(
'The global settings require you to enable Two-Factor Authentication for your account.'
)
end
it 'disallows skipping two-factor configuration', :js do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end end
end end
context 'after the grace period' do context 'without grace period defined' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) } before do
stub_application_setting(two_factor_grace_period: 0)
end
it 'redirects to two-factor configuration page' do it 'redirects to two-factor configuration page' do
expect(authentication_metrics) expect(authentication_metrics)
...@@ -523,54 +557,58 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do ...@@ -523,54 +557,58 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
'The global settings require you to enable Two-Factor Authentication for your account.' 'The global settings require you to enable Two-Factor Authentication for your account.'
) )
end end
it 'disallows skipping two-factor configuration', :js do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end end
end end
context 'without grace period defined' do context 'group setting' do
before do before do
stub_application_setting(two_factor_grace_period: 0) group1 = create :group, name: 'Group 1', require_two_factor_authentication: true
group1.add_user(user, GroupMember::DEVELOPER)
group2 = create :group, name: 'Group 2', require_two_factor_authentication: true
group2.add_user(user, GroupMember::DEVELOPER)
end end
it 'redirects to two-factor configuration page' do context 'with grace period defined' do
expect(authentication_metrics) before do
.to increment(:user_authenticated_counter) stub_application_setting(two_factor_grace_period: 48)
end
gitlab_sign_in(user) context 'within the grace period' do
it 'redirects to two-factor configuration page' do
freeze_time do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content(
'The group settings for Group 1 and Group 2 require you to enable '\
'Two-Factor Authentication for your account. '\
'You can leave Group 1 and leave Group 2. '\
'You need to do this '\
'before '\
"#{(Time.zone.now + 2.days).strftime("%a, %d %b %Y %H:%M:%S %z")}"
)
end
end
expect(current_path).to eq profile_two_factor_auth_path it 'allows skipping two-factor configuration', :js do
expect(page).to have_content( expect(authentication_metrics)
'The global settings require you to enable Two-Factor Authentication for your account.' .to increment(:user_authenticated_counter)
)
end
end
end
context 'group setting' do gitlab_sign_in(user)
before do
group1 = create :group, name: 'Group 1', require_two_factor_authentication: true
group1.add_user(user, GroupMember::DEVELOPER)
group2 = create :group, name: 'Group 2', require_two_factor_authentication: true
group2.add_user(user, GroupMember::DEVELOPER)
end
context 'with grace period defined' do expect(current_path).to eq profile_two_factor_auth_path
before do click_link 'Configure it later'
stub_application_setting(two_factor_grace_period: 48) expect(current_path).to eq root_path
end end
end
context 'within the grace period' do context 'after the grace period' do
it 'redirects to two-factor configuration page' do let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
freeze_time do
it 'redirects to two-factor configuration page' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
...@@ -578,30 +616,27 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do ...@@ -578,30 +616,27 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
expect(current_path).to eq profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content( expect(page).to have_content(
'The group settings for Group 1 and Group 2 require you to enable '\ 'The group settings for Group 1 and Group 2 require you to enable ' \
'Two-Factor Authentication for your account. '\ 'Two-Factor Authentication for your account.'
'You can leave Group 1 and leave Group 2. '\
'You need to do this '\
'before '\
"#{(Time.zone.now + 2.days).strftime("%a, %d %b %Y %H:%M:%S %z")}"
) )
end end
end
it 'allows skipping two-factor configuration', :js do it 'disallows skipping two-factor configuration', :js do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
gitlab_sign_in(user) gitlab_sign_in(user)
expect(current_path).to eq profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later' expect(page).not_to have_link('Configure it later')
expect(current_path).to eq root_path end
end end
end end
context 'after the grace period' do context 'without grace period defined' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) } before do
stub_application_setting(two_factor_grace_period: 0)
end
it 'redirects to two-factor configuration page' do it 'redirects to two-factor configuration page' do
expect(authentication_metrics) expect(authentication_metrics)
...@@ -612,234 +647,230 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do ...@@ -612,234 +647,230 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
expect(current_path).to eq profile_two_factor_auth_path expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content( expect(page).to have_content(
'The group settings for Group 1 and Group 2 require you to enable ' \ 'The group settings for Group 1 and Group 2 require you to enable ' \
'Two-Factor Authentication for your account.' 'Two-Factor Authentication for your account. '\
'You can leave Group 1 and leave Group 2.'
) )
end end
end
end
end
it 'disallows skipping two-factor configuration', :js do describe 'UI tabs and panes' do
expect(authentication_metrics) context 'when no defaults are changed' do
.to increment(:user_authenticated_counter) it 'does not render any tabs' do
visit new_user_session_path
gitlab_sign_in(user) ensure_no_tabs
end
expect(current_path).to eq profile_two_factor_auth_path it 'renders link to sign up path' do
expect(page).not_to have_link('Configure it later') visit new_user_session_path
end
expect(page.body).to have_link('Register now', href: new_user_registration_path)
end end
end end
context 'without grace period defined' do context 'when signup is disabled' do
before do before do
stub_application_setting(two_factor_grace_period: 0) stub_application_setting(signup_enabled: false)
visit new_user_session_path
end end
it 'redirects to two-factor configuration page' do it 'does not render any tabs' do
expect(authentication_metrics) ensure_no_tabs
.to increment(:user_authenticated_counter) end
gitlab_sign_in(user) it 'does not render link to sign up path' do
visit new_user_session_path
expect(current_path).to eq profile_two_factor_auth_path expect(page.body).not_to have_link('Register now', href: new_user_registration_path)
expect(page).to have_content(
'The group settings for Group 1 and Group 2 require you to enable ' \
'Two-Factor Authentication for your account. '\
'You can leave Group 1 and leave Group 2.'
)
end end
end end
end
end
describe 'UI tabs and panes' do context 'when ldap is enabled' do
context 'when no defaults are changed' do include LdapHelpers
it 'does not render any tabs' do
visit new_user_session_path
ensure_no_tabs let(:provider) { 'ldapmain' }
end let(:ldap_server_config) do
{
it 'renders link to sign up path' do 'label' => 'Main LDAP',
visit new_user_session_path 'provider_name' => provider,
'attributes' => {},
'encryption' => 'plain',
'uid' => 'uid',
'base' => 'dc=example,dc=com'
}
end
expect(page.body).to have_link('Register now', href: new_user_registration_path) before do
end stub_ldap_setting(enabled: true)
end allow(::Gitlab::Auth::Ldap::Config).to receive_messages(enabled: true, servers: [ldap_server_config])
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [provider.to_sym])
context 'when signup is disabled' do Ldap::OmniauthCallbacksController.define_providers!
before do Rails.application.reload_routes!
stub_application_setting(signup_enabled: false)
visit new_user_session_path allow_next_instance_of(ActionDispatch::Routing::RoutesProxy) do |instance|
end allow(instance).to receive(:"user_#{provider}_omniauth_callback_path")
.and_return("/users/auth/#{provider}/callback")
end
it 'does not render any tabs' do visit new_user_session_path
ensure_no_tabs end
end
it 'does not render link to sign up path' do it 'correctly renders tabs and panes' do
visit new_user_session_path ensure_tab_pane_correctness(['Main LDAP', 'Standard'])
end
expect(page.body).not_to have_link('Register now', href: new_user_registration_path) it 'renders link to sign up path' do
expect(page.body).to have_link('Register now', href: new_user_registration_path)
end
end end
end
context 'when ldap is enabled' do context 'when crowd is enabled' do
include LdapHelpers before do
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:crowd])
let(:provider) { 'ldapmain' } stub_application_setting(crowd_enabled: true)
let(:ldap_server_config) do
{
'label' => 'Main LDAP',
'provider_name' => provider,
'attributes' => {},
'encryption' => 'plain',
'uid' => 'uid',
'base' => 'dc=example,dc=com'
}
end
before do Ldap::OmniauthCallbacksController.define_providers!
stub_ldap_setting(enabled: true) Rails.application.reload_routes!
allow(::Gitlab::Auth::Ldap::Config).to receive_messages(enabled: true, servers: [ldap_server_config])
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [provider.to_sym])
Ldap::OmniauthCallbacksController.define_providers! allow_next_instance_of(ActionDispatch::Routing::RoutesProxy) do |instance|
Rails.application.reload_routes! allow(instance).to receive(:user_crowd_omniauth_authorize_path)
.and_return("/users/auth/crowd/callback")
end
allow_next_instance_of(ActionDispatch::Routing::RoutesProxy) do |instance| visit new_user_session_path
allow(instance).to receive(:"user_#{provider}_omniauth_callback_path")
.and_return("/users/auth/#{provider}/callback")
end end
visit new_user_session_path it 'correctly renders tabs and panes' do
end ensure_tab_pane_correctness(%w(Crowd Standard))
end
it 'correctly renders tabs and panes' do
ensure_tab_pane_correctness(['Main LDAP', 'Standard'])
end end
end
it 'renders link to sign up path' do describe 'Client helper classes and flags' do
expect(page.body).to have_link('Register now', href: new_user_registration_path) it 'adds client browser and platform classes to page body' do
visit root_path
expect(find('body')[:class]).to include('gl-browser-generic')
expect(find('body')[:class]).to include('gl-platform-other')
end end
end end
context 'when crowd is enabled' do context 'when terms are enforced', :js do
before do let(:user) { create(:user) }
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:crowd])
stub_application_setting(crowd_enabled: true)
Ldap::OmniauthCallbacksController.define_providers! before do
Rails.application.reload_routes! enforce_terms
end
allow_next_instance_of(ActionDispatch::Routing::RoutesProxy) do |instance| it 'asks to accept the terms on first login' do
allow(instance).to receive(:user_crowd_omniauth_authorize_path) expect(authentication_metrics)
.and_return("/users/auth/crowd/callback") .to increment(:user_authenticated_counter)
end
visit new_user_session_path visit new_user_session_path
end
it 'correctly renders tabs and panes' do fill_in 'user_login', with: user.email
ensure_tab_pane_correctness(%w(Crowd Standard)) fill_in 'user_password', with: '12345678'
end
end
end
describe 'Client helper classes and flags' do click_button 'Sign in'
it 'adds client browser and platform classes to page body' do
visit root_path
expect(find('body')[:class]).to include('gl-browser-generic')
expect(find('body')[:class]).to include('gl-platform-other')
end
end
context 'when terms are enforced', :js do expect_to_be_on_terms_page
let(:user) { create(:user) }
before do click_button 'Accept terms'
enforce_terms
end
it 'asks to accept the terms on first login' do expect(current_path).to eq(root_path)
expect(authentication_metrics) expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
.to increment(:user_authenticated_counter) end
visit new_user_session_path it 'does not ask for terms when the user already accepted them' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
fill_in 'user_login', with: user.email accept_terms(user)
fill_in 'user_password', with: '12345678'
click_button 'Sign in' visit new_user_session_path
expect_to_be_on_terms_page fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Accept terms' click_button 'Sign in'
expect(current_path).to eq(root_path) expect(current_path).to eq(root_path)
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated')) end
end
it 'does not ask for terms when the user already accepted them' do context 'when 2FA is required for the user' do
expect(authentication_metrics) before do
.to increment(:user_authenticated_counter) group = create(:group, require_two_factor_authentication: true)
group.add_developer(user)
end
accept_terms(user) context 'when the user did not enable 2FA' do
it 'asks to set 2FA before asking to accept the terms' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path visit new_user_session_path
fill_in 'user_login', with: user.email fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678' fill_in 'user_password', with: '12345678'
click_button 'Sign in' click_button 'Sign in'
expect(current_path).to eq(root_path) expect_to_be_on_terms_page
end click_button 'Accept terms'
context 'when 2FA is required for the user' do expect(current_path).to eq(profile_two_factor_auth_path)
before do
group = create(:group, require_two_factor_authentication: true)
group.add_developer(user)
end
context 'when the user did not enable 2FA' do fill_in 'pin_code', with: user.reload.current_otp
it 'asks to set 2FA before asking to accept the terms' do fill_in 'current_password', with: user.password
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path click_button 'Register with two-factor app'
click_button 'Copy codes'
click_link 'Proceed'
fill_in 'user_login', with: user.email expect(current_path).to eq(profile_account_path)
fill_in 'user_password', with: '12345678' expect(page).to have_content('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can use that key to generate additional recovery codes.')
end
end
click_button 'Sign in' context 'when the user already enabled 2FA' do
before do
user.update!(otp_required_for_login: true,
otp_secret: User.generate_otp_secret(32))
end
expect_to_be_on_terms_page it 'asks the user to accept the terms' do
click_button 'Accept terms' expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
expect(current_path).to eq(profile_two_factor_auth_path) visit new_user_session_path
fill_in 'pin_code', with: user.reload.current_otp fill_in 'user_login', with: user.email
fill_in 'current_password', with: user.password fill_in 'user_password', with: '12345678'
click_button 'Sign in'
click_button 'Register with two-factor app' fill_in 'user_otp_attempt', with: user.reload.current_otp
click_button 'Copy codes' click_button 'Verify code'
click_link 'Proceed'
expect(current_path).to eq(profile_account_path) expect_to_be_on_terms_page
expect(page).to have_content('You have set up 2FA for your account! If you lose access to your 2FA device, you can use your recovery codes to access your account. Alternatively, if you upload an SSH key, you can use that key to generate additional recovery codes.') click_button 'Accept terms'
expect(current_path).to eq(root_path)
end
end end
end end
context 'when the user already enabled 2FA' do context 'when the users password is expired' do
before do before do
user.update!(otp_required_for_login: true, user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
otp_secret: User.generate_otp_secret(32))
end end
it 'asks the user to accept the terms' do it 'asks the user to accept the terms before setting a new password' do
expect(authentication_metrics) expect(authentication_metrics)
.to increment(:user_authenticated_counter) .to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
visit new_user_session_path visit new_user_session_path
...@@ -847,109 +878,86 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do ...@@ -847,109 +878,86 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
fill_in 'user_password', with: '12345678' fill_in 'user_password', with: '12345678'
click_button 'Sign in' click_button 'Sign in'
fill_in 'user_otp_attempt', with: user.reload.current_otp
click_button 'Verify code'
expect_to_be_on_terms_page expect_to_be_on_terms_page
click_button 'Accept terms' click_button 'Accept terms'
expect(current_path).to eq(root_path) expect(current_path).to eq(new_profile_password_path)
fill_in 'user_password', with: '12345678'
fill_in 'user_new_password', with: 'new password'
fill_in 'user_password_confirmation', with: 'new password'
click_button 'Set new password'
expect(page).to have_content('Password successfully changed')
end end
end end
end
context 'when the users password is expired' do context 'when the user does not have an email configured' do
before do let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') }
user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
end
it 'asks the user to accept the terms before setting a new password' do before do
expect(authentication_metrics) stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
.to increment(:user_authenticated_counter) end
visit new_user_session_path it 'asks the user to accept the terms before setting an email' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
fill_in 'user_login', with: user.email gitlab_sign_in_via('saml', user, 'my-uid')
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect_to_be_on_terms_page expect_to_be_on_terms_page
click_button 'Accept terms' click_button 'Accept terms'
expect(current_path).to eq(new_profile_password_path) expect(current_path).to eq(profile_path)
fill_in 'user_password', with: '12345678' fill_in 'Email', with: 'hello@world.com'
fill_in 'user_new_password', with: 'new password'
fill_in 'user_password_confirmation', with: 'new password'
click_button 'Set new password'
expect(page).to have_content('Password successfully changed') click_button 'Update profile settings'
expect(page).to have_content('Profile was successfully updated')
end
end end
end end
context 'when the user does not have an email configured' do context 'when sending confirmation email and not yet confirmed' do
let(:user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'saml', email: 'temp-email-for-oauth-user@gitlab.localhost') } let!(:user) { create(:user, confirmed_at: nil) }
let(:grace_period) { 2.days }
let(:alert_title) { 'Please confirm your email address' }
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" }
before do before do
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config]) stub_application_setting(send_user_confirmation_email: true)
stub_feature_flags(soft_email_confirmation: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
end end
it 'asks the user to accept the terms before setting an email' do it 'allows login and shows a flash warning to confirm the email address' do
expect(authentication_metrics) expect(authentication_metrics).to increment(:user_authenticated_counter)
.to increment(:user_authenticated_counter)
gitlab_sign_in_via('saml', user, 'my-uid')
expect_to_be_on_terms_page
click_button 'Accept terms'
expect(current_path).to eq(profile_path)
fill_in 'Email', with: 'hello@world.com'
click_button 'Update profile settings' gitlab_sign_in(user)
expect(page).to have_content('Profile was successfully updated') expect(current_path).to eq root_path
expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address and unlock the power of CI/CD.")
end end
end
end
context 'when sending confirmation email and not yet confirmed' do context "when not having confirmed within Devise's allow_unconfirmed_access_for time" do
let!(:user) { create(:user, confirmed_at: nil) } it 'does not allow login and shows a flash alert to confirm the email address', :js do
let(:grace_period) { 2.days } travel_to((grace_period + 1.day).from_now) do
let(:alert_title) { 'Please confirm your email address' } expect(authentication_metrics)
let(:alert_message) { "To continue, you need to select the link in the confirmation email we sent to verify your email address. If you didn't get our email, select Resend confirmation email" } .to increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
before do
stub_application_setting(send_user_confirmation_email: true)
stub_feature_flags(soft_email_confirmation: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
end
it 'allows login and shows a flash warning to confirm the email address' do
expect(authentication_metrics).to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect(current_path).to eq root_path
expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address and unlock the power of CI/CD.")
end
context "when not having confirmed within Devise's allow_unconfirmed_access_for time" do
it 'does not allow login and shows a flash alert to confirm the email address', :js do
travel_to((grace_period + 1.day).from_now) do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
gitlab_sign_in(user) gitlab_sign_in(user)
expect(current_path).to eq new_user_session_path expect(current_path).to eq new_user_session_path
expect(page).to have_content(alert_title) expect(page).to have_content(alert_title)
expect(page).to have_content(alert_message) expect(page).to have_content(alert_message)
expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path) expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path)
end
end end
end end
end end
end end
it_behaves_like 'redis sessions store', 'login'
end end
...@@ -10,25 +10,37 @@ RSpec.describe 'Session initializer for GitLab' do ...@@ -10,25 +10,37 @@ RSpec.describe 'Session initializer for GitLab' do
end end
describe 'config#session_store' do describe 'config#session_store' do
context 'when the GITLAB_REDIS_STORE_WITH_SESSION_STORE env is not set' do context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is not set' do
before do before do
stub_env('GITLAB_REDIS_STORE_WITH_SESSION_STORE', nil) stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', nil)
end end
it 'initialized as a redis_store with a proper Redis::Store instance' do it 'initialized with Multistore as ENV var defaults to true' do
expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store))) expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
load_session_store load_session_store
end end
end end
context 'when the GITLAB_REDIS_STORE_WITH_SESSION_STORE env is disabled' do context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is disabled' do
before do before do
stub_env('GITLAB_REDIS_STORE_WITH_SESSION_STORE', false) stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', false)
end end
it 'initialized as a redis_store with a proper servers configuration' do it 'initialized as a redis_store with a proper servers configuration' do
expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(servers: kind_of(Hash))) expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(Redis::Store)))
load_session_store
end
end
context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is enabled' do
before do
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', true)
end
it 'initialized as a redis_store with a proper servers configuration' do
expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
load_session_store load_session_store
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do RSpec.describe Gitlab::AnonymousSession do
let(:default_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' } let(:default_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
let(:additional_session_id) { '7919a6f1bb119dd7396fadc38fd18d0d' } let(:additional_session_id) { '7919a6f1bb119dd7396fadc38fd18d0d' }
...@@ -12,56 +12,60 @@ RSpec.describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do ...@@ -12,56 +12,60 @@ RSpec.describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do
described_class.new('127.0.0.1') described_class.new('127.0.0.1')
end end
describe '#store_session_ip' do RSpec.shared_examples_for 'anonymous sessions' do
it 'adds session id to proper key' do describe '#store_session_ip' do
subject.count_session_ip it 'adds session id to proper key' do
subject.count_session_ip
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq 1 expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq 1
end
end end
end
it 'adds expiration time to key' do it 'adds expiration time to key' do
freeze_time do freeze_time do
subject.count_session_ip subject.count_session_ip
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.ttl("session:lookup:ip:gitlab2:127.0.0.1")).to eq(24.hours.to_i) expect(redis.ttl("session:lookup:ip:gitlab2:127.0.0.1")).to eq(24.hours.to_i)
end
end end
end end
end
context 'when there is already one session' do context 'when there is already one session' do
it 'increments the session count' do it 'increments the session count' do
subject.count_session_ip subject.count_session_ip
new_anonymous_session.count_session_ip new_anonymous_session.count_session_ip
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq(2) expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq(2)
end
end end
end end
end end
end
describe '#stored_sessions' do describe '#stored_sessions' do
it 'returns all anonymous sessions per ip' do it 'returns all anonymous sessions per ip' do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:lookup:ip:gitlab2:127.0.0.1", 2) redis.set("session:lookup:ip:gitlab2:127.0.0.1", 2)
end end
expect(subject.session_count).to eq(2) expect(subject.session_count).to eq(2)
end
end end
end
it 'removes obsolete lookup through ip entries' do it 'removes obsolete lookup through ip entries' do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:lookup:ip:gitlab2:127.0.0.1", 2) redis.set("session:lookup:ip:gitlab2:127.0.0.1", 2)
end end
subject.cleanup_session_per_ip_count subject.cleanup_session_per_ip_count
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.exists("session:lookup:ip:gitlab2:127.0.0.1")).to eq(false) expect(redis.exists("session:lookup:ip:gitlab2:127.0.0.1")).to eq(false)
end
end end
end end
it_behaves_like 'redis sessions store', 'anonymous sessions'
end end
...@@ -4,4 +4,54 @@ require 'spec_helper' ...@@ -4,4 +4,54 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::Sessions do RSpec.describe Gitlab::Redis::Sessions do
include_examples "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState include_examples "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
describe 'redis instance used in connection pool' do
before do
clear_pool
end
context 'when redis.sessions configuration is not provided' do
it 'uses ::Redis instance' do
expect(described_class).to receive(:config_fallback?).and_return(true)
described_class.pool.with do |redis_instance|
expect(redis_instance).to be_instance_of(::Redis)
end
end
end
context 'when redis.sessions configuration is provided' do
it 'instantiates an instance of MultiStore' do
expect(described_class).to receive(:config_fallback?).and_return(false)
described_class.pool.with do |redis_instance|
expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
end
end
end
def clear_pool
described_class.remove_instance_variable(:@pool)
rescue NameError
# raised if @pool was not set; ignore
end
end
describe '#store' do
subject { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
context 'when redis.sessions configuration is provided' do
it 'instantiates ::Redis instance' do
expect(described_class).to receive(:config_fallback?).and_return(true)
expect(subject).to be_instance_of(::Redis::Store)
end
end
context 'when redis.sessions configuration is not provided' do
it 'instantiates an instance of MultiStore' do
expect(described_class).to receive(:config_fallback?).and_return(false)
expect(subject).to be_instance_of(::Gitlab::Redis::MultiStore)
end
end
end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do RSpec.describe ActiveSession do
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
...@@ -21,457 +21,461 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do ...@@ -21,457 +21,461 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
}) })
end end
describe '#current?' do RSpec.shared_examples_for 'active session' do
it 'returns true if the active session matches the current session' do describe '#current?' do
active_session = ActiveSession.new(session_private_id: rack_session.private_id) it 'returns true if the active session matches the current session' do
active_session = ActiveSession.new(session_private_id: rack_session.private_id)
expect(active_session.current?(session)).to be true expect(active_session.current?(session)).to be true
end end
it 'returns false if the active session does not match the current session' do it 'returns false if the active session does not match the current session' do
active_session = ActiveSession.new(session_id: Rack::Session::SessionId.new('59822c7d9fcdfa03725eff41782ad97d')) active_session = ActiveSession.new(session_id: Rack::Session::SessionId.new('59822c7d9fcdfa03725eff41782ad97d'))
expect(active_session.current?(session)).to be false expect(active_session.current?(session)).to be false
end end
it 'returns false if the session id is nil' do it 'returns false if the session id is nil' do
active_session = ActiveSession.new(session_id: nil) active_session = ActiveSession.new(session_id: nil)
session = double(:session, id: nil) session = double(:session, id: nil)
expect(active_session.current?(session)).to be false expect(active_session.current?(session)).to be false
end
end end
end
describe '.list' do describe '.list' do
it 'returns all sessions by user' do it 'returns all sessions by user' do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' })) 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:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' }))
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '') 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' }] redis.sadd(
end "session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
it 'does not return obsolete entries and cleans them up' do expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
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 end
expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }] it 'does not return obsolete entries and cleans them up' do
redis_store_class.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
Gitlab::Redis::SharedState.with do |redis| redis.sadd(
expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d'] "session:lookup:user:gitlab:#{user.id}",
end %w[
end 6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
it 'returns an empty array if the use does not have any active session' do expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
expect(ActiveSession.list(user)).to eq []
end
end
describe '.list_sessions' do redis_store_class.with do |redis|
it 'uses the ActiveSession lookup to return original sessions' do expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
Gitlab::Redis::SharedState.with do |redis| end
# 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' }))
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae
2::d2ee6f70d6ef0e8701efa3f6b281cbe8e6bf3d109ef052a8b5ce88bfc7e71c26
]
)
end end
expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }] it 'returns an empty array if the use does not have any active session' do
expect(ActiveSession.list(user)).to eq []
end
end end
end
describe '.session_ids_for_user' do describe '.list_sessions' do
it 'uses the user lookup table to return session ids' do it 'uses the ActiveSession lookup to return original sessions' do
session_ids = ['59822c7d9fcdfa03725eff41782ad97d'] redis_store_class.with do |redis|
# 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' }))
Gitlab::Redis::SharedState.with do |redis| redis.sadd(
redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids) "session:lookup:user:gitlab:#{user.id}",
end %w[
2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae
2::d2ee6f70d6ef0e8701efa3f6b281cbe8e6bf3d109ef052a8b5ce88bfc7e71c26
]
)
end
expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to eq(session_ids) expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
end
end end
end
describe '.sessions_from_ids' do describe '.session_ids_for_user' do
it 'uses the ActiveSession lookup to return original sessions' do it 'uses the user lookup table to return session ids' do
Gitlab::Redis::SharedState.with do |redis| session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
# 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' }))
end
expect(ActiveSession.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }] redis_store_class.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids)
end
expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to eq(session_ids)
end
end end
it 'avoids a redis lookup for an empty array' do describe '.sessions_from_ids' do
expect(Gitlab::Redis::SharedState).not_to receive(:with) it 'uses the ActiveSession lookup to return original sessions' do
redis_store_class.with do |redis|
# 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' }))
end
expect(ActiveSession.sessions_from_ids([])).to eq([]) expect(ActiveSession.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }]
end end
it 'uses redis lookup in batches' do it 'avoids a redis lookup for an empty array' do
stub_const('ActiveSession::SESSION_BATCH_SIZE', 1) expect(redis_store_class).not_to receive(:with)
redis = double(:redis) expect(ActiveSession.sessions_from_ids([])).to eq([])
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis) end
sessions = %w[session-a session-b] it 'uses redis lookup in batches' do
mget_responses = sessions.map { |session| [Marshal.dump(session)]} stub_const('ActiveSession::SESSION_BATCH_SIZE', 1)
expect(redis).to receive(:mget).twice.times.and_return(*mget_responses)
expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions) redis = double(:redis)
end expect(redis_store_class).to receive(:with).and_yield(redis)
end
describe '.set' do sessions = %w[session-a session-b]
it 'sets a new redis entry for the user session and a lookup entry' do mget_responses = sessions.map { |session| [Marshal.dump(session)]}
ActiveSession.set(user, request) expect(redis).to receive(:mget).twice.times.and_return(*mget_responses)
Gitlab::Redis::SharedState.with do |redis| expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions)
expect(redis.scan_each.to_a).to include(
"session:user:gitlab:#{user.id}:2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae",
"session:lookup:user:gitlab:#{user.id}"
)
end end
end end
it 'adds timestamps and information from the request' do describe '.set' do
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) 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 = ActiveSession.list(user) redis_store_class.with do |redis|
expect(redis.scan_each.to_a).to include(
expect(session.count).to eq 1 "session:user:gitlab:#{user.id}:2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae",
expect(session.first).to have_attributes( "session:lookup:user:gitlab:#{user.id}"
ip_address: '127.0.0.1', )
browser: 'Mobile Safari', end
os: 'iOS',
device_name: 'iPhone 6',
device_type: 'smartphone',
created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:06')
)
end 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 it 'adds timestamps and information from the request' do
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
ActiveSession.set(user, request) ActiveSession.set(user, request)
session = ActiveSession.list(user) session = ActiveSession.list(user)
expect(session.count).to eq 1
expect(session.first).to have_attributes( 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'), created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:07') updated_at: Time.zone.parse('2018-03-12 09:06')
) )
end end
end end
end
end
describe '.destroy_session' do it 'keeps the created_at from the login on consecutive requests' do
shared_examples 'removes all session data' do now = Time.zone.parse('2018-03-12 09:06')
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{active_session_lookup_key}", '')
# 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(described_class.key_name(user.id, active_session_lookup_key), Timecop.freeze(now) do
Marshal.dump(active_session)) ActiveSession.set(user, request)
redis.sadd(described_class.lookup_key_name(user.id),
active_session_lookup_key) 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 end
end
it 'removes the devise session' do describe '.destroy_session' do
subject shared_examples 'removes all session data' do
before do
redis_store_class.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{active_session_lookup_key}", '')
# 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}", '')
Gitlab::Redis::SharedState.with do |redis| redis.set(described_class.key_name(user.id, active_session_lookup_key),
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty Marshal.dump(active_session))
redis.sadd(described_class.lookup_key_name(user.id),
active_session_lookup_key)
end
end end
end
it 'removes the lookup entry' do it 'removes the devise session' do
subject subject
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
end
end
it 'removes the lookup entry' do
subject
redis_store_class.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
end
end end
end
it 'removes the ActiveSession' do it 'removes the ActiveSession' do
subject subject
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect(redis.scan_each(match: "session:user:gitlab:*").to_a).to be_empty expect(redis.scan_each(match: "session:user:gitlab:*").to_a).to be_empty
end
end end
end end
end
context 'destroy called with Rack::Session::SessionId#private_id' do context 'destroy called with Rack::Session::SessionId#private_id' do
subject { ActiveSession.destroy_session(user, rack_session.private_id) } subject { ActiveSession.destroy_session(user, rack_session.private_id) }
it 'calls .destroy_sessions' do it 'calls .destroy_sessions' do
expect(ActiveSession).to( expect(ActiveSession).to(
receive(:destroy_sessions) receive(:destroy_sessions)
.with(anything, user, [rack_session.private_id])) .with(anything, user, [rack_session.private_id]))
subject subject
end end
context 'ActiveSession with session_private_id' do context 'ActiveSession with session_private_id' 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 }
include_examples 'removes all session data' include_examples 'removes all session data'
end
end end
end end
end
describe '.destroy_all_but_current' do describe '.destroy_all_but_current' do
it 'gracefully handles a nil session ID' do it 'gracefully handles a nil session ID' do
expect(described_class).not_to receive(:destroy_sessions) expect(described_class).not_to receive(:destroy_sessions)
ActiveSession.destroy_all_but_current(user, nil) ActiveSession.destroy_all_but_current(user, nil)
end end
context 'with user sessions' do context 'with user sessions' do
let(:current_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' } let(:current_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
# setup for current user # setup for current user
[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)
redis.set(described_class.key_name(user.id, session_private_id),
Marshal.dump(active_session))
redis.sadd(described_class.lookup_key_name(user.id),
session_private_id)
end
# setup for unrelated user
unrelated_user_id = 9999
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(user.id, session_private_id),
redis.set(described_class.key_name(unrelated_user_id, session_private_id),
Marshal.dump(active_session)) Marshal.dump(active_session))
redis.sadd(described_class.lookup_key_name(user.id), redis.sadd(described_class.lookup_key_name(unrelated_user_id),
session_private_id) session_private_id)
end end
# setup for unrelated user
unrelated_user_id = 9999
session_private_id = Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358').private_id
active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(described_class.key_name(unrelated_user_id, session_private_id),
Marshal.dump(active_session))
redis.sadd(described_class.lookup_key_name(unrelated_user_id),
session_private_id)
end end
end
it 'removes the entry associated with the all user sessions but current' do it 'removes the entry associated with the all user sessions but current' do
expect { ActiveSession.destroy_all_but_current(user, request.session) } expect { ActiveSession.destroy_all_but_current(user, request.session) }
.to(change { ActiveSession.session_ids_for_user(user.id).size }.from(2).to(1)) .to(change { ActiveSession.session_ids_for_user(user.id).size }.from(2).to(1))
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1) expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
end end
it 'removes the lookup entry of deleted sessions' do it 'removes the lookup entry of deleted sessions' do
session_private_id = Rack::Session::SessionId.new(current_session_id).private_id session_private_id = Rack::Session::SessionId.new(current_session_id).private_id
ActiveSession.destroy_all_but_current(user, request.session) ActiveSession.destroy_all_but_current(user, request.session)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
expect( expect(
redis.smembers(described_class.lookup_key_name(user.id)) redis.smembers(described_class.lookup_key_name(user.id))
).to eq([session_private_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::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set(described_class.key_name(user.id, impersonated_session_id), redis.set(described_class.key_name(user.id, impersonated_session_id),
Marshal.dump(ActiveSession.new(session_id: Rack::Session::SessionId.new(impersonated_session_id), is_impersonated: true))) Marshal.dump(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(described_class.lookup_key_name(user.id), 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)
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 end
end
describe '.cleanup' do
before do
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
end
it 'removes obsolete lookup entries' do describe '.cleanup' do
Gitlab::Redis::SharedState.with do |redis| before do
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '') stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end end
ActiveSession.cleanup(user) it 'removes obsolete lookup entries' do
redis_store_class.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| redis_store_class.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d'] expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
end end
end
it 'does not bail if there are no lookup entries' do it 'does not bail if there are no lookup entries' do
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
end end
context 'cleaning up old sessions' do 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_one) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 1 }
let(:max_number_of_sessions_plus_two) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 2 } let(:max_number_of_sessions_plus_two) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 2 }
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.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}", "session:user:gitlab:#{user.id}:#{number}",
Marshal.dump(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago)) Marshal.dump(ActiveSession.new(session_id: number.to_s, updated_at: number.days.ago))
) )
redis.sadd( redis.sadd(
"session:lookup:user:gitlab:#{user.id}", "session:lookup:user:gitlab:#{user.id}",
"#{number}" "#{number}"
) )
end
end end
end end
end
it 'removes obsolete active sessions entries' do it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a 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.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("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}", "session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}")
end
end end
end
it 'removes obsolete lookup entries' do it 'removes obsolete lookup entries' do
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}") 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.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)
end
end end
end
it 'removes obsolete lookup entries even without active session' do it 'removes obsolete lookup entries even without active session' do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.sadd( redis.sadd(
"session:lookup:user:gitlab:#{user.id}", "session:lookup:user:gitlab:#{user.id}",
"#{max_number_of_sessions_plus_two + 1}" "#{max_number_of_sessions_plus_two + 1}"
) )
end end
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}") 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.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
expect(lookup_entries).not_to include( expect(lookup_entries).not_to include(
max_number_of_sessions_plus_one.to_s, max_number_of_sessions_plus_one.to_s,
max_number_of_sessions_plus_two.to_s, max_number_of_sessions_plus_two.to_s,
(max_number_of_sessions_plus_two + 1).to_s (max_number_of_sessions_plus_two + 1).to_s
) )
end
end end
end
context 'when the number of active sessions is lower than the limit' do context 'when the number of active sessions is lower than the limit' do
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.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("session:user:gitlab:#{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::SharedState.with do |redis| lookup_entries_before_cleanup = redis_store_class.with do |redis|
redis.smembers("session:lookup:user:gitlab:#{user.id}") redis.smembers("session:lookup:user:gitlab:#{user.id}")
end end
sessions_before_cleanup = Gitlab::Redis::SharedState.with do |redis| sessions_before_cleanup = redis_store_class.with do |redis|
redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
end end
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}") lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
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 end
end end
end end
end
context 'cleaning up old sessions stored by Rack::Session::SessionId#private_id' do context 'cleaning up old sessions stored by Rack::Session::SessionId#private_id' do
let(:max_number_of_sessions_plus_one) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 1 } 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 } let(:max_number_of_sessions_plus_two) { ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS + 2 }
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.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}", "session:user:gitlab:#{user.id}:#{number}",
Marshal.dump(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago)) Marshal.dump(ActiveSession.new(session_private_id: number.to_s, updated_at: number.days.ago))
) )
redis.sadd( redis.sadd(
"session:lookup:user:gitlab:#{user.id}", "session:lookup:user:gitlab:#{user.id}",
"#{number}" "#{number}"
) )
end
end end
end end
end
it 'removes obsolete active sessions entries' do it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user) ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a 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.count).to eq(ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
expect(sessions).not_to( expect(sessions).not_to(
include("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}", include("session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_one}",
"session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}")) "session:user:gitlab:#{user.id}:#{max_number_of_sessions_plus_two}"))
end
end end
end end
end end
end end
it_behaves_like 'redis sessions store', 'active session'
end end
...@@ -376,24 +376,28 @@ RSpec.describe API::Commits do ...@@ -376,24 +376,28 @@ RSpec.describe API::Commits do
end end
end end
context 'when using warden' do RSpec.shared_examples_for 'warden user session' do
it 'increments usage counters', :clean_gitlab_redis_shared_state do context 'when using warden' do
session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') it 'increments usage counters' do
session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] } session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] }
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash)) redis_store_class.with do |redis|
end redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
end
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count) expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action) expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
post api(url), params: valid_c_params post api(url), params: valid_c_params
end
end end
end end
it_behaves_like 'redis sessions store', 'warden user session'
context 'a new file in project repo' do context 'a new file in project repo' do
before do before do
post api(url, user), params: valid_c_params post api(url, user), params: valid_c_params
......
# frozen_string_literal: true # frozen_string_literal: true
module SessionHelpers module SessionHelpers
def expect_single_session_with_authenticated_ttl def expect_single_session_with_authenticated_ttl(redis_store_class)
expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60) expect_single_session_with_expiration(redis_store_class, Settings.gitlab['session_expire_delay'] * 60)
end end
def expect_single_session_with_short_ttl def expect_single_session_with_short_ttl(redis_store_class)
expect_single_session_with_expiration(Settings.gitlab['unauthenticated_session_expire_delay']) expect_single_session_with_expiration(redis_store_class, Settings.gitlab['unauthenticated_session_expire_delay'])
end end
def expect_single_session_with_expiration(expiration) def expect_single_session_with_expiration(redis_store_class, expiration)
session_keys = get_session_keys session_keys = get_session_keys(redis_store_class)
expect(session_keys.size).to eq(1) expect(session_keys.size).to eq(1)
expect(get_ttl(session_keys.first)).to be_within(5).of(expiration) expect(get_ttl(redis_store_class, session_keys.first)).to be_within(5).of(expiration)
end end
def get_session_keys def get_session_keys(redis_store_class)
Gitlab::Redis::SharedState.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a } redis_store_class.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a }
end end
def get_ttl(key) def get_ttl(redis_store_class, key)
Gitlab::Redis::SharedState.with { |redis| redis.ttl(key) } redis_store_class.with { |redis| redis.ttl(key) }
end end
end end
...@@ -93,18 +93,23 @@ RSpec.shared_examples "redis_shared_examples" do ...@@ -93,18 +93,23 @@ RSpec.shared_examples "redis_shared_examples" do
subject { described_class.new(rails_env).store } subject { described_class.new(rails_env).store }
shared_examples 'redis store' do shared_examples 'redis store' do
let(:redis_store) { ::Redis::Store }
let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database}" }
it 'instantiates Redis::Store' do it 'instantiates Redis::Store' do
is_expected.to be_a(::Redis::Store) is_expected.to be_a(redis_store)
expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database}")
expect(subject.to_s).to eq(redis_store_to_s)
end end
context 'with the namespace' do context 'with the namespace' do
let(:namespace) { 'namespace_name' } let(:namespace) { 'namespace_name' }
let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}" }
subject { described_class.new(rails_env).store(namespace: namespace) } subject { described_class.new(rails_env).store(namespace: namespace) }
it "uses specified namespace" do it "uses specified namespace" do
expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}") expect(subject.to_s).to eq(redis_store_to_s)
end end
end end
end end
......
# frozen_string_literal: true
RSpec.shared_examples 'redis sessions store' do |example|
context 'when ENV[GITLAB_USE_REDIS_SESSIONS_STORE] is true', :clean_gitlab_redis_sessions do
before do
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', 'true')
end
it_behaves_like example do
let(:redis_store_class) { Gitlab::Redis::Sessions }
end
end
context 'when ENV[GITLAB_USE_REDIS_SESSIONS_STORE] is false', :clean_gitlab_redis_shared_state do
before do
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', 'false')
end
it_behaves_like example do
let(:redis_store_class) { Gitlab::Redis::SharedState }
end
end
end
...@@ -18,32 +18,36 @@ RSpec.shared_examples 'snippet edit usage data counters' do ...@@ -18,32 +18,36 @@ RSpec.shared_examples 'snippet edit usage data counters' do
end end
end end
context 'when user is not sessionless' do RSpec.shared_examples_for 'sessionless user' do
before do context 'when user is not sessionless' do
session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') before do
session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] } session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
session_hash = { 'warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]] }
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash)) redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
end end
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
end end
it 'tracks usage data actions', :clean_gitlab_redis_shared_state do it 'tracks usage data actions' do
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action) expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action)
post_graphql_mutation(mutation) post_graphql_mutation(mutation)
end end
context 'when mutation result raises an error' do context 'when mutation result raises an error' do
it 'does not track usage data actions' do it 'does not track usage data actions' do
mutation_vars[:title] = nil mutation_vars[:title] = nil
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action) expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).not_to receive(:track_snippet_editor_edit_action)
post_graphql_mutation(mutation) post_graphql_mutation(mutation)
end
end end
end end
end end
it_behaves_like 'redis sessions store', 'sessionless user'
end end
...@@ -3,195 +3,199 @@ ...@@ -3,195 +3,199 @@
require 'rake_helper' require 'rake_helper'
RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout do RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout do
before do RSpec.shared_examples_for 'rake gitlab:cleanup' do
Rake.application.rake_require 'tasks/gitlab/cleanup'
end
# A single integration test that is redundant with one part of the
# Gitlab::Cleanup::ProjectUploads spec.
#
# Additionally, this tests DRY_RUN env var values, and the extra line of
# output that says you can disable DRY_RUN if it's enabled.
describe 'cleanup:project_uploads' do
let!(:logger) { double(:logger) }
before do before do
expect(main_object).to receive(:logger).and_return(logger).at_least(:once) Rake.application.rake_require 'tasks/gitlab/cleanup'
allow(logger).to receive(:info).at_least(:once)
allow(logger).to receive(:debug).at_least(:once)
end end
context 'with a fixable orphaned project upload file' do # A single integration test that is redundant with one part of the
let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) } # Gitlab::Cleanup::ProjectUploads spec.
let(:new_path) { orphaned.absolute_path } #
let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) } # Additionally, this tests DRY_RUN env var values, and the extra line of
# output that says you can disable DRY_RUN if it's enabled.
describe 'cleanup:project_uploads' do
let!(:logger) { double(:logger) }
before do before do
FileUtils.mkdir_p(File.dirname(path)) expect(main_object).to receive(:logger).and_return(logger).at_least(:once)
FileUtils.mv(new_path, path)
allow(logger).to receive(:info).at_least(:once)
allow(logger).to receive(:debug).at_least(:once)
end end
context 'with DRY_RUN disabled' do context 'with a fixable orphaned project upload file' do
let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) }
let(:new_path) { orphaned.absolute_path }
let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) }
before do before do
stub_env('DRY_RUN', 'false') FileUtils.mkdir_p(File.dirname(path))
FileUtils.mv(new_path, path)
end end
it 'moves the file to its proper location' do context 'with DRY_RUN disabled' do
run_rake_task('gitlab:cleanup:project_uploads') before do
stub_env('DRY_RUN', 'false')
end
it 'moves the file to its proper location' do
run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(path)).to be_falsey expect(File.exist?(path)).to be_falsey
expect(File.exist?(new_path)).to be_truthy expect(File.exist?(new_path)).to be_truthy
end
it 'logs action as done' do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up...")
expect(logger).to receive(:info).with("Did fix #{path} -> #{new_path}")
run_rake_task('gitlab:cleanup:project_uploads')
end
end end
it 'logs action as done' do shared_examples_for 'does not move the file' do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up...") it 'does not move the file' do
expect(logger).to receive(:info).with("Did fix #{path} -> #{new_path}") run_rake_task('gitlab:cleanup:project_uploads')
expect(File.exist?(path)).to be_truthy
expect(File.exist?(new_path)).to be_falsey
end
it 'logs action as able to be done' do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up. Dry run...")
expect(logger).to receive(:info).with("Can fix #{path} -> #{new_path}")
expect(logger).to receive(:info).with(/To clean up these files run this command with DRY_RUN=false/)
run_rake_task('gitlab:cleanup:project_uploads') run_rake_task('gitlab:cleanup:project_uploads')
end
end end
end
shared_examples_for 'does not move the file' do context 'with DRY_RUN explicitly enabled' do
it 'does not move the file' do before do
run_rake_task('gitlab:cleanup:project_uploads') stub_env('DRY_RUN', 'true')
end
expect(File.exist?(path)).to be_truthy it_behaves_like 'does not move the file'
expect(File.exist?(new_path)).to be_falsey
end end
it 'logs action as able to be done' do context 'with DRY_RUN set to an unknown value' do
expect(logger).to receive(:info).with("Looking for orphaned project uploads to clean up. Dry run...") before do
expect(logger).to receive(:info).with("Can fix #{path} -> #{new_path}") stub_env('DRY_RUN', 'foo')
expect(logger).to receive(:info).with(/To clean up these files run this command with DRY_RUN=false/) end
run_rake_task('gitlab:cleanup:project_uploads') it_behaves_like 'does not move the file'
end end
end
context 'with DRY_RUN explicitly enabled' do context 'with DRY_RUN unset' do
before do it_behaves_like 'does not move the file'
stub_env('DRY_RUN', 'true')
end end
end
end
describe 'gitlab:cleanup:orphan_job_artifact_files' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_job_artifact_files') }
it 'runs the task without errors' do
expect(Gitlab::Cleanup::OrphanJobArtifactFiles)
.to receive(:new).and_call_original
it_behaves_like 'does not move the file' expect { rake_task }.not_to raise_error
end end
context 'with DRY_RUN set to an unknown value' do context 'with DRY_RUN set to false' do
before do before do
stub_env('DRY_RUN', 'foo') stub_env('DRY_RUN', 'false')
end end
it_behaves_like 'does not move the file' it 'passes dry_run correctly' do
end expect(Gitlab::Cleanup::OrphanJobArtifactFiles)
.to receive(:new)
.with(dry_run: false,
niceness: anything,
logger: anything)
.and_call_original
context 'with DRY_RUN unset' do rake_task
it_behaves_like 'does not move the file' end
end end
end end
end
describe 'gitlab:cleanup:orphan_job_artifact_files' do describe 'gitlab:cleanup:orphan_lfs_file_references' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_job_artifact_files') } subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_file_references') }
it 'runs the task without errors' do let(:project) { create(:project, :repository) }
expect(Gitlab::Cleanup::OrphanJobArtifactFiles)
.to receive(:new).and_call_original
expect { rake_task }.not_to raise_error
end
context 'with DRY_RUN set to false' do
before do before do
stub_env('DRY_RUN', 'false') stub_env('PROJECT_ID', project.id)
end end
it 'passes dry_run correctly' do it 'runs the task without errors' do
expect(Gitlab::Cleanup::OrphanJobArtifactFiles) expect(Gitlab::Cleanup::OrphanLfsFileReferences)
.to receive(:new) .to receive(:new).and_call_original
.with(dry_run: false,
niceness: anything,
logger: anything)
.and_call_original
rake_task expect { rake_task }.not_to raise_error
end end
end
end
describe 'gitlab:cleanup:orphan_lfs_file_references' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_file_references') }
let(:project) { create(:project, :repository) } context 'with DRY_RUN set to false' do
before do
before do stub_env('DRY_RUN', 'false')
stub_env('PROJECT_ID', project.id) end
end
it 'runs the task without errors' do it 'passes dry_run correctly' do
expect(Gitlab::Cleanup::OrphanLfsFileReferences) expect(Gitlab::Cleanup::OrphanLfsFileReferences)
.to receive(:new).and_call_original .to receive(:new)
.with(project,
dry_run: false,
logger: anything)
.and_call_original
expect { rake_task }.not_to raise_error rake_task
end
end
end end
context 'with DRY_RUN set to false' do describe 'gitlab:cleanup:orphan_lfs_files' do
before do subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_files') }
stub_env('DRY_RUN', 'false')
end
it 'passes dry_run correctly' do it 'runs RemoveUnreferencedLfsObjectsWorker' do
expect(Gitlab::Cleanup::OrphanLfsFileReferences) expect_any_instance_of(RemoveUnreferencedLfsObjectsWorker)
.to receive(:new) .to receive(:perform)
.with(project,
dry_run: false,
logger: anything)
.and_call_original .and_call_original
rake_task rake_task
end end
end end
end
describe 'gitlab:cleanup:orphan_lfs_files' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_files') }
it 'runs RemoveUnreferencedLfsObjectsWorker' do
expect_any_instance_of(RemoveUnreferencedLfsObjectsWorker)
.to receive(:perform)
.and_call_original
rake_task
end
end
context 'sessions' do context 'sessions' do
describe 'gitlab:cleanup:sessions:active_sessions_lookup_keys', :clean_gitlab_redis_shared_state do describe 'gitlab:cleanup:sessions:active_sessions_lookup_keys' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:sessions:active_sessions_lookup_keys') } subject(:rake_task) { run_rake_task('gitlab:cleanup:sessions:active_sessions_lookup_keys') }
let!(:user) { create(:user) } let!(:user) { create(:user) }
let(:existing_session_id) { '5' } let(:existing_session_id) { '5' }
before do before do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{existing_session_id}", redis.set("session:user:gitlab:#{user.id}:#{existing_session_id}",
Marshal.dump(true)) Marshal.dump(true))
redis.sadd("session:lookup:user:gitlab:#{user.id}", (1..10).to_a) redis.sadd("session:lookup:user:gitlab:#{user.id}", (1..10).to_a)
end
end end
end
it 'runs the task without errors' do it 'runs the task without errors' do
expect { rake_task }.not_to raise_error expect { rake_task }.not_to raise_error
end end
it 'removes expired active session lookup keys' do it 'removes expired active session lookup keys' do
Gitlab::Redis::SharedState.with do |redis| redis_store_class.with do |redis|
lookup_key = "session:lookup:user:gitlab:#{user.id}" lookup_key = "session:lookup:user:gitlab:#{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("session:lookup:user:gitlab:#{user.id}")).to(
eql([existing_session_id])) eql([existing_session_id]))
end
end end
end end
end end
end end
it_behaves_like 'redis sessions store', 'rake gitlab:cleanup'
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