require 'rails_helper' # rails autoloading issue require 'ldap' require 'ldap/user' require 'tcr/net/ldap' RSpec.describe Ldap::User do let(:mocked_ldap) { double() } context '.uid_attribute' do it 'responds to .uid_attribute' do expect(described_class).to respond_to(:uid_attribute) end it 'returns uid attribute string from given attribute strucutre' do attributes = { objectguid: 'TEST', custom: 'value', } expect(described_class.uid_attribute(attributes)).to eq('objectguid') end it 'returns nil if no attribute could be found' do attributes = { custom: 'value', } expect(described_class.uid_attribute(attributes)).to be nil end end # required as 'let' to perform test based # expectations and reuse it in 'let' instance # as additional parameter context 'initialization config parameters' do it 'reuses given Ldap instance if given' do expect(Ldap).not_to receive(:new) described_class.new(ldap: mocked_ldap) end it 'takes optional filter' do filter = '(objectClass=custom)' config = { filter: filter } instance = described_class.new(config, ldap: mocked_ldap) expect(instance.filter).to eq(filter) end it 'takes optional uid_attribute' do uid_attribute = 'objectguid' config = { uid_attribute: uid_attribute } instance = described_class.new(config, ldap: mocked_ldap) expect(instance.uid_attribute).to eq(uid_attribute) end it 'creates own Ldap instance if none given' do expect(Ldap).to receive(:new) described_class.new() end end context 'instance methods' do let(:initialization_config) do { uid_attribute: 'objectguid', filter: '(objectClass=user)', } end let(:instance) do described_class.new(initialization_config, ldap: mocked_ldap) end context '#valid?' do it 'responds to #valid?' do expect(instance).to respond_to(:valid?) end it 'validates username and password' do connection = double() expect(mocked_ldap).to receive(:connection).and_return(connection) build(:ldap_entry) expect(mocked_ldap).to receive(:base_dn) expect(connection).to receive(:bind_as).and_return(true) expect(instance.valid?('example_username', 'password')).to be true end it 'fails for invalid credentials' do connection = double() expect(mocked_ldap).to receive(:connection).and_return(connection) build(:ldap_entry) expect(mocked_ldap).to receive(:base_dn) expect(connection).to receive(:bind_as).and_return(false) expect(instance.valid?('example_username', 'wrong_password')).to be false end end context '#attributes' do it 'responds to #attributes' do expect(instance).to respond_to(:attributes) end it 'lists user attributes with example values' do ldap_entry = build(:ldap_entry) # selectable attribute ldap_entry['mail'] = 'test@example.com' # blacklisted attribute ldap_entry['lastlogon'] = DateTime.current expect(mocked_ldap).to receive(:search).and_yield(ldap_entry) attributes = instance.attributes expected_attributes = { dn: String, mail: String, } expect(attributes).to include(expected_attributes) expect(attributes).not_to include(:lastlogon) end end context '#filter' do let(:initialization_config) do { uid_attribute: 'objectguid', } end it 'responds to #filter' do expect(instance).to respond_to(:filter) end it 'tries filters and returns first one with entries' do expect(mocked_ldap).to receive(:entries?).and_return(true) expect(instance.filter).to be_a(String) end it 'fails if no filter found entries' do allow(mocked_ldap).to receive(:entries?).and_return(false) expect(instance.filter).to be nil end end context '#uid_attribute' do let(:initialization_config) do { filter: '(objectClass=user)', } end it 'responds to #uid_attribute' do expect(instance).to respond_to(:uid_attribute) end it 'tries to find uid attribute in example attributes' do ldap_entry = build(:ldap_entry) # selectable attribute ldap_entry['objectguid'] = 'f742b361-32c6-4a92-baaa-eaae7df657ee' expect(mocked_ldap).to receive(:search).and_yield(ldap_entry) expect(instance.uid_attribute).to be_a(String) end it 'fails if no uid attribute could be found' do expect(mocked_ldap).to receive(:search) expect(instance.uid_attribute).to be nil end end end # Each of these test cases depends on # sample TCP transmission data recorded with TCR, # stored in test/data/tcr_cassettes. context 'on mocked LDAP connections' do around do |example| cassette_name = example.description.gsub(/[^0-9A-Za-z.\-]+/, '_') begin original_tcr_format = TCR.configuration.format TCR.configuration.format = 'marshal' TCR.use_cassette("lib/ldap/user/#{cassette_name}") { example.run } ensure TCR.configuration.format = original_tcr_format end end describe '#attributes' do let(:subject) { described_class.new(config, ldap: ldap) } let(:ldap) { Ldap.new(config) } let(:config) do { 'host_url' => 'ldap://localhost', 'options' => { 'dc=example,dc=org' => 'dc=example,dc=org' }, 'option' => 'dc=example,dc=org', 'base_dn' => 'dc=example,dc=org', 'bind_user' => 'cn=admin,dc=example,dc=org', 'bind_pw' => 'admin' }.with_indifferent_access end # see https://github.com/zammad/zammad/issues/2140 # # This method grabs sample values of user attributes on the LDAP server. # It used to coerce ALL values to Unicode strings, including binary attributes # (e.g., usersmimecertificate / msexchmailboxsecuritydescriptor), # which led to valid Unicode gibberish (e.g., "\u0001\u0001\u0004...") # # When saving these values to the database, # ActiveRecord::Store would convert them to binary (ASCII-8BIT) strings, # which would then break #to_json with an Encoding::UndefinedConversion error. it 'skips binary attributes (#2140)' do Setting.set('ldap_config', subject.attributes) expect { Setting.get('ldap_config').to_json } .not_to raise_error end end end end