Commit 862d0b5c authored by nmilojevic1's avatar nmilojevic1

Simplify specs to only cover Redis::Sessions use case

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