summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/auth
diff options
context:
space:
mode:
authorHoratiu Eugen Vlad <horatiu@vlad.eu>2018-02-23 13:10:39 +0100
committerHoratiu Eugen Vlad <horatiu@vlad.eu>2018-02-28 16:53:02 +0100
commit1ad5df49b1925f1865e99c3fd8576a762aea9cae (patch)
treeb9cee2aabea4c4584883245ada7e8e91e1a01295 /spec/lib/gitlab/auth
parent77097c9196da7c43d1102249da1d40446176f803 (diff)
downloadgitlab-ce-1ad5df49b1925f1865e99c3fd8576a762aea9cae.tar.gz
Moved o_auth/saml/ldap modules under gitlab/auth
Diffstat (limited to 'spec/lib/gitlab/auth')
-rw-r--r--spec/lib/gitlab/auth/ldap/access_spec.rb166
-rw-r--r--spec/lib/gitlab/auth/ldap/adapter_spec.rb144
-rw-r--r--spec/lib/gitlab/auth/ldap/auth_hash_spec.rb110
-rw-r--r--spec/lib/gitlab/auth/ldap/authentication_spec.rb58
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb373
-rw-r--r--spec/lib/gitlab/auth/ldap/dn_spec.rb224
-rw-r--r--spec/lib/gitlab/auth/ldap/person_spec.rb160
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb241
-rw-r--r--spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb111
-rw-r--r--spec/lib/gitlab/auth/o_auth/provider_spec.rb42
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb742
-rw-r--r--spec/lib/gitlab/auth/saml/auth_hash_spec.rb40
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb403
13 files changed, 2814 insertions, 0 deletions
diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb
new file mode 100644
index 00000000000..9b3916bf9e3
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/access_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::Access do
+ let(:access) { described_class.new user }
+ let(:user) { create(:omniauth_user) }
+
+ describe '.allowed?' do
+ it 'updates the users `last_credential_check_at' do
+ expect(access).to receive(:allowed?) { true }
+ expect(described_class).to receive(:open).and_yield(access)
+
+ expect { described_class.allowed?(user) }
+ .to change { user.last_credential_check_at }
+ end
+ end
+
+ describe '#allowed?' do
+ subject { access.allowed? }
+
+ context 'when the user cannot be found' do
+ before do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'does not exist anymore')
+
+ access.allowed?
+ end
+ end
+
+ context 'when the user is found' do
+ before do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
+ end
+
+ context 'and the user is disabled via active directory' do
+ before do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true)
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory')
+
+ access.allowed?
+ end
+ end
+
+ context 'and has no disabled flag in active diretory' do
+ before do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when auto-created users are blocked' do
+ before do
+ user.block
+ end
+
+ it 'does not unblock user in GitLab' do
+ expect(access).not_to receive(:unblock_user)
+
+ access.allowed?
+
+ expect(user).to be_blocked
+ expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
+ end
+ end
+
+ context 'when auto-created users are not blocked' do
+ before do
+ user.ldap_block
+ end
+
+ it 'unblocks user in GitLab' do
+ expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore')
+
+ access.allowed?
+ end
+ end
+ end
+
+ context 'without ActiveDirectory enabled' do
+ before do
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive(:active_directory).and_return(false)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when user cannot be found' do
+ before do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'blocks user in GitLab' do
+ expect(access).to receive(:block_user).with(user, 'does not exist anymore')
+
+ access.allowed?
+ end
+ end
+
+ context 'when user was previously ldap_blocked' do
+ before do
+ user.ldap_block
+ end
+
+ it 'unblocks the user if it exists' do
+ expect(access).to receive(:unblock_user).with(user, 'is available again')
+
+ access.allowed?
+ end
+ end
+ end
+ end
+ end
+
+ describe '#block_user' do
+ before do
+ user.activate
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ access.block_user user, 'reason'
+ end
+
+ it 'blocks the user' do
+ expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
+ end
+
+ it 'logs the reason' do
+ expect(Gitlab::AppLogger).to have_received(:info).with(
+ "LDAP account \"123456\" reason, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
+
+ describe '#unblock_user' do
+ before do
+ user.ldap_block
+ allow(Gitlab::AppLogger).to receive(:info)
+
+ access.unblock_user user, 'reason'
+ end
+
+ it 'activates the user' do
+ expect(user).not_to be_blocked
+ expect(user).not_to be_ldap_blocked
+ end
+
+ it 'logs the reason' do
+ Gitlab::AppLogger.info(
+ "LDAP account \"123456\" reason, " \
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/adapter_spec.rb b/spec/lib/gitlab/auth/ldap/adapter_spec.rb
new file mode 100644
index 00000000000..10c60d792bd
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/adapter_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::Adapter do
+ include LdapHelpers
+
+ let(:ldap) { double(:ldap) }
+ let(:adapter) { ldap_adapter('ldapmain', ldap) }
+
+ describe '#users' do
+ before do
+ stub_ldap_config(base: 'dc=example,dc=com')
+ end
+
+ it 'searches with the proper options when searching by uid' do
+ # Requires this expectation style to match the filter
+ expect(adapter).to receive(:ldap_search) do |arg|
+ expect(arg[:filter].to_s).to eq('(uid=johndoe)')
+ expect(arg[:base]).to eq('dc=example,dc=com')
+ expect(arg[:attributes]).to match(ldap_attributes)
+ end.and_return({})
+
+ adapter.users('uid', 'johndoe')
+ end
+
+ it 'searches with the proper options when searching by dn' do
+ expect(adapter).to receive(:ldap_search).with(
+ base: 'uid=johndoe,ou=users,dc=example,dc=com',
+ scope: Net::LDAP::SearchScope_BaseObject,
+ attributes: ldap_attributes,
+ filter: nil
+ ).and_return({})
+
+ adapter.users('dn', 'uid=johndoe,ou=users,dc=example,dc=com')
+ end
+
+ it 'searches with the proper options when searching with a limit' do
+ expect(adapter)
+ .to receive(:ldap_search).with(hash_including(size: 100)).and_return({})
+
+ adapter.users('uid', 'johndoe', 100)
+ end
+
+ it 'returns an LDAP::Person if search returns a result' do
+ entry = ldap_user_entry('johndoe')
+ allow(adapter).to receive(:ldap_search).and_return([entry])
+
+ results = adapter.users('uid', 'johndoe')
+
+ expect(results.size).to eq(1)
+ expect(results.first.uid).to eq('johndoe')
+ end
+
+ it 'returns empty array if search entry does not respond to uid' do
+ entry = Net::LDAP::Entry.new
+ entry['dn'] = user_dn('johndoe')
+ allow(adapter).to receive(:ldap_search).and_return([entry])
+
+ results = adapter.users('uid', 'johndoe')
+
+ expect(results).to be_empty
+ end
+
+ it 'uses the right uid attribute when non-default' do
+ stub_ldap_config(uid: 'sAMAccountName')
+ expect(adapter).to receive(:ldap_search).with(
+ hash_including(attributes: ldap_attributes)
+ ).and_return({})
+
+ adapter.users('sAMAccountName', 'johndoe')
+ end
+ end
+
+ describe '#dn_matches_filter?' do
+ subject { adapter.dn_matches_filter?(:dn, :filter) }
+
+ context "when the search result is non-empty" do
+ before do
+ allow(adapter).to receive(:ldap_search).and_return([:foo])
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "when the search result is empty" do
+ before do
+ allow(adapter).to receive(:ldap_search).and_return([])
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#ldap_search' do
+ subject { adapter.ldap_search(base: :dn, filter: :filter) }
+
+ context "when the search is successful" do
+ context "and the result is non-empty" do
+ before do
+ allow(ldap).to receive(:search).and_return([:foo])
+ end
+
+ it { is_expected.to eq [:foo] }
+ end
+
+ context "and the result is empty" do
+ before do
+ allow(ldap).to receive(:search).and_return([])
+ end
+
+ it { is_expected.to eq [] }
+ end
+ end
+
+ context "when the search encounters an error" do
+ before do
+ allow(ldap).to receive_messages(
+ search: nil,
+ get_operation_result: double(code: 1, message: 'some error')
+ )
+ end
+
+ it { is_expected.to eq [] }
+ end
+
+ context "when the search raises an LDAP exception" do
+ before do
+ allow(ldap).to receive(:search) { raise Net::LDAP::Error, "some error" }
+ allow(Rails.logger).to receive(:warn)
+ end
+
+ it { is_expected.to eq [] }
+
+ it 'logs the error' do
+ subject
+ expect(Rails.logger).to have_received(:warn).with(
+ "LDAP search raised exception Net::LDAP::Error: some error")
+ end
+ end
+ end
+
+ def ldap_attributes
+ Gitlab::Auth::LDAP::Person.ldap_attributes(Gitlab::Auth::LDAP::Config.new('ldapmain'))
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
new file mode 100644
index 00000000000..05541972f87
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::AuthHash do
+ include LdapHelpers
+
+ let(:auth_hash) do
+ described_class.new(
+ OmniAuth::AuthHash.new(
+ uid: given_uid,
+ provider: 'ldapmain',
+ info: info,
+ extra: {
+ raw_info: raw_info
+ }
+ )
+ )
+ end
+
+ let(:info) do
+ {
+ name: 'Smith, J.',
+ email: 'johnsmith@example.com',
+ nickname: '123456'
+ }
+ end
+
+ let(:raw_info) do
+ {
+ uid: ['123456'],
+ email: ['johnsmith@example.com'],
+ cn: ['Smith, J.'],
+ fullName: ['John Smith']
+ }
+ end
+
+ context "without overridden attributes" do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
+ it "has the correct username" do
+ expect(auth_hash.username).to eq("123456")
+ end
+
+ it "has the correct name" do
+ expect(auth_hash.name).to eq("Smith, J.")
+ end
+ end
+
+ context "with overridden attributes" do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
+ let(:attributes) do
+ {
+ 'username' => %w(mail email),
+ 'name' => 'fullName'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive(:attributes).and_return(attributes)
+ end
+
+ it "has the correct username" do
+ expect(auth_hash.username).to eq("johnsmith@example.com")
+ end
+
+ it "has the correct name" do
+ expect(auth_hash.name).to eq("John Smith")
+ end
+ end
+
+ describe '#uid' do
+ context 'when there is extraneous (but valid) whitespace' do
+ let(:given_uid) { 'uid =john smith , ou = people, dc= example,dc =com' }
+
+ it 'removes the extraneous whitespace' do
+ expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com')
+ end
+ end
+
+ context 'when there are upper case characters' do
+ let(:given_uid) { 'UID=John Smith,ou=People,dc=example,dc=com' }
+
+ it 'downcases' do
+ expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com')
+ end
+ end
+ end
+
+ describe '#username' do
+ context 'if lowercase_usernames setting is' do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
+ before do
+ raw_info[:uid] = ['JOHN']
+ end
+
+ it 'enabled the username attribute is lower cased' do
+ stub_ldap_config(lowercase_usernames: true)
+
+ expect(auth_hash.username).to eq 'john'
+ end
+
+ it 'disabled the username attribute is not lower cased' do
+ stub_ldap_config(lowercase_usernames: false)
+
+ expect(auth_hash.username).to eq 'JOHN'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/authentication_spec.rb b/spec/lib/gitlab/auth/ldap/authentication_spec.rb
new file mode 100644
index 00000000000..111572d043b
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/authentication_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::Authentication do
+ let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' }
+ let(:user) { create(:omniauth_user, extern_uid: Gitlab::Auth::LDAP::Person.normalize_dn(dn)) }
+ let(:login) { 'john' }
+ let(:password) { 'password' }
+
+ describe 'login' do
+ before do
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
+ end
+
+ it "finds the user if authentication is successful" do
+ expect(user).not_to be_nil
+
+ # try only to fake the LDAP call
+ adapter = double('adapter', dn: dn).as_null_object
+ allow_any_instance_of(described_class)
+ .to receive(:adapter).and_return(adapter)
+
+ expect(described_class.login(login, password)).to be_truthy
+ end
+
+ it "is false if the user does not exist" do
+ # try only to fake the LDAP call
+ adapter = double('adapter', dn: dn).as_null_object
+ allow_any_instance_of(described_class)
+ .to receive(:adapter).and_return(adapter)
+
+ expect(described_class.login(login, password)).to be_falsey
+ end
+
+ it "is false if authentication fails" do
+ expect(user).not_to be_nil
+
+ # try only to fake the LDAP call
+ adapter = double('adapter', bind_as: nil).as_null_object
+ allow_any_instance_of(described_class)
+ .to receive(:adapter).and_return(adapter)
+
+ expect(described_class.login(login, password)).to be_falsey
+ end
+
+ it "fails if ldap is disabled" do
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(false)
+ expect(described_class.login(login, password)).to be_falsey
+ end
+
+ it "fails if no login is supplied" do
+ expect(described_class.login('', password)).to be_falsey
+ end
+
+ it "fails if no password is supplied" do
+ expect(described_class.login(login, '')).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
new file mode 100644
index 00000000000..82587e2ba55
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -0,0 +1,373 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::Config do
+ include LdapHelpers
+
+ let(:config) { described_class.new('ldapmain') }
+
+ describe '.servers' do
+ it 'returns empty array if no server information is available' do
+ allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false)
+
+ expect(described_class.servers).to eq []
+ end
+ end
+
+ describe '#initialize' do
+ it 'requires a provider' do
+ expect { described_class.new }.to raise_error ArgumentError
+ end
+
+ it 'works' do
+ expect(config).to be_a described_class
+ end
+
+ it 'raises an error if a unknown provider is used' do
+ expect { described_class.new 'unknown' }.to raise_error(RuntimeError)
+ end
+ end
+
+ describe '#adapter_options' do
+ it 'constructs basic options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
+ 'encryption' => 'plain'
+ }
+ )
+
+ expect(config.adapter_options).to eq(
+ host: 'ldap.example.com',
+ port: 386,
+ encryption: nil
+ )
+ end
+
+ it 'includes authentication options when auth is configured' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true,
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.adapter_options).to include({
+ auth: {
+ method: :simple,
+ username: 'uid=admin,dc=example,dc=com',
+ password: 'super_secret'
+ }
+ })
+ end
+
+ it 'sets encryption method to simple_tls when configured as simple_tls' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls'
+ }
+ )
+
+ expect(config.adapter_options[:encryption]).to include({ method: :simple_tls })
+ end
+
+ it 'sets encryption method to start_tls when configured as start_tls' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls'
+ }
+ )
+
+ expect(config.adapter_options[:encryption]).to include({ method: :start_tls })
+ end
+
+ context 'when verify_certificates is enabled' do
+ it 'sets tls_options to OpenSSL defaults' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true
+ }
+ )
+
+ expect(config.adapter_options[:encryption]).to include({ tls_options: OpenSSL::SSL::SSLContext::DEFAULT_PARAMS })
+ end
+ end
+
+ context 'when verify_certificates is disabled' do
+ it 'sets verify_mode to OpenSSL VERIFY_NONE' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => false
+ }
+ )
+
+ expect(config.adapter_options[:encryption]).to include({
+ tls_options: {
+ verify_mode: OpenSSL::SSL::VERIFY_NONE
+ }
+ })
+ end
+ end
+
+ context 'when ca_file is specified' do
+ it 'passes it through in tls_options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'ca_file' => '/etc/ca.pem'
+ }
+ )
+
+ expect(config.adapter_options[:encryption][:tls_options]).to include({ ca_file: '/etc/ca.pem' })
+ end
+ end
+
+ context 'when ca_file is a blank string' do
+ it 'does not add the ca_file key to tls_options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'ca_file' => ' '
+ }
+ )
+
+ expect(config.adapter_options[:encryption][:tls_options]).not_to have_key(:ca_file)
+ end
+ end
+
+ context 'when ssl_version is specified' do
+ it 'passes it through in tls_options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'ssl_version' => 'TLSv1_2'
+ }
+ )
+
+ expect(config.adapter_options[:encryption][:tls_options]).to include({ ssl_version: 'TLSv1_2' })
+ end
+ end
+
+ context 'when ssl_version is a blank string' do
+ it 'does not add the ssl_version key to tls_options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'ssl_version' => ' '
+ }
+ )
+
+ expect(config.adapter_options[:encryption][:tls_options]).not_to have_key(:ssl_version)
+ end
+ end
+ end
+
+ describe '#omniauth_options' do
+ it 'constructs basic options' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 386,
+ 'base' => 'ou=users,dc=example,dc=com',
+ 'encryption' => 'plain',
+ 'uid' => 'uid'
+ }
+ )
+
+ expect(config.omniauth_options).to include(
+ host: 'ldap.example.com',
+ port: 386,
+ base: 'ou=users,dc=example,dc=com',
+ encryption: 'plain',
+ filter: '(uid=%{username})'
+ )
+ expect(config.omniauth_options.keys).not_to include(:bind_dn, :password)
+ end
+
+ it 'includes authentication options when auth is configured' do
+ stub_ldap_config(
+ options: {
+ 'uid' => 'sAMAccountName',
+ 'user_filter' => '(memberOf=cn=group1,ou=groups,dc=example,dc=com)',
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.omniauth_options).to include(
+ filter: '(&(sAMAccountName=%{username})(memberOf=cn=group1,ou=groups,dc=example,dc=com))',
+ bind_dn: 'uid=admin,dc=example,dc=com',
+ password: 'super_secret'
+ )
+ end
+
+ context 'when verify_certificates is enabled' do
+ it 'specifies disable_verify_certificates as false' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true
+ }
+ )
+
+ expect(config.omniauth_options).to include({ disable_verify_certificates: false })
+ end
+ end
+
+ context 'when verify_certificates is disabled' do
+ it 'specifies disable_verify_certificates as true' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => false
+ }
+ )
+
+ expect(config.omniauth_options).to include({ disable_verify_certificates: true })
+ end
+ end
+
+ context 'when ca_file is present' do
+ it 'passes it through' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true,
+ 'ca_file' => '/etc/ca.pem'
+ }
+ )
+
+ expect(config.omniauth_options).to include({ ca_file: '/etc/ca.pem' })
+ end
+ end
+
+ context 'when ca_file is blank' do
+ it 'does not include the ca_file option' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true,
+ 'ca_file' => ' '
+ }
+ )
+
+ expect(config.omniauth_options).not_to have_key(:ca_file)
+ end
+ end
+
+ context 'when ssl_version is present' do
+ it 'passes it through' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true,
+ 'ssl_version' => 'TLSv1_2'
+ }
+ )
+
+ expect(config.omniauth_options).to include({ ssl_version: 'TLSv1_2' })
+ end
+ end
+
+ context 'when ssl_version is blank' do
+ it 'does not include the ssl_version option' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'simple_tls',
+ 'verify_certificates' => true,
+ 'ssl_version' => ' '
+ }
+ )
+
+ expect(config.omniauth_options).not_to have_key(:ssl_version)
+ end
+ end
+ end
+
+ describe '#has_auth?' do
+ it 'is true when password is set' do
+ stub_ldap_config(
+ options: {
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => 'super_secret'
+ }
+ )
+
+ expect(config.has_auth?).to be_truthy
+ end
+
+ it 'is true when bind_dn is set and password is empty' do
+ stub_ldap_config(
+ options: {
+ 'bind_dn' => 'uid=admin,dc=example,dc=com',
+ 'password' => ''
+ }
+ )
+
+ expect(config.has_auth?).to be_truthy
+ end
+
+ it 'is false when password and bind_dn are not set' do
+ stub_ldap_config(options: { 'bind_dn' => nil, 'password' => nil })
+
+ expect(config.has_auth?).to be_falsey
+ end
+ end
+
+ describe '#attributes' do
+ it 'uses default attributes when no custom attributes are configured' do
+ expect(config.attributes).to eq(config.default_attributes)
+ end
+
+ it 'merges the configuration attributes with default attributes' do
+ stub_ldap_config(
+ options: {
+ 'attributes' => {
+ 'username' => %w(sAMAccountName),
+ 'email' => %w(userPrincipalName)
+ }
+ }
+ )
+
+ expect(config.attributes).to include({
+ 'username' => %w(sAMAccountName),
+ 'email' => %w(userPrincipalName),
+ 'name' => 'cn'
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/dn_spec.rb b/spec/lib/gitlab/auth/ldap/dn_spec.rb
new file mode 100644
index 00000000000..f2983a02602
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/dn_spec.rb
@@ -0,0 +1,224 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::DN do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#normalize_value' do
+ subject { described_class.normalize_value(given) }
+
+ it_behaves_like 'normalizes a DN attribute value'
+
+ context 'when the given DN is malformed' do
+ context 'when ending with a comma' do
+ let(:given) { 'John Smith,' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a space in it' do
+ let(:given) { '#aa aa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '#aaXaaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '#aaaYaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ end
+ end
+
+ context 'when given a hex pair with a non-hex character in it, inside double quotes' do
+ let(:given) { '"Sebasti\\cX\\a1n"' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ end
+ end
+
+ context 'with an open (as opposed to closed) double quote' do
+ let(:given) { '"James' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid escaped hex code' do
+ let(:given) { 'J\ames' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ end
+ end
+
+ context 'with a value ending with the escape character' do
+ let(:given) { 'foo\\' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+ end
+ end
+
+ describe '#to_normalized_s' do
+ subject { described_class.new(given).to_normalized_s }
+
+ it_behaves_like 'normalizes a DN'
+
+ context 'when we do not support the given DN format' do
+ context 'multivalued RDNs' do
+ context 'without extraneous whitespace' do
+ let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError)
+ end
+ end
+
+ context 'with extraneous whitespace' do
+ context 'around the phone number plus sign' do
+ let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError)
+ end
+ end
+
+ context 'not around the phone number plus sign' do
+ let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' }
+
+ it 'raises UnsupportedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when the given DN is malformed' do
+ context 'when ending with a comma' do
+ let(:given) { 'uid=John Smith,' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a space in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ end
+ end
+
+ context 'when given a BER encoded attribute value with a non-hex character in it' do
+ let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ end
+ end
+
+ context 'when given a hex pair with a non-hex character in it, inside double quotes' do
+ let(:given) { 'uid="Sebasti\\cX\\a1n"' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ end
+ end
+
+ context 'without a name value pair' do
+ let(:given) { 'John' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an open (as opposed to closed) double quote' do
+ let(:given) { 'cn="James' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid escaped hex code' do
+ let(:given) { 'cn=J\ames' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ end
+ end
+
+ context 'with a value ending with the escape character' do
+ let(:given) { 'cn=\\' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ end
+ end
+
+ context 'with an invalid OID attribute type name' do
+ let(:given) { '1.2.d=Value' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"')
+ end
+ end
+
+ context 'with a period in a non-OID attribute type name' do
+ let(:given) { 'd1.2=Value' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."')
+ end
+ end
+
+ context 'when starting with non-space, non-alphanumeric character' do
+ let(:given) { ' -uid=John Smith' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"')
+ end
+ end
+
+ context 'when given a UID with an escaped equal sign' do
+ let(:given) { 'uid\\=john' }
+
+ it 'raises MalformedError' do
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"')
+ end
+ end
+ end
+ end
+
+ def assert_generic_test(test_description, got, expected)
+ test_failure_message = "Failed test description: '#{test_description}'\n\n expected: \"#{expected}\"\n got: \"#{got}\""
+ expect(got).to eq(expected), test_failure_message
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb
new file mode 100644
index 00000000000..1527fe60fb9
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/person_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::Person do
+ include LdapHelpers
+
+ let(:entry) { ldap_user_entry('john.doe') }
+
+ before do
+ stub_ldap_config(
+ options: {
+ 'uid' => 'uid',
+ 'attributes' => {
+ 'name' => 'cn',
+ 'email' => %w(mail email userPrincipalName),
+ 'username' => username_attribute
+ }
+ }
+ )
+ end
+ let(:username_attribute) { %w(uid sAMAccountName userid) }
+
+ describe '.normalize_dn' do
+ subject { described_class.normalize_dn(given) }
+
+ it_behaves_like 'normalizes a DN'
+
+ context 'with an exception during normalization' do
+ let(:given) { 'John "Smith,' } # just something that will cause an exception
+
+ it 'returns the given DN unmodified' do
+ expect(subject).to eq(given)
+ end
+ end
+ end
+
+ describe '.normalize_uid' do
+ subject { described_class.normalize_uid(given) }
+
+ it_behaves_like 'normalizes a DN attribute value'
+
+ context 'with an exception during normalization' do
+ let(:given) { 'John "Smith,' } # just something that will cause an exception
+
+ it 'returns the given UID unmodified' do
+ expect(subject).to eq(given)
+ end
+ end
+ end
+
+ describe '.ldap_attributes' do
+ it 'returns a compact and unique array' do
+ stub_ldap_config(
+ options: {
+ 'uid' => nil,
+ 'attributes' => {
+ 'name' => 'cn',
+ 'email' => 'mail',
+ 'username' => %w(uid mail memberof)
+ }
+ }
+ )
+ config = Gitlab::Auth::LDAP::Config.new('ldapmain')
+ ldap_attributes = described_class.ldap_attributes(config)
+
+ expect(ldap_attributes).to match_array(%w(dn uid cn mail memberof))
+ end
+ end
+
+ describe '#name' do
+ it 'uses the configured name attribute and handles values as an array' do
+ name = 'John Doe'
+ entry['cn'] = [name]
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.name).to eq(name)
+ end
+ end
+
+ describe '#email' do
+ it 'returns the value of mail, if present' do
+ mail = 'john@example.com'
+ entry['mail'] = mail
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.email).to eq([mail])
+ end
+
+ it 'returns the value of userPrincipalName, if mail and email are not present' do
+ user_principal_name = 'john.doe@example.com'
+ entry['userPrincipalName'] = user_principal_name
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.email).to eq([user_principal_name])
+ end
+ end
+
+ describe '#username' do
+ context 'with default uid username attribute' do
+ let(:username_attribute) { 'uid' }
+
+ it 'returns the proper username value' do
+ attr_value = 'johndoe'
+ entry[username_attribute] = attr_value
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.username).to eq(attr_value)
+ end
+ end
+
+ context 'with a different username attribute' do
+ let(:username_attribute) { 'sAMAccountName' }
+
+ it 'returns the proper username value' do
+ attr_value = 'johndoe'
+ entry[username_attribute] = attr_value
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.username).to eq(attr_value)
+ end
+ end
+
+ context 'with a non-standard username attribute' do
+ let(:username_attribute) { 'mail' }
+
+ it 'returns the proper username value' do
+ attr_value = 'john.doe@example.com'
+ entry[username_attribute] = attr_value
+ person = described_class.new(entry, 'ldapmain')
+
+ expect(person.username).to eq(attr_value)
+ end
+ end
+
+ context 'if lowercase_usernames setting is' do
+ let(:username_attribute) { 'uid' }
+
+ before do
+ entry[username_attribute] = 'JOHN'
+ @person = described_class.new(entry, 'ldapmain')
+ end
+
+ it 'enabled the username attribute is lower cased' do
+ stub_ldap_config(lowercase_usernames: true)
+
+ expect(@person.username).to eq 'john'
+ end
+
+ it 'disabled the username attribute is not lower cased' do
+ stub_ldap_config(lowercase_usernames: false)
+
+ expect(@person.username).to eq 'JOHN'
+ end
+ end
+ end
+
+ def assert_generic_test(test_description, got, expected)
+ test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}"
+ expect(got).to eq(expected), test_failure_message
+ end
+end
diff --git a/spec/lib/gitlab/auth/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
new file mode 100644
index 00000000000..cab2169593a
--- /dev/null
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -0,0 +1,241 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::LDAP::User do
+ let(:ldap_user) { described_class.new(auth_hash) }
+ let(:gl_user) { ldap_user.gl_user }
+ let(:info) do
+ {
+ name: 'John',
+ email: 'john@example.com',
+ nickname: 'john'
+ }
+ end
+ let(:auth_hash) do
+ OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info)
+ end
+ let(:ldap_user_upper_case) { described_class.new(auth_hash_upper_case) }
+ let(:info_upper_case) do
+ {
+ name: 'John',
+ email: 'John@Example.com', # Email address has upper case chars
+ nickname: 'john'
+ }
+ end
+ let(:auth_hash_upper_case) do
+ OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case)
+ end
+
+ describe '#changed?' do
+ it "marks existing ldap user as changed" do
+ create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain')
+ expect(ldap_user.changed?).to be_truthy
+ end
+
+ it "marks existing non-ldap user if the email matches as changed" do
+ create(:user, email: 'john@example.com')
+ expect(ldap_user.changed?).to be_truthy
+ end
+
+ it "does not mark existing ldap user as changed" do
+ create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
+ expect(ldap_user.changed?).to be_falsey
+ end
+ end
+
+ describe '.find_by_uid_and_provider' do
+ let(:dn) { 'CN=John Åström, CN=Users, DC=Example, DC=com' }
+
+ it 'retrieves the correct user' do
+ special_info = {
+ name: 'John Åström',
+ email: 'john@example.com',
+ nickname: 'jastrom'
+ }
+ special_hash = OmniAuth::AuthHash.new(uid: dn, provider: 'ldapmain', info: special_info)
+ special_chars_user = described_class.new(special_hash)
+ user = special_chars_user.save
+
+ expect(described_class.find_by_uid_and_provider(dn, 'ldapmain')).to eq user
+ end
+ end
+
+ describe 'find or create' do
+ it "finds the user if already existing" do
+ create(:omniauth_user, extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain')
+
+ expect { ldap_user.save }.not_to change { User.count }
+ end
+
+ it "connects to existing non-ldap user if the email matches" do
+ existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter")
+ expect { ldap_user.save }.not_to change { User.count }
+
+ existing_user.reload
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
+ expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
+ end
+
+ it 'connects to existing ldap user if the extern_uid changes' do
+ existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain')
+ expect { ldap_user.save }.not_to change { User.count }
+
+ existing_user.reload
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
+ expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
+ expect(existing_user.id).to eql ldap_user.gl_user.id
+ end
+
+ it 'connects to existing ldap user if the extern_uid changes and email address has upper case characters' do
+ existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'old-uid', provider: 'ldapmain')
+ expect { ldap_user_upper_case.save }.not_to change { User.count }
+
+ existing_user.reload
+ expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com'
+ expect(existing_user.ldap_identity.provider).to eql 'ldapmain'
+ expect(existing_user.id).to eql ldap_user.gl_user.id
+ end
+
+ it 'maintains an identity per provider' do
+ existing_user = create(:omniauth_user, email: 'john@example.com', provider: 'twitter')
+ expect(existing_user.identities.count).to be(1)
+
+ ldap_user.save
+ expect(ldap_user.gl_user.identities.count).to be(2)
+
+ # Expect that find_by provider only returns a single instance of an identity and not an Enumerable
+ expect(ldap_user.gl_user.identities.find_by(provider: 'twitter')).to be_instance_of Identity
+ expect(ldap_user.gl_user.identities.find_by(provider: auth_hash.provider)).to be_instance_of Identity
+ end
+
+ it "creates a new user if not found" do
+ expect { ldap_user.save }.to change { User.count }.by(1)
+ end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ ldap_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ ldap_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
+ end
+
+ describe 'updating email' do
+ context "when LDAP sets an email" do
+ it "has a real email" do
+ expect(ldap_user.gl_user.email).to eq(info[:email])
+ end
+
+ it "has email set as synced" do
+ expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
+ end
+
+ it "has email set as read-only" do
+ expect(ldap_user.gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to ldapmain" do
+ expect(ldap_user.gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain'
+ end
+ end
+
+ context "when LDAP doesn't set an email" do
+ before do
+ info.delete(:email)
+ end
+
+ it "has a temp email" do
+ expect(ldap_user.gl_user.temp_oauth_email?).to be_truthy
+ end
+
+ it "has email set as not synced" do
+ expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
+ end
+
+ it "does not have email set as read-only" do
+ expect(ldap_user.gl_user.read_only_attribute?(:email)).to be_falsey
+ end
+ end
+ end
+
+ describe 'blocking' do
+ def configure_block(value)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config)
+ .to receive(:block_auto_created_users).and_return(value)
+ end
+
+ context 'signup' do
+ context 'dont block on create' do
+ before do
+ configure_block(false)
+ end
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before do
+ configure_block(true)
+ end
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to be_blocked
+ end
+ end
+ end
+
+ context 'sign-in' do
+ before do
+ ldap_user.save
+ ldap_user.gl_user.activate
+ end
+
+ context 'dont block on create' do
+ before do
+ configure_block(false)
+ end
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before do
+ configure_block(true)
+ end
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
new file mode 100644
index 00000000000..40001cea22e
--- /dev/null
+++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
@@ -0,0 +1,111 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::OAuth::AuthHash do
+ let(:provider) { 'ldap'.freeze }
+ let(:auth_hash) do
+ described_class.new(
+ OmniAuth::AuthHash.new(
+ provider: provider,
+ uid: uid_ascii,
+ info: info_hash
+ )
+ )
+ end
+
+ let(:uid_raw) do
+ "CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net"
+ end
+ let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk_ABC-123@example.net" }
+ let(:nickname_raw) { "ok\xC3\xBC\xC3\xA7\xC3\xBCk" }
+ let(:first_name_raw) { 'Onur' }
+ let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" }
+ let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" }
+
+ let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:first_name_ascii) { first_name_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) }
+
+ let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) }
+ let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) }
+ let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) }
+ let(:name_utf8) { name_ascii.force_encoding(Encoding::UTF_8) }
+
+ let(:info_hash) do
+ {
+ email: email_ascii,
+ first_name: first_name_ascii,
+ last_name: last_name_ascii,
+ name: name_ascii,
+ nickname: nickname_ascii,
+ uid: uid_ascii
+ }
+ end
+
+ context 'defaults' do
+ it { expect(auth_hash.provider).to eq provider }
+ it { expect(auth_hash.uid).to eql uid_utf8 }
+ it { expect(auth_hash.email).to eql email_utf8 }
+ it { expect(auth_hash.username).to eql nickname_utf8 }
+ it { expect(auth_hash.name).to eql name_utf8 }
+ it { expect(auth_hash.password).not_to be_empty }
+ end
+
+ context 'email not provided' do
+ before do
+ info_hash.delete(:email)
+ end
+
+ it 'generates a temp email' do
+ expect( auth_hash.email).to start_with('temp-email-for-oauth')
+ end
+ end
+
+ context 'username not provided' do
+ before do
+ info_hash.delete(:nickname)
+ end
+
+ it 'takes the first part of the email as username' do
+ expect(auth_hash.username).to eql 'onur.kucuk_ABC-123'
+ end
+ end
+
+ context 'name not provided' do
+ before do
+ info_hash.delete(:name)
+ end
+
+ it 'concats first and lastname as the name' do
+ expect(auth_hash.name).to eql name_utf8
+ end
+ end
+
+ context 'auth_hash constructed with ASCII-8BIT encoding' do
+ it 'forces utf8 encoding on uid' do
+ expect(auth_hash.uid.encoding).to eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on provider' do
+ expect(auth_hash.provider.encoding).to eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on name' do
+ expect(auth_hash.name.encoding).to eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on username' do
+ expect(auth_hash.username.encoding).to eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on email' do
+ expect(auth_hash.email.encoding).to eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on password' do
+ expect(auth_hash.password.encoding).to eql Encoding::UTF_8
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
new file mode 100644
index 00000000000..fc35d430917
--- /dev/null
+++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::OAuth::Provider do
+ describe '#config_for' do
+ context 'for an LDAP provider' do
+ context 'when the provider exists' do
+ it 'returns the config' do
+ expect(described_class.config_for('ldapmain')).to be_a(Hash)
+ end
+ end
+
+ context 'when the provider does not exist' do
+ it 'returns nil' do
+ expect(described_class.config_for('ldapfoo')).to be_nil
+ end
+ end
+ end
+
+ context 'for an OmniAuth provider' do
+ before do
+ provider = OpenStruct.new(
+ name: 'google',
+ app_id: 'asd123',
+ app_secret: 'asd123'
+ )
+ allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider])
+ end
+
+ context 'when the provider exists' do
+ it 'returns the config' do
+ expect(described_class.config_for('google')).to be_a(OpenStruct)
+ end
+ end
+
+ context 'when the provider does not exist' do
+ it 'returns nil' do
+ expect(described_class.config_for('foo')).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
new file mode 100644
index 00000000000..0c71f1d8ca6
--- /dev/null
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -0,0 +1,742 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::OAuth::User do
+ let(:oauth_user) { described_class.new(auth_hash) }
+ let(:gl_user) { oauth_user.gl_user }
+ let(:uid) { 'my-uid' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
+ let(:provider) { 'my-provider' }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
+ let(:info_hash) do
+ {
+ nickname: '-john+gitlab-ETC%.git@gmail.com',
+ name: 'John',
+ email: 'john@mail.com',
+ address: {
+ locality: 'locality',
+ country: 'country'
+ }
+ }
+ end
+ let(:ldap_user) { Gitlab::Auth::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
+
+ describe '#persisted?' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ it "finds an existing user based on uid and provider (facebook)" do
+ expect( oauth_user.persisted? ).to be_truthy
+ end
+
+ it 'returns false if user is not found in database' do
+ allow(auth_hash).to receive(:uid).and_return('non-existing')
+ expect( oauth_user.persisted? ).to be_falsey
+ end
+ end
+
+ def stub_omniauth_config(messages)
+ allow(Gitlab.config.omniauth).to receive_messages(messages)
+ end
+
+ describe '#save' do
+ def stub_ldap_config(messages)
+ allow(Gitlab::Auth::LDAP::Config).to receive_messages(messages)
+ end
+
+ let(:provider) { 'twitter' }
+
+ describe 'when account exists on server' do
+ it 'does not mark the user as external' do
+ create(:omniauth_user, extern_uid: 'my-uid', provider: provider)
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
+
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+
+ describe 'signup' do
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ stub_omniauth_config(allow_single_sign_on: [provider])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ stub_omniauth_config(allow_single_sign_on: [provider])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
+
+ it 'marks user as having password_automatically_set' do
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_password_automatically_set
+ end
+
+ shared_examples 'to verify compliance with allow_single_sign_on' do
+ context 'provider is marked as external' do
+ it 'marks user as external' do
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ context 'provider was external, now has been removed' do
+ it 'does not mark external user as internal' do
+ create(:omniauth_user, extern_uid: 'my-uid', provider: provider, external: true)
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: ['facebook'])
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ context 'provider is not external' do
+ context 'when adding a new OAuth identity' do
+ it 'does not promote an external user to internal' do
+ user = create(:user, email: 'john@mail.com', external: true)
+ user.identities.create(provider: provider, extern_uid: uid)
+
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+ end
+
+ context 'with new allow_single_sign_on enabled syntax' do
+ before do
+ stub_omniauth_config(allow_single_sign_on: [provider])
+ end
+
+ it "creates a user from Omniauth" do
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ identity = gl_user.identities.first
+ expect(identity.extern_uid).to eql uid
+ expect(identity.provider).to eql provider
+ end
+ end
+
+ context "with old allow_single_sign_on enabled syntax" do
+ before do
+ stub_omniauth_config(allow_single_sign_on: true)
+ end
+
+ it "creates a user from Omniauth" do
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ identity = gl_user.identities.first
+ expect(identity.extern_uid).to eql uid
+ expect(identity.provider).to eql provider
+ end
+ end
+
+ context 'with new allow_single_sign_on disabled syntax' do
+ before do
+ stub_omniauth_config(allow_single_sign_on: [])
+ end
+
+ it 'throws an error' do
+ expect { oauth_user.save }.to raise_error StandardError
+ end
+ end
+
+ context 'with old allow_single_sign_on disabled (Default)' do
+ before do
+ stub_omniauth_config(allow_single_sign_on: false)
+ end
+
+ it 'throws an error' do
+ expect { oauth_user.save }.to raise_error StandardError
+ end
+ end
+ end
+
+ context "with auto_link_ldap_user disabled (default)" do
+ before do
+ stub_omniauth_config(auto_link_ldap_user: false)
+ end
+
+ include_examples "to verify compliance with allow_single_sign_on"
+ end
+
+ context "with auto_link_ldap_user enabled" do
+ before do
+ stub_omniauth_config(auto_link_ldap_user: true)
+ end
+
+ context "and no LDAP provider defined" do
+ before do
+ stub_ldap_config(providers: [])
+ end
+
+ include_examples "to verify compliance with allow_single_sign_on"
+ end
+
+ context "and at least one LDAP provider is defined" do
+ before do
+ stub_ldap_config(providers: %w(ldapmain))
+ end
+
+ context "and a corresponding LDAP person" do
+ before do
+ allow(ldap_user).to receive(:uid) { uid }
+ allow(ldap_user).to receive(:username) { uid }
+ allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
+ allow(ldap_user).to receive(:dn) { dn }
+ end
+
+ context "and no account for the LDAP user" do
+ before do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
+ oauth_user.save
+ end
+
+ it "creates a user with dual LDAP and omniauth identities" do
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql uid
+ expect(gl_user.email).to eql 'johndoe@example.com'
+ expect(gl_user.identities.length).to be 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: dn },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
+
+ it "has email set as synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
+ end
+
+ it "has email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to ldapmain" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain'
+ end
+ end
+
+ context "and LDAP user has an account already" do
+ let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
+ it "adds the omniauth identity to the LDAP account" do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'john'
+ expect(gl_user.email).to eql 'john@example.com'
+ expect(gl_user.identities.length).to be 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: dn },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
+ end
+
+ context 'when an LDAP person is not found by uid' do
+ it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+
+ oauth_user.save
+
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash)
+ .to match_array(
+ [
+ { provider: 'ldapmain', extern_uid: dn },
+ { provider: 'twitter', extern_uid: uid }
+ ]
+ )
+ end
+ end
+ end
+
+ context 'and a corresponding LDAP person with a non-default username' do
+ before do
+ allow(ldap_user).to receive(:uid) { uid }
+ allow(ldap_user).to receive(:username) { 'johndoe@example.com' }
+ allow(ldap_user).to receive(:email) { %w(johndoe@example.com john2@example.com) }
+ allow(ldap_user).to receive(:dn) { dn }
+ end
+
+ context 'and no account for the LDAP user' do
+ it 'creates a user favoring the LDAP username and strips email domain' do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'johndoe'
+ end
+ end
+ end
+
+ context "and no corresponding LDAP person" do
+ before do
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+ end
+
+ include_examples "to verify compliance with allow_single_sign_on"
+ end
+ end
+ end
+ end
+
+ describe 'blocking' do
+ let(:provider) { 'twitter' }
+
+ before do
+ stub_omniauth_config(allow_single_sign_on: ['twitter'])
+ end
+
+ context 'signup with omniauth only' do
+ context 'dont block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: false)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: true)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to be_blocked
+ end
+ end
+ end
+
+ context 'signup with linked omniauth and LDAP account' do
+ before do
+ stub_omniauth_config(auto_link_ldap_user: true)
+ allow(ldap_user).to receive(:uid) { uid }
+ allow(ldap_user).to receive(:username) { uid }
+ allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
+ allow(ldap_user).to receive(:dn) { dn }
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+ end
+
+ context "and no account for the LDAP user" do
+ context 'dont block on create (LDAP)' do
+ before do
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create (LDAP)' do
+ before do
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to be_blocked
+ end
+ end
+ end
+
+ context 'and LDAP user has an account already' do
+ let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
+
+ context 'dont block on create (LDAP)' do
+ before do
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create (LDAP)' do
+ before do
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+ end
+ end
+
+ context 'sign-in' do
+ before do
+ oauth_user.save
+ oauth_user.gl_user.activate
+ end
+
+ context 'dont block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: false)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: true)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'dont block on create (LDAP)' do
+ before do
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create (LDAP)' do
+ before do
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true)
+ end
+
+ it do
+ oauth_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+ end
+ end
+ end
+
+ describe 'ensure backwards compatibility with with sync email from provider option' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ before do
+ stub_omniauth_config(sync_email_from_provider: 'my-provider')
+ stub_omniauth_config(sync_profile_from_provider: ['my-provider'])
+ end
+
+ context "when provider sets an email" do
+ it "updates the user email" do
+ expect(gl_user.email).to eq(info_hash[:email])
+ end
+
+ it "has email set as synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
+ end
+
+ it "has email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to my-provider" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
+ end
+ end
+
+ context "when provider doesn't set an email" do
+ before do
+ info_hash.delete(:email)
+ end
+
+ it "does not update the user email" do
+ expect(gl_user.email).not_to eq(info_hash[:email])
+ end
+
+ it "has email set as not synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
+ end
+
+ it "does not have email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_falsey
+ end
+ end
+ end
+
+ describe 'generating username' do
+ context 'when no collision with existing user' do
+ it 'generates the username with no counter' do
+ expect(gl_user.username).to eq('johngitlab-ETC')
+ end
+ end
+
+ context 'when collision with existing user' do
+ it 'generates the username with a counter' do
+ oauth_user.save
+ oauth_user2 = described_class.new(OmniAuth::AuthHash.new(uid: 'my-uid2', provider: provider, info: { nickname: 'johngitlab-ETC@othermail.com', email: 'john@othermail.com' }))
+
+ expect(oauth_user2.gl_user.username).to eq('johngitlab-ETC1')
+ end
+ end
+
+ context 'when username is a reserved word' do
+ let(:info_hash) do
+ {
+ nickname: 'admin@othermail.com',
+ email: 'admin@othermail.com'
+ }
+ end
+
+ it 'generates the username with a counter' do
+ expect(gl_user.username).to eq('admin1')
+ end
+ end
+ end
+
+ describe 'updating email with sync profile' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ before do
+ stub_omniauth_config(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_config(sync_profile_attributes: true)
+ end
+
+ context "when provider sets an email" do
+ it "updates the user email" do
+ expect(gl_user.email).to eq(info_hash[:email])
+ end
+
+ it "has email set as synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true)
+ end
+
+ it "has email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_truthy
+ end
+
+ it "has synced attributes provider set to my-provider" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider'
+ end
+ end
+
+ context "when provider doesn't set an email" do
+ before do
+ info_hash.delete(:email)
+ end
+
+ it "does not update the user email" do
+ expect(gl_user.email).not_to eq(info_hash[:email])
+ end
+
+ it "has email set as not synced" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey
+ end
+
+ it "does not have email set as read-only" do
+ expect(gl_user.read_only_attribute?(:email)).to be_falsey
+ end
+ end
+ end
+
+ describe 'updating name' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+ end
+
+ context "when provider sets a name" do
+ it "updates the user name" do
+ expect(gl_user.name).to eq(info_hash[:name])
+ end
+ end
+
+ context "when provider doesn't set a name" do
+ before do
+ info_hash.delete(:name)
+ end
+
+ it "does not update the user name" do
+ expect(gl_user.name).not_to eq(info_hash[:name])
+ expect(gl_user.user_synced_attributes_metadata.name_synced).to be(false)
+ end
+ end
+ end
+
+ describe 'updating location' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+ end
+
+ context "when provider sets a location" do
+ it "updates the user location" do
+ expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country])
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true)
+ end
+ end
+
+ context "when provider doesn't set a location" do
+ before do
+ info_hash[:address].delete(:country)
+ info_hash[:address].delete(:locality)
+ end
+
+ it "does not update the user location" do
+ expect(gl_user.location).to be_nil
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(false)
+ end
+ end
+ end
+
+ describe 'updating user info' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ context "update all info" do
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: true)
+ end
+
+ it "updates the user email" do
+ expect(gl_user.email).to eq(info_hash[:email])
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true)
+ end
+
+ it "updates the user name" do
+ expect(gl_user.name).to eq(info_hash[:name])
+ expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true)
+ end
+
+ it "updates the user location" do
+ expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country])
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true)
+ end
+
+ it "sets my-provider as the attributes provider" do
+ expect(gl_user.user_synced_attributes_metadata.provider).to eql('my-provider')
+ end
+ end
+
+ context "update only requested info" do
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ stub_omniauth_setting(sync_profile_attributes: %w(name location))
+ end
+
+ it "updates the user name" do
+ expect(gl_user.name).to eq(info_hash[:name])
+ expect(gl_user.user_synced_attributes_metadata.name_synced).to be(true)
+ end
+
+ it "updates the user location" do
+ expect(gl_user.location).to eq(info_hash[:address][:locality] + ', ' + info_hash[:address][:country])
+ expect(gl_user.user_synced_attributes_metadata.location_synced).to be(true)
+ end
+
+ it "does not update the user email" do
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false)
+ end
+ end
+
+ context "update default_scope" do
+ before do
+ stub_omniauth_setting(sync_profile_from_provider: ['my-provider'])
+ end
+
+ it "updates the user email" do
+ expect(gl_user.email).to eq(info_hash[:email])
+ expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true)
+ end
+ end
+
+ context "update no info when profile sync is nil" do
+ it "does not have sync_attribute" do
+ expect(gl_user.user_synced_attributes_metadata).to be(nil)
+ end
+
+ it "does not update the user email" do
+ expect(gl_user.email).not_to eq(info_hash[:email])
+ end
+
+ it "does not update the user name" do
+ expect(gl_user.name).not_to eq(info_hash[:name])
+ end
+
+ it "does not update the user location" do
+ expect(gl_user.location).not_to eq(info_hash[:address][:country])
+ end
+
+ it 'does not create associated user synced attributes metadata' do
+ expect(gl_user.user_synced_attributes_metadata).to be_nil
+ end
+ end
+ end
+
+ describe '.find_by_uid_and_provider' do
+ let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
+
+ it 'normalizes extern_uid' do
+ allow(oauth_user.auth_hash).to receive(:uid).and_return('MY-UID')
+ expect(oauth_user.find_user).to eql gl_user
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
new file mode 100644
index 00000000000..bb950e6bbf8
--- /dev/null
+++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::Saml::AuthHash do
+ include LoginHelpers
+
+ let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } }
+ subject(:saml_auth_hash) { described_class.new(omniauth_auth_hash) }
+
+ let(:info_hash) do
+ {
+ name: 'John',
+ email: 'john@mail.com'
+ }
+ end
+
+ let(:omniauth_auth_hash) do
+ OmniAuth::AuthHash.new(uid: 'my-uid',
+ provider: 'saml',
+ info: info_hash,
+ extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) } )
+ end
+
+ before do
+ stub_saml_group_config(%w(Developers Freelancers Designers))
+ end
+
+ describe '#groups' do
+ it 'returns array of groups' do
+ expect(saml_auth_hash.groups).to eq(%w(Developers Freelancers))
+ end
+
+ context 'raw info hash attributes empty' do
+ let(:raw_info_attr) { {} }
+
+ it 'returns an empty array' do
+ expect(saml_auth_hash.groups).to be_a(Array)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
new file mode 100644
index 00000000000..62514ca0688
--- /dev/null
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -0,0 +1,403 @@
+require 'spec_helper'
+
+describe Gitlab::Auth::Saml::User do
+ include LdapHelpers
+ include LoginHelpers
+
+ let(:saml_user) { described_class.new(auth_hash) }
+ let(:gl_user) { saml_user.gl_user }
+ let(:uid) { 'my-uid' }
+ let(:dn) { 'uid=user1,ou=people,dc=example' }
+ let(:provider) { 'saml' }
+ let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers Designers) } }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash, extra: { raw_info: OneLogin::RubySaml::Attributes.new(raw_info_attr) }) }
+ let(:info_hash) do
+ {
+ name: 'John',
+ email: 'john@mail.com'
+ }
+ end
+ let(:ldap_user) { Gitlab::Auth::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
+
+ describe '#save' do
+ before do
+ stub_basic_saml_config
+ end
+
+ describe 'account exists on server' do
+ before do
+ stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true })
+ end
+
+ let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') }
+
+ context 'and should bind with SAML' do
+ it 'adds the SAML identity to the existing user' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to eq existing_user
+ identity = gl_user.identities.first
+ expect(identity.extern_uid).to eql uid
+ expect(identity.provider).to eql 'saml'
+ end
+ end
+
+ context 'external groups' do
+ context 'are defined' do
+ it 'marks the user as external' do
+ stub_saml_group_config(%w(Freelancers))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ before do
+ stub_saml_group_config(%w(Interns))
+ end
+
+ context 'are defined but the user does not belong there' do
+ it 'does not mark the user as external' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+
+ context 'user was external, now should not be' do
+ it 'makes user internal' do
+ existing_user.update_attribute('external', true)
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe 'no account exists on server' do
+ shared_examples 'to verify compliance with allow_single_sign_on' do
+ context 'with allow_single_sign_on enabled' do
+ before do
+ stub_omniauth_config(allow_single_sign_on: ['saml'])
+ end
+
+ it 'creates a user from SAML' do
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ identity = gl_user.identities.first
+ expect(identity.extern_uid).to eql uid
+ expect(identity.provider).to eql 'saml'
+ end
+ end
+
+ context 'with allow_single_sign_on default (["saml"])' do
+ before do
+ stub_omniauth_config(allow_single_sign_on: ['saml'])
+ end
+
+ it 'does not throw an error' do
+ expect { saml_user.save }.not_to raise_error
+ end
+ end
+
+ context 'with allow_single_sign_on disabled' do
+ before do
+ stub_omniauth_config(allow_single_sign_on: false)
+ end
+
+ it 'throws an error' do
+ expect { saml_user.save }.to raise_error StandardError
+ end
+ end
+ end
+
+ context 'external groups' do
+ context 'are defined' do
+ it 'marks the user as external' do
+ stub_saml_group_config(%w(Freelancers))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_truthy
+ end
+ end
+
+ context 'are defined but the user does not belong there' do
+ it 'does not mark the user as external' do
+ stub_saml_group_config(%w(Interns))
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+ end
+
+ context 'with auto_link_ldap_user disabled (default)' do
+ before do
+ stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] })
+ end
+
+ include_examples 'to verify compliance with allow_single_sign_on'
+ end
+
+ context 'with auto_link_ldap_user enabled' do
+ before do
+ stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false })
+ end
+
+ context 'and at least one LDAP provider is defined' do
+ before do
+ stub_ldap_config(providers: %w(ldapmain))
+ end
+
+ context 'and a corresponding LDAP person' do
+ let(:adapter) { ldap_adapter('ldapmain') }
+
+ before do
+ allow(ldap_user).to receive(:uid) { uid }
+ allow(ldap_user).to receive(:username) { uid }
+ allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
+ allow(ldap_user).to receive(:dn) { dn }
+ allow(Gitlab::Auth::LDAP::Adapter).to receive(:new).and_return(adapter)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user)
+ end
+
+ context 'and no account for the LDAP user' do
+ it 'creates a user with dual LDAP and SAML identities' do
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql uid
+ expect(gl_user.email).to eql 'john@mail.com'
+ expect(gl_user.identities.length).to be 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn },
+ { provider: 'saml', extern_uid: uid }])
+ end
+ end
+
+ context 'and LDAP user has an account already' do
+ let(:auth_hash_base_attributes) do
+ {
+ uid: uid,
+ provider: provider,
+ info: info_hash,
+ extra: {
+ raw_info: OneLogin::RubySaml::Attributes.new(
+ { 'groups' => %w(Developers Freelancers Designers) }
+ )
+ }
+ }
+ end
+ let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes) }
+ let(:uid_types) { %w(uid dn email) }
+
+ before do
+ create(:omniauth_user,
+ email: 'john@mail.com',
+ extern_uid: dn,
+ provider: 'ldapmain',
+ username: 'john')
+ end
+
+ shared_examples 'find LDAP person' do |uid_type, uid|
+ let(:auth_hash) { OmniAuth::AuthHash.new(auth_hash_base_attributes.merge(uid: extern_uid)) }
+
+ before do
+ nil_types = uid_types - [uid_type]
+
+ nil_types.each do |type|
+ allow(Gitlab::Auth::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil)
+ end
+
+ allow(Gitlab::Auth::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user)
+ end
+
+ it 'adds the omniauth identity to the LDAP account' do
+ identities = [
+ { provider: 'ldapmain', extern_uid: dn },
+ { provider: 'saml', extern_uid: extern_uid }
+ ]
+
+ identities_as_hash = gl_user.identities.map do |id|
+ { provider: id.provider, extern_uid: id.extern_uid }
+ end
+
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'john'
+ expect(gl_user.email).to eql 'john@mail.com'
+ expect(gl_user.identities.length).to be 2
+ expect(identities_as_hash).to match_array(identities)
+ end
+ end
+
+ context 'when uid is an uid' do
+ it_behaves_like 'find LDAP person', 'uid' do
+ let(:extern_uid) { uid }
+ end
+ end
+
+ context 'when uid is a dn' do
+ it_behaves_like 'find LDAP person', 'dn' do
+ let(:extern_uid) { dn }
+ end
+ end
+
+ context 'when uid is an email' do
+ it_behaves_like 'find LDAP person', 'email' do
+ let(:extern_uid) { 'john@mail.com' }
+ end
+ end
+
+ it 'adds the omniauth identity to the LDAP account' do
+ saml_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.username).to eql 'john'
+ expect(gl_user.email).to eql 'john@mail.com'
+ expect(gl_user.identities.length).to be 2
+ identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn },
+ { provider: 'saml', extern_uid: uid }])
+ end
+
+ it 'saves successfully on subsequent tries, when both identities are present' do
+ saml_user.save
+ local_saml_user = described_class.new(auth_hash)
+ local_saml_user.save
+
+ expect(local_saml_user.gl_user).to be_valid
+ expect(local_saml_user.gl_user).to be_persisted
+ end
+ end
+
+ context 'user has SAML user, and wants to add their LDAP identity' do
+ it 'adds the LDAP identity to the existing SAML user' do
+ create(:omniauth_user, email: 'john@mail.com', extern_uid: dn, provider: 'saml', username: 'john')
+
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user)
+
+ local_hash = OmniAuth::AuthHash.new(uid: dn, provider: provider, info: info_hash)
+ local_saml_user = described_class.new(local_hash)
+
+ local_saml_user.save
+ local_gl_user = local_saml_user.gl_user
+
+ expect(local_gl_user).to be_valid
+ expect(local_gl_user.identities.length).to be 2
+ identities_as_hash = local_gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+ expect(identities_as_hash).to match_array([{ provider: 'ldapmain', extern_uid: dn },
+ { provider: 'saml', extern_uid: dn }])
+ end
+ end
+ end
+ end
+ end
+
+ context 'when signup is disabled' do
+ before do
+ stub_application_setting signup_enabled: false
+ end
+
+ it 'creates the user' do
+ saml_user.save
+
+ expect(gl_user).to be_persisted
+ end
+ end
+
+ context 'when user confirmation email is enabled' do
+ before do
+ stub_application_setting send_user_confirmation_email: true
+ end
+
+ it 'creates and confirms the user anyway' do
+ saml_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_confirmed
+ end
+ end
+ end
+
+ describe 'blocking' do
+ before do
+ stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true })
+ end
+
+ context 'signup with SAML only' do
+ context 'dont block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: false)
+ end
+
+ it 'does not block the user' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: true)
+ end
+
+ it 'blocks user' do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to be_blocked
+ end
+ end
+ end
+
+ context 'sign-in' do
+ before do
+ saml_user.save
+ saml_user.gl_user.activate
+ end
+
+ context 'dont block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: false)
+ end
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before do
+ stub_omniauth_config(block_auto_created_users: true)
+ end
+
+ it do
+ saml_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+ end
+ end
+ end
+
+ describe '#find_user' do
+ context 'raw info hash attributes empty' do
+ let(:raw_info_attr) { {} }
+
+ it 'does not mark user as external' do
+ stub_saml_group_config(%w(Freelancers))
+
+ expect(saml_user.find_user.external).to be_falsy
+ end
+ end
+ end
+end