From cad3e2cc1bddad4a8ff742f541c0136efdadf8d7 Mon Sep 17 00:00:00 2001 From: Imre Farkas <ifarkas@gitlab.com> Date: Mon, 21 Jan 2019 10:29:00 +0100 Subject: [PATCH] Add LDAP integration to smartcard authentication It implements certificate matching using certificateExactMatch matching rule defined in RFC4523. --- app/views/devise/sessions/_new_ldap.html.haml | 2 + config/gitlab.yml.example | 4 + config/initializers/1_settings.rb | 2 + doc/administration/auth/smartcard.md | 74 ++++++- ee/app/controllers/smartcard_controller.rb | 14 +- ee/app/helpers/ee/application_helper.rb | 4 + ee/app/helpers/ee/auth_helper.rb | 21 ++ ee/app/models/ee/identity.rb | 4 + .../views/devise/sessions/_new_ldap.html.haml | 5 + .../devise/sessions/_new_smartcard.html.haml | 2 +- .../sessions/_new_smartcard_ldap.html.haml | 17 ++ .../ee-7693-smartcard_ldap_integration.yml | 5 + ee/config/routes/smartcard.rb | 1 + ee/lib/ee/gitlab/auth/ldap/adapter.rb | 19 ++ ee/lib/ee/gitlab/auth/ldap/person.rb | 5 + ee/lib/gitlab/auth/smartcard/base.rb | 57 ++++++ ee/lib/gitlab/auth/smartcard/certificate.rb | 64 ++---- .../gitlab/auth/smartcard/ldap_certificate.rb | 61 ++++++ ee/lib/gitlab/auth_logger.rb | 9 + ee/spec/features/users/login_spec.rb | 107 +++++++++- ee/spec/helpers/ee/auth_helper_spec.rb | 100 ++++++++++ ee/spec/lib/gitlab/auth/ldap/adapter_spec.rb | 36 ++++ ee/spec/lib/gitlab/auth/ldap/person_spec.rb | 12 ++ .../gitlab/auth/smartcard/certificate_spec.rb | 53 +---- .../auth/smartcard/ldap_certificate_spec.rb | 134 +++++++++++++ ee/spec/requests/smartcard_controller_spec.rb | 187 +++++++++++++----- .../smartcard_certificate_store.rb | 59 ++++++ lib/gitlab/auth/ldap/adapter.rb | 19 +- locale/gitlab.pot | 9 + 29 files changed, 910 insertions(+), 176 deletions(-) create mode 100644 ee/app/views/devise/sessions/_new_ldap.html.haml create mode 100644 ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml create mode 100644 ee/changelogs/unreleased/ee-7693-smartcard_ldap_integration.yml create mode 100644 ee/lib/gitlab/auth/smartcard/base.rb create mode 100644 ee/lib/gitlab/auth/smartcard/ldap_certificate.rb create mode 100644 ee/lib/gitlab/auth_logger.rb create mode 100644 ee/spec/lib/gitlab/auth/smartcard/ldap_certificate_spec.rb create mode 100644 ee/spec/support/shared_examples/smartcard_certificate_store.rb diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 796c0cadda8..f856773526d 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,3 +1,5 @@ +- server = local_assigns.fetch(:server) + = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do .form-group = label_tag :username, "#{server['label']} Username" diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 5807ae6cee6..07604c19d19 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -458,6 +458,10 @@ production: &base # A value of 0 means there is no timeout. timeout: 10 + # Enable smartcard authentication against the LDAP server. Valid values + # are "false", "optional", and "required". + smartcard_auth: false + # This setting specifies if LDAP server is Active Directory LDAP server. # For non AD servers it skips the AD specific queries. # If your LDAP server is not AD, set this to false. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 595cfa1adce..660237765d6 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -27,6 +27,7 @@ if Settings.ldap['enabled'] || Rails.env.test? server['timeout'] ||= 10.seconds server['block_auto_created_users'] = false if server['block_auto_created_users'].nil? server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil? + server['smartcard_auth'] = false unless %w[optional required].include?(server['smartcard_auth']) server['active_directory'] = true if server['active_directory'].nil? server['attributes'] = {} if server['attributes'].nil? server['lowercase_usernames'] = false if server['lowercase_usernames'].nil? @@ -52,6 +53,7 @@ end Settings['smartcard'] ||= Settingslogic.new({}) Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? +Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil? Settings['omniauth'] ||= Settingslogic.new({}) Settings.omniauth['enabled'] = true if Settings.omniauth['enabled'].nil? diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md index ad613274210..1107b955c4a 100644 --- a/doc/administration/auth/smartcard.md +++ b/doc/administration/auth/smartcard.md @@ -1,16 +1,26 @@ # Smartcard authentication +GitLab supports authentication using smartcards. + +## Authentication methods + +GitLab supports two authentication methods: + +- X.509 certificates with local databases. +- LDAP servers. + +### Authentication against a local database with X.509 certificates + > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/726) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.6 as an experimental -feature. Smartcard authentication may change or be removed completely in future -releases. +feature. Smartcard authentication against local databases may change or be +removed completely in future releases. Smartcards with X.509 certificates can be used to authenticate with GitLab. -## X.509 certificates - -To use a smartcard with an X.509 certificate to authenticate with GitLab, `CN` -and `emailAddress` must be defined in the certificate. For example: +To use a smartcard with an X.509 certificate to authenticate against a local +database with GitLab, `CN` and `emailAddress` must be defined in the +certificate. For example: ``` Certificate: @@ -25,6 +35,21 @@ Certificate: Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com ``` +### Authentication against an LDAP server + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7693) in +[GitLab Premium](https://about.gitlab.com/pricing/) 11.8 as an experimental +feature. Smartcard authentication against an LDAP server may change or be +removed completely in future releases. + +GitLab implements a standard way of certificate matching following +[RFC4523](https://tools.ietf.org/html/rfc4523). It uses the +`certificateExactMatch` certificate matching rule against the `userCertificate` +attribute. As a prerequisite, you must use an LDAP server that: + +- Supports the `certificateExactMatch` matching rule. +- Has the certificate stored in the `userCertificate` attribute. + ## Configure GitLab for smartcard authentication **For Omnibus installations** @@ -122,3 +147,40 @@ Certificate: 1. Save the file and [restart](../restart_gitlab.md#installations-from-source) GitLab for the changes to take effect. + +### Additional steps when authenticating against an LDAP server + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['ldap_servers'] = YAML.load <<-EOS + main: + # snip... + # Enable smartcard authentication against the LDAP server. Valid values + # are "false", "optional", and "required". + smartcard_auth: optional + EOS + ``` + +1. Save the file and [reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) + GitLab for the changes to take effect. + +**For installations from source** + +1. Edit `config/gitlab.yml`: + + ```yaml + production: + ldap: + servers: + main: + # snip... + # Enable smartcard authentication against the LDAP server. Valid values + # are "false", "optional", and "required". + smartcard_auth: optional + ``` + +1. Save the file and [restart](../restart_gitlab.md#installations-from-source) + GitLab for the changes to take effect. diff --git a/ee/app/controllers/smartcard_controller.rb b/ee/app/controllers/smartcard_controller.rb index 8a92681cae7..67ac86e8ac8 100644 --- a/ee/app/controllers/smartcard_controller.rb +++ b/ee/app/controllers/smartcard_controller.rb @@ -9,7 +9,17 @@ class SmartcardController < ApplicationController def auth certificate = Gitlab::Auth::Smartcard::Certificate.new(certificate_header) + sign_in_with(certificate) + end + + def ldap_auth + certificate = Gitlab::Auth::Smartcard::LDAPCertificate.new(params[:provider], certificate_header) + sign_in_with(certificate) + end + private + + def sign_in_with(certificate) user = certificate.find_or_create_user unless user flash[:alert] = _('Failed to signing using smartcard authentication') @@ -18,12 +28,10 @@ class SmartcardController < ApplicationController return end - log_audit_event(user, with: 'smartcard') + log_audit_event(user, with: certificate.auth_method) sign_in_and_redirect(user) end - protected - def check_feature_availability render_404 unless ::Gitlab::Auth::Smartcard.enabled? end diff --git a/ee/app/helpers/ee/application_helper.rb b/ee/app/helpers/ee/application_helper.rb index b04cd3838ea..1365dd72381 100644 --- a/ee/app/helpers/ee/application_helper.rb +++ b/ee/app/helpers/ee/application_helper.rb @@ -49,6 +49,10 @@ module EE end end + def smartcard_config_port + ::Gitlab.config.smartcard.client_certificate_required_port + end + def page_class class_names = super class_names += system_message_class diff --git a/ee/app/helpers/ee/auth_helper.rb b/ee/app/helpers/ee/auth_helper.rb index 42933dbdd43..2b9ab5713a8 100644 --- a/ee/app/helpers/ee/auth_helper.rb +++ b/ee/app/helpers/ee/auth_helper.rb @@ -50,6 +50,27 @@ module EE ::Gitlab::Auth::Smartcard.enabled? end + def smartcard_enabled_for_ldap?(provider_name, required: false) + return false unless smartcard_enabled? + + server = ::Gitlab::Auth::LDAP::Config.servers.find do |server| + server['provider_name'] == provider_name + end + + return false unless server + + truthy_values = ['required'] + truthy_values << 'optional' unless required + + truthy_values.include? server['smartcard_auth'] + end + + def smartcard_login_button_classes(provider_name) + css_classes = %w[btn btn-success] + css_classes << 'btn-inverted' unless smartcard_enabled_for_ldap?(provider_name, required: true) + css_classes.join(' ') + end + def group_saml_enabled? auth_providers.include?(:group_saml) end diff --git a/ee/app/models/ee/identity.rb b/ee/app/models/ee/identity.rb index b145774ca1a..749c161a8e3 100644 --- a/ee/app/models/ee/identity.rb +++ b/ee/app/models/ee/identity.rb @@ -20,6 +20,10 @@ module EE end class_methods do + def find_by_extern_uid(provider, extern_uid) + with_extern_uid(provider, extern_uid).take + end + def preload_saml_group preload(saml_provider: { group: :route }) end diff --git a/ee/app/views/devise/sessions/_new_ldap.html.haml b/ee/app/views/devise/sessions/_new_ldap.html.haml new file mode 100644 index 00000000000..3c6b049c06d --- /dev/null +++ b/ee/app/views/devise/sessions/_new_ldap.html.haml @@ -0,0 +1,5 @@ +- unless smartcard_enabled_for_ldap?(server['provider_name'], required: true) + = render_ce('devise/sessions/new_ldap', server: server) + %hr + += render 'devise/sessions/new_smartcard_ldap', server: server diff --git a/ee/app/views/devise/sessions/_new_smartcard.html.haml b/ee/app/views/devise/sessions/_new_smartcard.html.haml index 51084830524..ae19cd25794 100644 --- a/ee/app/views/devise/sessions/_new_smartcard.html.haml +++ b/ee/app/views/devise/sessions/_new_smartcard.html.haml @@ -1,6 +1,6 @@ - if smartcard_enabled? .login-box.tab-pane{ id: 'smartcard', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:smartcard)) } .login-body - = form_tag(smartcard_auth_url(port: Gitlab.config.smartcard.client_certificate_required_port), html: { 'aria-live' => 'assertive'}) do + = form_tag(smartcard_auth_url(port: smartcard_config_port), html: { 'aria-live' => 'assertive'}) do .submit-container = submit_tag _('Login with smartcard'), class: 'btn btn-success' diff --git a/ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml b/ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml new file mode 100644 index 00000000000..46125b1ebe1 --- /dev/null +++ b/ee/app/views/devise/sessions/_new_smartcard_ldap.html.haml @@ -0,0 +1,17 @@ +- if smartcard_enabled_for_ldap?(server['provider_name']) + %div{ id: "#{server['provider_name']}_smartcard" } + %span + %strong + = sprite_icon('smart-card', size: 16, css_class: 'vertical-align-middle' ) + %span.vertical-align-middle + = _('Sign in using smart card') + %p + = _('Use your smart card to authenticate with the LDAP server.') + + .login-body + = form_tag(smartcard_ldap_auth_url(provider: server['provider_name'], + port: smartcard_config_port), + html: { 'aria-live' => 'assertive'}) do + .submit-container + = submit_tag(_('Sign in with smart card'), + class: smartcard_login_button_classes(server['provider_name'])) diff --git a/ee/changelogs/unreleased/ee-7693-smartcard_ldap_integration.yml b/ee/changelogs/unreleased/ee-7693-smartcard_ldap_integration.yml new file mode 100644 index 00000000000..262c6257a02 --- /dev/null +++ b/ee/changelogs/unreleased/ee-7693-smartcard_ldap_integration.yml @@ -0,0 +1,5 @@ +--- +title: Add LDAP integration to smartcard authentication +merge_request: 9235 +author: +type: added diff --git a/ee/config/routes/smartcard.rb b/ee/config/routes/smartcard.rb index 4141e1ed2d6..4fdaeaabc25 100644 --- a/ee/config/routes/smartcard.rb +++ b/ee/config/routes/smartcard.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true post 'smartcard/auth' => 'smartcard#auth' +post 'smartcard/ldap_auth' => 'smartcard#ldap_auth' diff --git a/ee/lib/ee/gitlab/auth/ldap/adapter.rb b/ee/lib/ee/gitlab/auth/ldap/adapter.rb index 6a3caac36ac..ee92dfc3fa1 100644 --- a/ee/lib/ee/gitlab/auth/ldap/adapter.rb +++ b/ee/lib/ee/gitlab/auth/ldap/adapter.rb @@ -55,6 +55,25 @@ module EE LDAP::Group.new(entry, self) end end + + def user_by_certificate_assertion(certificate_assertion) + options = user_options_for_cert(certificate_assertion) + users_search(options).first + end + + private + + def user_options_for_cert(certificate_assertion) + options = { + attributes: ::Gitlab::Auth::LDAP::Person.ldap_attributes(config), + base: config.base + } + + filter = Net::LDAP::Filter.ex( + 'userCertificate:certificateExactMatch', certificate_assertion) + + options.merge(filter: user_filter(filter)) + end end end end diff --git a/ee/lib/ee/gitlab/auth/ldap/person.rb b/ee/lib/ee/gitlab/auth/ldap/person.rb index b6537724be2..e64d328e7fa 100644 --- a/ee/lib/ee/gitlab/auth/ldap/person.rb +++ b/ee/lib/ee/gitlab/auth/ldap/person.rb @@ -21,6 +21,11 @@ module EE nil end + def find_by_certificate_issuer_and_serial(issuer_dn, serial, adapter) + certificate_assertion = "{ serialNumber #{serial}, issuer \"#{issuer_dn}\" }" + adapter.user_by_certificate_assertion(certificate_assertion) + end + def find_by_kerberos_principal(principal, adapter) uid, domain = principal.split('@', 2) return nil unless uid && domain diff --git a/ee/lib/gitlab/auth/smartcard/base.rb b/ee/lib/gitlab/auth/smartcard/base.rb new file mode 100644 index 00000000000..c28f20f9f1b --- /dev/null +++ b/ee/lib/gitlab/auth/smartcard/base.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Smartcard + class Base + InvalidCAFilePath = Class.new(StandardError) + InvalidCertificate = Class.new(StandardError) + + delegate :allow_signup?, + to: :'Gitlab::CurrentSettings.current_application_settings' + + def self.store + @store ||= OpenSSL::X509::Store.new.tap do |store| + store.add_cert( + OpenSSL::X509::Certificate.new( + File.read(Gitlab.config.smartcard.ca_file))) + end + rescue Errno::ENOENT => ex + logger.error(message: 'Failed to open Gitlab.config.smartcard.ca_file', + error: ex) + + raise InvalidCAFilePath + rescue OpenSSL::X509::CertificateError => ex + logger.error(message: 'Gitlab.config.smartcard.ca_file is not a valid certificate', + error: ex) + + raise InvalidCertificate + end + + def self.logger + @logger ||= ::Gitlab::AuthLogger.build + end + + def initialize(certificate) + @certificate = OpenSSL::X509::Certificate.new(certificate) + rescue OpenSSL::X509::CertificateError + # no-op, certificate verification fails in this case in #valid? + end + + def find_or_create_user + return unless valid? + + user = find_user + user ||= create_user if allow_signup? + user + end + + private + + def valid? + self.class.store.verify(@certificate) if @certificate + end + end + end + end +end diff --git a/ee/lib/gitlab/auth/smartcard/certificate.rb b/ee/lib/gitlab/auth/smartcard/certificate.rb index b779941dd34..ac943a712d3 100644 --- a/ee/lib/gitlab/auth/smartcard/certificate.rb +++ b/ee/lib/gitlab/auth/smartcard/certificate.rb @@ -3,53 +3,15 @@ module Gitlab module Auth module Smartcard - class Certificate - InvalidCAFilePath = Class.new(StandardError) - InvalidCertificate = Class.new(StandardError) - - delegate :allow_signup?, - to: :'Gitlab::CurrentSettings.current_application_settings' - - def self.store - @store ||= OpenSSL::X509::Store.new.tap do |store| - store.add_cert( - OpenSSL::X509::Certificate.new( - File.read(Gitlab.config.smartcard.ca_file))) - end - rescue Errno::ENOENT => ex - Gitlab::AppLogger.error('Failed to open Gitlab.config.smartcard.ca_file') - Gitlab::AppLogger.error(ex) - raise InvalidCAFilePath - rescue OpenSSL::X509::CertificateError => ex - Gitlab::AppLogger.error('Gitlab.config.smartcard.ca_file is not a valid certificate') - Gitlab::AppLogger.error(ex) - raise InvalidCertificate - end - - def initialize(certificate) - @certificate = OpenSSL::X509::Certificate.new(certificate) - @subject = @certificate.subject.to_s - @issuer = @certificate.issuer.to_s - rescue OpenSSL::X509::CertificateError - # no-op - end - - def find_or_create_user - return unless valid? - - user = find_user - user ||= create_user if allow_signup? - user + class Certificate < Gitlab::Auth::Smartcard::Base + def auth_method + 'smartcard' end private - def valid? - self.class.store.verify(@certificate) if @certificate - end - def find_user - User.find_by_smartcard_identity(@subject, @issuer) + User.find_by_smartcard_identity(subject, issuer) end def create_user @@ -66,8 +28,8 @@ module Gitlab password: password, password_confirmation: password, password_automatically_set: true, - certificate_subject: @subject, - certificate_issuer: @issuer, + certificate_subject: subject, + certificate_issuer: issuer, skip_confirmation: true } @@ -75,15 +37,23 @@ module Gitlab end def create_smartcard_identity_for(user) - SmartcardIdentity.create(user: user, subject: @subject, issuer: @issuer) + SmartcardIdentity.create(user: user, subject: subject, issuer: issuer) + end + + def issuer + @certificate.issuer.to_s + end + + def subject + @certificate.subject.to_s end def common_name - @subject.split('/').find { |part| part =~ /CN=/ }&.remove('CN=')&.strip + subject.split('/').find { |part| part =~ /CN=/ }&.remove('CN=')&.strip end def email - @subject.split('/').find { |part| part =~ /emailAddress=/ }&.remove('emailAddress=')&.strip + subject.split('/').find { |part| part =~ /emailAddress=/ }&.remove('emailAddress=')&.strip end def username diff --git a/ee/lib/gitlab/auth/smartcard/ldap_certificate.rb b/ee/lib/gitlab/auth/smartcard/ldap_certificate.rb new file mode 100644 index 00000000000..3eaad930b02 --- /dev/null +++ b/ee/lib/gitlab/auth/smartcard/ldap_certificate.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Smartcard + class LDAPCertificate < Gitlab::Auth::Smartcard::Base + def initialize(provider, certificate) + super(certificate) + + @provider = provider + end + + def auth_method + 'smartcard_ldap' + end + + private + + def find_user + identity = Identity.find_by_extern_uid(@provider, ldap_user.dn) + identity&.user + end + + def create_user + user_params = { + name: ldap_user.name, + username: username, + email: ldap_user.email.first, + extern_uid: ldap_user.dn, + provider: @provider, + password: password, + password_confirmation: password, + password_automatically_set: true, + skip_confirmation: true + } + + Users::CreateService.new(nil, user_params).execute(skip_authorization: true) + end + + def adapter + @adapter ||= Gitlab::Auth::LDAP::Adapter.new(@provider) + end + + def ldap_user + @ldap_user ||= ::Gitlab::Auth::LDAP::Person.find_by_certificate_issuer_and_serial( + @certificate.issuer.to_s(OpenSSL::X509::Name::RFC2253), + @certificate.serial.to_s, + adapter) + end + + def username + ::Namespace.clean_path(ldap_user.username) + end + + def password + @password ||= Devise.friendly_token(8) + end + end + end + end +end diff --git a/ee/lib/gitlab/auth_logger.rb b/ee/lib/gitlab/auth_logger.rb new file mode 100644 index 00000000000..f6221e23e5a --- /dev/null +++ b/ee/lib/gitlab/auth_logger.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gitlab + class AuthLogger < Gitlab::JsonLogger + def self.file_name_noext + 'auth_json' + end + end +end diff --git a/ee/spec/features/users/login_spec.rb b/ee/spec/features/users/login_spec.rb index 6a8f842bd81..263c0979071 100644 --- a/ee/spec/features/users/login_spec.rb +++ b/ee/spec/features/users/login_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Login' do + include LdapHelpers include UserLoginHelper before do @@ -28,22 +29,30 @@ describe 'Login' do .to change { SecurityEvent.where(entity_id: -1).count }.from(0).to(1) end - describe 'UI tabs and panes' do - context 'when smartcard is enabled' do - before do - visit new_user_session_path - allow(page).to receive(:form_based_providers).and_return([:smartcard]) - allow(page).to receive(:smartcard_enabled?).and_return(true) - end + describe 'smartcard authentication' do + before do + allow(Gitlab.config.smartcard).to receive(:enabled).and_return(true) + end + subject { visit new_user_session_path } + + context 'when smartcard is enabled' do context 'with smartcard_auth feature flag off' do before do stub_licensed_features(smartcard_auth: false) end it 'correctly renders tabs and panes' do + subject + ensure_tab_pane_correctness(false) end + + it 'does not show smartcard login form' do + subject + + expect(page).not_to have_selector('.nav-tabs a[href="#smartcard"]') + end end context 'with smartcard_auth feature flag on' do @@ -52,9 +61,91 @@ describe 'Login' do end it 'correctly renders tabs and panes' do - ensure_tab_pane_correctness(false) + subject + + expect(page.all('.nav-tabs a[data-toggle="tab"]').length).to be(3) + + ensure_one_active_tab + ensure_one_active_pane + end + + it 'shows smartcard login form' do + subject + + expect(page).to have_selector('.nav-tabs a[href="#smartcard"]') end end end end + + describe 'smartcard authentication against LDAP server' do + let(:ldap_server_config) do + { + 'provider_name' => 'ldapmain', + 'attributes' => {}, + 'encryption' => 'plain', + 'smartcard_auth' => smartcard_auth_status, + 'uid' => 'uid', + 'base' => 'dc=example,dc=com' + } + end + + subject { visit new_user_session_path } + + before do + stub_licensed_features(smartcard_auth: true) + stub_ldap_setting(enabled: true) + allow(Gitlab.config.smartcard).to receive(:enabled).and_return(true) + allow(::Gitlab::Auth::LDAP::Config).to receive_messages(enabled: true, servers: [ldap_server_config]) + allow_any_instance_of(ActionDispatch::Routing::RoutesProxy) + .to receive(:user_ldapmain_omniauth_callback_path) + .and_return('/users/auth/ldapmain/callback') + end + + context 'when smartcard auth is optional' do + let(:smartcard_auth_status) { 'optional' } + + it 'correctly renders tabs and panes' do + subject + + ensure_one_active_tab + ensure_one_active_pane + end + + it 'shows LDAP login form' do + subject + + expect(page).to have_selector('#ldapmain.tab-pane form#new_ldap_user') + end + + it 'shows LDAP smartcard login form' do + subject + + expect(page).to have_selector('#ldapmain_smartcard input[value="Sign in with smart card"]') + end + end + + context 'when smartcard auth is required' do + let(:smartcard_auth_status) { 'required' } + + it 'correctly renders tabs and panes' do + subject + + ensure_one_active_tab + ensure_one_active_pane + end + + it 'does not show LDAP login form' do + subject + + expect(page).not_to have_selector('#ldapmain.tab-pane form#new_ldap_user') + end + + it 'shows LDAP smartcard login form' do + subject + + expect(page).to have_selector('#ldapmain_smartcard input[value="Sign in with smart card"]') + end + end + end end diff --git a/ee/spec/helpers/ee/auth_helper_spec.rb b/ee/spec/helpers/ee/auth_helper_spec.rb index 3826175e6f9..d481a2f14d8 100644 --- a/ee/spec/helpers/ee/auth_helper_spec.rb +++ b/ee/spec/helpers/ee/auth_helper_spec.rb @@ -28,4 +28,104 @@ describe EE::AuthHelper do end end end + + describe 'smartcard_enabled_for_ldap?' do + let(:provider_name) { 'ldapmain' } + let(:ldap_server_config) do + { + 'provider_name' => provider_name, + 'attributes' => {}, + 'encryption' => 'plain', + 'smartcard_auth' => smartcard_auth_status, + 'uid' => 'uid', + 'base' => 'dc=example,dc=com' + } + end + + before do + allow(::Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true) + allow(::Gitlab::Auth::LDAP::Config).to receive(:servers).and_return([ldap_server_config]) + end + + context 'LDAP server with optional smartcard auth' do + let(:smartcard_auth_status) { 'optional' } + + it 'returns true' do + expect(smartcard_enabled_for_ldap?(provider_name, required: false)).to be(true) + end + + it 'returns false with required flag' do + expect(smartcard_enabled_for_ldap?(provider_name, required: true)).to be(false) + end + end + + context 'LDAP server with required smartcard auth' do + let(:smartcard_auth_status) { 'required' } + + it 'returns true' do + expect(smartcard_enabled_for_ldap?(provider_name, required: false)).to be(true) + end + + it 'returns true with required flag' do + expect(smartcard_enabled_for_ldap?(provider_name, required: true)).to be(true) + end + end + + context 'LDAP server with disabled smartcard auth' do + let(:smartcard_auth_status) { false } + + it 'returns false' do + expect(smartcard_enabled_for_ldap?(provider_name, required: false)).to be(false) + end + + it 'returns false with required flag' do + expect(smartcard_enabled_for_ldap?(provider_name, required: true)).to be(false) + end + end + + context 'no matching LDAP server' do + let(:smartcard_auth_status) { 'optional' } + + it 'returns false' do + expect(smartcard_enabled_for_ldap?('nonexistent')).to be(false) + end + end + end + + describe 'smartcard_login_button_classes' do + let(:provider_name) { 'ldapmain' } + let(:ldap_server_config) do + { + 'provider_name' => provider_name, + 'attributes' => {}, + 'encryption' => 'plain', + 'smartcard_auth' => smartcard_auth_status, + 'uid' => 'uid', + 'base' => 'dc=example,dc=com' + } + end + + subject { smartcard_login_button_classes(provider_name) } + + before do + allow(::Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true) + allow(::Gitlab::Auth::LDAP::Config).to receive(:servers).and_return([ldap_server_config]) + end + + context 'when smartcard auth is optional' do + let(:smartcard_auth_status) { 'optional' } + + it 'returns the correct CSS classes' do + expect(subject).to eql('btn btn-success btn-inverted') + end + end + + context 'when smartcard auth is required' do + let(:smartcard_auth_status) { 'required' } + + it 'returns the correct CSS classes' do + expect(subject).to eql('btn btn-success') + end + end + end end diff --git a/ee/spec/lib/gitlab/auth/ldap/adapter_spec.rb b/ee/spec/lib/gitlab/auth/ldap/adapter_spec.rb index 5cb4d743bad..ee4dba121d3 100644 --- a/ee/spec/lib/gitlab/auth/ldap/adapter_spec.rb +++ b/ee/spec/lib/gitlab/auth/ldap/adapter_spec.rb @@ -39,4 +39,40 @@ describe Gitlab::Auth::LDAP::Adapter do expect(results.first.member_dns).to match_array(%w(uid=john uid=mary)) end end + + describe '#user_by_certificate_assertion' do + let(:certificate_assertion) { 'certificate_assertion' } + + subject { adapter.user_by_certificate_assertion(certificate_assertion) } + + context 'return value' do + let(:entry) { ldap_user_entry('john') } + + before do + allow(adapter).to receive(:ldap_search).and_return([entry]) + end + + it 'returns a person object' do + expect(subject).to be_a(::EE::Gitlab::Auth::LDAP::Person) + end + + it 'returns correct attributes' do + result = subject + + expect(result.uid).to eq('john') + expect(result.dn).to eq('uid=john,ou=users,dc=example,dc=com') + end + end + + it 'searches with the proper options' do + expect(adapter).to receive(:ldap_search).with( + { attributes: array_including('dn', 'cn', 'mail', 'uid', 'userid'), + base: 'dc=example,dc=com', + filter: Net::LDAP::Filter.ex( + 'userCertificate:certificateExactMatch', certificate_assertion) } + ).and_return({}) + + subject + end + end end diff --git a/ee/spec/lib/gitlab/auth/ldap/person_spec.rb b/ee/spec/lib/gitlab/auth/ldap/person_spec.rb index b87afaf29e3..7aa67e73216 100644 --- a/ee/spec/lib/gitlab/auth/ldap/person_spec.rb +++ b/ee/spec/lib/gitlab/auth/ldap/person_spec.rb @@ -36,6 +36,18 @@ describe Gitlab::Auth::LDAP::Person do end end + describe '.find_by_certificate_issuer_and_serial' do + it 'searches by certificate assertion' do + adapter = ldap_adapter + serial = 'serial' + issuer_dn = 'issuer_dn' + + expect(adapter).to receive(:user_by_certificate_assertion).with("{ serialNumber #{serial}, issuer \"#{issuer_dn}\" }") + + described_class.find_by_certificate_issuer_and_serial(issuer_dn, serial, adapter) + end + end + describe '.find_by_kerberos_principal' do let(:adapter) { ldap_adapter } let(:username) { 'foo' } diff --git a/ee/spec/lib/gitlab/auth/smartcard/certificate_spec.rb b/ee/spec/lib/gitlab/auth/smartcard/certificate_spec.rb index a9765419ef7..84709676692 100644 --- a/ee/spec/lib/gitlab/auth/smartcard/certificate_spec.rb +++ b/ee/spec/lib/gitlab/auth/smartcard/certificate_spec.rb @@ -119,57 +119,8 @@ describe Gitlab::Auth::Smartcard::Certificate do end end - context 'invalid certificate' do - before do - allow(openssl_certificate_store).to receive(:verify).and_return(false) - end - - it 'returns nil' do - expect(subject).to be_nil - end - end - - context 'incorrect certificate' do - before do - allow(OpenSSL::X509::Certificate).to receive(:new).and_call_original - end - - it 'returns nil' do - expect(subject).to be_nil - end - end + it_behaves_like 'a valid certificate is required' end - describe '.store' do - before do - allow(Gitlab.config.smartcard).to receive(:ca_file).and_return('ca_file') - allow(described_class).to receive(:store).and_call_original - allow(OpenSSL::X509::Certificate).to receive(:new).and_call_original - clear_store - end - after do - clear_store - end - - subject { described_class.store } - - context 'file does not exist' do - it 'raises error' do - expect { subject }.to raise_error(Gitlab::Auth::Smartcard::Certificate::InvalidCAFilePath) - end - end - - context 'smartcard.ca_file is not a valid certificate' do - it 'raises error' do - expect(File).to receive(:read).with('ca_file').and_return('invalid certificate') - expect { subject }.to raise_error(Gitlab::Auth::Smartcard::Certificate::InvalidCertificate) - end - end - end - - def clear_store - described_class.remove_instance_variable(:@store) - rescue NameError - # raised if @store was not set; ignore - end + it_behaves_like 'a certificate store' end diff --git a/ee/spec/lib/gitlab/auth/smartcard/ldap_certificate_spec.rb b/ee/spec/lib/gitlab/auth/smartcard/ldap_certificate_spec.rb new file mode 100644 index 00000000000..3599bf90528 --- /dev/null +++ b/ee/spec/lib/gitlab/auth/smartcard/ldap_certificate_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Auth::Smartcard::LDAPCertificate do + let(:certificate_header) { 'certificate' } + let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) } + let(:user_build_service) { instance_double(Users::BuildService) } + let(:subject_ldap_dn) { 'subject_ldap_dn' } + let(:issuer) { instance_double(OpenSSL::X509::Name, to_s: 'issuer_dn') } + let(:openssl_certificate) do + instance_double(OpenSSL::X509::Certificate, + { issuer: issuer, + serial: '42' } ) + end + let(:ldap_provider) { 'ldapmain' } + let(:ldap_connection) { instance_double(::Net::LDAP) } + let(:ldap_person_name) { 'John Doe' } + let(:ldap_person_email) { 'john.doe@example.com' } + let(:ldap_entry) do + Net::LDAP::Entry.new.tap do |entry| + entry['dn'] = subject_ldap_dn + entry['uid'] = 'john doe' + entry['cn'] = ldap_person_name + entry['mail'] = ldap_person_email + end + end + let(:ldap_person) { ::Gitlab::Auth::LDAP::Person.new(ldap_entry, ldap_provider) } + + before do + allow(described_class).to( + receive(:store).and_return(openssl_certificate_store)) + allow(OpenSSL::X509::Certificate).to( + receive(:new).and_return(openssl_certificate)) + allow(openssl_certificate_store).to( + receive(:verify).and_return(true)) + allow(Net::LDAP).to receive(:new).and_return(ldap_connection) + allow(ldap_connection).to receive(:search).and_return([ldap_entry]) + end + + describe '#find_or_create_user' do + subject { described_class.new(ldap_provider, certificate_header).find_or_create_user } + + context 'user already exists' do + let(:user) { create(:user) } + + before do + create(:identity, { provider: ldap_provider, + extern_uid: subject_ldap_dn, + user: user }) + end + + it 'finds existing user' do + expect(subject).to eql(user) + end + + it 'does not create new user' do + expect { subject }.not_to change { User.count } + end + end + + context 'user does not exist' do + let(:user) { create(:user) } + + it 'creates user' do + expect { subject }.to change { User.count }.from(0).to(1) + end + + it 'creates user with correct attributes' do + subject + + user = User.find_by(username: 'johndoe') + + expect(user).not_to be_nil + expect(user.email).to eql(ldap_person_email) + end + + it 'creates identity' do + expect { subject }.to change { Identity.count }.from(0).to(1) + end + + it 'creates identity with correct attributes' do + subject + + identity = Identity.find_by(provider: ldap_provider, extern_uid: subject_ldap_dn) + + expect(identity).not_to be_nil + end + + it 'calls Users::BuildService with correct params' do + user_params = { name: ldap_person_name, + username: 'johndoe', + email: ldap_person_email, + extern_uid: 'subject_ldap_dn', + provider: ldap_provider, + password_automatically_set: true, + skip_confirmation: true } + + expect(Users::BuildService).to( + receive(:new) + .with(nil, hash_including(user_params)) + .and_return(user_build_service)) + expect(user_build_service).to( + receive(:execute).with(skip_authorization: true).and_return(user)) + + subject + end + + context 'username generation' do + context 'uses LDAP uid' do + it 'creates user with correct username' do + subject + + user = User.find_by(username: 'johndoe') + expect(user).not_to be_nil + end + end + + context 'avoids conflicting namespaces' do + let!(:existing_user) { create(:user, username: 'johndoe') } + + it 'creates user with correct username' do + expect { subject }.to change { User.count }.from(1).to(2) + expect(User.last.username).to eql('johndoe1') + end + end + end + end + + it_behaves_like 'a valid certificate is required' + end + + it_behaves_like 'a certificate store' +end diff --git a/ee/spec/requests/smartcard_controller_spec.rb b/ee/spec/requests/smartcard_controller_spec.rb index d8c0b2bb979..a64ce03996d 100644 --- a/ee/spec/requests/smartcard_controller_spec.rb +++ b/ee/spec/requests/smartcard_controller_spec.rb @@ -3,24 +3,14 @@ require 'spec_helper' describe SmartcardController, type: :request do - let(:subject_dn) { '/O=Random Corp Ltd/CN=gitlab-user/emailAddress=gitlab-user@random-corp.org' } - let(:issuer_dn) { '/O=Random Corp Ltd/CN=Random Corp' } + include LdapHelpers + let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } } let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) } - let(:openssl_certificate) { instance_double(OpenSSL::X509::Certificate, subject: subject_dn, issuer: issuer_dn) } let(:audit_event_service) { instance_double(AuditEventService) } - subject { post '/-/smartcard/auth', params: {}, headers: certificate_headers } - - describe '#auth' do + shared_examples 'a client certificate authentication' do |auth_method| context 'with smartcard_auth enabled' do - before do - allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true) - allow(Gitlab::Auth::Smartcard::Certificate).to receive(:store).and_return(openssl_certificate_store) - allow(OpenSSL::X509::Certificate).to receive(:new).and_return(openssl_certificate) - allow(openssl_certificate_store).to receive(:verify).and_return(true) - end - it 'allows sign in' do subject @@ -36,7 +26,7 @@ describe SmartcardController, type: :request do it 'logs audit event' do expect(AuditEventService).to( receive(:new) - .with(instance_of(User), instance_of(User), with: 'smartcard') + .with(instance_of(User), instance_of(User), with: auth_method) .and_return(audit_event_service)) expect(audit_event_service).to receive_message_chain(:for_authentication, :security_event) @@ -63,66 +53,159 @@ describe SmartcardController, type: :request do end end end + end - context 'user already exists' do - before do - user = create(:user) - create(:smartcard_identity, subject: subject_dn, issuer: issuer_dn, user: user) - end + context 'with smartcard_auth disabled' do + before do + allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(false) + end - it 'finds existing user' do - expect { subject }.not_to change { User.count } - expect(request.env['warden']).to be_authenticated - end + it 'renders 404' do + subject + + expect(response).to have_gitlab_http_status(404) end + end + end - context 'certificate header formats from NGINX' do - shared_examples 'valid certificate header' do - it 'authenticates user' do - expect(Gitlab::Auth::Smartcard::Certificate).to receive(:new).with(expected_certificate).and_call_original + describe '#auth' do + let(:subject_dn) { '/O=Random Corp Ltd/CN=gitlab-user/emailAddress=gitlab-user@random-corp.org' } + let(:issuer_dn) { '/O=Random Corp Ltd/CN=Random Corp' } + let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } } + let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) } + let(:openssl_certificate) { instance_double(OpenSSL::X509::Certificate, subject: subject_dn, issuer: issuer_dn) } + let(:audit_event_service) { instance_double(AuditEventService) } + + before do + allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true) + allow(Gitlab::Auth::Smartcard::Certificate).to receive(:store).and_return(openssl_certificate_store) + allow(openssl_certificate_store).to receive(:verify).and_return(true) + allow(OpenSSL::X509::Certificate).to receive(:new).and_return(openssl_certificate) + end - subject + subject { post '/-/smartcard/auth', params: {}, headers: certificate_headers } - expect(request.env['warden']).to be_authenticated - end - end + it_behaves_like 'a client certificate authentication', 'smartcard' - let(:expected_certificate) { "-----BEGIN CERTIFICATE-----\nrow\nrow\n-----END CERTIFICATE-----" } + context 'user already exists' do + before do + user = create(:user) + create(:smartcard_identity, subject: subject_dn, issuer: issuer_dn, user: user) + end - context 'escaped format' do - let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN%20CERTIFICATE-----%0Arow%0Arow%0A-----END%20CERTIFICATE-----' } } + it 'finds existing user' do + expect { subject }.not_to change { User.count } + expect(request.env['warden']).to be_authenticated + end + end - it_behaves_like 'valid certificate header' - end + context 'certificate header formats from NGINX' do + shared_examples 'valid certificate header' do + it 'authenticates user' do + expect(Gitlab::Auth::Smartcard::Certificate).to receive(:new).with(expected_certificate).and_call_original - context 'deprecated format' do - let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN CERTIFICATE----- row row -----END CERTIFICATE-----' } } + subject - it_behaves_like 'valid certificate header' + expect(request.env['warden']).to be_authenticated end end - context 'missing certificate headers' do - let(:certificate_headers) { nil } + let(:expected_certificate) { "-----BEGIN CERTIFICATE-----\nrow\nrow\n-----END CERTIFICATE-----" } - it 'renders 401' do - subject + context 'escaped format' do + let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN%20CERTIFICATE-----%0Arow%0Arow%0A-----END%20CERTIFICATE-----' } } - expect(response).to have_gitlab_http_status(401) - expect(request.env['warden']).not_to be_authenticated - end + it_behaves_like 'valid certificate header' end - end - context 'with smartcard_auth disabled' do - before do - allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(false) + context 'deprecated format' do + let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': '-----BEGIN CERTIFICATE----- row row -----END CERTIFICATE-----' } } + + it_behaves_like 'valid certificate header' end + end - it 'renders 404' do + context 'missing certificate headers' do + let(:certificate_headers) { nil } + + it 'renders 401' do subject - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(401) + expect(request.env['warden']).not_to be_authenticated + end + end + end + + describe '#ldap_auth ' do + let(:subject_ldap_dn) { 'uid=john doe,ou=people,dc=example,dc=com' } + let(:issuer_dn) { 'CN=Random Corp,O=Random Corp Ltd,C=US' } + let(:issuer) { instance_double(OpenSSL::X509::Name, to_s: issuer_dn) } + let(:serial) { '42' } + let(:openssl_certificate) do + instance_double(OpenSSL::X509::Certificate, + issuer: issuer, serial: serial) + end + + let(:ldap_connection) { instance_double(::Net::LDAP) } + let(:ldap_entry) do + Net::LDAP::Entry.new.tap do |entry| + entry['dn'] = subject_ldap_dn + entry['uid'] = 'john doe' + entry['cn'] = 'John Doe' + entry['mail'] = 'john.doe@example.com' + end + end + let(:ldap_user_search_scope) { 'dc=example,dc=com' } + let(:ldap_search_params) do + { attributes: array_including('dn', 'cn', 'mail', 'uid', 'userid'), + base: ldap_user_search_scope, + filter: Net::LDAP::Filter.ex( + 'userCertificate:certificateExactMatch', + "{ serialNumber #{serial}, issuer \"#{issuer_dn}\" }") } + end + + subject do + post('/-/smartcard/ldap_auth', + { params: { provider: 'ldapmain' }, + headers: certificate_headers } ) + end + + before do + allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true) + + allow(Gitlab::Auth::Smartcard::LDAPCertificate).to( + receive(:store).and_return(openssl_certificate_store)) + allow(openssl_certificate_store).to receive(:verify).and_return(true) + + allow(OpenSSL::X509::Certificate).to( + receive(:new).and_return(openssl_certificate)) + + allow(Net::LDAP).to receive(:new).and_return(ldap_connection) + allow(ldap_connection).to( + receive(:search).with(ldap_search_params).and_return([ldap_entry])) + end + + it_behaves_like 'a client certificate authentication', 'smartcard_ldap' + + it 'sets correct parameters for LDAP search' do + expect(ldap_connection).to( + receive(:search).with(ldap_search_params).and_return([ldap_entry])) + + subject + end + + context 'user already exists' do + before do + user = create(:user) + create(:identity, { provider: 'ldapmain', + extern_uid: subject_ldap_dn, + user: user }) + end + + it 'finds existing user' do + expect { subject }.not_to change { User.count } + expect(request.env['warden']).to be_authenticated end end end diff --git a/ee/spec/support/shared_examples/smartcard_certificate_store.rb b/ee/spec/support/shared_examples/smartcard_certificate_store.rb new file mode 100644 index 00000000000..8a356d5d483 --- /dev/null +++ b/ee/spec/support/shared_examples/smartcard_certificate_store.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +shared_examples 'a certificate store' do + describe '.store' do + before do + allow(Gitlab.config.smartcard).to receive(:ca_file).and_return('ca_file') + allow(described_class).to receive(:store).and_call_original + allow(OpenSSL::X509::Certificate).to receive(:new).and_call_original + + clear_store + end + + after do + clear_store + end + + subject { described_class.store } + + context 'file does not exist' do + it 'raises error' do + expect { subject }.to( + raise_error(Gitlab::Auth::Smartcard::Certificate::InvalidCAFilePath)) + end + end + + context 'smartcard ca_file is not a valid certificate' do + it 'raises error' do + expect(File).to( + receive(:read).with('ca_file').and_return('invalid certificate')) + expect { subject }.to( + raise_error(Gitlab::Auth::Smartcard::Certificate::InvalidCertificate)) + end + end + end + + def clear_store + described_class.remove_instance_variable(:@store) + rescue NameError + # raised if @store was not set; ignore + end +end + +shared_examples 'a valid certificate is required' do + context 'invalid certificate' do + it 'returns nil' do + allow(openssl_certificate_store).to receive(:verify).and_return(false) + + expect(subject).to be_nil + end + end + + context 'incorrect certificate' do + it 'returns nil' do + allow(OpenSSL::X509::Certificate).to receive(:new).and_call_original + + expect(subject).to be_nil + end + end +end diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb index 8c001b18e13..32ab89d8786 100644 --- a/lib/gitlab/auth/ldap/adapter.rb +++ b/lib/gitlab/auth/ldap/adapter.rb @@ -32,14 +32,7 @@ module Gitlab def users(fields, value, limit = nil) options = user_options(Array(fields), value, limit) - - entries = ldap_search(options).select do |entry| - entry.respond_to? config.uid - end - - entries.map do |entry| - Gitlab::Auth::LDAP::Person.new(entry, provider) - end + users_search(options) end def user(*args) @@ -92,6 +85,16 @@ module Gitlab SEARCH_RETRY_FACTOR[retry_number] * config.timeout end + def users_search(options) + entries = ldap_search(options).select do |entry| + entry.respond_to? config.uid + end + + entries.map do |entry| + Gitlab::Auth::LDAP::Person.new(entry, provider) + end + end + def user_options(fields, value, limit) options = { attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 81b1ffad3df..ce9f9880d4e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8363,12 +8363,18 @@ msgstr "" msgid "Sign in to \"%{group_name}\"" msgstr "" +msgid "Sign in using smart card" +msgstr "" + msgid "Sign in via 2FA code" msgstr "" msgid "Sign in with Single Sign-On" msgstr "" +msgid "Sign in with smart card" +msgstr "" + msgid "Sign out" msgstr "" @@ -9933,6 +9939,9 @@ msgstr "" msgid "Use your global notification setting" msgstr "" +msgid "Use your smart card to authenticate with the LDAP server." +msgstr "" + msgid "Used by members to sign in to your group in GitLab" msgstr "" -- 2.30.9