Commit 1be33b34 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'bvl-ext-auth-mutual-tls' into 'master'

Allow TLS authentication for the external authorization service

Closes #4838

See merge request gitlab-org/gitlab-ee!5028
parents ff6d63d6 911a91a9
...@@ -186,6 +186,11 @@ ActiveRecord::Schema.define(version: 20180323150945) do ...@@ -186,6 +186,11 @@ ActiveRecord::Schema.define(version: 20180323150945) do
t.boolean "pages_domain_verification_enabled", default: true, null: false t.boolean "pages_domain_verification_enabled", default: true, null: false
t.float "external_authorization_service_timeout", default: 0.5, null: false t.float "external_authorization_service_timeout", default: 0.5, null: false
t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false t.boolean "allow_local_requests_from_hooks_and_services", default: false, null: false
t.text "external_auth_client_cert"
t.text "encrypted_external_auth_client_key"
t.string "encrypted_external_auth_client_key_iv"
t.string "encrypted_external_auth_client_key_pass"
t.string "encrypted_external_auth_client_key_pass_iv"
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
......
...@@ -173,7 +173,9 @@ PUT /application/settings ...@@ -173,7 +173,9 @@ PUT /application/settings
| `external_authorization_service_enabled` | boolean | no | Enable using an external authorization service for accessing projects | | `external_authorization_service_enabled` | boolean | no | Enable using an external authorization service for accessing projects |
| `external_authorization_service_url` | string | no | URL to which authorization requests will be directed | | `external_authorization_service_url` | string | no | URL to which authorization requests will be directed |
| `external_authorization_service_default_label` | string | no | The default classification label to use when requesting authorization and no classification label has been specified on the project | | `external_authorization_service_default_label` | string | no | The default classification label to use when requesting authorization and no classification label has been specified on the project |
| `external_authorization_service_timeout` | float | no | The timeout to enforce when performing requests to the external authorization service | | `external_auth_client_cert` | string | no | The certificate to use to authenticate with the external authorization service |
| `external_auth_client_key` | string | no | Private key for the certificate when authentication is required for the external authorization service, this is encrypted when stored |
| `external_auth_client_key_pass` | string | no | Passphrase to use for the private key when authenticating with the external service this is encrypted when stored |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
......
...@@ -43,9 +43,21 @@ The available required properties are: ...@@ -43,9 +43,21 @@ The available required properties are:
- **External authorization request timeout**: The timeout after which an - **External authorization request timeout**: The timeout after which an
authorization request is aborted. When a request times out, access is denied authorization request is aborted. When a request times out, access is denied
to the user. to the user.
- **Client authentication certificate**: The certificate to use to authenticate
with the external authorization service.
- **Client authentication key**: Private key for the certificate when
authentication is required for the external authorization service, this is
encrypted when stored.
- **Client authentication key password**: Passphrase to use for the private key when authenticating with the external service this is encrypted when stored.
- **Default classification label**: The classification label to use when - **Default classification label**: The classification label to use when
requesting authorization if no specific label is defined on the project requesting authorization if no specific label is defined on the project
When using TLS Authentication with a self signed certificate, the CA certificate
needs to be trused by the openssl installation. When using GitLab installed using
Omnibus, learn to install a custom CA in the
[omnibus documentation][omnibus-ssl-docs]. Alternatively learn where to install
custom certificates using `openssl version -d`.
## How it works ## How it works
When GitLab requests access, it will send a JSON POST request to the external When GitLab requests access, it will send a JSON POST request to the external
...@@ -90,3 +102,5 @@ label defined in the [global settings](#configuration) will be used. ...@@ -90,3 +102,5 @@ label defined in the [global settings](#configuration) will be used.
The label will be shown on all project pages in the upper right corner. The label will be shown on all project pages in the upper right corner.
![classification label on project page](img/classification_label_on_project_page.png) ![classification label on project page](img/classification_label_on_project_page.png)
[omnibus-ssl-docs]: https://docs.gitlab.com/omnibus/settings/ssl.html
...@@ -19,6 +19,22 @@ module EE ...@@ -19,6 +19,22 @@ module EE
"external authorization checks.") "external authorization checks.")
end end
def external_authorization_client_certificate_help_text
_("The X509 Certificate to use when mutual TLS is required to communicate "\
"with the external authorization service. If left blank, the server "\
"certificate is still validated when accessing over HTTPS.")
end
def external_authorization_client_key_help_text
_("The private key to use when a client certificate is provided. This value "\
"is encrypted at rest.")
end
def external_authorization_client_pass_help_text
_("The passphrase required to decrypt the private key. This is optional "\
"and the value is encrypted at rest.")
end
override :visible_attributes override :visible_attributes
def visible_attributes def visible_attributes
super + [ super + [
...@@ -57,7 +73,10 @@ module EE ...@@ -57,7 +73,10 @@ module EE
:external_authorization_service_enabled, :external_authorization_service_enabled,
:external_authorization_service_url, :external_authorization_service_url,
:external_authorization_service_default_label, :external_authorization_service_default_label,
:external_authorization_service_timeout :external_authorization_service_timeout,
:external_auth_client_cert,
:external_auth_client_key,
:external_auth_client_key_pass
] ]
end end
......
...@@ -51,6 +51,28 @@ module EE ...@@ -51,6 +51,28 @@ module EE
validates :external_authorization_service_timeout, validates :external_authorization_service_timeout,
numericality: { greater_than: 0, less_than_or_equal_to: 10 }, numericality: { greater_than: 0, less_than_or_equal_to: 10 },
if: :external_authorization_service_enabled? if: :external_authorization_service_enabled?
validates :external_auth_client_key,
presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? }
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
pass: :external_auth_client_key_pass,
if: -> (setting) { setting.external_auth_client_cert.present? }
attr_encrypted :external_auth_client_key,
mode: :per_attribute_iv,
key: ::Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-gcm',
encode: true
attr_encrypted :external_auth_client_key_pass,
mode: :per_attribute_iv,
key: ::Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-gcm',
encode: true
end end
module ClassMethods module ClassMethods
......
# X509CertificateCredentialsValidator
#
# Custom validator to check if certificate-attribute was signed using the
# private key stored in an attrebute.
#
# This can be used as an `ActiveModel::Validator` as follows:
#
# validates_with X509CertificateCredentialsValidator,
# certificate: :client_certificate,
# pkey: :decrypted_private_key,
# pass: :decrypted_passphrase
#
#
# Required attributes:
# - certificate: The name of the accessor that returns the certificate to check
# - pkey: The name of the accessor that returns the private key
# Optional:
# - pass: The name of the accessor that returns the passphrase to decrypt the
# private key
class X509CertificateCredentialsValidator < ActiveModel::Validator
def initialize(*args)
super
# We can't validate if we don't have a private key or certificate attributes
# in which case this validator is useless.
if options[:pkey].nil? || options[:certificate].nil?
raise 'Provide at least `certificate` and `pkey` attribute names'
end
end
def validate(record)
unless certificate = read_certificate(record)
record.errors.add(options[:certificate], _('is not a valid X509 certificate.'))
end
unless private_key = read_private_key(record)
record.errors.add(options[:pkey], _('could not read private key, is the passphrase correct?'))
end
return if private_key.nil? || certificate.nil?
unless certificate.public_key.fingerprint == private_key.public_key.fingerprint
record.errors.add(options[:pkey], _('private key does not match certificate.'))
end
end
private
def read_private_key(record)
OpenSSL::PKey.read(pkey(record).to_s, pass(record).to_s)
rescue ArgumentError
# When the primary key could not be read, an ArgumentError is raised.
# This hapens when the passed key is not valid or the passphrase is incorrect
nil
end
def read_certificate(record)
OpenSSL::X509::Certificate.new(certificate(record).to_s)
rescue OpenSSL::X509::CertificateError
nil
end
# rubocop:disable GitlabSecurity/PublicSend
#
# Allowing `#public_send` here because we don't want the validator to really
# care about the names of the attributes or where they come from.
#
# The credentials are mostly stored encrypted so we need to go through the
# accessors to get the values, `read_attribute` bypasses those.
def certificate(record)
record.public_send(options[:certificate])
end
def pkey(record)
record.public_send(options[:pkey])
end
def pass(record)
return nil unless options[:pass]
record.public_send(options[:pass])
end
# rubocop:enable GitlabSecurity/PublicSend
end
...@@ -23,6 +23,23 @@ ...@@ -23,6 +23,23 @@
= f.number_field :external_authorization_service_timeout, class: 'form-control', min: 0.001, max: 10, step: 0.001 = f.number_field :external_authorization_service_timeout, class: 'form-control', min: 0.001, max: 10, step: 0.001
%span.help-block %span.help-block
= external_authorization_timeout_help_text = external_authorization_timeout_help_text
= f.label :external_auth_client_cert, _('Client authentication certificate'), class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :external_auth_client_cert, class: 'form-control'
%span.help-block
= external_authorization_client_certificate_help_text
.form-group
= f.label :external_auth_client_key, _('Client authentication key'), class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :external_auth_client_key, class: 'form-control'
%span.help-block
= external_authorization_client_key_help_text
.form-group
= f.label :external_auth_client_key_pass, _('Client authentication key password'), class: 'control-label col-sm-2'
.col-sm-10
= f.password_field :external_auth_client_key_pass, class: 'form-control'
%span.help-block
= external_authorization_client_pass_help_text
.form-group .form-group
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'control-label col-sm-2' = f.label :external_authorization_service_default_label, _('Default classification label'), class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
......
---
title: Authenticate using TLS certificate for requests to external authorization service
merge_request: 5028
author:
type: added
class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration
DOWNTIME = false
def change
add_column :application_settings,
:external_auth_client_cert, :text
add_column :application_settings,
:encrypted_external_auth_client_key, :text
add_column :application_settings,
:encrypted_external_auth_client_key_iv, :string
add_column :application_settings,
:encrypted_external_auth_client_key_pass, :string
add_column :application_settings,
:encrypted_external_auth_client_key_pass_iv, :string
end
end
module EE module EE
module Gitlab module Gitlab
module ExternalAuthorization module ExternalAuthorization
extend Config
RequestFailed = Class.new(StandardError) RequestFailed = Class.new(StandardError)
def self.access_allowed?(user, label) def self.access_allowed?(user, label)
...@@ -26,28 +28,6 @@ module EE ...@@ -26,28 +28,6 @@ module EE
EE::Gitlab::ExternalAuthorization::Access.new(user, label).load! EE::Gitlab::ExternalAuthorization::Access.new(user, label).load!
end end
end end
def self.enabled?
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_enabled?
end
def self.perform_check?
enabled? && service_url.present?
end
def self.service_url
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_url
end
def self.timeout
::Gitlab::CurrentSettings
.current_application_settings
.external_authorization_service_timeout
end
end end
end end
end end
...@@ -29,7 +29,7 @@ module EE ...@@ -29,7 +29,7 @@ module EE
end end
def load_from_service def load_from_service
response = Client.build(@user, @label).request_access response = Client.new(@user, @label).request_access
@access = response.successful? @access = response.successful?
@reason = response.reason @reason = response.reason
@loaded_at = Time.now @loaded_at = Time.now
......
Excon.defaults[:ssl_verify_peer] = false
module EE module EE
module Gitlab module Gitlab
module ExternalAuthorization module ExternalAuthorization
class Client class Client
include Config
REQUEST_HEADERS = { REQUEST_HEADERS = {
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'Accept' => 'application/json' 'Accept' => 'application/json'
}.freeze }.freeze
def self.build(user, label) def initialize(user, label)
new( @user, @label = user, label
::EE::Gitlab::ExternalAuthorization.service_url,
::EE::Gitlab::ExternalAuthorization.timeout,
user,
label
)
end
def initialize(url, timeout, user, label)
@url, @timeout, @user, @label = url, timeout, user, label
end end
def request_access def request_access
response = Excon.post( response = Excon.post(
@url, service_url,
headers: REQUEST_HEADERS, post_params
body: body.to_json,
connect_timeout: @timeout,
read_timeout: @timeout,
write_timeout: @timeout
) )
EE::Gitlab::ExternalAuthorization::Response.new(response) EE::Gitlab::ExternalAuthorization::Response.new(response)
rescue Excon::Error => e rescue Excon::Error => e
...@@ -36,6 +27,22 @@ module EE ...@@ -36,6 +27,22 @@ module EE
private private
def post_params
params = { headers: REQUEST_HEADERS,
body: body.to_json,
connect_timeout: timeout,
read_timeout: timeout,
write_timeout: timeout }
if has_tls?
params[:client_cert_data] = client_cert
params[:client_key_data] = client_key
params[:client_key_pass] = client_key_pass
end
params
end
def body def body
@body ||= begin @body ||= begin
body = { body = {
......
module EE
module Gitlab
module ExternalAuthorization
module Config
extend self
def timeout
application_settings.external_authorization_service_timeout
end
def service_url
application_settings.external_authorization_service_url
end
def enabled?
application_settings.external_authorization_service_enabled?
end
def perform_check?
enabled? && service_url.present?
end
def client_cert
application_settings.external_auth_client_cert
end
def client_key
application_settings.external_auth_client_key
end
def client_key_pass
application_settings.external_auth_client_key_pass
end
def has_tls?
client_cert.present? && client_key.present?
end
private
def application_settings
::Gitlab::CurrentSettings.current_application_settings
end
end
end
end
end
...@@ -86,7 +86,10 @@ describe Admin::ApplicationSettingsController do ...@@ -86,7 +86,10 @@ describe Admin::ApplicationSettingsController do
external_authorization_service_enabled: true, external_authorization_service_enabled: true,
external_authorization_service_url: 'https://custom.service/', external_authorization_service_url: 'https://custom.service/',
external_authorization_service_default_label: 'default', external_authorization_service_default_label: 'default',
external_authorization_service_timeout: 3 external_authorization_service_timeout: 3,
external_auth_client_cert: File.read('ee/spec/fixtures/passphrase_x509_certificate.crt'),
external_auth_client_key: File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key'),
external_auth_client_key_pass: "5iveL!fe"
} }
end end
let(:feature) { :external_authorization_service } let(:feature) { :external_authorization_service }
......
-----BEGIN CERTIFICATE-----
MIIEpTCCAo0CAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
MB4XDTE4MDMyMzE0MDIwOFoXDTE5MDMyMzE0MDIwOFowHTEbMBkGA1UEAwwSZ2l0
bGFiLXBhc3NwaHJhc2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
zpsWHOewP/khfDsLUWxaRCinrBzVJm2C01bVahKVR3g/JD4vEH901Wod9Pvbh/9e
PEfE+YZmgSUUopbL3JUheMnyW416F43HKE/fPW4+QeuIEceuhCXg20eOXmvnWWNM
0hXZh4hq69rwvMPREC/LkZy/QkTDKhJNLNAqAQu2AJ3C7Yga8hFQYEhx1hpfGtwD
z/Nf3efat9WN/d6yW9hfJ98NCmImTm5l9Pc0YPNWCAf96vsqsNHBrTkFy6CQwkhH
K1ynVYuqnHYxSc4FPCT5SAleD9gR/xFBAHb7pPy4yGxMSEmiWaMjjZCVPsghj1jM
Ej77MTDL3U9LeDfiILhvZ+EeQxqPiFwwG2eaIn3ZEs2Ujvw7Z2VpG9VMcPTnB4jK
ot6qPM1YXnkGWQ6iT0DTPS3h7zg1xIJXI5N2sI6GXuKrXXwZ1wPqzFLKPv+xBjp8
P6dih+EImfReFi9zIO1LqGMY+XmRcqodsb6jzsmBimJkqBtatJM7FuUUUN56wiaj
q9+BWbm+ZdQ2lvqndMljjUjTh6pNERfGAJgkNuLn3X9hXVE0TSpmn0nOgaL5izP3
7FWUt0PTyGgK2zq9SEhZmK2TKckLkKMk/ZBBBVM/nrnjs72IlbsqdcVoTnApytZr
xVYTj1hV7QlAfaU3w/M534qXDiy8+HfX5ksWQMtSklECAwEAATANBgkqhkiG9w0B
AQUFAAOCAgEAMMhzSRq9PqCpui74nwjhmn8Dm2ky7A+MmoXNtk70cS/HWrjzaacb
B/rxsAUp7f0pj4QMMM0ETMFpbNs8+NPd2FRY0PfWE4yyDpvZO2Oj1HZKLHX72Gjn
K5KB9DYlVsXhGPfuFWXpxGWF2Az9hDWnj58M3DOAps+6tHuAtudQUuwf5ENQZWwE
ySpr7yoHm1ykgl0Tsb9ZHi9qLrWRRMNYXRT+gvwP1bba8j9jOtjO/xYiIskwMPLM
W8SFmQxbg0Cvi8Q89PB6zoTNOhPQyoyeSlw9meeZJHAMK2zxeglEm8C4EQ+I9Y6/
yylM5/Sc55TjWAvRFgbsq+OozgMvffk/Q2fzcGF44J9DEQ7nrhmJxJ+X4enLknR5
Hw4+WhdYA+bwjx3YZBNTh9/YMgNPYwQhf5gtcZGTd6X4j6qZfJ6CXBmhkC1Cbfyl
yM7B7i4JAqPWMeDP50pXCgyKlwgw1JuFW+xkbkYQAj7wtggQ6z1Vjb5W8R8kYn9q
LXClVtThEeSV5KkVwNX21aFcUs8qeQ+zsgKqpEyM5oILQQ1gDSxLTtrr2KuN+WJN
wM0acwD45X7gA/aZYpCGkIgHIBq0zIDP1s6IqeebFJjW8lWofhRxOEWomWdRweJG
N7qQ1WCTQxAPGAkDI8QPjaspvnAhFKmpBG/mR5IXLFKDbttu7WNdYDo=
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C
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
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
MB4XDTE4MDMxOTE1MjYzMloXDTE5MDMxOTE1MjYzMlowFDESMBAGA1UEAwwJbG9j
YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+tcM7iphsLlR
ccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzAn/eVU4jyVWkaBym6MHa8CiDOro9H
OXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/2FAgFWzrB2HnYSShiN8tBeeDI5cJ
ii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UTK37k2kbDQZ41rv1ng2w0AUZt0LRA
NWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ
+1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNkI+cyv0Gle6tk+CkOfE1m0CvNWlNg
b8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5xMXpdUCsh22CZZHe/4SeFE64amkf
1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q/nLdY8haMC6KOtpbAWvKX/Jqq0z1
nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVrVef0pb2mfdtzjzUrYCP0PtnQExPB
rocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8RSvAoEUs9VbPiUfN7WAyU1K1rTYH
KV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRlq07Q5LDz33h9KXw1LZT8MWRinVJf
RePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA
Skp0tbvVsg3RG2pX0GP25j0ix+f78zG0+BJ6LiKGMoCIBtGKitfUjBg83ru/ILpa
fpgrQpNQVUnGQ9tmpnqV605ZBBRUC1CRDsvUnyN6p7+yQAq6Fl+2ZKONHpPk+Bl4
CIewgdkHjTwTpvIM/1DFVCz4R1FxNjY3uqOVcNDczMYEk2Pn2GZNNN35hUHHxWh4
89ZvI+XKuRFZq3cDPA60PySeJJpCRScWGgnkdEX1gTtWH3WUlq9llxIvRexyNyzZ
Yqvcfx5UT75/Pp+JPh9lpUCcKLHeUiadjkiLxu3IcrYa4gYx4lA8jgm7adNEahd0
oMAHoO9DU6XMo7o6tnQH3xQv9RAbQanjuyJR9N7mwmc59bQ6mW+pxCk843GwT73F
slseJ1nE1fQQQD7mn/KGjmeWtxY2ElUjTay9ff9/AgJeQYRW+oH0cSdo8WCpc2+G
+LZtLWfBgFLHseRlmarSe2pP8KmbaTd3q7Bu0GekVQOxYcNX59Pj4muQZDVLh8aX
mSQ+Ifts/ljT649MISHn2AZMR4+BUx63tFcatQhbAGGH5LeFdbaGcaVdsUVyZ9a2
HBmFWNsgEPtcC+WmNzCXbv7jQsLAJXufKG5MnurJgNf/n5uKCmpGsEJDT/KF1k/3
x9YnqM7zTyV6un+LS3HjEJvwQmqPWe+vFAeXWGCoWxE=
-----END CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
...@@ -39,7 +39,7 @@ describe EE::Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do ...@@ -39,7 +39,7 @@ describe EE::Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
before do before do
allow(access).to receive(:load_from_cache) allow(access).to receive(:load_from_cache)
allow(fake_client).to receive(:request_access).and_return(fake_response) allow(fake_client).to receive(:request_access).and_return(fake_response)
allow(EE::Gitlab::ExternalAuthorization::Client).to receive(:build) { fake_client } allow(EE::Gitlab::ExternalAuthorization::Client).to receive(:new) { fake_client }
end end
context 'when loading from the webservice' do context 'when loading from the webservice' do
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe EE::Gitlab::ExternalAuthorization::Client do describe EE::Gitlab::ExternalAuthorization::Client do
let(:user) { build(:user, email: 'dummy_user@example.com') } let(:user) { build(:user, email: 'dummy_user@example.com') }
let(:dummy_url) { 'https://dummy.net/' } let(:dummy_url) { 'https://dummy.net/' }
subject(:client) { described_class.build(user, 'dummy_label') } subject(:client) { described_class.new(user, 'dummy_label') }
before do before do
stub_application_setting(external_authorization_service_url: dummy_url) stub_application_setting(external_authorization_service_url: dummy_url)
...@@ -28,7 +28,9 @@ describe EE::Gitlab::ExternalAuthorization::Client do ...@@ -28,7 +28,9 @@ describe EE::Gitlab::ExternalAuthorization::Client do
end end
it 'respects the the timeout' do it 'respects the the timeout' do
allow(EE::Gitlab::ExternalAuthorization).to receive(:timeout).and_return(3) stub_application_setting(
external_authorization_service_timeout: 3
)
expect(Excon).to receive(:post).with(dummy_url, expect(Excon).to receive(:post).with(dummy_url,
hash_including( hash_including(
...@@ -40,6 +42,23 @@ describe EE::Gitlab::ExternalAuthorization::Client do ...@@ -40,6 +42,23 @@ describe EE::Gitlab::ExternalAuthorization::Client do
client.request_access client.request_access
end end
it 'adds the mutual tls params when they are present' do
stub_application_setting(
external_auth_client_cert: 'the certificate data',
external_auth_client_key: 'the key data',
external_auth_client_key_pass: 'open sesame'
)
expected_params = {
client_cert_data: 'the certificate data',
client_key_data: 'the key data',
client_key_pass: 'open sesame'
}
expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params))
client.request_access
end
it 'returns an expected response' do it 'returns an expected response' do
expect(Excon).to receive(:post) expect(Excon).to receive(:post)
......
...@@ -36,6 +36,39 @@ describe ApplicationSetting do ...@@ -36,6 +36,39 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) } it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) }
it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) } it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) }
it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) } it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) }
it { is_expected.not_to allow_value('not a certificate').for(:external_auth_client_cert) }
it { is_expected.to allow_value('').for(:external_auth_client_cert) }
it { is_expected.to allow_value('').for(:external_auth_client_key) }
context 'when setting a valid client certificate for external authorization' do
let(:certificate_data) { File.read('ee/spec/fixtures/passphrase_x509_certificate.crt') }
before do
setting.external_auth_client_cert = certificate_data
end
it 'requires a valid client key when a certificate is set' do
expect(setting).not_to allow_value('fefefe').for(:external_auth_client_key)
end
it 'requires a matching certificate' do
other_private_key = File.read('ee/spec/fixtures/x509_certificate_pk.key')
expect(setting).not_to allow_value(other_private_key).for(:external_auth_client_key)
end
it 'the credentials are valid when the private key can be read and matches the certificate' do
tls_attributes = [:external_auth_client_key_pass,
:external_auth_client_key,
:external_auth_client_cert]
setting.external_auth_client_key = File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key')
setting.external_auth_client_key_pass = '5iveL!fe'
setting.validate
expect(setting.errors).not_to include(*tls_attributes)
end
end
end end
end end
......
...@@ -84,7 +84,10 @@ describe API::Settings, 'EE Settings' do ...@@ -84,7 +84,10 @@ describe API::Settings, 'EE Settings' do
external_authorization_service_enabled: true, external_authorization_service_enabled: true,
external_authorization_service_url: 'https://custom.service/', external_authorization_service_url: 'https://custom.service/',
external_authorization_service_default_label: 'default', external_authorization_service_default_label: 'default',
external_authorization_service_timeout: 9.99 external_authorization_service_timeout: 9.99,
external_auth_client_cert: File.read('ee/spec/fixtures/passphrase_x509_certificate.crt'),
external_auth_client_key: File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key'),
external_auth_client_key_pass: "5iveL!fe"
} }
end end
let(:feature) { :external_authorization_service } let(:feature) { :external_authorization_service }
......
require 'spec_helper'
describe X509CertificateCredentialsValidator do
let(:certificate_data) { File.read('ee/spec/fixtures/x509_certificate.crt') }
let(:pkey_data) { File.read('ee/spec/fixtures/x509_certificate_pk.key') }
let(:validatable) do
Class.new do
include ActiveModel::Validations
attr_accessor :certificate, :private_key, :passphrase
def initialize(certificate, private_key, passphrase = nil)
@certificate, @private_key, @passphrase = certificate, private_key, passphrase
end
end
end
subject(:validator) do
described_class.new(certificate: :certificate, pkey: :private_key)
end
it 'is not valid when the certificate is not valid' do
record = validatable.new('not a certificate', nil)
validator.validate(record)
expect(record.errors[:certificate]).to include('is not a valid X509 certificate.')
end
it 'is not valid without a certificate' do
record = validatable.new(nil, nil)
validator.validate(record)
expect(record.errors[:certificate]).not_to be_empty
end
context 'when a valid certificate is passed' do
let(:record) { validatable.new(certificate_data, nil) }
it 'does not track an error for the certificate' do
validator.validate(record)
expect(record.errors[:certificate]).to be_empty
end
it 'adds an error when not passing a correct private key' do
validator.validate(record)
expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?')
end
it 'has no error when the private key is correct' do
record.private_key = pkey_data
validator.validate(record)
expect(record.errors).to be_empty
end
end
context 'when using a passphrase' do
let(:passphrase_certificate_data) { File.read('ee/spec/fixtures/passphrase_x509_certificate.crt') }
let(:passphrase_pkey_data) { File.read('ee/spec/fixtures/passphrase_x509_certificate_pk.key') }
let(:record) { validatable.new(passphrase_certificate_data, passphrase_pkey_data, '5iveL!fe') }
subject(:validator) do
described_class.new(certificate: :certificate, pkey: :private_key, pass: :passphrase)
end
it 'is valid with the correct data' do
validator.validate(record)
expect(record.errors).to be_empty
end
it 'adds an error when the passphrase is wrong' do
record.passphrase = 'wrong'
validator.validate(record)
expect(record.errors[:private_key]).not_to be_empty
end
end
end
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-03-23 20:21+0100\n" "POT-Creation-Date: 2018-03-26 15:11+0200\n"
"PO-Revision-Date: 2018-03-23 20:21+0100\n" "PO-Revision-Date: 2018-03-26 15:11+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -864,6 +864,15 @@ msgstr "" ...@@ -864,6 +864,15 @@ msgstr ""
msgid "Click to expand text" msgid "Click to expand text"
msgstr "" msgstr ""
msgid "Client authentication certificate"
msgstr ""
msgid "Client authentication key"
msgstr ""
msgid "Client authentication key password"
msgstr ""
msgid "Clone repository" msgid "Clone repository"
msgstr "" msgstr ""
...@@ -3838,6 +3847,9 @@ msgstr "" ...@@ -3838,6 +3847,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project." msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr "" msgstr ""
msgid "The X509 Certificate to use when mutual TLS is required to communicate with the external authorization service. If left blank, the server certificate is still validated when accessing over HTTPS."
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr "" msgstr ""
...@@ -3865,12 +3877,18 @@ msgstr "" ...@@ -3865,12 +3877,18 @@ msgstr ""
msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}." msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
msgstr "" msgstr ""
msgid "The passphrase required to decrypt the private key. This is optional and the value is encrypted at rest."
msgstr ""
msgid "The phase of the development lifecycle." msgid "The phase of the development lifecycle."
msgstr "" msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit." msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr "" msgstr ""
msgid "The private key to use when a client certificate is provided. This value is encrypted at rest."
msgstr ""
msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle." msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr "" msgstr ""
...@@ -4679,6 +4697,9 @@ msgstr "" ...@@ -4679,6 +4697,9 @@ msgstr ""
msgid "connecting" msgid "connecting"
msgstr "" msgstr ""
msgid "could not read private key, is the passphrase correct?"
msgstr ""
msgid "day" msgid "day"
msgid_plural "days" msgid_plural "days"
msgstr[0] "" msgstr[0] ""
...@@ -4699,6 +4720,9 @@ msgstr "" ...@@ -4699,6 +4720,9 @@ msgstr ""
msgid "is invalid because there is upstream lock" msgid "is invalid because there is upstream lock"
msgstr "" msgstr ""
msgid "is not a valid X509 certificate."
msgstr ""
msgid "locked by %{path_lock_user_name} %{created_at}" msgid "locked by %{path_lock_user_name} %{created_at}"
msgstr "" msgstr ""
...@@ -4710,6 +4734,15 @@ msgstr[1] "" ...@@ -4710,6 +4734,15 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "" msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
msgstr ""
msgid "mrWidget|Add approval" msgid "mrWidget|Add approval"
msgstr "" msgstr ""
...@@ -4755,18 +4788,27 @@ msgstr "" ...@@ -4755,18 +4788,27 @@ msgstr ""
msgid "mrWidget|Closes" msgid "mrWidget|Closes"
msgstr "" msgstr ""
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
msgid "mrWidget|Did not close" msgid "mrWidget|Did not close"
msgstr "" msgstr ""
msgid "mrWidget|Email patches" msgid "mrWidget|Email patches"
msgstr "" msgstr ""
msgid "mrWidget|Failed to load deployment statistics"
msgstr ""
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the" msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
msgstr "" msgstr ""
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line" msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
msgstr "" msgstr ""
msgid "mrWidget|Loading deployment statistics"
msgstr ""
msgid "mrWidget|Mentions" msgid "mrWidget|Mentions"
msgstr "" msgstr ""
...@@ -4889,6 +4931,9 @@ msgstr "" ...@@ -4889,6 +4931,9 @@ msgstr ""
msgid "personal access token" msgid "personal access token"
msgstr "" msgstr ""
msgid "private key does not match certificate."
msgstr ""
msgid "remove due date" msgid "remove due date"
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