Commit b334ce67 authored by James Lopez's avatar James Lopez

Merge branch 'feat/user-mode-in-session-for-admins' into 'master'

feat: user mode in session for admins

See merge request gitlab-org/gitlab!16981
parents 38931822 a1fddbb7
# frozen_string_literal: true
class Admin::SessionsController < ApplicationController
include InternalRedirect
before_action :user_is_admin!
def new
# Renders a form in which the admin can enter their password
end
def create
if current_user_mode.enable_admin_mode!(password: params[:password])
redirect_location = stored_location_for(:redirect) || admin_root_path
redirect_to safe_redirect_path(redirect_location)
else
flash.now[:alert] = _('Invalid Login or password')
render :new
end
end
def destroy
current_user_mode.disable_admin_mode!
redirect_to root_path, status: :found, notice: _('Admin mode disabled')
end
private
def user_is_admin!
render_404 unless current_user&.admin?
end
end
...@@ -36,6 +36,7 @@ class ApplicationController < ActionController::Base ...@@ -36,6 +36,7 @@ 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?,
...@@ -533,6 +534,10 @@ class ApplicationController < ActionController::Base ...@@ -533,6 +534,10 @@ class ApplicationController < ActionController::Base
yield yield
end end
end end
def current_user_mode
@current_user_mode ||= Gitlab::Auth::CurrentUserMode.new(current_user)
end
end end
ApplicationController.prepend_if_ee('EE::ApplicationController') ApplicationController.prepend_if_ee('EE::ApplicationController')
...@@ -14,6 +14,16 @@ module EnforcesAdminAuthentication ...@@ -14,6 +14,16 @@ module EnforcesAdminAuthentication
end end
def authenticate_admin! def authenticate_admin!
render_404 unless current_user.admin? return render_404 unless current_user.admin?
return unless Feature.enabled?(:user_mode_in_session)
unless current_user_mode.admin_mode?
store_location_for(:redirect, request.fullpath) if storable_location?
redirect_to(new_admin_session_path, notice: _('Re-authentication required'))
end
end
def storable_location?
request.path != new_admin_session_path
end end
end end
...@@ -5,6 +5,12 @@ ...@@ -5,6 +5,12 @@
# Controller concern to handle PAT, RSS, and static objects token authentication methods # Controller concern to handle PAT, RSS, and static objects token authentication methods
# #
module SessionlessAuthentication module SessionlessAuthentication
extend ActiveSupport::Concern
included do
before_action :enable_admin_mode!, if: :sessionless_user?
end
# This filter handles personal access tokens, atom requests with rss tokens, and static object tokens # This filter handles personal access tokens, atom requests with rss tokens, and static object tokens
def authenticate_sessionless_user!(request_format) def authenticate_sessionless_user!(request_format)
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format) user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format)
...@@ -25,4 +31,8 @@ module SessionlessAuthentication ...@@ -25,4 +31,8 @@ module SessionlessAuthentication
sign_in(user, store: false, message: :sessionless_sign_in) sign_in(user, store: false, message: :sessionless_sign_in)
end end
end end
def enable_admin_mode!
current_user_mode.enable_admin_mode!(skip_password_validation: true) if Feature.enabled?(:user_mode_in_session)
end
end end
...@@ -86,6 +86,12 @@ module NavHelper ...@@ -86,6 +86,12 @@ module NavHelper
links << :admin_impersonation links << :admin_impersonation
end end
if Feature.enabled?(:user_mode_in_session)
if current_user&.admin? && current_user_mode&.admin_mode?
links << :admin_mode
end
end
links links
end end
end end
......
...@@ -5,7 +5,13 @@ require_dependency 'declarative_policy' ...@@ -5,7 +5,13 @@ require_dependency 'declarative_policy'
class BasePolicy < DeclarativePolicy::Base class BasePolicy < DeclarativePolicy::Base
desc "User is an instance admin" desc "User is an instance admin"
with_options scope: :user, score: 0 with_options scope: :user, score: 0
condition(:admin) { @user&.admin? } condition(:admin) do
if Feature.enabled?(:user_mode_in_session)
Gitlab::Auth::CurrentUserMode.new(@user).admin_mode?
else
@user&.admin?
end
end
desc "User is blocked" desc "User is blocked"
with_options scope: :user, score: 0 with_options scope: :user, score: 0
......
= form_tag(admin_session_path, method: :post, html: { class: 'new_user gl-show-field-errors', 'aria-live': 'assertive'}) do
.form-group
= label_tag :password, _('Password'), class: 'label-bold'
= password_field_tag :password, nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
.submit-container.move-submit-down
= submit_tag _('Enter admin mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
- if form_based_providers.any?
- 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'
%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
%a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter admin mode')
- @hide_breadcrumbs = true
- page_title _('Enter admin mode')
.row.justify-content-center
.col-6.new-session-forms-container
.login-page
#signin-container
= render 'admin/sessions/tabs_normal'
.tab-content
- if password_authentication_enabled_for_web?
= render 'admin/sessions/signin_box'
- else
-# Show a message if none of the mechanisms above are enabled
.prepend-top-default.center
= _('No authentication methods configured.')
...@@ -68,6 +68,15 @@ ...@@ -68,6 +68,15 @@
= nav_link(controller: 'admin/dashboard') do = nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'd-lg-none admin-icon qa-admin-area-link' do = link_to admin_root_path, class: 'd-lg-none admin-icon qa-admin-area-link' do
= _('Admin Area') = _('Admin Area')
- if Feature.enabled?(:user_mode_in_session)
- if header_link?(:admin_mode)
= nav_link(controller: 'admin/sessions') do
= link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
= _('Leave admin mode')
- elsif current_user.admin?
= nav_link(controller: 'admin/sessions') do
= link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
= _('Enter admin mode')
- if Gitlab::Sherlock.enabled? - if Gitlab::Sherlock.enabled?
%li %li
= link_to sherlock_transactions_path, class: 'd-lg-none admin-icon' do = link_to sherlock_transactions_path, class: 'd-lg-none admin-icon' do
...@@ -95,6 +104,17 @@ ...@@ -95,6 +104,17 @@
= nav_link(controller: 'admin/dashboard', html_options: { class: "d-none d-lg-block d-xl-block"}) do = nav_link(controller: 'admin/dashboard', html_options: { class: "d-none d-lg-block d-xl-block"}) do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin', size: 18) = sprite_icon('admin', size: 18)
- if Feature.enabled?(:user_mode_in_session)
- if header_link?(:admin_mode)
= nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
= link_to destroy_admin_session_path, title: _('Leave admin mode'), aria: { label: _('Leave admin mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= sprite_icon('lock-open', size: 18)
- elsif current_user.admin?
= nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
= link_to new_admin_session_path, title: _('Enter admin mode'), aria: { label: _('Enter admin mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= sprite_icon('lock', size: 18)
- if Gitlab::Sherlock.enabled? - if Gitlab::Sherlock.enabled?
%li %li
= link_to sherlock_transactions_path, class: 'admin-icon d-none d-lg-block d-xl-block', title: _('Sherlock Transactions'), = link_to sherlock_transactions_path, class: 'admin-icon d-none d-lg-block d-xl-block', title: _('Sherlock Transactions'),
......
---
title: Require admins to enter admin-mode by re-authenticating before performing
administrative operations
merge_request: 16981
author: Roger Rüttimann & Diego Louzán
type: added
...@@ -21,6 +21,10 @@ namespace :admin do ...@@ -21,6 +21,10 @@ namespace :admin do
end end
end end
resource :session, only: [:new, :create] do
get 'destroy', action: :destroy, as: :destroy
end
resource :impersonation, only: :destroy resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy] resources :abuse_reports, only: [:index, :destroy]
......
...@@ -4,6 +4,8 @@ describe API::GeoNodes, :geo, :prometheus, api: true do ...@@ -4,6 +4,8 @@ describe API::GeoNodes, :geo, :prometheus, api: true do
include ApiHelpers include ApiHelpers
include ::EE::GeoHelpers include ::EE::GeoHelpers
include_context 'custom session'
set(:primary) { create(:geo_node, :primary) } set(:primary) { create(:geo_node, :primary) }
set(:secondary) { create(:geo_node) } set(:secondary) { create(:geo_node) }
set(:secondary_status) { create(:geo_node_status, :healthy, geo_node: secondary) } set(:secondary_status) { create(:geo_node_status, :healthy, geo_node: secondary) }
......
...@@ -355,6 +355,8 @@ describe API::Projects do ...@@ -355,6 +355,8 @@ describe API::Projects do
end end
context 'as an admin' do context 'as an admin' do
include_context 'custom session'
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
it 'returns 500 when repository storage is unknown' do it 'returns 500 when repository storage is unknown' do
......
...@@ -17,6 +17,8 @@ module API ...@@ -17,6 +17,8 @@ module API
request.access_token request.access_token
end end
use AdminModeMiddleware
helpers HelperMethods helpers HelperMethods
install_error_responders(base) install_error_responders(base)
...@@ -52,6 +54,11 @@ module API ...@@ -52,6 +54,11 @@ module API
forbidden!(api_access_denied_message(user)) forbidden!(api_access_denied_message(user))
end end
# Set admin mode for API requests (if admin)
if Feature.enabled?(:user_mode_in_session)
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(skip_password_validation: true)
end
user user
end end
...@@ -141,5 +148,22 @@ module API ...@@ -141,5 +148,22 @@ module API
end end
end end
end end
class AdminModeMiddleware < ::Grape::Middleware::Base
def initialize(app, **options)
super
end
def call(env)
if Feature.enabled?(:user_mode_in_session)
session = {}
Gitlab::Session.with_session(session) do
app.call(env)
end
else
app.call(env)
end
end
end
end end
end end
# frozen_string_literal: true
module Gitlab
module Auth
# Keeps track of the current session user mode
#
# In order to perform administrative tasks over some interfaces,
# an administrator must have explicitly enabled admin-mode
# e.g. on web access require re-authentication
class CurrentUserMode
SESSION_STORE_KEY = :current_user_mode
ADMIN_MODE_START_TIME_KEY = 'admin_mode'
MAX_ADMIN_MODE_TIME = 6.hours
def initialize(user)
@user = user
end
def admin_mode?
return false unless user
Gitlab::SafeRequestStore.fetch(request_store_key) do
user&.admin? && any_session_with_admin_mode?
end
end
def enable_admin_mode!(password: nil, skip_password_validation: false)
return unless user&.admin?
return unless skip_password_validation || user&.valid_password?(password)
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
end
def disable_admin_mode!
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
Gitlab::SafeRequestStore.delete(request_store_key)
end
private
attr_reader :user
def request_store_key
@request_store_key ||= { res: :current_user_mode, user: user.id }
end
def current_session_data
@current_session ||= Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY)
end
def any_session_with_admin_mode?
return true if current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
all_sessions.any? do |session|
session[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
end
end
def all_sessions
@all_sessions ||= ActiveSession.list_sessions(user).lazy.map do |session|
Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY, session.with_indifferent_access )
end
end
end
end
end
...@@ -1031,6 +1031,9 @@ msgstr "" ...@@ -1031,6 +1031,9 @@ msgstr ""
msgid "Admin Section" msgid "Admin Section"
msgstr "" msgstr ""
msgid "Admin mode disabled"
msgstr ""
msgid "Admin notes" msgid "Admin notes"
msgstr "" msgstr ""
...@@ -5734,6 +5737,9 @@ msgstr "" ...@@ -5734,6 +5737,9 @@ msgstr ""
msgid "Enter a number" msgid "Enter a number"
msgstr "" msgstr ""
msgid "Enter admin mode"
msgstr ""
msgid "Enter at least three characters to search" msgid "Enter at least three characters to search"
msgstr "" msgstr ""
...@@ -9123,6 +9129,9 @@ msgstr "" ...@@ -9123,6 +9129,9 @@ msgstr ""
msgid "Leave" msgid "Leave"
msgstr "" msgstr ""
msgid "Leave admin mode"
msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost." msgid "Leave edit mode? All unsaved changes will be lost."
msgstr "" msgstr ""
...@@ -12739,6 +12748,9 @@ msgstr "" ...@@ -12739,6 +12748,9 @@ msgstr ""
msgid "Raw blob request rate limit per minute" msgid "Raw blob request rate limit per minute"
msgstr "" msgstr ""
msgid "Re-authentication required"
msgstr ""
msgid "Read more" msgid "Read more"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe Admin::SessionsController, :do_not_mock_admin_mode do
include_context 'custom session'
let(:user) { create(:user) }
before do
sign_in(user)
end
describe '#new' do
context 'for regular users' do
it 'shows error page' do
get :new
expect(response).to have_gitlab_http_status(:not_found)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
end
end
context 'for admin users' do
let(:user) { create(:admin) }
it 'renders a password form' do
get :new
expect(response).to render_template :new
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
end
end
end
describe '#create' do
context 'for regular users' do
it 'shows error page' do
post :create
expect(response).to have_gitlab_http_status(:not_found)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
end
end
context 'for admin users' do
let(:user) { create(:admin) }
it 'sets admin mode with a valid password' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
post :create, params: { password: user.password }
expect(response).to redirect_to admin_root_path
expect(controller.send(:current_user_mode).admin_mode?).to be(true)
end
it 'fails with an invalid password' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
post :create, params: { password: '' }
expect(response).to render_template :new
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
end
end
end
describe '#destroy' do
context 'for regular users' do
it 'shows error page' do
get :destroy
expect(response).to have_gitlab_http_status(404)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
end
end
context 'for admin users' do
let(:user) { create(:admin) }
it 'disables admin mode and redirects to main page' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
post :create, params: { password: user.password }
expect(controller.send(:current_user_mode).admin_mode?).to be(true)
get :destroy
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(root_path)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
end
end
end
end
...@@ -777,4 +777,48 @@ describe ApplicationController do ...@@ -777,4 +777,48 @@ describe ApplicationController do
end end
end end
end end
describe '#current_user_mode', :do_not_mock_admin_mode do
include_context 'custom session'
controller(described_class) do
def index
render html: 'authenticated'
end
end
before do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
sign_in(user)
get :index
end
context 'with a regular user' do
it 'admin mode is not set' do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Auth::CurrentUserMode.new(user).admin_mode?).to be(false)
end
end
context 'with an admin user' do
let(:user) { create(:admin) }
it 'admin mode is not set' do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Auth::CurrentUserMode.new(user).admin_mode?).to be(false)
end
context 'that re-authenticated' do
before do
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end
it 'admin mode is set' do
expect(response).to have_gitlab_http_status(:ok)
expect(Gitlab::Auth::CurrentUserMode.new(user).admin_mode?).to be(true)
end
end
end
end
end end
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
require 'spec_helper' require 'spec_helper'
describe EnforcesAdminAuthentication do describe EnforcesAdminAuthentication, :do_not_mock_admin_mode do
include AdminModeHelper
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
...@@ -10,30 +12,86 @@ describe EnforcesAdminAuthentication do ...@@ -10,30 +12,86 @@ describe EnforcesAdminAuthentication do
end end
controller(ApplicationController) do controller(ApplicationController) do
# `described_class` is not available in this context include EnforcesAdminAuthentication
include EnforcesAdminAuthentication # rubocop:disable RSpec/DescribedClass
def index def index
head :ok head :ok
end end
end end
describe 'authenticate_admin!' do context 'feature flag :user_mode_in_session is enabled' do
context 'as an admin' do describe 'authenticate_admin!' do
let(:user) { create(:admin) } context 'as an admin' do
let(:user) { create(:admin) }
it 'renders ok' do it 'renders redirect for re-authentication and does not set admin mode' do
get :index get :index
expect(response).to redirect_to new_admin_session_path
expect(assigns(:current_user_mode)&.admin_mode?).to be(false)
end
context 'when admin mode is active' do
before do
enable_admin_mode!(user)
end
it 'renders ok' do
get :index
expect(response).to have_gitlab_http_status(200)
end
end
end
context 'as a user' do
it 'renders a 404' do
get :index
expect(response).to have_gitlab_http_status(404)
end
it 'does not set admin mode' do
get :index
expect(response).to have_gitlab_http_status(200) # check for nil too since on 404, current_user_mode might not be initialized
expect(assigns(:current_user_mode)&.admin_mode?).to be_falsey
end
end end
end end
end
context 'feature flag :user_mode_in_session is disabled' do
before do
stub_feature_flags(user_mode_in_session: false)
end
context 'as a user' do describe 'authenticate_admin!' do
it 'renders a 404' do before do
get :index get :index
end
context 'as an admin' do
let(:user) { create(:admin) }
it 'allows direct access to page' do
expect(response).to have_gitlab_http_status(200)
end
it 'does not set admin mode' do
expect(assigns(:current_user_mode)&.admin_mode?).to be_falsey
end
end
context 'as a user' do
it 'renders a 404' do
expect(response).to have_gitlab_http_status(404)
end
expect(response).to have_gitlab_http_status(404) it 'does not set admin mode' do
# check for nil too since on 404, current_user_mode might not be initialized
expect(assigns(:current_user_mode)&.admin_mode?).to be_falsey
end
end end
end end
end end
......
This diff is collapsed.
require 'spec_helper' require 'spec_helper'
describe NavHelper do describe NavHelper, :do_not_mock_admin_mode do
describe '#header_links' do describe '#header_links' do
include_context 'custom session'
before do before do
allow(helper).to receive(:session) { {} } allow(helper).to receive(:session).and_return(session)
end end
context 'when the user is logged in' do context 'when the user is logged in' do
let(:user) { build(:user) } let(:user) { create(:user) }
let(:current_user_mode) { Gitlab::Auth::CurrentUserMode.new(user) }
before do before do
allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:current_user_mode).and_return(current_user_mode)
allow(helper).to receive(:can?) { true } allow(helper).to receive(:can?) { true }
end end
...@@ -26,6 +30,46 @@ describe NavHelper do ...@@ -26,6 +30,46 @@ describe NavHelper do
expect(helper.header_links).to include(:admin_impersonation) expect(helper.header_links).to include(:admin_impersonation)
end end
context 'as admin' do
let(:user) { create(:user, :admin) }
context 'feature flag :user_mode_in_session is enabled' do
it 'does not contain the admin mode link by default' do
expect(helper.header_links).not_to include(:admin_mode)
end
context 'with admin mode enabled' do
before do
current_user_mode.enable_admin_mode!(password: user.password)
end
it 'contains the admin mode link' do
expect(helper.header_links).to include(:admin_mode)
end
end
end
context 'feature flag :user_mode_in_session is disabled' do
before do
stub_feature_flags(user_mode_in_session: false)
end
it 'does not contain the admin mode link' do
expect(helper.header_links).not_to include(:admin_mode)
end
context 'with admin mode enabled' do
before do
current_user_mode.enable_admin_mode!(password: user.password)
end
it 'has no effect on header links' do
expect(helper.header_links).not_to include(:admin_mode)
end
end
end
end
context 'when the user cannot read cross project' do context 'when the user cannot read cross project' do
before do before do
allow(helper).to receive(:can?).with(user, :read_cross_project) { false } allow(helper).to receive(:can?).with(user, :read_cross_project) { false }
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
include_context 'custom session'
let(:user) { build(:user) }
subject { described_class.new(user) }
before do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
end
describe '#admin_mode?', :request_store do
context 'when the user is a regular user' do
it 'is false by default' do
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password)
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 'cannot be enabled with empty params' do
subject.enable_admin_mode!
expect(subject.admin_mode?).to be(false)
end
it 'disable has no effect' do
subject.enable_admin_mode!
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(false)
end
context 'skipping password validation' do
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true)
expect(subject.admin_mode?).to be(false)
end
end
end
context 'when the user is an admin' do
let(:user) { build(:user, :admin) }
it 'is false by default' do
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
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true)
end
it 'can be disabled' do
subject.enable_admin_mode!(password: user.password)
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(false)
end
it 'will expire in the future' do
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present'
Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
# 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
context 'skipping password validation' do
it 'can be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
expect(subject.admin_mode?).to be(true)
end
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.enable_admin_mode!(password: user.password)
end
expect(subject.admin_mode?).to be(true)
end
end
end
end
describe '#enable_admin_mode!' do
let(:user) { build(:user, :admin) }
it 'creates a timestamp in the session' do
subject.enable_admin_mode!(password: user.password)
expect(session).to include(expected_session_entry(be_within(1.second).of Time.now))
end
end
describe '#disable_admin_mode!' do
let(:user) { build(:user, :admin) }
it 'sets the session timestamp to nil' do
subject.disable_admin_mode!
expect(session).to include(expected_session_entry(be_nil))
end
end
def expected_session_entry(value_matcher)
{
Gitlab::Auth::CurrentUserMode::SESSION_STORE_KEY => a_hash_including(
Gitlab::Auth::CurrentUserMode::ADMIN_MODE_START_TIME_KEY => value_matcher)
}
end
end
...@@ -343,6 +343,8 @@ describe API::Helpers do ...@@ -343,6 +343,8 @@ describe API::Helpers do
end end
context 'sudo' do context 'sudo' do
include_context 'custom session'
shared_examples 'successful sudo' do shared_examples 'successful sudo' do
it 'sets current_user' do it 'sets current_user' do
expect(current_user).to eq(user) expect(current_user).to eq(user)
......
...@@ -3,9 +3,14 @@ require 'spec_helper' ...@@ -3,9 +3,14 @@ require 'spec_helper'
describe BuildActionEntity do describe BuildActionEntity do
let(:job) { create(:ci_build, name: 'test_job') } let(:job) { create(:ci_build, name: 'test_job') }
let(:request) { double('request') } let(:request) { double('request') }
let(:user) { create(:user) }
let(:entity) do let(:entity) do
described_class.new(job, request: spy('request')) described_class.new(job, request: request)
end
before do
allow(request).to receive(:current_user).and_return(user)
end end
describe '#as_json' do describe '#as_json' do
......
...@@ -160,6 +160,25 @@ RSpec.configure do |config| ...@@ -160,6 +160,25 @@ RSpec.configure do |config|
allow(Gitlab::Git::KeepAround).to receive(:execute) allow(Gitlab::Git::KeepAround).to receive(:execute)
Gitlab::ThreadMemoryCache.cache_backend.clear Gitlab::ThreadMemoryCache.cache_backend.clear
# Temporary patch to force admin mode to be active by default in tests when
# using the feature flag :user_mode_in_session, since this will require
# modifying a significant number of specs to test both states for admin
# mode enabled / disabled.
#
# See https://gitlab.com/gitlab-org/gitlab/issues/31511
# See gitlab/spec/support/helpers/admin_mode_helpers.rb
#
# If it is required to have the real behaviour that an admin is signed in
# with normal user mode and needs to switch to admin mode, it is possible to
# mark such tests with the `do_not_mock_admin_mode` metadata tag, e.g:
#
# context 'some test with normal user mode', :do_not_mock_admin_mode do ... end
unless example.metadata[:do_not_mock_admin_mode]
allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode|
current_user_mode.send(:user)&.admin?
end
end
end end
config.around(:example, :quarantine) do |example| config.around(:example, :quarantine) do |example|
......
# frozen_string_literal: true
# Helper for enabling admin mode in tests
module AdminModeHelper
# Users 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
# be in admin mode without requiring a second authentication step (provided
# the user is an admin)
def enable_admin_mode!(user)
fake_user_mode = instance_double(Gitlab::Auth::CurrentUserMode)
allow(Gitlab::Auth::CurrentUserMode).to receive(:new).with(user).and_return(fake_user_mode)
allow(fake_user_mode).to receive(:admin_mode?).and_return(user&.admin?)
end
end
...@@ -48,6 +48,14 @@ module LoginHelpers ...@@ -48,6 +48,14 @@ module LoginHelpers
@current_user = user @current_user = user
end end
def gitlab_enable_admin_mode_sign_in(user)
visit new_admin_session_path
fill_in 'password', with: user.password
click_button 'Enter admin mode'
end
def gitlab_sign_in_via(provider, user, uid, saml_response = nil) def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response) mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response)
visit new_user_session_path visit new_user_session_path
......
# frozen_string_literal: true
# the session is empty by default; you can overwrite it by defining your own
# let(:session) variable
# we do not use a parameter such as |session| because it does not play nice
# with let variables
shared_context 'custom session' do
let!(:session) { {} }
around do |example|
Gitlab::Session.with_session(session) do
example.run
end
end
end
require 'spec_helper'
describe 'admin/sessions/new.html.haml' do
context 'admin has password set' do
before do
allow(view).to receive(:password_authentication_enabled_for_web?).and_return(true)
end
it "shows enter password form" do
render
expect(rendered).to have_css('#login-pane.active')
expect(rendered).to have_selector('input[name="password"]')
end
end
context 'admin has no password set' do
before do
allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false)
end
it "warns authentication not possible" do
render
expect(rendered).not_to have_css('#login-pane')
expect(rendered).to have_content 'No authentication methods configured'
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