Commit 7bfb6111 authored by Diego Louzán's avatar Diego Louzán Committed by Bob Van Landuyt

feat: support omniauth for admin mode

- Extend admin mode support to OmniAuth authentication
- Improve admin mode flow by adding a request step with a grace period
parent bc4ad61f
...@@ -6,17 +6,23 @@ class Admin::SessionsController < ApplicationController ...@@ -6,17 +6,23 @@ class Admin::SessionsController < ApplicationController
before_action :user_is_admin! before_action :user_is_admin!
def new def new
# Renders a form in which the admin can enter their password if current_user_mode.admin_mode?
redirect_to redirect_path, notice: _('Admin mode already enabled')
else
current_user_mode.request_admin_mode! unless current_user_mode.admin_mode_requested?
store_location_for(:redirect, redirect_path)
end
end end
def create def create
if current_user_mode.enable_admin_mode!(password: params[:password]) if current_user_mode.enable_admin_mode!(password: params[:password])
redirect_location = stored_location_for(:redirect) || admin_root_path redirect_to redirect_path, notice: _('Admin mode enabled')
redirect_to safe_redirect_path(redirect_location)
else else
flash.now[:alert] = _('Invalid Login or password') flash.now[:alert] = _('Invalid login or password')
render :new render :new
end end
rescue Gitlab::Auth::CurrentUserMode::NotRequestedError
redirect_to new_admin_session_path, alert: _('Re-authentication period expired or never requested. Please try again')
end end
def destroy def destroy
...@@ -30,4 +36,19 @@ class Admin::SessionsController < ApplicationController ...@@ -30,4 +36,19 @@ class Admin::SessionsController < ApplicationController
def user_is_admin! def user_is_admin!
render_404 unless current_user&.admin? render_404 unless current_user&.admin?
end end
def redirect_path
redirect_to_path = safe_redirect_path(stored_location_for(:redirect)) || safe_redirect_path_for_url(request.referer)
if redirect_to_path &&
excluded_redirect_paths.none? { |excluded| redirect_to_path.include?(excluded) }
redirect_to_path
else
admin_root_path
end
end
def excluded_redirect_paths
[new_admin_session_path, admin_session_path]
end
end end
...@@ -16,6 +16,7 @@ class ApplicationController < ActionController::Base ...@@ -16,6 +16,7 @@ class ApplicationController < ActionController::Base
include ConfirmEmailWarning include ConfirmEmailWarning
include Gitlab::Tracking::ControllerConcern include Gitlab::Tracking::ControllerConcern
include Gitlab::Experimentation::ControllerConcern include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
before_action :authenticate_user!, except: [:route_not_found] before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms? before_action :enforce_terms!, if: :should_enforce_terms?
...@@ -41,7 +42,6 @@ class ApplicationController < ActionController::Base ...@@ -41,7 +42,6 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception, prepend: true protect_from_forgery with: :exception, prepend: true
helper_method :can? helper_method :can?
helper_method :current_user_mode
helper_method :import_sources_enabled?, :github_import_enabled?, helper_method :import_sources_enabled?, :github_import_enabled?,
:gitea_import_enabled?, :github_import_configured?, :gitea_import_enabled?, :github_import_configured?,
:gitlab_import_enabled?, :gitlab_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?,
...@@ -546,10 +546,6 @@ class ApplicationController < ActionController::Base ...@@ -546,10 +546,6 @@ class ApplicationController < ActionController::Base
end end
end end
def current_user_mode
@current_user_mode ||= Gitlab::Auth::CurrentUserMode.new(current_user)
end
# A user requires a role and have the setup_for_company attribute set when they are part of the experimental signup # A user requires a role and have the setup_for_company attribute set when they are part of the experimental signup
# flow (executed by the Growth team). Users are redirected to the welcome page when their role is required and the # flow (executed by the Growth team). Users are redirected to the welcome page when their role is required and the
# experiment is enabled for the current user. # experiment is enabled for the current user.
......
...@@ -18,6 +18,7 @@ module EnforcesAdminAuthentication ...@@ -18,6 +18,7 @@ module EnforcesAdminAuthentication
return unless Feature.enabled?(:user_mode_in_session) return unless Feature.enabled?(:user_mode_in_session)
unless current_user_mode.admin_mode? unless current_user_mode.admin_mode?
current_user_mode.request_admin_mode!
store_location_for(:redirect, request.fullpath) if storable_location? store_location_for(:redirect, request.fullpath) if storable_location?
redirect_to(new_admin_session_path, notice: _('Re-authentication required')) redirect_to(new_admin_session_path, notice: _('Re-authentication required'))
end end
......
# frozen_string_literal: true
module InitializesCurrentUserMode
extend ActiveSupport::Concern
included do
helper_method :current_user_mode
end
def current_user_mode
@current_user_mode ||= Gitlab::Auth::CurrentUserMode.new(current_user)
end
end
...@@ -33,6 +33,8 @@ module SessionlessAuthentication ...@@ -33,6 +33,8 @@ module SessionlessAuthentication
end end
def enable_admin_mode! def enable_admin_mode!
current_user_mode.enable_admin_mode!(skip_password_validation: true) if Feature.enabled?(:user_mode_in_session) return unless Feature.enabled?(:user_mode_in_session)
current_user_mode.enable_sessionless_admin_mode!
end end
end end
...@@ -6,6 +6,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController ...@@ -6,6 +6,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include PageLayoutHelper include PageLayoutHelper
include OauthApplications include OauthApplications
include Gitlab::Experimentation::ControllerConcern include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
before_action :verify_user_oauth_applications_enabled, except: :index before_action :verify_user_oauth_applications_enabled, except: :index
before_action :authenticate_user! before_action :authenticate_user!
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
include Gitlab::Experimentation::ControllerConcern include Gitlab::Experimentation::ControllerConcern
include InitializesCurrentUserMode
layout 'profile' layout 'profile'
# Overridden from Doorkeeper::AuthorizationsController to # Overridden from Doorkeeper::AuthorizationsController to
......
...@@ -4,6 +4,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -4,6 +4,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthenticatesWithTwoFactor include AuthenticatesWithTwoFactor
include Devise::Controllers::Rememberable include Devise::Controllers::Rememberable
include AuthHelper include AuthHelper
include InitializesCurrentUserMode
protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true
...@@ -94,8 +95,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -94,8 +95,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
return render_403 unless link_provider_allowed?(oauth['provider']) return render_403 unless link_provider_allowed?(oauth['provider'])
log_audit_event(current_user, with: oauth['provider']) log_audit_event(current_user, with: oauth['provider'])
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
if Feature.enabled?(:user_mode_in_session)
return admin_mode_flow if current_user_mode.admin_mode_requested?
end
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
link_identity(identity_linker) link_identity(identity_linker)
if identity_linker.changed? if identity_linker.changed?
...@@ -239,6 +244,24 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -239,6 +244,24 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
store_location_for(:user, uri.to_s) store_location_for(:user, uri.to_s)
end end
end end
def admin_mode_flow
if omniauth_identity_matches_current_user?
current_user_mode.enable_admin_mode!(skip_password_validation: true)
redirect_to stored_location_for(:redirect) || admin_root_path, notice: _('Admin mode enabled')
else
fail_admin_mode_invalid_credentials
end
end
def omniauth_identity_matches_current_user?
current_user.matches_identity?(oauth['provider'], oauth['uid'])
end
def fail_admin_mode_invalid_credentials
redirect_to new_admin_session_path, alert: _('Invalid login or password')
end
end end
OmniauthCallbacksController.prepend_if_ee('EE::OmniauthCallbacksController') OmniauthCallbacksController.prepend_if_ee('EE::OmniauthCallbacksController')
...@@ -87,7 +87,7 @@ module NavHelper ...@@ -87,7 +87,7 @@ module NavHelper
end end
if Feature.enabled?(:user_mode_in_session) if Feature.enabled?(:user_mode_in_session)
if current_user&.admin? && current_user_mode&.admin_mode? if current_user_mode.admin_mode?
links << :admin_mode links << :admin_mode
end end
end end
......
...@@ -996,6 +996,10 @@ class User < ApplicationRecord ...@@ -996,6 +996,10 @@ class User < ApplicationRecord
@ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"]) @ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
end end
def matches_identity?(provider, extern_uid)
identities.where(provider: provider, extern_uid: extern_uid).exists?
end
def project_deploy_keys def project_deploy_keys
DeployKey.in_projects(authorized_projects.select(:id)).distinct(:id) DeployKey.in_projects(authorized_projects.select(:id)).distinct(:id)
end end
......
- if any_form_based_providers_enabled?
- if password_authentication_enabled_for_web?
.login-box.tab-pane{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'admin/sessions/new_base'
- elsif password_authentication_enabled_for_web?
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'admin/sessions/new_base'
...@@ -7,9 +7,16 @@ ...@@ -7,9 +7,16 @@
#signin-container #signin-container
= render 'admin/sessions/tabs_normal' = render 'admin/sessions/tabs_normal'
.tab-content .tab-content
- if password_authentication_enabled_for_web? - if !current_user.require_password_creation_for_web?
= render 'admin/sessions/signin_box' .login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
- else .login-body
-# Show a message if none of the mechanisms above are enabled = render 'admin/sessions/new_base'
- if omniauth_enabled? && button_based_providers_enabled?
.clearfix
= render 'devise/shared/omniauth_box'
-# Show a message if none of the mechanisms above are enabled
- if current_user.require_password_creation_for_web? && !omniauth_enabled?
.prepend-top-default.center .prepend-top-default.center
= _('No authentication methods configured.') = _('No authentication methods configured.')
---
title: Add OmniAuth authentication support to admin mode feature
merge_request: 18214
author: Diego Louzán
type: added
...@@ -67,6 +67,7 @@ describe Gitlab::Elastic::SnippetSearchResults, :elastic, :sidekiq_might_not_nee ...@@ -67,6 +67,7 @@ describe Gitlab::Elastic::SnippetSearchResults, :elastic, :sidekiq_might_not_nee
context 'admin mode enabled' do context 'admin mode enabled' do
before do before do
Gitlab::Auth::CurrentUserMode.new(user).request_admin_mode!
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password) Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end end
......
...@@ -56,7 +56,9 @@ module API ...@@ -56,7 +56,9 @@ module API
# Set admin mode for API requests (if admin) # Set admin mode for API requests (if admin)
if Feature.enabled?(:user_mode_in_session) if Feature.enabled?(:user_mode_in_session)
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(skip_password_validation: true) current_user_mode = Gitlab::Auth::CurrentUserMode.new(user)
current_user_mode.enable_sessionless_admin_mode!
end end
user user
......
...@@ -8,9 +8,13 @@ module Gitlab ...@@ -8,9 +8,13 @@ module Gitlab
# an administrator must have explicitly enabled admin-mode # an administrator must have explicitly enabled admin-mode
# e.g. on web access require re-authentication # e.g. on web access require re-authentication
class CurrentUserMode class CurrentUserMode
NotRequestedError = Class.new(StandardError)
SESSION_STORE_KEY = :current_user_mode SESSION_STORE_KEY = :current_user_mode
ADMIN_MODE_START_TIME_KEY = 'admin_mode' ADMIN_MODE_START_TIME_KEY = 'admin_mode'
ADMIN_MODE_REQUESTED_TIME_KEY = 'admin_mode_requested'
MAX_ADMIN_MODE_TIME = 6.hours MAX_ADMIN_MODE_TIME = 6.hours
ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes
def initialize(user) def initialize(user)
@user = user @user = user
...@@ -19,8 +23,16 @@ module Gitlab ...@@ -19,8 +23,16 @@ module Gitlab
def admin_mode? def admin_mode?
return false unless user return false unless user
Gitlab::SafeRequestStore.fetch(request_store_key) do Gitlab::SafeRequestStore.fetch(admin_mode_rs_key) do
user&.admin? && any_session_with_admin_mode? user.admin? && any_session_with_admin_mode?
end
end
def admin_mode_requested?
return false unless user
Gitlab::SafeRequestStore.fetch(admin_mode_requested_rs_key) do
user.admin? && admin_mode_requested_in_grace_period?
end end
end end
...@@ -28,20 +40,45 @@ module Gitlab ...@@ -28,20 +40,45 @@ module Gitlab
return unless user&.admin? return unless user&.admin?
return unless skip_password_validation || user&.valid_password?(password) return unless skip_password_validation || user&.valid_password?(password)
raise NotRequestedError unless admin_mode_requested?
reset_request_store
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
end end
def enable_sessionless_admin_mode!
request_admin_mode! && enable_admin_mode!(skip_password_validation: true)
end
def disable_admin_mode! def disable_admin_mode!
return unless user&.admin?
reset_request_store
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
Gitlab::SafeRequestStore.delete(request_store_key) end
def request_admin_mode!
return unless user&.admin?
reset_request_store
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
end end
private private
attr_reader :user attr_reader :user
def request_store_key def admin_mode_rs_key
@request_store_key ||= { res: :current_user_mode, user: user.id } @admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
end
def admin_mode_requested_rs_key
@admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
end end
def current_session_data def current_session_data
...@@ -61,6 +98,15 @@ module Gitlab ...@@ -61,6 +98,15 @@ module Gitlab
Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY, session.with_indifferent_access ) Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY, session.with_indifferent_access )
end end
end end
def admin_mode_requested_in_grace_period?
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
end
def reset_request_store
Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
end
end end
end end
end end
...@@ -1134,9 +1134,15 @@ msgstr "" ...@@ -1134,9 +1134,15 @@ msgstr ""
msgid "Admin Section" msgid "Admin Section"
msgstr "" msgstr ""
msgid "Admin mode already enabled"
msgstr ""
msgid "Admin mode disabled" msgid "Admin mode disabled"
msgstr "" msgstr ""
msgid "Admin mode enabled"
msgstr ""
msgid "Admin notes" msgid "Admin notes"
msgstr "" msgstr ""
...@@ -9678,6 +9684,9 @@ msgstr "" ...@@ -9678,6 +9684,9 @@ msgstr ""
msgid "Invalid input, please avoid emojis" msgid "Invalid input, please avoid emojis"
msgstr "" msgstr ""
msgid "Invalid login or password"
msgstr ""
msgid "Invalid pin code" msgid "Invalid pin code"
msgstr "" msgstr ""
...@@ -14397,6 +14406,9 @@ msgstr "" ...@@ -14397,6 +14406,9 @@ msgstr ""
msgid "Raw blob request rate limit per minute" msgid "Raw blob request rate limit per minute"
msgstr "" msgstr ""
msgid "Re-authentication period expired or never requested. Please try again"
msgstr ""
msgid "Re-authentication required" msgid "Re-authentication required"
msgstr "" msgstr ""
......
...@@ -17,7 +17,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do ...@@ -17,7 +17,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
get :new get :new
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
end end
end end
...@@ -28,7 +28,21 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do ...@@ -28,7 +28,21 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
get :new get :new
expect(response).to render_template :new expect(response).to render_template :new
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
end
context 'already in admin mode' do
before do
controller.current_user_mode.request_admin_mode!
controller.current_user_mode.enable_admin_mode!(password: user.password)
end
it 'redirects to original location' do
get :new
expect(response).to redirect_to(admin_root_path)
expect(controller.current_user_mode.admin_mode?).to be(true)
end
end end
end end
end end
...@@ -39,7 +53,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do ...@@ -39,7 +53,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
post :create post :create
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
end end
end end
...@@ -47,24 +61,60 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do ...@@ -47,24 +61,60 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
let(:user) { create(:admin) } let(:user) { create(:admin) }
it 'sets admin mode with a valid password' do it 'sets admin mode with a valid password' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path) controller.store_location_for(:redirect, admin_root_path)
# triggering the auth form will request admin mode
get :new
post :create, params: { password: user.password } post :create, params: { password: user.password }
expect(response).to redirect_to admin_root_path expect(response).to redirect_to admin_root_path
expect(controller.send(:current_user_mode).admin_mode?).to be(true) expect(controller.current_user_mode.admin_mode?).to be(true)
end end
it 'fails with an invalid password' do it 'fails with an invalid password' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path) controller.store_location_for(:redirect, admin_root_path)
# triggering the auth form will request admin mode
get :new
post :create, params: { password: '' } post :create, params: { password: '' }
expect(response).to render_template :new expect(response).to render_template :new
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
end
it 'fails if not requested first' do
expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
# do not trigger the auth form
post :create, params: { password: user.password }
expect(response).to redirect_to(new_admin_session_path)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
it 'fails if request period expired' do
expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
# triggering the auth form will request admin mode
get :new
Timecop.freeze(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do
post :create, params: { password: user.password }
expect(response).to redirect_to(new_admin_session_path)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
end end
end end
end end
...@@ -75,7 +125,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do ...@@ -75,7 +125,7 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
get :destroy get :destroy
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(404)
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
end end
end end
...@@ -83,15 +133,17 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do ...@@ -83,15 +133,17 @@ describe Admin::SessionsController, :do_not_mock_admin_mode do
let(:user) { create(:admin) } let(:user) { create(:admin) }
it 'disables admin mode and redirects to main page' do it 'disables admin mode and redirects to main page' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
get :new
post :create, params: { password: user.password } post :create, params: { password: user.password }
expect(controller.send(:current_user_mode).admin_mode?).to be(true) expect(controller.current_user_mode.admin_mode?).to be(true)
get :destroy get :destroy
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(root_path) expect(response).to redirect_to(root_path)
expect(controller.send(:current_user_mode).admin_mode?).to be(false) expect(controller.current_user_mode.admin_mode?).to be(false)
end end
end end
end end
......
...@@ -814,6 +814,7 @@ describe ApplicationController do ...@@ -814,6 +814,7 @@ describe ApplicationController do
context 'that re-authenticated' do context 'that re-authenticated' do
before do before do
Gitlab::Auth::CurrentUserMode.new(user).request_admin_mode!
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password) Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end end
......
...@@ -62,6 +62,12 @@ describe Oauth::ApplicationsController do ...@@ -62,6 +62,12 @@ describe Oauth::ApplicationsController do
end end
end end
context 'Helpers' do
it 'current_user_mode available' do
expect(subject.current_user_mode).not_to be_nil
end
end
def disable_user_oauth def disable_user_oauth
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false) allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe OmniauthCallbacksController, type: :controller do describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode: true do
include LoginHelpers include LoginHelpers
describe 'omniauth' do describe 'omniauth' do
...@@ -336,4 +336,109 @@ describe OmniauthCallbacksController, type: :controller do ...@@ -336,4 +336,109 @@ describe OmniauthCallbacksController, type: :controller do
end end
end end
end end
describe 'enable admin mode' do
include_context 'custom session'
let(:provider) { :auth0 }
let(:extern_uid) { 'my-uid' }
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
def reauthenticate_and_check_admin_mode(expected_admin_mode:)
# Initially admin mode disabled
expect(subject.current_user_mode.admin_mode?).to be(false)
# Trigger OmniAuth admin mode flow and expect admin mode status
post provider
expect(request.env['warden']).to be_authenticated
expect(subject.current_user_mode.admin_mode?).to be(expected_admin_mode)
end
context 'user and admin mode requested by the same user' do
before do
sign_in user
mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: {})
stub_omniauth_provider(provider, context: request)
end
context 'with a regular user' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(root_path)
end
end
context 'with an admin user' do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
context 'when requested first' do
before do
subject.current_user_mode.request_admin_mode!
end
it 'can be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: true)
expect(response).to redirect_to(admin_root_path)
end
end
context 'when not requested first' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(root_path)
end
end
end
end
context 'user and admin mode requested by different users' do
let(:reauth_extern_uid) { 'another_uid' }
let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider) }
before do
sign_in user
mock_auth_hash(provider.to_s, reauth_extern_uid, reauth_user.email, additional_info: {})
stub_omniauth_provider(provider, context: request)
end
context 'with a regular user' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(profile_account_path)
end
end
context 'with an admin user' do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider, access_level: :admin) }
context 'when requested first' do
before do
subject.current_user_mode.request_admin_mode!
end
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(new_admin_session_path)
end
end
context 'when not requested first' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(profile_account_path)
end
end
end
end
end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Gcp Cluster', :js do describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
include GoogleApi::CloudPlatformHelpers include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -42,6 +42,7 @@ describe NavHelper, :do_not_mock_admin_mode do ...@@ -42,6 +42,7 @@ describe NavHelper, :do_not_mock_admin_mode do
context 'with admin mode enabled' do context 'with admin mode enabled' do
before do before do
current_user_mode.request_admin_mode!
current_user_mode.enable_admin_mode!(password: user.password) current_user_mode.enable_admin_mode!(password: user.password)
end end
...@@ -62,6 +63,7 @@ describe NavHelper, :do_not_mock_admin_mode do ...@@ -62,6 +63,7 @@ describe NavHelper, :do_not_mock_admin_mode do
context 'with admin mode enabled' do context 'with admin mode enabled' do
before do before do
current_user_mode.request_admin_mode!
current_user_mode.enable_admin_mode!(password: user.password) current_user_mode.enable_admin_mode!(password: user.password)
end end
...@@ -89,11 +91,18 @@ describe NavHelper, :do_not_mock_admin_mode do ...@@ -89,11 +91,18 @@ describe NavHelper, :do_not_mock_admin_mode do
end end
end end
it 'returns only the sign in and search when the user is not logged in' do context 'when the user is not logged in' do
allow(helper).to receive(:current_user).and_return(nil) let(:current_user_mode) { Gitlab::Auth::CurrentUserMode.new(nil) }
allow(helper).to receive(:can?).with(nil, :read_cross_project) { true }
expect(helper.header_links).to contain_exactly(:sign_in, :search) before do
allow(helper).to receive(:current_user).and_return(nil)
allow(helper).to receive(:current_user_mode).and_return(current_user_mode)
allow(helper).to receive(:can?).with(nil, :read_cross_project) { true }
end
it 'returns only the sign in and search when the user is not logged in' do
expect(helper.header_links).to contain_exactly(:sign_in, :search)
end
end end
end end
......
...@@ -62,69 +62,90 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do ...@@ -62,69 +62,90 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
context 'when the user is an admin' do context 'when the user is an admin' do
let(:user) { build(:user, :admin) } let(:user) { build(:user, :admin) }
it 'is false by default' do context 'when admin mode not requested' do
expect(subject.admin_mode?).to be(false) it 'is false by default' do
end expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil)
expect(subject.admin_mode?).to be(false)
end
it 'can be enabled with a valid password' do it 'raises exception if we try to enable it' do
subject.enable_admin_mode!(password: user.password) expect do
subject.enable_admin_mode!(password: user.password)
end.to raise_error(::Gitlab::Auth::CurrentUserMode::NotRequestedError)
expect(subject.admin_mode?).to be(true) expect(subject.admin_mode?).to be(false)
end
end end
it 'can be disabled' do context 'when admin mode requested first' do
subject.enable_admin_mode!(password: user.password) before do
subject.disable_admin_mode! subject.request_admin_mode!
end
expect(subject.admin_mode?).to be(false)
end
it 'will expire in the future' do it 'is false by default' do
subject.enable_admin_mode!(password: user.password) expect(subject.admin_mode?).to be(false)
expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present' end
Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do it 'cannot be enabled with an invalid password' do
# in the future this will be a new request, simulate by clearing the RequestStore subject.enable_admin_mode!(password: nil)
Gitlab::SafeRequestStore.clear!
expect(subject.admin_mode?).to be(false), 'admin mode did not expire in the future' expect(subject.admin_mode?).to be(false)
end end
end
context 'skipping password validation' do
it 'can be enabled with a valid password' do it 'can be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true) subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true) expect(subject.admin_mode?).to be(true)
end end
it 'can be enabled with an invalid password' do it 'can be disabled' do
subject.enable_admin_mode!(skip_password_validation: true) subject.enable_admin_mode!(password: user.password)
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(true) expect(subject.admin_mode?).to be(false)
end end
end
context 'with two independent sessions' do it 'will expire in the future' do
let(:another_session) { {} } subject.enable_admin_mode!(password: user.password)
let(:another_subject) { described_class.new(user) } expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present'
before do Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session, another_session]) # in the future this will be a new request, simulate by clearing the RequestStore
Gitlab::SafeRequestStore.clear!
expect(subject.admin_mode?).to be(false), 'admin mode did not expire in the future'
end
end end
it 'can be enabled in one and seen in the other' do context 'skipping password validation' do
Gitlab::Session.with_session(another_session) do it 'can be enabled with a valid password' do
another_subject.enable_admin_mode!(password: user.password) subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
expect(subject.admin_mode?).to be(true)
end end
expect(subject.admin_mode?).to be(true) it 'can be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true)
expect(subject.admin_mode?).to be(true)
end
end
context 'with two independent sessions' do
let(:another_session) { {} }
let(:another_subject) { described_class.new(user) }
before do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session, another_session])
end
it 'can be enabled in one and seen in the other' do
Gitlab::Session.with_session(another_session) do
another_subject.request_admin_mode!
another_subject.enable_admin_mode!(password: user.password)
end
expect(subject.admin_mode?).to be(true)
end
end end
end end
end end
...@@ -134,16 +155,28 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do ...@@ -134,16 +155,28 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
let(:user) { build(:user, :admin) } let(:user) { build(:user, :admin) }
it 'creates a timestamp in the session' do it 'creates a timestamp in the session' do
subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password) subject.enable_admin_mode!(password: user.password)
expect(session).to include(expected_session_entry(be_within(1.second).of Time.now)) expect(session).to include(expected_session_entry(be_within(1.second).of Time.now))
end end
end end
describe '#enable_sessionless_admin_mode!' do
let(:user) { build(:user, :admin) }
it 'enabled admin mode without password' do
subject.enable_sessionless_admin_mode!
expect(subject.admin_mode?).to be(true)
end
end
describe '#disable_admin_mode!' do describe '#disable_admin_mode!' do
let(:user) { build(:user, :admin) } let(:user) { build(:user, :admin) }
it 'sets the session timestamp to nil' do it 'sets the session timestamp to nil' do
subject.request_admin_mode!
subject.disable_admin_mode! subject.disable_admin_mode!
expect(session).to include(expected_session_entry(be_nil)) expect(session).to include(expected_session_entry(be_nil))
......
...@@ -2839,6 +2839,7 @@ describe User, :do_not_mock_admin_mode do ...@@ -2839,6 +2839,7 @@ describe User, :do_not_mock_admin_mode do
context 'when admin mode is enabled' do context 'when admin mode is enabled' do
before do before do
Gitlab::Auth::CurrentUserMode.new(user).request_admin_mode!
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password) Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
# Helper for enabling admin mode in tests # Helper for enabling admin mode in tests
module AdminModeHelper module AdminModeHelper
# Users are logged in by default in user mode and have to switch to admin # Administrators are logged in by default in user mode and have to switch to admin
# mode for accessing any administrative functionality. This helper lets a user # mode for accessing any administrative functionality. This helper lets a user
# be in admin mode without requiring a second authentication step (provided # be in admin mode without requiring a second authentication step (provided
# the user is an admin) # the user is an admin)
......
...@@ -3,29 +3,44 @@ ...@@ -3,29 +3,44 @@
require 'spec_helper' require 'spec_helper'
describe 'admin/sessions/new.html.haml' do describe 'admin/sessions/new.html.haml' do
context 'admin has password set' do let(:user) { create(:admin) }
before do
allow(view).to receive(:password_authentication_enabled_for_web?).and_return(true) before do
end allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:omniauth_enabled?).and_return(false)
end
it "shows enter password form" do context 'internal admin user' do
it 'shows enter password form' do
render render
expect(rendered).to have_css('#login-pane.active') expect(rendered).to have_css('#login-pane.active')
expect(rendered).to have_selector('input[name="password"]') expect(rendered).to have_selector('input[name="password"]')
end end
it 'warns authentication not possible if password not set' do
allow(user).to receive(:require_password_creation_for_web?).and_return(true)
render
expect(rendered).not_to have_css('#login-pane')
expect(rendered).to have_content _('No authentication methods configured.')
end
end end
context 'admin has no password set' do context 'omniauth authentication enabled' do
before do before do
allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false) allow(view).to receive(:omniauth_enabled?).and_return(true)
allow(view).to receive(:button_based_providers_enabled?).and_return(true)
end end
it "warns authentication not possible" do it 'shows omniauth form' do
render render
expect(rendered).not_to have_css('#login-pane') expect(rendered).to have_css('.omniauth-container')
expect(rendered).to have_content 'No authentication methods configured' expect(rendered).to have_content _('Sign in with')
expect(rendered).not_to have_content _('No authentication methods configured.')
end end
end end
end end
...@@ -11,6 +11,7 @@ describe 'layouts/application' do ...@@ -11,6 +11,7 @@ describe 'layouts/application' do
allow(view).to receive(:session).and_return({}) allow(view).to receive(:session).and_return({})
allow(view).to receive(:user_signed_in?).and_return(true) allow(view).to receive(:user_signed_in?).and_return(true)
allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
end end
context 'body data elements for pageview context' do context 'body data elements for pageview context' do
......
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