#!/usr/bin/python3
#
# Univention S4 Connector
# Basic class for the AD connector part
#
# SPDX-FileCopyrightText: 2004-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only
import base64
import calendar
import copy
import os
import re
import string
import sys
import time
import urllib.parse
from logging import getLogger
import ldap
from ldap.controls import LDAPControl, SimplePagedResultsControl
from ldap.filter import escape_filter_chars
from samba.dcerpc import security
from samba.ndr import ndr_pack, ndr_unpack
import univention.s4connector
import univention.uldap
from univention.config_registry import ConfigRegistry
from univention.logging import Structured
log = Structured(getLogger("LDAP").getChild(__name__))
LDAP_SERVER_SHOW_DELETED_OID = "1.2.840.113556.1.4.417"
LDB_CONTROL_DOMAIN_SCOPE_OID = "1.2.840.113556.1.4.1339"
LDB_CONTROL_RELAX_OID = "1.3.6.1.4.1.4203.666.5.12"
LDB_CONTROL_PROVISION_OID = '1.3.6.1.4.1.7165.4.3.16'
DSDB_CONTROL_REPLICATED_UPDATE_OID = '1.3.6.1.4.1.7165.4.3.3'
# page results
PAGE_SIZE = 1000
[docs]
def group_members_sync_from_ucs(connector, key, object):
return connector.group_members_sync_from_ucs(key, object)
[docs]
def object_memberships_sync_from_ucs(connector, key, object):
return connector.object_memberships_sync_from_ucs(key, object)
[docs]
def group_members_sync_to_ucs(connector, key, object):
return connector.group_members_sync_to_ucs(key, object)
[docs]
def object_memberships_sync_to_ucs(connector, key, object):
return connector.object_memberships_sync_to_ucs(key, object)
[docs]
def primary_group_sync_from_ucs(connector, key, object):
return connector.primary_group_sync_from_ucs(key, object)
[docs]
def primary_group_sync_to_ucs(connector, key, object):
return connector.primary_group_sync_to_ucs(key, object)
[docs]
def disable_user_from_ucs(connector, key, object):
return connector.disable_user_from_ucs(key, object)
[docs]
def disable_user_to_ucs(connector, key, object):
return connector.disable_user_to_ucs(key, object)
[docs]
def add_primary_group_to_addlist(connector, property_type, object, addlist, serverctrls):
gidNumber = object.get('attributes', {}).get('gidNumber')
primary_group_sid = object.get('attributes', {}).get('sambaPrimaryGroupSID')
if gidNumber:
if isinstance(gidNumber, list):
gidNumber = gidNumber[0]
gidNumber = gidNumber.decode('UTF-8')
log.debug('add_primary_group_to_addlist: gidNumber: %s', gidNumber)
ucs_group_filter = format_escaped('(&(objectClass=univentionGroup)(gidNumber={0!e}))', gidNumber)
ucs_group_ldap = connector.search_ucs(filter=ucs_group_filter) # is empty !?
if not ucs_group_ldap:
log.warning('add_primary_group_to_addlist: Did not find UCS group with gidNumber %s', gidNumber)
return
member_key = 'group'
ad_group_object = connector._object_mapping(member_key, {'dn': ucs_group_ldap[0][0], 'attributes': ucs_group_ldap[0][1]}, 'ucs')
ldap_object_ad_group = connector.get_object(ad_group_object['dn'])
primary_group_sid = decode_sid(ldap_object_ad_group['objectSid'][0])
primary_group_rid = primary_group_sid.split('-')[-1].encode('ASCII')
# Is the primary group Domain Users (the default)?
if primary_group_rid == b'513':
return
log.debug('add_primary_group_to_addlist: Set primary group to %s (rid) for %s', primary_group_rid, object.get('dn'))
addlist.append(('primaryGroupID', [primary_group_rid]))
serverctrls.append(LDAPControl(LDB_CONTROL_RELAX_OID, criticality=0))
def __is_groupType_local(groupType):
try:
return int(groupType) & 0x1
except ValueError:
return False
[docs]
def check_for_local_group_and_extend_serverctrls_and_sid(connector, property_type, object, add_or_modlist, serverctrls):
groupType = object.get('attributes', {}).get('univentionGroupType', [None])[0]
if not groupType:
return
log.debug("groupType: %s", groupType)
if __is_groupType_local(groupType):
serverctrls.append(LDAPControl(LDB_CONTROL_RELAX_OID, criticality=0))
sambaSID = object['attributes']['sambaSID'][0].decode('ASCII')
log.debug("sambaSID: %r", sambaSID)
objectSid = ndr_pack(security.dom_sid(sambaSID))
add_or_modlist.append(('objectSid', [objectSid]))
[docs]
def fix_dn_in_search(result):
return [(fix_dn(dn), attrs) for dn, attrs in result]
[docs]
def fix_dn(dn):
# Samba LDAP returns broken DN, which cannot be parsed: ldap.dn.str2dn('cn=foo\\?,dc=base')
return dn.replace('\\?', '?') if dn is not None else dn
[docs]
def str2dn(dn):
try:
return ldap.dn.str2dn(dn)
except ldap.DECODING_ERROR:
return ldap.dn.str2dn(fix_dn(dn))
[docs]
def unix2s4_time(ltime):
d = 116444736000000000 # difference between 1601 and 1970
return int(calendar.timegm(time.strptime(ltime, "%Y-%m-%d"))) * 10000000 + d # AD stores end of day in accountExpires
[docs]
def s42unix_time(ltime):
d = 116444736000000000 # difference between 1601 and 1970
return time.strftime("%Y-%m-%d", time.gmtime((ltime - d) / 10000000)) # shadowExpire treats day of expiry as exclusive
[docs]
def samba2s4_time(ltime):
d = 116444736000000000 # difference between 1601 and 1970
return int(time.mktime(time.localtime(ltime))) * 10000000 + d
[docs]
def s42samba_time(ltime):
if ltime == 0:
return ltime
d = 116444736000000000 # difference between 1601 and 1970
return int((ltime - d) / 10000000)
# mapping functions
[docs]
def samaccountname_dn_mapping(connector, given_object, dn_mapping_stored, ucsobject, propertyname, propertyattrib, ocucs, ucsattrib, ocad, dn_attr=None):
"""
map dn of given object (which must have an samaccountname in S4)
ocucs and ocad are objectclasses in UCS and S4
"""
object = copy.deepcopy(given_object)
samaccountname = ''
dn_attr_val = ''
if object['dn'] is not None:
if 'sAMAccountName' in object['attributes']:
samaccountname = object['attributes']['sAMAccountName'][0].decode('UTF-8')
if dn_attr:
try:
dn_attr_vals = [value for key, value in object['attributes'].items() if dn_attr.lower() == key.lower()][0] # noqa: RUF015
except IndexError:
pass
else:
dn_attr_val = dn_attr_vals[0].decode('UTF-8')
def dn_premapped(object, dn_key, dn_mapping_stored):
if (dn_key not in dn_mapping_stored) or (not object[dn_key]):
log.trace("samaccount_dn_mapping: not premapped (in first instance)")
return False
if object.get('modtype') == 'delete':
# In case the object was deleted, the mapping premapped DN should be used.
# But in case the sAMAccountName has been changed we should search for
# the sAMAccountName. That's not the best solution but it works for now:
# See the following test cases:
# 125sync_recreate_user_at_different_position
# 272read_ad_change_username
t_dn = object.get('dn')
if t_dn:
(_rdn_attribute, rdn_value, _flags) = str2dn(t_dn)[0][0]
t_samaccount = ''
if object.get('attributes'):
t_samaccount = object['attributes'].get('sAMAccountName', [b''])[0].decode('UTF-8')
if rdn_value.lower() == t_samaccount.lower():
log.trace("samaccount_dn_mapping: modtype is delete, use the premapped DN: %s", object[dn_key])
return True
if ucsobject:
if connector.get_object(object[dn_key]) is not None:
log.trace("samaccount_dn_mapping: premapped AD object found")
return True
else:
log.trace("samaccount_dn_mapping: premapped AD object not found")
return False
else:
if connector.get_ucs_ldap_object(object[dn_key]) is not None:
log.trace("samaccount_dn_mapping: premapped UCS object found")
return True
else:
log.trace("samaccount_dn_mapping: premapped UCS object not found")
return False
for dn_key in ['dn', 'olddn']:
log.trace("samaccount_dn_mapping: check newdn for key %s: %s", dn_key, object.get(dn_key))
if dn_key in object and not dn_premapped(object, dn_key, dn_mapping_stored):
dn = object[dn_key]
# Skip Configuration objects with empty DNs
if dn is None:
break
exploded_dn = str2dn(dn)
(_fst_rdn_attribute_utf8, fst_rdn_value_utf8, _flags) = exploded_dn[0][0]
if ucsobject and object.get('attributes') and object['attributes'].get(ucsattrib):
value = object['attributes'][ucsattrib][0].decode('UTF-8')
else:
value = fst_rdn_value_utf8
if ucsobject:
# lookup the cn as sAMAccountName in AD to get corresponding DN, if not found create new
log.trace("samaccount_dn_mapping: got an UCS-Object")
filter_parts_ad = [format_escaped('(objectclass={0!e})', ocad)]
alternative_samaccountnames = []
for ucsval, conval in connector.property[propertyname].mapping_table.get(propertyattrib, []):
if value.lower() == ucsval.lower():
if ucsval == "Printer-Admins": # Also look for the original name (Bug #42675#c1)
alternative_samaccountnames.append(ucsval)
value = conval
log.trace("samaccount_dn_mapping: map %s according to mapping-table", propertyattrib)
break
else:
if propertyattrib in connector.property[propertyname].mapping_table:
log.trace("samaccount_dn_mapping: %s not in mapping-table", propertyattrib)
if len(alternative_samaccountnames) == 0:
filter_parts_ad.append(format_escaped('(samaccountname={0!e})', value))
else:
alternative_samaccountnames.append(value)
samaccountname_filter_parts = [format_escaped('(samaccountname={0!e})', x) for x in alternative_samaccountnames]
filter_parts_ad.append('(|{})'.format(''.join(samaccountname_filter_parts)))
if dn_attr and dn_attr_val:
# also look for dn attr (needed to detect modrdn)
filter_parts_ad.append(format_escaped('({0}={1!e})', dn_attr, dn_attr_val))
filter_ad = '(&{})'.format(''.join(filter_parts_ad))
log.trace("samaccount_dn_mapping: search in ad for %s", filter_ad)
result = connector.s4_search_ext_s(connector.lo_s4.base, ldap.SCOPE_SUBTREE, filter_ad, ['sAMAccountName'])
if result and len(result) > 0 and result[0] and len(result[0]) > 0 and result[0][0]: # no referral, so we've got a valid result
if dn_key == 'olddn' or (dn_key == 'dn' and 'olddn' not in object):
newdn = result[0][0]
else:
# move
# return a kind of frankenstein DN here, sync_from_ucs replaces the UCS LDAP base
# with the AD LDAP base at a later stage, see Bug #48440
newdn = ldap.dn.dn2str([str2dn(result[0][0])[0], *exploded_dn[1:]])
else:
newdn = ldap.dn.dn2str([[('cn', fst_rdn_value_utf8, ldap.AVA_STRING)], *exploded_dn[1:]]) # new object, don't need to change
log.trace("samaccount_dn_mapping: newdn: %s", newdn)
else:
# get the object to read the sAMAccountName in AD and use it as name
# we have no fallback here, the given dn must be found in AD or we've got an error
log.trace("samaccount_dn_mapping: got an AD-Object")
i = 0
while not samaccountname: # in case of olddn this is already set
i = i + 1
search_dn = dn
if 'deleted_dn' in object:
search_dn = object['deleted_dn']
try:
samaccountname_filter = format_escaped('(objectClass={0!e})', ocad)
samaccountname_search_result = connector.s4_search_ext_s(search_dn, ldap.SCOPE_BASE, samaccountname_filter, ['sAMAccountName'])
samaccountname = samaccountname_search_result[0][1]['sAMAccountName'][0].decode('UTF-8')
log.trace("samaccount_dn_mapping: got samaccountname from AD")
except ldap.NO_SUCH_OBJECT: # AD may need time
if i > 5:
raise
time.sleep(1) # AD may need some time...
for ucsval, conval in connector.property[propertyname].mapping_table.get(propertyattrib, []):
if samaccountname.lower() == conval.lower():
samaccountname = ucsval
log.trace("samaccount_dn_mapping: map samaccountanme according to mapping-table")
break
else:
if propertyattrib in connector.property[propertyname].mapping_table:
log.trace("samaccount_dn_mapping: samaccountname not in mapping-table")
# search for object with this dn in ucs, needed if it lies in a different container
ucsdn = ''
log.trace("samaccount_dn_mapping: samaccountname is: %r", samaccountname)
ucsdn_filter = format_escaped('(&(objectclass={0!e})({1}={2!e}))', ocucs, ucsattrib, samaccountname)
ucsdn_result = connector.search_ucs(filter=ucsdn_filter, base=connector.lo.base, scope='sub', attr=['objectClass'])
if ucsdn_result and len(ucsdn_result) > 0 and ucsdn_result[0] and len(ucsdn_result[0]) > 0:
ucsdn = ucsdn_result[0][0]
if ucsdn and (dn_key == 'olddn' or (dn_key == 'dn' and 'olddn' not in object)):
newdn = ucsdn
log.trace("samaccount_dn_mapping: newdn is ucsdn")
else:
if dn_attr:
newdn_rdn = [(dn_attr, dn_attr_val, ldap.AVA_STRING)]
else:
newdn_rdn = [(ucsattrib, samaccountname, ldap.AVA_STRING)]
newdn = ldap.dn.dn2str([newdn_rdn, *exploded_dn[1:]]) # guess the old dn
log.debug("samaccount_dn_mapping: newdn for key %r: olddn=%r newdn=%r", dn_key, dn, newdn)
object[dn_key] = newdn
return object
[docs]
def user_dn_mapping(connector, given_object, dn_mapping_stored, isUCSobject):
"""
map dn of given user using the samaccountname/uid
connector is an instance of univention.s4connector.s4, given_object an object-dict,
dn_mapping_stored a list of dn-types which are already mapped because they were stored in the config-file
"""
return samaccountname_dn_mapping(connector, given_object, dn_mapping_stored, isUCSobject, 'user', 'samAccountName', 'posixAccount', 'uid', 'user')
[docs]
def group_dn_mapping(connector, given_object, dn_mapping_stored, isUCSobject):
"""
map dn of given group using the samaccountname/cn
connector is an instance of univention.s4connector.s4, given_object an object-dict,
dn_mapping_stored a list of dn-types which are already mapped because they were stored in the config-file
"""
return samaccountname_dn_mapping(connector, given_object, dn_mapping_stored, isUCSobject, 'group', 'cn', 'posixGroup', 'cn', 'group')
[docs]
def windowscomputer_dn_mapping(connector, given_object, dn_mapping_stored, isUCSobject):
"""
map dn of given windows computer using the samaccountname/uid
s4connector is an instance of univention.s4connector.s4, given_object an object-dict,
dn_mapping_stored a list of dn-types which are already mapped because they were stored in the config-file
"""
return samaccountname_dn_mapping(connector, given_object, dn_mapping_stored, isUCSobject, 'windowscomputer', 'samAccountName', 'posixAccount', 'uid', 'computer', 'cn')
[docs]
def dc_dn_mapping(s4connector, given_object, dn_mapping_stored, isUCSobject):
"""
map dn of given dc computer using the samaccountname/uid
s4connector is an instance of univention.s4connector.s4, given_object an object-dict,
dn_mapping_stored a list of dn-types which are already mapped because they were stored in the config-file
"""
return samaccountname_dn_mapping(s4connector, given_object, dn_mapping_stored, isUCSobject, 'dc', 'samAccountName', 'posixAccount', 'uid', 'computer', 'cn')
[docs]
def decode_sid(value):
return str(ndr_unpack(security.dom_sid, value))
def __is_sid_string(sid):
return sid.startswith(b'S-')
def __is_int(value):
try:
int(value)
return True
except (ValueError, TypeError):
return False
[docs]
def compare_sid_lists(sid_list1, sid_list2):
"""
Compare the SID / RID attributes. Depending on the sync direction and
SID sync configuration the function gets two SID lists or two RID values.
"""
# RID comparison
if __is_int(sid_list1) or __is_int(sid_list2):
return sid_list1 == sid_list2
# SID comparison
len_sid_list1 = len(sid_list1)
if len_sid_list1 != len(sid_list2):
return False
for i in range(len_sid_list1):
sid1 = sid_list1[i]
if not __is_sid_string(sid1):
sid1 = decode_sid(sid1)
sid2 = sid_list2[i]
if not __is_sid_string(sid2):
sid2 = decode_sid(sid2)
if sid1 != sid2:
return False
return True
[docs]
class s4(univention.s4connector.ucs):
RANGE_RETRIEVAL_PATTERN = re.compile(r"^([^;]+);range=(\d+)-(\d+|\*)$")
[docs]
@classmethod
def main(cls, ucr=None, configbasename='connector', **kwargs):
if ucr is None:
ucr = ConfigRegistry()
ucr.load()
import univention.s4connector.s4.mapping
MAPPING_FILENAME = f'/etc/univention/{configbasename}/s4/localmapping.py'
s4_mapping = univention.s4connector.s4.mapping.load_localmapping(MAPPING_FILENAME)
_ucr = dict(ucr)
try:
ad_ldap_host = _ucr[f'{configbasename}/s4/ldap/host']
ad_ldap_port = _ucr[f'{configbasename}/s4/ldap/port']
ad_ldap_base = _ucr[f'{configbasename}/s4/ldap/base']
ad_ldap_binddn = _ucr.get(f'{configbasename}/s4/ldap/binddn')
ad_ldap_certificate = _ucr.get(f'{configbasename}/s4/ldap/certificate')
if not ad_ldap_certificate and ucr.is_true(f'{configbasename}/s4/ldap/ssl'):
raise KeyError(f'{configbasename}/s4/ldap/certificate')
listener_dir = _ucr[f'{configbasename}/s4/listener/dir']
except KeyError as exc:
raise SystemExit(f'UCR variable {exc} is not set')
ad_ldap_bindpw = None
if ucr.get(f'{configbasename}/s4/ldap/bindpw') and os.path.exists(ucr[f'{configbasename}/s4/ldap/bindpw']):
with open(ucr[f'{configbasename}/s4/ldap/bindpw']) as fd:
ad_ldap_bindpw = fd.read().rstrip()
return cls(
configbasename,
s4_mapping,
ucr,
ad_ldap_host,
ad_ldap_port,
ad_ldap_base,
ad_ldap_binddn,
ad_ldap_bindpw,
ad_ldap_certificate,
listener_dir,
**kwargs,
)
def __init__(
self,
CONFIGBASENAME,
property,
configRegistry,
s4_ldap_host,
s4_ldap_port,
s4_ldap_base,
s4_ldap_binddn,
s4_ldap_bindpw,
s4_ldap_certificate,
listener_dir,
logfilename=None,
debug_level=None,
):
univention.s4connector.ucs.__init__(self, CONFIGBASENAME, property, configRegistry, listener_dir, logfilename, debug_level)
self.s4_ldap_host = s4_ldap_host
self.s4_ldap_port = s4_ldap_port
self.s4_ldap_base = s4_ldap_base
self.s4_ldap_binddn = s4_ldap_binddn
self.s4_ldap_bindpw = s4_ldap_bindpw
self.s4_ldap_certificate = s4_ldap_certificate
if not self.config.has_section('S4'):
log.debug("__init__: init add config section 'S4'")
self.config.add_section('S4')
if not self.config.has_section('S4 rejected'):
log.debug("__init__: init add config section 'S4 rejected'")
self.config.add_section('S4 rejected')
if not self.config.has_option('S4', 'lastUSN'):
log.debug("__init__: init lastUSN with 0")
self._set_config_option('S4', 'lastUSN', '0')
self.__lastUSN = 0
else:
self.__lastUSN = int(self._get_config_option('S4', 'lastUSN'))
if not self.config.has_section('S4 GUID'):
log.debug("__init__: init add config section 'S4 GUID'")
self.config.add_section('S4 GUID')
self.serverctrls_for_add_and_modify = []
if 'univention_samaccountname_ldap_check' in self.configRegistry.get('samba4/ldb/sam/module/prepend', '').split():
# The S4 connector must bypass this LDB module if it is activated via samba4/ldb/sam/module/prepend
# The OID of the 'bypass_samaccountname_ldap_check' control is defined in ldb.h
ldb_ctrl_bypass_samaccountname_ldap_check = LDAPControl('1.3.6.1.4.1.10176.1004.0.4.1', criticality=0)
self.serverctrls_for_add_and_modify.append(ldb_ctrl_bypass_samaccountname_ldap_check)
# objectSid modification for an Samba4 object is only possible with the "provision" control:
if self.configRegistry.is_true('connector/s4/mapping/sid_to_s4', False):
self.serverctrls_for_add_and_modify.append(LDAPControl(LDB_CONTROL_PROVISION_OID, criticality=0))
self.serverctrls_for_add_and_modify.append(LDAPControl(DSDB_CONTROL_REPLICATED_UPDATE_OID, criticality=0))
# wish list, but AD does not support: ldap.UNAVAILABLE_CRITICAL_EXTENSION: {'desc': 'Critical extension is unavailable'}
# from ldap.controls.readentry import PostReadControl
# self.serverctrls_for_add_and_modify.append(PostReadControl(True, ['objectGUID']))
# Save a list of objects just created, this is needed to
# prevent the back sync of a password if it was changed just
# after the creation
self.creation_list = []
# Build an internal cache with AD as key and the UCS object as cache
# UCS group member DNs to AD group member DN
# * entry used and updated while reading in group_members_sync_from_ucs
# * entry flushed during delete+move at in sync_to_ucs and sync_from_ucs
self.group_member_mapping_cache_ucs = {}
# AD group member DNs to UCS group member DN
# * entry used and updated while reading in group_members_sync_to_ucs
# * entry flushed during delete+move at in sync_to_ucs and sync_from_ucs
self.group_member_mapping_cache_con = {}
# Save the old members of a group
# The connector is object based, at least in the direction AD/AD to LDAP, because we don't
# have a local cache. group_members_cache_ucs and group_members_cache_con help to
# determine if the group membership was already saved. For example, one group and
# five users are created on UCS side. After two users have been synced to AD/S4,
# the group is snyced. But in AD/S4 only existing members can be stored in the group.
# Now the sync goes back from AD/S4 to LDAP and we should not remove the three users
# from the group. For this we remove only members who are in the local cache.
# UCS groups and UCS members
# * initialized during start
# * entry updated in group_members_sync_from_ucs and object_memberships_sync_from_ucs
# * entry flushed for group object in sync_to_ucs / add_in_ucs
# * entry used for decision in group_members_sync_to_ucs
self.group_members_cache_ucs = {}
# AD groups and AD members
# * initialized during start
# * entry updated in group_members_sync_to_ucs and object_memberships_sync_to_ucs
# * entry flushed for group object in sync_from_ucs / ADD
# * entry used for decision in group_members_sync_from_ucs
self.group_members_cache_con = {}
[docs]
def init_ldap_connections(self):
super().init_ldap_connections()
self.open_s4()
self.s4_sid = decode_sid(self.s4_search_ext_s(self.s4_ldap_base, ldap.SCOPE_BASE, 'objectclass=domain', ['objectSid'])[0][1]['objectSid'][0])
for prop in self.property.values():
prop.con_default_dn = self.dn_mapped_to_base(prop.con_default_dn, self.lo_s4.base)
[docs]
def init_group_cache(self):
log.process('Building internal group membership cache')
s4_groups = self.__search_s4(filter='objectClass=group', attrlist=['member'])
log.trace("__init__: s4_groups: %s", s4_groups)
for s4_group in s4_groups:
if not s4_group or not s4_group[0]:
continue
s4_group_dn, s4_group_attrs = s4_group
self.group_members_cache_con[s4_group_dn.lower()] = set()
if s4_group_attrs:
s4_members = self.get_s4_members(s4_group_dn, s4_group_attrs)
member_cache = self.group_members_cache_con[s4_group_dn.lower()]
member_cache.update(m.lower() for m in s4_members)
log.trace("__init__: self.group_members_cache_con: %s", self.group_members_cache_con)
for ucs_group in self.search_ucs(filter='objectClass=univentionGroup', attr=['uniqueMember']):
group_lower = ucs_group[0].lower()
self.group_members_cache_ucs[group_lower] = set()
if ucs_group[1]:
for member in ucs_group[1].get('uniqueMember'):
self.group_members_cache_ucs[group_lower].add(member.decode('UTF-8').lower())
log.trace("__init__: self.group_members_cache_ucs: %s", self.group_members_cache_ucs)
log.process('Internal group membership cache was created')
[docs]
def s4_search_ext_s(self, *args, **kwargs):
return fix_dn_in_search(self.lo_s4.lo.search_ext_s(*args, **kwargs))
[docs]
def open_s4(self):
tls_mode = 2
if f'{self.CONFIGBASENAME}/s4/ldap/ssl' in self.configRegistry and self.configRegistry[f'{self.CONFIGBASENAME}/s4/ldap/ssl'] == "no":
log.debug('__init__: The LDAP connection to S4 does not use SSL (switched off by UCR "%s/s4/ldap/ssl").', self.CONFIGBASENAME)
tls_mode = 0
protocol = self.configRegistry.get(f'{self.CONFIGBASENAME}/s4/ldap/protocol', 'ldap').lower()
if protocol == 'ldapi':
socket = urllib.parse.quote(self.configRegistry.get(f'{self.CONFIGBASENAME}/s4/ldap/socket', ''), '')
ldapuri = f"{protocol}://{socket}"
else:
ldapuri = "%s://%s:%d" % (protocol, self.configRegistry[f'{self.CONFIGBASENAME}/s4/ldap/host'], int(self.configRegistry[f'{self.CONFIGBASENAME}/s4/ldap/port']))
# Determine s4_ldap_base with exact case
try:
self.lo_s4 = univention.uldap.access(
host=self.s4_ldap_host,
port=int(self.s4_ldap_port),
base=self.s4_ldap_base or 'DC=unknown',
binddn=None,
bindpw=None,
start_tls=tls_mode,
ca_certfile=self.s4_ldap_certificate,
uri=ldapuri,
reconnect=False,
)
self.lo_s4.base = ''
self.s4_ldap_base = self.s4_search_ext_s('', ldap.SCOPE_BASE, 'objectclass=*', ['defaultNamingContext'])[0][1]['defaultNamingContext'][0].decode('UTF-8')
except Exception: # FIXME: which exception is to be caught
log.exception('Failed to lookup AD LDAP base, using UCR value.')
self.lo_s4 = univention.uldap.access(
host=self.s4_ldap_host,
port=int(self.s4_ldap_port),
base=self.s4_ldap_base,
binddn=self.s4_ldap_binddn,
bindpw=self.s4_ldap_bindpw,
start_tls=tls_mode,
ca_certfile=self.s4_ldap_certificate,
uri=ldapuri,
reconnect=False,
)
self.lo_s4.lo.set_option(ldap.OPT_REFERRALS, 0)
if self.configRegistry.get('connector/s4/mapping/dns/position') == 'legacy':
self.s4_ldap_partitions = (self.s4_ldap_base,)
else:
self.s4_ldap_partitions = (self.s4_ldap_base, f"DC=DomainDnsZones,{self.s4_ldap_base}", f"DC=ForestDnsZones,{self.s4_ldap_base}")
def _get_lastUSN(self):
return max(self.__lastUSN, int(self._get_config_option('S4', 'lastUSN')))
[docs]
def get_lastUSN(self):
return self._get_lastUSN()
def _commit_lastUSN(self):
self._set_config_option('S4', 'lastUSN', str(self.__lastUSN))
def _set_lastUSN(self, lastUSN):
log.debug("_set_lastUSN: new lastUSN is: %s", lastUSN)
self.__lastUSN = lastUSN
def __encode_GUID(self, GUID):
return base64.b64encode(GUID).decode('ASCII')
def _get_DN_for_GUID(self, GUID):
return self._get_config_option('S4 GUID', self.__encode_GUID(GUID))
def _set_DN_for_GUID(self, GUID, DN):
self._set_config_option('S4 GUID', self.__encode_GUID(GUID), DN)
def _remove_GUID(self, GUID):
self._remove_config_option('S4 GUID', self.__encode_GUID(GUID))
# handle rejected Objects
def _save_rejected(self, id, dn):
self._set_config_option('S4 rejected', str(id), dn)
def _get_rejected(self, id):
return self._get_config_option('S4 rejected', str(id))
def _remove_rejected(self, id):
self._remove_config_option('S4 rejected', str(id))
def _list_rejected(self):
"""Returns rejected AD-objects"""
return self._get_config_items('S4 rejected')[:]
[docs]
def list_rejected(self):
return self._list_rejected()
[docs]
def save_rejected(self, object):
"""save object as rejected"""
self._save_rejected(self.__get_change_usn(object), object['dn'])
[docs]
def remove_rejected(self, object):
"""remove object from rejected"""
self._remove_rejected(self.__get_change_usn(object), object['dn'])
[docs]
def addToCreationList(self, dn):
if dn.lower() not in self.creation_list:
self.creation_list.append(dn.lower())
[docs]
def removeFromCreationList(self, dn):
self.creation_list = [s for s in self.creation_list if s != dn.lower()]
[docs]
def isInCreationList(self, dn):
return dn.lower() in self.creation_list
[docs]
def get_object_dn(self, dn):
for i in [0, 1]: # do it twice if the LDAP connection was closed
try:
dn, _ad_object = self.s4_search_ext_s(dn, ldap.SCOPE_BASE, '(objectClass=*)', ('dn',))[0]
log.debug("get_object: got object: %r", dn)
return dn
except (IndexError, ldap.NO_SUCH_OBJECT):
return
except (ldap.SERVER_DOWN, SystemExit):
if i == 0:
self.open_s4()
continue
raise
except Exception: # FIXME: which exception is to be caught?
log.exception('Could not get object DN') # TODO: remove except block
[docs]
def parse_range_retrieval_attrs(self, ad_attrs, attr):
for k in ad_attrs:
m = self.RANGE_RETRIEVAL_PATTERN.match(k)
if not m or m.group(1) != attr:
continue
key = k
values = ad_attrs[key]
lower = int(m.group(2))
upper = m.group(3)
if upper != "*":
upper = int(upper)
break
else:
key = None
values = []
lower = 0
upper = "*"
return (key, values, lower, upper)
[docs]
def value_range_retrieval(self, ad_dn, ad_attrs, attr):
(key, values, lower, upper) = self.parse_range_retrieval_attrs(ad_attrs, attr)
log.debug("value_range_retrieval: response: %s", key)
if lower != 0:
log.error("value_range_retrieval: invalid range retrieval response: %s", key)
raise ldap.PROTOCOL_ERROR
all_values = values
while upper != "*":
next_key = "%s;range=%d-*" % (attr, upper + 1)
ad_attrs = self.get_object(ad_dn, [next_key])
returned_before = upper
(key, values, lower, upper) = self.parse_range_retrieval_attrs(ad_attrs, attr)
if lower != returned_before + 1:
log.error("value_range_retrieval: invalid range retrieval response: asked for %s but got %s", next_key, key)
raise ldap.PARTIAL_RESULTS
log.debug("value_range_retrieval: response: %s", key)
all_values.extend(values)
return all_values
[docs]
def get_s4_members(self, ad_dn, ad_attrs):
ad_members = ad_attrs.get('member', [])
if not ad_members:
ad_members = self.value_range_retrieval(ad_dn, ad_attrs, 'member')
ad_attrs['member'] = ad_members
return [x.decode('UTF-8') for x in ad_members]
[docs]
def get_object(self, dn, attrlist=None):
"""Get an object from S4-LDAP"""
for i in [0, 1]: # do it twice if the LDAP connection was closed
try:
dn, ad_object = self.s4_search_ext_s(dn, ldap.SCOPE_BASE, '(objectClass=*)', attrlist=attrlist)[0]
log.debug("get_object: got object: %r", dn)
return ad_object
except (IndexError, ldap.NO_SUCH_OBJECT):
return
except (ldap.SERVER_DOWN, SystemExit):
if i == 0:
self.open_s4()
continue
raise
except Exception: # FIXME: which exception is to be caught?
log.exception('Could not get object') # TODO: remove except block?
def __get_change_usn(self, ad_object):
"""get change USN as max(uSNCreated, uSNChanged)"""
if not ad_object:
return 0
usncreated = int(ad_object['attributes'].get('uSNCreated', [b'0'])[0])
usnchanged = int(ad_object['attributes'].get('uSNChanged', [b'0'])[0])
return max(usnchanged, usncreated)
def __search_ad_partitions(self, scope=ldap.SCOPE_SUBTREE, filter='', attrlist=[], show_deleted=False):
"""search s4 across all partitions listed in self.s4_ldap_partitions"""
res = []
for base in self.s4_ldap_partitions:
res += self.__search_s4(base, scope, filter, attrlist, show_deleted)
return res
def __get_s4_deleted(self, dn):
return self.__search_s4(dn, scope=ldap.SCOPE_BASE, filter='(objectClass=*)', show_deleted=True)[0]
def __search_s4(self, base=None, scope=ldap.SCOPE_SUBTREE, filter='', attrlist=[], show_deleted=False):
"""search s4"""
if not base:
base = self.lo_s4.base
ctrls = [
SimplePagedResultsControl(True, PAGE_SIZE, ''), # Must be the first
LDAPControl(LDB_CONTROL_DOMAIN_SCOPE_OID, criticality=0), # Don't show referrals
]
if show_deleted:
ctrls.append(LDAPControl(LDAP_SERVER_SHOW_DELETED_OID, criticality=1))
log.trace("Search S4 with filter: %s", filter)
msgid = self.lo_s4.lo.search_ext(base, scope, filter, attrlist, serverctrls=ctrls, timeout=-1, sizelimit=0)
res = []
pages = 0
while True:
pages += 1
_rtype, rdata, _rmsgid, serverctrls = self.lo_s4.lo.result3(msgid)
res += rdata
pctrls = [
c
for c in serverctrls
if c.controlType == SimplePagedResultsControl.controlType
]
if pctrls:
cookie = pctrls[0].cookie
if cookie:
if pages > 1:
log.debug("S4 search continues, already found %s objects", len(res))
ctrls[0].cookie = cookie
msgid = self.lo_s4.lo.search_ext(base, scope, filter, attrlist, serverctrls=ctrls, timeout=-1, sizelimit=0)
else:
break
else:
log.warning("S4 ignores PAGE_RESULTS")
break
return fix_dn_in_search(res)
def __search_ad_changes(self, show_deleted=False, filter=''):
"""search AD for changes since last update (changes greater lastUSN)"""
lastUSN = self._get_lastUSN()
# filter erweitern um "(|(uSNChanged>=lastUSN+1)(uSNCreated>=lastUSN+1))"
# +1 da suche nur nach '>=', nicht nach '>' möglich
def _ad_changes_filter(attribute, lowerUSN, higherUSN=''):
if higherUSN:
usn_filter_format = '(&({attribute}>={lower_usn!e})({attribute}<={higher_usn!e}))'
else:
usn_filter_format = '({attribute}>={lower_usn!e})'
return format_escaped(usn_filter_format, attribute=attribute, lower_usn=lowerUSN, higher_usn=higherUSN)
def search_ad_changes_by_attribute(usnFilter):
if filter != '':
usnFilter = f'(&({filter})({usnFilter}))'
return self.__search_ad_partitions(filter=usnFilter, show_deleted=show_deleted)
def sort_ad_changes(res, last_usn):
def _sortkey_ascending_usncreated(element):
return int(element[1]['uSNCreated'][0])
def _sortkey_ascending_usnchanged(element):
return int(element[1]['uSNChanged'][0])
if last_usn <= 0:
return sorted(res, key=_sortkey_ascending_usncreated)
else:
created_since_last = [x for x in res if int(x[1]['uSNCreated'][0]) > last_usn]
changed_since_last = [x for x in res if int(x[1]['uSNChanged'][0]) > last_usn and x not in created_since_last]
return sorted(created_since_last, key=_sortkey_ascending_usncreated) + sorted(changed_since_last, key=_sortkey_ascending_usnchanged)
# search for objects with uSNCreated and uSNChanged in the known range
try:
usn_filter = _ad_changes_filter('uSNCreated', lastUSN + 1)
if lastUSN > 0:
# During the init phase we have to search for created and changed objects
usn_filter = '(|{}{})'.format(_ad_changes_filter('uSNChanged', lastUSN + 1), usn_filter)
return sort_ad_changes(search_ad_changes_by_attribute(usn_filter), lastUSN)
except (ldap.SERVER_DOWN, SystemExit):
raise
except ldap.SIZELIMIT_EXCEEDED:
# The LDAP control page results was not successful. Without this control
# AD does not return more than 1000 results. We are going to split the
# search.
highestCommittedUSN = self.__get_highestCommittedUSN()
tmpUSN = lastUSN
log.process("Need to split results. highest USN is %s, lastUSN is %s", highestCommittedUSN, lastUSN)
returnObjects = []
while tmpUSN != highestCommittedUSN:
tmp_lastUSN = tmpUSN
tmpUSN += 999
if tmpUSN > highestCommittedUSN:
tmpUSN = highestCommittedUSN
log.debug("__search_ad_changes: search between USNs %s and %s", tmp_lastUSN + 1, tmpUSN)
usn_filter = _ad_changes_filter('uSNCreated', tmp_lastUSN + 1, tmpUSN)
if tmp_lastUSN > 0:
# During the init phase we have to search for created and changed objects
usn_filter = '(|{}{})'.format(_ad_changes_filter('uSNChanged', tmp_lastUSN + 1, tmpUSN), usn_filter)
returnObjects += search_ad_changes_by_attribute(usn_filter)
return sort_ad_changes(returnObjects, lastUSN)
def __search_ad_changeUSN(self, changeUSN, show_deleted=True, filter=''):
"""search ad for change with id"""
usn_filter = format_escaped('(|(uSNChanged={0!e})(uSNCreated={0!e}))', changeUSN)
if filter != '':
usn_filter = f'(&({filter}){usn_filter})'
return self.__search_ad_partitions(filter=usn_filter, show_deleted=show_deleted)
def __dn_from_deleted_object(self, object):
"""gets dn for deleted object (original dn before the object was moved into the deleted objects container)"""
rdn = object['dn'].split('\\0ADEL:')[0]
last_known_parent = object['attributes'].get('lastKnownParent', [b''])[0].decode('UTF-8')
if last_known_parent and '\\0ADEL:' in last_known_parent:
dn, attr = self.__get_s4_deleted(last_known_parent)
last_known_parent = self.__dn_from_deleted_object({'dn': dn, 'attributes': attr})
if last_known_parent:
log.debug("__dn_from_deleted_object: get DN from lastKnownParent (%r) and rdn (%r)", last_known_parent, rdn)
return ldap.dn.dn2str(str2dn(rdn) + str2dn(last_known_parent))
else:
log.warning('lastKnownParent attribute for deleted object rdn="%s" was not set, so we must ignore the object', rdn)
return None
def __object_from_element(self, element):
"""
gets an object from an AD LDAP-element, implements necessary mapping
:param element:
(dn, attributes) tuple from a search in AD-LDAP
:ptype element: tuple
"""
if element[0] == 'None' or element[0] is None:
return None # referrals
object = {}
object['dn'] = element[0]
object['attributes'] = element[1]
deleted_object = False
# modtype
if b'TRUE' in element[1].get('isDeleted', []):
object['modtype'] = 'delete'
deleted_object = True
else:
# check if is moved
olddn = self._get_DN_for_GUID(element[1]['objectGUID'][0])
log.debug("object_from_element: olddn: %s", olddn)
if olddn and olddn.lower() != element[0].lower() and ldap.explode_rdn(olddn.lower()) == ldap.explode_rdn(element[0].lower()):
object['modtype'] = 'move'
object['olddn'] = olddn
log.debug("object_from_element: detected move of AD-Object")
else:
object['modtype'] = 'modify'
if olddn and olddn.lower() != element[0].lower(): # modrdn
object['olddn'] = olddn
if deleted_object: # dn is in deleted-objects-container, need to parse to original dn
object['deleted_dn'] = object['dn']
object['dn'] = self.__dn_from_deleted_object(object)
log.debug("object_from_element: DN of removed object: %r", object['dn'])
# self._remove_GUID(element[1]['objectGUID'][0]) # cache is not needed anymore?
if not object['dn']:
return None
return object
def __identify_s4_type(self, object):
"""Identify the type of the specified AD object"""
if not object or 'attributes' not in object:
return None
for key in self.property.keys():
if self._filter_match(self.property[key].con_search_filter, object['attributes']):
return key
def __update_lastUSN(self, object):
"""Update der lastUSN"""
if self.__get_change_usn(object) > self._get_lastUSN():
self._set_lastUSN(self.__get_change_usn(object))
def __get_highestCommittedUSN(self):
"""get highestCommittedUSN stored in AD"""
try:
return int(self.s4_search_ext_s(
'', # base
ldap.SCOPE_BASE,
'objectclass=*', # filter
['highestCommittedUSN'],
)[0][1]['highestCommittedUSN'][0].decode('ASCII'))
except ldap.LDAPError:
log.exception("search for highestCommittedUSN failed")
print("ERROR: initial search in AD failed, check network and configuration")
return 0
[docs]
def set_primary_group_to_ucs_user(self, object_key, object_ucs):
"""check if correct primary group is set to a fresh UCS-User"""
rid_filter = format_escaped("(samaccountname={0!e})", object_ucs['username'])
s4_group_rid_resultlist = self.__search_s4(base=self.lo_s4.base, scope=ldap.SCOPE_SUBTREE, filter=rid_filter, attrlist=['dn', 'primaryGroupID'])
if s4_group_rid_resultlist[0][0] not in [b"None", b"", None]:
s4_group_rid = s4_group_rid_resultlist[0][1]['primaryGroupID'][0].decode('UTF-8')
log.debug("set_primary_group_to_ucs_user: S4 rid: %r", s4_group_rid)
ldap_group_filter = format_escaped("(objectSid={0!e}-{1!e})", self.s4_sid, s4_group_rid)
ldap_group_s4 = self.__search_s4(base=self.lo_s4.base, scope=ldap.SCOPE_SUBTREE, filter=ldap_group_filter)
if not ldap_group_s4[0][0]:
log.error("s4.set_primary_group_to_ucs_user: Primary Group in S4 not found (not enough rights?), sync of this object will fail!")
ucs_group = self._object_mapping('group', {'dn': ldap_group_s4[0][0], 'attributes': ldap_group_s4[0][1]}, object_type='con')
object_ucs['primaryGroup'] = ucs_group['dn']
[docs]
def primary_group_sync_from_ucs(self, key, object): # object mit ad-dn
"""sync primary group of an ucs-object to ad"""
object_key = key
object_ucs = self._object_mapping(object_key, object)
ldap_object_ucs = self.get_ucs_ldap_object(object_ucs['dn'])
if not ldap_object_ucs:
log.process('primary_group_sync_from_ucs: The UCS object (%s) was not found. The object was removed.', object_ucs['dn'])
return
ldap_object_s4 = self.get_object(object['dn'])
if not ldap_object_s4:
log.process('primary_group_sync_from_ucs: The S4 object (%s) was not found. The object was removed.', object['dn'])
return
ucs_group_id = ldap_object_ucs['gidNumber'][0].decode('UTF-8') # FIXME: fails if group does not exists
ucs_group_filter = format_escaped('(&(objectClass=univentionGroup)(gidNumber={0!e}))', ucs_group_id)
ucs_group_ldap = self.search_ucs(filter=ucs_group_filter) # is empty !?
if ucs_group_ldap == []:
log.warning("primary_group_sync_from_ucs: failed to get UCS-Group with gid %s, can't sync to S4", ucs_group_id)
return
member_key = 'group' # FIXME: generate by identify-function ?
s4_group_object = self._object_mapping(member_key, {'dn': ucs_group_ldap[0][0], 'attributes': ucs_group_ldap[0][1]}, 'ucs')
ldap_object_s4_group = self.get_object(s4_group_object['dn'])
# FIXME: default value "513" should be configurable
rid = b'513'
if 'objectSid' in ldap_object_s4_group:
rid = decode_sid(ldap_object_s4_group['objectSid'][0]).rsplit('-', 1)[-1].encode('ASCII')
# to set a valid primary group we need to:
# - check if either the primaryGroupID is already set to rid or
# - prove that the user is member of this group, so: at first we need the ad_object for this element
# this means we need to map the user to get it's S4-DN which would call this function recursively
if "primaryGroupID" in ldap_object_s4 and ldap_object_s4["primaryGroupID"][0] == rid:
log.debug("primary_group_sync_from_ucs: primary Group is correct, no changes needed")
return True # nothing left to do
else:
s4_members = self.get_s4_members(s4_group_object['dn'], ldap_object_s4_group)
s4_members_lower = [x.lower() for x in s4_members]
if object['dn'].lower() not in s4_members_lower: # add as member
s4_members.append(object['dn'])
log.debug("primary_group_sync_from_ucs: primary Group needs change of membership in S4")
self.lo_s4.lo.modify_s(s4_group_object['dn'], [(ldap.MOD_REPLACE, 'member', [x.encode('UTF-8') for x in s4_members])])
# set new primary group
log.debug("primary_group_sync_from_ucs: changing primary Group in S4")
self.lo_s4.lo.modify_s(object['dn'], [(ldap.MOD_REPLACE, 'primaryGroupID', rid)])
# If the user is not member in UCS of the previous primary group, the user must
# be removed from this group in AD: https://forge.univention.org/bugzilla/show_bug.cgi?id=26514
prev_samba_primary_group_id = ldap_object_s4['primaryGroupID'][0].decode('UTF-8')
s4_group_filter = format_escaped('(objectSid={0!e}-{1!e})', self.s4_sid, prev_samba_primary_group_id)
s4_group = self.__search_s4(base=self.lo_s4.base, scope=ldap.SCOPE_SUBTREE, filter=s4_group_filter)
ucs_group_object = self._object_mapping('group', {'dn': s4_group[0][0], 'attributes': s4_group[0][1]}, 'con')
ucs_group = self.get_ucs_ldap_object(ucs_group_object['dn'])
is_member = False
for member in ucs_group.get('uniqueMember', []):
if member.lower() == object_ucs['dn'].lower():
is_member = True
break
if not is_member:
# remove AD member from previous group
log.debug("primary_group_sync_from_ucs: remove S4 member from previous group")
self.lo_s4.lo.modify_s(s4_group[0][0], [(ldap.MOD_DELETE, 'member', [object['dn'].encode('UTF-8')])])
return True
[docs]
def primary_group_sync_to_ucs(self, key, object): # object mit ucs-dn
"""sync primary group of an ad-object to ucs"""
object_key = key
ad_object = self._object_mapping(object_key, object, 'ucs')
ldap_object_s4 = self.get_object(ad_object['dn'])
s4_group_rid = ldap_object_s4['primaryGroupID'][0].decode('UTF-8')
log.debug("primary_group_sync_to_ucs: S4 rid: %s", s4_group_rid)
ldap_group_filter = format_escaped('(objectSid={0!e}-{1!e})', self.s4_sid, s4_group_rid)
ldap_group_s4 = self.__search_s4(base=self.lo_s4.base, scope=ldap.SCOPE_SUBTREE, filter=ldap_group_filter)
ucs_group = self._object_mapping('group', {'dn': ldap_group_s4[0][0], 'attributes': ldap_group_s4[0][1]})
log.debug("primary_group_sync_to_ucs: ucs-group: %s", ucs_group['dn'])
ucs_admin_object = univention.admin.objects.get(self.modules[object_key], co='', lo=self.lo, position='', dn=object['dn'])
ucs_admin_object.open()
if ucs_admin_object["primaryGroup"].lower() != ucs_group["dn"].lower():
# need to set to dn with correct case or the ucs-module will fail
new_group = ucs_group['dn'].lower()
ucs_admin_object['primaryGroup'] = new_group
ucs_admin_object.modify()
log.debug("primary_group_sync_to_ucs: changed primary Group in ucs")
else:
log.debug("primary_group_sync_to_ucs: change of primary Group in ucs not needed")
[docs]
def object_memberships_sync_from_ucs(self, key, object):
"""sync group membership in AD if object was changend in UCS"""
log.trace("object_memberships_sync_from_ucs: object: %s", object)
if 'group' in self.property and getattr(self.property['group'], 'sync_mode', '') in ['read', 'none']:
log.debug("group memberships sync to s4 ignored, group sync_mode is read")
return
# search groups in UCS which have this object as member
object_ucs = self._object_mapping(key, object)
# Exclude primary group
ucs_object_gid = object_ucs['attributes']['gidNumber'][0].decode('UTF-8')
ucs_group_filter = format_escaped('(&(objectClass=univentionGroup)(uniqueMember={0!e})(!(gidNumber={1!e})))', object_ucs['dn'], ucs_object_gid)
ucs_groups_ldap = self.search_ucs(filter=ucs_group_filter)
if ucs_groups_ldap == []:
log.debug("object_memberships_sync_from_ucs: No group-memberships in UCS for %s", object['dn'])
return
log.debug("object_memberships_sync_from_ucs: is member in %s groups", len(ucs_groups_ldap))
for groupDN, attributes in ucs_groups_ldap:
if groupDN not in ['None', '', None]:
ad_object = {'dn': groupDN, 'attributes': attributes, 'modtype': 'modify'}
if not self._ignore_object('group', ad_object):
sync_object = self._object_mapping('group', ad_object, 'ucs')
sync_object_s4 = self.get_object(sync_object['dn'])
s4_group_object = {'dn': sync_object['dn'], 'attributes': sync_object_s4}
if sync_object_s4:
# self.group_members_sync_from_ucs( 'group', sync_object )
self.one_group_member_sync_from_ucs(s4_group_object, object)
self.__group_cache_ucs_append_member(groupDN, object_ucs['dn'])
def __group_cache_ucs_append_member(self, group, member):
member_cache = self.group_members_cache_ucs.setdefault(group.lower(), set())
if member.lower() not in member_cache:
log.debug("__group_cache_ucs_append_member: Append user %r to UCS group member cache of %r", member, group)
member_cache.add(member.lower())
[docs]
def group_members_sync_from_ucs(self, key, object): # object mit ad-dn
"""sync groupmembers in AD if changend in UCS"""
log.debug("group_members_sync_from_ucs: %s", object)
object_key = key
object_ucs = self._object_mapping(object_key, object)
object_ucs_dn = object_ucs['dn']
log.debug("group_members_sync_from_ucs: dn is: %r", object_ucs_dn)
ldap_object_ucs = self.get_ucs_ldap_object(object_ucs_dn)
if not ldap_object_ucs:
log.process('group_members_sync_from_ucs:: The UCS object (%s) was not found. The object was removed.', object_ucs_dn)
return
ldap_object_ucs_gidNumber = ldap_object_ucs['gidNumber'][0].decode('UTF-8')
ucs_members = {x.decode('UTF-8') for x in ldap_object_ucs.get('uniqueMember', [])}
log.debug("ucs_members: %s", ucs_members)
if ucs_members:
# skip members which have this group as primary group (set same gidNumber)
prim_members_ucs_filter = format_escaped('(gidNumber={0!e})', ldap_object_ucs_gidNumber)
prim_members_ucs = self.lo.lo.search(filter=prim_members_ucs_filter, attr=['gidNumber'])
for prim_object in prim_members_ucs:
if prim_object[0].lower() in ucs_members:
ucs_members.remove(prim_object[0].lower())
log.debug("group_members_sync_from_ucs: clean ucs_members: %s", ucs_members)
# all dn's need to be lower-case so we can compare them later and put them in the UCS group member cache:
self.group_members_cache_ucs[object_ucs_dn.lower()] = set()
log.debug("group_members_sync_from_ucs: UCS group member cache reset")
# lookup all current members of S4 group
ldap_object_s4 = self.get_object(object['dn'])
if not ldap_object_s4:
log.process('group_members_sync_from_ucs:: The S4 object (%s) was not found. The object was removed.', object['dn'])
return
s4_members = set(self.get_s4_members(object['dn'], ldap_object_s4))
log.debug("group_members_sync_from_ucs: s4_members %s", s4_members)
# map members from UCS to AD and check if they exist
s4_members_from_ucs = set() # Code review comment: For some reason this is a list of lowercase DNs
for member_dn in ucs_members:
s4_dn = self.group_member_mapping_cache_ucs.get(member_dn.lower())
if s4_dn:
log.debug("Found %s in UCS group member cache: %s", member_dn, s4_dn)
s4_members_from_ucs.add(s4_dn.lower())
self.__group_cache_ucs_append_member(object_ucs_dn, member_dn)
else:
log.debug("Did not find %s in UCS group member cache", member_dn)
member_object = {'dn': member_dn, 'modtype': 'modify', 'attributes': self.lo.get(member_dn)}
try:
# check if this is members primary group, if true it shouldn't be added to s4
if member_object['attributes']['gidNumber'][0] == ldap_object_ucs_gidNumber.encode('UTF-8'):
# is primary group
continue
except (KeyError, IndexError):
# can't sync them if users have no posix-account
continue
_mod, mo_key = self.identify_udm_object(member_dn, member_object['attributes'])
if not mo_key:
log.warning("group_members_sync_from_ucs: failed to identify object type of ucs member, ignore membership: %s", member_dn)
continue # member is an object which will not be synced
s4_dn = self._object_mapping(mo_key, member_object, 'ucs')['dn']
# check if dn exists in ad
try:
if self.lo_s4.get(s4_dn, attr=['cn']): # search only for cn to suppress coding errors
s4_members_from_ucs.add(s4_dn.lower())
log.debug("group_members_sync_from_ucs: Adding %s to UCS group member cache, value: %s", member_dn.lower(), s4_dn)
self.group_member_mapping_cache_ucs[member_dn.lower()] = s4_dn
self.__group_cache_ucs_append_member(object_ucs_dn, member_dn)
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
log.process("group_members_sync_from_ucs: failed to get S4 dn for UCS group member %s, assume object doesn't exist", member_dn, exc_info=True)
log.debug("group_members_sync_from_ucs: UCS-members in s4_members_from_ucs %s", s4_members_from_ucs)
# check if members in S4 don't exist in UCS, if true they need to be added in S4
for member_dn in s4_members_from_ucs.copy():
if member_dn.lower() not in s4_members_from_ucs:
try:
ad_object = self.get_object(member_dn)
mo_key = self.__identify_s4_type({'dn': member_dn, 'attributes': ad_object})
ucs_dn = self._object_mapping(mo_key, {'dn': member_dn, 'attributes': ad_object})['dn']
if not self.lo.get(ucs_dn, attr=['cn']):
# Leave the following line commented out, as we don't want to keep the member in Samba/AD if it's not present in OpenLDAP
# Note: in this case the membership gets removed even if the object itself is ignored for synchronization
# s4_members_from_ucs.add(member_dn.lower())
log.debug("group_members_sync_from_ucs: Object exists only in S4 [%s]", ucs_dn)
elif self._ignore_object(mo_key, {'dn': member_dn, 'attributes': ad_object}):
# Keep the member in Samba/AD if it's also present in OpenLDAP but ignored in synchronization?
s4_members_from_ucs.add(member_dn.lower())
log.debug("group_members_sync_from_ucs: Object ignored in S4 [%s], key = [%s]", ucs_dn, mo_key)
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
log.process("group_members_sync_from_ucs: failed to get UCS dn for S4 group member %s", member_dn, exc_info=True)
log.debug("group_members_sync_from_ucs: UCS-and S4-members in s4_members_from_ucs %s", s4_members_from_ucs)
# compare lists and generate modlist
# direct compare is not possible, because s4_members_from_ucs are all lowercase, s4_members are not, so we need to iterate...
# FIXME: should be done in the last iteration (above)
# need to remove users from s4_members_from_ucs which have this group as primary group. may failed earlier if groupnames are mapped
try:
group_rid = decode_sid(fix_dn_in_search(self.lo_s4.lo.search_s(object['dn'], ldap.SCOPE_BASE, '(objectClass=*)', ['objectSid']))[0][1]['objectSid'][0]).rsplit('-', 1)[-1]
except ldap.NO_SUCH_OBJECT:
group_rid = None
if group_rid:
# search for members who have this as their primaryGroup
prim_members_s4_filter = format_escaped('(primaryGroupID={0!e})', group_rid)
prim_members_s4 = self.__search_s4(self.lo_s4.base, ldap.SCOPE_SUBTREE, prim_members_s4_filter, ['cn'])
for prim_dn, prim_object in prim_members_s4:
if prim_dn not in ['None', '', None]: # filter referrals
if prim_dn.lower() in s4_members_from_ucs:
s4_members_from_ucs.remove(prim_dn.lower())
elif prim_dn in s4_members_from_ucs:
# Code review comment: Obsolete? s4_members_from_ucs should be all lowercase at this point
s4_members_from_ucs.remove(prim_dn)
log.debug("group_members_sync_from_ucs: s4_members_from_ucs without members with this as their primary group: %s", s4_members_from_ucs)
add_members = s4_members_from_ucs
del_members = set()
log.debug("group_members_sync_from_ucs: members to add initialized: %s", add_members)
for member_dn in s4_members:
log.debug("group_members_sync_from_ucs: %s in s4_members_from_ucs?", member_dn)
member_dn_lower = member_dn.lower()
if member_dn_lower in s4_members_from_ucs:
log.debug("group_members_sync_from_ucs: Yes")
add_members.remove(member_dn_lower)
else:
if object['modtype'] == 'add':
log.process("group_members_sync_from_ucs: %s is newly added. For this case don't remove current S4 members.", object['dn'].lower())
elif (member_dn_lower in self.group_members_cache_con.get(object['dn'].lower(), set())) or (
self.property.get('group') and self.property['group'].sync_mode in ['write', 'none']
):
# FIXME: Should this really also be done if sync_mode for group is 'none'?
# remove member only if he was in the cache on AD side
# otherwise it is possible that the user was just created on AD and we are on the way back
log.debug("group_members_sync_from_ucs: No")
del_members.add(member_dn)
else:
log.process("group_members_sync_from_ucs: %s was not found in S4 group member cache of %s, don't delete", member_dn_lower, object['dn'].lower())
log.debug("group_members_sync_from_ucs: members to add: %s", add_members)
log.debug("group_members_sync_from_ucs: members to del: %s", del_members)
if add_members or del_members:
s4_members |= add_members # Note: add_members are only lowercase
s4_members -= del_members # Note: del_members are case sensitive
log.debug("group_members_sync_from_ucs: members result: %r", s4_members)
self.lo_s4.lo.modify_s(object['dn'], [(ldap.MOD_REPLACE, 'member', [x.encode('UTF-8') for x in s4_members])])
return True
[docs]
def object_memberships_sync_to_ucs(self, key, object):
"""sync group membership in UCS if object was changend in AD"""
# disable this debug line, see Bug #12031
# log.debug("object_memberships_sync_to_ucs: object: %s" % object)
if 'group' in self.property and getattr(self.property['group'], 'sync_mode', '') in ['write', 'none']:
log.debug(self.context_log(key, object, "ignored group memberships sync: group sync_mode is write", to_ucs=True))
return
if 'memberOf' in object['attributes']:
for groupDN in object['attributes']['memberOf']:
groupDN = groupDN.decode('UTF-8')
ad_object = {'dn': groupDN, 'attributes': self.get_object(groupDN), 'modtype': 'modify'}
if not self._ignore_object('group', ad_object):
sync_object = self._object_mapping('group', ad_object)
ldap_object_ucs = self.get_ucs_ldap_object(sync_object['dn'])
ucs_group_object = {'dn': sync_object['dn'], 'attributes': ldap_object_ucs}
log.debug("object_memberships_sync_to_ucs: sync_object: %s", ldap_object_ucs)
# check if group exists in UCS, may fail
# if the group will be synced later
if ldap_object_ucs:
self.one_group_member_sync_to_ucs(ucs_group_object, object)
dn = object['attributes'].get('distinguishedName', [None])[0]
if dn:
groupDN_lower = groupDN.lower()
member_cache = self.group_members_cache_con.setdefault(groupDN_lower, set())
dn_lower = dn.decode('UTF-8').lower()
if dn_lower not in member_cache:
log.debug("object_memberships_sync_to_ucs: Append user %s to AD group member cache of %s", dn_lower, groupDN_lower)
member_cache.add(dn_lower)
else:
log.debug("object_memberships_sync_to_ucs: Failed to append user %s to AD group member cache of %s", object['dn'].lower(), groupDN.lower())
def __compare_lowercase(self, value, value_list):
"""Checks if value is in value_list"""
return any(value.lower() == v.lower() for v in value_list)
def __compare_lowercase_dn(self, dn, dn_list):
"""Checks if dn is in dn_list"""
dn_lower = dn.lower()
return any(self.lo.compare_dn(dn_lower, d.lower()) for d in dn_list)
[docs]
def one_group_member_sync_to_ucs(self, ucs_group_object, object):
"""sync groupmembers in UCS if changend one member in AD"""
# In AD the object['dn'] is member of the group sync_object
ml = []
if not self.__compare_lowercase_dn(object['dn'].encode('UTF-8'), ucs_group_object['attributes'].get('uniqueMember', [])):
ml.append((ldap.MOD_ADD, 'uniqueMember', [object['dn'].encode('UTF-8')]))
if object['attributes'].get('uid'):
uid = object['attributes']['uid'][0]
if not self.__compare_lowercase(uid, ucs_group_object['attributes'].get('memberUid', [])):
ml.append((ldap.MOD_ADD, 'memberUid', [uid]))
if ml:
log.trace("one_group_member_sync_to_ucs: modlist: %s", ml)
try:
self.lo.lo.modify_s(ucs_group_object['dn'], ml)
except ldap.ALREADY_EXISTS:
# The user is already member in this group or it is his primary group
# This might happen, if we synchronize a rejected file with old information
# See Bug #25709 Comment #17: https://forge.univention.org/bugzilla/show_bug.cgi?id=25709#c17
log.debug("one_group_member_sync_to_ucs: User is already member of the group: %s modlist: %s", ucs_group_object['dn'], ml)
[docs]
def one_group_member_sync_from_ucs(self, s4_group_object, object):
"""sync groupmembers in AD if changend one member in AD"""
ml = []
if not self.__compare_lowercase_dn(object['dn'].encode('UTF-8'), s4_group_object['attributes'].get('member', [])):
ml.append((ldap.MOD_ADD, 'member', [object['dn'].encode('UTF-8')]))
if ml:
log.trace("one_group_member_sync_from_ucs: modlist: %s", ml)
try:
self.lo_s4.lo.modify_s(s4_group_object['dn'], ml)
except ldap.ALREADY_EXISTS:
# The user is already member in this group or it is his primary group
# This might happen, if we synchronize a rejected file with old information
# See Bug #25709 Comment #17: https://forge.univention.org/bugzilla/show_bug.cgi?id=25709#c17
log.debug("one_group_member_sync_from_ucs: User is already member of the group: %s modlist: %s", s4_group_object['dn'], ml)
# The user has been removed from the cache. He must be added in any case
log.debug("one_group_member_sync_from_ucs: Append user %s to S4 group member cache of %s", object['dn'].lower(), s4_group_object['dn'].lower())
self.group_members_cache_con.setdefault(s4_group_object['dn'].lower(), set()).add(object['dn'].lower())
def __group_cache_con_append_member(self, group, member):
group_lower = group.lower()
member_cache = self.group_members_cache_con.setdefault(group_lower, set())
member_lower = member.lower()
if member_lower not in member_cache:
log.debug("__group_cache_con_append_member: Append user %s to S4 group member cache of %s", member_lower, group_lower)
member_cache.add(member_lower)
[docs]
def group_members_sync_to_ucs(self, key, object):
"""sync groupmembers in UCS if changend in AD"""
log.debug("group_members_sync_to_ucs: object: %s", object)
object_key = key
ad_object = self._object_mapping(object_key, object, 'ucs')
ad_object_dn = ad_object['dn']
log.debug("group_members_sync_to_ucs: ad_object (mapped): %s", ad_object)
# FIXME: does not use dn-mapping-function
ldap_object_s4 = self.get_object(ad_object_dn)
if not ldap_object_s4:
log.process('group_members_sync_to_ucs:: The S4 object (%s) was not found. The object was removed.', ad_object_dn)
return
s4_members = self.get_s4_members(ad_object_dn, ldap_object_s4)
log.debug("group_members_sync_to_ucs: s4_members %s", s4_members)
# search and add members which have this as their primaryGroup
group_rid = decode_sid(ldap_object_s4['objectSid'][0]).rsplit('-', 1)[-1]
prim_members_s4_filter = format_escaped('(primaryGroupID={0!e})', group_rid)
prim_members_s4 = self.__search_s4(self.lo_s4.base, ldap.SCOPE_SUBTREE, prim_members_s4_filter)
for prim_dn, _prim_object in prim_members_s4:
if prim_dn not in ['None', '', None]: # filter referrals
s4_members.append(prim_dn)
log.debug("group_members_sync_to_ucs: clean s4_members %s", s4_members)
self.group_members_cache_con[ad_object_dn.lower()] = set()
log.debug("group_members_sync_to_ucs: S4 group member cache reset")
# lookup all current members of UCS group
ldap_object_ucs = self.get_ucs_ldap_object(object['dn'])
ucs_members = {x.decode('UTF-8') for x in ldap_object_ucs.get('uniqueMember', [])}
log.debug("group_members_sync_to_ucs: ucs_members: %s", ucs_members)
# map members from AD to UCS and check if they exist
ucs_members_from_s4 = {'user': [], 'group': [], 'unknown': []}
dn_mapping_ucs_member_to_s4 = {}
for member_dn in s4_members:
ucs_dn = self.group_member_mapping_cache_con.get(member_dn.lower())
if ucs_dn:
log.debug("Found %s in AD group member cache: DN: %s", member_dn, ucs_dn)
ucs_members_from_s4['unknown'].append(ucs_dn.lower())
dn_mapping_ucs_member_to_s4[ucs_dn.lower()] = member_dn
self.__group_cache_con_append_member(ad_object_dn, member_dn)
else:
log.debug("Did not find %s in AD group member cache", member_dn)
member_object = self.get_object(member_dn)
if member_object:
mo_key = self.__identify_s4_type({'dn': member_dn, 'attributes': member_object})
if not mo_key:
log.warning("group_members_sync_to_ucs: failed to identify object type of S4 group member, ignore membership: %s", member_dn)
continue # member is an object which will not be synced
if self._ignore_object(mo_key, {'dn': member_dn, 'attributes': member_object}):
log.debug("group_members_sync_to_ucs: Object dn %s should be ignored, ignore membership", member_dn)
continue
ucs_dn = self._object_mapping(mo_key, {'dn': member_dn, 'attributes': member_object})['dn']
log.debug("group_members_sync_to_ucs: mapped AD group member to ucs DN %s", ucs_dn)
dn_mapping_ucs_member_to_s4[ucs_dn.lower()] = member_dn
try:
if self.lo.get(ucs_dn):
ucs_members_from_s4['unknown'].append(ucs_dn.lower())
self.group_member_mapping_cache_con[member_dn.lower()] = ucs_dn
self.__group_cache_con_append_member(ad_object_dn, member_dn)
else:
log.debug("Failed to find %s via self.lo.get", ucs_dn)
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
log.process("group_members_sync_to_ucs: failed to get UCS dn for S4 group member %s, assume object doesn't exist", member_dn, exc_info=True)
# build an internal cache
cache = {}
# check if members in UCS don't exist in AD, if true they need to be added in UCS
for member_dn in ucs_members:
member_dn_lower = member_dn.lower()
if not (member_dn_lower in ucs_members_from_s4['user'] or member_dn_lower in ucs_members_from_s4['group'] or member_dn_lower in ucs_members_from_s4['unknown']):
try:
cache[member_dn] = self.lo.get(member_dn)
ucs_object = {'dn': member_dn, 'modtype': 'modify', 'attributes': cache[member_dn]}
if self._ignore_object(key, object):
continue
_mod, k = self.identify_udm_object(member_dn, ucs_object['attributes'])
if k and _mod.module in ('users/user', 'groups/group', 'computers/windows_domaincontroller', 'computers/windows'):
s4_dn = self._object_mapping(k, ucs_object, 'ucs')['dn']
if not dn_mapping_ucs_member_to_s4.get(member_dn_lower):
dn_mapping_ucs_member_to_s4[member_dn_lower] = s4_dn
log.debug("group_members_sync_to_ucs: search for: %s", s4_dn)
# search only for cn to suppress coding errors
if not self.lo_s4.get(s4_dn, attr=['cn']):
# member does not exist in S4 but should
# stay a member in UCS
ucs_members_from_s4[k].append(member_dn_lower)
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
log.process("group_members_sync_to_ucs: failed to get AD dn for UCS group member %s", member_dn, exc_info=True)
log.debug("group_members_sync_to_ucs: dn_mapping_ucs_member_to_s4=%s", dn_mapping_ucs_member_to_s4)
add_members = copy.deepcopy(ucs_members_from_s4)
del_members = {'user': [], 'group': []}
log.debug("group_members_sync_to_ucs: members to add initialized: %s", add_members)
for member_dn in ucs_members:
member_dn_lower = member_dn.lower()
if member_dn_lower in ucs_members_from_s4['user']:
add_members['user'].remove(member_dn_lower)
elif member_dn_lower in ucs_members_from_s4['group']:
add_members['group'].remove(member_dn_lower)
elif member_dn_lower in ucs_members_from_s4['unknown']:
add_members['unknown'].remove(member_dn_lower)
else:
# remove member only if he was in the cache
# otherwise it is possible that the user was just created on UCS
if (member_dn_lower in self.group_members_cache_ucs.get(object['dn'].lower(), set())) or (
self.property.get('group') and self.property['group'].sync_mode in ['read', 'none']
):
# FIXME: Should this really also be done if sync_mode for group is 'none'?
log.debug("group_members_sync_to_ucs: %s was found in UCS group member cache of %s", member_dn_lower, object['dn'].lower())
ucs_object_attr = cache.get(member_dn)
if not ucs_object_attr:
ucs_object_attr = self.lo.get(member_dn)
cache[member_dn] = ucs_object_attr
ucs_object = {'dn': member_dn, 'modtype': 'modify', 'attributes': ucs_object_attr}
_mod, k = self.identify_udm_object(member_dn, ucs_object['attributes'])
if k and _mod.module in ('users/user', 'groups/group', 'computers/windows_domaincontroller', 'computers/windows'):
# identify if DN is a user or a group (will be ignored if it is a host)
if not self._ignore_object(k, ucs_object):
del_members[k].append(member_dn)
else:
log.debug("group_members_sync_to_ucs: %s was not found in UCS group member cache of %s, don't delete", member_dn_lower, object['dn'].lower())
log.debug("group_members_sync_to_ucs: members to add: %s", add_members)
log.debug("group_members_sync_to_ucs: members to del: %s", del_members)
if add_members['user'] or add_members['group'] or del_members['user'] or del_members['group'] or add_members['unknown']:
ucs_admin_object = univention.admin.objects.get(self.modules[object_key], co='', lo=self.lo, position='', dn=object['dn'])
ucs_admin_object.open()
uniqueMember_add = add_members['user'] + add_members['group'] + add_members['unknown']
uniqueMember_del = del_members['user'] + del_members['group']
memberUid_add = []
memberUid_del = []
for member in add_members['user']:
(_rdn_attribute, uid, _flags) = str2dn(member)[0][0]
memberUid_add.append(uid)
for member in add_members['unknown']: # user or group?
ucs_object_attr = self.lo.get(member)
uid = ucs_object_attr.get('uid')
if uid:
memberUid_add.append(uid[0].decode('UTF-8'))
for member in del_members['user']:
(_rdn_attribute, uid, _flags) = str2dn(member)[0][0]
memberUid_del.append(uid)
if uniqueMember_del or memberUid_del:
ucs_admin_object.fast_member_remove(uniqueMember_del, memberUid_del, ignore_license=True)
if uniqueMember_add or memberUid_del:
ucs_admin_object.fast_member_add(uniqueMember_add, memberUid_add)
[docs]
def disable_user_from_ucs(self, key, object):
object_key = key
object_ucs = self._object_mapping(object_key, object)
ldap_object_ad = self.get_object(object['dn'])
try:
ucs_admin_object = univention.admin.objects.get(self.modules[object_key], co='', lo=self.lo, position='', dn=object_ucs['dn'])
except univention.admin.uexceptions.noObject as exc:
log.warning("Ignore already removed object %s.", exc)
return
ucs_admin_object.open()
modlist = []
log.debug("Disabled state: %s", ucs_admin_object['disabled'].lower())
if ucs_admin_object["disabled"].lower() not in ["none", "0"]:
# user disabled in UCS
if 'userAccountControl' in ldap_object_ad and (int(ldap_object_ad['userAccountControl'][0]) & 2) == 0:
# user enabled in S4 -> change
res = str(int(ldap_object_ad['userAccountControl'][0]) | 2).encode('ASCII')
modlist.append((ldap.MOD_REPLACE, 'userAccountControl', [res]))
else:
# user enabled in UCS
if 'userAccountControl' in ldap_object_ad and (int(ldap_object_ad['userAccountControl'][0]) & 2) > 0:
# user disabled in S4 -> change
res = str(int(ldap_object_ad['userAccountControl'][0]) - 2).encode('ASCII')
modlist.append((ldap.MOD_REPLACE, 'userAccountControl', [res]))
# account expires
# This value represents the number of 100 nanosecond intervals since January 1, 1601 (UTC). A value of 0 or 0x7FFFFFFFFFFFFFFF (9223372036854775807) indicates that the account never expires.
if not ucs_admin_object['userexpiry']:
# ucs account not expired
if 'accountExpires' in ldap_object_ad and (int(ldap_object_ad['accountExpires'][0]) != 9223372036854775807 or int(ldap_object_ad['accountExpires'][0]) == 0):
# ad account expired -> change
modlist.append((ldap.MOD_REPLACE, 'accountExpires', [b'9223372036854775807']))
else:
# ucs account expired
if 'accountExpires' in ldap_object_ad and int(ldap_object_ad['accountExpires'][0]) != unix2s4_time(ucs_admin_object['userexpiry']):
# s4 account not expired -> change
modlist.append((ldap.MOD_REPLACE, 'accountExpires', [str(unix2s4_time(ucs_admin_object['userexpiry'])).encode('ASCII')]))
if modlist:
log.trace("disable_user_from_ucs: modlist: %s", modlist)
self.lo_s4.lo.modify_s(object['dn'], modlist)
[docs]
def disable_user_to_ucs(self, key, object):
object_key = key
ad_object = self._object_mapping(object_key, object, 'ucs')
ldap_object_ad = self.get_object(ad_object['dn'])
modified = 0
ucs_admin_object = univention.admin.objects.get(self.modules[object_key], co='', lo=self.lo, position='', dn=object['dn'])
ucs_admin_object.open()
if 'userAccountControl' in ldap_object_ad and (int(ldap_object_ad['userAccountControl'][0]) & 2) == 0:
# user enabled in S4
if ucs_admin_object["disabled"].lower() not in ["none", "0"]:
# user disabled in UCS -> change
ucs_admin_object['disabled'] = '0'
modified = 1
else:
# user disabled in S4
if ucs_admin_object['disabled'].lower() in ['none', '0']:
# user enabled in UCS -> change
ucs_admin_object['disabled'] = '1'
modified = 1
if 'accountExpires' in ldap_object_ad and (int(ldap_object_ad['accountExpires'][0]) == 9223372036854775807 or int(ldap_object_ad['accountExpires'][0]) == 0):
# ad account not expired
if ucs_admin_object['userexpiry']:
# ucs account expired -> change
ucs_admin_object['userexpiry'] = None
modified = 1
else:
# ad account expired
log.debug("sync account_expire: s4time: %s unixtime: %s", int(ldap_object_ad['accountExpires'][0]), ucs_admin_object['userexpiry'])
if s42unix_time(int(ldap_object_ad['accountExpires'][0])) != ucs_admin_object['userexpiry']:
# ucs account not expired -> change
ucs_admin_object['userexpiry'] = s42unix_time(int(ldap_object_ad['accountExpires'][0]))
modified = 1
if modified:
ucs_admin_object.modify()
[docs]
def initialize(self):
print("--------------------------------------")
print("Initialize sync from AD")
if self._get_lastUSN() == 0: # we startup new
log.process("initialize AD: last USN is 0, sync all")
# query highest USN in LDAP
highestCommittedUSN = self.__get_highestCommittedUSN()
# poll for all objects without deleted objects
self.poll(show_deleted=False)
# compare highest USN from poll with highest before poll, if the last changes deletes
# the highest USN from poll is to low
self._set_lastUSN(max(highestCommittedUSN, self._get_lastUSN()))
self._commit_lastUSN()
log.debug("initialize S4: sync of all objects finished, lastUSN is %d", self.__get_highestCommittedUSN())
else:
self.resync_rejected()
self.poll()
self._commit_lastUSN()
print("--------------------------------------")
[docs]
def resync_rejected(self):
"""tries to resync rejected dn"""
print("--------------------------------------")
change_count = 0
rejected = self._list_rejected()
print(f"Sync {len(rejected)} rejected changes from S4 to UCS")
sys.stdout.flush()
for change_usn, dn in rejected:
log.process("sync AD > UCS: Resync rejected dn: %r", dn)
try:
sync_successfull = False
elements = self.__search_ad_changeUSN(change_usn, show_deleted=True)
if not elements or len(elements) < 1 or not elements[0][0]:
log.debug("rejected change with id %s not found, don't need to sync", change_usn)
self._remove_rejected(change_usn)
elif len(elements) > 1 and not (elements[1][0] == 'None' or elements[1][0] is None): # all except the first should be referrals
log.warning("more than one rejected object with id %s found, can't proceed", change_usn)
else:
ad_object = self.__object_from_element(elements[0])
property_key = self.__identify_s4_type(ad_object)
mapped_object = self._object_mapping(property_key, ad_object)
try:
if not self._ignore_object(property_key, mapped_object) and not self._ignore_object(property_key, ad_object):
sync_successfull = self.sync_to_ucs(property_key, mapped_object, dn, ad_object)
else:
sync_successfull = True
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
log.exception("sync of rejected object failed \n\t%s", ad_object['dn'])
sync_successfull = False
if sync_successfull:
change_count += 1
self._remove_rejected(change_usn)
self.__update_lastUSN(ad_object)
self._set_DN_for_GUID(elements[0][1]['objectGUID'][0], elements[0][0])
except ldap.SERVER_DOWN:
raise
except Exception:
log.exception("unexpected Error during s4.resync_rejected")
print(f"restored {change_count} rejected changes")
print("--------------------------------------")
sys.stdout.flush()
[docs]
def poll(self, show_deleted=True):
"""poll for changes in AD"""
# search from last_usn for changes
log.debug("sync AD > UCS: polling")
change_count = 0
changes = []
try:
changes = self.__search_ad_changes(show_deleted=show_deleted)
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
log.warning("Exception during search_s4_changes", exc_info=True)
print("--------------------------------------")
print(f"try to sync {len(changes)} changes from S4")
print("done:", end=' ')
sys.stdout.flush()
done = {'counter': 0}
ad_object = None
lastUSN = self._get_lastUSN()
newUSN = lastUSN
def print_progress(ignore=False):
done['counter'] += 1
message = '(%s)' if ignore else '%s'
print(message % (done['counter'],), end=' ')
sys.stdout.flush()
# Check if the connection to UCS ldap exists. Otherwise re-create the session.
try:
self.search_ucs(scope=ldap.SCOPE_BASE)
except ldap.SERVER_DOWN:
log.debug("UCS LDAP connection was closed, re-open the connection.")
self.open_ucs()
for element in changes:
old_element = copy.deepcopy(element)
ad_object = self.__object_from_element(element)
if not ad_object:
print_progress(True)
continue
property_key = self.__identify_s4_type(ad_object)
if not property_key:
log.info(self.context_log(property_key, ad_object, 'ignoring not identified object'))
newUSN = max(self.__get_change_usn(ad_object), newUSN)
print_progress(True)
continue
if self._ignore_object(property_key, ad_object):
if ad_object['modtype'] == 'move':
log.debug("object_from_element: Detected a move of an S4 object into a ignored tree: dn: %s", ad_object['dn'])
ad_object['deleted_dn'] = ad_object['olddn']
ad_object['dn'] = ad_object['olddn']
ad_object['modtype'] = 'delete'
# check the move target
else:
self.__update_lastUSN(ad_object)
print_progress()
continue
if ad_object['dn'].find('\\0ACNF:') > 0:
log.process('Ignore conflicted object: %s', ad_object['dn'])
self.__update_lastUSN(ad_object)
print_progress()
continue
sync_successfull = False
try:
try:
mapped_object = self._object_mapping(property_key, ad_object)
if not self._ignore_object(property_key, mapped_object):
sync_successfull = self.sync_to_ucs(property_key, mapped_object, ad_object['dn'], ad_object)
else:
sync_successfull = True
except univention.admin.uexceptions.ldapError as msg:
if isinstance(msg.original_exception, ldap.SERVER_DOWN):
raise msg.original_exception
raise
except ldap.SERVER_DOWN:
log.error("Got server down during sync, re-open the connection to UCS and S4")
time.sleep(1)
self.open_ucs()
self.open_s4()
except Exception: # FIXME: which exception is to be caught?
log.warning("Exception during poll/sync_to_ucs", exc_info=True)
if sync_successfull:
change_count += 1
newUSN = max(self.__get_change_usn(ad_object), newUSN)
try:
GUID = old_element[1]['objectGUID'][0]
self._set_DN_for_GUID(GUID, old_element[0])
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
log.warning("Exception during set_DN_for_GUID", exc_info=True)
else:
log.warning(self.context_log(property_key, ad_object, 'sync was not successful, save rejected'))
self.save_rejected(ad_object)
self.__update_lastUSN(ad_object)
print_progress()
print("")
if newUSN != lastUSN:
self._set_lastUSN(newUSN)
self._commit_lastUSN()
# return number of synced objects
rejected = self._list_rejected()
print(f"Changes from S4: {change_count} ({len(rejected)} saved rejected)")
print("--------------------------------------")
sys.stdout.flush()
return change_count
def __has_attribute_value_changed(self, attribute, old_ucs_object, new_ucs_object):
return old_ucs_object.get(attribute) != new_ucs_object.get(attribute)
def _remove_dn_from_group_cache(self, con_dn=None, ucs_dn=None):
if con_dn:
try:
log.debug("sync_from_ucs: Removing %s from S4 group member mapping cache", con_dn)
del self.group_member_mapping_cache_con[con_dn.lower()]
except KeyError:
log.trace("sync_from_ucs: %s was not present in S4 group member mapping cache", con_dn)
if ucs_dn:
try:
log.debug("sync_from_ucs: Removing %s from UCS group member mapping cache", ucs_dn)
del self.group_member_mapping_cache_ucs[ucs_dn.lower()]
except KeyError:
log.trace("sync_from_ucs: %s was not present in UCS group member mapping cache", ucs_dn)
def _update_group_member_cache(self, remove_con_dn=None, remove_ucs_dn=None, add_con_dn=None, add_ucs_dn=None):
for group in self.group_members_cache_con:
if remove_con_dn and remove_con_dn in self.group_members_cache_con[group]:
log.debug("_update_group_member_cache: remove %s from con cache for group %s", remove_con_dn, group)
self.group_members_cache_con[group].remove(remove_con_dn)
if add_con_dn and add_con_dn not in self.group_members_cache_con[group]:
log.debug("_update_group_member_cache: add %s to con cache for group %s", add_con_dn, group)
self.group_members_cache_con[group].add(add_con_dn)
for group in self.group_members_cache_ucs:
if remove_ucs_dn and remove_ucs_dn in self.group_members_cache_ucs[group]:
log.debug("_update_group_member_cache: remove %s from ucs cache for group %s", remove_ucs_dn, group)
self.group_members_cache_ucs[group].remove(remove_ucs_dn)
if add_ucs_dn and add_ucs_dn not in self.group_members_cache_ucs[group]:
log.debug("_update_group_member_cache: add %s to ucs cache for group %s", add_ucs_dn, group)
self.group_members_cache_ucs[group].add(add_ucs_dn)
[docs]
def sync_from_ucs(self, property_type, object, pre_mapped_ucs_dn, old_dn=None, old_ucs_object=None, new_ucs_object=None):
# NOTE: pre_mapped_ucs_dn means: original ucs_dn (i.e. before _object_mapping)
# Diese Methode erhaelt von der UCS Klasse ein Objekt,
# welches hier bearbeitet wird und in das AD geschrieben wird.
# object ist brereits vom eingelesenen UCS-Objekt nach AD gemappt, old_dn ist die alte UCS-DN
log.debug("sync_from_ucs: sync object: %s", object['dn'])
# if sync is read (sync from AD) or none, there is nothing to do
if self.property[property_type].sync_mode in ['read', 'none']:
log.debug("sync_from_ucs ignored, sync_mode is %s", self.property[property_type].sync_mode)
return True
# check for move, if old_object exists, set modtype move
pre_mapped_ucs_old_dn = old_dn
if old_dn:
old_dn = object['olddn']
# the old object was moved in UCS, but does this object exist in S4?
try:
old_object = self.s4_search_ext_s(old_dn, ldap.SCOPE_BASE, 'objectClass=*')
except ldap.SERVER_DOWN:
raise
except Exception:
old_object = None
if old_object:
log.debug("move %s from [%s] to [%s]", property_type, old_dn, object['dn'])
try:
self.lo_s4.rename(old_dn, object['dn'])
except ldap.NO_SUCH_OBJECT: # check if object is already moved (we may resync now)
new = self.s4_search_ext_s(object['dn'], ldap.SCOPE_BASE, 'objectClass=*')
if not new:
raise
# need to actualise the GUID, group cache and DN-Mapping
object['modtype'] = 'move'
self._remove_dn_from_group_cache(con_dn=old_dn, ucs_dn=pre_mapped_ucs_old_dn)
self._update_group_member_cache(
remove_con_dn=old_dn.lower(),
remove_ucs_dn=pre_mapped_ucs_old_dn.lower(),
add_con_dn=object['dn'].lower(),
add_ucs_dn=pre_mapped_ucs_dn.lower())
log.debug("sync_from_ucs: Updating UCS and S4 group member mapping cache for %s to %s", pre_mapped_ucs_dn, object['dn'])
self.group_member_mapping_cache_ucs[pre_mapped_ucs_dn.lower()] = object['dn']
self.group_member_mapping_cache_con[object['dn'].lower()] = pre_mapped_ucs_dn
self._set_DN_for_GUID(self.s4_search_ext_s(object['dn'], ldap.SCOPE_BASE, 'objectClass=*')[0][1]['objectGUID'][0], object['dn'])
self._remove_dn_mapping(pre_mapped_ucs_old_dn, old_dn)
self._check_dn_mapping(pre_mapped_ucs_dn, object['dn'])
log.process(self.context_log(property_type, object, to_ucs=False))
if 'olddn' in object:
object.pop('olddn') # not needed anymore, will fail object_mapping in later functions
old_dn = None
addlist = []
modlist = []
# get current object
ad_object = self.get_object(object['dn'])
if ad_object:
objectGUID = univention.s4connector.decode_guid(ad_object.get('objectGUID')[0])
if self.lockingdb.is_s4_locked(objectGUID):
log.process("Unable to sync %s (GUID: %s). The object is currently locked.", object['dn'], objectGUID)
return False
try:
entryUUID = object['attributes']['entryUUID'][0].decode('ASCII')
except KeyError:
entryUUID = None # may be empty for back_mapped_subobject for leaf object delete_in_s4
#
# ADD
#
if not ad_object and object['modtype'] in ('add', 'modify', 'move'):
log.debug("sync_from_ucs: add object: %s", object['dn'])
log.debug("sync_from_ucs: lock UCS entryUUID: %s", entryUUID)
if entryUUID and not self.lockingdb.is_ucs_locked(entryUUID):
self.lockingdb.lock_ucs(entryUUID)
self.addToCreationList(object['dn'])
if hasattr(self.property[property_type], "con_sync_function"):
self.property[property_type].con_sync_function(self, property_type, object)
else:
# objectClass
if self.property[property_type].con_create_objectclass:
addlist.append(('objectClass', [x.encode('UTF-8') for x in self.property[property_type].con_create_objectclass]))
# fixed Attributes
if self.property[property_type].con_create_attributes:
addlist += self.property[property_type].con_create_attributes
# Copy the LDAP controls, because they may be modified
# in an ucs_create_extensions
ctrls = copy.deepcopy(self.serverctrls_for_add_and_modify)
if hasattr(self.property[property_type], 'attributes') and self.property[property_type].attributes is not None:
for attr, value in object['attributes'].items():
for attr_key in self.property[property_type].attributes.keys():
attribute = self.property[property_type].attributes[attr_key]
if attr in (attribute.con_attribute, attribute.con_other_attribute):
addlist.append((attr, value))
if hasattr(self.property[property_type], 'con_create_extensions') and self.property[property_type].con_create_extensions is not None:
for con_create_extension in self.property[property_type].con_create_extensions:
log.debug("Call con_create_extensions: %s", con_create_extension)
con_create_extension(self, property_type, object, addlist, ctrls)
if hasattr(self.property[property_type], 'post_attributes') and self.property[property_type].post_attributes is not None:
for attr, value in object['attributes'].items():
for attr_key in self.property[property_type].post_attributes.keys():
post_attribute = self.property[property_type].post_attributes[attr_key]
if post_attribute.reverse_attribute_check and not object['attributes'].get(post_attribute.ldap_attribute):
continue
if attr not in (post_attribute.con_attribute, post_attribute.con_other_attribute):
continue
if value:
modlist.append((ldap.MOD_REPLACE, attr, value))
log.debug("to add: %s", object['dn'])
log.trace("sync_from_ucs: addlist: %s", addlist)
try:
self.lo_s4.lo.add_ext_s(object['dn'], addlist, serverctrls=ctrls)
except (ldap.ALREADY_EXISTS, ldap.CONSTRAINT_VIOLATION):
sAMAccountName = object['attributes'].get('sAMAccountName', [b''])[0]
sambaSID = object['attributes'].get('sambaSID', [b''])[0]
if not (sAMAccountName and sambaSID):
raise # unknown situation, raise original traceback
filter_s4 = format_escaped('(&(sAMAccountName={0!e})(objectSid={1!e})(isDeleted=TRUE))', sAMAccountName.decode('UTF-8'), sambaSID.decode('UTF-8'))
log.process("sync_from_ucs: error during add, searching for conflicting deleted object in S4")
log.debug("sync_from_ucs: search filter: %s", filter_s4)
result = self.s4_search_ext_s(self.lo_s4.base, ldap.SCOPE_SUBTREE, filter_s4, ['dn'], serverctrls=[LDAPControl(
LDAP_SERVER_SHOW_DELETED_OID, criticality=1), LDAPControl(LDB_CONTROL_DOMAIN_SCOPE_OID, criticality=0)])
if not result or len(result) > 1: # the latter would indicate corruption
log.process("sync_from_ucs: no conflicting deleted object found")
raise # unknown situation, raise original traceback
log.process("sync_from_ucs: reanimating conflicting object: %s", result[0][0])
reanimate_modlist = [
(ldap.MOD_DELETE, 'isDeleted', None),
(ldap.MOD_REPLACE, 'distinguishedName', object['dn'].encode('UTF-8')),
]
self.lo_s4.lo.modify_ext_s(result[0][0], reanimate_modlist, serverctrls=[LDAPControl(LDAP_SERVER_SHOW_DELETED_OID, criticality=1)])
# and try the sync again
return self.sync_from_ucs(property_type, object, pre_mapped_ucs_dn, old_dn, old_ucs_object, new_ucs_object)
except Exception:
log.error("sync_from_ucs: traceback during add object: %s", object['dn'])
log.error("sync_from_ucs: traceback due to addlist: %s", addlist)
raise
# TODO: move the following into a PostReadControl
objectGUID = self._get_objectGUID(object['dn'])
self.update_add_cache_after_creation(entryUUID, objectGUID)
if property_type == 'group':
self.group_members_cache_con[object['dn'].lower()] = set()
log.debug("group_members_cache_con[%s]: {}", object['dn'].lower())
if hasattr(self.property[property_type], "post_con_create_functions"):
for post_con_create_function in self.property[property_type].post_con_create_functions:
log.debug("Call post_con_create_functions: %s", post_con_create_function)
post_con_create_function(self, property_type, object)
log.debug("and modify: %s", object['dn'])
if modlist:
log.trace("sync_from_ucs: modlist: %s", modlist)
try:
self.lo_s4.lo.modify_ext_s(object['dn'], modlist, serverctrls=ctrls)
except Exception:
log.error("sync_from_ucs: traceback during modify object: %s", object['dn'])
log.error("sync_from_ucs: traceback due to modlist: %s", modlist)
raise
if hasattr(self.property[property_type], "post_con_modify_functions"):
for post_con_modify_function in self.property[property_type].post_con_modify_functions:
log.debug("Call post_con_modify_functions: %s", post_con_modify_function)
post_con_modify_function(self, property_type, object)
log.debug("Call post_con_modify_functions: %s (done)", post_con_modify_function)
#
# MODIFY
#
elif ad_object and object['modtype'] in ('add', 'modify', 'move'):
log.debug("sync_from_ucs: modify object: %s", object['dn'])
log.debug("sync_from_ucs: old_object: %s", old_ucs_object)
log.debug("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
log.debug("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
log.debug("sync_from_ucs: Found a corresponding mapping definition: %s", attribute)
s4_attribute = attribute_type[attribute].con_attribute
s4_other_attribute = attribute_type[attribute].con_other_attribute
if attribute_type[attribute].sync_mode not in ['write', 'sync']:
log.debug("sync_from_ucs: %s is in not in write or sync mode. Skipping", attribute)
continue
# Get the UCS attributes
old_values = set(old_ucs_object.get(attr, []))
new_values = set(new_ucs_object.get(attr, []))
log.debug("sync_from_ucs: %s old_values: %s", attr, old_values)
log.debug("sync_from_ucs: %s new_values: %s", attr, new_values)
if attribute_type[attribute].compare_function(list(old_values), list(new_values)):
log.debug("sync_from_ucs: no modification necessary for %s", attribute)
continue
# 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
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 ad_object.items() if s4_attribute.lower() == k.lower()][0]) # noqa: RUF015
except IndexError:
current_s4_values = set()
log.debug("sync_from_ucs: The current S4 values: %s", current_s4_values)
try:
current_s4_other_values = set([v for k, v in ad_object.items() if s4_other_attribute.lower() == k.lower()][0]) # noqa: RUF015
except IndexError:
current_s4_other_values = set()
log.debug("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 - {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, list(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, list(new_s4_other_values)))
else:
try:
current_s4_values = set([v for k, v in ad_object.items() if s4_attribute.lower() == k.lower()][0]) # noqa: RUF015
except IndexError:
current_s4_values = set()
log.debug("sync_from_ucs: The current S4 values: %s", current_s4_values)
has_mapping_function = (
hasattr(attribute_type[attribute], 'mapping') and len(attribute_type[attribute].mapping) > 0 and attribute_type[attribute].mapping[0]
)
if (to_add or to_remove) and (attribute_type[attribute].single_value or has_mapping_function):
modified = (not current_s4_values or not value) or not attribute_type[attribute].compare_function(list(current_s4_values), list(value))
if modified:
if has_mapping_function:
log.process("Calling value mapping function for attribute %s", attribute)
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 attribute_type[attribute].compare_function:
for _value in to_remove:
for org in current_s4_values:
if attribute_type[attribute].compare_function([_value], [org]): # values are equal
r.add(org)
if r:
modlist.append((ldap.MOD_DELETE, s4_attribute, list(r)))
if to_add:
to_really_add = copy.copy(to_add)
if attribute_type[attribute].compare_function:
for _value in to_add:
for org in current_s4_values:
if attribute_type[attribute].compare_function([_value], [org]): # values are equal
to_really_add.discard(_value)
to_add = to_really_add
a = to_add - current_s4_values
if a:
modlist.append((ldap.MOD_ADD, s4_attribute, list(a)))
if not modlist:
log.trace("nothing to modify: %s", object['dn'])
else:
log.debug("to modify: %s", object['dn'])
log.trace("sync_from_ucs: modlist: %s", modlist)
try:
self.lo_s4.lo.modify_ext_s(object['dn'], modlist, serverctrls=self.serverctrls_for_add_and_modify)
except Exception:
log.error("sync_from_ucs: traceback during modify object: %s", object['dn'])
log.error("sync_from_ucs: traceback due to modlist: %s", modlist)
raise
if hasattr(self.property[property_type], "post_con_modify_functions"):
for post_con_modify_function in self.property[property_type].post_con_modify_functions:
log.debug("Call post_con_modify_functions: %s", post_con_modify_function)
post_con_modify_function(self, property_type, object)
log.debug("Call post_con_modify_functions: %s (done)", post_con_modify_function)
#
# DELETE
#
elif object['modtype'] == 'delete':
if hasattr(self.property[property_type], "con_sync_function"):
self.property[property_type].con_sync_function(self, property_type, object)
else:
self.delete_in_s4(object, property_type)
# update group cache
self._remove_dn_from_group_cache(con_dn=object['dn'], ucs_dn=pre_mapped_ucs_dn)
self._update_group_member_cache(remove_con_dn=object['dn'].lower(), remove_ucs_dn=pre_mapped_ucs_dn.lower())
else:
log.warning("unknown modtype (%s : %s)", object['dn'], object['modtype'])
return False
log.debug("sync_from_ucs: unlock UCS entryUUID: %s", entryUUID)
if entryUUID:
self.lockingdb.unlock_ucs(entryUUID)
self._check_dn_mapping(pre_mapped_ucs_dn, object['dn'])
log.trace("sync from ucs return True")
return True # FIXME: return correct False if sync fails
def _get_objectGUID(self, dn):
try:
ad_object = self.get_object(dn, ['objectGUID'])
return univention.s4connector.decode_guid(ad_object['objectGUID'][0])
except (KeyError, Exception): # FIXME: catch only necessary exceptions
log.warning("Failed to search objectGUID for %s", dn)
return ''
[docs]
def delete_in_s4(self, object, property_type):
log.trace("delete: %s", object['dn'])
log.trace("delete_in_s4: %s", object)
try:
objectGUID = self._get_objectGUID(object['dn'])
self.lo_s4.lo.delete_s(object['dn'])
except ldap.NO_SUCH_OBJECT:
pass # object already deleted
except ldap.NOT_ALLOWED_ON_NONLEAF:
log.debug("remove object from AD failed, need to delete subtree")
if self._remove_subtree_in_s4(object, property_type):
# FIXME: endless recursion if there is one subtree-object which is ignored, not identifyable or can't be removed.
return self.delete_in_s4(object, property_type)
return False
entryUUID = object.get('attributes').get('entryUUID', [b''])[0].decode('ASCII')
if entryUUID:
self.update_deleted_cache_after_removal(entryUUID, objectGUID)
else:
log.debug("delete_in_s4: Object without entryUUID: %s", object['dn'])
self.remove_add_cache_after_removal(entryUUID)
def _remove_subtree_in_s4(self, parent_ad_object, property_type):
if self.property[property_type].con_subtree_delete_objects:
_l = [f"({x})" for x in self.property[property_type].con_subtree_delete_objects]
allow_delete_filter = "(|{})".format(''.join(_l))
for sub_dn, _ in self.s4_search_ext_s(parent_ad_object['dn'], ldap.SCOPE_SUBTREE, allow_delete_filter):
if self.lo.compare_dn(sub_dn.lower(), parent_ad_object['dn'].lower()): # FIXME: remove and search with scope=children instead
continue
log.debug("delete: %r", sub_dn)
self.lo_s4.lo.delete_s(sub_dn)
for subdn, subattr in self.s4_search_ext_s(parent_ad_object['dn'], ldap.SCOPE_SUBTREE, 'objectClass=*'):
if self.lo.compare_dn(subdn.lower(), parent_ad_object['dn'].lower()): # FIXME: remove and search with scope=children instead
continue
log.debug("delete: %r", subdn)
subobject_s4 = {'dn': subdn, 'modtype': 'delete', 'attributes': subattr}
key = self.__identify_s4_type(subobject_s4)
back_mapped_subobject = self._object_mapping(key, subobject_s4)
log.warning("delete subobject: %r", back_mapped_subobject['dn'])
if not self._ignore_object(key, back_mapped_subobject):
# FIXME: this call is wrong!: sync_from_ucs() must be called with a ucs_object not with a ad_object!
if not self.sync_from_ucs(key, subobject_s4, back_mapped_subobject['dn']):
log.warning("delete of subobject failed: %r", subdn)
return False
return True