Add LdapClient
This commit is contained in:
parent
1cd9432e25
commit
a325803130
1 changed files with 311 additions and 0 deletions
311
mylib/ldap.py
311
mylib/ldap.py
|
@ -14,6 +14,10 @@ from ldap.controls.simple import RelaxRulesControl
|
||||||
import ldap.modlist as modlist
|
import ldap.modlist as modlist
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
from mylib import pretty_format_dict
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LdapServer:
|
class LdapServer:
|
||||||
""" LDAP server connection helper """ # pylint: disable=useless-object-inheritance
|
""" LDAP server connection helper """ # pylint: disable=useless-object-inheritance
|
||||||
|
@ -314,6 +318,313 @@ class LdapServerException(BaseException):
|
||||||
def __init__(self, msg):
|
def __init__(self, msg):
|
||||||
BaseException.__init__(self, msg)
|
BaseException.__init__(self, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class LdapClientException(LdapServerException):
|
||||||
|
""" Generic exception raised by LdapServer """
|
||||||
|
|
||||||
|
def __init__(self, msg):
|
||||||
|
LdapServerException.__init__(self, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class LdapClient:
|
||||||
|
|
||||||
|
""" LDAP Client (based on python-mylib.LdapServer) """
|
||||||
|
|
||||||
|
options = {}
|
||||||
|
|
||||||
|
# Cache objects
|
||||||
|
_cached_objects = dict()
|
||||||
|
|
||||||
|
def __init__(self, options):
|
||||||
|
self.options = options
|
||||||
|
log.info("Connect to LDAP server %s as %s", options.ldap_uri, options.ldap_binddn)
|
||||||
|
self.cnx = LdapServer(options.ldap_uri, dn=options.ldap_binddn, pwd=options.ldap_bindpwd, raiseOnError=True)
|
||||||
|
self.cnx.connect()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def decode(cls, value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [cls.decode(v) for v in value]
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value
|
||||||
|
return value.decode('utf-8', 'ignore')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def encode(cls, value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [cls.encode(v) for v in value]
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
return value
|
||||||
|
return value.encode('utf-8')
|
||||||
|
|
||||||
|
def get_attrs(self, dn, attrs):
|
||||||
|
obj = dict(dn=dn)
|
||||||
|
for attr in attrs:
|
||||||
|
obj[attr] = [self.decode(v) for v in self.cnx.get_attr(attrs, attr, all=True)]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_attr(obj, attr, default="", all_values=False):
|
||||||
|
vals = obj.get(attr, [])
|
||||||
|
if vals:
|
||||||
|
return vals if all_values else vals[0]
|
||||||
|
return default if default or not all_values else []
|
||||||
|
|
||||||
|
def get_objects(self, name, filterstr, basedn, attrs, key_attr=None, warn=True):
|
||||||
|
if name in self._cached_objects:
|
||||||
|
log.debug('Retreived %s objects from cache', name)
|
||||||
|
else:
|
||||||
|
log.debug('Looking for LDAP %s with (filter="%s" / basedn="%s")', name, filterstr, basedn)
|
||||||
|
ldap_data = self.cnx.search(
|
||||||
|
basedn=basedn,
|
||||||
|
filterstr=filterstr,
|
||||||
|
attrs=attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ldap_data:
|
||||||
|
if warn:
|
||||||
|
log.warning('No %s found in LDAP', name)
|
||||||
|
else:
|
||||||
|
log.debug('No %s found in LDAP', name)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
objects = {}
|
||||||
|
for obj_dn, obj_attrs in ldap_data.items():
|
||||||
|
objects[obj_dn] = self.get_attrs(obj_dn, obj_attrs)
|
||||||
|
self._cached_objects[name] = objects
|
||||||
|
if not key_attr or key_attr == 'dn':
|
||||||
|
return self._cached_objects[name]
|
||||||
|
return dict(
|
||||||
|
(self.get_attr(self._cached_objects[name][dn], key_attr), self._cached_objects[name][dn])
|
||||||
|
for dn in self._cached_objects[name]
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_object(self, type_name, object_name, filterstr, basedn, attrs, warn=True):
|
||||||
|
log.debug('Looking for LDAP %s "%s" with (filter="%s" / basedn="%s")', type_name, object_name, filterstr, basedn)
|
||||||
|
ldap_data = self.cnx.search(
|
||||||
|
basedn=basedn, filterstr=filterstr,
|
||||||
|
attrs=attrs
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ldap_data:
|
||||||
|
if warn:
|
||||||
|
log.warning('No %s "%s" found in LDAP', type_name, object_name)
|
||||||
|
else:
|
||||||
|
log.debug('No %s "%s" found in LDAP', type_name, object_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(ldap_data) > 1:
|
||||||
|
raise LdapClientException('More than one %s "%s": %s' % (type_name, object_name, ' / '.join(ldap_data.keys())))
|
||||||
|
|
||||||
|
dn = next(iter(ldap_data))
|
||||||
|
return self.get_attrs(dn, ldap_data[dn])
|
||||||
|
|
||||||
|
def get_object_by_dn(self, type_name, dn, populate_cache_method=None, warn=True):
|
||||||
|
if type_name not in self._cached_objects:
|
||||||
|
if not populate_cache_method:
|
||||||
|
return False
|
||||||
|
populate_cache_method()
|
||||||
|
if dn not in self._cached_objects[type_name]:
|
||||||
|
if warn:
|
||||||
|
log.warning('No %s found with DN "%s"', type_name, dn)
|
||||||
|
else:
|
||||||
|
log.debug('No %s found with DN "%s"', type_name, dn)
|
||||||
|
return None
|
||||||
|
return self._cached_objects[type_name][dn]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def object_attr_mached(cls, obj, attr, value, case_sensitive=False):
|
||||||
|
if case_sensitive:
|
||||||
|
return value in cls.get_attr(obj, attr, all_values=True)
|
||||||
|
return value.lower() in [v.lower() for v in cls.get_attr(obj, attr, all_values=True)]
|
||||||
|
|
||||||
|
def get_object_by_attr(self, type_name, attr, value, populate_cache_method=None, case_sensitive=False, warn=True):
|
||||||
|
if type_name not in self._cached_objects:
|
||||||
|
if not populate_cache_method:
|
||||||
|
return False
|
||||||
|
populate_cache_method()
|
||||||
|
matched = dict(
|
||||||
|
(dn, obj)
|
||||||
|
for dn, obj in self._cached_objects[type_name].items()
|
||||||
|
if self.object_attr_mached(obj, attr, value, case_sensitive=case_sensitive)
|
||||||
|
)
|
||||||
|
if not matched:
|
||||||
|
if warn:
|
||||||
|
log.warning('No %s found with %s="%s"', type_name, attr, value)
|
||||||
|
else:
|
||||||
|
log.debug('No %s found with %s="%s"', type_name, attr, value)
|
||||||
|
return None
|
||||||
|
if len(matched) > 1:
|
||||||
|
raise LdapClientException('More than one %s with %s="%s" found: %s' % (type_name, attr, value , ' / '.join(matched.keys())))
|
||||||
|
|
||||||
|
dn = next(iter(matched))
|
||||||
|
return matched[dn]
|
||||||
|
|
||||||
|
def get_changes(self, ldap_obj, attrs, protected_attrs=None):
|
||||||
|
"""
|
||||||
|
Retrieve changes on a LDAP object
|
||||||
|
|
||||||
|
:param ldap_obj: The original LDAP object
|
||||||
|
:param attrs: The new LDAP object attributes values
|
||||||
|
:param protected_attrs: Optional list of protected attributes
|
||||||
|
"""
|
||||||
|
old = {}
|
||||||
|
new = {}
|
||||||
|
protected_attrs = [a.lower() for a in protected_attrs or list()]
|
||||||
|
protected_attrs.append('dn')
|
||||||
|
# New/updated attributes
|
||||||
|
for attr in attrs:
|
||||||
|
if protected_attrs and attr.lower() in protected_attrs:
|
||||||
|
continue
|
||||||
|
if attr in ldap_obj and ldap_obj[attr]:
|
||||||
|
if sorted(ldap_obj[attr]) == sorted(attrs[attr]):
|
||||||
|
continue
|
||||||
|
old[attr] = self.encode(ldap_obj[attr])
|
||||||
|
new[attr] = self.encode(attrs[attr])
|
||||||
|
|
||||||
|
# Deleted attributes
|
||||||
|
for attr in ldap_obj:
|
||||||
|
if (not protected_attrs or attr.lower() not in protected_attrs) and ldap_obj[attr] and attr not in attrs:
|
||||||
|
old[attr] = self.encode(ldap_obj[attr])
|
||||||
|
if old == new:
|
||||||
|
return None
|
||||||
|
return (old, new)
|
||||||
|
|
||||||
|
def add_object(self, dn, attrs):
|
||||||
|
"""
|
||||||
|
Add an object
|
||||||
|
|
||||||
|
:param dn: The LDAP object DN
|
||||||
|
:param attrs: The LDAP object attributes (as dict)
|
||||||
|
"""
|
||||||
|
attrs = dict(
|
||||||
|
(attr, self.encode(values))
|
||||||
|
for attr, values in attrs.items()
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
if self.options.just_try:
|
||||||
|
log.debug('Just-try mode : do not really add object in LDAP')
|
||||||
|
return True
|
||||||
|
return self.cnx.add_object(dn, attrs)
|
||||||
|
except LdapServerException:
|
||||||
|
log.error(
|
||||||
|
"An error occurred adding object %s in LDAP:\n%s\n",
|
||||||
|
dn, pretty_format_dict(attrs), exc_info=True
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_object(self, ldap_obj, changes, protected_attrs=None, rdn_attr=None):
|
||||||
|
"""
|
||||||
|
Update an object
|
||||||
|
|
||||||
|
:param ldap_obj: The original LDAP object
|
||||||
|
:param changes: The changes to make on LDAP object (as formated by get_changes() method)
|
||||||
|
:param protected_attrs: An optional list of protected attributes
|
||||||
|
:param rdn_attr: The LDAP object RDN attribute (to detect renaming, default: auto-detected)
|
||||||
|
"""
|
||||||
|
assert isinstance(changes, (list, tuple)) and len(changes) == 2 and isinstance(changes[0], dict) and isinstance(changes[1], dict), "changes parameter must be a result of get_changes() method (%s given)" % type(changes)
|
||||||
|
if not rdn_attr:
|
||||||
|
rdn_attr = ldap_obj['dn'].split('=')[0]
|
||||||
|
log.debug('Auto-detected RDN attribute from DN: %s => %s', ldap_obj['dn'], rdn_attr)
|
||||||
|
old_rdn_values = self.get_attr(changes[0], rdn_attr, all_values=True)
|
||||||
|
new_rdn_values = self.get_attr(changes[1], rdn_attr, all_values=True)
|
||||||
|
if old_rdn_values or new_rdn_values:
|
||||||
|
if not new_rdn_values:
|
||||||
|
log.error(
|
||||||
|
"%s : Attribute %s can't be deleted because it's used as RDN.",
|
||||||
|
ldap_obj['dn'], rdn_attr
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
log.debug(
|
||||||
|
'%s: Changes detected on %s RDN attribute: must rename object before updating it',
|
||||||
|
ldap_obj['dn'], rdn_attr
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compute new object DN
|
||||||
|
dn_parts = ldap_obj['dn'].split(',')
|
||||||
|
basedn = ','.join(dn_parts[1:])
|
||||||
|
new_rdn = '%s=%s' % (rdn_attr, new_rdn_values[0])
|
||||||
|
new_dn = '%s,%s' % (new_rdn, basedn)
|
||||||
|
|
||||||
|
# Rename object
|
||||||
|
log.debug('%s: Rename to %s', ldap_obj['dn'], new_dn)
|
||||||
|
if not self.move_object(ldap_obj, new_rdn):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Remove RDN in changes list
|
||||||
|
for attr in changes[0].keys():
|
||||||
|
if attr.lower() == rdn_attr.lower():
|
||||||
|
del changes[0][attr]
|
||||||
|
for attr in changes[1].keys():
|
||||||
|
if attr.lower() == rdn_attr.lower():
|
||||||
|
del changes[1][attr]
|
||||||
|
|
||||||
|
# Check that there are other changes
|
||||||
|
if not changes[0] and not changes[1]:
|
||||||
|
log.debug('%s: No other change after renaming', new_dn)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Otherwise, update object DN
|
||||||
|
ldap_obj['dn'] = new_dn
|
||||||
|
else:
|
||||||
|
log.debug('%s: No change detected on RDN attibute %s', ldap_obj['dn'], rdn_attr)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.options.just_try:
|
||||||
|
log.debug('Just-try mode : do not really update object in LDAP')
|
||||||
|
return True
|
||||||
|
return self.cnx.update_object(
|
||||||
|
ldap_obj['dn'],
|
||||||
|
changes[0],
|
||||||
|
changes[1],
|
||||||
|
ignore_attrs=protected_attrs
|
||||||
|
)
|
||||||
|
except LdapServerException:
|
||||||
|
log.error(
|
||||||
|
"An error occurred updating object %s in LDAP:\n%s\n -> \n%s\n\n",
|
||||||
|
ldap_obj['dn'], pretty_format_dict(changes[0]), pretty_format_dict(changes[1]),
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def move_object(self, ldap_obj, new_dn_or_rdn):
|
||||||
|
"""
|
||||||
|
Move/rename an object
|
||||||
|
|
||||||
|
:param ldap_obj: The original LDAP object
|
||||||
|
:param new_dn_or_rdn: The new LDAP object's DN (or RDN)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.options.just_try:
|
||||||
|
log.debug('Just-try mode : do not really move object in LDAP')
|
||||||
|
return True
|
||||||
|
return self.cnx.rename_object(ldap_obj['dn'], new_dn_or_rdn)
|
||||||
|
except LdapServerException:
|
||||||
|
log.error(
|
||||||
|
"An error occurred moving object %s in LDAP (destination: %s)",
|
||||||
|
ldap_obj['dn'], new_dn_or_rdn, exc_info=True
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def drop_object(self, ldap_obj):
|
||||||
|
"""
|
||||||
|
Drop/delete an object
|
||||||
|
|
||||||
|
:param ldap_obj: The original LDAP object to delete/drop
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.options.just_try:
|
||||||
|
log.debug('Just-try mode : do not really drop object in LDAP')
|
||||||
|
return True
|
||||||
|
return self.cnx.drop_object(ldap_obj['dn'])
|
||||||
|
except LdapServerException:
|
||||||
|
log.error(
|
||||||
|
"An error occurred removing object %s in LDAP",
|
||||||
|
ldap_obj['dn'], exc_info=True
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# LDAP date string helpers
|
# LDAP date string helpers
|
||||||
#
|
#
|
||||||
|
|
Loading…
Reference in a new issue