Commit b6316689 authored by Imre Farkas's avatar Imre Farkas

Smartcard authentication with basic X.509 cert

This adds support for smartcard authentication with basic X.509
certificates. NGINX needs to be setup in a way to run the same server
context on separate port requesting the client side certificate. Another
limitation is that the email address must be a part of the Common-Name
attribute.
parent b875431f
......@@ -189,11 +189,11 @@ class ApplicationController < ActionController::Base
Ability.allowed?(object, action, subject)
end
def access_denied!(message = nil)
def access_denied!(message = nil, status = nil)
# If we display a custom access denied message to the user, we don't want to
# hide existence of the resource, rather tell them they cannot access it using
# the provided message
status = message.present? ? :forbidden : :not_found
status ||= message.present? ? :forbidden : :not_found
respond_to do |format|
format.any { head status }
......
......@@ -24,6 +24,23 @@ module AuthHelper
Gitlab::Auth::OAuth::Provider.label_for(name)
end
def form_based_provider_priority
['crowd', /^ldap/, 'kerberos']
end
def form_based_provider_with_highest_priority
@form_based_provider_with_highest_priority ||= begin
form_based_provider_priority.each do |provider_regexp|
highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) }
break highest_priority unless highest_priority.nil?
end
end
end
def form_based_auth_provider_has_active_class?(provider)
form_based_provider_with_highest_priority == provider
end
def form_based_provider?(name)
[LDAP_PROVIDER, 'crowd'].any? { |pattern| pattern === name.to_s }
end
......
- if form_based_providers.any?
- if crowd_enabled?
.login-box.tab-pane.active{ id: "crowd", role: 'tabpanel' }
.login-box.tab-pane{ id: "crowd", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:crowd)) }
.login-body
= render 'devise/sessions/new_crowd'
- if kerberos_enabled?
.login-box.tab-pane{ id: "kerberos", role: 'tabpanel', class: (:active unless crowd_enabled? || ldap_enabled?) }
.login-box.tab-pane{ id: "kerberos", role: 'tabpanel', class: active_when(form_based_auth_provider_has_active_class?(:kerberos)) }
.login-body
= render 'devise/sessions/new_kerberos'
- @ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain)) }
.login-body
= render 'devise/sessions/new_ldap', server: server
= render_if_exists 'devise/sessions/new_smartcard'
- if password_authentication_enabled_for_web?
.login-box.tab-pane{ id: 'login-pane', role: 'tabpanel' }
.login-body
......
%ul.nav-links.new-session-tabs.nav-tabs.nav{ class: ('custom-provider-tabs' if form_based_providers.any?) }
- if crowd_enabled?
%li.nav-item
= link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab'
= link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab'
- if kerberos_enabled?
%li.nav-item
= link_to "Kerberos", "#kerberos", class: "nav-link #{active_when(!crowd_enabled? && !ldap_enabled?)}", 'data-toggle' => 'tab'
= link_to "Kerberos", "#kerberos", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:kerberos))}", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li.nav-item
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)} qa-ldap-tab", 'data-toggle' => 'tab'
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))} qa-ldap-tab", 'data-toggle' => 'tab'
= render_if_exists 'devise/shared/tab_smartcard'
- if password_authentication_enabled_for_web?
%li.nav-item
= link_to 'Standard', '#login-pane', class: 'nav-link qa-standard-tab', 'data-toggle' => 'tab'
......
......@@ -550,6 +550,17 @@ production: &base
# host:
# ....
## Smartcard authentication settings
smartcard:
# Allow smartcard authentication
enabled: false
# Path to a file containing a CA certificate
ca_file: '/etc/ssl/certs/CA.pem'
# Port where the client side certificate is requested by the webserver (NGINX/Apache)
# client_certificate_required_port: 3444
## Kerberos settings
kerberos:
# Allow the HTTP Negotiate authentication method for Git clients
......
......@@ -50,6 +50,9 @@ if Settings.ldap['enabled'] || Rails.env.test?
end
end
Settings['smartcard'] ||= Settingslogic.new({})
Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil?
Settings['omniauth'] ||= Settingslogic.new({})
Settings.omniauth['enabled'] = true if Settings.omniauth['enabled'].nil?
Settings.omniauth['auto_sign_in_with_provider'] = false if Settings.omniauth['auto_sign_in_with_provider'].nil?
......
......@@ -89,6 +89,7 @@ Rails.application.routes.draw do
draw :operations
draw :instance_statistics
draw :smartcard
if ENV['GITLAB_ENABLE_CHAOS_ENDPOINTS']
get '/chaos/leakmem' => 'chaos#leakmem'
......
......@@ -2581,6 +2581,14 @@ ActiveRecord::Schema.define(version: 20181107054254) do
t.index ["team_id", "alias"], name: "index_slack_integrations_on_team_id_and_alias", unique: true, using: :btree
end
create_table "smartcard_identities", id: :bigserial, force: :cascade do |t|
t.integer "user_id", null: false
t.string "subject", null: false
t.string "issuer", null: false
t.index ["subject", "issuer"], name: "index_smartcard_identities_on_subject_and_issuer", unique: true, using: :btree
t.index ["user_id"], name: "index_smartcard_identities_on_user_id", using: :btree
end
create_table "snippets", force: :cascade do |t|
t.string "title"
t.text "content"
......@@ -3293,6 +3301,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do
add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "smartcard_identities", "users", on_delete: :cascade
add_foreign_key "snippets", "projects", name: "fk_be41fd4bb7", on_delete: :cascade
add_foreign_key "software_license_policies", "projects", on_delete: :cascade
add_foreign_key "software_license_policies", "software_licenses", on_delete: :cascade
......
......@@ -16,3 +16,4 @@ providers.
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta
- [Authentiq](authentiq.md): Enable the Authentiq OmniAuth provider for passwordless authentication
- [Smartcard](smartcard.md) Smartcard authentication
# Smartcard authentication
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/726) in
[GitLab Premium](https://about.gitlab.com/pricing/) 11.5 as an experimental
feature. Smartcard authentication 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:
```
Certificate:
Data:
Version: 1 (0x0)
Serial Number: 12856475246677808609 (0xb26b601ecdd555e1)
Signature Algorithm: sha256WithRSAEncryption
Issuer: O=Random Corp Ltd, CN=Random Corp
Validity
Not Before: Oct 30 12:00:00 2018 GMT
Not After : Oct 30 12:00:00 2019 GMT
Subject: CN=Gitlab User, emailAddress=gitlab-user@example.com
```
## Configure NGINX to request a client side certificate
In NGINX configuration, an **additional** server context must be defined with
the same configuration except:
- The additional NGINX server context must be configured to run on a different
port:
```
listen *:3444 ssl;
```
- The additional NGINX server context must be configured to require the client
side certificate:
```
ssl_verify_depth 2;
ssl_client_certificate /etc/ssl/certs/CA.pem;
ssl_verify_client on;
```
- The additional NGINX server context must be configured to forward the client
side certificate:
```
proxy_set_header X-SSL-Client-Certificate $ssl_client_escaped_cert;
```
For example, the following is an example server context in an NGINX
configuration file (eg. in `/etc/nginx/sites-available/gitlab-ssl`):
```
server {
listen *:3444 ssl;
# certificate for configuring SSL
ssl_certificate /path/to/example.com.crt;
ssl_certificate_key /path/to/example.com.key;
ssl_verify_depth 2;
# CA certificate for client side certificate verification
ssl_client_certificate /etc/ssl/certs/CA.pem;
ssl_verify_client on;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-SSL-Client-Certificate $ssl_client_escaped_cert;
proxy_read_timeout 300;
proxy_pass http://gitlab-workhorse;
}
}
```
## Configure GitLab for smartcard authentication
**For installations from source**
1. Edit `config/gitlab.yml`:
```yaml
## Smartcard authentication settings
smartcard:
# Allow smartcard authentication
enabled: true
# Path to a file containing a CA certificate
ca_file: '/etc/ssl/certs/CA.pem'
# Port where the client side certificate is requested by NGINX
client_certificate_required_port: 3444
```
1. Save the file and restart GitLab for the changes to take effect.
# frozen_string_literal: true
class SmartcardController < ApplicationController
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
before_action :check_feature_availability
before_action :check_certificate_headers
def auth
certificate = Gitlab::Auth::Smartcard::Certificate.new(CGI.unescape(certificate_header))
user = certificate.find_or_create_user
unless user
flash[:alert] = _('Failed to signing using smartcard authentication')
redirect_to new_user_session_path(port: Gitlab.config.gitlab.port)
return
end
log_audit_event(user, with: 'smartcard')
sign_in_and_redirect(user)
end
protected
def check_feature_availability
render_404 unless ::Gitlab::Auth::Smartcard.enabled?
end
def check_certificate_headers
# Failing on requests coming from the port not requiring client side certificate
unless certificate_header.present?
access_denied!(_('Smartcard authentication failed: client certificate header is missing.'), 401)
end
end
def log_audit_event(user, options = {})
AuditEventService.new(user, user, options).for_authentication.security_event
end
def certificate_header
request.headers['HTTP_X_SSL_CLIENT_CERTIFICATE']
end
def after_sign_in_path_for(resource)
stored_location_for(:redirect) || stored_location_for(resource) || root_url(port: Gitlab.config.gitlab.port)
end
end
......@@ -16,15 +16,33 @@ module EE
super - GROUP_LEVEL_PROVIDERS
end
override :form_based_provider_priority
def form_based_provider_priority
super << 'smartcard'
end
override :form_based_provider?
def form_based_provider?(name)
super || name.to_s == 'kerberos'
end
override :form_based_providers
def form_based_providers
providers = super
providers << :smartcard if smartcard_enabled?
providers
end
def kerberos_enabled?
auth_providers.include?(:kerberos)
end
def smartcard_enabled?
::Gitlab::Auth::Smartcard.enabled?
end
def slack_redirect_uri(project)
slack_auth_project_settings_slack_url(project)
end
......
......@@ -45,6 +45,8 @@ module EE
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ::ProtectedBranch::PushAccessLevel # rubocop:disable Cop/ActiveRecordDependent
has_many :protected_branch_unprotect_access_levels, dependent: :destroy, class_name: ::ProtectedBranch::UnprotectAccessLevel # rubocop:disable Cop/ActiveRecordDependent
has_many :smartcard_identities
scope :excluding_guests, -> { joins(:members).where('members.access_level > ?', ::Gitlab::Access::GUEST).distinct }
scope :subscribed_for_admin_email, -> { where(admin_email_unsubscribed_at: nil) }
......@@ -77,6 +79,11 @@ module EE
joins('LEFT JOIN identities ON identities.user_id = users.id')
.where('identities.provider IS NULL OR identities.provider NOT LIKE ?', 'ldap%')
end
def find_by_smartcard_identity(certificate_subject, certificate_issuer)
joins(:smartcard_identities)
.find_by(smartcard_identities: { subject: certificate_subject, issuer: certificate_issuer })
end
end
def cannot_be_admin_and_auditor
......
......@@ -57,6 +57,7 @@ class License < ActiveRecord::Base
object_storage
group_saml
service_desk
smartcard_auth
unprotection_restrictions
variable_environment_scope
reject_unsigned_commits
......
# frozen_string_literal: true
class SmartcardIdentity < ActiveRecord::Base
belongs_to :user
validates :user,
presence: true
validates :subject,
presence: true,
uniqueness: { scope: :issuer }
validates :issuer,
presence: true
end
module EE
module Users
module BuildService
extend ::Gitlab::Utils::Override
override :execute
def execute(skip_authorization: false)
user = super
build_smartcard_identity(user, params) if ::Gitlab::Auth::Smartcard.enabled?
user
end
private
def signup_params
......@@ -15,6 +26,15 @@ module EE
:email_opted_in_at
]
end
def build_smartcard_identity(user, params)
smartcard_identity_attrs = params.slice(:certificate_subject, :certificate_issuer)
if smartcard_identity_attrs.any?
user.smartcard_identities.build(subject: params[:certificate_subject],
issuer: params[:certificate_issuer])
end
end
end
end
end
- 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
.submit-container
= submit_tag _('Login with smartcard'), class: 'btn btn-success'
- if smartcard_enabled?
%li.nav-item
= link_to _('Smartcard'), '#smartcard', class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:smartcard))}", 'data-toggle' => 'tab'
---
title: Smartcard authentication
merge_request: 8120
author:
type: added
# frozen_string_literal: true
post 'smartcard/auth' => 'smartcard#auth'
# frozen_string_literal: true
class CreateSmartcardIdentities < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :smartcard_identities, id: :bigserial do |t|
t.references :user, null: false, index: true, foreign_key: { on_delete: :cascade }
t.string 'subject', null: false
t.string 'issuer', null: false
end
end
end
# frozen_string_literal: true
class AddIndexToSmartcardIdentities < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :smartcard_identities, [:subject, :issuer], unique: true
end
def down
remove_concurrent_index :smartcard_identities, [:subject, :issuer]
end
end
# frozen_string_literal: true
module Gitlab
module Auth
module Smartcard
extend self
def enabled?
::License.feature_available?(:smartcard_auth) && ::Gitlab.config.smartcard.enabled
end
end
end
end
# frozen_string_literal: true
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
end
private
def valid?
self.class.store.verify(@certificate) if @certificate
end
def find_user
User.find_by_smartcard_identity(@subject, @issuer)
end
def create_user
user = User.find_by_email(email)
if user
create_smartcard_identity_for(user)
return user
end
user_params = {
name: common_name,
username: username,
email: email,
password: password,
password_confirmation: password,
password_automatically_set: true,
certificate_subject: @subject,
certificate_issuer: @issuer,
skip_confirmation: true
}
Users::CreateService.new(nil, user_params).execute(skip_authorization: true)
end
def create_smartcard_identity_for(user)
SmartcardIdentity.create(user: user, subject: @subject, issuer: @issuer)
end
def common_name
@subject.split('/').find { |part| part =~ /CN=/ }&.remove('CN=')&.strip
end
def email
@subject.split('/').find { |part| part =~ /emailAddress=/ }&.remove('emailAddress=')&.strip
end
def username
::Namespace.clean_path(common_name)
end
def password
@password ||= Devise.friendly_token(8)
end
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :smartcard_identity do
subject { 'CN=gitlab-user/emailAddress=gitlab-user@random-corp.org' }
issuer { 'O=Random Corp Ltd, CN=Random Corp' }
association :user
end
end
require 'spec_helper'
describe 'Login' do
include UserLoginHelper
before do
stub_licensed_features(extended_audit_events: true)
end
......@@ -25,4 +27,34 @@ describe 'Login' do
expect { gitlab_sign_in_via('saml', user, 'wrong-uid') }
.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
context 'with smartcard_auth feature flag off' do
before do
stub_licensed_features(smartcard_auth: false)
end
it 'correctly renders tabs and panes' do
ensure_tab_pane_correctness(false)
end
end
context 'with smartcard_auth feature flag on' do
before do
stub_licensed_features(smartcard_auth: true)
end
it 'correctly renders tabs and panes' do
ensure_tab_pane_correctness(false)
end
end
end
end
end
# frozen_string_literal: true
#
require 'spec_helper'
describe EE::AuthHelper do
describe "form_based_providers" do
context 'with smartcard_auth feature flag off' do
before do
stub_licensed_features(smartcard_auth: false)
allow(helper).to receive(:smartcard_enabled?).and_call_original
end
it 'does not include smartcard provider' do
allow(helper).to receive(:auth_providers) { [:twitter, :smartcard] }
expect(helper.form_based_providers).to be_empty
end
end
context 'with smartcard_auth feature flag on' do
before do
stub_licensed_features(smartcard_auth: true)
allow(helper).to receive(:smartcard_enabled?).and_return(true)
end
it 'includes smartcard provider' do
allow(helper).to receive(:auth_providers) { [:twitter, :smartcard] }
expect(helper.form_based_providers).to eq %i(smartcard)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Auth::Smartcard::Certificate 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_header) { '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(:user_build_service) { instance_double(Users::BuildService) }
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)
end
shared_examples 'a new smartcard identity' do
it 'creates smartcard identity' do
expect { subject }.to change { SmartcardIdentity.count }.from(0).to(1)
identity = SmartcardIdentity.first
expect(identity.subject).to eql(subject_dn)
expect(identity.issuer).to eql(issuer_dn)
end
end
shared_examples 'an existing user' do
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
describe '#find_or_create_user' do
subject { described_class.new(certificate_header).find_or_create_user }
context 'user and smartcard identity already exist' do
let(:user) { create(:user) }
before do
create(:smartcard_identity, subject: subject_dn, issuer: issuer_dn, user: user)
end
it_behaves_like 'an existing user'
end
context 'user exists but smartcard identity does not' do
let!(:user) { create(:user, email: 'gitlab-user@random-corp.org') }
it_behaves_like 'an existing user'
it_behaves_like 'a new smartcard identity'
it 'associates the new smartcard identity with the user' do
subject
expect(SmartcardIdentity.first.user).to eql(user)
end
end
context 'user and smartcard identity do not exist' do
let(:user) { create(:user) }
before do
allow(Gitlab::Auth::Smartcard).to receive(:enabled?).and_return(true)
end
it 'creates user' do
expect { subject }.to change { User.count }.from(0).to(1)
expect(User.first.username).to eql('gitlab-user')
expect(User.first.email).to eql('gitlab-user@random-corp.org')
end
it_behaves_like 'a new smartcard identity'
it 'calls Users::BuildService with correct params' do
user_params = { name: 'gitlab-user',
username: 'gitlab-user',
email: 'gitlab-user@random-corp.org',
password_automatically_set: true,
certificate_subject: subject_dn,
certificate_issuer: issuer_dn,
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 CN from certificate' do
let(:subject_dn) { '/CN=Gitlab User/emailAddress=gitlab-user@random-corp.org' }
it 'creates user with correct username' do
subject
expect(User.first.username).to eql('GitlabUser')
end
end
context 'avoids conflicting namespaces' do
let(:subject_dn) { '/CN=Gitlab User/emailAddress=gitlab-user@random-corp.org' }
let!(:existing_user) { create(:user, username: 'GitlabUser') }
it 'creates user with correct usnername' do
expect { subject }.to change { User.count }.from(1).to(2)
expect(User.last.username).to eql('GitlabUser1')
end
end
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
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
end
......@@ -28,6 +28,17 @@ describe EE::User do
end
end
describe '.find_by_smartcard_identity' do
let!(:user) { create(:user) }
let!(:smartcard_identity) { create(:smartcard_identity, user: user) }
it 'returns the user' do
expect(User.find_by_smartcard_identity(smartcard_identity.subject,
smartcard_identity.issuer))
.to eq(user)
end
end
describe '#access_level=' do
let(:user) { build(:user) }
......
# frozen_string_literal: true
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' }
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', {}, certificate_headers }
describe '#auth' 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
subject
expect(request.env['warden']).to be_authenticated
end
it 'redirects to root' do
subject
expect(response).to redirect_to(root_url)
end
it 'logs audit event' do
expect(AuditEventService).to(
receive(:new)
.with(instance_of(User), instance_of(User), with: 'smartcard')
.and_return(audit_event_service))
expect(audit_event_service).to receive_message_chain(:for_authentication, :security_event)
subject
end
context 'user does not exist' do
context 'signup allowed' do
it 'creates user' do
expect { subject }.to change { User.count }.from(0).to(1)
end
end
context 'signup disabled' do
it 'renders 401' do
allow(Gitlab::CurrentSettings.current_application_settings).to(
receive(:allow_signup?).and_return(false))
subject
expect(flash[:alert]).to eql('Failed to signing using smartcard authentication')
expect(response).to redirect_to(new_user_session_path)
expect(request.env['warden']).not_to be_authenticated
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
it 'finds existing user' do
expect { subject }.not_to change { User.count }
expect(request.env['warden']).to be_authenticated
end
end
context 'missing certificate headers' do
let(:certificate_headers) { nil }
it 'renders 401' do
subject
expect(response).to have_gitlab_http_status(401)
expect(request.env['warden']).not_to be_authenticated
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
end
......@@ -3340,6 +3340,9 @@ msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
msgid "Failed to signing using smartcard authentication"
msgstr ""
msgid "Failed to update issues, please try again."
msgstr ""
......@@ -4887,6 +4890,9 @@ msgstr ""
msgid "Locks give the ability to lock specific file or folder."
msgstr ""
msgid "Login with smartcard"
msgstr ""
msgid "Logs"
msgstr ""
......@@ -7462,6 +7468,12 @@ msgstr ""
msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job"
msgstr ""
msgid "Smartcard"
msgstr ""
msgid "Smartcard authentication failed: client certificate header is missing."
msgstr ""
msgid "Snippets"
msgstr ""
......
......@@ -650,7 +650,7 @@ describe ApplicationController do
describe '#access_denied' do
controller(described_class) do
def index
access_denied!(params[:message])
access_denied!(params[:message], params[:status])
end
end
......@@ -669,6 +669,12 @@ describe ApplicationController do
expect(response).to have_gitlab_http_status(403)
end
it 'renders a status passed to access denied' do
get :index, status: 401
expect(response).to have_gitlab_http_status(401)
end
end
context 'when invalid UTF-8 parameters are received' do
......
......@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Login' do
include TermsHelper
include UserLoginHelper
before do
stub_authentication_activity_metrics(debug: true)
......@@ -546,29 +547,6 @@ describe 'Login' do
ensure_tab_pane_correctness(false)
end
end
def ensure_tab_pane_correctness(visit_path = true)
if visit_path
visit new_user_session_path
end
ensure_tab_pane_counts
ensure_one_active_tab
ensure_one_active_pane
end
def ensure_tab_pane_counts
tabs_count = page.all('[role="tab"]').size
expect(page).to have_selector('[role="tabpanel"]', count: tabs_count)
end
def ensure_one_active_tab
expect(page).to have_selector('ul.new-session-tabs > li > a.active', count: 1)
end
def ensure_one_active_pane
expect(page).to have_selector('.tab-pane.active', count: 1)
end
end
context 'when terms are enforced' do
......
......@@ -57,6 +57,16 @@ describe AuthHelper do
end
end
describe 'form_based_auth_provider_has_active_class?' do
it 'selects main LDAP server' do
allow(helper).to receive(:auth_providers) { [:twitter, :ldapprimary, :ldapsecondary, :kerberos] }
expect(helper.form_based_auth_provider_has_active_class?(:twitter)).to be(false)
expect(helper.form_based_auth_provider_has_active_class?(:ldapprimary)).to be(true)
expect(helper.form_based_auth_provider_has_active_class?(:ldapsecondary)).to be(false)
expect(helper.form_based_auth_provider_has_active_class?(:kerberos)).to be(false)
end
end
describe 'enabled_button_based_providers' do
before do
allow(helper).to receive(:auth_providers) { [:twitter, :github] }
......
# frozen_string_literal: true
module UserLoginHelper
def ensure_tab_pane_correctness(visit_path = true)
if visit_path
visit new_user_session_path
end
ensure_tab_pane_counts
ensure_one_active_tab
ensure_one_active_pane
end
def ensure_tab_pane_counts
tabs_count = page.all('[role="tab"]').size
expect(page).to have_selector('[role="tabpanel"]', count: tabs_count)
end
def ensure_one_active_tab
expect(page).to have_selector('ul.new-session-tabs > li > a.active', count: 1)
end
def ensure_one_active_pane
expect(page).to have_selector('.tab-pane.active', count: 1)
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment