From 12a63ac17f16084a45859dad9ed38b0d8412b934 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Mon, 8 Jan 2018 16:27:23 +0100 Subject: [PATCH] =?UTF-8?q?Replaced=20old=20LDAP=20sync=20with=20refactore?= =?UTF-8?q?d=20Sequencer=20based=20version=20=F0=9F=9A=80.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../concerns/integration/import_job_base.rb | 30 +- .../integration/exchange_controller.rb | 5 - .../integration/ldap_controller.rb | 77 ++- lib/import/ldap.rb | 24 +- lib/import/ldap/user.rb | 263 --------- lib/import/ldap/user_factory.rb | 166 ------ spec/lib/import/ldap/user_factory_spec.rb | 546 ------------------ spec/lib/import/ldap/user_spec.rb | 293 ---------- spec/lib/import/ldap_spec.rb | 11 +- spec/support/sequencer.rb | 26 +- 10 files changed, 76 insertions(+), 1365 deletions(-) delete mode 100644 lib/import/ldap/user.rb delete mode 100644 lib/import/ldap/user_factory.rb delete mode 100644 spec/lib/import/ldap/user_factory_spec.rb delete mode 100644 spec/lib/import/ldap/user_spec.rb diff --git a/app/controllers/concerns/integration/import_job_base.rb b/app/controllers/concerns/integration/import_job_base.rb index 8761987cf..bf1fbc7a4 100644 --- a/app/controllers/concerns/integration/import_job_base.rb +++ b/app/controllers/concerns/integration/import_job_base.rb @@ -9,7 +9,7 @@ module Integration::ImportJobBase end def job_try_create - ImportJob.dry_run(name: import_backend_namespace, payload: payload_dry_run) + ImportJob.dry_run(name: backend, payload: payload_dry_run) render json: { result: 'ok', } @@ -20,8 +20,8 @@ module Integration::ImportJobBase end def job_start_create - if !ImportJob.exists?(name: import_backend_namespace, finished_at: nil) - job = ImportJob.create(name: import_backend_namespace, payload: payload_import) + if !ImportJob.exists?(name: backend, finished_at: nil) + job = ImportJob.create(name: backend) job.delay.start end render json: { @@ -33,10 +33,6 @@ module Integration::ImportJobBase clean_payload(params.permit!.to_h) end - def payload_import - clean_payload(import_setting) - end - private def clean_payload(payload) @@ -54,31 +50,19 @@ module Integration::ImportJobBase } end - def import_setting - Setting.get(import_setting_name) - end - - def import_setting_name - "#{import_backend_name.downcase}_config" - end - - def import_backend_namespace - "Import::#{import_backend_name}" - end - - def import_backend_name - self.class.name.split('::').last.sub('Controller', '') + def backend + "Import::#{controller_name.classify}" end def job_index(dry_run:, take_finished: true) job = ImportJob.find_by( - name: import_backend_namespace, + name: backend, dry_run: dry_run, finished_at: nil ) if !job && take_finished job = ImportJob.where( - name: import_backend_namespace, + name: backend, dry_run: dry_run ).order(created_at: :desc).limit(1).first end diff --git a/app/controllers/integration/exchange_controller.rb b/app/controllers/integration/exchange_controller.rb index c3db9c4a3..7f7f2c20d 100644 --- a/app/controllers/integration/exchange_controller.rb +++ b/app/controllers/integration/exchange_controller.rb @@ -48,7 +48,6 @@ class Integration::ExchangeController < ApplicationController private - # currently a workaround till LDAP is migrated to Sequencer def payload_dry_run { ews_attributes: params[:attributes].permit!.to_h, @@ -57,10 +56,6 @@ class Integration::ExchangeController < ApplicationController } end - def payload_import - nil - end - def ews_config { disable_ssl_verify: params[:disable_ssl_verify], diff --git a/app/controllers/integration/ldap_controller.rb b/app/controllers/integration/ldap_controller.rb index cbf30654c..80ddb62c0 100644 --- a/app/controllers/integration/ldap_controller.rb +++ b/app/controllers/integration/ldap_controller.rb @@ -9,57 +9,50 @@ class Integration::LdapController < ApplicationController prepend_before_action { authentication_check(permission: 'admin.integration.ldap') } def discover - ldap = ::Ldap.new(params) + answer_with do + begin + ldap = ::Ldap.new(params) - render json: { - result: 'ok', - attributes: ldap.preferences, - } - rescue => e - # workaround for issue #1114 - if e.message.end_with?(', 48, Inappropriate Authentication') - result = { - result: 'ok', - attributes: {}, - } - else - logger.error e - result = { - result: 'failed', - message: e.message, - } + { + attributes: ldap.preferences + } + rescue => e + # workaround for issue #1114 + raise if !e.message.end_with?(', 48, Inappropriate Authentication') + # return empty result + {} + end end - - render json: result end def bind - # create single instance so - # User and Group don't have to - # open new connections - ldap = ::Ldap.new(params) - user = ::Ldap::User.new(params, ldap: ldap) - group = ::Ldap::Group.new(params, ldap: ldap) + answer_with do + # create single instance so + # User and Group don't have to + # open new connections + ldap = ::Ldap.new(params) + user = ::Ldap::User.new(params, ldap: ldap) + group = ::Ldap::Group.new(params, ldap: ldap) - render json: { - result: 'ok', + { + # the order of these calls is relevant! + user_filter: user.filter, + user_attributes: user.attributes, + user_uid: user.uid_attribute, - # the order of these calls is relevant! - user_filter: user.filter, - user_attributes: user.attributes, - user_uid: user.uid_attribute, + # the order of these calls is relevant! + group_filter: group.filter, + groups: group.list, + group_uid: group.uid_attribute, + } + end + end - # the order of these calls is relevant! - group_filter: group.filter, - groups: group.list, - group_uid: group.uid_attribute, - } - rescue => e - logger.error e + private - render json: { - result: 'failed', - message: e.message, + def payload_dry_run + { + ldap_config: super } end end diff --git a/lib/import/ldap.rb b/lib/import/ldap.rb index 882981bb6..6bcac2ea9 100644 --- a/lib/import/ldap.rb +++ b/lib/import/ldap.rb @@ -5,30 +5,12 @@ require 'ldap/group' module Import class Ldap < Import::IntegrationBase - - # Provides the name that is used in texts visible to the user. - # - # @example - # Import::Ldap.display_name - # #=> "LDAP" - # - # return [String] - def self.display_name - identifier.upcase - end + include Import::Mixin::Sequence private - def start_import - Import::Ldap::UserFactory.reset_statistics - - Import::Ldap::UserFactory.import( - config: @import_job.payload, - dry_run: @import_job.dry_run, - import_job: @import_job - ) - - @import_job.result = Import::Ldap::UserFactory.statistics + def sequence_name + 'Import::Ldap::Users' end end end diff --git a/lib/import/ldap/user.rb b/lib/import/ldap/user.rb deleted file mode 100644 index aee72561b..000000000 --- a/lib/import/ldap/user.rb +++ /dev/null @@ -1,263 +0,0 @@ -module Import - class Ldap - class User < Import::ModelResource - - def remote_id(_resource, *_args) - @remote_id - end - - def self.lost_map(found_remote_ids) - ExternalSync.joins('INNER JOIN users ON (users.id = external_syncs.o_id)') - .where( - source: source, - object: import_class.name, - users: { - active: true - } - ) - .pluck(:source_id, :o_id) - .to_h - .except(*found_remote_ids) - end - - def self.deactivate_lost(lost_ids) - # we need to update in slices since some DBs - # have a limit for IN length - lost_ids.each_slice(5000) do |slice| - - # we need to instanciate every entry and set - # the active state this way to send notifications - # to the client - ::User.where(id: slice).each do |user| - user.update!(active: false) - end - end - end - - private - - def import(resource, *args) - normalized_entry = normalize_entry(resource) - - # extract the uid attribute and store it as - # the remote ID so we can access it later - # when working with ExternalSync - @remote_id = normalized_entry[ @ldap_config[:user_uid].to_sym ] - - super(normalized_entry, *args) - end - - def normalize_entry(resource) - normalized_entry = resource.to_h - - normalized_entry.each do |key, values| - normalized_entry[key] = values.first - end - - normalized_entry - end - - def create_or_update(resource, *args) - result = nil - if skip?(resource) - ldap_log( - action: "skipped -> #{@remote_id}", - status: 'success', - request: resource, - ) - else - catch(:no_roles_assigned) do - determine_role_ids(resource) - - result = super(resource, *args) - - ldap_log( - action: "#{action} -> #{@resource.login}", - status: 'success', - request: resource, - ) - end - end - - result - end - - def skip?(resource) - return true if resource[:login].blank? - - # skip resource if only ignored attributes are set - ignored_attributes = %i[login dn created_by_id updated_by_id active] - resource.except(*ignored_attributes).values.none?(&:present?) - end - - def determine_role_ids(resource) - # remove temporary added and get value - dn = resource.delete(:dn) - raise "Missing 'dn' attribute for remote id '#{@remote_id}'" if dn.blank? - - if @dn_roles.present? - # check if roles are mapped for the found dn - roles = @dn_roles[ dn.downcase ] - - if roles.present? - # LDAP is the leading source if - # a mapping entry is present - @update_role_ids = roles - @create_role_ids = roles - elsif @ldap_config[:unassigned_users] == 'skip_sync' - throw :no_roles_assigned - else - use_signup_roles - end - else - use_signup_roles - end - end - - def use_signup_roles - @update_role_ids = nil # use existing - @create_role_ids = @signup_role_ids - end - - def updated?(resource, *_args) - - resource[:role_ids] = @update_role_ids if @update_role_ids - - user_found = false - import_class.without_callback(:update, :after, :avatar_for_email_check) do - user_found = super - end - - user_found - rescue => e - ldap_log( - action: "update -> #{resource[:login]}", - status: 'failed', - request: resource, - response: e.message, - ) - raise - end - - def lookup_existing(resource, *args) - instance = super - - return instance if instance.present? - - # in some cases the User will get created in - # Zammad before it's created in the LDAP - # therefore we have to make a local lookup, too - instance = local_lookup(resource) - - # create an external sync entry to connect - # the LDAP and local account for future runs - if instance.present? - external_sync_create( - local: instance, - remote: resource, - ) - - store_associations(:before, instance) - end - - instance - end - - def local_lookup(resource, *_args) - instance = import_class.identify(@remote_id) - - if instance.blank? - checked_values = [@remote_id] - %i[login email].each do |attribute| - check_value = resource[attribute] - next if check_value.blank? - next if checked_values.include?(check_value) - instance = import_class.identify(check_value) - break if instance.present? - checked_values.push(check_value) - end - end - instance - end - - def tracked_associations - [:role_ids] - end - - def create(resource, *_args) - resource[:role_ids] = @create_role_ids - import_class.without_callback(:create, :after, :avatar_for_email_check) do - super - end - rescue => e - ldap_log( - action: "create -> #{resource[:login]}", - status: 'failed', - request: resource, - response: e.message, - ) - raise - end - - def map(_resource, *_args) - mapped = super - - # we have to manually downcase the login and email - # to avoid wrong attribute change detection - %i[login email].each do |attribute| - next if mapped[attribute].blank? - mapped[attribute] = mapped[attribute].downcase - end - - # we have to add the active state manually - # because otherwise disabled instances won't get - # re-activated if they should get synced again - mapped[:active] = true - - mapped - end - - def mapping(*_args) - @mapping ||= begin - mapping = @ldap_config[:user_attributes] - - # add temporary dn to mapping so we can use it - # for the role lookup later and delete it afterwards - mapping['dn'] = 'dn' - - # fallback to uid if no login is given via mapping - if !mapping.values.include?('login') - mapping[ @ldap_config[:user_uid] ] = 'login' - end - - mapping - end - end - - def handle_args(resource, *args) - @ldap_config = args.shift - @dn_roles = args.shift - @signup_role_ids = args.shift - - super(resource, *args) - end - - def ldap_log(action:, status:, request:, response: nil) - return if @dry_run - - HttpLog.create( - direction: 'out', - facility: 'ldap', - url: action, - status: status, - ip: nil, - request: { content: request.to_json }, - response: { message: response || status }, - method: 'tcp', - created_by_id: 1, - updated_by_id: 1, - ) - end - end - end -end diff --git a/lib/import/ldap/user_factory.rb b/lib/import/ldap/user_factory.rb deleted file mode 100644 index 7a9c77e75..000000000 --- a/lib/import/ldap/user_factory.rb +++ /dev/null @@ -1,166 +0,0 @@ -module Import - class Ldap - module UserFactory - extend Import::StatisticalFactory - - def self.import(config: nil, ldap: nil, **kargs) - - # config might be an empty Hash due to the ImportJob payload - # store column which will be an empty hash if the content is NULL - if config.blank? - config = Setting.get('ldap_config') - end - - ldap ||= ::Ldap.new(config) - - @config = config - @ldap = ldap - - user_roles = user_roles(ldap: @ldap, config: config) - - if config[:unassigned_users].blank? || config[:unassigned_users] == 'sigup_roles' - signup_role_ids = Role.signup_role_ids.sort - end - - @dry_run = kargs[:dry_run] - pre_import_hook([], config, user_roles, signup_role_ids, kargs) - - import_job = kargs[:import_job] - import_job_count = 0 - - # limit the fetched attributes for an entry to only - # those which are needed to improve the performance - relevant_attributes = config[:user_attributes].keys - relevant_attributes.push('dn') - - @found_lost_remote_ids = [] - @found_remote_ids = [] - @ldap.search(config[:user_filter], attributes: relevant_attributes) do |entry| - backend_instance = create_instance(entry, config, user_roles, signup_role_ids, kargs) - post_import_hook(entry, backend_instance, config, user_roles, signup_role_ids, kargs) - - track_found_remote_ids(backend_instance) - - next if import_job.blank? - import_job_count += 1 - next if import_job_count < 100 - - import_job.result = @statistics - import_job.save - - import_job_count = 0 - end - - handle_lost - end - - def self.pre_import_hook(_records, *_args) - super - add_sum_to_statistics - end - - def self.add_sum_to_statistics - cache_key = "#{@ldap.host}::#{@ldap.port}::#{@ldap.ssl}::#{@ldap.base_dn}::#{@config[:user_filter]}" - if !@dry_run - sum = Cache.get(cache_key) - end - - sum ||= @ldap.count(@config[:user_filter]) - - @statistics[:sum] = sum - - return if !@dry_run - Cache.write(cache_key, sum, { expires_in: 1.hour }) - end - - def self.add_to_statistics(backend_instance) - super - - # no need to count if no resource was created - resource = backend_instance.resource - return if resource.blank? - - action = backend_instance.action - - add_resource_role_ids_to_statistics(resource.role_ids, action) - - action - end - - def self.add_resource_role_ids_to_statistics(role_ids, action) - return if role_ids.blank? - - known_actions = { - created: 0, - updated: 0, - unchanged: 0, - failed: 0, - deactivated: 0, - } - - @statistics[:role_ids] ||= {} - - role_ids.each do |role_id| - - next if !known_actions.key?(action) - - @statistics[:role_ids][role_id] ||= known_actions.dup - - # exit early if we have an unloggable action - break if @statistics[:role_ids][role_id][action].nil? - - @statistics[:role_ids][role_id][action] += 1 - end - end - - def self.user_roles(ldap:, config:) - group_config = { - filter: config[:group_filter] - } - - ldap_group = ::Ldap::Group.new(group_config, ldap: ldap) - ldap_group.user_roles(config[:group_role_map]) - end - - def self.track_found_remote_ids(backend_instance) - remote_id = backend_instance.remote_id(nil) - @deactivation_actions ||= %i[skipped failed] - if @deactivation_actions.include?(backend_instance.action) - @found_lost_remote_ids.push(remote_id) - else - @found_remote_ids.push(remote_id) - end - end - - def self.handle_lost - backend_class = backend_class(nil) - lost_map = backend_class.lost_map(@found_remote_ids) - - # disabled count is tracked as a separate number - # since they don't have to be in the sum (e.g. deleted in LDAP) - @statistics[:deactivated] = lost_map.size - - # skipped deactivated are those who - # were found, skipped and will get deactivated - skipped_deactivated = @found_lost_remote_ids & lost_map.keys - @statistics[:skipped] -= skipped_deactivated.size - - # loop over every lost user ID and add the - # deactivated count to the statistics - lost_ids = lost_map.values - - lost_ids.each do |user_id| - role_ids = ::User.joins(:roles) - .where(id: user_id) - .pluck(:'roles_users.role_id') - - add_resource_role_ids_to_statistics(role_ids, :deactivated) - end - - # deactivate entries only on live syncs - return if @dry_run - backend_class.deactivate_lost(lost_ids) - end - end - end -end diff --git a/spec/lib/import/ldap/user_factory_spec.rb b/spec/lib/import/ldap/user_factory_spec.rb deleted file mode 100644 index df091375b..000000000 --- a/spec/lib/import/ldap/user_factory_spec.rb +++ /dev/null @@ -1,546 +0,0 @@ -require 'rails_helper' - -RSpec.describe Import::Ldap::UserFactory do - - describe '.import' do - - it 'responds to .import' do - expect(described_class).to respond_to(:import) - end - - it 'imports users matching the configured filter' do - - config = { - user_filter: '(objectClass=user)', - group_filter: '(objectClass=group)', - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - } - } - - mocked_entry = build(:ldap_entry) - - mocked_entry['uid'] = ['exampleuid'] - mocked_entry['email'] = ['example@example.com'] - - mocked_ldap = double( - host: 'ldap.example.com', - port: 636, - ssl: true, - base_dn: 'dc=example,dc=com' - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - allow(mocked_ldap).to receive(:count).and_return(1) - # user search - expect(mocked_ldap).to receive(:search).and_yield(mocked_entry) - - expect do - described_class.import( - config: config, - ldap: mocked_ldap - ) - end.to change { - User.count - }.by(1) - end - - it 'deactivates lost users' do - - config = { - user_filter: '(objectClass=user)', - group_filter: '(objectClass=group)', - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - } - } - - persistent_entry = build(:ldap_entry) - persistent_entry['uid'] = ['exampleuid'] - persistent_entry['email'] = ['example@example.com'] - - lost_entry = build(:ldap_entry) - lost_entry['uid'] = ['exampleuid_lost'] - lost_entry['email'] = ['lost@example.com'] - - mocked_ldap = double( - host: 'ldap.example.com', - port: 636, - ssl: true, - base_dn: 'dc=example,dc=com' - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(2) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry) - - described_class.import( - config: config, - ldap: mocked_ldap, - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(1) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry) - - expect do - described_class.import( - config: config, - ldap: mocked_ldap, - ) - end.to change { - User.find_by(email: 'lost@example.com').active - } - end - - it 're-activates previously lost users' do - - config = { - user_filter: '(objectClass=user)', - group_filter: '(objectClass=group)', - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - } - } - - persistent_entry = build(:ldap_entry) - persistent_entry['uid'] = ['exampleuid'] - persistent_entry['email'] = ['example@example.com'] - - lost_entry = build(:ldap_entry) - lost_entry['uid'] = ['exampleuid_lost'] - lost_entry['email'] = ['lost@example.com'] - - mocked_ldap = double( - host: 'ldap.example.com', - port: 636, - ssl: true, - base_dn: 'dc=example,dc=com' - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(2) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry) - - described_class.import( - config: config, - ldap: mocked_ldap, - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(1) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry) - - described_class.import( - config: config, - ldap: mocked_ldap, - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(2) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry) - - expect do - described_class.import( - config: config, - ldap: mocked_ldap, - ) - end.to change { - User.find_by(email: 'lost@example.com').active - } - end - - it 'deactivates skipped users' do - - config = { - user_filter: '(objectClass=user)', - group_filter: '(objectClass=group)', - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - }, - } - - lost_entry = build(:ldap_entry) - lost_entry['uid'] = ['exampleuid'] - lost_entry['email'] = ['example@example.com'] - - mocked_ldap = double( - host: 'ldap.example.com', - port: 636, - ssl: true, - base_dn: 'dc=example,dc=com' - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(2) - # user search - expect(mocked_ldap).to receive(:search).and_yield(lost_entry) - - described_class.import( - config: config, - ldap: mocked_ldap, - ) - - # activate skipping - config[:unassigned_users] = 'skip_sync' - config[:group_role_map] = { - 'dummy' => %w[1 2], - } - - # group user role mapping - mocked_entry = build(:ldap_entry) - mocked_entry['dn'] = 'dummy' - mocked_entry['member'] = ['dummy'] - expect(mocked_ldap).to receive(:search).and_yield(mocked_entry) - - # user counting - expect(mocked_ldap).to receive(:count).and_return(1) - # user search - expect(mocked_ldap).to receive(:search).and_yield(lost_entry) - - expect do - described_class.import( - config: config, - ldap: mocked_ldap, - ) - end.to change { - User.find_by(email: 'example@example.com').active - } - end - - context 'dry run' do - - it "doesn't sync users" do - - config = { - user_filter: '(objectClass=user)', - group_filter: '(objectClass=group)', - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - } - } - - mocked_entry = build(:ldap_entry) - - mocked_entry['uid'] = ['exampleuid'] - mocked_entry['email'] = ['example@example.com'] - - mocked_ldap = double( - host: 'ldap.example.com', - port: 636, - ssl: true, - base_dn: 'dc=example,dc=com' - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(1) - # user search - expect(mocked_ldap).to receive(:search).and_yield(mocked_entry) - - expect do - described_class.import( - config: config, - ldap: mocked_ldap, - dry_run: true - ) - end.not_to change { - User.count - } - end - - it "doesn't deactivates lost users" do - - config = { - user_filter: '(objectClass=user)', - group_filter: '(objectClass=group)', - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - } - } - - persistent_entry = build(:ldap_entry) - persistent_entry['uid'] = ['exampleuid'] - persistent_entry['email'] = ['example@example.com'] - - lost_entry = build(:ldap_entry) - lost_entry['uid'] = ['exampleuid'] - lost_entry['email'] = ['example@example.com'] - - mocked_ldap = double( - host: 'ldap.example.com', - port: 636, - ssl: true, - base_dn: 'dc=example,dc=com' - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(2) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry) - - described_class.import( - config: config, - ldap: mocked_ldap, - dry_run: true - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - expect(mocked_ldap).to receive(:count).and_return(1) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry) - - expect do - described_class.import( - config: config, - ldap: mocked_ldap, - dry_run: true - ) - end.not_to change { - User.count - } - end - end - end - - describe '.add_to_statistics' do - - it 'responds to .add_to_statistics' do - expect(described_class).to respond_to(:add_to_statistics) - end - - it 'adds statistics per user role' do - - mocked_backend_instance = double( - action: :created, - resource: double( - role_ids: [1, 2] - ) - ) - - # initialize empty statistic - described_class.reset_statistics - - described_class.add_to_statistics(mocked_backend_instance) - - expected = { - role_ids: { - 1 => { - created: 1, - updated: 0, - unchanged: 0, - failed: 0, - deactivated: 0, - }, - 2 => { - created: 1, - updated: 0, - unchanged: 0, - failed: 0, - deactivated: 0, - }, - }, - skipped: 0, - created: 1, - updated: 0, - unchanged: 0, - failed: 0, - deactivated: 0, - } - - expect(described_class.statistics).to include(expected) - end - - it 'adds deactivated users' do - config = { - user_filter: '(objectClass=user)', - group_filter: '(objectClass=group)', - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - } - } - - persistent_entry = build(:ldap_entry) - persistent_entry['uid'] = ['exampleuid'] - persistent_entry['email'] = ['example@example.com'] - - lost_entry = build(:ldap_entry) - lost_entry['uid'] = ['exampleuid_lost'] - lost_entry['email'] = ['lost@example.com'] - - mocked_ldap = double( - host: 'ldap.example.com', - port: 636, - ssl: true, - base_dn: 'dc=example,dc=com' - ) - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - allow(mocked_ldap).to receive(:count).and_return(2) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry).and_yield(lost_entry) - - described_class.import( - config: config, - ldap: mocked_ldap, - ) - - # simulate new import - described_class.reset_statistics - - # group user role mapping - expect(mocked_ldap).to receive(:search) - # user counting - allow(mocked_ldap).to receive(:count).and_return(1) - # user search - expect(mocked_ldap).to receive(:search).and_yield(persistent_entry) - - described_class.import( - config: config, - ldap: mocked_ldap, - ) - - expected = { - skipped: 0, - created: 0, - updated: 0, - unchanged: 1, - failed: 0, - deactivated: 1, - } - - expect(described_class.statistics).to include(expected) - end - - it 'skips not created instances' do - - mocked_backend_instance = double( - action: :skipped, - resource: nil, - ) - - # initialize empty statistic - described_class.reset_statistics - - described_class.add_to_statistics(mocked_backend_instance) - - expected = { - skipped: 1, - created: 0, - updated: 0, - unchanged: 0, - failed: 0, - deactivated: 0, - } - - expect(described_class.statistics).to include(expected) - end - - it 'skips unwanted actions instances' do - - mocked_backend_instance = double( - action: :skipped, - resource: double( - role_ids: [1, 2] - ) - ) - - # initialize empty statistic - described_class.reset_statistics - - described_class.add_to_statistics(mocked_backend_instance) - - expected = { - skipped: 1, - created: 0, - updated: 0, - unchanged: 0, - failed: 0, - deactivated: 0, - } - - expect(described_class.statistics).to include(expected) - end - - end - - describe '.user_roles' do - - it 'responds to .user_roles' do - expect(described_class).to respond_to(:user_roles) - end - - it 'fetches the user DN to local role mapping' do - - group_dn = 'dn=... admin group...' - user_dn = 'dn=... admin user...' - - config = { - group_filter: '(objectClass=group)', - group_role_map: { - group_dn => %w[1 2], - } - } - - mocked_entry = build(:ldap_entry) - - mocked_entry['dn'] = group_dn - mocked_entry['member'] = [user_dn] - - mocked_ldap = double() - expect(mocked_ldap).to receive(:search).and_yield(mocked_entry) - - user_roles = described_class.user_roles( - ldap: mocked_ldap, - config: config, - ) - - expected = { - user_dn => [1, 2] - } - - expect(user_roles).to be_a(Hash) - expect(user_roles).to eq(expected) - end - end -end diff --git a/spec/lib/import/ldap/user_spec.rb b/spec/lib/import/ldap/user_spec.rb deleted file mode 100644 index 6fe2ea46c..000000000 --- a/spec/lib/import/ldap/user_spec.rb +++ /dev/null @@ -1,293 +0,0 @@ -require 'rails_helper' -require 'import/ldap/user' - -RSpec.describe Import::Ldap::User do - - let(:uid) { 'exampleuid' } - - let(:ldap_config) do - { - user_uid: 'uid', - user_attributes: { - 'uid' => 'login', - 'email' => 'email', - } - } - end - - let(:user_entry) do - user_entry = build(:ldap_entry) - - user_entry['uid'] = [uid] - user_entry['email'] = ['example@example.com'] - - user_entry - end - - let(:user_roles) do - { - user_entry.dn => [ - Role.find_by(name: 'Admin').id, - Role.find_by(name: 'Agent').id - ] - } - end - - let(:signup_role_ids) do - Role.signup_role_ids.sort - end - - context 'create' do - - it 'creates users from LDAP Entry' do - expect do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.to change { - User.count - }.by(1).and change { - ExternalSync.count - }.by(1) - end - - it "doesn't contact avatar webservice" do - # sadly we can't ensure that there are no - # outgoing HTTP calls with WebMock - expect(Avatar).not_to receive(:auto_detection) - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end - - it 'creates an HTTP Log entry' do - expect do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.to change { - HttpLog.count - }.by(1) - - expect(HttpLog.last.status).to eq('success') - end - - it 'logs failures to HTTP Log' do - expect_any_instance_of(User).to receive(:save!).and_raise('SOME ERROR') - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - - expect(HttpLog.last.status).to eq('failed') - end - - context 'role assignment' do - - it 'uses mapped roles from group role' do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - expect(User.last.role_ids).not_to eq(signup_role_ids) - end - - context 'no mapping entry' do - - before(:each) do - # create mapping that won't match - # since dn will change below - # this is needed since if 'user_roles' - # gets called later it will get initialized - # with the changed dn - user_roles[ user_entry.dn ] = [ - Role.find_by(name: 'Admin').id, - Role.find_by(name: 'Agent').id - ] - - # change dn so no mapping will match - user_entry['dn'] = ['some_unmapped_dn'] - end - - it 'uses signup roles by default' do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - expect(User.last.role_ids).to eq(signup_role_ids) - end - - it 'uses signup roles if configured' do - - ldap_config[:unassigned_users] = 'sigup_roles' - - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - expect(User.last.role_ids).to eq(signup_role_ids) - end - - it 'skips user if configured' do - - ldap_config[:unassigned_users] = 'skip_sync' - - instance = nil - expect do - instance = described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.not_to change { - User.count - } - expect(instance.action).to eq(:skipped) - end - end - end - end - - context 'update' do - - before(:each) do - user = create(:user, - login: uid, - role_ids: [ - Role.find_by(name: 'Agent').id, - Role.find_by(name: 'Admin').id - ]) - - ExternalSync.create( - source: 'Ldap::User', - source_id: uid, - object: 'User', - o_id: user.id - ) - end - - it 'updates users from LDAP Entry' do - expect do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.to not_change { - User.count - }.and not_change { - ExternalSync.count - } - end - - it "doesn't contact avatar webservice" do - # sadly we can't ensure that there are no - # outgoing HTTP calls with WebMock - expect(Avatar).not_to receive(:auto_detection) - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end - - it 'creates an HTTP Log entry' do - expect do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.to change { - HttpLog.count - }.by(1) - - expect(HttpLog.last.status).to eq('success') - end - - it 'finds existing Users without ExternalSync entries' do - ExternalSync.find_by( - source: 'Ldap::User', - source_id: uid, - object: 'User', - ).destroy - - expect do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.to not_change { - User.count - }.and change { - ExternalSync.count - }.by(1) - end - - it 'logs failures to HTTP Log' do - expect_any_instance_of(User).to receive(:save!).and_raise('SOME ERROR') - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - - expect(HttpLog.last.status).to eq('failed') - end - - context 'no mapping entry' do - - before(:each) do - # create mapping that won't match - # since dn will change below - # this is needed since if 'user_roles' - # gets called later it will get initialized - # with the changed dn - user_roles[ user_entry.dn ] = [ - Role.find_by(name: 'Agent').id, - Role.find_by(name: 'Admin').id - ] - - # change dn so no mapping will match - user_entry['dn'] = ['some_unmapped_dn'] - end - - it 'keeps local roles by default' do - expect do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.not_to change { - User.last.role_ids - } - end - - it 'skips user if configured' do - - ldap_config[:unassigned_users] = 'skip_sync' - - instance = nil - expect do - instance = described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.not_to change { - User.count - } - expect(instance.action).to eq(:skipped) - end - - context 'signup roles configuration' do - it 'keeps local roles' do - - ldap_config[:unassigned_users] = 'sigup_roles' - expect do - described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - end.not_to change { - User.last.role_ids - } - end - - it "doesn't detect false changes" do - # make sure that the nothing has changed - User.find_by(login: uid).update!(email: 'example@example.com') - - expect_any_instance_of(User).not_to receive(:save!) - instance = described_class.new(user_entry, ldap_config, user_roles, signup_role_ids) - expect(instance.action).to eq(:unchanged) - end - end - end - end - - context 'skipped' do - - it 'skips entries without login' do - skip_entry = build(:ldap_entry) - instance = nil - - expect do - instance = described_class.new(skip_entry, ldap_config, user_roles, signup_role_ids) - end.to not_change { - User.count - } - expect(instance.action).to eq(:skipped) - end - - it 'skips entries without attributes' do - skip_entry = build(:ldap_entry) - skip_entry['uid'] = [uid] - instance = nil - - expect do - instance = described_class.new(skip_entry, ldap_config, user_roles, signup_role_ids) - end.to not_change { - User.count - } - expect(instance.action).to eq(:skipped) - end - - it 'logs skips to HTTP Log' do - skip_entry = build(:ldap_entry) - described_class.new(skip_entry, ldap_config, user_roles, signup_role_ids) - - expect(HttpLog.last.status).to eq('success') - expect(HttpLog.last.url).to start_with('skipped') - end - end -end diff --git a/spec/lib/import/ldap_spec.rb b/spec/lib/import/ldap_spec.rb index 1359c8904..05672b236 100644 --- a/spec/lib/import/ldap_spec.rb +++ b/spec/lib/import/ldap_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' require 'lib/import/import_job_backend_examples' -RSpec.describe Import::Ldap do +RSpec.describe Import::Ldap, sequencer: :caller do it_behaves_like 'ImportJob backend' describe '.queueable?' do @@ -32,7 +32,8 @@ RSpec.describe Import::Ldap do allow(Setting).to receive(:get).with('ldap_integration').and_return(true) allow(Setting).to receive(:get).with('ldap_config').and_return(true) - expect(Import::Ldap::UserFactory).to receive(:import) + + expect_sequence instance.start end @@ -43,7 +44,7 @@ RSpec.describe Import::Ldap do import_job = create(:import_job, dry_run: true) instance = described_class.new(import_job) - expect(Import::Ldap::UserFactory).to receive(:import) + expect_sequence instance.start end @@ -54,7 +55,7 @@ RSpec.describe Import::Ldap do allow(Setting).to receive(:get).with('ldap_integration').and_return(false) - expect(Import::Ldap::UserFactory).not_to receive(:import) + expect_no_sequence expect do instance.start @@ -73,7 +74,7 @@ RSpec.describe Import::Ldap do allow(Setting).to receive(:get).with('ldap_integration').and_return(true) allow(Setting).to receive(:get).with('ldap_config').and_return({}) - expect(Import::Ldap::UserFactory).not_to receive(:import) + expect_no_sequence expect do instance.start diff --git a/spec/support/sequencer.rb b/spec/support/sequencer.rb index bbbfa9f8b..ade60d0a1 100644 --- a/spec/support/sequencer.rb +++ b/spec/support/sequencer.rb @@ -1,6 +1,6 @@ module SequencerUnit - def process(parameters, &block) + def process(parameters = {}, &block) Sequencer::Unit.process(described_class.name, parameters, &block) end end @@ -13,7 +13,31 @@ module SequencerSequence end end +module SequencerCaller + + def expect_sequence(sequence_name = nil) + + expected_method_call = receive(:process) + if sequence_name + expected_method_call.with(sequence_name) + end + + expect(Sequencer).to expected_method_call + end + + def expect_no_sequence(sequence_name = nil) + + expected_method_call = receive(:process) + if sequence_name + expected_method_call.with(sequence_name) + end + + expect(Sequencer).not_to expected_method_call + end +end + RSpec.configure do |config| config.include SequencerUnit, sequencer: :unit config.include SequencerSequence, sequencer: :sequence + config.include SequencerCaller, sequencer: :caller end