Commit cad3e2cc authored by Imre Farkas's avatar Imre Farkas

Add LDAP integration to smartcard authentication

It implements certificate matching using certificateExactMatch matching
rule defined in RFC4523.
parent e6f9ebd1
- 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"
......
......@@ -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.
......
......@@ -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?
......
# 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.
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
- 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?
.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'
- 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
post 'smartcard/auth' => 'smartcard#auth'
post 'smartcard/ldap_auth' => 'smartcard#ldap_auth'
......@@ -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
......
......@@ -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
......
# 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 @@
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
......
# 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'
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
......@@ -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
......@@ -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
......@@ -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' }
......
......@@ -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
# 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 @@
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
......
# 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
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),
......
......@@ -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 ""
......
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