View | Details | Raw Unified | Return to bug 45252 | Differences between
and this patch

Collapse All | Expand All

(-)a/services/univention-s4-connector/modules/univention/s4connector/__init__.py (-7 / +4 lines)
 Lines 42-47   import traceback Link Here 
42
import types
42
import types
43
import ldap
43
import ldap
44
import univention.uldap
44
import univention.uldap
45
from univention.lib import ordered_set
45
import univention.admin.uldap
46
import univention.admin.uldap
46
import univention.admin.modules
47
import univention.admin.modules
47
import univention.admin.objects
48
import univention.admin.objects
 Lines 1203-1215   class ucs: Link Here 
1203
						else:
1204
						else:
1204
							equal = compare[0] == compare[1]
1205
							equal = compare[0] == compare[1]
1205
						if not equal:
1206
						if not equal:
1206
							# This is deduplication of LDAP attribute values for S4 -> UCS.
1207
							# This is deduplication of LDAP attribute values
1207
							# It destroys ordering of multi-valued attributes. This seems problematic
1208
							# for S4 -> UCS with preserved order.
1208
							# as the handling of `con_other_attribute` assumes preserved ordering
1209
							# (this is not guaranteed by LDAP).
1210
							# See the MODIFY-case in `sync_from_ucs()` for more.
1209
							# See the MODIFY-case in `sync_from_ucs()` for more.
1211
							if isinstance(value, list):
1210
							if isinstance(value, list):
1212
								ucs_object[ucs_key] = list(set(value))
1211
								ucs_object[ucs_key] = list(ordered_set.OrderedSet(value))
1213
							else:
1212
							else:
1214
								ucs_object[ucs_key] = value
1213
								ucs_object[ucs_key] = value
1215
							ud.debug(ud.LDAP, ud.INFO, "set key in ucs-object: %s" % ucs_key)
1214
							ud.debug(ud.LDAP, ud.INFO, "set key in ucs-object: %s" % ucs_key)
1216
- 
1217
--
1218
.../modules/univention/s4connector/s4/__init__.py  | 253 +++++++++------------
1215
.../modules/univention/s4connector/s4/__init__.py  | 253 +++++++++------------
1219
1 file changed, 113 insertions(+), 140 deletions(-)
1216
1 file changed, 113 insertions(+), 140 deletions(-)
(-)a/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py (-142 / +113 lines)
 Lines 2579-2738   class s4(univention.s4connector.ucs): Link Here 
2579
		#
2579
		#
2580
		elif (object['modtype'] == 'modify' and s4_object) or (object['modtype'] == 'add' and s4_object):
2580
		elif (object['modtype'] == 'modify' and s4_object) or (object['modtype'] == 'add' and s4_object):
2581
			ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: modify object: %s" % object['dn'])
2581
			ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: modify object: %s" % object['dn'])
2582
			ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_object: %s" % old_ucs_object)
2582
			ud.debug(ud.LDAP, ud.INFO,
2583
			ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_object: %s" % new_ucs_object)
2583
				"sync_from_ucs: old_object: %s" % old_ucs_object)
2584
			ud.debug(ud.LDAP, ud.INFO,
2585
				"sync_from_ucs: new_object: %s" % new_ucs_object)
2584
			object['old_ucs_object'] = old_ucs_object
2586
			object['old_ucs_object'] = old_ucs_object
2585
			object['new_ucs_object'] = new_ucs_object
2587
			object['new_ucs_object'] = new_ucs_object
2586
			attribute_list = set(old_ucs_object.keys()).union(set(new_ucs_object.keys()))
2588
			attribute_list = set(old_ucs_object.keys() + new_ucs_object.keys())
2587
			if hasattr(self.property[property_type], "con_sync_function"):
2588
				self.property[property_type].con_sync_function(self, property_type, object)
2589
			else:
2590
				# Iterate over attributes and post_attributes
2591
				for attribute_type_name, attribute_type in [('attributes', self.property[property_type].attributes), ('post_attributes', self.property[property_type].post_attributes)]:
2592
					if hasattr(self.property[property_type], attribute_type_name) and attribute_type is not None:
2593
						for attr in attribute_list:
2594
							value = new_ucs_object.get(attr)
2595
							if not self.__has_attribute_value_changed(attr, old_ucs_object, new_ucs_object):
2596
								continue
2597
2589
2598
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The following attribute has been changed: %s" % attr)
2590
			def find_case_independent(s4_object, attribute):
2599
2591
				attr = attribute.lower()
2600
							for attribute in attribute_type.keys():
2592
				matching = (v for (k, v) in s4_object.iteritems() if k.lower() == attr)
2601
								if attribute_type[attribute].ldap_attribute != attr:
2593
				try:
2602
									continue
2594
					values = next(matching)
2595
				except StopIteration:
2596
					values = []
2597
				return set(values)
2598
2599
			# Iterate over attributes and post_attributes
2600
			for attribute_type_name, attribute_type in [('attributes', self.property[property_type].attributes),
2601
					('post_attributes', self.property[property_type].post_attributes)]:
2602
				if hasattr(self.property[property_type], attribute_type_name) and attribute_type is not None:
2603
					for attr in attribute_list:
2604
						if not self.__has_attribute_value_changed(attr, old_ucs_object, new_ucs_object):
2605
							continue
2603
2606
2604
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: Found a corresponding mapping defintion: %s" % attribute)
2607
						ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The following attribute has been changed: %s" % attr)
2605
								s4_attribute = attribute_type[attribute].con_attribute
2606
								s4_other_attribute = attribute_type[attribute].con_other_attribute
2607
2608
2608
								if not attribute_type[attribute].sync_mode in ['write', 'sync']:
2609
						for attribute in attribute_type.keys():
2609
									ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: %s is in not in write or sync mode. Skipping" % attribute)
2610
							if attribute_type[attribute].ldap_attribute != attr:
2610
									continue
2611
								continue
2611
2612
2612
								modify = False
2613
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: Found a corresponding mapping defintion: %s" % attribute)
2614
							s4_attribute = attribute_type[attribute].con_attribute
2615
							s4_other_attribute = attribute_type[attribute].con_other_attribute
2613
2616
2614
								# Get the UCS attributes
2617
							if not attribute_type[attribute].sync_mode in ['write', 'sync']:
2615
								old_values = set(old_ucs_object.get(attr, []))
2618
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: %s is in not in write or sync mode. Skipping" % attribute)
2616
								new_values = set(new_ucs_object.get(attr, []))
2619
								continue
2617
2620
2618
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_values: %s" % old_values)
2621
							# Get the UCS attributes
2619
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_values: %s" % new_values)
2622
							old_values = set(old_ucs_object.get(attr, []))
2623
							new_values = set(new_ucs_object.get(attr, []))
2620
2624
2621
								if attribute_type[attribute].compare_function:
2625
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_values: %s" % old_values)
2622
									if not attribute_type[attribute].compare_function(list(old_values), list(new_values)):
2626
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_values: %s" % new_values)
2623
										modify = True
2624
								elif not univention.s4connector.compare_lowercase(list(old_values), list(new_values)):  # FIXME: use defined compare-function from mapping.py
2625
									modify = True
2626
2627
2627
								if not modify:
2628
							current_s4_values = find_case_independent(s4_object, s4_attribute)
2628
									ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: no modification necessary for %s" % attribute)
2629
									continue
2630
2629
2631
								# So, at this point we have the old and the new UCS object.
2630
							compare_function = attribute_type[attribute].compare_function or \
2632
								# Thus we can create the diff, but we have to check the current S4 object
2631
								univention.s4connector.compare_lowercase
2632
							if compare_function(list(old_values), list(new_values)):
2633
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: no modification necessary for %s" % attribute)
2634
								continue
2633
2635
2634
								if not old_values:
2636
							# So, at this point we have the old and the new UCS object.
2635
									to_add = new_values
2637
							# Thus we can create the diff, but we have to check the current S4 object
2636
									to_remove = set([])
2638
							to_add = new_values - old_values
2637
								elif not new_values:
2639
							to_remove = old_values - new_values
2638
									to_remove = old_values
2640
2639
									to_add = set([])
2641
							if s4_other_attribute:
2640
								else:
2642
								# This is the case, where we map from a multi-valued UCS attribute to two S4 attributes.
2641
									to_add = new_values - old_values
2643
								# telephoneNumber/otherTelephone (S4) to telephoneNumber (UCS) would be an example.
2642
									to_remove = old_values - new_values
2644
								#
2643
2645
								# The direct mapping assumes preserved ordering of the multi-valued UCS
2644
								if s4_other_attribute:
2646
								# attributes and places the first value in the primary S4 attribute,
2645
									# This is the case, where we map from a multi-valued UCS attribute to two S4 attributes.
2647
								# the rest in the secondary S4 attributes.
2646
									# telephoneNumber/otherTelephone (S4) to telephoneNumber (UCS) would be an example.
2648
								#
2647
									#
2649
								# The following code handles the correct distribution of the UCS attribute,
2648
									# The direct mapping assumes preserved ordering of the multi-valued UCS
2650
								# to two S4 attributes. It also ensures, that the primary S4 attribute keeps
2649
									# attributes and places the first value in the primary S4 attribute,
2651
								# its value as long as that value is not removed. If removed the primary
2650
									# the rest in the secondary S4 attributes.
2652
								# attribute is assigned a random value from the UCS attribute.
2651
									# Assuming preserved ordering is wrong, as LDAP does not guarantee is and the
2653
								current_s4_other_values = find_case_independent(s4_object, s4_other_attribute)
2652
									# deduplication of LDAP attribute values in `__set_values()` destroys it.
2654
2653
									#
2655
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values)
2654
									# The following code handles the correct distribution of the UCS attribute,
2656
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values)
2655
									# to two S4 attributes. It also ensures, that the primary S4 attribute keeps
2657
2656
									# its value as long as that value is not removed. If removed the primary
2658
								# If we removed the value on the UCS side that was contained in the `s4_attribute`,
2657
									# attribute is assigned a random value from the UCS attribute.
2659
								# but are adding new values, we choose a random value from the new values.
2658
									try:
2660
								new_s4_values = current_s4_values - to_remove
2659
										current_s4_values = set([v for k, v in s4_object.iteritems() if s4_attribute.lower() == k.lower()][0])
2661
								if not new_s4_values and to_add:
2660
									except IndexError:
2662
									new_s4_values.add(to_add.pop())
2661
										current_s4_values = set([])
2663
								new_s4_other_values = (current_s4_other_values | to_add) - to_remove - current_s4_values
2662
									ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values)
2664
2663
2665
								if current_s4_values != new_s4_values:
2664
									try:
2666
									modlist.append((ldap.MOD_REPLACE, s4_attribute, new_s4_values))
2665
										current_s4_other_values = set([v for k, v in s4_object.iteritems() if s4_other_attribute.lower() == k.lower()][0])
2667
								if current_s4_other_values != new_s4_other_values:
2666
									except IndexError:
2668
									modlist.append((ldap.MOD_REPLACE, s4_other_attribute, new_s4_other_values))
2667
										current_s4_other_values = set([])
2669
							else:
2668
									ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values)
2670
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values)
2669
2671
2670
									new_s4_values = current_s4_values - to_remove
2672
								if (to_add or to_remove) and attribute_type[attribute].single_value:
2671
									if not new_s4_values and to_add:
2673
									value = new_ucs_object.get(attr)
2672
										for n_value in new_ucs_object.get(attr, []):
2674
									modify = not current_s4_values or not value or \
2673
											if n_value in to_add:
2675
										not compare_function(list(current_s4_values), list(value))
2674
												to_add = to_add - set([n_value])
2676
									if modify:
2675
												new_s4_values = [n_value]
2677
										mapping = getattr(attribute_type[attribute], 'mapping', ())
2676
												break
2678
										if len(mapping) > 0 and mapping[0]:
2677
2679
											ud.debug(ud.LDAP, ud.PROCESS, "Calling single value mapping function")
2678
									new_s4_other_values = (current_s4_other_values | to_add) - to_remove - current_s4_values
2680
											value = mapping[0](self, None, object)
2679
									if current_s4_values != new_s4_values:
2681
										modlist.append((ldap.MOD_REPLACE, s4_attribute, value))
2680
										if new_s4_values:
2681
											modlist.append((ldap.MOD_REPLACE, s4_attribute, new_s4_values))
2682
										else:
2683
											modlist.append((ldap.MOD_REPLACE, s4_attribute, []))
2684
2685
									if current_s4_other_values != new_s4_other_values:
2686
										modlist.append((ldap.MOD_REPLACE, s4_other_attribute, new_s4_other_values))
2687
								else:
2682
								else:
2688
									try:
2683
									if to_remove:
2689
										current_s4_values = set([v for k, v in s4_object.iteritems() if s4_attribute.lower() == k.lower()][0])
2684
										r = current_s4_values & to_remove
2690
									except IndexError:
2685
										if r:
2691
										current_s4_values = set([])
2686
											modlist.append((ldap.MOD_DELETE, s4_attribute, r))
2692
2687
									if to_add:
2693
									ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values)
2688
										a = to_add - current_s4_values
2694
2689
										if a:
2695
									if (to_add or to_remove) and attribute_type[attribute].single_value:
2690
											modlist.append((ldap.MOD_ADD, s4_attribute, a))
2696
										modify = False
2691
2697
										if not current_s4_values or not value:
2692
			if not modlist:
2698
											modify = True
2693
				ud.debug(ud.LDAP, ud.ALL, "nothing to modify: %s" % object['dn'])
2699
										elif attribute_type[attribute].compare_function:
2694
			else:
2700
											if not attribute_type[attribute].compare_function(list(current_s4_values), list(value)):
2695
				ud.debug(ud.LDAP, ud.INFO, "to modify: %s" % object['dn'])
2701
												modify = True
2696
				ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: modlist: %s" % modlist)
2702
										elif not univention.s4connector.compare_lowercase(list(current_s4_values), list(value)):
2697
				try:
2703
											modify = True
2698
					self.lo_s4.lo.modify_ext_s(compatible_modstring(object['dn']), compatible_modlist(modlist), serverctrls=self.serverctrls_for_add_and_modify)
2704
										if modify:
2699
				except:
2705
											if hasattr(attribute_type[attribute], 'mapping') and len(attribute_type[attribute].mapping) > 0 and attribute_type[attribute].mapping[0]:
2700
					ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback during modify object: %s" % object['dn'])
2706
												ud.debug(ud.LDAP, ud.PROCESS, "Calling single value mapping function")
2701
					ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback due to modlist: %s" % modlist)
2707
												value = attribute_type[attribute].mapping[0](self, None, object)
2702
					raise
2708
											modlist.append((ldap.MOD_REPLACE, s4_attribute, value))
2709
									else:
2710
										if to_remove:
2711
											r = current_s4_values & to_remove
2712
											if r:
2713
												modlist.append((ldap.MOD_DELETE, s4_attribute, r))
2714
										if to_add:
2715
											a = to_add - current_s4_values
2716
											if a:
2717
												modlist.append((ldap.MOD_ADD, s4_attribute, a))
2718
2719
				if not modlist:
2720
					ud.debug(ud.LDAP, ud.ALL, "nothing to modify: %s" % object['dn'])
2721
				else:
2722
					ud.debug(ud.LDAP, ud.INFO, "to modify: %s" % object['dn'])
2723
					ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: modlist: %s" % modlist)
2724
					try:
2725
						self.lo_s4.lo.modify_ext_s(compatible_modstring(object['dn']), compatible_modlist(modlist), serverctrls=self.serverctrls_for_add_and_modify)
2726
					except:
2727
						ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback during modify object: %s" % object['dn'])
2728
						ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback due to modlist: %s" % modlist)
2729
						raise
2730
2703
2731
				if hasattr(self.property[property_type], "post_con_modify_functions"):
2704
			if hasattr(self.property[property_type], "post_con_modify_functions"):
2732
					for f in self.property[property_type].post_con_modify_functions:
2705
				for f in self.property[property_type].post_con_modify_functions:
2733
						ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s" % f)
2706
					ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s" % f)
2734
						f(self, property_type, object)
2707
					f(self, property_type, object)
2735
						ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s (done)" % f)
2708
					ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s (done)" % f)
2736
		#
2709
		#
2737
		# DELETE
2710
		# DELETE
2738
		#
2711
		#
2739
- 
2740
handling
2712
handling
2741
--
2742
.../modules/univention/s4connector/s4/__init__.py  | 57 ++++++++++++++++------
2713
.../modules/univention/s4connector/s4/__init__.py  | 57 ++++++++++++++++------
2743
1 file changed, 41 insertions(+), 16 deletions(-)
2714
1 file changed, 41 insertions(+), 16 deletions(-)
(-)a/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py (-18 / +41 lines)
 Lines 42-47   import time Link Here 
42
import types
42
import types
43
import array
43
import array
44
import univention.uldap
44
import univention.uldap
45
from univention.lib import ordered_set
45
import univention.s4connector
46
import univention.s4connector
46
import univention.debug2 as ud
47
import univention.debug2 as ud
47
from ldap.controls import LDAPControl
48
from ldap.controls import LDAPControl
 Lines 51-56   from samba.dcerpc import security Link Here 
51
from samba.ndr import ndr_pack, ndr_unpack
52
from samba.ndr import ndr_pack, ndr_unpack
52
from samba.dcerpc import misc
53
from samba.dcerpc import misc
53
54
55
54
DECODE_IGNORELIST = ['objectSid', 'objectGUID', 'repsFrom', 'replUpToDateVector', 'ipsecData', 'logonHours', 'userCertificate', 'dNSProperty', 'dnsRecord']
56
DECODE_IGNORELIST = ['objectSid', 'objectGUID', 'repsFrom', 'replUpToDateVector', 'ipsecData', 'logonHours', 'userCertificate', 'dNSProperty', 'dnsRecord']
55
57
56
LDAP_SERVER_SHOW_DELETED_OID = "1.2.840.113556.1.4.417"
58
LDAP_SERVER_SHOW_DELETED_OID = "1.2.840.113556.1.4.417"
 Lines 2585-2591   class s4(univention.s4connector.ucs): Link Here 
2585
				"sync_from_ucs: new_object: %s" % new_ucs_object)
2587
				"sync_from_ucs: new_object: %s" % new_ucs_object)
2586
			object['old_ucs_object'] = old_ucs_object
2588
			object['old_ucs_object'] = old_ucs_object
2587
			object['new_ucs_object'] = new_ucs_object
2589
			object['new_ucs_object'] = new_ucs_object
2588
			attribute_list = set(old_ucs_object.keys() + new_ucs_object.keys())
2590
			attribute_list = ordered_set.OrderedSet(old_ucs_object.keys() + new_ucs_object.keys())
2589
2591
2590
			def find_case_independent(s4_object, attribute):
2592
			def find_case_independent(s4_object, attribute):
2591
				attr = attribute.lower()
2593
				attr = attribute.lower()
 Lines 2594-2600   class s4(univention.s4connector.ucs): Link Here 
2594
					values = next(matching)
2596
					values = next(matching)
2595
				except StopIteration:
2597
				except StopIteration:
2596
					values = []
2598
					values = []
2597
				return set(values)
2599
				return ordered_set.OrderedSet(values)
2598
2600
2599
			# Iterate over attributes and post_attributes
2601
			# Iterate over attributes and post_attributes
2600
			for attribute_type_name, attribute_type in [('attributes', self.property[property_type].attributes),
2602
			for attribute_type_name, attribute_type in [('attributes', self.property[property_type].attributes),
 Lines 2619-2626   class s4(univention.s4connector.ucs): Link Here 
2619
								continue
2621
								continue
2620
2622
2621
							# Get the UCS attributes
2623
							# Get the UCS attributes
2622
							old_values = set(old_ucs_object.get(attr, []))
2624
							old_values = ordered_set.OrderedSet(old_ucs_object.get(attr, []))
2623
							new_values = set(new_ucs_object.get(attr, []))
2625
							new_values = ordered_set.OrderedSet(new_ucs_object.get(attr, []))
2624
2626
2625
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_values: %s" % old_values)
2627
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_values: %s" % old_values)
2626
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_values: %s" % new_values)
2628
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_values: %s" % new_values)
 Lines 2638-2666   class s4(univention.s4connector.ucs): Link Here 
2638
							to_add = new_values - old_values
2640
							to_add = new_values - old_values
2639
							to_remove = old_values - new_values
2641
							to_remove = old_values - new_values
2640
2642
2643
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: to_add: %s" % to_add)
2644
							ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: to_remove: %s" % to_remove)
2645
2641
							if s4_other_attribute:
2646
							if s4_other_attribute:
2642
								# This is the case, where we map from a multi-valued UCS attribute to two S4 attributes.
2647
								# This is the case, where we map from a multi-valued UCS attribute to two S4 attributes.
2643
								# telephoneNumber/otherTelephone (S4) to telephoneNumber (UCS) would be an example.
2648
								# telephoneNumber/otherTelephone (S4) to telephoneNumber (UCS) would be an example.
2644
								#
2649
								#
2645
								# The direct mapping assumes preserved ordering of the multi-valued UCS
2650
								# In Active Directory, for attributes that are split in two the administrator is
2646
								# attributes and places the first value in the primary S4 attribute,
2651
								# responsible for keeping a value in `telephoneNumber`. Imagine the following:
2647
								# the rest in the secondary S4 attributes.
2652
								# (a) telephoneNumber = '123', otherTelephone = ['123', '456']
2653
								# In this case, if the administrator deletes the value of `telephoneNumber`,
2654
								# Active Directory does NOT automatically pull a new value from `otherTelephone`.
2655
								#
2656
								# This is impossible to support with the connector. Imagine again case (a). If
2657
								# we delete `123` from `phone` via UDM, AD would be synced into the following
2658
								# state: (b) telephoneNumber = '', otherTelephone = ['456']
2659
								# From now on, whenever we add a new value to `phone` via UDM, for example:
2660
								# (c) phone = ['456', '789'] it MUST be synced as
2661
								# (d) telephoneNumber = '', otherTelephone = ['456', '789'] as '456' came
2662
								# before '789' and '456' is definitely in `otherTelephone`.
2648
								#
2663
								#
2649
								# The following code handles the correct distribution of the UCS attribute,
2664
								# We therefore implement, that `telephoneNumber` is never empty, as long as there
2650
								# to two S4 attributes. It also ensures, that the primary S4 attribute keeps
2665
								# are values in `otherTelephone`. If a modification would delete the value of
2651
								# its value as long as that value is not removed. If removed the primary
2666
								# `telephoneNumber` and at least one value exists in `otherTelephone`, the
2652
								# attribute is assigned a random value from the UCS attribute.
2667
								# connector duplicates the first entry of `otherTelephone` into
2668
								# `telephoneNumber`.
2653
								current_s4_other_values = find_case_independent(s4_object, s4_other_attribute)
2669
								current_s4_other_values = find_case_independent(s4_object, s4_other_attribute)
2654
2670
2655
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values)
2671
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values)
2656
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values)
2672
								ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values)
2657
2673
2658
								# If we removed the value on the UCS side that was contained in the `s4_attribute`,
2659
								# but are adding new values, we choose a random value from the new values.
2660
								new_s4_values = current_s4_values - to_remove
2674
								new_s4_values = current_s4_values - to_remove
2661
								if not new_s4_values and to_add:
2675
								retained_s4_other_values = current_s4_other_values - to_remove
2662
									new_s4_values.add(to_add.pop())
2676
2663
								new_s4_other_values = (current_s4_other_values | to_add) - to_remove - current_s4_values
2677
								# If we removed the value on the UCS side that was contained in the `current_s4_values`,
2678
								# but have values, we duplicate the first value from the `new_s4_other_values`.
2679
								if not new_s4_values:
2680
									if retained_s4_other_values:
2681
										new_s4_values.add(retained_s4_other_values[0])
2682
									elif to_add:
2683
										new_s4_values.add(to_add[0])
2684
2685
								# Take the old values without those to be removed. Add the new ones
2686
								# (without duplicating the `new_s4_values`, but preserving existing
2687
								# duplicates in `con_other_attribute`)
2688
								new_s4_other_values = retained_s4_other_values | (to_add - new_s4_values)
2664
2689
2665
								if current_s4_values != new_s4_values:
2690
								if current_s4_values != new_s4_values:
2666
									modlist.append((ldap.MOD_REPLACE, s4_attribute, new_s4_values))
2691
									modlist.append((ldap.MOD_REPLACE, s4_attribute, new_s4_values))
2667
- 
2668
--
2669
.../52_s4connector/502_other_attribute_sync.py     | 184 +++++++++++++++++++++
2692
.../52_s4connector/502_other_attribute_sync.py     | 184 +++++++++++++++++++++
2670
1 file changed, 184 insertions(+)
2693
1 file changed, 184 insertions(+)
2671
create mode 100755 test/ucs-test/tests/52_s4connector/502_other_attribute_sync.py
2694
create mode 100755 test/ucs-test/tests/52_s4connector/502_other_attribute_sync.py
(-)a/test/ucs-test/tests/52_s4connector/502_other_attribute_sync.py (-1 / +184 lines)
Line 0    Link Here 
0
- 
1
#!/usr/share/ucs-test/runner /usr/bin/py.test -s
2
# coding: utf-8
3
## desc: "Test the UCS<->AD sync in {read,write,sync} mode for `con_other_attribute`s."
4
## exposure: dangerous
5
## packages:
6
## - univention-s4-connector
7
## bugs:
8
##  - 35903
9
##  - 36480
10
11
import pytest
12
13
from univention.testing.udm import UCSTestUDM
14
import univention.testing.connector_common as tcommon
15
from univention.testing.connector_common import (NormalUser, create_udm_user,
16
	delete_udm_user, create_con_user, delete_con_user)
17
18
import s4connector
19
from s4connector import (connector_running_on_this_host, connector_setup)
20
21
MAPPINGS = (
22
	# ucs_attribute, con_attribute, con_other_attribute
23
	('phone', 'telephoneNumber', 'otherTelephone'),
24
	('homeTelephoneNumber', 'homePhone', 'otherHomePhone'),
25
	('mobileTelephoneNumber', 'mobile', 'otherMobile'),
26
	('pagerTelephoneNumber', 'pager', 'otherPager'),
27
)
28
29
30
def random_number():
31
	return tcommon.random_string(numeric=True)
32
33
34
# General Information: In Active Directory, for attributes that are split in
35
# two (e.g. `telephoneNumber` and `otherTelephone`), the administrator is
36
# responsible for keeping a value in `telephoneNumber`. Imagine the following:
37
# (a) telephoneNumber = '123', otherTelephone = ['123', '456']
38
# In this case, if the administrator deletes the value of `telephoneNumber`,
39
# Active Directory does NOT automatically pull a new value from `otherTelephone`.
40
#
41
# This is impossible to support with the connector. Imagine again case (a). If
42
# we delete `123` from `phone` via UDM, AD would be synced into the following
43
# state: (b) telephoneNumber = [], otherTelephone = ['456']
44
# From now on, whenever we add a new value to `phone` via UDM, for example:
45
# (c) phone = ['456', '789'] it MUST be synced as
46
# (d) telephoneNumber = [], otherTelephone = ['456', '789'] as '456' came
47
# before '789' and '456' is definitely in `otherTelephone`.
48
#
49
# These tests enforce, that `telephoneNumber` is never empty, as long as there
50
# are values in `otherTelephone`. If a modification would delete the value of
51
# `telephoneNumber` and at least one value exists in `otherTelephone`, the
52
# connector duplicates the first entry of `otherTelephone` into
53
# `telephoneNumber`.
54
55
56
@pytest.mark.parametrize("attribute", MAPPINGS)
57
@pytest.mark.parametrize("sync_mode", ["write", "sync"])
58
@pytest.mark.skipif(not connector_running_on_this_host(),
59
	reason="Univention S4 Connector not configured.")
60
def test_attribute_sync_from_udm_to_s4(attribute, sync_mode):
61
	(ucs_attribute, con_attribute, con_other_attribute) = attribute
62
	udm_user = NormalUser(selection=("username", "lastname", ucs_attribute))
63
	primary_value = udm_user.basic.get(ucs_attribute)
64
	all_values = (primary_value, random_number(), random_number())
65
	secondary_values = all_values[1:]
66
67
	with connector_setup(sync_mode) as s4, UCSTestUDM() as udm:
68
		# A single `phone` number must be synced to `telephoneNumber` in AD.
69
		(udm_user_dn, s4_user_dn) = create_udm_user(udm, s4, udm_user, s4connector.wait_for_sync)
70
71
		# Additional `phone` values must be synced to `otherTelephone`,
72
		# `telephoneNumber` must keep its value.
73
		print("\nModifying UDM user: {}={}\n".format(ucs_attribute, all_values))
74
		udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: all_values})
75
		s4connector.wait_for_sync()
76
		s4.verify_object(s4_user_dn,
77
			{con_attribute: primary_value, con_other_attribute: secondary_values})
78
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: all_values})
79
80
		# If we delete the first `phone` value via UDM, we want to duplicate
81
		# the first value of `otherTelephone` into `telephoneNumber`.
82
		(new_primary, next_primary) = secondary_values
83
		print("\nModifying UDM user: {}={}\n".format(ucs_attribute, secondary_values))
84
		udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: secondary_values})
85
		s4connector.wait_for_sync()
86
		s4.verify_object(s4_user_dn,
87
			{con_attribute: new_primary, con_other_attribute: secondary_values})
88
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: secondary_values})
89
90
		# If we delete a `phone` value via UDM that is duplicated in AD, we want
91
		# it to be deleted from `telephoneNumber` and `otherTelephone`.
92
		print("\nModifying UDM user: {}={}\n".format(ucs_attribute, next_primary))
93
		udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: next_primary})
94
		s4connector.wait_for_sync()
95
		s4.verify_object(s4_user_dn,
96
			{con_attribute: next_primary, con_other_attribute: next_primary})
97
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: next_primary})
98
99
		# Setting a completely new `phone` value via UDM, this must be synced
100
		# to `telephoneNumber` and `otherTelephone` must be empty.
101
		new_phone_who_dis = random_number()
102
		print("\nModifying UDM user: {}={}\n".format(ucs_attribute, new_phone_who_dis))
103
		udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: new_phone_who_dis})
104
		s4connector.wait_for_sync()
105
		s4.verify_object(s4_user_dn, {con_attribute: new_phone_who_dis, con_other_attribute: []})
106
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_phone_who_dis})
107
108
		# No `phone` value via UDM, must result in an empty `telephoneNumber`
109
		# and `otherTelephone`.
110
		print("\nModifying UDM user: {}={}\n".format(ucs_attribute, []))
111
		udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: ''})
112
		s4connector.wait_for_sync()
113
		s4.verify_object(s4_user_dn, {con_attribute: [], con_other_attribute: []})
114
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: []})
115
116
		delete_udm_user(udm, s4, udm_user_dn, s4_user_dn, s4connector.wait_for_sync)
117
118
119
@pytest.mark.parametrize("attribute", MAPPINGS)
120
@pytest.mark.parametrize("sync_mode", ["read", "sync"])
121
@pytest.mark.skipif(not connector_running_on_this_host(),
122
	reason="Univention S4 Connector not configured.")
123
def test_attribute_sync_from_s4_to_udm(attribute, sync_mode):
124
	(ucs_attribute, con_attribute, con_other_attribute) = attribute
125
	udm_user = NormalUser(selection=("username", "lastname", ucs_attribute))
126
	primary_value = udm_user.basic.get(ucs_attribute)
127
	all_values = (primary_value, random_number(), random_number())
128
	secondary_values = all_values[1:]
129
130
	with connector_setup(sync_mode) as s4:
131
		# A single `telephoneNumber` must be synced to `phone` in UDM.
132
		(basic_s4_user, s4_user_dn, udm_user_dn) = create_con_user(s4,
133
			udm_user, s4connector.wait_for_sync)
134
135
		# Additional values in `otherTelephone` must be appended to `phone`.
136
		print("\nModifying S4 user: {}={}, {}={}\n".format(con_attribute,
137
			primary_value, con_other_attribute, secondary_values))
138
		s4.set_attributes(s4_user_dn,
139
			**{con_attribute: primary_value, con_other_attribute: secondary_values})
140
		s4connector.wait_for_sync()
141
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: all_values})
142
		s4.verify_object(s4_user_dn,
143
			{con_attribute: primary_value, con_other_attribute: secondary_values})
144
145
		if sync_mode == "sync":  # otherwise the connector can't write into AD
146
			# If we delete the value of `telephoneNumber` from AD, we expect to get
147
			# the first value of `otherTelephone` duplicated into
148
			# `telephoneNumber`.
149
			(new_primary, _) = secondary_values
150
			print("\nModifying S4 user: {}={}\n".format(con_attribute, []))
151
			s4.set_attributes(s4_user_dn, **{con_attribute: []})
152
			s4connector.wait_for_sync()
153
			tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: secondary_values})
154
			s4.verify_object(s4_user_dn,
155
				{con_attribute: new_primary, con_other_attribute: secondary_values})
156
157
			# Deleting the duplicate from `otherTelephone` must retain the value of
158
			# `telephoneNumber` and `phone` in UDM.
159
			print("\nModifying S4 user: {}={}\n".format(con_other_attribute, []))
160
			s4.set_attributes(s4_user_dn, **{con_other_attribute: []})
161
			s4connector.wait_for_sync()
162
			tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_primary})
163
			s4.verify_object(s4_user_dn,
164
				{con_attribute: new_primary, con_other_attribute: []})
165
166
		# Setting a new `telephoneNumber` and no `otherTelephone` in AD must
167
		# result in a single new value in `phone`.
168
		new_phone_who_dis = random_number()
169
		print("\nModifying S4 user: {}={}\n".format(con_attribute, new_phone_who_dis))
170
		s4.set_attributes(s4_user_dn, **{con_attribute: new_phone_who_dis, con_other_attribute: []})
171
		s4connector.wait_for_sync()
172
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_phone_who_dis})
173
		s4.verify_object(s4_user_dn,
174
			{con_attribute: new_phone_who_dis, con_other_attribute: []})
175
176
		# Setting no `telephoneNumber` and no `otherTelephone` in AD must
177
		# result in no value in `phone`.
178
		print("\nModifying S4 user: {}={}\n".format(con_attribute, []))
179
		s4.set_attributes(s4_user_dn, **{con_attribute: [], con_other_attribute: []})
180
		s4connector.wait_for_sync()
181
		tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: []})
182
		s4.verify_object(s4_user_dn, {con_attribute: [], con_other_attribute: []})
183
184
		delete_con_user(s4, s4_user_dn, udm_user_dn, s4connector.wait_for_sync)

Return to bug 45252