2024-02-08 17:21:39 +00:00
|
|
|
import ldap3
|
2024-03-01 09:07:39 +00:00
|
|
|
import random
|
2024-04-05 11:50:34 +00:00
|
|
|
from datetime import datetime
|
2024-02-08 17:21:39 +00:00
|
|
|
|
|
|
|
MAXENTRIES = 1000000
|
|
|
|
OBJECTCLASSES = ['top', 'person', 'organizationalPerson', 'inetOrgPerson', 'posixAccount', 'shadowAccount']
|
|
|
|
USERATTRIBUTES = ['cn' , 'sn', 'givenName', 'uid', 'uidNumber' , 'gidNumber', 'homeDirectory', 'loginShell', 'gecos' , 'shadowLastChange', 'shadowMax', 'userPassword', 'mail', 'description']
|
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
|
|
|
|
class logserver():
|
|
|
|
def __init__(self, _host: str, admin_user: str, admin_pass: str, base: str, ssl: bool = True ):
|
|
|
|
|
|
|
|
self._host = ldap_host
|
2024-02-08 17:21:39 +00:00
|
|
|
self.admin_user = admin_user
|
2024-03-01 09:07:39 +00:00
|
|
|
self.admin_pass = admin_pass
|
2024-02-08 17:21:39 +00:00
|
|
|
self.base = base
|
2024-04-05 11:50:34 +00:00
|
|
|
self.ssl = ssl
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
if ssl:
|
|
|
|
server = ldap3.Server(self.ldap_host,use_ssl=True)
|
|
|
|
else:
|
|
|
|
server = ldap3.Server(self.ldap_host)
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
self.conn = ldap3.Connection(ldapserver, admin_user, admin_pass, auto_bind=True)
|
2024-03-01 09:07:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
self.organization, self.dc, self.dcfull, self.domain = self.expandbase()
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
self.logbase = f'ou=log,{self.dc}'
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-03-01 09:07:39 +00:00
|
|
|
# Unique id of the log branch
|
|
|
|
self.id = self.getid()
|
|
|
|
|
2024-02-08 17:21:39 +00:00
|
|
|
# How many changes we applied to the LDAP server
|
|
|
|
self.loaded = self.getloaded()
|
|
|
|
|
|
|
|
# How many changes are recoreded in total
|
|
|
|
self.total = self.gettotal()
|
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
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.logbase
|
|
|
|
|
|
|
|
# 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
|
2024-03-01 09:07:39 +00:00
|
|
|
if response == []:
|
|
|
|
id = random.randint(10**12, 10**13-1)
|
2024-04-05 11:50:34 +00:00
|
|
|
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)
|
2024-03-01 09:07:39 +00:00
|
|
|
return id
|
2024-04-05 11:50:34 +00:00
|
|
|
else:
|
|
|
|
response = int(response[0]['attributes']['uidNumber'])
|
|
|
|
|
|
|
|
return response
|
2024-03-01 09:07:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
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
|
2024-02-08 17:21:39 +00:00
|
|
|
if response == []:
|
2024-04-05 11:50:34 +00:00
|
|
|
response = 0
|
|
|
|
else:
|
|
|
|
response = int(response[0]['attributes']['uidNumber'])
|
|
|
|
return response
|
|
|
|
|
|
|
|
def settotal(self, newvalue: int, base: str = ''):
|
|
|
|
if base == '':
|
|
|
|
base = self.logbase
|
2024-02-08 17:21:39 +00:00
|
|
|
|
|
|
|
newvalue = int(newvalue)
|
|
|
|
|
|
|
|
# Check if total record already present
|
2024-04-05 11:50:34 +00:00
|
|
|
self.conn.search(search_base=f'uid=total,{base}',search_filter='(objectClass=person)', attributes=['uidNumber'])
|
|
|
|
response = self.conn.response
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
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}' }
|
|
|
|
|
2024-02-08 17:21:39 +00:00
|
|
|
if response == []:
|
2024-04-05 11:50:34 +00:00
|
|
|
self.conn.add(f'uid=total,{base}', OBJECTCLASSES, attributes)
|
|
|
|
else:
|
|
|
|
self.conn.modify(f'uid=total,{base}', {'uidNumber' : (ldap3.MODIFY_REPLACE, [newvalue])})
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
return self.conn.response
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
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
|
2024-02-08 17:21:39 +00:00
|
|
|
if response == []:
|
2024-04-05 11:50:34 +00:00
|
|
|
response = 0
|
|
|
|
else:
|
|
|
|
response = int(response[0]['attributes']['uidNumber'])
|
|
|
|
return response
|
|
|
|
|
|
|
|
def setloaded(self, newvalue: int, base: str = ''):
|
|
|
|
if base == '':
|
|
|
|
base = self.logbase
|
2024-02-08 17:21:39 +00:00
|
|
|
|
|
|
|
newvalue = int(newvalue)
|
|
|
|
|
|
|
|
# Check if loaded record already present
|
2024-04-05 11:50:34 +00:00
|
|
|
self.conn.search(search_base=f'uid=loaded,{base}',search_filter='(objectClass=person)', attributes=['uidNumber'])
|
|
|
|
response = self.conn.response
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
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}' }
|
|
|
|
|
2024-02-08 17:21:39 +00:00
|
|
|
if response == []:
|
2024-04-05 11:50:34 +00:00
|
|
|
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
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
def refreshtotal(self, base: str = '')->int:
|
|
|
|
if base == '':
|
|
|
|
base = self.logbase
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
self.settotal(self.findtotal(base))
|
|
|
|
return self.gettotal(base)
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
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 == []:
|
2024-02-08 17:21:39 +00:00
|
|
|
return entry - 1
|
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
def applylogs(self, base: str = '')->int:
|
|
|
|
if base == '':
|
|
|
|
base = self.logbase
|
|
|
|
|
|
|
|
self.refreshtotal(base)
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
loaded = self.getloaded(base) + 1
|
|
|
|
total = self.gettotal(base) + 1
|
2024-02-08 17:21:39 +00:00
|
|
|
|
|
|
|
for log_number in range(loaded, total):
|
2024-04-05 11:50:34 +00:00
|
|
|
self.applylog(log_number, base)
|
2024-02-08 17:21:39 +00:00
|
|
|
|
|
|
|
return total - loaded
|
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
def applylog(self, log_number: int, base: str = ''):
|
|
|
|
if base == '':
|
|
|
|
base = self.logbase
|
|
|
|
|
|
|
|
log = self.getlog(log_number, base)
|
|
|
|
|
|
|
|
attributes = log['attribtes']
|
|
|
|
uid = attributes['uid']
|
|
|
|
action = attributes['description']
|
|
|
|
attributes['description'] = ''
|
|
|
|
|
|
|
|
if action == 'ADD':
|
|
|
|
self.conn.add(f'uid={uid},{base}', OBJECTCLASSES, attributes)
|
|
|
|
elif action == 'DELETE':
|
|
|
|
self.conn.delete(f'uid={uid},{base}')
|
|
|
|
elif action == 'CHANGEPASS':
|
|
|
|
self.conn.modify(f'uid={uid},{base}', {'userPassword': (MODIFY_REPLACE,attributes['userPassword'])})
|
|
|
|
else:
|
|
|
|
return f'Error: Unrecognized action in log entry: {action}'
|
|
|
|
|
|
|
|
self.setloaded(self.getloaded() + 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)
|
|
|
|
return self.conn.response[0]
|
|
|
|
|
|
|
|
def setlog(self, attributes: dict, base: str = '', log_number: int = -1):
|
|
|
|
if base == '':
|
|
|
|
base = self.logbase
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
if log_number == -1:
|
|
|
|
log_number = self.gettotal(base) + 1
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
self.conn.add(f'uid={log_number},{base}', OBJECTCLASSES, attributes)
|
|
|
|
return self.conn.response
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
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()
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
basecopy = f'ou=sync-{remoteid},{localdcfull}'
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
# 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}'})
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
return basecopy
|
|
|
|
|
|
|
|
def pullfrom(self, source)->int:
|
|
|
|
basecopy = self.getbasecopy(source)
|
|
|
|
|
|
|
|
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)
|
|
|
|
attributes = log['attributes']
|
|
|
|
|
|
|
|
self.setlog(attributes, basecopy)
|
|
|
|
|
|
|
|
return total_src - total_dest
|
|
|
|
|
|
|
|
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():
|
2024-03-01 09:07:39 +00:00
|
|
|
def __init__(self):
|
2024-04-05 11:50:34 +00:00
|
|
|
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()
|
2024-02-08 17:21:39 +00:00
|
|
|
|
2024-04-05 11:50:34 +00:00
|
|
|
return appliedlogs
|