Possible bug in s4-connector [BUG Report]

Hello.
Yesterday we found a bug in s4-connector when synchronizing AD > UCS. The error is related to incorrect determination of the need to update the uniqueMember group attribute. In the log /var/log/univention/connector-s4.log there are Tracebacks of the form:

12.11.2023 00:00:07.280 LDAP        (PROCESS): Building internal group membership cache
12.11.2023 00:00:07.315 LDAP        (PROCESS): Internal group membership cache was created
12.11.2023 00:00:07.859 LDAP        (PROCESS): sync AD > UCS: Resync rejected dn: 'CN=test,OU=ТПшки,OU=<masked>,OU=Users,DC=<masked>'
12.11.2023 00:00:07.872 LDAP        (PROCESS): sync AD > UCS: [          user] [    modify] 'uid=test,ou=тпшки,ou=<masked>,ou=users,dc=<masked>'
12.11.2023 00:00:07.944 LDAP        (ERROR  ): failed in post_con_modify_functions
12.11.2023 00:00:07.948 LDAP        (ERROR  ): Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/univention/s4connector/__init__.py", line 1503, in sync_to_ucs
    post_ucs_modify_function(self, property_type, object)
  File "/usr/lib/python3/dist-packages/univention/s4connector/s4/__init__.py", line 86, in object_memberships_sync_to_ucs
    return connector.object_memberships_sync_to_ucs(key, object)
  File "/usr/lib/python3/dist-packages/univention/s4connector/s4/__init__.py", line 1402, in object_memberships_sync_to_ucs
    self.one_group_member_sync_to_ucs(ucs_group_object, object)
  File "/usr/lib/python3/dist-packages/univention/s4connector/s4/__init__.py", line 1435, in one_group_member_sync_to_ucs
    self.lo.lo.modify_s(ucs_group_object['dn'], ml)
  File "/usr/lib/python3/dist-packages/univention/uldap.py", line 212, in _decorated
    return func(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/univention/uldap.py", line 801, in modify_s
    self.lo.modify_ext_s(dn, ml)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 1253, in modify_ext_s
    return self._apply_method_s(SimpleLDAPObject.modify_ext_s,*args,**kwargs)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 1197, in _apply_method_s
    return func(self,*args,**kwargs)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 602, in modify_ext_s
    resp_type, resp_data, resp_msgid, resp_ctrls = self.result3(msgid,all=1,timeout=self.timeout)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 749, in result3
    resp_ctrl_classes=resp_ctrl_classes
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 756, in result4
    ldap_result = self._ldap_call(self._l.result4,msgid,all,timeout,add_ctrls,add_intermediates,add_extop)
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 329, in _ldap_call
    reraise(exc_type, exc_value, exc_traceback)
  File "/usr/lib/python3/dist-packages/ldap/compat.py", line 44, in reraise
    raise exc_value
  File "/usr/lib/python3/dist-packages/ldap/ldapobject.py", line 313, in _ldap_call
    result = func(*args,**kwargs)
ldap.TYPE_OR_VALUE_EXISTS: {'desc': 'Type or value exists', 'info': 'modify/add: uniqueMember: value #0 already exists'}

(We use Cyrillic characters in ou names )
The error is related to passing incorrect arguments to the _compare_lowercase function in the /usr/lib/python3/dist-packages/univention/s4connector/s4/__init__.py module (line 1416)
An incorrect function call occurs on line 1425:

self.__compare_lowercase(object['dn'].encode('UTF-8'), ucs_group_object['attributes'].get('uniqueMember', [])):

ucs_group_object['attributes'].get('uniqueMember', []) contains a string unprocessed by the lower() method but encoded with encode('UTF-8'), while the string object['dn'] is processed by the lower() method.
As a result, __compare_lowercase() always returns False at this location, and the LDAP module returns a TYPE_OR_VALUE_EXISTS exception when trying to add the uniqueMember attribute.
For an example, try to find the difference between the strings:

b'uid=<masked>,ou=\xd0\xbe\xd1\x82\xd0\xb4\xd0\xb5\xd0\xbb \xd1\x81\xd0\xb8\xd1\x81\xd1\x82\xd0\xb5\xd0\xbc\xd0\xbd\xd1\x8b\xd1\x85 \xd0\xb0\xd0\xb4\xd0\xbc\xd0\xb8\xd0\xbd\xd0\xb8\xd1\x81\xd1\x82\xd1\x80\xd0\x
b0\xd1\x82\xd0\xbe\xd1\x80\xd0\xbe\xd0\xb2,ou=<masked>,ou=users,dc=<masked>,dc=<masked>,dc=<masked>'

b'uid=<masked>,ou=\xd0\x9e\xd1\x82\xd0\xb4\xd0\xb5\xd0\xbb \xd1\x81\xd0\xb8\xd1\x81\xd1\x82\xd0\xb5\xd0\xbc\xd0\xbd\xd1\x8b\xd1\x85 \xd0\xb0\xd0\xb4\xd0\xbc\xd0\xb8\xd0\xbd\xd0\xb8\xd1\x81\xd1\x82\xd1\x80\xd0\xb0\xd1
\x82\xd0\xbe\xd1\x80\xd0\xbe\xd0\xb2,ou=<masked>,ou=users,dc=<masked>,dc=<masked>,dc=<masked>'

As a quick (and dirty) hotfix, we fixed the __compare_lowercase() comparison operation as follows:

return any(dn.lower() == d.decode('UTF-8').lower().encode('UTF-8') for d in dn_list)

This is the official MS naming requirements for AD’s.

What is not being considered is the other infrastructure … like DNS servers.

Just becasue an AD or web server takes a particular character set, does NOT mean , that thinks like reverse DNS/ & DNS names are valid, but they are buried so deep in infrastructure that most upper levels rarely if ever check.

also something that many overlook…, it length requirements…
MS talks about characters, not multi-byte characters…

Mastodon