Ejemplo n.º 1
0
    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)
Ejemplo n.º 3
0
    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)
Ejemplo n.º 5
0
 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)
Ejemplo n.º 6
0
 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')
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
 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))
Ejemplo n.º 9
0
 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)
Ejemplo n.º 10
0
 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)
Ejemplo n.º 11
0
 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
Ejemplo n.º 12
0
 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)
Ejemplo n.º 13
0
 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)
Ejemplo n.º 14
0
 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')
Ejemplo n.º 15
0
    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)
Ejemplo n.º 16
0
 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)
Ejemplo n.º 17
0
 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)
Ejemplo n.º 18
0
 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)
Ejemplo n.º 19
0
 def test_create_empty_case_insensitive_dict(self):
     cid = CaseInsensitiveDict()
     self.assertTrue(isinstance(cid, CaseInsensitiveDict))
Ejemplo n.º 20
0
 def __init__(self, server):
     self.database = CaseInsensitiveDict()
     self.connections = dict()
     self.ready_to_send = dict()
     self.lock = Lock()
     self.server = server
Ejemplo n.º 21
0
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')
Ejemplo n.º 22
0
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")
Ejemplo n.º 23
0
 def test_len_empty_case_insensitive_dict(self):
     cid = CaseInsensitiveDict()
     self.assertEqual(len(cid), 0)
Ejemplo n.º 24
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))",