Replaced old LDAP sync with refactored Sequencer based version 🚀.

This commit is contained in:
Thorsten Eckel 2018-01-08 16:27:23 +01:00
parent e73f75c458
commit 12a63ac17f
10 changed files with 76 additions and 1365 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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