Browse Source

Replaced old LDAP sync with refactored Sequencer based version 🚀.

Thorsten Eckel 7 years ago
parent
commit
12a63ac17f

+ 7 - 23
app/controllers/concerns/integration/import_job_base.rb

@@ -9,7 +9,7 @@ module Integration::ImportJobBase
   end
   end
 
 
   def job_try_create
   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: {
     render json: {
       result: 'ok',
       result: 'ok',
     }
     }
@@ -20,8 +20,8 @@ module Integration::ImportJobBase
   end
   end
 
 
   def job_start_create
   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
       job.delay.start
     end
     end
     render json: {
     render json: {
@@ -33,10 +33,6 @@ module Integration::ImportJobBase
     clean_payload(params.permit!.to_h)
     clean_payload(params.permit!.to_h)
   end
   end
 
 
-  def payload_import
-    clean_payload(import_setting)
-  end
-
   private
   private
 
 
   def clean_payload(payload)
   def clean_payload(payload)
@@ -54,31 +50,19 @@ module Integration::ImportJobBase
     }
     }
   end
   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
   end
 
 
   def job_index(dry_run:, take_finished: true)
   def job_index(dry_run:, take_finished: true)
     job = ImportJob.find_by(
     job = ImportJob.find_by(
-      name:        import_backend_namespace,
+      name:        backend,
       dry_run:     dry_run,
       dry_run:     dry_run,
       finished_at: nil
       finished_at: nil
     )
     )
     if !job && take_finished
     if !job && take_finished
       job = ImportJob.where(
       job = ImportJob.where(
-        name:    import_backend_namespace,
+        name:    backend,
         dry_run: dry_run
         dry_run: dry_run
       ).order(created_at: :desc).limit(1).first
       ).order(created_at: :desc).limit(1).first
     end
     end

+ 0 - 5
app/controllers/integration/exchange_controller.rb

@@ -48,7 +48,6 @@ class Integration::ExchangeController < ApplicationController
 
 
   private
   private
 
 
-  # currently a workaround till LDAP is migrated to Sequencer
   def payload_dry_run
   def payload_dry_run
     {
     {
       ews_attributes: params[:attributes].permit!.to_h,
       ews_attributes: params[:attributes].permit!.to_h,
@@ -57,10 +56,6 @@ class Integration::ExchangeController < ApplicationController
     }
     }
   end
   end
 
 
-  def payload_import
-    nil
-  end
-
   def ews_config
   def ews_config
     {
     {
       disable_ssl_verify: params[:disable_ssl_verify],
       disable_ssl_verify: params[:disable_ssl_verify],

+ 38 - 45
app/controllers/integration/ldap_controller.rb

@@ -9,57 +9,50 @@ class Integration::LdapController < ApplicationController
   prepend_before_action { authentication_check(permission: 'admin.integration.ldap') }
   prepend_before_action { authentication_check(permission: 'admin.integration.ldap') }
 
 
   def discover
   def discover
-    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,
-      }
+    answer_with do
+      begin
+        ldap = ::Ldap.new(params)
+
+        {
+          attributes: ldap.preferences
+        }
+      rescue => e
+        # workaround for issue #1114
+        raise if !e.message.end_with?(', 48, Inappropriate Authentication')
+        # return empty result
+        {}
+      end
     end
     end
-
-    render json: result
   end
   end
 
 
   def bind
   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)
-
-    render json: {
-      result: 'ok',
-
-      # the order of these calls is relevant!
-      user_filter:     user.filter,
-      user_attributes: user.attributes,
-      user_uid:        user.uid_attribute,
+    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)
+
+      {
+        # 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
 end
 end

+ 3 - 21
lib/import/ldap.rb

@@ -5,30 +5,12 @@ require 'ldap/group'
 
 
 module Import
 module Import
   class Ldap < Import::IntegrationBase
   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
     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
   end
 end
 end

+ 0 - 263
lib/import/ldap/user.rb

@@ -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

+ 0 - 166
lib/import/ldap/user_factory.rb

@@ -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

+ 0 - 546
spec/lib/import/ldap/user_factory_spec.rb

@@ -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

+ 0 - 293
spec/lib/import/ldap/user_spec.rb

@@ -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

+ 6 - 5
spec/lib/import/ldap_spec.rb

@@ -1,7 +1,7 @@
 require 'rails_helper'
 require 'rails_helper'
 require 'lib/import/import_job_backend_examples'
 require 'lib/import/import_job_backend_examples'
 
 
-RSpec.describe Import::Ldap do
+RSpec.describe Import::Ldap, sequencer: :caller do
   it_behaves_like 'ImportJob backend'
   it_behaves_like 'ImportJob backend'
 
 
   describe '.queueable?' do
   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_integration').and_return(true)
       allow(Setting).to receive(:get).with('ldap_config').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
       instance.start
     end
     end
@@ -43,7 +44,7 @@ RSpec.describe Import::Ldap do
         import_job = create(:import_job, dry_run: true)
         import_job = create(:import_job, dry_run: true)
         instance   = described_class.new(import_job)
         instance   = described_class.new(import_job)
 
 
-        expect(Import::Ldap::UserFactory).to receive(:import)
+        expect_sequence
 
 
         instance.start
         instance.start
       end
       end
@@ -54,7 +55,7 @@ RSpec.describe Import::Ldap do
 
 
         allow(Setting).to receive(:get).with('ldap_integration').and_return(false)
         allow(Setting).to receive(:get).with('ldap_integration').and_return(false)
 
 
-        expect(Import::Ldap::UserFactory).not_to receive(:import)
+        expect_no_sequence
 
 
         expect do
         expect do
           instance.start
           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_integration').and_return(true)
         allow(Setting).to receive(:get).with('ldap_config').and_return({})
         allow(Setting).to receive(:get).with('ldap_config').and_return({})
 
 
-        expect(Import::Ldap::UserFactory).not_to receive(:import)
+        expect_no_sequence
 
 
         expect do
         expect do
           instance.start
           instance.start

+ 25 - 1
spec/support/sequencer.rb

@@ -1,6 +1,6 @@
 module SequencerUnit
 module SequencerUnit
 
 
-  def process(parameters, &block)
+  def process(parameters = {}, &block)
     Sequencer::Unit.process(described_class.name, parameters, &block)
     Sequencer::Unit.process(described_class.name, parameters, &block)
   end
   end
 end
 end
@@ -13,7 +13,31 @@ module SequencerSequence
   end
   end
 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|
 RSpec.configure do |config|
   config.include SequencerUnit, sequencer: :unit
   config.include SequencerUnit, sequencer: :unit
   config.include SequencerSequence, sequencer: :sequence
   config.include SequencerSequence, sequencer: :sequence
+  config.include SequencerCaller, sequencer: :caller
 end
 end