Commit 22ef029a authored by Imre Farkas's avatar Imre Farkas

Merge branch 'feat/ldap-auth-admin-mode' into 'master'

LDAP auth support for admin mode

Closes #212434

See merge request gitlab-org/gitlab!28572
parents 51171a57 106775e9
......@@ -3,6 +3,7 @@
class Admin::SessionsController < ApplicationController
include Authenticates2FAForAdminMode
include InternalRedirect
include RendersLdapServers
before_action :user_is_admin!
......
# frozen_string_literal: true
module RendersLdapServers
extend ActiveSupport::Concern
included do
helper_method :ldap_servers
end
def ldap_servers
@ldap_servers ||= begin
if Gitlab::Auth::Ldap::Config.sign_in_enabled?
Gitlab::Auth::Ldap::Config.available_servers
else
[]
end
end
end
end
......@@ -16,6 +16,10 @@ class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
def ldap
return unless Gitlab::Auth::Ldap::Config.sign_in_enabled?
if Feature.enabled?(:user_mode_in_session)
return admin_mode_flow(Gitlab::Auth::Ldap::User) if current_user_mode.admin_mode_requested?
end
sign_in_user_flow(Gitlab::Auth::Ldap::User)
end
......
......@@ -87,6 +87,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private
def after_omniauth_failure_path_for(scope)
if Feature.enabled?(:user_mode_in_session)
return new_admin_session_path if current_user_mode.admin_mode_requested?
end
super
end
def omniauth_flow(auth_module, identity_linker: nil)
if fragment = request.env.dig('omniauth.params', 'redirect_fragment').presence
store_redirect_fragment(fragment)
......
......@@ -6,6 +6,7 @@ class SessionsController < Devise::SessionsController
include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper
include Recaptcha::Verify
include RendersLdapServers
skip_before_action :check_two_factor_requirement, only: [:destroy]
# replaced with :require_no_authentication_without_flash
......@@ -16,7 +17,6 @@ class SessionsController < Devise::SessionsController
if: -> { action_name == 'create' && two_factor_enabled? }
prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
prepend_before_action :ldap_servers, only: [:new, :create]
prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? }
......@@ -269,16 +269,6 @@ class SessionsController < Devise::SessionsController
Gitlab::Recaptcha.load_configurations!
end
def ldap_servers
@ldap_servers ||= begin
if Gitlab::Auth::Ldap::Config.sign_in_enabled?
Gitlab::Auth::Ldap::Config.available_servers
else
[]
end
end
end
def unverified_anonymous_user?
exceeded_failed_login_attempts? || exceeded_anonymous_sessions?
end
......
......@@ -145,6 +145,10 @@ module AuthHelper
IdentityProviderPolicy.new(current_user, provider).can?(:link)
end
def allow_admin_mode_password_authentication_for_web?
current_user.allow_password_authentication_for_web? && !current_user.password_automatically_set?
end
extend self
end
......
= form_tag(admin_session_path, method: :post, html: { class: 'new_user gl-show-field-errors', 'aria-live': 'assertive'}) do
= form_tag(admin_session_path, method: :post, class: 'new_user gl-show-field-errors', 'aria-live': 'assertive') do
.form-group
= label_tag :user_password, _('Password'), class: 'label-bold'
= password_field_tag 'user[password]', nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
......
- if any_form_based_providers_enabled?
- if crowd_enabled?
.login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) }
.login-body
= render 'devise/sessions/new_crowd'
= render_if_exists 'devise/sessions/new_kerberos_tab'
- ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server, hide_remember_me: true, submit_message: _('Enter Admin Mode')
= render_if_exists 'devise/sessions/new_smartcard'
- if allow_admin_mode_password_authentication_for_web?
.login-box.tab-pane{ id: 'login-pane', role: 'tabpanel', class: active_when(!any_form_based_providers_enabled?) }
.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' }= tab_title
......@@ -5,18 +5,19 @@
.col-md-5.new-session-forms-container
.login-page
#signin-container
= render 'admin/sessions/tabs_normal', tab_title: _('Enter Admin Mode')
- if any_form_based_providers_enabled?
= render 'devise/shared/tabs_ldap', show_password_form: allow_admin_mode_password_authentication_for_web?, render_signup_link: false
- else
= render 'devise/shared/tabs_normal', tab_title: _('Enter Admin Mode'), render_signup_link: false
.tab-content
- if !current_user.require_password_creation_for_web?
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
= render 'admin/sessions/new_base'
- if allow_admin_mode_password_authentication_for_web? || ldap_sign_in_enabled? || crowd_enabled?
= render 'admin/sessions/signin_box'
- if omniauth_enabled? && button_based_providers_enabled?
.clearfix
= render 'devise/shared/omniauth_box', hide_remember_me: true
-# Show a message if none of the mechanisms above are enabled
- if !allow_admin_mode_password_authentication_for_web? && !ldap_sign_in_enabled? && !omniauth_enabled?
.prepend-top-default.center
= _('No authentication methods configured.')
-# 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
= _('No authentication methods configured.')
- if omniauth_enabled? && button_based_providers_enabled?
.clearfix
= render 'devise/shared/omniauth_box', hide_remember_me: true
......@@ -5,7 +5,7 @@
.col-md-5.new-session-forms-container
.login-page
#signin-container
= render 'admin/sessions/tabs_normal', tab_title: _('Enter Admin Mode')
= render 'devise/shared/tabs_normal', tab_title: _('Enter Admin Mode'), render_signup_link: false
.tab-content
.login-box.tab-pane.active{ id: 'login-pane', role: 'tabpanel' }
.login-body
......
- server = local_assigns.fetch(:server)
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
- submit_message = local_assigns.fetch(:submit_message, _('Sign in'))
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group
......@@ -7,9 +9,11 @@
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", data: { qa_selector: 'password_field' }, required: true }
- if devise_mapping.rememberable?
- if !hide_remember_me && devise_mapping.rememberable?
.remember-me
%label{ for: "remember_me" }
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
= submit_tag "Sign in", class: "btn-success btn", data: { qa_selector: 'sign_in_button' }
.submit-container.move-submit-down
= submit_tag submit_message, class: "btn-success btn", data: { qa_selector: 'sign_in_button' }
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
.omniauth-container.prepend-top-15
%label.label-bold.d-block
Sign in with
......@@ -10,7 +12,7 @@
= provider_image_tag(provider)
%span
= label_for_provider(provider)
- unless defined?(hide_remember_me) && hide_remember_me
- unless hide_remember_me
%fieldset.remember-me
%label
= check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
......
......@@ -6,7 +6,7 @@
= render_if_exists 'devise/sessions/new_kerberos_tab'
- @ldap_servers.each_with_index do |server, i|
- ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server
......
- show_password_form = local_assigns.fetch(:show_password_form, password_authentication_enabled_for_web?)
- render_signup_link = local_assigns.fetch(:render_signup_link, true)
%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if any_form_based_providers_enabled?) }
- if crowd_enabled?
%li.nav-item
= link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab'
= render_if_exists "devise/shared/kerberos_tab"
- @ldap_servers.each_with_index do |server, i|
- ldap_servers.each_with_index do |server, i|
%li.nav-item
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))}", data: { toggle: 'tab', qa_selector: 'ldap_tab' }
= render_if_exists 'devise/shared/tab_smartcard'
- if password_authentication_enabled_for_web?
- if show_password_form
%li.nav-item
= link_to 'Standard', '#login-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'standard_tab' }
- if allow_signup?
= link_to _('Standard'), '#login-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'standard_tab' }
- if render_signup_link && allow_signup?
%li.nav-item
= link_to 'Register', '#register-pane', class: 'nav-link', data: { toggle: 'tab', qa_selector: 'register_tab' }
- tab_title = local_assigns.fetch(:tab_title, _('Sign in'))
- render_signup_link = local_assigns.fetch(:render_signup_link, true)
%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' } Sign in
- if allow_signup?
%a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= tab_title
- if render_signup_link && allow_signup?
%li.nav-item{ role: 'presentation' }
%a.nav-link{ href: '#register-pane', data: { track_label: 'sign_in_register', track_property: '', track_event: 'click_button', track_value: '', toggle: 'tab', qa_selector: 'register_tab' }, role: 'tab' } Register
---
title: LDAP authentication support for admin mode
merge_request: 28572
author: Diego Louzán
type: added
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
- submit_message = local_assigns.fetch(:submit_message, _('Sign in'))
- unless smartcard_enabled_for_ldap?(server['provider_name'], required: true)
= render_ce('devise/sessions/new_ldap', server: server)
%hr
= render_ce('devise/sessions/new_ldap', server: server, hide_remember_me: hide_remember_me, submit_message: submit_message)
- if smartcard_enabled_for_ldap?(server['provider_name'])
%hr
= render 'devise/sessions/new_smartcard_ldap', server: server
......@@ -6,7 +6,7 @@
= render_if_exists 'devise/sessions/new_kerberos_tab'
- @ldap_servers.each_with_index do |server, i|
- ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server
......
......@@ -19526,6 +19526,9 @@ msgstr ""
msgid "Stage removed"
msgstr ""
msgid "Standard"
msgstr ""
msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging."
msgstr ""
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Ldap::OmniauthCallbacksController do
describe Ldap::OmniauthCallbacksController, :do_not_mock_admin_mode do
include_context 'Ldap::OmniauthCallbacksController'
it 'allows sign in' do
......@@ -65,4 +65,55 @@ describe Ldap::OmniauthCallbacksController do
expect(request.env['warden']).to be_authenticated
end
end
describe 'enable admin mode' do
include_context 'custom session'
before do
sign_in user
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, :admin, extern_uid: uid, provider: provider) }
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
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
end
......@@ -41,10 +41,10 @@ describe SessionsController do
stub_ldap_setting(enabled: true)
end
it 'assigns ldap_servers' do
it 'ldap_servers available in helper' do
get(:new)
expect(assigns[:ldap_servers].first.to_h).to include('label' => 'ldap', 'provider_name' => 'ldapmain')
expect(subject.ldap_servers.first.to_h).to include('label' => 'ldap', 'provider_name' => 'ldapmain')
end
context 'with sign_in disabled' do
......@@ -52,10 +52,10 @@ describe SessionsController do
stub_ldap_setting(prevent_ldap_sign_in: true)
end
it 'assigns no ldap_servers' do
it 'no ldap_servers available in helper' do
get(:new)
expect(assigns[:ldap_servers]).to eq []
expect(subject.ldap_servers).to eq []
end
end
end
......
......@@ -5,6 +5,7 @@ require 'spec_helper'
describe 'Admin Mode Login', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include TermsHelper
include UserLoginHelper
include LdapHelpers
describe 'with two-factor authentication', :js do
def enter_code(code)
......@@ -179,6 +180,82 @@ describe 'Admin Mode Login', :clean_gitlab_redis_shared_state, :do_not_mock_admi
gitlab_enable_admin_mode_sign_in_via('saml', user, 'my-uid', mock_saml_response)
end
end
context 'when logging in via ldap' do
let(:uid) { 'my-uid' }
let(:provider_label) { 'Main LDAP' }
let(:provider_name) { 'main' }
let(:provider) { "ldap#{provider_name}" }
let(:ldap_server_config) do
{
'label' => provider_label,
'provider_name' => provider,
'attributes' => {},
'encryption' => 'plain',
'uid' => 'uid',
'base' => 'dc=example,dc=com'
}
end
let(:user) { create(:omniauth_user, :admin, :two_factor, extern_uid: uid, provider: provider) }
before do
setup_ldap(provider, user, uid, ldap_server_config)
end
context 'when two factor authentication is required' do
it 'shows 2FA prompt after ldap login' do
sign_in_using_ldap!(user, provider_label)
expect(page).to have_content('Two-Factor Authentication')
enter_code(user.current_otp)
enable_admin_mode_using_ldap!(user)
expect(page).to have_content('Two-Factor Authentication')
# Cannot reuse the TOTP
Timecop.travel(30.seconds.from_now) do
enter_code(user.current_otp)
expect(current_path).to eq admin_root_path
expect(page).to have_content('Admin mode enabled')
end
end
end
def setup_ldap(provider, user, uid, ldap_server_config)
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!
mock_auth_hash(provider, uid, user.email)
allow(Gitlab::Auth::Ldap::Access).to receive(:allowed?).with(user).and_return(true)
allow_any_instance_of(ActionDispatch::Routing::RoutesProxy)
.to receive(:"user_#{provider}_omniauth_callback_path")
.and_return("/users/auth/#{provider}/callback")
end
def sign_in_using_ldap!(user, provider_label)
visit new_user_session_path
click_link provider_label
fill_in 'username', with: user.username
fill_in 'password', with: user.password
click_button 'Sign in'
end
def enable_admin_mode_using_ldap!(user)
visit new_admin_session_path
click_link provider_label
fill_in 'username', with: user.username
fill_in 'password', with: user.password
click_button 'Enter Admin Mode'
end
end
end
end
end
......@@ -184,4 +184,40 @@ describe AuthHelper do
end
end
end
describe '#allow_admin_mode_password_authentication_for_web?' do
let(:user) { create(:user) }
subject { helper.allow_admin_mode_password_authentication_for_web? }
before do
allow(helper).to receive(:current_user).and_return(user)
end
it { is_expected.to be(true) }
context 'when password authentication for web is disabled' do
before do
stub_application_setting(password_authentication_enabled_for_web: false)
end
it { is_expected.to be(false) }
end
context 'when current_user is an ldap user' do
before do
allow(user).to receive(:ldap_user?).and_return(true)
end
it { is_expected.to be(false) }
end
context 'when user got password automatically set' do
before do
user.update_attribute(:password_automatically_set, true)
end
it { is_expected.to be(false) }
end
end
end
......@@ -6,20 +6,26 @@ describe 'admin/sessions/new.html.haml' do
let(:user) { create(:admin) }
before do
disable_all_signin_methods
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:omniauth_enabled?).and_return(false)
end
context 'internal admin user' do
before do
allow(view).to receive(:allow_admin_mode_password_authentication_for_web?).and_return(true)
end
it 'shows enter password form' do
render
expect(rendered).to have_selector('[data-qa-selector="sign_in_tab"]')
expect(rendered).to have_css('#login-pane.active')
expect(rendered).to have_selector('input[name="user[password]"]')
expect(rendered).to have_selector('[data-qa-selector="password_field"]')
end
it 'warns authentication not possible if password not set' do
allow(user).to receive(:require_password_creation_for_web?).and_return(true)
allow(view).to receive(:allow_admin_mode_password_authentication_for_web?).and_return(false)
render
......@@ -39,8 +45,53 @@ describe 'admin/sessions/new.html.haml' do
expect(rendered).to have_css('.omniauth-container')
expect(rendered).to have_content _('Sign in with')
expect(rendered).not_to have_content _('No authentication methods configured.')
end
end
context 'ldap authentication' do
let(:user) { create(:omniauth_user, :admin, extern_uid: 'my-uid', provider: 'ldapmain') }
let(:server) { { provider_name: 'ldapmain', label: 'LDAP' }.with_indifferent_access }
before do
enable_ldap
end
it 'is shown when enabled' do
render
expect(rendered).to have_selector('[data-qa-selector="ldap_tab"]')
expect(rendered).to have_css('.login-box#ldapmain')
expect(rendered).to have_field('LDAP Username')
expect(rendered).not_to have_content('No authentication methods configured')
end
it 'is not shown when LDAP sign in is disabled' do
disable_ldap_sign_in
render
expect(rendered).not_to have_selector('[data-qa-selector="ldap_tab"]')
expect(rendered).not_to have_field('LDAP Username')
expect(rendered).to have_content('No authentication methods configured')
end
def enable_ldap
allow(view).to receive(:ldap_servers).and_return([server])
allow(view).to receive(:form_based_providers).and_return([:ldapmain])
allow(view).to receive(:omniauth_callback_path).with(:user, 'ldapmain').and_return('/ldapmain')
allow(view).to receive(:ldap_sign_in_enabled?).and_return(true)
end
def disable_ldap_sign_in
allow(view).to receive(:ldap_sign_in_enabled?).and_return(false)
allow(view).to receive(:ldap_servers).and_return([])
end
end
def disable_all_signin_methods
allow(view).to receive(:password_authentication_enabled_for_web?).and_return(false)
allow(view).to receive(:omniauth_enabled?).and_return(false)
allow(view).to receive(:ldap_sign_in_enabled?).and_return(false)
end
end
......@@ -54,14 +54,14 @@ describe 'devise/sessions/new' do
def enable_ldap
stub_ldap_setting(enabled: true)
assign(:ldap_servers, [server])
allow(view).to receive(:ldap_servers).and_return([server])
allow(view).to receive(:form_based_providers).and_return([:ldapmain])
allow(view).to receive(:omniauth_callback_path).with(:user, 'ldapmain').and_return('/ldapmain')
end
def disable_ldap_sign_in
allow(view).to receive(:ldap_sign_in_enabled?).and_return(false)
assign(:ldap_servers, [])
allow(view).to receive(:ldap_servers).and_return([])
end
def disable_captcha
......
......@@ -6,7 +6,7 @@ describe 'devise/shared/_signin_box' do
describe 'Crowd form' do
before do
stub_devise
assign(:ldap_servers, [])
allow(view).to receive(:ldap_servers).and_return([])
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
allow(view).to receive(:captcha_enabled?).and_return(false)
allow(view).to receive(:captcha_on_login_required?).and_return(false)
......
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