Univention Bugzilla – Attachment 9140 Details for
Bug 45252
S4C: Fixup `con_other_attribute` sync (e.g. telephoneNumber)
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
35903-s4c-test-telephone-421.patch
35903-s4c-test-telephone-421.patch (text/plain), 32.83 KB, created by
Lukas Oyen
on 2017-08-23 14:46:29 CEST
(
hide
)
Description:
35903-s4c-test-telephone-421.patch
Filename:
MIME Type:
Creator:
Lukas Oyen
Created:
2017-08-23 14:46:29 CEST
Size:
32.83 KB
patch
obsolete
>From bd6ec9e5266b1e6d20241b65cd37eb69a72ba4f7 Mon Sep 17 00:00:00 2001 >From: Lukas Oyen <oyen@univention.de> >Date: Thu, 17 Aug 2017 17:51:35 +0200 >Subject: [PATCH 1/4] Bug #35903: s4c: fix attribute deduplication: preserve > order > >--- > .../modules/univention/s4connector/__init__.py | 9 ++++----- > 1 file changed, 4 insertions(+), 5 deletions(-) > >diff --git a/services/univention-s4-connector/modules/univention/s4connector/__init__.py b/services/univention-s4-connector/modules/univention/s4connector/__init__.py >index a59565f..c92b205 100644 >--- a/services/univention-s4-connector/modules/univention/s4connector/__init__.py >+++ b/services/univention-s4-connector/modules/univention/s4connector/__init__.py >@@ -42,6 +42,7 @@ import traceback > import types > import ldap > import univention.uldap >+from univention.lib import ordered_set > import univention.admin.uldap > import univention.admin.modules > import univention.admin.objects >@@ -1203,13 +1204,11 @@ class ucs: > else: > equal = compare[0] == compare[1] > if not equal: >- # This is deduplication of LDAP attribute values for S4 -> UCS. >- # It destroys ordering of multi-valued attributes. This seems problematic >- # as the handling of `con_other_attribute` assumes preserved ordering >- # (this is not guaranteed by LDAP). >+ # This is deduplication of LDAP attribute values >+ # for S4 -> UCS with preserved order. > # See the MODIFY-case in `sync_from_ucs()` for more. > if isinstance(value, list): >- ucs_object[ucs_key] = list(set(value)) >+ ucs_object[ucs_key] = list(ordered_set.OrderedSet(value)) > else: > ucs_object[ucs_key] = value > ud.debug(ud.LDAP, ud.INFO, "set key in ucs-object: %s" % ucs_key) >-- >2.7.4 > > >From 9f6476ba543600b0561a18c2f15f69c9877d1302 Mon Sep 17 00:00:00 2001 >From: Lukas Oyen <oyen@univention.de> >Date: Thu, 17 Aug 2017 18:21:55 +0200 >Subject: [PATCH 2/4] Bug #35903: s4c: cleanup `sync_from_ucs()` MODIFY case > >--- > .../modules/univention/s4connector/s4/__init__.py | 253 +++++++++------------ > 1 file changed, 113 insertions(+), 140 deletions(-) > >diff --git a/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py b/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py >index c7b9ffb..b30a2df 100644 >--- a/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py >+++ b/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py >@@ -2579,160 +2579,133 @@ class s4(univention.s4connector.ucs): > # > elif (object['modtype'] == 'modify' and s4_object) or (object['modtype'] == 'add' and s4_object): > ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: modify object: %s" % object['dn']) >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_object: %s" % old_ucs_object) >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_object: %s" % new_ucs_object) >+ ud.debug(ud.LDAP, ud.INFO, >+ "sync_from_ucs: old_object: %s" % old_ucs_object) >+ ud.debug(ud.LDAP, ud.INFO, >+ "sync_from_ucs: new_object: %s" % new_ucs_object) > object['old_ucs_object'] = old_ucs_object > object['new_ucs_object'] = new_ucs_object >- attribute_list = set(old_ucs_object.keys()).union(set(new_ucs_object.keys())) >- if hasattr(self.property[property_type], "con_sync_function"): >- self.property[property_type].con_sync_function(self, property_type, object) >- else: >- # Iterate over attributes and post_attributes >- for attribute_type_name, attribute_type in [('attributes', self.property[property_type].attributes), ('post_attributes', self.property[property_type].post_attributes)]: >- if hasattr(self.property[property_type], attribute_type_name) and attribute_type is not None: >- for attr in attribute_list: >- value = new_ucs_object.get(attr) >- if not self.__has_attribute_value_changed(attr, old_ucs_object, new_ucs_object): >- continue >+ attribute_list = set(old_ucs_object.keys() + new_ucs_object.keys()) > >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The following attribute has been changed: %s" % attr) >- >- for attribute in attribute_type.keys(): >- if attribute_type[attribute].ldap_attribute != attr: >- continue >+ def find_case_independent(s4_object, attribute): >+ attr = attribute.lower() >+ matching = (v for (k, v) in s4_object.iteritems() if k.lower() == attr) >+ try: >+ values = next(matching) >+ except StopIteration: >+ values = [] >+ return set(values) >+ >+ # Iterate over attributes and post_attributes >+ for attribute_type_name, attribute_type in [('attributes', self.property[property_type].attributes), >+ ('post_attributes', self.property[property_type].post_attributes)]: >+ if hasattr(self.property[property_type], attribute_type_name) and attribute_type is not None: >+ for attr in attribute_list: >+ if not self.__has_attribute_value_changed(attr, old_ucs_object, new_ucs_object): >+ continue > >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: Found a corresponding mapping defintion: %s" % attribute) >- s4_attribute = attribute_type[attribute].con_attribute >- s4_other_attribute = attribute_type[attribute].con_other_attribute >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The following attribute has been changed: %s" % attr) > >- if not attribute_type[attribute].sync_mode in ['write', 'sync']: >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: %s is in not in write or sync mode. Skipping" % attribute) >- continue >+ for attribute in attribute_type.keys(): >+ if attribute_type[attribute].ldap_attribute != attr: >+ continue > >- modify = False >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: Found a corresponding mapping defintion: %s" % attribute) >+ s4_attribute = attribute_type[attribute].con_attribute >+ s4_other_attribute = attribute_type[attribute].con_other_attribute > >- # Get the UCS attributes >- old_values = set(old_ucs_object.get(attr, [])) >- new_values = set(new_ucs_object.get(attr, [])) >+ if not attribute_type[attribute].sync_mode in ['write', 'sync']: >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: %s is in not in write or sync mode. Skipping" % attribute) >+ continue > >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_values: %s" % old_values) >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_values: %s" % new_values) >+ # Get the UCS attributes >+ old_values = set(old_ucs_object.get(attr, [])) >+ new_values = set(new_ucs_object.get(attr, [])) > >- if attribute_type[attribute].compare_function: >- if not attribute_type[attribute].compare_function(list(old_values), list(new_values)): >- modify = True >- elif not univention.s4connector.compare_lowercase(list(old_values), list(new_values)): # FIXME: use defined compare-function from mapping.py >- modify = True >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_values: %s" % old_values) >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_values: %s" % new_values) > >- if not modify: >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: no modification necessary for %s" % attribute) >- continue >+ current_s4_values = find_case_independent(s4_object, s4_attribute) > >- # So, at this point we have the old and the new UCS object. >- # Thus we can create the diff, but we have to check the current S4 object >+ compare_function = attribute_type[attribute].compare_function or \ >+ univention.s4connector.compare_lowercase >+ if compare_function(list(old_values), list(new_values)): >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: no modification necessary for %s" % attribute) >+ continue > >- if not old_values: >- to_add = new_values >- to_remove = set([]) >- elif not new_values: >- to_remove = old_values >- to_add = set([]) >- else: >- to_add = new_values - old_values >- to_remove = old_values - new_values >- >- if s4_other_attribute: >- # This is the case, where we map from a multi-valued UCS attribute to two S4 attributes. >- # telephoneNumber/otherTelephone (S4) to telephoneNumber (UCS) would be an example. >- # >- # The direct mapping assumes preserved ordering of the multi-valued UCS >- # attributes and places the first value in the primary S4 attribute, >- # the rest in the secondary S4 attributes. >- # Assuming preserved ordering is wrong, as LDAP does not guarantee is and the >- # deduplication of LDAP attribute values in `__set_values()` destroys it. >- # >- # The following code handles the correct distribution of the UCS attribute, >- # to two S4 attributes. It also ensures, that the primary S4 attribute keeps >- # its value as long as that value is not removed. If removed the primary >- # attribute is assigned a random value from the UCS attribute. >- try: >- current_s4_values = set([v for k, v in s4_object.iteritems() if s4_attribute.lower() == k.lower()][0]) >- except IndexError: >- current_s4_values = set([]) >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values) >- >- try: >- current_s4_other_values = set([v for k, v in s4_object.iteritems() if s4_other_attribute.lower() == k.lower()][0]) >- except IndexError: >- current_s4_other_values = set([]) >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values) >- >- new_s4_values = current_s4_values - to_remove >- if not new_s4_values and to_add: >- for n_value in new_ucs_object.get(attr, []): >- if n_value in to_add: >- to_add = to_add - set([n_value]) >- new_s4_values = [n_value] >- break >- >- new_s4_other_values = (current_s4_other_values | to_add) - to_remove - current_s4_values >- if current_s4_values != new_s4_values: >- if new_s4_values: >- modlist.append((ldap.MOD_REPLACE, s4_attribute, new_s4_values)) >- else: >- modlist.append((ldap.MOD_REPLACE, s4_attribute, [])) >- >- if current_s4_other_values != new_s4_other_values: >- modlist.append((ldap.MOD_REPLACE, s4_other_attribute, new_s4_other_values)) >+ # So, at this point we have the old and the new UCS object. >+ # Thus we can create the diff, but we have to check the current S4 object >+ to_add = new_values - old_values >+ to_remove = old_values - new_values >+ >+ if s4_other_attribute: >+ # This is the case, where we map from a multi-valued UCS attribute to two S4 attributes. >+ # telephoneNumber/otherTelephone (S4) to telephoneNumber (UCS) would be an example. >+ # >+ # The direct mapping assumes preserved ordering of the multi-valued UCS >+ # attributes and places the first value in the primary S4 attribute, >+ # the rest in the secondary S4 attributes. >+ # >+ # The following code handles the correct distribution of the UCS attribute, >+ # to two S4 attributes. It also ensures, that the primary S4 attribute keeps >+ # its value as long as that value is not removed. If removed the primary >+ # attribute is assigned a random value from the UCS attribute. >+ current_s4_other_values = find_case_independent(s4_object, s4_other_attribute) >+ >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values) >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values) >+ >+ # If we removed the value on the UCS side that was contained in the `s4_attribute`, >+ # but are adding new values, we choose a random value from the new values. >+ new_s4_values = current_s4_values - to_remove >+ if not new_s4_values and to_add: >+ new_s4_values.add(to_add.pop()) >+ new_s4_other_values = (current_s4_other_values | to_add) - to_remove - current_s4_values >+ >+ if current_s4_values != new_s4_values: >+ modlist.append((ldap.MOD_REPLACE, s4_attribute, new_s4_values)) >+ if current_s4_other_values != new_s4_other_values: >+ modlist.append((ldap.MOD_REPLACE, s4_other_attribute, new_s4_other_values)) >+ else: >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values) >+ >+ if (to_add or to_remove) and attribute_type[attribute].single_value: >+ value = new_ucs_object.get(attr) >+ modify = not current_s4_values or not value or \ >+ not compare_function(list(current_s4_values), list(value)) >+ if modify: >+ mapping = getattr(attribute_type[attribute], 'mapping', ()) >+ if len(mapping) > 0 and mapping[0]: >+ ud.debug(ud.LDAP, ud.PROCESS, "Calling single value mapping function") >+ value = mapping[0](self, None, object) >+ modlist.append((ldap.MOD_REPLACE, s4_attribute, value)) > else: >- try: >- current_s4_values = set([v for k, v in s4_object.iteritems() if s4_attribute.lower() == k.lower()][0]) >- except IndexError: >- current_s4_values = set([]) >- >- ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values) >- >- if (to_add or to_remove) and attribute_type[attribute].single_value: >- modify = False >- if not current_s4_values or not value: >- modify = True >- elif attribute_type[attribute].compare_function: >- if not attribute_type[attribute].compare_function(list(current_s4_values), list(value)): >- modify = True >- elif not univention.s4connector.compare_lowercase(list(current_s4_values), list(value)): >- modify = True >- if modify: >- if hasattr(attribute_type[attribute], 'mapping') and len(attribute_type[attribute].mapping) > 0 and attribute_type[attribute].mapping[0]: >- ud.debug(ud.LDAP, ud.PROCESS, "Calling single value mapping function") >- value = attribute_type[attribute].mapping[0](self, None, object) >- modlist.append((ldap.MOD_REPLACE, s4_attribute, value)) >- else: >- if to_remove: >- r = current_s4_values & to_remove >- if r: >- modlist.append((ldap.MOD_DELETE, s4_attribute, r)) >- if to_add: >- a = to_add - current_s4_values >- if a: >- modlist.append((ldap.MOD_ADD, s4_attribute, a)) >- >- if not modlist: >- ud.debug(ud.LDAP, ud.ALL, "nothing to modify: %s" % object['dn']) >- else: >- ud.debug(ud.LDAP, ud.INFO, "to modify: %s" % object['dn']) >- ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: modlist: %s" % modlist) >- try: >- self.lo_s4.lo.modify_ext_s(compatible_modstring(object['dn']), compatible_modlist(modlist), serverctrls=self.serverctrls_for_add_and_modify) >- except: >- ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback during modify object: %s" % object['dn']) >- ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback due to modlist: %s" % modlist) >- raise >+ if to_remove: >+ r = current_s4_values & to_remove >+ if r: >+ modlist.append((ldap.MOD_DELETE, s4_attribute, r)) >+ if to_add: >+ a = to_add - current_s4_values >+ if a: >+ modlist.append((ldap.MOD_ADD, s4_attribute, a)) >+ >+ if not modlist: >+ ud.debug(ud.LDAP, ud.ALL, "nothing to modify: %s" % object['dn']) >+ else: >+ ud.debug(ud.LDAP, ud.INFO, "to modify: %s" % object['dn']) >+ ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: modlist: %s" % modlist) >+ try: >+ self.lo_s4.lo.modify_ext_s(compatible_modstring(object['dn']), compatible_modlist(modlist), serverctrls=self.serverctrls_for_add_and_modify) >+ except: >+ ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback during modify object: %s" % object['dn']) >+ ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback due to modlist: %s" % modlist) >+ raise > >- if hasattr(self.property[property_type], "post_con_modify_functions"): >- for f in self.property[property_type].post_con_modify_functions: >- ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s" % f) >- f(self, property_type, object) >- ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s (done)" % f) >+ if hasattr(self.property[property_type], "post_con_modify_functions"): >+ for f in self.property[property_type].post_con_modify_functions: >+ ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s" % f) >+ f(self, property_type, object) >+ ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s (done)" % f) > # > # DELETE > # >-- >2.7.4 > > >From fc33a15333610e3a5bcd8959e43da86602378681 Mon Sep 17 00:00:00 2001 >From: Lukas Oyen <oyen@univention.de> >Date: Mon, 21 Aug 2017 11:01:03 +0200 >Subject: [PATCH 3/4] Bug #35903: s4c: implement correct `con_other_attribute` > handling > >--- > .../modules/univention/s4connector/s4/__init__.py | 57 ++++++++++++++++------ > 1 file changed, 41 insertions(+), 16 deletions(-) > >diff --git a/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py b/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py >index b30a2df..3de6e52 100644 >--- a/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py >+++ b/services/univention-s4-connector/modules/univention/s4connector/s4/__init__.py >@@ -42,6 +42,7 @@ import time > import types > import array > import univention.uldap >+from univention.lib import ordered_set > import univention.s4connector > import univention.debug2 as ud > from ldap.controls import LDAPControl >@@ -51,6 +52,7 @@ from samba.dcerpc import security > from samba.ndr import ndr_pack, ndr_unpack > from samba.dcerpc import misc > >+ > DECODE_IGNORELIST = ['objectSid', 'objectGUID', 'repsFrom', 'replUpToDateVector', 'ipsecData', 'logonHours', 'userCertificate', 'dNSProperty', 'dnsRecord'] > > LDAP_SERVER_SHOW_DELETED_OID = "1.2.840.113556.1.4.417" >@@ -2585,7 +2587,7 @@ class s4(univention.s4connector.ucs): > "sync_from_ucs: new_object: %s" % new_ucs_object) > object['old_ucs_object'] = old_ucs_object > object['new_ucs_object'] = new_ucs_object >- attribute_list = set(old_ucs_object.keys() + new_ucs_object.keys()) >+ attribute_list = ordered_set.OrderedSet(old_ucs_object.keys() + new_ucs_object.keys()) > > def find_case_independent(s4_object, attribute): > attr = attribute.lower() >@@ -2594,7 +2596,7 @@ class s4(univention.s4connector.ucs): > values = next(matching) > except StopIteration: > values = [] >- return set(values) >+ return ordered_set.OrderedSet(values) > > # Iterate over attributes and post_attributes > for attribute_type_name, attribute_type in [('attributes', self.property[property_type].attributes), >@@ -2619,8 +2621,8 @@ class s4(univention.s4connector.ucs): > continue > > # Get the UCS attributes >- old_values = set(old_ucs_object.get(attr, [])) >- new_values = set(new_ucs_object.get(attr, [])) >+ old_values = ordered_set.OrderedSet(old_ucs_object.get(attr, [])) >+ new_values = ordered_set.OrderedSet(new_ucs_object.get(attr, [])) > > ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_values: %s" % old_values) > ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_values: %s" % new_values) >@@ -2638,29 +2640,52 @@ class s4(univention.s4connector.ucs): > to_add = new_values - old_values > to_remove = old_values - new_values > >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: to_add: %s" % to_add) >+ ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: to_remove: %s" % to_remove) >+ > if s4_other_attribute: > # This is the case, where we map from a multi-valued UCS attribute to two S4 attributes. > # telephoneNumber/otherTelephone (S4) to telephoneNumber (UCS) would be an example. > # >- # The direct mapping assumes preserved ordering of the multi-valued UCS >- # attributes and places the first value in the primary S4 attribute, >- # the rest in the secondary S4 attributes. >+ # In Active Directory, for attributes that are split in two the administrator is >+ # responsible for keeping a value in `telephoneNumber`. Imagine the following: >+ # (a) telephoneNumber = '123', otherTelephone = ['123', '456'] >+ # In this case, if the administrator deletes the value of `telephoneNumber`, >+ # Active Directory does NOT automatically pull a new value from `otherTelephone`. >+ # >+ # This is impossible to support with the connector. Imagine again case (a). If >+ # we delete `123` from `phone` via UDM, AD would be synced into the following >+ # state: (b) telephoneNumber = '', otherTelephone = ['456'] >+ # From now on, whenever we add a new value to `phone` via UDM, for example: >+ # (c) phone = ['456', '789'] it MUST be synced as >+ # (d) telephoneNumber = '', otherTelephone = ['456', '789'] as '456' came >+ # before '789' and '456' is definitely in `otherTelephone`. > # >- # The following code handles the correct distribution of the UCS attribute, >- # to two S4 attributes. It also ensures, that the primary S4 attribute keeps >- # its value as long as that value is not removed. If removed the primary >- # attribute is assigned a random value from the UCS attribute. >+ # We therefore implement, that `telephoneNumber` is never empty, as long as there >+ # are values in `otherTelephone`. If a modification would delete the value of >+ # `telephoneNumber` and at least one value exists in `otherTelephone`, the >+ # connector duplicates the first entry of `otherTelephone` into >+ # `telephoneNumber`. > current_s4_other_values = find_case_independent(s4_object, s4_other_attribute) > > ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values) > ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values) > >- # If we removed the value on the UCS side that was contained in the `s4_attribute`, >- # but are adding new values, we choose a random value from the new values. > new_s4_values = current_s4_values - to_remove >- if not new_s4_values and to_add: >- new_s4_values.add(to_add.pop()) >- new_s4_other_values = (current_s4_other_values | to_add) - to_remove - current_s4_values >+ retained_s4_other_values = current_s4_other_values - to_remove >+ >+ # If we removed the value on the UCS side that was contained in the `current_s4_values`, >+ # but have values, we duplicate the first value from the `new_s4_other_values`. >+ if not new_s4_values: >+ if retained_s4_other_values: >+ new_s4_values.add(retained_s4_other_values[0]) >+ elif to_add: >+ new_s4_values.add(to_add[0]) >+ >+ # Take the old values without those to be removed. Add the new ones >+ # (without duplicating the `new_s4_values`, but preserving existing >+ # duplicates in `con_other_attribute`) >+ new_s4_other_values = retained_s4_other_values | (to_add - new_s4_values) > > if current_s4_values != new_s4_values: > modlist.append((ldap.MOD_REPLACE, s4_attribute, new_s4_values)) >-- >2.7.4 > > >From f98d8ad5bc76697b9ba3ab70de4317e0aba4247a Mon Sep 17 00:00:00 2001 >From: Lukas Oyen <oyen@univention.de> >Date: Thu, 17 Aug 2017 16:51:12 +0200 >Subject: [PATCH 4/4] Bug #35903: s4c-test: add `con_other_attribute` sync test > >--- > .../52_s4connector/502_other_attribute_sync.py | 184 +++++++++++++++++++++ > 1 file changed, 184 insertions(+) > create mode 100755 test/ucs-test/tests/52_s4connector/502_other_attribute_sync.py > >diff --git a/test/ucs-test/tests/52_s4connector/502_other_attribute_sync.py b/test/ucs-test/tests/52_s4connector/502_other_attribute_sync.py >new file mode 100755 >index 0000000..3dedb90 >--- /dev/null >+++ b/test/ucs-test/tests/52_s4connector/502_other_attribute_sync.py >@@ -0,0 +1,184 @@ >+#!/usr/share/ucs-test/runner /usr/bin/py.test -s >+# coding: utf-8 >+## desc: "Test the UCS<->AD sync in {read,write,sync} mode for `con_other_attribute`s." >+## exposure: dangerous >+## packages: >+## - univention-s4-connector >+## bugs: >+## - 35903 >+## - 36480 >+ >+import pytest >+ >+from univention.testing.udm import UCSTestUDM >+import univention.testing.connector_common as tcommon >+from univention.testing.connector_common import (NormalUser, create_udm_user, >+ delete_udm_user, create_con_user, delete_con_user) >+ >+import s4connector >+from s4connector import (connector_running_on_this_host, connector_setup) >+ >+MAPPINGS = ( >+ # ucs_attribute, con_attribute, con_other_attribute >+ ('phone', 'telephoneNumber', 'otherTelephone'), >+ ('homeTelephoneNumber', 'homePhone', 'otherHomePhone'), >+ ('mobileTelephoneNumber', 'mobile', 'otherMobile'), >+ ('pagerTelephoneNumber', 'pager', 'otherPager'), >+) >+ >+ >+def random_number(): >+ return tcommon.random_string(numeric=True) >+ >+ >+# General Information: In Active Directory, for attributes that are split in >+# two (e.g. `telephoneNumber` and `otherTelephone`), the administrator is >+# responsible for keeping a value in `telephoneNumber`. Imagine the following: >+# (a) telephoneNumber = '123', otherTelephone = ['123', '456'] >+# In this case, if the administrator deletes the value of `telephoneNumber`, >+# Active Directory does NOT automatically pull a new value from `otherTelephone`. >+# >+# This is impossible to support with the connector. Imagine again case (a). If >+# we delete `123` from `phone` via UDM, AD would be synced into the following >+# state: (b) telephoneNumber = [], otherTelephone = ['456'] >+# From now on, whenever we add a new value to `phone` via UDM, for example: >+# (c) phone = ['456', '789'] it MUST be synced as >+# (d) telephoneNumber = [], otherTelephone = ['456', '789'] as '456' came >+# before '789' and '456' is definitely in `otherTelephone`. >+# >+# These tests enforce, that `telephoneNumber` is never empty, as long as there >+# are values in `otherTelephone`. If a modification would delete the value of >+# `telephoneNumber` and at least one value exists in `otherTelephone`, the >+# connector duplicates the first entry of `otherTelephone` into >+# `telephoneNumber`. >+ >+ >+@pytest.mark.parametrize("attribute", MAPPINGS) >+@pytest.mark.parametrize("sync_mode", ["write", "sync"]) >+@pytest.mark.skipif(not connector_running_on_this_host(), >+ reason="Univention S4 Connector not configured.") >+def test_attribute_sync_from_udm_to_s4(attribute, sync_mode): >+ (ucs_attribute, con_attribute, con_other_attribute) = attribute >+ udm_user = NormalUser(selection=("username", "lastname", ucs_attribute)) >+ primary_value = udm_user.basic.get(ucs_attribute) >+ all_values = (primary_value, random_number(), random_number()) >+ secondary_values = all_values[1:] >+ >+ with connector_setup(sync_mode) as s4, UCSTestUDM() as udm: >+ # A single `phone` number must be synced to `telephoneNumber` in AD. >+ (udm_user_dn, s4_user_dn) = create_udm_user(udm, s4, udm_user, s4connector.wait_for_sync) >+ >+ # Additional `phone` values must be synced to `otherTelephone`, >+ # `telephoneNumber` must keep its value. >+ print("\nModifying UDM user: {}={}\n".format(ucs_attribute, all_values)) >+ udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: all_values}) >+ s4connector.wait_for_sync() >+ s4.verify_object(s4_user_dn, >+ {con_attribute: primary_value, con_other_attribute: secondary_values}) >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: all_values}) >+ >+ # If we delete the first `phone` value via UDM, we want to duplicate >+ # the first value of `otherTelephone` into `telephoneNumber`. >+ (new_primary, next_primary) = secondary_values >+ print("\nModifying UDM user: {}={}\n".format(ucs_attribute, secondary_values)) >+ udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: secondary_values}) >+ s4connector.wait_for_sync() >+ s4.verify_object(s4_user_dn, >+ {con_attribute: new_primary, con_other_attribute: secondary_values}) >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: secondary_values}) >+ >+ # If we delete a `phone` value via UDM that is duplicated in AD, we want >+ # it to be deleted from `telephoneNumber` and `otherTelephone`. >+ print("\nModifying UDM user: {}={}\n".format(ucs_attribute, next_primary)) >+ udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: next_primary}) >+ s4connector.wait_for_sync() >+ s4.verify_object(s4_user_dn, >+ {con_attribute: next_primary, con_other_attribute: next_primary}) >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: next_primary}) >+ >+ # Setting a completely new `phone` value via UDM, this must be synced >+ # to `telephoneNumber` and `otherTelephone` must be empty. >+ new_phone_who_dis = random_number() >+ print("\nModifying UDM user: {}={}\n".format(ucs_attribute, new_phone_who_dis)) >+ udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: new_phone_who_dis}) >+ s4connector.wait_for_sync() >+ s4.verify_object(s4_user_dn, {con_attribute: new_phone_who_dis, con_other_attribute: []}) >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_phone_who_dis}) >+ >+ # No `phone` value via UDM, must result in an empty `telephoneNumber` >+ # and `otherTelephone`. >+ print("\nModifying UDM user: {}={}\n".format(ucs_attribute, [])) >+ udm.modify_object('users/user', dn=udm_user_dn, set={ucs_attribute: ''}) >+ s4connector.wait_for_sync() >+ s4.verify_object(s4_user_dn, {con_attribute: [], con_other_attribute: []}) >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: []}) >+ >+ delete_udm_user(udm, s4, udm_user_dn, s4_user_dn, s4connector.wait_for_sync) >+ >+ >+@pytest.mark.parametrize("attribute", MAPPINGS) >+@pytest.mark.parametrize("sync_mode", ["read", "sync"]) >+@pytest.mark.skipif(not connector_running_on_this_host(), >+ reason="Univention S4 Connector not configured.") >+def test_attribute_sync_from_s4_to_udm(attribute, sync_mode): >+ (ucs_attribute, con_attribute, con_other_attribute) = attribute >+ udm_user = NormalUser(selection=("username", "lastname", ucs_attribute)) >+ primary_value = udm_user.basic.get(ucs_attribute) >+ all_values = (primary_value, random_number(), random_number()) >+ secondary_values = all_values[1:] >+ >+ with connector_setup(sync_mode) as s4: >+ # A single `telephoneNumber` must be synced to `phone` in UDM. >+ (basic_s4_user, s4_user_dn, udm_user_dn) = create_con_user(s4, >+ udm_user, s4connector.wait_for_sync) >+ >+ # Additional values in `otherTelephone` must be appended to `phone`. >+ print("\nModifying S4 user: {}={}, {}={}\n".format(con_attribute, >+ primary_value, con_other_attribute, secondary_values)) >+ s4.set_attributes(s4_user_dn, >+ **{con_attribute: primary_value, con_other_attribute: secondary_values}) >+ s4connector.wait_for_sync() >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: all_values}) >+ s4.verify_object(s4_user_dn, >+ {con_attribute: primary_value, con_other_attribute: secondary_values}) >+ >+ if sync_mode == "sync": # otherwise the connector can't write into AD >+ # If we delete the value of `telephoneNumber` from AD, we expect to get >+ # the first value of `otherTelephone` duplicated into >+ # `telephoneNumber`. >+ (new_primary, _) = secondary_values >+ print("\nModifying S4 user: {}={}\n".format(con_attribute, [])) >+ s4.set_attributes(s4_user_dn, **{con_attribute: []}) >+ s4connector.wait_for_sync() >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: secondary_values}) >+ s4.verify_object(s4_user_dn, >+ {con_attribute: new_primary, con_other_attribute: secondary_values}) >+ >+ # Deleting the duplicate from `otherTelephone` must retain the value of >+ # `telephoneNumber` and `phone` in UDM. >+ print("\nModifying S4 user: {}={}\n".format(con_other_attribute, [])) >+ s4.set_attributes(s4_user_dn, **{con_other_attribute: []}) >+ s4connector.wait_for_sync() >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_primary}) >+ s4.verify_object(s4_user_dn, >+ {con_attribute: new_primary, con_other_attribute: []}) >+ >+ # Setting a new `telephoneNumber` and no `otherTelephone` in AD must >+ # result in a single new value in `phone`. >+ new_phone_who_dis = random_number() >+ print("\nModifying S4 user: {}={}\n".format(con_attribute, new_phone_who_dis)) >+ s4.set_attributes(s4_user_dn, **{con_attribute: new_phone_who_dis, con_other_attribute: []}) >+ s4connector.wait_for_sync() >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: new_phone_who_dis}) >+ s4.verify_object(s4_user_dn, >+ {con_attribute: new_phone_who_dis, con_other_attribute: []}) >+ >+ # Setting no `telephoneNumber` and no `otherTelephone` in AD must >+ # result in no value in `phone`. >+ print("\nModifying S4 user: {}={}\n".format(con_attribute, [])) >+ s4.set_attributes(s4_user_dn, **{con_attribute: [], con_other_attribute: []}) >+ s4connector.wait_for_sync() >+ tcommon.verify_udm_object("users/user", udm_user_dn, {ucs_attribute: []}) >+ s4.verify_object(s4_user_dn, {con_attribute: [], con_other_attribute: []}) >+ >+ delete_con_user(s4, s4_user_dn, udm_user_dn, s4connector.wait_for_sync) >-- >2.7.4 >
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
Attachments on
bug 45252
:
9140
|
9176