def test_copy_case_insensitive_dict(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 cid2 = cid.copy() self.assertEqual(cid2['ONE'], 1) self.assertEqual(cid2['one'], 1) self.assertEqual(cid2['TWO'], 2) self.assertEqual(cid2['two'], 2) self.assertEqual(cid2[3], 3)
def test_copy_case_insensitive_dict(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 cid2 = cid.copy() self.assertEqual(cid2['ONE'], 1) self.assertEqual(cid2['one'], 1) self.assertEqual(cid2['TWO'], 2) self.assertEqual(cid2['two'], 2) self.assertEqual(cid2[3], 3)
def test_equality_case_insensitive_dict_with_different_case(self): cid = CaseInsensitiveDict() cid['one'] = 1 cid['two'] = 2 cid[3] = 3 cid2 = CaseInsensitiveDict() cid2['ONE'] = 1 cid2['TWO'] = 2 cid2[3] = 3 self.assertEqual(cid, cid2)
def test_preserve_key_case_case_insensitive_dict(self): cid = CaseInsensitiveDict() cid['One'] = 1 cid['Two'] = 2 cid[3] = 3 key_list = list(cid.keys()) self.assertTrue('One' in key_list) self.assertTrue('Two' in key_list) self.assertTrue(3 in key_list) self.assertFalse('ONE' in key_list) self.assertFalse('one' in key_list) self.assertFalse('TWO' in key_list) self.assertFalse('TWO' in key_list) self.assertFalse(4 in key_list)
def test_preserve_key_case_case_insensitive_dict(self): cid = CaseInsensitiveDict() cid['One'] = 1 cid['Two'] = 2 cid[3] = 3 key_list = list(cid.keys()) self.assertTrue('One' in key_list) self.assertTrue('Two' in key_list) self.assertTrue(3 in key_list) self.assertFalse('ONE' in key_list) self.assertFalse('one' in key_list) self.assertFalse('TWO' in key_list) self.assertFalse('TWO' in key_list) self.assertFalse(4 in key_list)
def test_delete_item_in_case_insentitive_dict_different_case_key(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 self.assertEqual(cid['ONE'], 1) self.assertEqual(cid['oNe'], 1) del cid['one'] self.assertEqual(cid['TWO'], 2) self.assertEqual(cid['two'], 2) self.assertEqual(cid[3], 3) try: cid['oNe'] except KeyError: self.assertTrue(True) except Exception: self.assertTrue(False) else: self.fail('key still present') try: cid['ONE'] except KeyError: self.assertTrue(True) except Exception: self.assertTrue(False) else: self.fail('key still present')
def increment_attr(conn, dn, attr, incr=1): # optimization: RFC 4525 Modify-Increment + RFC 4527 Post-Read if _has_control(conn, LDAP_CONTROL_POSTREAD) and \ _has_feature(conn, LDAP_FEATURE_MODIFY_INCREMENT): # this is far uglier than the Perl version already postread_ctrl = ldap3.protocol.rfc4527.post_read_control([attr]) res = conn.modify(dn, {attr: [(ldap3.MODIFY_INCREMENT, incr)]}, controls=[postread_ctrl]) if not res: raise Exception("modify-increment failed", conn.result) res = conn.result["controls"][LDAP_CONTROL_POSTREAD]["value"]["result"] res = CaseInsensitiveDict(res) return res[attr][0] done = False wait = 0 while not done: val = read_attr(conn, dn, attr) val = int(val[0]) if val else 0 # _cas_attr done = conn.modify(dn, { attr: [(ldap3.MODIFY_DELETE, val), (ldap3.MODIFY_ADD, val + incr)] }) if not done: Core.debug("modify failed: %r", conn.result) wait += 1 time.usleep(0.05 * 2**int(rand(wait))) return val + incr
def op_adconnect(self, opseq, optotal): self.stepmsg("Conecting to the AD", opseq, optotal) self.substepmsg("connecting to the first available LDAP server") try: servers = [ ldap3.Server(host=server, get_info=ldap3.ALL) for server in self.cfg.get("adsync", "host").split() ] random.shuffle(servers) server_pool = ldap3.ServerPool(servers, pool_strategy=ldap3.FIRST, active=1) user = self.cfg.get("adsync", "user") password = self.cfg.get("adsync", "password") self.lconn = ldap3.Connection(server=server_pool, user=user, password=password, raise_exceptions=True, auto_bind=ldap3.AUTO_BIND_NO_TLS, return_empty_attributes=True) except configparser.Error: self.handle_cfg_exception(sys.exc_info()) except LDAPException: self.handle_ldap_exception(sys.exc_info()) self.substepmsg("processing root DSE") rootDSE_keys = [ "defaultNamingContext", "configurationNamingContext", "domainFunctionality", "serverName", "dnsHostName" ] rootDSE_values = [ x[0] if isinstance(x, list) else x for x in map( lambda var: self.lconn.server.info.other[var], rootDSE_keys) ] self.rootDSE = CaseInsensitiveDict(zip(rootDSE_keys, rootDSE_values))
def test_len_case_insentitive_dict(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 self.assertEqual(len(cid), 3) cid['ONE'] = 'ONE' self.assertEqual(len(cid), 3)
def test_case_insensitive_dict_contains_different_case_key(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 self.assertTrue('ONE' in cid) self.assertFalse('THREE' in cid) self.assertFalse(4 in cid)
def __init__(self): IPlugin.__init__(self) libemailmgr.BasePlugin.__init__(self) self.cfg, self.actions = None, None self.opchain = [ self.op_adconnect, self.op_adidentify, self.op_applock, self.op_syncdomain, self.op_inittracking, self.op_syncrequired, self.op_retrchanges, self.op_syncdeleted, self.op_syncchanged, self.op_updateusn, self.op_sendgreetings ] self.lconn = None self.rootDSE = None self.domain_attrs = CaseInsensitiveDict( ) # Domain attributes from the AD self.db_domain_entry = {} # Domain data from the DB self.db_dit_entry = { } # Domain DIT (Directory Information Tree) data from the DB self.max_oper_usn = 0 # Max USN from all operations is stored in this var and saved to the DB at the end of synchronization
def test_add_values_to_case_insentitive_dict(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 self.assertEqual(cid['ONE'], 1) self.assertEqual(cid['one'], 1) self.assertEqual(cid['TWO'], 2) self.assertEqual(cid['two'], 2) self.assertEqual(cid[3], 3)
def test_create_case_insensitive_dict_from_dict(self): dic = dict() dic['ONE'] = 1 dic['TWO'] = 2 dic[3] = 3 cid = CaseInsensitiveDict(dic) self.assertEqual(cid['ONE'], 1) self.assertEqual(cid['one'], 1) self.assertEqual(cid['TWO'], 2) self.assertEqual(cid['two'], 2) self.assertEqual(cid[3], 3)
def test_modify_value_in_case_insentitive_dict_immmutable_key(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 self.assertEqual(cid[3], 3) cid[3] = 'Three' self.assertEqual(cid['ONE'], 1) self.assertEqual(cid['oNe'], 1) self.assertEqual(cid['TWO'], 2) self.assertEqual(cid['tWo'], 2) self.assertEqual(cid[3], 'Three')
def test_equality_case_insensitive_dict_with_different_case_dict(self): cid = CaseInsensitiveDict() cid['one'] = 1 cid['two'] = 2 cid[3] = 3 dic = dict() dic['ONE'] = 1 dic['TWO'] = 2 dic[3] = 3 self.assertEqual(cid, dic)
def test_modify_value_in_case_insentitive_dict_different_case_key(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 self.assertEqual(cid['ONE'], 1) self.assertEqual(cid['oNe'], 1) cid['one'] = 'ONE' self.assertEqual(cid['ONE'], 'ONE') self.assertEqual(cid['oNe'], 'ONE') self.assertEqual(cid['TWO'], 2) self.assertEqual(cid['two'], 2) self.assertEqual(cid[3], 3)
def test_delete_item_in_case_insentitive_dict_invariant_key(self): cid = CaseInsensitiveDict() cid['oNe'] = 1 cid['tWo'] = 2 cid[3] = 3 self.assertEqual(cid[3], 3) del cid[3] self.assertEqual(cid['ONE'], 1) self.assertEqual(cid['oNe'], 1) self.assertEqual(cid['TWO'], 2) self.assertEqual(cid['tWo'], 2) try: cid[3] except KeyError: self.assertTrue(True) except Exception: self.assertTrue(False)
def test_create_case_insensitive_dict_from_parameters(self): cid = CaseInsensitiveDict(one=1, two=2) self.assertEqual(cid['ONE'], 1) self.assertEqual(cid['one'], 1) self.assertEqual(cid['TWO'], 2) self.assertEqual(cid['two'], 2)
def test_create_empty_case_insensitive_dict(self): cid = CaseInsensitiveDict() self.assertTrue(isinstance(cid, CaseInsensitiveDict))
def __init__(self, server): self.database = CaseInsensitiveDict() self.connections = dict() self.ready_to_send = dict() self.lock = Lock() self.server = server
class Test_Case_LDAP_Sync(_Test_Base, unittest.TestCase): # Used for creating user_status objects ldap_groups = \ { 'roundup-users' : dict \ ( name = 'valid-ad' , ldap_prio = 1 , roles = 'User,Nosy' , is_nosy = True ) , 's_ap_timetracker-system-users' : dict ( name = 'system-ad' , ldap_prio = 2 , roles = 'User,Nosy' , is_nosy = True ) , 's_or_ad-natural-user' : dict ( name = 'valid-ad-nopermission' , ldap_prio = 10 , roles = 'Anonymous' , is_nosy = False ) } person_dn_by_group = \ { 'roundup-users' : [ 'CN=Test User,OU=internal' , 'CN=Test Middlename Usernameold,OU=internal' , 'CN=Test2 User2,OU=external' , 'CN=Roman Case,OU=internal' , 'CN=Jane Doe,OU=external' , 'CN=Vincent Super,OU=external' , 'CN=Nonexisting Roundup,OU=internal' ] } # Note: things synced to user_contact must be a list or tuple mock_users_by_username = \ { '*****@*****.**' : ( 'CN=Test Middlename Usernameold,OU=internal', CaseInsensitiveDict ( objectGUID = Mock_Guid ('1') , UserPrincipalName = LDAP_Property ('*****@*****.**') , cn = LDAP_Property ('Test Middlename Usernameold') , givenname = LDAP_Property ('Test Middlename') , sn = LDAP_Property ('Usernameold') , mail = LDAP_Property ('*****@*****.**') , otherTelephone = LDAP_Property ('0815') , displayname = LDAP_Property ('Test Middlename Usernameold') ) ) , '*****@*****.**' : ( 'CN=Test2 User2,OU=external', CaseInsensitiveDict ( objectGUID = Mock_Guid ('2') , UserPrincipalName = LDAP_Property ('*****@*****.**') , cn = LDAP_Property ('Test2 User2') , givenname = LDAP_Property ('Test2') , sn = LDAP_Property ('User2') , mail = LDAP_Property ('*****@*****.**') , displayname = LDAP_Property ('Test2 User2') ) ) , '*****@*****.**' : ( 'CN=Roman Case,OU=internal', CaseInsensitiveDict ( objectGUID = Mock_Guid ('3') , UserPrincipalName = LDAP_Property ('*****@*****.**') , cn = LDAP_Property ('Roman Case') , givenname = LDAP_Property ('Roman') , sn = LDAP_Property ('Case') , mail = LDAP_Property ('*****@*****.**') , displayname = LDAP_Property ('Roman Case') , physicalDeliveryOfficeName = LDAP_Property ('ASD.MJH.402') , department = LDAP_Property ('original_department') ) ) , '*****@*****.**' : ( 'CN=Jane Doe,OU=external', CaseInsensitiveDict ( objectGUID = Mock_Guid ('4') , UserPrincipalName = LDAP_Property ('*****@*****.**') , cn = LDAP_Property ('Jane Doe') , givenname = LDAP_Property ('Jane') , sn = LDAP_Property ('Doe') , mail = LDAP_Property ('*****@*****.**') , displayname = LDAP_Property ('Jane Doe') , physicalDeliveryOfficeName = LDAP_Property ('ASD.MJH.402') ) ) , '*****@*****.**' : ( 'CN=Vincent Super,OU=external', CaseInsensitiveDict ( objectGUID = Mock_Guid ('5') , UserPrincipalName = LDAP_Property ('*****@*****.**') , cn = LDAP_Property ('Vincent Super') , givenname = LDAP_Property ('Vincent') , sn = LDAP_Property ('Super') , mail = LDAP_Property ('*****@*****.**') , displayname = LDAP_Property ('Vincent Super') , physicalDeliveryOfficeName = LDAP_Property ('ASD.MJH.402') ) ) , '*****@*****.**' : ( 'CN=Nonexisting Roundup,OU=internal', CaseInsensitiveDict ( objectGUID = Mock_Guid ('6') , UserPrincipalName = LDAP_Property ('*****@*****.**') , cn = LDAP_Property ('Nonexisting Roundup') , givenname = LDAP_Property ('Nonexisting') , sn = LDAP_Property ('Roundup') , mail = LDAP_Property ('*****@*****.**') , displayname = LDAP_Property ('Nonexisting Roundup') ) ) } person_username_by_dn = dict \ ((v [0], k) for k, v in mock_users_by_username.items ()) def set_testuser1_testpic(self): with open('test/240px-Bald_Man.svg.png', 'rb') as f: fid = self.db.file.create \ ( name = 'picture' , type = 'image/png' , content = f.read () ) self.db.user.set(self.testuser1, pictures=[fid]) # end def set_testuser1_testpic def test_sync_contact_to_roundup(self): """ Test that modification of firstname and lastname is synced to LDAP """ # If different logging or ldap instrumentation is needed, # perform it here before calling setup_ldap, or modify self.ldap # and/or self.log afterwards. self.setup_ldap() self.ldap_sync.sync_user_from_ldap('*****@*****.**') user = self.db.user.getnode(self.testuser1) self.assertEqual(user.username, '*****@*****.**') self.assertEqual(user.firstname, 'Test') self.assertEqual(user.lastname, 'User') self.assertEqual(user.status, self.ustatus_valid_ad) self.assertEqual(user.guid, '31') self.assertEqual(user.ad_domain, 'ds1.internal') self.assertEqual(user.contacts, ['3', '4']) ct = self.db.user_contact.getnode('3') self.assertEqual(ct.contact, '0815') ct = self.db.user_contact.getnode('4') self.assertEqual(ct.contact, '*****@*****.**') # end def test_sync_contact_to_roundup def test_sync_new_user_to_roundup(self): self.setup_ldap() self.ldap_sync.sync_user_from_ldap('*****@*****.**') newuser = self.db.user.lookup('*****@*****.**') user = self.db.user.getnode(newuser) self.assertEqual(user.username, '*****@*****.**') self.assertEqual(user.firstname, 'Nonexisting') self.assertEqual(user.lastname, 'Roundup') self.assertEqual(user.status, self.ustatus_valid_ad) self.assertEqual(user.guid, '36') self.assertEqual(user.ad_domain, 'ds1.internal') # end def test_sync_new_user_to_roundup def test_sync_to_roundup_all(self): # Change behavior so that names are updated in roundup self.aux_ldap_parameters['update_ldap'] = False self.setup_ldap() self.log.info = self.mock_log self.ldap_sync.sync_all_users_from_ldap() msg = 'Synced %s users from LDAP to roundup' \ % (len (self.mock_users_by_username) - 1) self.assertEqual(self.messages[-2][0], msg) user = self.db.user.getnode(self.testuser1) self.assertEqual(user.firstname, 'Test Middlename') self.assertEqual(user.lastname, 'Usernameold') # end def test_sync_to_roundup_all def test_sync_to_roundup_all_dry(self): # Change behavior so that names are updated in roundup self.aux_ldap_parameters['update_ldap'] = False self.aux_ldap_parameters['dry_run_roundup'] = True self.setup_ldap() self.log.info = self.mock_log self.ldap_sync.sync_all_users_from_ldap() msg = '(Dry Run): Synced %s users from LDAP to roundup' \ % (len (self.mock_users_by_username) - 1) self.assertEqual(self.messages[-2][0], msg) user = self.db.user.getnode(self.testuser1) self.assertEqual(user.firstname, 'Test') self.assertEqual(user.lastname, 'User') # end def test_sync_to_roundup_all_dry def test_sync_to_roundup_all_limit(self): # Change behavior so that names are updated in roundup self.aux_ldap_parameters['update_ldap'] = False self.setup_ldap() self.log.error = self.mock_log self.ldap_sync.sync_all_users_from_ldap(max_changes=1) msg = ('Number of changes (%s) from LDAP to Roundup ' 'would exceed maximum 1, aborting') \ % (len (self.mock_users_by_username) - 1) self.assertEqual(self.messages[-1][0], msg) user = self.db.user.getnode(self.testuser1) self.assertEqual(user.firstname, 'Test') self.assertEqual(user.lastname, 'User') # end def test_sync_to_roundup_all_dry def test_sync_realname_to_ldap(self): self.setup_ldap() self.ldap_sync.sync_user_to_ldap('*****@*****.**') olddn = 'CN=Test Middlename Usernameold,OU=internal' newdn = 'CN=Test User,OU=internal' newcn = newdn.split(',')[0].lower() self.assertEqual \ (self.ldap_modify_result.keys (), [newdn]) d = self.ldap_modify_result[newdn] self.assertEqual(len(d), 7) for k in d: self.assertEqual(len(d[k]), 1) self.assertEqual(d['givenname'][0][0], 'MODIFY_REPLACE') self.assertEqual(d['givenname'][0][1], [u'Test']) self.assertEqual(d['displayname'][0][0], 'MODIFY_REPLACE') self.assertEqual(d['displayname'][0][1], [u'Test User']) self.assertEqual(d['employeenumber'][0][0], 'MODIFY_ADD') self.assertEqual(d['employeenumber'][0][1], [u'3']) self.assertEqual(d['sn'][0][0], 'MODIFY_REPLACE') self.assertEqual(d['sn'][0][1], [u'User']) self.assertEqual(d['otherTelephone'][0][0], 'MODIFY_DELETE') self.assertEqual(d['otherTelephone'][0][1], []) self.assertEqual(d['company'][0][0], 'MODIFY_ADD') self.assertEqual(d['company'][0][1][0], 'testorglocation1') self.assertEqual(d['department'][0][0], 'MODIFY_ADD') self.assertEqual(d['department'][0][1][0], 'testdepartment') self.assertEqual(self.ldap_modify_dn_result.keys(), [olddn]) changed = self.ldap_modify_dn_result[olddn].lower() self.assertEqual(changed, newcn) # end def test_sync_realname_to_ldap def test_dont_sync_cn_if_no_dynuser(self): self.setup_ldap() self.log.error = self.mock_log self.db.user_dynamic.retire(self.user_dynamic1_1) self.ldap_sync.sync_user_to_ldap('*****@*****.**') olddn = 'CN=Test Middlename Usernameold,OU=internal' self.assertEqual \ (self.ldap_modify_result.keys (), [olddn]) d = self.ldap_modify_result[olddn] self.assertEqual(len(d), 4) self.assertEqual(self.ldap_modify_dn_result.keys(), []) msg = 'Not syncing "realname"->"cn": no valid dyn. user for' self.assertTrue(self.messages[-1][0].startswith(msg)) # end def test_dont_sync_cn_if_no_dynuser def test_sync_cn_if_no_dynuser_but_system(self): self.setup_ldap() self.log.error = self.mock_log self.db.user_dynamic.retire(self.user_dynamic1_1) self.db.user_status.set(self.ustatus_valid_ad, is_system=True) self.ldap_sync.sync_user_to_ldap('*****@*****.**') newdn = 'CN=Test User,OU=internal' olddn = 'CN=Test Middlename Usernameold,OU=internal' newcn = newdn.split(',')[0].lower() self.assertEqual \ (self.ldap_modify_result.keys (), [newdn]) d = self.ldap_modify_result[newdn] # The number differs from the test_sync_realname_to_ldap above # because we've retired the dynamic user record self.assertEqual(len(d), 5) self.assertEqual(self.ldap_modify_dn_result.keys(), [olddn]) changed = self.ldap_modify_dn_result[olddn].lower() self.assertEqual(changed, newcn) # end def test_sync_cn_if_no_dynuser_but_system def test_sync_no_cn(self): self.tracker.config.ext.LDAP_DO_NOT_SYNC_LDAP_PROPERTIES = 'cn' self.setup_ldap() self.ldap_sync.sync_user_to_ldap('*****@*****.**') olddn = 'CN=Test Middlename Usernameold,OU=internal' self.assertEqual \ (self.ldap_modify_result.keys (), [olddn]) d = self.ldap_modify_result[olddn] self.assertEqual(len(d), 6) assert 'displayname' not in d self.assertEqual(d['givenname'][0][0], 'MODIFY_REPLACE') self.assertEqual(d['givenname'][0][1], [u'Test']) self.assertEqual(d['sn'][0][0], 'MODIFY_REPLACE') self.assertEqual(d['sn'][0][1], [u'User']) self.assertFalse(self.ldap_modify_dn_result) self.ldap_sync.sync_user_from_ldap('*****@*****.**') u = self.db.user.getnode(self.testuser1) # firstname/lastname/realname *not* changed in roundup self.assertEqual(u.firstname, 'Test') self.assertEqual(u.lastname, 'User') self.assertEqual(u.realname, 'Test User') # end def test_sync_no_cn def test_sync_realname_to_ldap_all(self): self.setup_ldap() nusers = len(self.db.user.filter(None, {})) / 2 - 1 self.log.info = self.mock_log self.ldap_sync.sync_all_users_to_ldap() newdn = 'CN=Test User,OU=internal' self.assertEqual(len(self.ldap_modify_result), nusers) self.assertEqual(len(self.ldap_modify_result[newdn]), 7) msg = 'Synced %s users from roundup to LDAP' % nusers self.assertEqual(self.messages[-2][0], msg) # end def test_sync_realname_to_ldap_all def test_sync_realname_to_ldap_all_dryrun(self): self.aux_ldap_parameters['dry_run_ldap'] = True self.setup_ldap() nusers = len(self.db.user.filter(None, {})) / 2 - 1 self.log.info = self.mock_log self.ldap_sync.sync_all_users_to_ldap() self.assertEqual(len(self.ldap_modify_result), 0) msg = '(Dry Run): Synced %s users from roundup to LDAP' % nusers self.assertEqual(self.messages[-2][0], msg) # end def test_sync_realname_to_ldap_all def test_sync_realname_to_ldap_all_limit(self): self.setup_ldap() nusers = len(self.db.user.filter(None, {})) / 2 - 1 self.log.error = self.mock_log self.ldap_sync.sync_all_users_to_ldap(max_changes=0) self.assertEqual(len(self.ldap_modify_result), 0) msg = ('Number of changes (%s) from Roundup to LDAP would exceed ' 'maximum 0, aborting') % nusers self.assertEqual(self.messages[-1][0], msg) # end def test_sync_realname_to_ldap_all def test_sync_room_to_ldap(self): self.setup_ldap() self.db.user.set(self.testuser1, room=self.room1) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') newdn = 'CN=Test User,OU=internal' d = self.ldap_modify_result[newdn] office = d['physicalDeliveryOfficeName'][0] self.assertEqual(office[0], 'MODIFY_ADD') self.assertEqual(office[1], [u'ASD.OIZ.501']) # end test_sync_room_to_ldap def test_dont_sync_if_vie_user_and_dyn_user(self): self.setup_ldap() self.log.error = self.mock_log self.assertEqual \ ( self.db.user.get (self.testuser2, 'vie_user_ml') , [self.testuser102] ) self.ldap_sync.sync_user_to_ldap('*****@*****.**') self.assertEqual(len(self.messages), 1) start_str = 'User [email protected] has a vie_user_ml link' self.assertTrue(self.messages[0][0].startswith(start_str)) self.assertEqual(self.ldap_modify_result, {}) # end test_dont_sync_if_vie_user_and_dyn_user def test_sync_if_vie_user_and_dyn_user_when_bl_override(self): self.setup_ldap() self.log.error = self.mock_log self.assertEqual \ ( self.db.user.get (self.testuser2, 'vie_user_ml') , [self.testuser102] ) self.ldap_sync.sync_user_to_ldap('*****@*****.**') self.assertEqual(len(self.messages), 1) start_str = 'User [email protected] has a vie_user_ml link' self.assertTrue(self.messages[0][0].startswith(start_str)) self.assertEqual(self.ldap_modify_result, {}) # override with user itself self.db.user.set(self.testuser2, vie_user_bl_override=self.testuser2) self.ldap_sync.sync_user_to_ldap('*****@*****.**') self.maxDiff = None dn = 'CN=Test2 User2,OU=external' self.assertEqual(len(self.ldap_modify_result), 1) self.assertEqual(len(self.ldap_modify_result[dn]), 4) self.assertEqual(self.ldap_modify_dn_result, {}) # override with user external user self.db.user.set \ (self.testuser2, vie_user_bl_override = self.testuser102) self.ldap_sync.sync_user_to_ldap('*****@*****.**') cn = 'cn=Test2 NewLastname' self.assertEqual(self.ldap_modify_dn_result[dn], cn) dn = 'CN=Test2 NewLastname,OU=external' self.assertEqual(len(self.ldap_modify_result[dn]), 6) results = \ ( ('displayname', 'Test2 NewLastname') , ('sn', 'NewLastname') , ('mobile', '08154711') ) for k, v in results: self.assertEqual(self.ldap_modify_result[dn][k][0][1][0], v) # end test_dont_sync_if_vie_user_and_dyn_user def test_sync_email_only_from_ad(self): self.setup_ldap() # check initial sync to Roundup def get_contacts(): return self.db.user.get(self.testuser3, 'contacts') self.assertFalse(get_contacts()) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.assertTrue(get_contacts()) user_contact = self.db.user_contact.get(get_contacts()[0], 'contact') self.assertEqual(user_contact, '*****@*****.**') # change contact in Roundup, check if not synced back self.user_contact1 = self.db.user_contact.create \ ( contact = '*****@*****.**' , contact_type = str ('1') ) self.db.user.set(self.testuser3, contacts=[self.user_contact1]) self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Roman Case,OU=internal' self.assertNotIn('mail', self.ldap_modify_result[dn]) # check if overwritten again with correct mail address self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.assertTrue(get_contacts()) user_contact = self.db.user_contact.get(get_contacts()[0], 'contact') self.assertEqual(user_contact, '*****@*****.**') # end test_sync_email_only_from_ad def test_sync_supervisor(self): self.setup_ldap() self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Jane Doe,OU=external' self.assertNotIn('manager', self.ldap_modify_result[dn]) # set supervisor on external user self.db.user.set(self.testuser104, supervisor=self.testuser105) self.ldap_sync.sync_user_to_ldap('*****@*****.**') self.assertIn('manager', self.ldap_modify_result[dn]) # unlink vie_user and set internal supervisor self.db.user.set(self.testuser104, vie_user=None) self.db.user.set(self.testuser4, supervisor=self.testuser3) self.ldap_sync.sync_user_to_ldap('*****@*****.**') new_secretary = self.ldap_modify_result[dn]['manager'][0][1][0] self.assertEqual(new_secretary, 'CN=Roman Case,OU=internal') # end test_sync_supervisor def test_sync_department(self): self.setup_ldap() self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Roman Case,OU=internal' new_department = self.ldap_modify_result[dn]['department'][0][1][0] self.assertEqual \ (new_department, self.db.department.get (self.department1, 'name')) # set department_temp and check override department_temp_name = 'dep_override' self.db.user.set \ (self.testuser3, department_temp = department_temp_name) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Roman Case,OU=internal' new_department = self.ldap_modify_result[dn]['department'][0][1][0] self.assertEqual(new_department, department_temp_name) # sync department from vie_user department_temp_name = 'dep_override_2' self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Jane Doe,OU=external' self.assertNotIn('department', self.ldap_modify_result[dn]) self.db.user.set \ (self.testuser104, department_temp = department_temp_name) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') new_department = self.ldap_modify_result[dn]['department'][0][1][0] self.assertEqual(new_department, department_temp_name) # end test_sync_department def test_sync_company(self): self.setup_ldap() self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Roman Case,OU=internal' new_company = self.ldap_modify_result[dn]['company'][0][1][0] self.assertEqual \ (new_company, self.db.org_location.get (self.org_location1, 'name')) # change org_location name new_org_location = 'new_org_location' self.db.org_location.set(self.org_location1, name=new_org_location) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') new_company = self.ldap_modify_result[dn]['company'][0][1][0] self.assertEqual(new_company, new_org_location) # end test_sync_company def test_sync_sap_cc(self): self.setup_ldap() self.sap_cc1 = self.db.sap_cc.create \ ( name = 'cc_test_name' , description = 'cc_test_description' ) self.db.user_dynamic.set(self.user_dynamic3_1, sap_cc=self.sap_cc1) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Roman Case,OU=internal' new_extAttr3 = self.ldap_modify_result \ [dn]['extensionAttribute3'][0][1][0] new_extAttr4 = self.ldap_modify_result \ [dn]['extensionAttribute4'][0][1][0] self.assertEqual \ (new_extAttr3, self.db.sap_cc.get (self.sap_cc1, 'name')) self.assertEqual \ (new_extAttr4, self.db.sap_cc.get (self.sap_cc1, 'description')) # change sap_cc name new_sap_cc_name = 'new_org_sap_cc_name' self.db.sap_cc.set(self.sap_cc1, name=new_sap_cc_name) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') new_extAttr3 = self.ldap_modify_result \ [dn]['extensionAttribute3'][0][1][0] self.assertEqual \ (new_extAttr3, self.db.sap_cc.get (self.sap_cc1, 'name')) # end test_sync_sap_cc def test_position_text_sync(self): self.setup_ldap() position = 'Developer' self.db.user.set(self.testuser3, position_text=position) self.ldap_sync.sync_user_from_ldap('*****@*****.**') self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Roman Case,OU=internal' new_position = self.ldap_modify_result[dn]['title'][0][1][0] self.assertEqual(new_position, position) # end test_position_text_sync def test_sync_attributes_not_directly_updating_vie_user(self): self.setup_ldap() intname = '*****@*****.**' self.ldap_sync.sync_user_to_ldap(intname) self.ldap_sync.sync_user_from_ldap(intname) # change external user, supervisor not synced directly self.db.user.set(self.testuser104, supervisor=self.testuser105) self.ldap_sync.sync_user_to_ldap('*****@*****.**') dn = 'CN=Jane Doe,OU=external' newdn = 'CN=Julia Doe,OU=external' new_supervisor = self.ldap_modify_result[dn]['manager'][0][1][0] self.assertEqual(new_supervisor, 'CN=Vincent Super,OU=external') supervisor_vie_user = self.db.user.get(self.testuser4, 'supervisor') # no supervisor set yet -> None self.assertIsNone(supervisor_vie_user) # copy from class to not modify globally self.mock_users_by_username = copy.deepcopy \ (self.mock_users_by_username) extname = '*****@*****.**' self.mock_users_by_username [intname][1]['manager'] = LDAP_Property \ (new_supervisor) self.ldap_sync.sync_user_from_ldap(intname) supervisor_vie_user = self.db.user.get(self.testuser4, 'supervisor') self.assertEqual(supervisor_vie_user, self.testuser5) # change external user, firstname new_firstname = 'Julia' self.db.user.set(self.testuser104, firstname=new_firstname) # Will not be changed because no dyn user exists self.log.error = self.mock_log self.ldap_sync.sync_user_to_ldap(intname) msg = 'Not syncing "realname"->"cn": no valid dyn. user' self.assertTrue(self.messages[0][0].startswith(msg)) self.assertTrue(newdn not in self.ldap_modify_result) # But the firstname has been changed: new_givenname = self.ldap_modify_result[dn]['givenname'][0][1][0] self.assertEqual(new_givenname, new_firstname) # Create *invalid* (valid_to in the past) user_dynamic ud = self.db.user_dynamic.create \ ( user = self.testuser104 , org_location = self.org_location1 , department = self.department1 , valid_from = Date (str ('2021-01-01')) , valid_to = Date (str ('2021-01-02')) , vacation_yearly = 25 ) # Will not be changed because no *valid* dyn user exists self.messages = [] self.ldap_sync.sync_user_to_ldap(intname) self.assertTrue(self.messages[0][0].startswith(msg)) self.assertTrue(newdn not in self.ldap_modify_result) # Verify that company contains a '*' due to invalid dynamic user newcomp = self.ldap_modify_result[dn]['company'][0][1][0] self.assertEqual(newcomp, '*testorglocation1') # And the firstname has been changed again: new_givenname = self.ldap_modify_result[dn]['givenname'][0][1][0] self.assertEqual(new_givenname, new_firstname) # Now make dynuser valid self.db.user_dynamic.set(ud, valid_to=None) self.ldap_sync.sync_user_to_ldap(intname) new_givenname = self.ldap_modify_result[newdn]['givenname'][0][1][0] self.assertEqual(new_givenname, new_firstname) firstname_vie_user = self.db.user.get(self.testuser4, 'firstname') self.assertEqual(firstname_vie_user, new_firstname) # end test_sync_attributes_not_directly_updating_vie_user def test_pic_convert_no_resize(self): # Although the original pic is > 15k in size it will be smaller # than the 9k limit after conversion to JPEG. self.setup_ldap() self.set_testuser1_testpic() self.ldap_sync.sync_user_to_ldap('*****@*****.**') newdn = 'CN=Test User,OU=internal' self.assertEqual \ (self.ldap_modify_result.keys (), [newdn]) d = self.ldap_modify_result[newdn] self.assertEqual(len(d), 8) pic = d['thumbnailPhoto'][0][1][0] self.assertEqual(len(pic), 6034) # compute md5sum over synced picture to assert that the picture # conversion is stable and produces same result every time. # Otherwise we would produce lots of ldap changes! # This *may* change for different versions of PIL. m = md5(pic) self.assertEqual(m.hexdigest(), 'c3b3e3bd46d5c7e9c82b71e1d92ad1a1') # end def test_pic_convert_no_resize def test_pic_convert_with_resize(self): # Although the original pic is > 15k in size it will be smaller # than the 9k limit after conversion to JPEG. So we need to set # the limit lower to trigger the resizing mechanism self.setup_ldap() self.set_testuser1_testpic() self.db.config.ext['LIMIT_PICTURE_SYNC_SIZE'] = '5000' self.ldap_sync.sync_user_to_ldap('*****@*****.**') newdn = 'CN=Test User,OU=internal' self.assertEqual \ (self.ldap_modify_result.keys (), [newdn]) d = self.ldap_modify_result[newdn] self.assertEqual(len(d), 8) pic = d['thumbnailPhoto'][0][1][0] self.assertEqual(len(pic), 4663) # compute md5sum over synced picture to assert that the picture # conversion is stable and produces same result every time. # Otherwise we would produce lots of ldap changes! # This *may* change for different versions of PIL. m = md5(pic) self.assertEqual(m.hexdigest(), '6d339cc744579e0349da05c78a2f1026')
class adsync(IPlugin, libemailmgr.BasePlugin): class OpChainStopException(Exception): pass account_control_flags = { "ADS_UF_ACCOUNTDISABLE": 0x00000002, "ADS_UF_NORMAL_ACCOUNT": 0x00000200 } def __init__(self): IPlugin.__init__(self) libemailmgr.BasePlugin.__init__(self) self.cfg, self.actions = None, None self.opchain = [ self.op_adconnect, self.op_adidentify, self.op_applock, self.op_syncdomain, self.op_inittracking, self.op_syncrequired, self.op_retrchanges, self.op_syncdeleted, self.op_syncchanged, self.op_updateusn, self.op_sendgreetings ] self.lconn = None self.rootDSE = None self.domain_attrs = CaseInsensitiveDict( ) # Domain attributes from the AD self.db_domain_entry = {} # Domain data from the DB self.db_dit_entry = { } # Domain DIT (Directory Information Tree) data from the DB self.max_oper_usn = 0 # Max USN from all operations is stored in this var and saved to the DB at the end of synchronization @staticmethod def ldapentry_mutli2singleval(entry): for key, val in entry.items(): if isinstance(val, list): entry[key] = None if len(val) < 1 else val[0] @staticmethod def ldapresponse_removerefs(lresponse): ref_idxs = [] counter = 0 for lentry in lresponse: if lentry["type"] == "searchResRef": ref_idxs.append(counter) counter += 1 ref_idxs.reverse() for idx in ref_idxs: del lresponse[idx:idx + 1] @staticmethod def stepmsg(msg, opseq, optotal): print("{} (operation {} of {})".format(msg, opseq, optotal)) @staticmethod def substepmsg(msg): print(" + {}".format(msg)) def configure(self, whoami, cfg, args, db): """ cfg - config from INI-file, args - rest of args in chosen context, db - database connection """ self.cfg = cfg self.actions = ["sync"] cmd = argparse.ArgumentParser( prog=os.path.basename(sys.argv[0]) + " {}".format(whoami), description="Synchronization with AD", epilog= "Only sync action is possible at the moment. And it's used by default" ) cmd.add_argument("action", help="Action to be performed", choices=self.actions, nargs="?", default="sync") cmdgroup = cmd.add_mutually_exclusive_group() cmdgroup.add_argument( "-greet", help="Send a greeting message to new accounts (default)", dest="do_greet", action='store_true') cmdgroup.add_argument( "-nogreet", help="Don't send a greeting message to new accounts", dest="do_greet", action='store_false') cmd.set_defaults(do_greet=True) self.args = cmd.parse_args(args) self.db = db self.configured = True def process_sync(self): opseq = 0 for oper in self.opchain: try: opseq += 1 oper(opseq, len(self.opchain)) except type(self).OpChainStopException: self.substepmsg( "the operation has requested to stop. stopping") break def op_adconnect(self, opseq, optotal): self.stepmsg("Conecting to the AD", opseq, optotal) self.substepmsg("connecting to the first available LDAP server") try: servers = [ ldap3.Server(host=server, get_info=ldap3.ALL) for server in self.cfg.get("adsync", "host").split() ] random.shuffle(servers) server_pool = ldap3.ServerPool(servers, pool_strategy=ldap3.FIRST, active=1) user = self.cfg.get("adsync", "user") password = self.cfg.get("adsync", "password") self.lconn = ldap3.Connection(server=server_pool, user=user, password=password, raise_exceptions=True, auto_bind=ldap3.AUTO_BIND_NO_TLS, return_empty_attributes=True) except configparser.Error: self.handle_cfg_exception(sys.exc_info()) except LDAPException: self.handle_ldap_exception(sys.exc_info()) self.substepmsg("processing root DSE") rootDSE_keys = [ "defaultNamingContext", "configurationNamingContext", "domainFunctionality", "serverName", "dnsHostName" ] rootDSE_values = [ x[0] if isinstance(x, list) else x for x in map( lambda var: self.lconn.server.info.other[var], rootDSE_keys) ] self.rootDSE = CaseInsensitiveDict(zip(rootDSE_keys, rootDSE_values)) def op_adidentify(self, opseq, optotal): self.stepmsg("Identifying the domain", opseq, optotal) self.substepmsg("retrieving name") try: self.lconn.search( search_base="CN=Partitions," + self.rootDSE["configurationNamingContext"], search_scope=ldap3.LEVEL, search_filter="(&(objectClass=crossRef)(nCName={}))".format( self.rootDSE["defaultNamingContext"]), attributes=["dnsRoot", "nETBIOSName"]) except LDAPException: self.handle_ldap_exception(sys.exc_info()) lentry = self.lconn.response[0]["attributes"] self.ldapentry_mutli2singleval(lentry) self.domain_attrs.update(lentry) self.substepmsg("retrieving additional attributes") try: self.lconn.search(search_base=self.rootDSE["defaultNamingContext"], search_scope=ldap3.BASE, search_filter="(objectClass=*)", attributes=["objectGUID", "whenChanged"]) except LDAPException: self.handle_ldap_exception(sys.exc_info()) lentry = self.lconn.response[0]["attributes"] lentry_raw = self.lconn.response[0]["raw_attributes"] for entry in [lentry, lentry_raw]: self.ldapentry_mutli2singleval(entry) self.domain_attrs.update(lentry) self.domain_attrs["objectGUID_raw"] = lentry_raw["objectGUID"] def op_applock(self, opseq, optotal): self.stepmsg("Obtaining an application lock", opseq, optotal) try: self.dbc.execute("SELECT pg_advisory_lock(%s)", [libemailmgr.SQL_ADV_LOCK_GENERAL]) self.db.commit() except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) def op_syncdomain(self, opseq, optotal): self.stepmsg("Synchronizing the domain", opseq, optotal) try: self.substepmsg("checking by GUID, {{{}}}".format( self.domain_attrs["objectGUID"])) self.dbc.execute( "SELECT id, name, ad_guid, ad_sync_enabled FROM domain WHERE ad_guid = %s", [self.domain_attrs["objectGUID_raw"]]) if self.dbc.rowcount < 1: self.substepmsg("checking by name, {}".format( self.domain_attrs["dnsRoot"])) self.dbc.execute( "SELECT id, name, ad_guid, ad_sync_enabled FROM domain WHERE LOWER(name) = LOWER(%s)", [self.domain_attrs["dnsRoot"]]) if self.dbc.rowcount < 1: self.substepmsg("the domain seems to be new - creating") self.dbc.execute( "INSERT INTO domain(name, spooldir, ad_guid, created, modified)" "VALUES(%s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)", [ self.domain_attrs["dnsRoot"], self.domain_attrs["objectGUID"], self.domain_attrs["objectGUID_raw"] ]) self.dbc.execute( "SELECT id, name, ad_guid, ad_sync_enabled FROM domain WHERE ad_guid = %s", [self.domain_attrs["objectGUID_raw"]]) self.db_domain_entry = dict( zip([item[0] for item in self.dbc.description], self.dbc.fetchone())) if self.db_domain_entry["ad_sync_enabled"]: if self.db_domain_entry["ad_guid"] is None: self.substepmsg( "binding existing domain {} to the AD (updating GUID)". format(self.domain_attrs["dnsRoot"])) self.dbc.execute( "UPDATE domain SET ad_guid = %s, modified = CURRENT_TIMESTAMP " "WHERE id = %s", [ self.domain_attrs["objectGUID_raw"], self.db_domain_entry["id"] ]) elif self.db_domain_entry["ad_guid"].tobytes( ) != self.domain_attrs["objectGUID_raw"]: self.substepmsg( "existing domain {} is already bound to a different AD - stopping here" .format(self.db_domain_entry["name"])) raise type(self).OpChainStopException elif self.db_domain_entry["name"].lower( ) != self.domain_attrs["dnsRoot"].lower(): self.substepmsg( "the domain seems to be renamed - updating name to {}". format(self.domain_attrs["dnsRoot"])) self.dbc.execute( "UPDATE domain SET name = %s, modified = CURRENT_TIMESTAMP WHERE id = %s", [ self.domain_attrs["dnsRoot"], self.db_domain_entry["id"] ]) else: self.substepmsg( "AD synchronization of the domain {} is not allowed - stopping here" .format(self.db_domain_entry["name"])) raise type(self).OpChainStopException self.db.commit() except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) def op_inittracking(self, opseq, optotal): self.stepmsg("Initializing tracking", opseq, optotal) try: self.substepmsg("retrieving DIT ID") self.lconn.search(search_base="CN=NTDS Settings," + self.rootDSE["serverName"], search_scope=ldap3.BASE, search_filter="(objectClass=*)", attributes=["invocationId"]) except LDAPException: self.handle_ldap_exception(sys.exc_info()) lentry = self.lconn.response[0][ "attributes"] # invocationId is a single octet-string value. so here and below we don't need any additional checks and conversions DITinvocationID = lentry["invocationId"] try: self.substepmsg("checking database for the tracking record") self.dbc.execute( "SELECT id, domain_id, dit_invocation_id, dit_usn FROM usn_tracking WHERE domain_id = %s AND dit_invocation_id = %s", [self.db_domain_entry["id"], DITinvocationID]) if self.dbc.rowcount < 1: self.substepmsg( "no tracking record for the retrieved DIT ID - creating") self.dbc.execute( "INSERT INTO usn_tracking(domain_id, dit_invocation_id) VALUES(%s, %s)", [self.db_domain_entry["id"], DITinvocationID]) self.dbc.execute( "SELECT id, domain_id, dit_invocation_id, dit_usn FROM usn_tracking WHERE domain_id = %s AND dit_invocation_id = %s", [self.db_domain_entry["id"], DITinvocationID]) self.db.commit() self.substepmsg("caching tracking record") self.db_dit_entry = dict( zip([item[0] for item in self.dbc.description], self.dbc.fetchone())) except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) def op_syncrequired(self, opseq, optotal): self.stepmsg("Synchronizing accounts with the \"required\" flag set", opseq, optotal) try: self.dbc.execute( "SELECT id, name FROM account WHERE domain_id = %s AND ad_sync_required = TRUE", [self.db_domain_entry["id"]]) for db_entry in self.dbc: db_account = dict( zip([item[0] for item in self.dbc.description], db_entry)) curr1 = self.db.cursor() try: self.lconn.search( search_base=self.rootDSE["defaultNamingContext"], search_filter= "(&(objectClass=user)(userPrincipalName={})" "(userAccountControl:1.2.840.113556.1.4.803:=512)" "(!(servicePrincipalName=*)))".format("@".join( [db_account["name"], self.domain_attrs["dnsRoot"]])), attributes=[ "userPrincipalName", "displayName", "objectGUID", "userAccountControl", "whenChanged" ]) except LDAPException: self.handle_ldap_exception(sys.exc_info()) self.ldapresponse_removerefs(self.lconn.response) if len(self.lconn.response) < 1: self.substepmsg("deleting {} - not found in the AD".format( db_account["name"])) curr1.execute("DELETE FROM account WHERE id = %s", [db_account["id"]]) else: for entry in [ self.lconn.response[0]["raw_attributes"], self.lconn.response[0]["attributes"] ]: self.ldapentry_mutli2singleval(entry) curr1.execute( "SELECT id, name FROM account WHERE ad_guid = %s", [ self.lconn.response[0]["raw_attributes"] ["objectGUID"] ]) db_account_check = None for db_entry1 in curr1: db_account_check = dict( zip([item[0] for item in curr1.description], db_entry1)) break if db_account_check and db_account[ "id"] != db_account_check["id"]: self.substepmsg( "deleting {} - GUID from AD conflicts with {}". format(db_account["name"], db_account_check["name"])) curr1.execute("DELETE FROM account WHERE id = %s", [db_account["id"]]) else: self.substepmsg("updating {}".format( db_account["name"])) curr1.execute( "UPDATE account SET name = %s, fullname = %s, modified = CURRENT_TIMESTAMP," "active = %s, ad_guid = %s, ad_sync_enabled = TRUE, ad_sync_required = FALSE," "ad_time_changed = %s WHERE id = %s", [ self.lconn.response[0]["attributes"] ["userPrincipalName"].split("@")[0], self. lconn.response[0]["attributes"]["displayName"], False if self.lconn.response[0]["attributes"] ["userAccountControl"] & type(self). account_control_flags["ADS_UF_ACCOUNTDISABLE"] else True, self.lconn.response[0] ["raw_attributes"]["objectGUID"], self.lconn.response[0]["attributes"] ["whenChanged"], db_account["id"] ]) self.db.commit() except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) def op_retrchanges(self, opseq, optotal): self.stepmsg("Retrieving changes", opseq, optotal) self.substepmsg("creating temporary storage for the working set") try: self.dbc.execute( "CREATE TEMPORARY TABLE tmp_ad_object (" "id SERIAL PRIMARY KEY," "name TEXT DEFAULT NULL UNIQUE," "fullname TEXT DEFAULT NULL," "guid BYTEA NOT NULL UNIQUE," "guid_txt TEXT DEFAULT NULL," "control_flags INTEGER DEFAULT NULL," "time_changed TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL," "deleted BOOLEAN NOT NULL DEFAULT FALSE)") self.dbc.execute("CREATE INDEX ON tmp_ad_object(deleted)") self.substepmsg("fetching changes from AD") try: # TODO: add an option to make full sync with AD instead with just deltas after dit_usn (use 0 instead of self.db_dit_entry["dit_usn"] in query below) self.lconn.search( search_base=self.rootDSE["defaultNamingContext"], search_filter="(&(objectClass=user)" "(!(uSNChanged<={}))(|(&(userAccountControl:1.2.840.113556.1.4.803:=512)" "(userPrincipalName=*)(!(servicePrincipalName=*))" "(!(isDeleted=TRUE)))(isDeleted=TRUE)))".format( self.db_dit_entry["dit_usn"]), attributes=[ "userPrincipalName", "displayName", "objectGUID", "userAccountControl", "usnChanged", "whenChanged", "isDeleted" ], controls=[("1.2.840.113556.1.4.417", False, None)]) except LDAPException: self.handle_ldap_exception(sys.exc_info()) self.ldapresponse_removerefs(self.lconn.response) for lentry in self.lconn.response: for attrs in [lentry["raw_attributes"], lentry["attributes"]]: self.ldapentry_mutli2singleval(attrs) if not lentry["attributes"]["isDeleted"]: if not lentry["attributes"]["userPrincipalName"]: self.substepmsg( "could not add an entry with GUID {{{}}} to the working set: userPrincipalName is empty" .format(lentry["attributes"]["objectGUID"])) continue user_principal_name = dict( zip(["name", "realm"], lentry["attributes"] ["userPrincipalName"].split("@"))) if user_principal_name["realm"].lower( ) != self.db_domain_entry["name"].lower(): self.substepmsg( "could not add an entry with GUID {{{}}} to the working set: realm doesn't match domain" .format(lentry["attributes"]["objectGUID"])) continue user_email = user_principal_name[ "name"] + "@" + self.db_domain_entry["name"] if not validators.email(user_email): self.substepmsg( "could not add an entry with GUID {{{}}} to the working set: \"{}\" is not a valid email address" .format(lentry["attributes"]["objectGUID"], user_email)) continue self.substepmsg( "adding live entry \"{}\" with GUID {{{}}} to the working set" .format(user_principal_name["name"], lentry["attributes"]["objectGUID"])) self.dbc.execute( "INSERT INTO tmp_ad_object(name, fullname, guid, guid_txt, control_flags, time_changed) " "VALUES(%s, %s, %s, %s, %s, %s)", [ user_principal_name["name"], lentry["attributes"]["displayName"], lentry["raw_attributes"]["objectGUID"], lentry["attributes"]["objectGUID"], lentry["attributes"]["userAccountControl"], lentry["attributes"]["whenChanged"] ]) else: self.substepmsg( "adding dead entry with GUID {{{}}} to the working set" .format(lentry["attributes"]["objectGUID"])) self.dbc.execute( "INSERT INTO tmp_ad_object(guid, deleted) VALUES(%s, TRUE)", [lentry["raw_attributes"]["objectGUID"]]) if self.max_oper_usn < lentry["attributes"]["usnChanged"]: self.max_oper_usn = lentry["attributes"]["usnChanged"] self.db.commit() except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) def op_syncdeleted(self, opseq, optotal): self.stepmsg("Synchronizing deleted accounts", opseq, optotal) try: self.dbc.execute( "DELETE FROM account USING tmp_ad_object " "WHERE domain_id = %s AND ad_sync_enabled = TRUE AND ad_guid = tmp_ad_object.guid " "AND tmp_ad_object.deleted = TRUE", [self.db_domain_entry["id"]]) self.substepmsg("{} row(s) affected (deleted)".format( self.dbc.rowcount)) self.db.commit() except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) def op_syncchanged(self, opseq, optotal): self.stepmsg("Synchronizing new and/or changed accounts", opseq, optotal) try: self.dbc.execute( "DO $$\n" "DECLARE\n" " v_account RECORD;\n" " v_account_enabled BOOLEAN;\n" " v_db_account_by_name RECORD;\n" " v_db_account_by_guid RECORD;\n" " v_account_flag_disabled INTEGER DEFAULT %s;\n" " v_domain_id INTEGER DEFAULT %s;\n" "BEGIN\n" " CREATE TEMPORARY TABLE tmp_syncchanged_log (\n" " id SERIAL PRIMARY KEY,\n" " action TEXT DEFAULT 'a_generic',\n" " message TEXT,\n" " info1 TEXT,\n" " info2 TEXT);\n" " CREATE INDEX ON tmp_syncchanged_log(action);\n" " FOR v_account IN SELECT id, name, fullname, guid, guid_txt, control_flags, time_changed FROM tmp_ad_object WHERE deleted = FALSE LOOP\n" " IF (v_account.control_flags & v_account_flag_disabled)::BOOLEAN THEN\n" " v_account_enabled = FALSE;\n" " ELSE\n" " v_account_enabled = TRUE;\n" " END IF;\n" " SELECT id, name, ad_guid, ad_sync_enabled, ad_time_changed INTO v_db_account_by_guid\n" " FROM account WHERE domain_id = v_domain_id AND ad_guid = v_account.guid;\n" " SELECT id, name, ad_guid, ad_sync_enabled, ad_time_changed INTO v_db_account_by_name\n" " FROM account WHERE domain_id = v_domain_id AND lower(name) = lower(v_account.name);\n" " IF v_db_account_by_guid.id IS NOT NULL AND (v_db_account_by_name.id IS NULL OR\n" " v_db_account_by_guid.ad_guid IS NOT DISTINCT FROM v_db_account_by_name.ad_guid) THEN\n" " IF v_db_account_by_guid.ad_sync_enabled AND v_account.time_changed > v_db_account_by_guid.ad_time_changed THEN\n" " UPDATE account SET\n" " name = v_account.name,\n" " fullname = CASE WHEN v_account.fullname IS NOT NULL THEN v_account.fullname ELSE v_account.name END,\n" " modified = CURRENT_TIMESTAMP,\n" " active = v_account_enabled,\n" " ad_time_changed = v_account.time_changed\n" " WHERE id = v_db_account_by_guid.id;\n" " INSERT INTO tmp_syncchanged_log(message) VALUES ('updated an AD-bound account ' || v_db_account_by_guid.name);\n" " ELSIF v_account.time_changed <= v_db_account_by_guid.ad_time_changed THEN\n" " INSERT INTO tmp_syncchanged_log(message) VALUES ('could not update an AD-bound account ' || v_db_account_by_guid.name || ' - source is out of date');\n" " ELSE\n" " INSERT INTO tmp_syncchanged_log(message) VALUES ('could not update an AD-bound account ' || v_db_account_by_guid.name || ' - not permitted');\n" " END IF;\n" " ELSIF v_db_account_by_name.id IS NOT NULL THEN\n" " IF v_db_account_by_name.ad_sync_enabled THEN\n" " UPDATE account SET\n" " name = v_account.name,\n" " fullname = CASE WHEN v_account.fullname IS NOT NULL THEN v_account.fullname ELSE v_account.name END,\n" " modified = CURRENT_TIMESTAMP,\n" " active = v_account_enabled,\n" " ad_time_changed = v_account.time_changed\n" " WHERE id = v_db_account_by_name.id;\n" " INSERT INTO tmp_syncchanged_log(message) VALUES ('bound an account ' || v_db_account_by_name.name || ' to the AD');\n" " ELSE\n" " INSERT INTO tmp_syncchanged_log(message) VALUES ('could not bind an account ' || v_db_account_by_name.name || ' to the AD - not permitted');\n" " END IF;\n" " ELSIF v_db_account_by_guid.id IS NULL THEN\n" " INSERT INTO account(domain_id, name, fullname, spooldir, created, modified, active, ad_guid, ad_time_changed) VALUES (\n" " v_domain_id, v_account.name,\n" " CASE WHEN v_account.fullname IS NOT NULL THEN v_account.fullname ELSE v_account.name END,\n" " v_account.guid_txt || '/', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, v_account_enabled,\n" " v_account.guid, v_account.time_changed);\n" " INSERT INTO tmp_syncchanged_log(action, message, info1, info2) VALUES (\n" " 'a_insert',\n" " 'added new account ' || v_account.name,\n" " v_account.name,\n" " CASE WHEN v_account.fullname IS NOT NULL THEN v_account.fullname ELSE v_account.name END);\n" " ELSE\n" " INSERT INTO tmp_syncchanged_log(message) VALUES (\n" " 'conflict found - an account from AD, ' || v_account.name || ', {{' || v_account.guid_txt || '}}, conflicts with ' || v_db_account_by_name.name);\n" " END IF;\n" " END LOOP;\n" "END $$", [ self.account_control_flags["ADS_UF_ACCOUNTDISABLE"], self.db_domain_entry["id"] ]) self.dbc.execute( "SELECT message FROM tmp_syncchanged_log ORDER BY id") for db_entry in self.dbc: self.substepmsg(db_entry[0]) self.db.commit() except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) def op_updateusn(self, opseq, optotal): self.stepmsg("Saving tracking state", opseq, optotal) if self.max_oper_usn > self.db_dit_entry["dit_usn"]: self.substepmsg("saving serial {} to the database".format( self.max_oper_usn)) try: self.dbc.execute( "UPDATE usn_tracking SET dit_usn = %s WHERE id = %s", [self.max_oper_usn, self.db_dit_entry["id"]]) except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) else: self.substepmsg( "nothing changed since last sync session. nothing to save") def op_sendgreetings(self, opseq, optotal): self.stepmsg("Sending greetings", opseq, optotal) if self.args.do_greet: try: self.dbc.execute( "SELECT info1, info2 FROM tmp_syncchanged_log WHERE action = 'a_insert'" ) for db_entry in self.dbc: self.substepmsg("greeting {}".format(db_entry[0])) msg = EmailMessage() msg["Subject"] = "Welcome!" msg["From"] = "postmaster@{}".format( self.db_domain_entry["name"]) msg["To"] = "{}@{}".format(db_entry[0], self.db_domain_entry["name"]) msg["Date"] = email.utils.formatdate() msg.set_content("Hello {},\n\n" "Greetings from email system at {}!\n\n" "--\n" "Best regards,\n" "Postmaster".format( db_entry[1], self.db_domain_entry["name"])) try: s = smtplib.SMTP(self.cfg.get("adsync", "smtp")) s.send_message(msg) s.quit() except configparser.Error: self.handle_cfg_exception(sys.exc_info()) except (TimeoutError, smtplib.SMTPConnectError) as e: self.substepmsg( "could not send a greeting message: could not connect to the mail server" ) print(e) raise type(self).OpChainStopException except smtplib.SMTPException: self.substepmsg( "could not send a greeting message to {}: SMTP error occured" .format(db_entry[0])) self.db.commit() except psycopg2.Error: self.handle_pg_exception(sys.exc_info()) else: self.substepmsg("operation disabled by user")
def test_len_empty_case_insensitive_dict(self): cid = CaseInsensitiveDict() self.assertEqual(len(cid), 0)
user=cmdlargs.u, password=cmdlargs.p, return_empty_attributes=True, raise_exceptions=True, auto_bind=ldap3.AUTO_BIND_NO_TLS) except LDAPException: handle_ldap_exception(sys.exc_info()) rootDSE_keys = [ "defaultNamingContext", "configurationNamingContext", "domainFunctionality", "serverName", "dnsHostName" ] rootDSE_values = [ x[0] if isinstance(x, list) else x for x in map(lambda var: lconn.server.info.other[var], rootDSE_keys) ] rootDSE = CaseInsensitiveDict(zip(rootDSE_keys, rootDSE_values)) print("Connected to the LDAP at {}".format(rootDSE["dnsHostName"])) print("Current domain functionality is {}\n".format( domain_functionality[rootDSE["domainFunctionality"]])) account_attrs = [ "name", "userPrincipalName", "displayName", "objectGUID", "userAccountControl", "usnChanged", "whenChanged", "isDeleted" ] control_showdeleted = ("1.2.840.113556.1.4.417", False, None) try: lconn.search( search_base=rootDSE["defaultNamingContext"], search_scope=ldap3.SUBTREE, search_filter= "(&(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:=512))",