diff options
Diffstat (limited to 'spec/lib/gitlab/auth')
-rw-r--r-- | spec/lib/gitlab/auth/ldap/access_spec.rb | 166 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/ldap/adapter_spec.rb | 144 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/ldap/auth_hash_spec.rb | 110 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/ldap/authentication_spec.rb | 58 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/ldap/config_spec.rb | 373 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/ldap/dn_spec.rb | 224 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/ldap/person_spec.rb | 160 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/ldap/user_spec.rb | 241 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb | 111 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/o_auth/provider_spec.rb | 42 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/o_auth/user_spec.rb | 742 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/saml/auth_hash_spec.rb | 40 | ||||
-rw-r--r-- | spec/lib/gitlab/auth/saml/user_spec.rb | 403 |
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 |