Add e2e spec for logging in with 2FA

Also add required page objects and qa selectors
parent 4aca628f
......@@ -8,10 +8,10 @@
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
= f.label 'Two-Factor Authentication code', name: :otp_attempt
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.', data: { qa_selector: 'two_fa_code_field' }
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-success"
= f.submit "Verify code", class: "btn btn-success", data: { qa_selector: 'verify_code_button' }
- if @user.two_factor_u2f_enabled?
= render "u2f/authenticate", params: params, resource: resource, resource_name: resource_name, render_remember_me: true, target_path: new_user_session_path
......@@ -9,5 +9,5 @@
%span.monospace= code
.d-flex
= link_to _('Proceed'), profile_account_path, class: 'btn btn-success append-right-10'
= link_to _('Proceed'), profile_account_path, class: 'btn btn-success append-right-10', data: { qa_selector: 'proceed_button' }
= link_to _('Download codes'), "data:text/plain;charset=utf-8,#{CGI.escape(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'btn btn-default'
......@@ -39,7 +39,7 @@
= _('To add the entry manually, provide the following details to the application on your phone.')
%p.gl-mt-0.gl-mb-0
= _('Account: %{account}') % { account: @account_string }
%p.gl-mt-0.gl-mb-0
%p.gl-mt-0.gl-mb-0{ data: { qa_selector: 'otp_secret_content' } }
= _('Key: %{key}') %{ key: current_user.otp_secret.scan(/.{4}/).join(' ') }
%p.two-factor-new-manual-content
= _('Time based: Yes')
......@@ -49,9 +49,9 @@
= @error
.form-group
= label_tag :pin_code, _('Pin code'), class: "label-bold"
= text_field_tag :pin_code, nil, class: "form-control", required: true
= text_field_tag :pin_code, nil, class: "form-control", required: true, data: { qa_selector: 'pin_code_field' }
.gl-mt-3
= submit_tag _('Register with two-factor app'), class: 'btn btn-success'
= submit_tag _('Register with two-factor app'), class: 'btn btn-success', data: { qa_selector: 'register_2fa_app_button' }
%hr
......
......@@ -15,6 +15,7 @@ gem 'rspec_junit_formatter', '~> 0.4.1'
gem 'faker', '~> 1.6', '>= 1.6.6'
gem 'knapsack', '~> 1.17'
gem 'parallel_tests', '~> 2.29'
gem 'rotp', '~> 3.1.0'
group :test do
gem 'pry-byebug', '~> 3.5.1', platform: :mri
......
......@@ -78,6 +78,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rotp (3.1.0)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
......@@ -129,6 +130,7 @@ DEPENDENCIES
pry-byebug (~> 3.5.1)
rake (~> 12.3.0)
rest-client (~> 2.1.0)
rotp (~> 3.1.0)
rspec (~> 3.7)
rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1)
......
......@@ -182,6 +182,7 @@ module QA
autoload :Login, 'qa/page/main/login'
autoload :Menu, 'qa/page/main/menu'
autoload :OAuth, 'qa/page/main/oauth'
autoload :TwoFactorAuth, 'qa/page/main/two_factor_auth'
autoload :SignUp, 'qa/page/main/sign_up'
autoload :Terms, 'qa/page/main/terms'
end
......@@ -563,6 +564,7 @@ module QA
autoload :Retrier, 'qa/support/retrier'
autoload :Waiter, 'qa/support/waiter'
autoload :WaitForRequests, 'qa/support/wait_for_requests'
autoload :OTP, 'qa/support/otp'
end
end
......
......@@ -22,9 +22,9 @@ module QA
end
end
def sign_in(as: nil, address: :gitlab)
def sign_in(as: nil, address: :gitlab, skip_page_validation: false)
Runtime::Browser.visit(address, Page::Main::Login)
Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: as) }
Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: as, skip_page_validation: skip_page_validation) }
end
def sign_in_as_admin(address: :gitlab)
......
# frozen_string_literal: true
module QA
module Page
module Main
class TwoFactorAuth < Page::Base
view 'app/views/devise/sessions/two_factor.html.haml' do
element :verify_code_button
element :two_fa_code_field
end
def click_verify_code_button
click_element :verify_code_button
end
def set_2fa_code(code)
fill_element(:two_fa_code_field, code)
end
end
end
end
end
......@@ -8,9 +8,35 @@ module QA
element :configure_it_later_button
end
view 'app/views/profiles/two_factor_auths/show.html.haml' do
element :otp_secret_content
element :pin_code_field
element :register_2fa_app_button
end
view 'app/views/profiles/two_factor_auths/_codes.html.haml' do
element :proceed_button
end
def click_configure_it_later_button
click_element :configure_it_later_button
end
def otp_secret_content
find_element(:otp_secret_content).text.gsub('Key:', '').delete(' ')
end
def set_pin_code(pin_code)
fill_element(:pin_code_field, pin_code)
end
def click_register_2fa_app_button
click_element :register_2fa_app_button
end
def click_proceed_button
click_element :proceed_button
end
end
end
end
......
......@@ -59,6 +59,10 @@ module QA
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end
def api_put_path
"/groups/#{id}"
end
def api_post_path
'/groups'
end
......@@ -75,6 +79,15 @@ module QA
def api_delete_path
"/groups/#{id}"
end
def set_require_two_factor_authentication(value:)
put_body = { require_two_factor_authentication: value }
response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
unless response.code == HTTP_STATUS_OK
raise ResourceUpdateFailedError, "Could not update require_two_factor_authentication to #{value}. Request returned (#{response.code}): `#{response}`."
end
end
end
end
end
......@@ -8,10 +8,14 @@ module QA
#
module Members
def add_member(user, access_level = AccessLevel::DEVELOPER)
QA::Runtime::Logger.debug(%Q[Adding user #{user.username} to #{full_path} #{self.class.name}])
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
end
def remove_member(user)
QA::Runtime::Logger.debug(%Q[Removing user #{user.username} from #{full_path} #{self.class.name}])
delete Runtime::API::Request.new(api_client, "#{api_members_path}/#{user.id}").url
end
......
......@@ -14,6 +14,7 @@ module QA
attribute :id
attribute :runners_token
attribute :name
attribute :full_path
def initialize
@path = Runtime::Namespace.sandbox_name
......
......@@ -117,7 +117,10 @@ module QA
user.password = password
end
else
self.fabricate!
self.fabricate! do |user|
user.username = username if username
user.password = password if password
end
end
end
......
......@@ -194,6 +194,14 @@ module QA
ENV['GITLAB_QA_PASSWORD_6']
end
def gitlab_qa_2fa_owner_username_1
ENV['GITLAB_QA_2FA_OWNER_USERNAME_1'] || 'gitlab-qa-2fa-owner-user1'
end
def gitlab_qa_2fa_owner_password_1
ENV['GITLAB_QA_2FA_OWNER_PASSWORD_1']
end
def gitlab_qa_1p_email
ENV['GITLAB_QA_1P_EMAIL']
end
......
# frozen_string_literal: true
module QA
context 'Manage', :requires_admin, :skip_live_env do
describe '2FA' do
let(:owner_user) do
Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_2fa_owner_username_1, Runtime::Env.gitlab_qa_2fa_owner_password_1)
end
let(:sandbox_group) do
Resource::Sandbox.fabricate! do |sandbox_group|
sandbox_group.path = "gitlab-qa-2fa-sandbox-group"
sandbox_group.api_client = owner_api_client
end
end
let(:group) do
QA::Resource::Group.fabricate_via_api! do |group|
group.sandbox = sandbox_group
group.api_client = owner_api_client
group.name = 'group-with-2fa'
end
end
let(:developer_user) do
Resource::User.fabricate_via_api! do |resource|
resource.api_client = admin_api_client
end
end
let(:two_fa_expected_text) { /The group settings for.*require you to enable Two-Factor Authentication for your account.*You need to do this before/ }
before do
group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER)
end
it 'allows enforcing and logging in with 2fa' do
enforce_two_factor_authentication_on_group(group)
enable_two_factor_authentication_for_user(developer_user)
Flow::Login.sign_in(as: developer_user, skip_page_validation: true)
Page::Main::TwoFactorAuth.perform do |two_fa_auth|
two_fa_auth.set_2fa_code(@otp.fresh_otp)
two_fa_auth.click_verify_code_button
end
expect(Page::Main::Menu.perform(&:signed_in?)).to be_truthy
end
after do
group.set_require_two_factor_authentication(value: 'false')
group.remove_via_api! do |resource|
resource.api_client = admin_api_client
end
developer_user.remove_via_api!
end
def admin_api_client
@admin_api_client ||= Runtime::API::Client.as_admin
end
def owner_api_client
@owner_api_client ||= Runtime::API::Client.new(:gitlab, user: owner_user)
end
def enforce_two_factor_authentication_on_group(group)
Flow::Login.while_signed_in(as: owner_user) do
group.visit!
Page::Group::Menu.perform(&:click_group_general_settings_item)
Page::Group::Settings::General.perform(&:set_require_2fa_enabled)
expect(page).to have_text(two_fa_expected_text)
Page::Profile::TwoFactorAuth.perform(&:click_configure_it_later_button)
expect(page).not_to have_text(two_fa_expected_text)
end
end
def enable_two_factor_authentication_for_user(user)
Flow::Login.while_signed_in(as: user) do
expect(page).to have_text(two_fa_expected_text)
Page::Profile::TwoFactorAuth.perform do |two_fa_auth|
@otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content)
two_fa_auth.set_pin_code(@otp.fresh_otp)
two_fa_auth.click_register_2fa_app_button
expect(two_fa_auth).to have_text('Congratulations! You have enabled Two-factor Authentication!')
two_fa_auth.click_proceed_button
end
end
end
end
end
end
# frozen_string_literal: true
require 'rotp'
module QA
module Support
class OTP
def initialize(secret)
@rotp = ROTP::TOTP.new(secret)
end
def fresh_otp
otps = []
# Fetches a fresh OTP and returns it only after rotp provides the same OTP twice
# An OTP is valid for 30 seconds so 70 attempts with 0.5 interval would ensure we complete 1 cycle
Support::Retrier.retry_until(max_attempts: 70, sleep_interval: 0.5) do
otps << @rotp.now
otps.size >= 3 && otps[-1] == otps[-2] && otps[-1] != otps[-3]
end
otps.last
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment