import ldap3 import random from datetime import datetime MAXENTRIES = 1000000 OBJECTCLASSES = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount'] USERATTRIBUTES = ['cn' , 'sn', 'givenName', 'uid', 'uidNumber' , 'gidNumber', 'homeDirectory', 'loginShell', 'gecos' , 'shadowLastChange', 'shadowMax', 'userPassword', 'mail', 'description'] class logserver(): def __init__(self, ldap_host: str, admin_user: str, admin_pass: str, base: str, ssl: bool = True ): self.ldap_host = ldap_host self.admin_user = admin_user self.admin_pass = admin_pass self.base = base self.ssl = ssl if ssl: server = ldap3.Server(self.ldap_host,use_ssl=True) else: server = ldap3.Server(self.ldap_host) self.conn = ldap3.Connection(server, admin_user, admin_pass, auto_bind=True) self.organization, self.dc, self.dcfull, self.domain = self.expandbase() self.logbase = f'ou=log,{self.dcfull}' # Unique id of the log branch self.id = self.getid() # How many changes we applied to the LDAP server self.loaded = self.getloaded() # How many changes are recoreded in total self.total = self.gettotal() self.refreshtotal() def expandbase(self, base: str = '')->(str,str,str,str): ''' Extract orgnaization, name of dc object and full domain part with al l dc values from base ''' if base == '': base = self.base # Split base string with commas to find values of organization and dc baselist = base.split(",") organization = '' dc = '' dcfull = '' domain = '' # Find ou in base and set it as organization variable for i in baselist: if i.split('=')[0] == 'ou': organization = i.split('=')[1] # Find first dc and set it as dc variable for i in baselist: if i.split('=')[0] == 'dc': dc = i.split('=')[1] break # Find full dc and set it as dcfull variable for i in baselist: if i.split('=')[0] == 'dc': # if first dc, add it from dc variable if dcfull == '' and domain == '': dcfull = f'dc={dc}' domain = dc else: dcfull += ',dc=' + i.split('=')[1] domain += f'.{i.split("=")[1]}' return organization, dc, dcfull, domain def getid(self, base: str = '')->int: if base == '': base = self.logbase self.conn.search(search_base=f'uid=id,{base}',search_filter='(objectClass=person)', attributes=['uidNumber']) response = self.conn.response if response == []: id = random.randint(10**12, 10**13-1) attributes = {'cn' : 'id', 'sn' : 'id', 'givenName' : 'id', 'uid' : 'id', 'uidNumber' : id, 'gidNumber' : id, 'homeDirectory' : f'/home/id', 'loginShell' : '/usr/bin/git-shell', 'gecos' : 'SystemUser', 'shadowLastChange' : self.lastpwchangenow(), 'shadowMax' : '45', 'userPassword' : 'id', 'mail' : f'id@{self.domain}' } self.conn.add(f'uid=id,{base}', OBJECTCLASSES, attributes) return id else: response = int(response[0]['attributes']['uidNumber']) return response def gettotal(self, base: str = '')->int: if base == '': base = self.logbase self.conn.search(search_base=f'uid=total,{base}',search_filter='(objectClass=person)', attributes=['uidNumber']) response = self.conn.response if response == []: response = 0 else: response = int(response[0]['attributes']['uidNumber']) return response def settotal(self, newvalue: int, base: str = ''): if base == '': base = self.logbase newvalue = int(newvalue) # Check if total record already present self.conn.search(search_base=f'uid=total,{base}',search_filter='(objectClass=person)', attributes=['uidNumber']) response = self.conn.response attributes = {'cn' : 'total', 'sn' : 'total', 'givenName' : 'total', 'uid' : 'total', 'uidNumber' : newvalue, 'gidNumber' : newvalue, 'homeDirectory' : f'/home/total', 'loginShell' : '/usr/bin/git-shell', 'gecos' : 'SystemUser', 'shadowLastChange' : self.lastpwchangenow(), 'shadowMax' : '45', 'userPassword' : 'total', 'mail' : f'total@{self.domain}' } if response == []: self.conn.add(f'uid=total,{base}', OBJECTCLASSES, attributes) else: self.conn.modify(f'uid=total,{base}', {'uidNumber' : (ldap3.MODIFY_REPLACE, [newvalue])}) return self.conn.response def getloaded(self, base: str = '')->int: if base == '': base = self.logbase self.conn.search(search_base=f'uid=loaded,{base}',search_filter='(objectClass=person)', attributes=['uidNumber']) response = self.conn.response if response == []: response = 0 else: response = int(response[0]['attributes']['uidNumber']) return response def setloaded(self, newvalue: int, base: str = ''): if base == '': base = self.logbase newvalue = int(newvalue) # Check if loaded record already present self.conn.search(search_base=f'uid=loaded,{base}',search_filter='(objectClass=person)', attributes=['uidNumber']) response = self.conn.response attributes = {'cn' : 'loaded', 'sn' : 'loaded', 'givenName' : 'loaded', 'uid' : 'loaded', 'uidNumber' : newvalue, 'gidNumber' : newvalue, 'homeDirectory' : f'/home/loaded', 'loginShell' : '/usr/bin/git-shell', 'gecos' : 'SystemUser', 'shadowLastChange' : self.lastpwchangenow(), 'shadowMax' : '45', 'userPassword' : 'loaded', 'mail' : f'loaded@{self.domain}' } if response == []: self.conn.add(f'uid=loaded,{base}', OBJECTCLASSES, attributes) else: self.conn.modify(f'uid=loaded,{base}', {'uidNumber' : (ldap3.MODIFY_REPLACE, [newvalue])}) return self.conn.response def refreshtotal(self, base: str = '')->int: if base == '': base = self.logbase self.settotal(self.findtotal(base), base) return self.gettotal(base) def findtotal(self, base: str = '')->int: if base == '': base = self.logbase for entry in range(self.gettotal(base) + 1, MAXENTRIES): self.conn.search(search_base=f'uid={entry},{base}',search_filter='(objectClass=person)', attributes=['uidNumber']) if self.conn.response == []: return entry - 1 def applylogs(self, base: str = '')->int: if base == '': base = self.logbase self.refreshtotal(base) loaded = self.getloaded(base) + 1 total = self.gettotal(base) + 1 for log_number in range(loaded, total): self.applylog(log_number, base) return total - loaded def applylog(self, log_number: int, base: str = ''): if base == '': base = self.logbase log = self.getlog(log_number, base) if log == []: return -1 attributes = log['attributes'] for key in attributes: if isinstance(attributes[key], list): attributes[key] = attributes[key][0] uid = attributes['cn'] action = attributes['description'] del attributes['description'] if action == 'ADD': self.conn.add(f'uid={uid},{self.base}', OBJECTCLASSES, attributes) elif action == 'DELETE': self.conn.delete(f'uid={uid},{self.base}') elif action == 'CHANGEPASS': self.conn.modify(f'uid={uid},{self.base}', {'userPassword': (ldap3.MODIFY_REPLACE,attributes['userPassword'])}) else: return f'Error: Unrecognized action in log entry: {action}' self.setloaded(self.getloaded(base) + 1, base) return self.conn.response def getlog(self, log_number: int, base: str = ''): if base == '': base = self.logbase self.conn.search(search_base=f'uid={log_number},{base}',search_filter = '(objectClass=person)', attributes = USERATTRIBUTES) response = self.conn.response if response == []: return response else: return response[0] def setlog(self, attributes: dict, base: str = '', log_number: int = -1): if base == '': base = self.logbase if log_number == -1: self.refreshtotal(base) log_number = self.gettotal(base) + 1 self.conn.add(f'uid={log_number},{base}', OBJECTCLASSES, attributes) return self.conn.response def getbasecopy(self, remote)->str: ''' Gets the base of a log copy of remote server on the local server ''' remoteid = remote.getid() localdcfull = self.dcfull basecopy = f'ou=sync-{remoteid},{localdcfull}' # Check if it exists and create it if not self.conn.search(search_base=basecopy,search_filter='(objectClass=organizationalUnit)', attributes=['ou']) if self.conn.response == []: self.conn.add(basecopy, ['top', 'organizationalUnit'], {'ou' : f'sync-{remoteid}'}) return basecopy def pullfrom(self, source)->int: pulled = 0 basecopy = self.getbasecopy(source) self.refreshtotal(basecopy) source.refreshtotal() total_dest = self.gettotal(basecopy) total_src = source.gettotal() for log_number in range(total_dest + 1, total_src + 1): log = source.getlog(log_number) if log == []: break attributes = log['attributes'] self.setlog(attributes, basecopy) pulled += 1 return pulled def applyfrom(self, source)->int: basecopy = self.getbasecopy(source) appliedlogs = self.applylogs(basecopy) return appliedlogs def lastpwchangenow(self): ''' Return time of last password change for the user set to current time messured in days from UNIX epoch ''' return str((datetime.utcnow() - datetime(1970,1,1)).days) class ldapsync(): def __init__(self): self.servers = [] def add(self, log_server: logserver)->list: self.servers.append(log_server) return self.servers def remove(self, log_server: logserver)->list: self.servers.remove(log_server) return self.servers def pullfromall(self, log_server: logserver)->int: pulledlogs = 0 for source in self.servers: if source != log_server: pulledlogs += log_server.pullfrom(source) return pulledlogs def pull(self)->int: pulledlogs = 0 for server in self.servers: pulledlogs += self.pullfromall(server) return pulledlogs def applyfromall(self, log_server: logserver)->int: appliedlogs = 0 for source in self.servers: if source != log_server: appliedlogs += log_server.applyfrom(source) return appliedlogs def apply(self)->int: appliedlogs = 0 for server in self.servers: appliedlogs += self.applyfromall(server) return appliedlogs def sync(self)->int: pulledlogs = self.pull() appliedlogs = self.apply() return appliedlogs