Commit 229fd8af authored by nmilojevic1's avatar nmilojevic1

Add specs for both SharedState and Sessions store

parent e6551492
......@@ -45,22 +45,26 @@ RSpec.describe Groups::DependencyProxyForContainersController do
expect(response).to have_gitlab_http_status(:not_found)
end
context 'with an active session', :clean_gitlab_redis_shared_state 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
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
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
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
end
end
it_behaves_like successful_example
it_behaves_like successful_example
end
end
it_behaves_like 'redis sessions store', 'active session'
end
context 'when git check is not enforced' do
......
......@@ -85,44 +85,52 @@ RSpec.describe 'Login' do
expect(page.body).to have_link('Register now', href: new_user_registration_path)
end
describe 'with two-factor authentication required', :clean_gitlab_redis_shared_state do
let_it_be(:user) { create(:user) }
let_it_be(:smartcard_identity) { create(:smartcard_identity, user: user) }
RSpec.shared_examples_for 'two-factor authentication' do
before do
stub_application_setting(require_two_factor_authentication: true)
load Rails.root.join('config/initializers/session_store.rb')
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)
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)
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)
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)
# 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')
expect(page).not_to have_content('Two-Factor Authentication')
end
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')
expect(page).to have_content('Two-Factor Authentication')
end
end
end
end
it_behaves_like 'redis sessions store', 'two-factor authentication'
end
end
end
......
......@@ -9,208 +9,212 @@ RSpec.describe Gitlab::Auth::GroupSaml::SessionEnforcer do
end
end
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) }
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) }
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
allow(saml_provider).to receive(:git_check_enforced?).and_return(true)
stub_licensed_features(group_saml: true)
end
context 'with an active session', :clean_gitlab_redis_shared_state 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 'when git check is enforced' do
before do
Gitlab::Redis::SharedState.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(saml_provider).to receive(:git_check_enforced?).and_return(true)
end
it_behaves_like 'not enforced'
context 'with an active session' do
let(:session_id) { '42' }
let(:session_time) { 5.minutes.ago }
let(:stored_session) do
{ 'active_group_sso_sign_ins' => { saml_provider.id => session_time } }
end
context 'with sub-group' do
before do
allow(group).to receive(:root_ancestor).and_return(root_group)
redis_store_class.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
end
end
let(:group) { create(:group) }
it_behaves_like 'not enforced'
subject(:enforced?) { described_class.new(user, group).access_restricted? }
context 'with sub-group' do
before do
allow(group).to receive(:root_ancestor).and_return(root_group)
end
it_behaves_like 'not enforced'
end
let(:group) { create(:group) }
context 'with expired session' do
let(:session_time) { 2.days.ago }
subject(:enforced?) { described_class.new(user, group).access_restricted? }
it 'returns true' do
expect(enforced?).to eq(true)
it_behaves_like 'not enforced'
end
end
context 'with two active sessions', :clean_gitlab_redis_shared_state 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 expired session' do
let(:session_time) { 2.days.ago }
before do
Gitlab::Redis::SharedState.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])
it 'returns true' do
expect(enforced?).to eq(true)
end
end
it_behaves_like 'not enforced'
end
context 'with two active sessions for the same provider and one pre-sso', :clean_gitlab_redis_shared_state 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' 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
before do
Gitlab::Redis::SharedState.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])
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'
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
context 'without enforced_sso_expiry feature flag' do
let(:session_time) { 2.days.ago }
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
end
before do
stub_feature_flags(enforced_sso_expiry: false)
it_behaves_like 'not enforced'
end
it_behaves_like 'not enforced'
end
context 'without group' do
let(:root_group) { nil }
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 saml_provider' do
let(:root_group) { create(:group) }
it_behaves_like 'not enforced'
end
it_behaves_like 'not enforced'
end
context 'without group' do
let(:root_group) { nil }
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 'without saml_provider' do
let(:root_group) { create(:group) }
context 'with auditor' do
let(:user) { create(:user, :auditor) }
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 group owner' do
before do
root_group.add_owner(user)
it_behaves_like 'not enforced'
end
it_behaves_like 'not enforced'
end
end
context 'with auditor' do
let(:user) { create(:user, :auditor) }
context 'without any session' do
it 'returns true' do
expect(enforced?).to eq(true)
end
it_behaves_like 'not enforced'
end
context 'with admin', :enable_admin_mode do
let(:user) { create(:user, :admin) }
context 'with group owner' do
before do
root_group.add_owner(user)
end
it_behaves_like 'not enforced'
it_behaves_like 'not enforced'
end
end
context 'with auditor' do
let(:user) { create(:user, :auditor) }
context 'without any session' do
it 'returns true' do
expect(enforced?).to eq(true)
end
it_behaves_like 'not enforced'
end
context 'with admin', :enable_admin_mode do
let(:user) { create(:user, :admin) }
context 'with group owner' do
before do
root_group.add_owner(user)
it_behaves_like 'not enforced'
end
it_behaves_like 'not enforced'
context 'with auditor' do
let(:user) { create(:user, :auditor) }
it_behaves_like 'not enforced'
end
context 'when group is a subgroup' do
context 'with group owner' do
before do
allow(group).to receive(:root_ancestor).and_return(root_group)
root_group.add_owner(user)
end
let(:group) { create(:group) }
it_behaves_like 'not enforced'
subject(:enforced?) { described_class.new(user, group).access_restricted? }
context 'when group is a subgroup' do
before do
allow(group).to receive(:root_ancestor).and_return(root_group)
end
it 'returns true' do
expect(enforced?).to eq(true)
let(:group) { create(:group) }
subject(:enforced?) { described_class.new(user, group).access_restricted? }
it 'returns true' do
expect(enforced?).to eq(true)
end
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'
it_behaves_like 'not enforced'
end
end
end
end
context 'when git check is not enforced' do
before do
allow(saml_provider).to receive(:git_check_enforced?).and_return(false)
end
context 'with an active session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'active_group_sso_sign_ins' => { saml_provider.id => 5.minutes.ago } }
context 'when git check is not enforced' do
before do
allow(saml_provider).to receive(:git_check_enforced?).and_return(false)
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
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
end
it_behaves_like 'not enforced'
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
end
context 'without any session' do
it_behaves_like 'not enforced'
it_behaves_like 'not enforced'
end
context 'without any session' do
it_behaves_like 'not enforced'
end
end
end
end
it_behaves_like 'redis sessions store', 'group saml session enforcer'
end
......@@ -2,62 +2,66 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::Otp::SessionEnforcer, :clean_gitlab_redis_shared_state do
let_it_be(:key) { create(:key)}
RSpec.describe Gitlab::Auth::Otp::SessionEnforcer do
shared_examples_for 'otp session enforcer' 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
it 'registers a session in Redis' do
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
session_expiry_in_seconds = Gitlab::CurrentSettings.git_two_factor_session_expiry.minutes.to_i
expect(redis).to(
receive(:setex)
.with("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}",
session_expiry_in_seconds,
true)
.once)
described_class.new(key).update_session
end
context 'when licensed feature is not available' do
before do
stub_licensed_features(git_two_factor_enforcement: false)
stub_licensed_features(git_two_factor_enforcement: true)
end
it 'does not register a session in Redis' do
expect(redis).not_to receive(:setex)
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
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
end
end
describe '#access_restricted?' do
subject { described_class.new(key).access_restricted? }
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)
before do
stub_licensed_features(git_two_factor_enforcement: true)
described_class.new(key).update_session
end
end
end
context 'with existing session' do
describe '#access_restricted?' do
subject { described_class.new(key).access_restricted? }
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("#{described_class::OTP_SESSIONS_NAMESPACE}:#{key.id}", true )
end
stub_licensed_features(git_two_factor_enforcement: true)
end
it { is_expected.to be_falsey }
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
end
it { is_expected.to be_falsey }
end
context 'without an existing session' do
it { is_expected.to be_truthy }
context 'without an existing session' do
it { is_expected.to be_truthy }
end
end
end
it_behaves_like 'redis sessions store', 'otp session enforcer'
end
......@@ -29,22 +29,26 @@ RSpec.describe Gitlab::Auth::Smartcard::SessionEnforcer do
stub_smartcard_setting(enabled: true, required_for_git_access: true)
end
context 'with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
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
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
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
end
end
it { is_expected.to be_falsey }
it { is_expected.to be_falsey }
end
end
it_behaves_like 'redis sessions store', 'smartcard session'
context 'without any session' do
it { is_expected.to be_truthy }
end
......
......@@ -3,45 +3,49 @@
require 'spec_helper'
RSpec.describe Gitlab::Auth::Smartcard::Session do
describe '#active?' do
let(:user) { create(:user) }
RSpec.shared_examples_for 'smartcard session' do
describe '#active?' do
let(:user) { create(:user) }
subject { described_class.new.active?(user) }
subject { described_class.new.active?(user) }
context 'with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
context 'with a smartcard session' do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
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
end
it { is_expected.to be_truthy }
end
it { is_expected.to be_truthy }
context 'without any session' do
it { is_expected.to be_falsey }
end
end
context 'without any session' do
it { is_expected.to be_falsey }
end
end
describe '#update_active' do
let(:now) { Time.now }
describe '#update_active' do
let(:now) { Time.now }
around do |example|
Gitlab::Session.with_session({}) do
example.run
around do |example|
Gitlab::Session.with_session({}) do
example.run
end
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 })
expect(Gitlab::Session.current[:smartcard_signins]).to eq({ 'last_signin_at' => now })
end
end
end
it_behaves_like 'redis sessions store', 'smartcard session'
end
......@@ -704,232 +704,236 @@ RSpec.describe Gitlab::GitAccess do
end
end
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
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)
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
project.add_developer(user)
end
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
context 'user with a smartcard session' do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
end
end
it 'allows pull changes' do
expect { pull_changes }.not_to raise_error
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
end
it 'allows push changes' do
expect { push_changes }.not_to raise_error
end
end
it 'allows pull changes' do
expect { pull_changes }.not_to raise_error
end
context 'user without a smartcard session' do
it 'does not allow pull changes' do
expect { pull_changes }.to raise_error(Gitlab::GitAccess::ForbiddenError)
it 'allows push changes' do
expect { push_changes }.not_to raise_error
end
end
it 'does not allow push changes' do
expect { push_changes }.to raise_error(Gitlab::GitAccess::ForbiddenError)
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 'with the setting off' do
before do
stub_smartcard_setting(required_for_git_access: false)
it 'does not allow push changes' do
expect { push_changes }.to raise_error(Gitlab::GitAccess::ForbiddenError)
end
end
it 'allows pull changes' do
expect { pull_changes }.not_to raise_error
end
context 'with the setting off' do
before do
stub_smartcard_setting(required_for_git_access: false)
end
it 'allows push changes' do
expect { push_changes }.not_to raise_error
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
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 }
let(:protocol) { 'ssh' }
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 }
before do
project.add_developer(user)
stub_feature_flags(two_factor_for_cli: true)
stub_licensed_features(git_two_factor_enforcement: true)
end
let(:protocol) { 'ssh' }
context 'with an OTP session', :clean_gitlab_redis_shared_state do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}", true)
end
project.add_developer(user)
stub_feature_flags(two_factor_for_cli: true)
stub_licensed_features(git_two_factor_enforcement: 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
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
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 }
before do
stub_application_setting(git_two_factor_session_expiry: git_two_factor_session_expiry)
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
def value_of_key
key_expired = Time.current > redis_key_expiry_at
return if key_expired
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 }
true
end
before do
stub_application_setting(git_two_factor_session_expiry: git_two_factor_session_expiry)
end
def stub_redis
redis = double(:redis)
expect(Gitlab::Redis::SharedState).to receive(:with).at_most(:twice).and_yield(redis)
def value_of_key
key_expired = Time.current > redis_key_expiry_at
return if key_expired
expect(redis).to(
receive(:get)
.with("#{Gitlab::Auth::Otp::SessionEnforcer::OTP_SESSIONS_NAMESPACE}:#{key.id}"))
.at_most(:twice)
.and_return(value_of_key)
end
true
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
def stub_redis
redis = double(:redis)
expect(redis_store_class).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
aggregate_failures do
expect { push_changes }.not_to raise_error
expect { pull_changes }.not_to raise_error
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
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)
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
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)
aggregate_failures do
expect { push_changes }.to raise_forbidden(error_message)
expect { pull_changes }.to raise_forbidden(error_message)
end
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
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
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
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
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
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
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
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
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
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
end
it_behaves_like 'redis sessions store', 'checks smartcard access & otp session'
describe '#check_sso_session!' do
before do
project.add_developer(user)
......
......@@ -154,26 +154,30 @@ RSpec.describe API::Internal::Base do
project.add_developer(user)
end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
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
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
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
end
end
it "allows access" do
subject
it "allows access" do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to have_gitlab_http_status(:ok)
end
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,26 +54,30 @@ RSpec.describe Repositories::GitHttpController, type: :request do
project.add_developer(user)
end
context 'user with a smartcard session', :clean_gitlab_redis_shared_state do
let(:session_id) { '42' }
let(:stored_session) do
{ 'smartcard_signins' => { 'last_signin_at' => 5.minutes.ago } }
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
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id}", Marshal.dump(stored_session))
redis.sadd("session:lookup:user:gitlab:#{user.id}", [session_id])
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
end
end
it "allows access" do
subject
it "allows access" do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to have_gitlab_http_status(:ok)
end
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,31 +2,43 @@
require 'spec_helper'
RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do
let(:session_id) { Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d') }
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
context 'when session cookie is set' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
end
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
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]] } }
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
it 'sets current_user' do
connect
expect(connection.current_user).to eq(user)
end
expect(connection.current_user).to eq(user)
context 'with a stale password' do
let(:partial_password_hash) { build(:user, password: 'some_old_password').encrypted_password[0, 29] }
let(:session_hash) { { 'warden.user.user.key' => [[user.id], partial_password_hash] } }
it 'sets current_user to nil' do
connect
expect(connection.current_user).to be_nil
end
end
end
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 not logged in' do
let(:session_hash) { {} }
it 'sets current_user to nil' do
connect
......@@ -36,32 +48,24 @@ RSpec.describe ApplicationCable::Connection, :clean_gitlab_redis_shared_state do
end
end
context 'when user is not logged in' do
let(:session_hash) { {} }
context 'when session cookie is not set' do
it 'sets current_user to nil' do
connect
expect(connection.current_user).to be_nil
end
end
end
context 'when session cookie is not set' do
it 'sets current_user to nil' do
connect
expect(connection.current_user).to be_nil
end
end
context 'when session cookie is an empty string' do
it 'sets current_user to nil' do
cookies[Gitlab::Application.config.session_options[:key]] = ''
context 'when session cookie is an empty string' do
it 'sets current_user to nil' do
cookies[Gitlab::Application.config.session_options[:key]] = ''
connect
connect
expect(connection.current_user).to be_nil
expect(connection.current_user).to be_nil
end
end
end
it_behaves_like 'redis sessions store', 'ApplicationCable::Connection'
end
......@@ -2,70 +2,74 @@
require 'spec_helper'
RSpec.describe 'Active user sessions', :clean_gitlab_redis_shared_state 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
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
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')
)
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
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)
Gitlab::Redis::SharedState.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
redis_store_class.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
gitlab_sign_in(user)
gitlab_sign_in(user)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d'
redis_store_class.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d'
end
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)
Gitlab::Redis::SharedState.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
redis_store_class.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
Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d'
redis_store_class.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d'
end
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
expect(ActiveSession.list(user)).to be_empty
end
end
it_behaves_like 'redis sessions store', 'active user sessions'
end
......@@ -2,38 +2,42 @@
require 'spec_helper'
RSpec.describe 'Session TTLs', :clean_gitlab_redis_shared_state do
RSpec.describe 'Session TTLs' do
include SessionHelpers
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'
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'
expect(page).to have_content('Invalid login or password')
expect(page).to have_content('Invalid login or password')
expect_single_session_with_short_ttl
end
expect_single_session_with_short_ttl(redis_store_class)
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
end
expect_single_session_with_authenticated_ttl(redis_store_class)
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
expect(page).to have_current_path(new_user_session_path)
expect_single_session_with_short_ttl(redis_store_class)
expect(page).to have_current_path(new_user_session_path)
end
end
end
it_behaves_like 'redis sessions store', 'session ttls'
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
RSpec.describe 'Login' do
include TermsHelper
include UserLoginHelper
include SessionHelpers
......@@ -11,506 +11,540 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
stub_authentication_activity_metrics(debug: true)
end
describe 'password reset token after successful sign in' do
it 'invalidates password reset token' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
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)
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
user.reload
expect(user.reset_password_token).to be_nil
end
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
expect(current_path).to eq root_path
end
expect_single_session_with_authenticated_ttl(redis_store_class)
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.')
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
end
describe 'with a blocked account' do
it 'prevents the user from logging in' do
expect(authentication_metrics)
.to increment(:user_blocked_counter)
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
user = create(:user, :blocked)
gitlab_sign_in(user)
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
expect(page).to have_content('Your account has been blocked.')
end
user = create(:user, :blocked)
it 'does not update Devise trackable attributes', :clean_gitlab_redis_shared_state do
expect(authentication_metrics)
.to increment(:user_blocked_counter)
.and increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
gitlab_sign_in(user)
user = create(:user, :blocked)
expect(page).to have_content('Your account has been blocked.')
end
expect { gitlab_sign_in(user) }.not_to change { user.reload.sign_in_count }
end
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
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" }
user = create(:user, :blocked)
before do
stub_application_setting(send_user_confirmation_email: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
expect { gitlab_sign_in(user) }.not_to change { user.reload.sign_in_count }
end
end
context 'within the grace period' do
it 'allows to login' do
expect(authentication_metrics).to increment(:user_authenticated_counter)
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" }
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)
before do
stub_application_setting(send_user_confirmation_email: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
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 'within the grace period' do
it 'allows to login' do
expect(authentication_metrics).to increment(:user_authenticated_counter)
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)
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
end
context 'when resending the confirmation email' do
it 'redirects to the "almost there" page' do
stub_feature_flags(soft_email_confirmation: false)
user = create(:user)
context 'when the confirmation grace period is expired' do
it 'prevents the user from logging in and renders a resend confirmation email link', :js do
travel_to((grace_period + 1.day).from_now) do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
visit new_user_confirmation_path
fill_in 'user_email', with: user.email
click_button 'Resend'
gitlab_sign_in(user)
expect(current_path).to eq users_almost_there_path
expect(page).to have_content(alert_title)
expect(page).to have_content(alert_message)
expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path)
end
end
end
end
end
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)
expect(page).to have_content('Invalid login or password.')
end
context 'when resending the confirmation email' do
it 'redirects to the "almost there" page' do
stub_feature_flags(soft_email_confirmation: false)
it 'does not update Devise trackable attributes', :clean_gitlab_redis_shared_state do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter)
user = create(:user)
expect { gitlab_sign_in(User.ghost) }
.not_to change { User.ghost.reload.sign_in_count }
end
end
visit new_user_confirmation_path
fill_in 'user_email', with: user.email
click_button 'Resend'
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'
expect(current_path).to eq users_almost_there_path
end
end
end
context 'with valid username/password' do
let(:user) { create(:user, :two_factor) }
describe 'with the ghost user' do
it 'disallows login' do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter)
before do
gitlab_sign_in(user, remember: true)
gitlab_sign_in(User.ghost)
expect(page).to have_content('Two-Factor Authentication')
expect(page).to have_content('Invalid login or password.')
end
it 'does not show a "You are already signed in." error message' do
it 'does not update Devise trackable attributes' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
enter_code(user.current_otp)
.to increment(:user_unauthenticated_counter)
.and increment(:user_password_invalid_counter)
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
expect_single_session_with_authenticated_ttl
expect { gitlab_sign_in(User.ghost) }
.not_to change { User.ghost.reload.sign_in_count }
end
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')
enter_code(user.current_otp)
expect(page).to have_content('An error occurred. Please sign in again.')
describe 'with OneTrust authentication' do
before do
stub_config(extra: { one_trust_id: SecureRandom.uuid })
end
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)
expect_single_session_with_authenticated_ttl
expect(current_path).to eq root_path
end
it 'has proper Content-Security-Policy headers' do
visit root_path
it 'persists remember_me value via hidden field' do
field = first('input#user_remember_me', visible: false)
expect(response_headers['Content-Security-Policy']).to include('https://cdn.cookielaw.org https://*.onetrust.com')
end
end
expect(field.value).to eq '1'
end
describe 'with two-factor authentication', :js do
def enter_code(code)
fill_in 'user_otp_attempt', with: code
click_button 'Verify code'
end
it 'blocks login with invalid code' do
# TODO invalid 2FA code does not generate any events
# See gitlab-org/gitlab-ce#49785
context 'with valid username/password' do
let(:user) { create(:user, :two_factor) }
enter_code('foo')
before do
gitlab_sign_in(user, remember: true)
expect(page).to have_content('Invalid two-factor code')
expect(page).to have_content('Two-Factor Authentication')
end
it 'allows login with invalid code, then valid code' do
it 'does not show a "You are already signed in." error message' do
expect(authentication_metrics)
.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_single_session_with_authenticated_ttl
expect(current_path).to eq root_path
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
expect_single_session_with_authenticated_ttl(redis_store_class)
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
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)
end
end
context 'using backup code' do
let(:codes) { user.generate_otp_backup_codes! }
before do
expect(codes.size).to eq 10
# Ensure the generated codes get saved
user.save!(touch: false)
expect(page).to have_content('An error occurred. Please sign in again.')
end
context 'with valid code' do
it 'allows login' do
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(codes.sample)
enter_code(user.current_otp)
expect_single_session_with_authenticated_ttl(redis_store_class)
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)
it 'persists remember_me value via hidden field' do
field = first('input#user_remember_me', visible: false)
expect(field.value).to eq '1'
end
expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
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 'invalidates backup codes twice in a row' do
it 'allows login with invalid code, then valid code' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter).twice
.and increment(:user_two_factor_authenticated_counter).twice
.and increment(:user_session_destroyed_counter)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
random_code = codes.delete(codes.sample)
expect { enter_code(random_code) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
enter_code('foo')
expect(page).to have_content('Invalid two-factor code')
gitlab_sign_out
gitlab_sign_in(user)
enter_code(user.current_otp)
expect { enter_code(codes.sample) }
.to change { user.reload.otp_backup_codes.size }.by(-1)
expect_single_session_with_authenticated_ttl(redis_store_class)
expect(current_path).to eq root_path
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(codes.sample)
enter_code(user.current_otp)
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
context 'using backup code' do
let(:codes) { user.generate_otp_backup_codes! }
code = codes.sample
expect(user.invalidate_otp_backup_code!(code)).to eq true
before do
expect(codes.size).to eq 10
# Ensure the generated codes get saved
user.save!(touch: false)
expect(user.reload.otp_backup_codes.size).to eq 9
end
enter_code(code)
expect(page).to have_content('Invalid two-factor code.')
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
end
end
end
end
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
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
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])
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
enter_code(code)
expect(page).to have_content('Invalid two-factor code.')
end
end
end
end
context 'when authn_context is worth two factors' do
context 'when logging in via OAuth' do
let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
let(:mock_saml_response) do
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
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
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
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
sign_in_using_saml!
it 'signs user in without prompting for second factor' do
# TODO, OAuth authentication does not fire events,
# see gitlab-org/gitlab-ce#49786
expect_single_session_with_authenticated_ttl
expect(page).not_to have_content('Two-Factor Authentication')
expect(current_path).to eq root_path
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
expect(ActiveSession).to receive(:cleanup).with(user).once.and_call_original
sign_in_using_saml!
expect_single_session_with_authenticated_ttl(redis_store_class)
expect(page).not_to have_content('Two-Factor Authentication')
expect(current_path).to eq root_path
end
end
end
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
expect(current_path).to eq root_path
expect_single_session_with_authenticated_ttl(redis_store_class)
expect(current_path).to eq root_path
end
end
end
def sign_in_using_saml!
gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response)
def sign_in_using_saml!
gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response)
end
end
end
end
describe 'without two-factor authentication' do
context 'with correct username and password' do
let(:user) { create(:user) }
it 'allows basic login' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect_single_session_with_authenticated_ttl
expect(current_path).to eq root_path
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
describe 'without two-factor authentication' do
context 'with correct username and password' do
let(:user) { create(:user) }
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 'allows basic login' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
visit new_user_session_path
gitlab_sign_in(user)
expect_single_session_with_authenticated_ttl
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
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
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 '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)
end
gitlab_sign_in(user)
visit new_user_session_path
context 'when the users password is expired' do
before do
user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
expect_single_session_with_authenticated_ttl(redis_store_class)
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
it 'asks for a new password' do
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
visit new_user_session_path
gitlab_sign_in(user)
end
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
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
expect(current_path).to eq(new_profile_password_path)
it 'asks for a new password' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect(current_path).to eq(new_profile_password_path)
end
end
end
end
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
expect(page).to have_content('Invalid login or password.')
expect_single_session_with_short_ttl(redis_store_class)
expect(page).to have_content('Invalid login or password.')
end
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
before do
stub_application_setting(require_two_factor_authentication: true)
end
context 'with grace period defined' do
context 'global setting' do
before do
stub_application_setting(two_factor_grace_period: 48)
stub_application_setting(require_two_factor_authentication: true)
end
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
context 'with grace period defined' do
before do
stub_application_setting(two_factor_grace_period: 48)
end
gitlab_sign_in(user)
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
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 ')
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)
gitlab_sign_in(user)
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
end
end
it 'allows skipping two-factor configuration', :js do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
context 'after the grace period' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
gitlab_sign_in(user)
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
expect(current_path).to eq profile_two_factor_auth_path
click_link 'Configure it later'
expect(current_path).to eq root_path
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)
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 'after the grace period' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
context 'without grace period defined' do
before do
stub_application_setting(two_factor_grace_period: 0)
end
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
......@@ -523,54 +557,58 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state do
'The global settings require you to enable Two-Factor Authentication for your account.'
)
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 'without grace period defined' do
context 'group setting' do
before do
stub_application_setting(two_factor_grace_period: 0)
group1 = create :group, name: 'Group 1', require_two_factor_authentication: true
group1.add_user(user, GroupMember::DEVELOPER)
group2 = create :group, name: 'Group 2', require_two_factor_authentication: true
group2.add_user(user, GroupMember::DEVELOPER)
end
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
context 'with grace period defined' do
before do
stub_application_setting(two_factor_grace_period: 48)
end
gitlab_sign_in(user)
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
freeze_time do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content(
'The group settings for Group 1 and Group 2 require you to enable '\
'Two-Factor Authentication for your account. '\
'You can leave Group 1 and leave Group 2. '\
'You need to do this '\
'before '\
"#{(Time.zone.now + 2.days).strftime("%a, %d %b %Y %H:%M:%S %z")}"
)
end
end
expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content(
'The global settings require you to enable Two-Factor Authentication for your account.'
)
end
end
end
it 'allows skipping two-factor configuration', :js do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
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
gitlab_sign_in(user)
context 'with grace period defined' do
before do
stub_application_setting(two_factor_grace_period: 48)
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 'within the grace period' do
it 'redirects to two-factor configuration page' do
freeze_time do
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)
......@@ -578,30 +616,27 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state 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. '\
'You need to do this '\
'before '\
"#{(Time.zone.now + 2.days).strftime("%a, %d %b %Y %H:%M:%S %z")}"
'The group settings for Group 1 and Group 2 require you to enable ' \
'Two-Factor Authentication for your account.'
)
end
end
it 'allows skipping two-factor configuration', :js do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
it 'disallows 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
click_link 'Configure it later'
expect(current_path).to eq root_path
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
end
end
context 'after the grace period' do
let(:user) { create(:user, otp_grace_period_started_at: 9999.hours.ago) }
context 'without grace period defined' do
before do
stub_application_setting(two_factor_grace_period: 0)
end
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
......@@ -612,234 +647,230 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state 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.'
'Two-Factor Authentication for your account. '\
'You can leave Group 1 and leave Group 2.'
)
end
end
end
end
it 'disallows skipping two-factor configuration', :js do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
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
gitlab_sign_in(user)
ensure_no_tabs
end
expect(current_path).to eq profile_two_factor_auth_path
expect(page).not_to have_link('Configure it later')
end
it 'renders link to sign up path' do
visit new_user_session_path
expect(page.body).to have_link('Register now', href: new_user_registration_path)
end
end
context 'without grace period defined' do
context 'when signup is disabled' do
before do
stub_application_setting(two_factor_grace_period: 0)
stub_application_setting(signup_enabled: false)
visit new_user_session_path
end
it 'redirects to two-factor configuration page' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
it 'does not render any tabs' do
ensure_no_tabs
end
gitlab_sign_in(user)
it 'does not render link to sign up path' do
visit new_user_session_path
expect(current_path).to eq profile_two_factor_auth_path
expect(page).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.'
)
expect(page.body).not_to have_link('Register now', href: new_user_registration_path)
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
context 'when ldap is enabled' do
include LdapHelpers
ensure_no_tabs
end
it 'renders link to sign up path' 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
expect(page.body).to have_link('Register now', href: new_user_registration_path)
end
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])
context 'when signup is disabled' do
before do
stub_application_setting(signup_enabled: false)
Ldap::OmniauthCallbacksController.define_providers!
Rails.application.reload_routes!
visit new_user_session_path
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
it 'does not render any tabs' do
ensure_no_tabs
end
visit new_user_session_path
end
it 'does not render link to sign up path' do
visit new_user_session_path
it 'correctly renders tabs and panes' do
ensure_tab_pane_correctness(['Main LDAP', 'Standard'])
end
expect(page.body).not_to have_link('Register now', href: new_user_registration_path)
it 'renders link to sign up path' do
expect(page.body).to have_link('Register now', href: new_user_registration_path)
end
end
end
context 'when ldap is enabled' do
include LdapHelpers
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
context 'when crowd is enabled' do
before do
allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:crowd])
stub_application_setting(crowd_enabled: true)
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])
Ldap::OmniauthCallbacksController.define_providers!
Rails.application.reload_routes!
Ldap::OmniauthCallbacksController.define_providers!
Rails.application.reload_routes!
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
allow_next_instance_of(ActionDispatch::Routing::RoutesProxy) do |instance|
allow(instance).to receive(:"user_#{provider}_omniauth_callback_path")
.and_return("/users/auth/#{provider}/callback")
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'])
it 'correctly renders tabs and panes' do
ensure_tab_pane_correctness(%w(Crowd Standard))
end
end
end
it 'renders link to sign up path' do
expect(page.body).to have_link('Register now', href: new_user_registration_path)
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
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 terms are enforced', :js do
let(:user) { create(:user) }
Ldap::OmniauthCallbacksController.define_providers!
Rails.application.reload_routes!
before do
enforce_terms
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
it 'asks to accept the terms on first login' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path
end
it 'correctly renders tabs and panes' do
ensure_tab_pane_correctness(%w(Crowd Standard))
end
end
end
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
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 'Sign in'
context 'when terms are enforced', :js do
let(:user) { create(:user) }
expect_to_be_on_terms_page
before do
enforce_terms
end
click_button 'Accept terms'
it 'asks to accept the terms on first login' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
expect(current_path).to eq(root_path)
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
visit new_user_session_path
it 'does not ask for terms when the user already accepted them' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
accept_terms(user)
click_button 'Sign in'
visit new_user_session_path
expect_to_be_on_terms_page
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Accept terms'
click_button 'Sign in'
expect(current_path).to eq(root_path)
expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
expect(current_path).to eq(root_path)
end
it 'does not ask for terms when the user already accepted them' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
context 'when 2FA is required for the user' do
before do
group = create(:group, require_two_factor_authentication: true)
group.add_developer(user)
end
accept_terms(user)
context 'when the user did not enable 2FA' do
it 'asks to set 2FA before asking to accept the terms' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_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(current_path).to eq(root_path)
end
expect_to_be_on_terms_page
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(profile_two_factor_auth_path)
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)
fill_in 'pin_code', with: user.reload.current_otp
fill_in 'current_password', with: user.password
visit new_user_session_path
click_button 'Register with two-factor app'
click_button 'Copy codes'
click_link 'Proceed'
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
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
click_button 'Sign in'
context 'when the user already enabled 2FA' do
before do
user.update!(otp_required_for_login: true,
otp_secret: User.generate_otp_secret(32))
end
expect_to_be_on_terms_page
click_button 'Accept terms'
it 'asks the user to accept the terms' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
expect(current_path).to eq(profile_two_factor_auth_path)
visit new_user_session_path
fill_in 'pin_code', with: user.reload.current_otp
fill_in 'current_password', with: user.password
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
click_button 'Register with two-factor app'
click_button 'Copy codes'
click_link 'Proceed'
fill_in 'user_otp_attempt', with: user.reload.current_otp
click_button 'Verify code'
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.')
expect_to_be_on_terms_page
click_button 'Accept terms'
expect(current_path).to eq(root_path)
end
end
end
context 'when the user already enabled 2FA' do
context 'when the users password is expired' do
before do
user.update!(otp_required_for_login: true,
otp_secret: User.generate_otp_secret(32))
user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
end
it 'asks the user to accept the terms' do
it 'asks the user to accept the terms before setting a new password' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
.and increment(:user_two_factor_authenticated_counter)
visit new_user_session_path
......@@ -847,109 +878,86 @@ RSpec.describe 'Login', :clean_gitlab_redis_shared_state 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(root_path)
expect(current_path).to eq(new_profile_password_path)
fill_in 'user_password', with: '12345678'
fill_in 'user_new_password', with: 'new password'
fill_in 'user_password_confirmation', with: 'new password'
click_button 'Set new password'
expect(page).to have_content('Password successfully changed')
end
end
end
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 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') }
it 'asks the user to accept the terms before setting a new password' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
before do
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
end
visit new_user_session_path
it 'asks the user to accept the terms before setting an email' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
gitlab_sign_in_via('saml', user, 'my-uid')
expect_to_be_on_terms_page
click_button 'Accept terms'
expect_to_be_on_terms_page
click_button 'Accept terms'
expect(current_path).to eq(new_profile_password_path)
expect(current_path).to eq(profile_path)
fill_in 'user_password', with: '12345678'
fill_in 'user_new_password', with: 'new password'
fill_in 'user_password_confirmation', with: 'new password'
click_button 'Set new password'
fill_in 'Email', with: 'hello@world.com'
expect(page).to have_content('Password successfully changed')
click_button 'Update profile settings'
expect(page).to have_content('Profile was successfully updated')
end
end
end
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') }
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" }
before do
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
stub_application_setting(send_user_confirmation_email: true)
stub_feature_flags(soft_email_confirmation: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
end
it 'asks the user to accept the terms before setting an email' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
gitlab_sign_in_via('saml', user, 'my-uid')
expect_to_be_on_terms_page
click_button 'Accept terms'
expect(current_path).to eq(profile_path)
fill_in 'Email', with: 'hello@world.com'
it 'allows login and shows a flash warning to confirm the email address' do
expect(authentication_metrics).to increment(:user_authenticated_counter)
click_button 'Update profile settings'
gitlab_sign_in(user)
expect(page).to have_content('Profile was successfully updated')
expect(current_path).to eq root_path
expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address and unlock the power of CI/CD.")
end
end
end
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" }
before do
stub_application_setting(send_user_confirmation_email: true)
stub_feature_flags(soft_email_confirmation: true)
allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
end
it 'allows login and shows a flash warning to confirm the email address' do
expect(authentication_metrics).to increment(:user_authenticated_counter)
gitlab_sign_in(user)
expect(current_path).to eq root_path
expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address and unlock the power of CI/CD.")
end
context "when not having confirmed within Devise's allow_unconfirmed_access_for time" do
it 'does not allow login and shows a flash alert to confirm the email address', :js do
travel_to((grace_period + 1.day).from_now) do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
context "when not having confirmed within Devise's allow_unconfirmed_access_for time" do
it 'does not allow login and shows a flash alert to confirm the email address', :js do
travel_to((grace_period + 1.day).from_now) do
expect(authentication_metrics)
.to increment(:user_unauthenticated_counter)
.and increment(:user_session_destroyed_counter).twice
gitlab_sign_in(user)
gitlab_sign_in(user)
expect(current_path).to eq new_user_session_path
expect(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)
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
end
it_behaves_like 'redis sessions store', 'login'
end
......@@ -10,25 +10,37 @@ RSpec.describe 'Session initializer for GitLab' do
end
describe 'config#session_store' do
context 'when the GITLAB_REDIS_STORE_WITH_SESSION_STORE env is not set' do
context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is not set' do
before do
stub_env('GITLAB_REDIS_STORE_WITH_SESSION_STORE', nil)
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', nil)
end
it 'initialized as a redis_store with a proper Redis::Store instance' do
it 'initialized with Multistore as ENV var defaults to true' do
expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
load_session_store
end
end
context 'when the GITLAB_REDIS_STORE_WITH_SESSION_STORE env is disabled' do
context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is disabled' do
before do
stub_env('GITLAB_REDIS_STORE_WITH_SESSION_STORE', false)
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', false)
end
it 'initialized as a redis_store with a proper servers configuration' do
expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(servers: kind_of(Hash)))
expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(Redis::Store)))
load_session_store
end
end
context 'when the GITLAB_USE_REDIS_SESSIONS_STORE env is enabled' do
before do
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', true)
end
it 'initialized as a redis_store with a proper servers configuration' do
expect(subject).to receive(:session_store).with(:redis_store, a_hash_including(redis_store: kind_of(::Redis::Store)))
load_session_store
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do
RSpec.describe Gitlab::AnonymousSession do
let(:default_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
let(:additional_session_id) { '7919a6f1bb119dd7396fadc38fd18d0d' }
......@@ -12,56 +12,60 @@ RSpec.describe Gitlab::AnonymousSession, :clean_gitlab_redis_shared_state do
described_class.new('127.0.0.1')
end
describe '#store_session_ip' do
it 'adds session id to proper key' do
subject.count_session_ip
RSpec.shared_examples_for 'anonymous sessions' do
describe '#store_session_ip' do
it 'adds session id to proper key' do
subject.count_session_ip
Gitlab::Redis::SharedState.with do |redis|
expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq 1
redis_store_class.with do |redis|
expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq 1
end
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
Gitlab::Redis::SharedState.with do |redis|
expect(redis.ttl("session:lookup:ip:gitlab2:127.0.0.1")).to eq(24.hours.to_i)
redis_store_class.with do |redis|
expect(redis.ttl("session:lookup:ip:gitlab2:127.0.0.1")).to eq(24.hours.to_i)
end
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
Gitlab::Redis::SharedState.with do |redis|
expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq(2)
redis_store_class.with do |redis|
expect(redis.get("session:lookup:ip:gitlab2:127.0.0.1").to_i).to eq(2)
end
end
end
end
end
describe '#stored_sessions' do
it 'returns all anonymous sessions per ip' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:lookup:ip:gitlab2:127.0.0.1", 2)
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)
expect(subject.session_count).to eq(2)
end
end
end
it 'removes obsolete lookup through ip entries' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:lookup:ip:gitlab2:127.0.0.1", 2)
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
subject.cleanup_session_per_ip_count
subject.cleanup_session_per_ip_count
Gitlab::Redis::SharedState.with do |redis|
expect(redis.exists("session:lookup:ip:gitlab2:127.0.0.1")).to eq(false)
redis_store_class.with do |redis|
expect(redis.exists("session:lookup:ip:gitlab2:127.0.0.1")).to eq(false)
end
end
end
it_behaves_like 'redis sessions store', 'anonymous sessions'
end
......@@ -4,4 +4,54 @@ require 'spec_helper'
RSpec.describe Gitlab::Redis::Sessions do
include_examples "redis_new_instance_shared_examples", 'sessions', Gitlab::Redis::SharedState
describe 'redis instance used in connection pool' do
before do
clear_pool
end
context 'when redis.sessions configuration is not provided' do
it 'uses ::Redis instance' do
expect(described_class).to receive(:config_fallback?).and_return(true)
described_class.pool.with do |redis_instance|
expect(redis_instance).to be_instance_of(::Redis)
end
end
end
context 'when redis.sessions configuration is provided' do
it 'instantiates an instance of MultiStore' do
expect(described_class).to receive(:config_fallback?).and_return(false)
described_class.pool.with do |redis_instance|
expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
end
end
end
def clear_pool
described_class.remove_instance_variable(:@pool)
rescue NameError
# raised if @pool was not set; ignore
end
end
describe '#store' do
subject { described_class.store(namespace: described_class::SESSION_NAMESPACE) }
context 'when redis.sessions configuration is provided' do
it 'instantiates ::Redis instance' do
expect(described_class).to receive(:config_fallback?).and_return(true)
expect(subject).to be_instance_of(::Redis::Store)
end
end
context 'when redis.sessions configuration is not provided' do
it 'instantiates an instance of MultiStore' do
expect(described_class).to receive(:config_fallback?).and_return(false)
expect(subject).to be_instance_of(::Gitlab::Redis::MultiStore)
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
RSpec.describe ActiveSession do
let(:user) do
create(:user).tap do |user|
user.current_sign_in_at = Time.current
......@@ -21,457 +21,461 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
})
end
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)
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)
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
expect(active_session.current?(session)).to be false
end
end
end
describe '.list' do
it 'returns all sessions by user' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' }))
redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '')
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
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", '')
expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
end
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
it 'does not return obsolete entries and cleans them up' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }]
end
expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
it 'does not return obsolete entries and cleans them up' do
redis_store_class.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' }))
Gitlab::Redis::SharedState.with do |redis|
expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
end
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
6919a6f1bb119dd7396fadc38fd18d0d
59822c7d9fcdfa03725eff41782ad97d
]
)
end
it 'returns an empty array if the use does not have any active session' do
expect(ActiveSession.list(user)).to eq []
end
end
expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }]
describe '.list_sessions' do
it 'uses the ActiveSession lookup to return original sessions' do
Gitlab::Redis::SharedState.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
]
)
redis_store_class.with do |redis|
expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
end
expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
it 'returns an empty array if the use does not have any active session' do
expect(ActiveSession.list(user)).to eq []
end
end
end
describe '.session_ids_for_user' do
it 'uses the user lookup table to return session ids' do
session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
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' }))
Gitlab::Redis::SharedState.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids)
end
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
%w[
2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae
2::d2ee6f70d6ef0e8701efa3f6b281cbe8e6bf3d109ef052a8b5ce88bfc7e71c26
]
)
end
expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to eq(session_ids)
expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
end
end
end
describe '.sessions_from_ids' do
it 'uses the ActiveSession lookup to return original sessions' do
Gitlab::Redis::SharedState.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
describe '.session_ids_for_user' do
it 'uses the user lookup table to return session ids' do
session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
expect(ActiveSession.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }]
redis_store_class.with do |redis|
redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids)
end
expect(ActiveSession.session_ids_for_user(user.id).map(&:to_s)).to eq(session_ids)
end
end
it 'avoids a redis lookup for an empty array' do
expect(Gitlab::Redis::SharedState).not_to receive(:with)
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
expect(ActiveSession.sessions_from_ids([])).to eq([])
end
expect(ActiveSession.sessions_from_ids([rack_session.private_id])).to eq [{ _csrf_token: 'abcd' }]
end
it 'uses redis lookup in batches' do
stub_const('ActiveSession::SESSION_BATCH_SIZE', 1)
it 'avoids a redis lookup for an empty array' do
expect(redis_store_class).not_to receive(:with)
redis = double(:redis)
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
expect(ActiveSession.sessions_from_ids([])).to eq([])
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)
it 'uses redis lookup in batches' do
stub_const('ActiveSession::SESSION_BATCH_SIZE', 1)
expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions)
end
end
redis = double(:redis)
expect(redis_store_class).to receive(:with).and_yield(redis)
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' do
ActiveSession.set(user, request)
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)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each.to_a).to include(
"session:user:gitlab:#{user.id}:2::418729c72310bbf349a032f0bb6e3fce9f5a69df8f000d8ae0ac5d159d8f21ae",
"session:lookup:user:gitlab:#{user.id}"
)
expect(ActiveSession.sessions_from_ids([1, 2])).to eql(sessions)
end
end
it 'adds timestamps and information from the request' do
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' 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')
)
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
end
end
it 'keeps the created_at from the login on consecutive requests' do
now = Time.zone.parse('2018-03-12 09:06')
Timecop.freeze(now) do
ActiveSession.set(user, request)
Timecop.freeze(now + 1.minute) do
it 'adds timestamps and information from the request' do
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
ActiveSession.set(user, request)
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:07')
updated_at: Time.zone.parse('2018-03-12 09:06')
)
end
end
end
end
describe '.destroy_session' do
shared_examples 'removes all session data' do
before do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:#{active_session_lookup_key}", '')
# Emulate redis-rack: https://github.com/redis-store/redis-rack/blob/c75f7f1a6016ee224e2615017fbfee964f23a837/lib/rack/session/redis.rb#L88
redis.set("session:gitlab:#{rack_session.private_id}", '')
it 'keeps the created_at from the login on consecutive requests' do
now = Time.zone.parse('2018-03-12 09:06')
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)
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
it 'removes the devise session' do
subject
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}", '')
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
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
end
it 'removes the lookup entry' do
subject
it 'removes the devise session' do
subject
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
redis_store_class.with do |redis|
expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty
end
end
it 'removes the lookup entry' do
subject
redis_store_class.with do |redis|
expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty
end
end
end
it 'removes the ActiveSession' do
subject
it 'removes the ActiveSession' do
subject
Gitlab::Redis::SharedState.with do |redis|
expect(redis.scan_each(match: "session:user:gitlab:*").to_a).to be_empty
redis_store_class.with do |redis|
expect(redis.scan_each(match: "session:user:gitlab:*").to_a).to be_empty
end
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'
include_examples 'removes all session data'
end
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)
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
ActiveSession.destroy_all_but_current(user, nil)
end
context 'with user sessions' do
let(:current_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
context 'with user sessions' do
let(:current_session_id) { '6919a6f1bb119dd7396fadc38fd18d0d' }
before do
Gitlab::Redis::SharedState.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
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
# setup for unrelated user
unrelated_user_id = 9999
session_private_id = Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358').private_id
active_session = ActiveSession.new(session_private_id: session_private_id)
redis.set(described_class.key_name(user.id, session_private_id),
redis.set(described_class.key_name(unrelated_user_id, session_private_id),
Marshal.dump(active_session))
redis.sadd(described_class.lookup_key_name(user.id),
redis.sadd(described_class.lookup_key_name(unrelated_user_id),
session_private_id)
end
# setup for unrelated user
unrelated_user_id = 9999
session_private_id = Rack::Session::SessionId.new('5c8611e4f9c69645ad1a1492f4131358').private_id
active_session = ActiveSession.new(session_private_id: session_private_id)
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 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))
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))
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
end
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
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 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)
Gitlab::Redis::SharedState.with do |redis|
expect(
redis.smembers(described_class.lookup_key_name(user.id))
).to eq([session_private_id])
redis_store_class.with do |redis|
expect(
redis.smembers(described_class.lookup_key_name(user.id))
).to eq([session_private_id])
end
end
end
it 'does not remove impersonated sessions' do
impersonated_session_id = '6919a6f1bb119dd7396fadc38fd18eee'
Gitlab::Redis::SharedState.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 '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
expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2)
expect { ActiveSession.destroy_all_but_current(user, request.session) }.to change { ActiveSession.session_ids_for_user(user.id).size }.from(3).to(2)
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
expect(ActiveSession.session_ids_for_user(9999).size).to eq(1)
end
end
end
end
describe '.cleanup' do
before do
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
end
it 'removes obsolete lookup entries' do
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
describe '.cleanup' do
before do
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
end
ActiveSession.cleanup(user)
it 'removes obsolete lookup entries' do
redis_store_class.with do |redis|
redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d')
redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d')
end
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
redis_store_class.with do |redis|
expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d']
end
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
Gitlab::Redis::SharedState.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}"
)
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
end
end
end
it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user)
it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
redis_store_class.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}")
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
it 'removes obsolete lookup entries' do
ActiveSession.cleanup(user)
it 'removes obsolete lookup entries' do
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
redis_store_class.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)
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
end
it 'removes obsolete lookup entries even without active session' do
Gitlab::Redis::SharedState.with do |redis|
redis.sadd(
"session:lookup:user:gitlab:#{user.id}",
"#{max_number_of_sessions_plus_two + 1}"
)
end
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
ActiveSession.cleanup(user)
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
redis_store_class.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
)
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
end
context 'when the number of active sessions is lower than the limit' do
before do
Gitlab::Redis::SharedState.with do |redis|
((max_number_of_sessions_plus_two - 4)..max_number_of_sessions_plus_two).each do |number|
redis.del("session:user:gitlab:#{user.id}:#{number}")
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
end
end
end
it 'does not remove active session entries, but removes lookup entries' do
lookup_entries_before_cleanup = Gitlab::Redis::SharedState.with do |redis|
redis.smembers("session:lookup:user:gitlab:#{user.id}")
end
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
sessions_before_cleanup = Gitlab::Redis::SharedState.with do |redis|
redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
end
sessions_before_cleanup = redis_store_class.with do |redis|
redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
end
ActiveSession.cleanup(user)
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
lookup_entries = redis.smembers("session:lookup:user:gitlab:#{user.id}")
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
expect(sessions.count).to eq(sessions_before_cleanup.count)
expect(lookup_entries.count).to be < lookup_entries_before_cleanup.count
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
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
Gitlab::Redis::SharedState.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}"
)
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
end
end
end
it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user)
it 'removes obsolete active sessions entries' do
ActiveSession.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
sessions = redis.scan_each(match: "session:user:gitlab:#{user.id}:*").to_a
redis_store_class.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}"))
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
end
it_behaves_like 'redis sessions store', 'active session'
end
......@@ -376,24 +376,28 @@ RSpec.describe API::Commits do
end
end
context 'when using warden' do
it 'increments usage counters', :clean_gitlab_redis_shared_state do
session_id = Rack::Session::SessionId.new('6919a6f1bb119dd7396fadc38fd18d0d')
session_hash = { 'warden.user.user.key' => [[user.id], user.encrypted_password[0, 29]] }
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
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
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
expect(::Gitlab::UsageDataCounters::WebIdeCounter).to receive(:increment_commits_count)
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_web_ide_edit_action)
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
post api(url), params: valid_c_params
end
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
expect_single_session_with_expiration(Settings.gitlab['session_expire_delay'] * 60)
def expect_single_session_with_authenticated_ttl(redis_store_class)
expect_single_session_with_expiration(redis_store_class, Settings.gitlab['session_expire_delay'] * 60)
end
def expect_single_session_with_short_ttl
expect_single_session_with_expiration(Settings.gitlab['unauthenticated_session_expire_delay'])
def expect_single_session_with_short_ttl(redis_store_class)
expect_single_session_with_expiration(redis_store_class, Settings.gitlab['unauthenticated_session_expire_delay'])
end
def expect_single_session_with_expiration(expiration)
session_keys = get_session_keys
def expect_single_session_with_expiration(redis_store_class, expiration)
session_keys = get_session_keys(redis_store_class)
expect(session_keys.size).to eq(1)
expect(get_ttl(session_keys.first)).to be_within(5).of(expiration)
expect(get_ttl(redis_store_class, session_keys.first)).to be_within(5).of(expiration)
end
def get_session_keys
Gitlab::Redis::SharedState.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a }
def get_session_keys(redis_store_class)
redis_store_class.with { |redis| redis.scan_each(match: 'session:gitlab:*').to_a }
end
def get_ttl(key)
Gitlab::Redis::SharedState.with { |redis| redis.ttl(key) }
def get_ttl(redis_store_class, key)
redis_store_class.with { |redis| redis.ttl(key) }
end
end
......@@ -93,18 +93,23 @@ RSpec.shared_examples "redis_shared_examples" do
subject { described_class.new(rails_env).store }
shared_examples 'redis store' do
let(:redis_store) { ::Redis::Store }
let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database}" }
it 'instantiates Redis::Store' do
is_expected.to be_a(::Redis::Store)
expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database}")
is_expected.to be_a(redis_store)
expect(subject.to_s).to eq(redis_store_to_s)
end
context 'with the namespace' do
let(:namespace) { 'namespace_name' }
let(:redis_store_to_s) { "Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}" }
subject { described_class.new(rails_env).store(namespace: namespace) }
it "uses specified namespace" do
expect(subject.to_s).to eq("Redis Client connected to #{host} against DB #{redis_database} with namespace #{namespace}")
expect(subject.to_s).to eq(redis_store_to_s)
end
end
end
......
# frozen_string_literal: true
RSpec.shared_examples 'redis sessions store' do |example|
context 'when ENV[GITLAB_USE_REDIS_SESSIONS_STORE] is true', :clean_gitlab_redis_sessions do
before do
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', 'true')
end
it_behaves_like example do
let(:redis_store_class) { Gitlab::Redis::Sessions }
end
end
context 'when ENV[GITLAB_USE_REDIS_SESSIONS_STORE] is false', :clean_gitlab_redis_shared_state do
before do
stub_env('GITLAB_USE_REDIS_SESSIONS_STORE', 'false')
end
it_behaves_like example do
let(:redis_store_class) { Gitlab::Redis::SharedState }
end
end
end
......@@ -18,32 +18,36 @@ RSpec.shared_examples 'snippet edit usage data counters' do
end
end
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]] }
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]] }
Gitlab::Redis::SharedState.with do |redis|
redis.set("session:gitlab:#{session_id.private_id}", Marshal.dump(session_hash))
end
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
end
cookies[Gitlab::Application.config.session_options[:key]] = session_id.public_id
end
it 'tracks usage data actions', :clean_gitlab_redis_shared_state do
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action)
it 'tracks usage data actions' do
expect(::Gitlab::UsageDataCounters::EditorUniqueCounter).to receive(:track_snippet_editor_edit_action)
post_graphql_mutation(mutation)
end
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
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)
post_graphql_mutation(mutation)
end
end
end
end
it_behaves_like 'redis sessions store', 'sessionless user'
end
......@@ -3,195 +3,199 @@
require 'rake_helper'
RSpec.describe 'gitlab:cleanup rake tasks', :silence_stdout 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) }
RSpec.shared_examples_for 'rake gitlab:cleanup' do
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)
Rake.application.rake_require 'tasks/gitlab/cleanup'
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) }
# 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
FileUtils.mkdir_p(File.dirname(path))
FileUtils.mv(new_path, path)
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
context 'with DRY_RUN disabled' do
context 'with a fixable orphaned project upload file' do
let(:orphaned) { create(:upload, :issuable_upload, :with_file, model: build(:project, :legacy_storage)) }
let(:new_path) { orphaned.absolute_path }
let(:path) { File.join(FileUploader.root, 'some', 'wrong', 'location', orphaned.path) }
before do
stub_env('DRY_RUN', 'false')
FileUtils.mkdir_p(File.dirname(path))
FileUtils.mv(new_path, path)
end
it 'moves the file to its proper location' do
run_rake_task('gitlab:cleanup:project_uploads')
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')
expect(File.exist?(path)).to be_falsey
expect(File.exist?(new_path)).to be_truthy
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
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}")
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/)
run_rake_task('gitlab:cleanup:project_uploads')
run_rake_task('gitlab:cleanup:project_uploads')
end
end
end
shared_examples_for 'does not move the file' do
it 'does not move the file' do
run_rake_task('gitlab:cleanup:project_uploads')
context 'with DRY_RUN explicitly enabled' do
before do
stub_env('DRY_RUN', 'true')
end
expect(File.exist?(path)).to be_truthy
expect(File.exist?(new_path)).to be_falsey
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 set to an unknown value' do
before do
stub_env('DRY_RUN', 'foo')
end
run_rake_task('gitlab:cleanup:project_uploads')
it_behaves_like 'does not move the file'
end
end
context 'with DRY_RUN explicitly enabled' do
before do
stub_env('DRY_RUN', 'true')
context 'with DRY_RUN unset' do
it_behaves_like 'does not move the file'
end
end
end
describe 'gitlab:cleanup:orphan_job_artifact_files' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_job_artifact_files') }
it 'runs the task without errors' do
expect(Gitlab::Cleanup::OrphanJobArtifactFiles)
.to receive(:new).and_call_original
it_behaves_like 'does not move the file'
expect { rake_task }.not_to raise_error
end
context 'with DRY_RUN set to an unknown value' do
context 'with DRY_RUN set to false' do
before do
stub_env('DRY_RUN', 'foo')
stub_env('DRY_RUN', 'false')
end
it_behaves_like 'does not move the file'
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
context 'with DRY_RUN unset' do
it_behaves_like 'does not move the file'
rake_task
end
end
end
end
describe 'gitlab:cleanup:orphan_job_artifact_files' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_job_artifact_files') }
describe 'gitlab:cleanup:orphan_lfs_file_references' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_file_references') }
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
let(:project) { create(:project, :repository) }
context 'with DRY_RUN set to false' do
before do
stub_env('DRY_RUN', 'false')
stub_env('PROJECT_ID', project.id)
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 'runs the task without errors' do
expect(Gitlab::Cleanup::OrphanLfsFileReferences)
.to receive(:new).and_call_original
rake_task
expect { rake_task }.not_to raise_error
end
end
end
describe 'gitlab:cleanup:orphan_lfs_file_references' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_file_references') }
let(:project) { create(:project, :repository) }
before do
stub_env('PROJECT_ID', project.id)
end
context 'with DRY_RUN set to false' do
before do
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::OrphanLfsFileReferences)
.to receive(:new)
.with(project,
dry_run: false,
logger: anything)
.and_call_original
expect { rake_task }.not_to raise_error
rake_task
end
end
end
context 'with DRY_RUN set to false' do
before do
stub_env('DRY_RUN', 'false')
end
describe 'gitlab:cleanup:orphan_lfs_files' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_files') }
it 'passes dry_run correctly' do
expect(Gitlab::Cleanup::OrphanLfsFileReferences)
.to receive(:new)
.with(project,
dry_run: false,
logger: anything)
it 'runs RemoveUnreferencedLfsObjectsWorker' do
expect_any_instance_of(RemoveUnreferencedLfsObjectsWorker)
.to receive(:perform)
.and_call_original
rake_task
end
end
end
describe 'gitlab:cleanup:orphan_lfs_files' do
subject(:rake_task) { run_rake_task('gitlab:cleanup:orphan_lfs_files') }
it 'runs RemoveUnreferencedLfsObjectsWorker' do
expect_any_instance_of(RemoveUnreferencedLfsObjectsWorker)
.to receive(:perform)
.and_call_original
rake_task
end
end
context 'sessions' do
describe 'gitlab:cleanup:sessions:active_sessions_lookup_keys', :clean_gitlab_redis_shared_state do
subject(:rake_task) { run_rake_task('gitlab:cleanup:sessions:active_sessions_lookup_keys') }
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') }
let!(:user) { create(:user) }
let(:existing_session_id) { '5' }
let!(:user) { create(:user) }
let(:existing_session_id) { '5' }
before do
Gitlab::Redis::SharedState.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)
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
end
it 'runs the task without errors' do
expect { rake_task }.not_to raise_error
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::SharedState.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]))
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
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