Commit fba9dc27 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'if-7693-smartcard_ldap_integration' into 'master'

Add LDAP integration to smartcard authentication

See merge request gitlab-org/gitlab-ee!9235
parents 67f56ef9 cad3e2cc
- 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_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do
.form-group .form-group
= label_tag :username, "#{server['label']} Username" = label_tag :username, "#{server['label']} Username"
......
...@@ -458,6 +458,10 @@ production: &base ...@@ -458,6 +458,10 @@ production: &base
# A value of 0 means there is no timeout. # A value of 0 means there is no timeout.
timeout: 10 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. # This setting specifies if LDAP server is Active Directory LDAP server.
# For non AD servers it skips the AD specific queries. # For non AD servers it skips the AD specific queries.
# If your LDAP server is not AD, set this to false. # If your LDAP server is not AD, set this to false.
......
...@@ -27,6 +27,7 @@ if Settings.ldap['enabled'] || Rails.env.test? ...@@ -27,6 +27,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
server['timeout'] ||= 10.seconds server['timeout'] ||= 10.seconds
server['block_auto_created_users'] = false if server['block_auto_created_users'].nil? 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['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['active_directory'] = true if server['active_directory'].nil?
server['attributes'] = {} if server['attributes'].nil? server['attributes'] = {} if server['attributes'].nil?
server['lowercase_usernames'] = false if server['lowercase_usernames'].nil? server['lowercase_usernames'] = false if server['lowercase_usernames'].nil?
...@@ -52,6 +53,7 @@ end ...@@ -52,6 +53,7 @@ end
Settings['smartcard'] ||= Settingslogic.new({}) Settings['smartcard'] ||= Settingslogic.new({})
Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? 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'] ||= Settingslogic.new({})
Settings.omniauth['enabled'] = true if Settings.omniauth['enabled'].nil? Settings.omniauth['enabled'] = true if Settings.omniauth['enabled'].nil?
......
# Smartcard authentication # 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 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/726) in
[GitLab Premium](https://about.gitlab.com/pricing/) 11.6 as an experimental [GitLab Premium](https://about.gitlab.com/pricing/) 11.6 as an experimental
feature. Smartcard authentication may change or be removed completely in future feature. Smartcard authentication against local databases may change or be
releases. removed completely in future releases.
Smartcards with X.509 certificates can be used to authenticate with GitLab. 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 against a local
database with GitLab, `CN` and `emailAddress` must be defined in the
To use a smartcard with an X.509 certificate to authenticate with GitLab, `CN` certificate. For example:
and `emailAddress` must be defined in the certificate. For example:
``` ```
Certificate: Certificate:
...@@ -25,6 +35,21 @@ Certificate: ...@@ -25,6 +35,21 @@ Certificate:
Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com 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 ## Configure GitLab for smartcard authentication
**For Omnibus installations** **For Omnibus installations**
...@@ -122,3 +147,40 @@ Certificate: ...@@ -122,3 +147,40 @@ Certificate:
1. Save the file and [restart](../restart_gitlab.md#installations-from-source) 1. Save the file and [restart](../restart_gitlab.md#installations-from-source)
GitLab for the changes to take effect. 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.
...@@ -9,7 +9,17 @@ class SmartcardController < ApplicationController ...@@ -9,7 +9,17 @@ class SmartcardController < ApplicationController
def auth def auth
certificate = Gitlab::Auth::Smartcard::Certificate.new(certificate_header) 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 user = certificate.find_or_create_user
unless user unless user
flash[:alert] = _('Failed to signing using smartcard authentication') flash[:alert] = _('Failed to signing using smartcard authentication')
...@@ -18,12 +28,10 @@ class SmartcardController < ApplicationController ...@@ -18,12 +28,10 @@ class SmartcardController < ApplicationController
return return
end end
log_audit_event(user, with: 'smartcard') log_audit_event(user, with: certificate.auth_method)
sign_in_and_redirect(user) sign_in_and_redirect(user)
end end
protected
def check_feature_availability def check_feature_availability
render_404 unless ::Gitlab::Auth::Smartcard.enabled? render_404 unless ::Gitlab::Auth::Smartcard.enabled?
end end
......
...@@ -49,6 +49,10 @@ module EE ...@@ -49,6 +49,10 @@ module EE
end end
end end
def smartcard_config_port
::Gitlab.config.smartcard.client_certificate_required_port
end
def page_class def page_class
class_names = super class_names = super
class_names += system_message_class class_names += system_message_class
......
...@@ -50,6 +50,27 @@ module EE ...@@ -50,6 +50,27 @@ module EE
::Gitlab::Auth::Smartcard.enabled? ::Gitlab::Auth::Smartcard.enabled?
end 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? def group_saml_enabled?
auth_providers.include?(:group_saml) auth_providers.include?(:group_saml)
end end
......
...@@ -20,6 +20,10 @@ module EE ...@@ -20,6 +20,10 @@ module EE
end end
class_methods do class_methods do
def find_by_extern_uid(provider, extern_uid)
with_extern_uid(provider, extern_uid).take
end
def preload_saml_group def preload_saml_group
preload(saml_provider: { group: :route }) preload(saml_provider: { group: :route })
end end
......
- 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
- if smartcard_enabled? - if smartcard_enabled?
.login-box.tab-pane{ id: 'smartcard', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:smartcard)) } .login-box.tab-pane{ id: 'smartcard', role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:smartcard)) }
.login-body .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-container
= submit_tag _('Login with smartcard'), class: 'btn btn-success' = submit_tag _('Login with smartcard'), class: 'btn btn-success'
- 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']))
---
title: Add LDAP integration to smartcard authentication
merge_request: 9235
author:
type: added
# frozen_string_literal: true # frozen_string_literal: true
post 'smartcard/auth' => 'smartcard#auth' post 'smartcard/auth' => 'smartcard#auth'
post 'smartcard/ldap_auth' => 'smartcard#ldap_auth'
...@@ -55,6 +55,25 @@ module EE ...@@ -55,6 +55,25 @@ module EE
LDAP::Group.new(entry, self) LDAP::Group.new(entry, self)
end end
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 end
end end
......
...@@ -21,6 +21,11 @@ module EE ...@@ -21,6 +21,11 @@ module EE
nil nil
end 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) def find_by_kerberos_principal(principal, adapter)
uid, domain = principal.split('@', 2) uid, domain = principal.split('@', 2)
return nil unless uid && domain return nil unless uid && domain
......
# 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
...@@ -3,53 +3,15 @@ ...@@ -3,53 +3,15 @@
module Gitlab module Gitlab
module Auth module Auth
module Smartcard module Smartcard
class Certificate class Certificate < Gitlab::Auth::Smartcard::Base
InvalidCAFilePath = Class.new(StandardError) def auth_method
InvalidCertificate = Class.new(StandardError) 'smartcard'
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
end end
private private
def valid?
self.class.store.verify(@certificate) if @certificate
end
def find_user def find_user
User.find_by_smartcard_identity(@subject, @issuer) User.find_by_smartcard_identity(subject, issuer)
end end
def create_user def create_user
...@@ -66,8 +28,8 @@ module Gitlab ...@@ -66,8 +28,8 @@ module Gitlab
password: password, password: password,
password_confirmation: password, password_confirmation: password,
password_automatically_set: true, password_automatically_set: true,
certificate_subject: @subject, certificate_subject: subject,
certificate_issuer: @issuer, certificate_issuer: issuer,
skip_confirmation: true skip_confirmation: true
} }
...@@ -75,15 +37,23 @@ module Gitlab ...@@ -75,15 +37,23 @@ module Gitlab
end end
def create_smartcard_identity_for(user) 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 end
def common_name def common_name
@subject.split('/').find { |part| part =~ /CN=/ }&.remove('CN=')&.strip subject.split('/').find { |part| part =~ /CN=/ }&.remove('CN=')&.strip
end end
def email def email
@subject.split('/').find { |part| part =~ /emailAddress=/ }&.remove('emailAddress=')&.strip subject.split('/').find { |part| part =~ /emailAddress=/ }&.remove('emailAddress=')&.strip
end end
def username def username
......
# 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
# frozen_string_literal: true
module Gitlab
class AuthLogger < Gitlab::JsonLogger
def self.file_name_noext
'auth_json'
end
end
end
require 'spec_helper' require 'spec_helper'
describe 'Login' do describe 'Login' do
include LdapHelpers
include UserLoginHelper include UserLoginHelper
before do before do
...@@ -28,22 +29,30 @@ describe 'Login' do ...@@ -28,22 +29,30 @@ describe 'Login' do
.to change { SecurityEvent.where(entity_id: -1).count }.from(0).to(1) .to change { SecurityEvent.where(entity_id: -1).count }.from(0).to(1)
end end
describe 'UI tabs and panes' do describe 'smartcard authentication' do
context 'when smartcard is enabled' do
before do before do
visit new_user_session_path allow(Gitlab.config.smartcard).to receive(:enabled).and_return(true)
allow(page).to receive(:form_based_providers).and_return([:smartcard])
allow(page).to receive(:smartcard_enabled?).and_return(true)
end end
subject { visit new_user_session_path }
context 'when smartcard is enabled' do
context 'with smartcard_auth feature flag off' do context 'with smartcard_auth feature flag off' do
before do before do
stub_licensed_features(smartcard_auth: false) stub_licensed_features(smartcard_auth: false)
end end
it 'correctly renders tabs and panes' do it 'correctly renders tabs and panes' do
subject
ensure_tab_pane_correctness(false) ensure_tab_pane_correctness(false)
end end
it 'does not show smartcard login form' do
subject
expect(page).not_to have_selector('.nav-tabs a[href="#smartcard"]')
end
end end
context 'with smartcard_auth feature flag on' do context 'with smartcard_auth feature flag on' do
...@@ -52,8 +61,90 @@ describe 'Login' do ...@@ -52,8 +61,90 @@ describe 'Login' do
end end
it 'correctly renders tabs and panes' do 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 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
end end
......
...@@ -28,4 +28,104 @@ describe EE::AuthHelper do ...@@ -28,4 +28,104 @@ describe EE::AuthHelper do
end end
end 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 end
...@@ -39,4 +39,40 @@ describe Gitlab::Auth::LDAP::Adapter do ...@@ -39,4 +39,40 @@ describe Gitlab::Auth::LDAP::Adapter do
expect(results.first.member_dns).to match_array(%w(uid=john uid=mary)) expect(results.first.member_dns).to match_array(%w(uid=john uid=mary))
end end
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 end
...@@ -36,6 +36,18 @@ describe Gitlab::Auth::LDAP::Person do ...@@ -36,6 +36,18 @@ describe Gitlab::Auth::LDAP::Person do
end end
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 describe '.find_by_kerberos_principal' do
let(:adapter) { ldap_adapter } let(:adapter) { ldap_adapter }
let(:username) { 'foo' } let(:username) { 'foo' }
......
...@@ -119,57 +119,8 @@ describe Gitlab::Auth::Smartcard::Certificate do ...@@ -119,57 +119,8 @@ describe Gitlab::Auth::Smartcard::Certificate do
end end
end end
context 'invalid certificate' do it_behaves_like 'a valid certificate is required'
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
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 end
def clear_store it_behaves_like 'a certificate store'
described_class.remove_instance_variable(:@store)
rescue NameError
# raised if @store was not set; ignore
end
end end
# 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
...@@ -3,24 +3,14 @@ ...@@ -3,24 +3,14 @@
require 'spec_helper' require 'spec_helper'
describe SmartcardController, type: :request do describe SmartcardController, type: :request do
let(:subject_dn) { '/O=Random Corp Ltd/CN=gitlab-user/emailAddress=gitlab-user@random-corp.org' } include LdapHelpers
let(:issuer_dn) { '/O=Random Corp Ltd/CN=Random Corp' }
let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } } let(:certificate_headers) { { 'X-SSL-CLIENT-CERTIFICATE': 'certificate' } }
let(:openssl_certificate_store) { instance_double(OpenSSL::X509::Store) } 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) } let(:audit_event_service) { instance_double(AuditEventService) }
subject { post '/-/smartcard/auth', params: {}, headers: certificate_headers } shared_examples 'a client certificate authentication' do |auth_method|
describe '#auth' do
context 'with smartcard_auth enabled' do 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 it 'allows sign in' do
subject subject
...@@ -36,7 +26,7 @@ describe SmartcardController, type: :request do ...@@ -36,7 +26,7 @@ describe SmartcardController, type: :request do
it 'logs audit event' do it 'logs audit event' do
expect(AuditEventService).to( expect(AuditEventService).to(
receive(:new) 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)) .and_return(audit_event_service))
expect(audit_event_service).to receive_message_chain(:for_authentication, :security_event) expect(audit_event_service).to receive_message_chain(:for_authentication, :security_event)
...@@ -63,6 +53,39 @@ describe SmartcardController, type: :request do ...@@ -63,6 +53,39 @@ describe SmartcardController, type: :request do
end end
end end
end end
end
context 'with smartcard_auth disabled' do
before do
allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(false)
end
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
end
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 { post '/-/smartcard/auth', params: {}, headers: certificate_headers }
it_behaves_like 'a client certificate authentication', 'smartcard'
context 'user already exists' do context 'user already exists' do
before do before do
...@@ -114,15 +137,75 @@ describe SmartcardController, type: :request do ...@@ -114,15 +137,75 @@ describe SmartcardController, type: :request do
end end
end end
context 'with smartcard_auth disabled' do 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 before do
allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(false) 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 end
it 'renders 404' do 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 subject
end
expect(response).to have_gitlab_http_status(404) 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 end
end end
......
# 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
...@@ -32,14 +32,7 @@ module Gitlab ...@@ -32,14 +32,7 @@ module Gitlab
def users(fields, value, limit = nil) def users(fields, value, limit = nil)
options = user_options(Array(fields), value, limit) options = user_options(Array(fields), value, limit)
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 end
def user(*args) def user(*args)
...@@ -92,6 +85,16 @@ module Gitlab ...@@ -92,6 +85,16 @@ module Gitlab
SEARCH_RETRY_FACTOR[retry_number] * config.timeout SEARCH_RETRY_FACTOR[retry_number] * config.timeout
end 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) def user_options(fields, value, limit)
options = { options = {
attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config), attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config),
......
...@@ -8447,12 +8447,18 @@ msgstr "" ...@@ -8447,12 +8447,18 @@ msgstr ""
msgid "Sign in to \"%{group_name}\"" msgid "Sign in to \"%{group_name}\""
msgstr "" msgstr ""
msgid "Sign in using smart card"
msgstr ""
msgid "Sign in via 2FA code" msgid "Sign in via 2FA code"
msgstr "" msgstr ""
msgid "Sign in with Single Sign-On" msgid "Sign in with Single Sign-On"
msgstr "" msgstr ""
msgid "Sign in with smart card"
msgstr ""
msgid "Sign out" msgid "Sign out"
msgstr "" msgstr ""
...@@ -10041,6 +10047,9 @@ msgstr "" ...@@ -10041,6 +10047,9 @@ msgstr ""
msgid "Use your global notification setting" msgid "Use your global notification setting"
msgstr "" 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" msgid "Used by members to sign in to your group in GitLab"
msgstr "" msgstr ""
......
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