#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention S4 Connector
# Basic class for the AD connector part
#
# Copyright 2004-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.
from __future__ import print_function
import os
import copy
import re
import sys
import time
import calendar
import string
import base64
import six
from six.moves import urllib_parse
import ldap
from ldap.controls import LDAPControl
from ldap.controls import SimplePagedResultsControl
from ldap.filter import escape_filter_chars
from samba.dcerpc import security
from samba.ndr import ndr_pack, ndr_unpack
from univention.config_registry import ConfigRegistry
import univention.uldap
import univention.s4connector
import univention.debug2 as ud
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')
ud.debug(ud.LDAP, ud.INFO, '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:
ud.debug(ud.LDAP, ud.WARN, '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
ud.debug(ud.LDAP, ud.INFO, '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
ud.debug(ud.LDAP, ud.INFO, "groupType: %s" % groupType)
if __is_groupType_local(groupType):
serverctrls.append(LDAPControl(LDB_CONTROL_RELAX_OID, criticality=0))
sambaSID = object['attributes']['sambaSID'][0].decode('ASCII')
ud.debug(ud.LDAP, ud.INFO, "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 = u''
dn_attr_val = u''
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]
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]):
ud.debug(ud.LDAP, ud.ALL, "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 = u''
if object.get('attributes'):
t_samaccount = object['attributes'].get('sAMAccountName', [b''])[0].decode('UTF-8')
if rdn_value.lower() == t_samaccount.lower():
ud.debug(ud.LDAP, ud.ALL, "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:
ud.debug(ud.LDAP, ud.ALL, "samaccount_dn_mapping: premapped AD object found")
return True
else:
ud.debug(ud.LDAP, ud.ALL, "samaccount_dn_mapping: premapped AD object not found")
return False
else:
if connector.get_ucs_ldap_object(object[dn_key]) is not None:
ud.debug(ud.LDAP, ud.ALL, "samaccount_dn_mapping: premapped UCS object found")
return True
else:
ud.debug(ud.LDAP, ud.ALL, "samaccount_dn_mapping: premapped UCS object not found")
return False
for dn_key in ['dn', 'olddn']:
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.ALL, "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 == u"Printer-Admins": # Also look for the original name (Bug #42675#c1)
alternative_samaccountnames.append(ucsval)
value = conval
ud.debug(ud.LDAP, ud.ALL, "samaccount_dn_mapping: map %s according to mapping-table" % (propertyattrib,))
break
else:
if propertyattrib in connector.property[propertyname].mapping_table:
ud.debug(ud.LDAP, ud.ALL, "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(u'(|{})'.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 = u'(&{})'.format(''.join(filter_parts_ad))
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.ALL, "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')
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.ALL, "samaccount_dn_mapping: map samaccountanme according to mapping-table")
break
else:
if propertyattrib in connector.property[propertyname].mapping_table:
ud.debug(ud.LDAP, ud.ALL, "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 = ''
ud.debug(ud.LDAP, ud.ALL, "samaccount_dn_mapping: samaccountname is: %r" % (samaccountname,))
ucsdn_filter = format_escaped(u'(&(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
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.INFO, "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', u'samAccountName', u'posixAccount', 'uid', u'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', u'cn', u'posixGroup', 'cn', u'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', u'samAccountName', u'posixAccount', 'uid', u'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', u'samAccountName', u'posixAccount', 'uid', u'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):
if sid_list1 == sid_list2:
return True
else:
return False
# SID comparison
len_sid_list1 = len(sid_list1)
if len_sid_list1 != len(sid_list2):
return False
for i in range(0, 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 = '/etc/univention/%s/s4/localmapping.py' % configbasename
s4_mapping = univention.s4connector.s4.mapping.load_localmapping(MAPPING_FILENAME)
_ucr = dict(ucr)
try:
ad_ldap_host = _ucr['%s/s4/ldap/host' % configbasename]
ad_ldap_port = _ucr['%s/s4/ldap/port' % configbasename]
ad_ldap_base = _ucr['%s/s4/ldap/base' % configbasename]
ad_ldap_binddn = _ucr.get('%s/s4/ldap/binddn' % configbasename, None)
ad_ldap_certificate = _ucr.get('%s/s4/ldap/certificate' % configbasename)
if not ad_ldap_certificate and ucr.is_true('%s/s4/ldap/ssl' % configbasename):
raise KeyError('%s/s4/ldap/certificate' % configbasename)
listener_dir = _ucr['%s/s4/listener/dir' % configbasename]
except KeyError as exc:
raise SystemExit('UCR variable %s is not set' % (exc,))
ad_ldap_bindpw = None
if ucr.get('%s/s4/ldap/bindpw' % configbasename) and os.path.exists(ucr['%s/s4/ldap/bindpw' % configbasename]):
with open(ucr['%s/s4/ldap/bindpw' % configbasename]) 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'):
ud.debug(ud.LDAP, ud.INFO, "__init__: init add config section 'S4'")
self.config.add_section('S4')
if not self.config.has_section('S4 rejected'):
ud.debug(ud.LDAP, ud.INFO, "__init__: init add config section 'S4 rejected'")
self.config.add_section('S4 rejected')
if not self.config.has_option('S4', 'lastUSN'):
ud.debug(ud.LDAP, ud.INFO, "__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'):
ud.debug(ud.LDAP, ud.INFO, "__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(s4, self).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):
ud.debug(ud.LDAP, ud.PROCESS, 'Building internal group membership cache')
s4_groups = self.__search_s4(filter='objectClass=group', attrlist=['member'])
ud.debug(ud.LDAP, ud.ALL, "__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)
ud.debug(ud.LDAP, ud.ALL, "__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())
ud.debug(ud.LDAP, ud.ALL, "__init__: self.group_members_cache_ucs: %s" % self.group_members_cache_ucs)
ud.debug(ud.LDAP, ud.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 '%s/s4/ldap/ssl' % self.CONFIGBASENAME in self.configRegistry and self.configRegistry['%s/s4/ldap/ssl' % self.CONFIGBASENAME] == "no":
ud.debug(ud.LDAP, ud.INFO, "__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('%s/s4/ldap/protocol' % self.CONFIGBASENAME, 'ldap').lower()
if protocol == 'ldapi':
socket = urllib_parse.quote(self.configRegistry.get('%s/s4/ldap/socket' % self.CONFIGBASENAME, ''), '')
ldapuri = "%s://%s" % (protocol, socket)
else:
ldapuri = "%s://%s:%d" % (protocol, self.configRegistry['%s/s4/ldap/host' % self.CONFIGBASENAME], int(self.configRegistry['%s/s4/ldap/port' % self.CONFIGBASENAME]))
# 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='', binddn=None, bindpw=None, start_tls=tls_mode,
ca_certfile=self.s4_ldap_certificate,
uri=ldapuri, reconnect=False
)
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
self._debug_traceback(ud.ERROR, '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, "DC=DomainDnsZones,%s" % self.s4_ldap_base, "DC=ForestDnsZones,%s" % 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):
ud.debug(ud.LDAP, ud.INFO, "_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 not dn.lower() 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]
ud.debug(ud.LDAP, ud.INFO, "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?
self._debug_traceback(ud.ERROR, '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)
ud.debug(ud.LDAP, ud.INFO, "value_range_retrieval: response: %s" % (key,))
if lower != 0:
ud.debug(ud.LDAP, ud.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:
ud.debug(ud.LDAP, ud.ERROR, "value_range_retrieval: invalid range retrieval response: asked for %s but got %s" % (next_key, key))
raise ldap.PARTIAL_RESULTS
ud.debug(ud.LDAP, ud.INFO, "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]
ud.debug(ud.LDAP, ud.INFO, "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?
self._debug_traceback(ud.ERROR, '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))
ud.debug(ud.LDAP, ud.ALL, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.WARN, "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 = '(&(%s)(%s))' % (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 = '(|%s%s)' % (_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
ud.debug(ud.LDAP, ud.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
ud.debug(ud.LDAP, ud.INFO, "__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 = '(|%s%s)' % (_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 = '(&({}){})'.format(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:
ud.debug(ud.LDAP, ud.INFO, "__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:
ud.debug(ud.LDAP, ud.WARN, '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])
ud.debug(ud.LDAP, ud.INFO, "object_from_element: olddn: %s" % olddn)
if olddn and not olddn.lower() == element[0].lower() and ldap.explode_rdn(olddn.lower()) == ldap.explode_rdn(element[0].lower()):
object['modtype'] = 'move'
object['olddn'] = olddn
ud.debug(ud.LDAP, ud.INFO, "object_from_element: detected move of AD-Object")
else:
object['modtype'] = 'modify'
if olddn and not 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)
ud.debug(ud.LDAP, ud.INFO, "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:
self._debug_traceback(ud.ERROR, "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 not s4_group_rid_resultlist[0][0] in [b'None', b'', None]:
s4_group_rid = s4_group_rid_resultlist[0][1]['primaryGroupID'][0].decode('UTF-8')
ud.debug(ud.LDAP, ud.INFO, "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]:
ud.debug(ud.LDAP, ud.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:
ud.debug(ud.LDAP, ud.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:
ud.debug(ud.LDAP, ud.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 == []:
ud.debug(ud.LDAP, ud.WARN, "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:
ud.debug(ud.LDAP, ud.INFO, "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'])
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.INFO, "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')
ud.debug(ud.LDAP, ud.INFO, "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]})
ud.debug(ud.LDAP, ud.INFO, "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 not 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()
ud.debug(ud.LDAP, ud.INFO, "primary_group_sync_to_ucs: changed primary Group in ucs")
else:
ud.debug(ud.LDAP, ud.INFO, "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
"""
ud.debug(ud.LDAP, ud.ALL, "object_memberships_sync_from_ucs: object: %s" % object)
if 'group' in self.property:
if getattr(self.property['group'], 'sync_mode', '') in ['read', 'none']:
ud.debug(ud.LDAP, ud.INFO, "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 == []:
ud.debug(ud.LDAP, ud.INFO, "object_memberships_sync_from_ucs: No group-memberships in UCS for %s" % object['dn'])
return
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "__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
"""
ud.debug(ud.LDAP, ud.INFO, "group_members_sync_from_ucs: %s" % object)
object_key = key
object_ucs = self._object_mapping(object_key, object)
object_ucs_dn = object_ucs['dn']
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.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 = set(x.decode('UTF-8') for x in ldap_object_ucs.get('uniqueMember', []))
ud.debug(ud.LDAP, ud.INFO, "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())
ud.debug(ud.LDAP, ud.INFO, "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()
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.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))
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.WARN, "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())
ud.debug(ud.LDAP, ud.INFO, "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?
self._debug_traceback(ud.PROCESS, "group_members_sync_from_ucs: failed to get S4 dn for UCS group member %s, assume object doesn't exist" % member_dn)
ud.debug(ud.LDAP, ud.INFO, "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:
if not member_dn.lower() 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.append(member_dn.lower())
ud.debug(ud.LDAP, ud.INFO, "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())
ud.debug(ud.LDAP, ud.INFO, "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?
self._debug_traceback(ud.PROCESS, "group_members_sync_from_ucs: failed to get UCS dn for S4 group member %s" % member_dn)
ud.debug(ud.LDAP, ud.INFO, "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)
ud.debug(ud.LDAP, ud.INFO, "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()
ud.debug(ud.LDAP, ud.INFO, "group_members_sync_from_ucs: members to add initialized: %s" % add_members)
for member_dn in s4_members:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "group_members_sync_from_ucs: Yes")
add_members.remove(member_dn_lower)
else:
if object['modtype'] == 'add':
ud.debug(ud.LDAP, ud.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
ud.debug(ud.LDAP, ud.INFO, "group_members_sync_from_ucs: No")
del_members.add(member_dn)
else:
ud.debug(ud.LDAP, ud.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()))
ud.debug(ud.LDAP, ud.INFO, "group_members_sync_from_ucs: members to add: %s" % add_members)
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.INFO, "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
# ud.debug(ud.LDAP, ud.INFO, "object_memberships_sync_to_ucs: object: %s" % object)
if 'group' in self.property:
if getattr(self.property['group'], 'sync_mode', '') in ['write', 'none']:
self.context_log(key, object, "ignored group memberships sync: group sync_mode is write", level=ud.INFO, 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}
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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, dn, dn_list):
"""
Checks if dn is in dn_list
"""
for d in dn_list:
if dn.lower() == d.lower():
return True
return False
[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(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:
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.INFO, "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(object['dn'].encode('UTF-8'), s4_group_object['attributes'].get('member', [])):
ml.append((ldap.MOD_ADD, 'member', [object['dn'].encode('UTF-8')]))
if ml:
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "__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
"""
ud.debug(ud.LDAP, ud.INFO, "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']
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.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(object['dn'], ldap_object_s4)
ud.debug(ud.LDAP, ud.INFO, "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)
ud.debug(ud.LDAP, ud.INFO, "group_members_sync_to_ucs: clean s4_members %s" % s4_members)
self.group_members_cache_con[ad_object_dn.lower()] = set()
ud.debug(ud.LDAP, ud.INFO, "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 = set(x.decode('UTF-8') for x in ldap_object_ucs.get('uniqueMember', []))
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.WARN, "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}):
ud.debug(ud.LDAP, ud.INFO, "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']
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "Failed to find %s via self.lo.get" % ucs_dn)
except ldap.SERVER_DOWN:
raise
except Exception: # FIXME: which exception is to be caught?
self._debug_traceback(ud.PROCESS, "group_members_sync_to_ucs: failed to get UCS dn for S4 group member %s, assume object doesn't exist" % member_dn)
# 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
ud.debug(ud.LDAP, ud.INFO, "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?
self._debug_traceback(ud.PROCESS, "group_members_sync_to_ucs: failed to get AD dn for UCS group member %s" % member_dn)
ud.debug(ud.LDAP, ud.INFO, "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': []}
ud.debug(ud.LDAP, ud.INFO, "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'?
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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()))
ud.debug(ud.LDAP, ud.INFO, "group_members_sync_to_ucs: members to add: %s" % add_members)
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.WARN, "Ignore already removed object %s." % (exc,))
return
ucs_admin_object.open()
modlist = []
ud.debug(ud.LDAP, ud.INFO, "Disabled state: %s" % ucs_admin_object['disabled'].lower())
if not (ucs_admin_object['disabled'].lower() 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]) != int(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:
ud.debug(ud.LDAP, ud.ALL, "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 not ucs_admin_object['disabled'].lower() 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]) == int(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
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.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()
ud.debug(ud.LDAP, ud.INFO, "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("Sync %s rejected changes from S4 to UCS" % len(rejected))
sys.stdout.flush()
for change_usn, dn in rejected:
ud.debug(ud.LDAP, ud.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]:
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.WARN, "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?
self._debug_traceback(ud.ERROR, "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:
self._debug_traceback(ud.ERROR, "unexpected Error during s4.resync_rejected")
print("restored %s rejected changes" % change_count)
print("--------------------------------------")
sys.stdout.flush()
[docs] def poll(self, show_deleted=True):
'''
poll for changes in AD
'''
# search from last_usn for changes
ud.debug(ud.LDAP, ud.INFO, "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?
self._debug_traceback(ud.WARN, "Exception during search_s4_changes")
print("--------------------------------------")
print("try to sync %s changes from S4" % len(changes))
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:
ud.debug(ud.LDAP, ud.INFO, "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:
self.context_log(property_key, ad_object, 'ignoring not identified object', level=ud.INFO)
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':
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.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:
ud.debug(ud.LDAP, ud.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?
self._debug_traceback(ud.WARN, "Exception during poll/sync_to_ucs")
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?
self._debug_traceback(ud.WARN, "Exception during set_DN_for_GUID")
else:
self.context_log(property_key, ad_object, 'sync was not successful, save rejected', level=ud.INFO)
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("Changes from S4: %s (%s saved rejected)" % (change_count, len(rejected)))
print("--------------------------------------")
sys.stdout.flush()
return change_count
def __has_attribute_value_changed(self, attribute, old_ucs_object, new_ucs_object):
return not 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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: %s was not present in S4 group member mapping cache" % con_dn)
pass
if ucs_dn:
try:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: %s was not present in UCS group member mapping cache" % ucs_dn)
pass
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]:
ud.debug(ud.LDAP, ud.INFO, "_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]:
ud.debug(ud.LDAP, ud.INFO, "_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]:
ud.debug(ud.LDAP, ud.INFO, "_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]:
ud.debug(ud.LDAP, ud.INFO, "_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
ud.debug(ud.LDAP, ud.INFO, "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']:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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())
ud.debug(ud.LDAP, ud.INFO, "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'])
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):
ud.debug(ud.LDAP, ud.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'):
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: add object: %s" % object['dn'])
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
if 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))
ud.debug(ud.LDAP, ud.INFO, "to add: %s" % object['dn'])
ud.debug(ud.LDAP, ud.ALL, "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(u'(&(sAMAccountName={0!e})(objectSid={1!e})(isDeleted=TRUE))', sAMAccountName.decode('UTF-8'), sambaSID.decode('UTF-8'))
ud.debug(ud.LDAP, ud.PROCESS, "sync_from_ucs: error during add, searching for conflicting deleted object in S4")
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.PROCESS, "sync_from_ucs: no conflicting deleted object found")
raise # unknown situation, raise original traceback
ud.debug(ud.LDAP, ud.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:
ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback during add object: %s" % object['dn'])
ud.debug(ud.LDAP, ud.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()
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "Call post_con_create_functions: %s" % post_con_create_function)
post_con_create_function(self, property_type, object)
ud.debug(ud.LDAP, ud.INFO, "and modify: %s" % object['dn'])
if modlist:
ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: modlist: %s" % modlist)
try:
self.lo_s4.lo.modify_ext_s(object['dn'], modlist, serverctrls=ctrls)
except Exception:
ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback during modify object: %s" % object['dn'])
ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback due to modlist: %s" % modlist)
raise
if hasattr(self.property[property_type], "post_con_modify_functions"):
for post_con_modify_function in self.property[property_type].post_con_modify_functions:
ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s" % post_con_modify_function)
post_con_modify_function(self, property_type, object)
ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s (done)" % post_con_modify_function)
#
# MODIFY
#
elif ad_object and object['modtype'] in ('add', 'modify', 'move'):
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: modify object: %s" % object['dn'])
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: old_object: %s" % old_ucs_object)
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: new_object: %s" % new_ucs_object)
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
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The following attribute has been changed: %s" % attr)
for attribute in attribute_type.keys():
if attribute_type[attribute].ldap_attribute != attr:
continue
ud.debug(ud.LDAP, ud.INFO, "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']:
ud.debug(ud.LDAP, ud.INFO, "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, []))
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: %s old_values: %s" % (attr, old_values))
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: %s new_values: %s" % (attr, new_values))
if attribute_type[attribute].compare_function(list(old_values), list(new_values)):
ud.debug(ud.LDAP, ud.INFO, "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])
except IndexError:
current_s4_values = set([])
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 values: %s" % current_s4_values)
try:
current_s4_other_values = set([v for k, v in ad_object.items() if s4_other_attribute.lower() == k.lower()][0])
except IndexError:
current_s4_other_values = set([])
ud.debug(ud.LDAP, ud.INFO, "sync_from_ucs: The current S4 other values: %s" % current_s4_other_values)
new_s4_values = current_s4_values - to_remove
if not new_s4_values and to_add:
for n_value in new_ucs_object.get(attr, []):
if n_value in to_add:
to_add = to_add - set([n_value])
new_s4_values = [n_value]
break
new_s4_other_values = (current_s4_other_values | to_add) - to_remove - current_s4_values
if current_s4_values != new_s4_values:
if new_s4_values:
modlist.append((ldap.MOD_REPLACE, s4_attribute, 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])
except IndexError:
current_s4_values = set([])
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.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:
ud.debug(ud.LDAP, ud.ALL, "nothing to modify: %s" % object['dn'])
else:
ud.debug(ud.LDAP, ud.INFO, "to modify: %s" % object['dn'])
ud.debug(ud.LDAP, ud.ALL, "sync_from_ucs: modlist: %s" % modlist)
try:
self.lo_s4.lo.modify_ext_s(object['dn'], modlist, serverctrls=self.serverctrls_for_add_and_modify)
except Exception:
ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback during modify object: %s" % object['dn'])
ud.debug(ud.LDAP, ud.ERROR, "sync_from_ucs: traceback due to modlist: %s" % modlist)
raise
if hasattr(self.property[property_type], "post_con_modify_functions"):
for post_con_modify_function in self.property[property_type].post_con_modify_functions:
ud.debug(ud.LDAP, ud.INFO, "Call post_con_modify_functions: %s" % post_con_modify_function)
post_con_modify_function(self, property_type, object)
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.WARN, "unknown modtype (%s : %s)" % (object['dn'], object['modtype']))
return False
ud.debug(ud.LDAP, ud.INFO, "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'])
ud.debug(ud.LDAP, ud.ALL, "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
ud.debug(ud.LDAP, ud.WARN, "Failed to search objectGUID for %s" % dn)
return ''
[docs] def delete_in_s4(self, object, property_type):
ud.debug(ud.LDAP, ud.ALL, "delete: %s" % object['dn'])
ud.debug(ud.LDAP, ud.ALL, "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:
ud.debug(ud.LDAP, ud.INFO, "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:
ud.debug(ud.LDAP, ud.INFO, "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 = ["(%s)" % x for x in self.property[property_type].con_subtree_delete_objects]
allow_delete_filter = "(|%s)" % ''.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
ud.debug(ud.LDAP, ud.INFO, "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
ud.debug(ud.LDAP, ud.INFO, "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)
ud.debug(ud.LDAP, ud.WARN, "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']):
ud.debug(ud.LDAP, ud.WARN, "delete of subobject failed: %r" % (subdn,))
return False
return True