|
|
|
@ -1,4 +1,6 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
""" LDAP server connection helper """
|
|
|
|
|
|
|
|
|
|
import copy
|
|
|
|
|
import datetime
|
|
|
|
@ -12,7 +14,9 @@ from ldap.controls.simple import RelaxRulesControl
|
|
|
|
|
import ldap.modlist as modlist
|
|
|
|
|
import pytz
|
|
|
|
|
|
|
|
|
|
class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
|
|
|
|
|
class LdapServer:
|
|
|
|
|
""" LDAP server connection helper """ # pylint: disable=useless-object-inheritance
|
|
|
|
|
|
|
|
|
|
uri = None
|
|
|
|
|
dn = None
|
|
|
|
@ -21,24 +25,25 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
|
|
|
|
|
con = 0
|
|
|
|
|
|
|
|
|
|
def __init__(self,uri,dn=None,pwd=None,v2=None,raiseOnError=False, logger=False):
|
|
|
|
|
self.uri = uri
|
|
|
|
|
self.dn = dn
|
|
|
|
|
self.pwd = pwd
|
|
|
|
|
self.raiseOnError = raiseOnError
|
|
|
|
|
def __init__(self, uri, dn=None, pwd=None, v2=None, raiseOnError=False, logger=False):
|
|
|
|
|
self.uri = uri
|
|
|
|
|
self.dn = dn
|
|
|
|
|
self.pwd = pwd
|
|
|
|
|
self.raiseOnError = raiseOnError
|
|
|
|
|
if v2:
|
|
|
|
|
self.v2=True
|
|
|
|
|
self.v2 = True
|
|
|
|
|
if logger:
|
|
|
|
|
self.logger = logger
|
|
|
|
|
else:
|
|
|
|
|
self.logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
def _error(self,error,level=logging.WARNING):
|
|
|
|
|
def _error(self, error, level=logging.WARNING):
|
|
|
|
|
if self.raiseOnError:
|
|
|
|
|
raise LdapServerException(error)
|
|
|
|
|
self.logger.log(level, error)
|
|
|
|
|
|
|
|
|
|
def connect(self):
|
|
|
|
|
""" Start connection to LDAP server """
|
|
|
|
|
if self.con == 0:
|
|
|
|
|
try:
|
|
|
|
|
con = ldap.initialize(self.uri)
|
|
|
|
@ -48,19 +53,20 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
con.protocol_version = ldap.VERSION3 # pylint: disable=no-member
|
|
|
|
|
|
|
|
|
|
if self.dn:
|
|
|
|
|
con.simple_bind_s(self.dn,self.pwd)
|
|
|
|
|
con.simple_bind_s(self.dn, self.pwd)
|
|
|
|
|
elif self.uri.startswith('ldapi://'):
|
|
|
|
|
con.sasl_interactive_bind_s("", ldap.sasl.external())
|
|
|
|
|
|
|
|
|
|
self.con = con
|
|
|
|
|
return True
|
|
|
|
|
except ldap.LDAPError as e: # pylint: disable=no-member
|
|
|
|
|
self._error('LdapServer - Error connecting and binding to LDAP server : %s' % e,logging.CRITICAL)
|
|
|
|
|
self._error('LdapServer - Error connecting and binding to LDAP server : %s' % e, logging.CRITICAL)
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_scope(scope):
|
|
|
|
|
""" Map scope parameter to python-ldap value """
|
|
|
|
|
if scope == 'base':
|
|
|
|
|
return ldap.SCOPE_BASE # pylint: disable=no-member
|
|
|
|
|
if scope == 'one':
|
|
|
|
@ -70,6 +76,7 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
raise Exception("Unknown LDAP scope '%s'" % scope)
|
|
|
|
|
|
|
|
|
|
def search(self, basedn, filterstr=None, attrs=None, sizelimit=0, scope=None):
|
|
|
|
|
""" Run a search on LDAP server """
|
|
|
|
|
res_id = self.con.search(
|
|
|
|
|
basedn,
|
|
|
|
|
self.get_scope(scope if scope else 'sub'),
|
|
|
|
@ -79,7 +86,7 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
ret = {}
|
|
|
|
|
c = 0
|
|
|
|
|
while True:
|
|
|
|
|
res_type, res_data = self.con.result(res_id,0)
|
|
|
|
|
res_type, res_data = self.con.result(res_id, 0)
|
|
|
|
|
if res_data == [] or (sizelimit and c > sizelimit):
|
|
|
|
|
break
|
|
|
|
|
if res_type == ldap.RES_SEARCH_ENTRY: # pylint: disable=no-member
|
|
|
|
@ -88,10 +95,12 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
def get_object(self, dn, filterstr=None, attrs=None):
|
|
|
|
|
""" Retrieve a LDAP object specified by its DN """
|
|
|
|
|
result = self.search(dn, filterstr=filterstr, scope='base', attrs=attrs)
|
|
|
|
|
return result[dn] if dn in result else None
|
|
|
|
|
|
|
|
|
|
def paged_search(self, basedn, filterstr, attrs, scope='sub', pagesize=500):
|
|
|
|
|
""" Run a paged search on LDAP server """
|
|
|
|
|
assert not self.v2, "Paged search is not available on LDAP version 2"
|
|
|
|
|
# Initialize SimplePagedResultsControl object
|
|
|
|
|
page_control = SimplePagedResultsControl(
|
|
|
|
@ -161,18 +170,20 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
self.logger.debug("LdapServer - Paged search end: %d object(s) retreived in %d page(s) of %d object(s)", len(ret), pages_count, pagesize)
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
def add_object(self,dn,attrs):
|
|
|
|
|
def add_object(self, dn, attrs):
|
|
|
|
|
""" Add an object in LDAP directory """
|
|
|
|
|
ldif = modlist.addModlist(attrs)
|
|
|
|
|
try:
|
|
|
|
|
self.logger.debug("LdapServer - Add %s", dn)
|
|
|
|
|
self.con.add_s(dn,ldif)
|
|
|
|
|
self.con.add_s(dn, ldif)
|
|
|
|
|
return True
|
|
|
|
|
except ldap.LDAPError as e: # pylint: disable=no-member
|
|
|
|
|
self._error("LdapServer - Error adding %s : %s" % (dn,e), logging.ERROR)
|
|
|
|
|
self._error("LdapServer - Error adding %s : %s" % (dn, e), logging.ERROR)
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def update_object(self, dn, old, new, ignore_attrs=None, relax=False):
|
|
|
|
|
""" Update an object in LDAP directory """
|
|
|
|
|
assert not relax or not self.v2, "Relax modification is not available on LDAP version 2"
|
|
|
|
|
ldif = modlist.modifyModlist(
|
|
|
|
|
old, new,
|
|
|
|
@ -192,6 +203,7 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def update_need(old, new, ignore_attrs=None):
|
|
|
|
|
""" Check if an update is need on a LDAP object based on its old and new attributes values """
|
|
|
|
|
ldif = modlist.modifyModlist(
|
|
|
|
|
old, new,
|
|
|
|
|
ignore_attr_types=ignore_attrs if ignore_attrs else []
|
|
|
|
@ -202,6 +214,7 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_changes(old, new, ignore_attrs=None):
|
|
|
|
|
""" Retrieve changes (as modlist) on an object based on its old and new attributes values """
|
|
|
|
|
return modlist.modifyModlist(
|
|
|
|
|
old, new,
|
|
|
|
|
ignore_attr_types=ignore_attrs if ignore_attrs else []
|
|
|
|
@ -209,6 +222,7 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def format_changes(old, new, ignore_attrs=None, prefix=None):
|
|
|
|
|
""" Format changes (modlist) on an object based on its old and new attributes values to display/log it """
|
|
|
|
|
msg = []
|
|
|
|
|
for (op, attr, val) in modlist.modifyModlist(old, new, ignore_attr_types=ignore_attrs if ignore_attrs else []):
|
|
|
|
|
if op == ldap.MOD_ADD: # pylint: disable=no-member
|
|
|
|
@ -226,6 +240,7 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
return '\n'.join(msg)
|
|
|
|
|
|
|
|
|
|
def rename_object(self, dn, new_rdn, new_sup=None, delete_old=True):
|
|
|
|
|
""" Rename an object in LDAP directory """
|
|
|
|
|
# If new_rdn is a complete DN, split new RDN and new superior DN
|
|
|
|
|
if len(new_rdn.split(',')) > 1:
|
|
|
|
|
self.logger.debug(
|
|
|
|
@ -261,21 +276,24 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def drop_object(self, dn):
|
|
|
|
|
""" Drop an object in LDAP directory """
|
|
|
|
|
try:
|
|
|
|
|
self.logger.debug("LdapServer - Delete %s", dn)
|
|
|
|
|
self.con.delete_s(dn)
|
|
|
|
|
return True
|
|
|
|
|
except ldap.LDAPError as e: # pylint: disable=no-member
|
|
|
|
|
self._error("LdapServer - Error deleting %s : %s" % (dn,e), logging.ERROR)
|
|
|
|
|
self._error("LdapServer - Error deleting %s : %s" % (dn, e), logging.ERROR)
|
|
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_dn(obj):
|
|
|
|
|
""" Retreive an on object DN from its entry in LDAP search result """
|
|
|
|
|
return obj[0][0]
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_attr(obj, attr, all=None, default=None):
|
|
|
|
|
""" Retreive an on object attribute value(s) from the object entry in LDAP search result """
|
|
|
|
|
if attr not in obj:
|
|
|
|
|
for k in obj:
|
|
|
|
|
if k.lower() == attr.lower():
|
|
|
|
@ -289,13 +307,18 @@ class LdapServer(object): # pylint: disable=useless-object-inheritance
|
|
|
|
|
return obj[attr][0]
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LdapServerException(BaseException):
|
|
|
|
|
def __init__(self,msg):
|
|
|
|
|
""" Generic exception raised by LdapServer """
|
|
|
|
|
|
|
|
|
|
def __init__(self, msg):
|
|
|
|
|
BaseException.__init__(self, msg)
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
|
# Helpers
|
|
|
|
|
# LDAP date string helpers
|
|
|
|
|
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_datetime(value, to_timezone=None, default_timezone=None, naive=None):
|
|
|
|
|
"""
|
|
|
|
|
Convert LDAP date string to datetime.datetime object
|
|
|
|
@ -335,6 +358,7 @@ def parse_datetime(value, to_timezone=None, default_timezone=None, naive=None):
|
|
|
|
|
return date.astimezone(to_timezone)
|
|
|
|
|
return date
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_date(value, to_timezone=None, default_timezone=None, naive=None):
|
|
|
|
|
"""
|
|
|
|
|
Convert LDAP date string to datetime.date object
|
|
|
|
@ -348,6 +372,7 @@ def parse_date(value, to_timezone=None, default_timezone=None, naive=None):
|
|
|
|
|
"""
|
|
|
|
|
return parse_datetime(value, to_timezone, default_timezone, naive).date()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_datetime(value, from_timezone=None, to_timezone=None, naive=None):
|
|
|
|
|
"""
|
|
|
|
|
Convert datetime.datetime object to LDAP date string
|
|
|
|
@ -388,6 +413,7 @@ def format_datetime(value, from_timezone=None, to_timezone=None, naive=None):
|
|
|
|
|
datestring = datestring.replace('+0000', 'Z')
|
|
|
|
|
return datestring
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format_date(value, from_timezone=None, to_timezone=None, naive=None):
|
|
|
|
|
"""
|
|
|
|
|
Convert datetime.date object to LDAP date string
|
|
|
|
|