LdapClient: add support to config lib
This commit is contained in:
parent
dc0f17dd20
commit
73c19816f7
1 changed files with 189 additions and 31 deletions
220
mylib/ldap.py
220
mylib/ldap.py
|
@ -5,6 +5,7 @@
|
|||
import copy
|
||||
import datetime
|
||||
import logging
|
||||
import pytz
|
||||
|
||||
import dateutil.parser
|
||||
import dateutil.tz
|
||||
|
@ -12,11 +13,11 @@ import ldap
|
|||
from ldap.controls import SimplePagedResultsControl
|
||||
from ldap.controls.simple import RelaxRulesControl
|
||||
import ldap.modlist as modlist
|
||||
import pytz
|
||||
|
||||
from mylib import pretty_format_dict
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
|
||||
|
||||
class LdapServer:
|
||||
|
@ -81,6 +82,7 @@ class LdapServer:
|
|||
|
||||
def search(self, basedn, filterstr=None, attrs=None, sizelimit=0, scope=None):
|
||||
""" Run a search on LDAP server """
|
||||
assert self.con or self.connect()
|
||||
res_id = self.con.search(
|
||||
basedn,
|
||||
self.get_scope(scope if scope else 'sub'),
|
||||
|
@ -106,6 +108,7 @@ class LdapServer:
|
|||
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"
|
||||
assert self.con or self.connect()
|
||||
# Initialize SimplePagedResultsControl object
|
||||
page_control = SimplePagedResultsControl(
|
||||
True,
|
||||
|
@ -177,6 +180,7 @@ class LdapServer:
|
|||
def add_object(self, dn, attrs):
|
||||
""" Add an object in LDAP directory """
|
||||
ldif = modlist.addModlist(attrs)
|
||||
assert self.con or self.connect()
|
||||
try:
|
||||
self.logger.debug("LdapServer - Add %s", dn)
|
||||
self.con.add_s(dn, ldif)
|
||||
|
@ -195,6 +199,7 @@ class LdapServer:
|
|||
)
|
||||
if ldif == []:
|
||||
return True
|
||||
assert self.con or self.connect()
|
||||
try:
|
||||
if relax:
|
||||
self.con.modify_ext_s(dn, ldif, serverctrls=[RelaxRulesControl()])
|
||||
|
@ -256,6 +261,7 @@ class LdapServer:
|
|||
new_dn_parts = new_rdn.split(',')
|
||||
new_sup = ','.join(new_dn_parts[1:])
|
||||
new_rdn = new_dn_parts[0]
|
||||
assert self.con or self.connect()
|
||||
try:
|
||||
self.logger.debug(
|
||||
"LdapServer - Rename %s in %s (new superior: %s, delete old: %s)",
|
||||
|
@ -282,6 +288,7 @@ class LdapServer:
|
|||
|
||||
def drop_object(self, dn):
|
||||
""" Drop an object in LDAP directory """
|
||||
assert self.con or self.connect()
|
||||
try:
|
||||
self.logger.debug("LdapServer - Delete %s", dn)
|
||||
self.con.delete_s(dn)
|
||||
|
@ -331,52 +338,139 @@ class LdapClient:
|
|||
|
||||
""" LDAP Client (based on python-mylib.LdapServer) """
|
||||
|
||||
options = {}
|
||||
_options = {}
|
||||
_config = None
|
||||
_config_section = None
|
||||
|
||||
# Connection
|
||||
_conn = None
|
||||
|
||||
# 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()
|
||||
def __init__(self, options=None, options_prefix=None, config=None, config_section=None, initialize=False):
|
||||
self._options = options if options else {}
|
||||
self._options_prefix = options_prefix if options_prefix else 'ldap_'
|
||||
self._config = config if config else None
|
||||
self._config_section = config_section if config_section else 'ldap'
|
||||
if initialize:
|
||||
self.initialize()
|
||||
|
||||
@classmethod
|
||||
def decode(cls, value):
|
||||
def __get_option(self, option, default=None, required=False):
|
||||
""" Retreive option value """
|
||||
if self._options and hasattr(self._options, self._options_prefix + option):
|
||||
return getattr(self._options, self._options_prefix + option)
|
||||
|
||||
if self._config and self._config.defined(self._config_section, option):
|
||||
return self._config.get(self._config_section, option)
|
||||
|
||||
assert not required, "Options %s not defined" % option
|
||||
|
||||
return default
|
||||
|
||||
def configure(self, comment=None, **kwargs):
|
||||
""" Configure options on registered mylib.Config object """
|
||||
assert self._config, "mylib.Config object not registered. Must be passed to __init__ as config keyword argument."
|
||||
|
||||
from mylib.config import StringOption, PasswordOption
|
||||
|
||||
section = self._config.add_section(
|
||||
self._config_section,
|
||||
comment=comment if comment else 'LDAP connection',
|
||||
loaded_callback=self.initialize, **kwargs)
|
||||
|
||||
section.add_option(
|
||||
StringOption, 'uri', default='ldap://localhost',
|
||||
comment='LDAP server URI')
|
||||
section.add_option(
|
||||
StringOption, 'binddn', comment='LDAP Bind DN')
|
||||
section.add_option(
|
||||
PasswordOption, 'bindpwd',
|
||||
comment='LDAP Bind password (set to "keyring" to use XDG keyring)',
|
||||
username_option='binddn', keyring_value='keyring')
|
||||
|
||||
return section
|
||||
|
||||
def initialize(self, loaded_config=None):
|
||||
""" Initialize LDAP connection """
|
||||
if loaded_config:
|
||||
self.config = loaded_config
|
||||
uri = self.__get_option('uri', required=True)
|
||||
binddn = self.__get_option('binddn')
|
||||
log.info("Connect to LDAP server %s as %s", uri, binddn if binddn else 'annonymous')
|
||||
self._conn = LdapServer(
|
||||
uri, dn=binddn, pwd=self.__get_option('bindpwd'),
|
||||
raiseOnError=True
|
||||
)
|
||||
return self._conn.connect()
|
||||
|
||||
def decode(self, value):
|
||||
""" Decode LDAP attribute value """
|
||||
if isinstance(value, list):
|
||||
return [cls.decode(v) for v in value]
|
||||
return [self.decode(v) for v in value]
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return value.decode('utf-8', 'ignore')
|
||||
return value.decode(
|
||||
self.__get_option('encoding', default=DEFAULT_ENCODING),
|
||||
self.__get_option('encoding_error_policy', default='ignore')
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def encode(cls, value):
|
||||
def encode(self, value):
|
||||
""" Encode LDAP attribute value """
|
||||
if isinstance(value, list):
|
||||
return [cls.encode(v) for v in value]
|
||||
return [self.encode(v) for v in value]
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
return value.encode('utf-8')
|
||||
return value.encode(self.__get_option('encoding', default=DEFAULT_ENCODING))
|
||||
|
||||
def get_attrs(self, dn, attrs):
|
||||
def _get_obj(self, dn, attrs):
|
||||
"""
|
||||
Build and return LDAP object as dict
|
||||
|
||||
:param dn: The object DN
|
||||
:param attrs: The object attributes as return by python-ldap search
|
||||
"""
|
||||
obj = dict(dn=dn)
|
||||
for attr in attrs:
|
||||
obj[attr] = [self.decode(v) for v in self.cnx.get_attr(attrs, attr, all=True)]
|
||||
obj[attr] = [self.decode(v) for v in self._conn.get_attr(attrs, attr, all=True)]
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def get_attr(obj, attr, default="", all_values=False):
|
||||
"""
|
||||
Get LDAP object attribute value(s)
|
||||
|
||||
:param obj: The LDAP object as returned by get_object()/get_objects
|
||||
:param attr: The attribute name
|
||||
:param all_values: If True, all values of the attribute will be
|
||||
returned instead of the first value only
|
||||
(optinal, default: 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):
|
||||
"""
|
||||
Retrieve objects from LDAP
|
||||
|
||||
:param name: The object type name
|
||||
:param filterstr: The LDAP filter to use to search objects on LDAP directory
|
||||
:param basedn: The base DN of the search
|
||||
:param attrs: The list of attribute names to retreive
|
||||
:param key_attr: The attribute name or 'dn' to use as key in result
|
||||
(optional, if leave to None, the result will be a list)
|
||||
:param warn: If True, a warning message will be logged if no object is found
|
||||
in LDAP directory (otherwise, it will be just a debug message)
|
||||
(optional, default: True)
|
||||
"""
|
||||
if name in self._cached_objects:
|
||||
log.debug('Retreived %s objects from cache', name)
|
||||
else:
|
||||
assert self._conn or self.initialize()
|
||||
log.debug('Looking for LDAP %s with (filter="%s" / basedn="%s")', name, filterstr, basedn)
|
||||
ldap_data = self.cnx.search(
|
||||
ldap_data = self._conn.search(
|
||||
basedn=basedn,
|
||||
filterstr=filterstr,
|
||||
attrs=attrs
|
||||
|
@ -391,7 +485,7 @@ class LdapClient:
|
|||
|
||||
objects = {}
|
||||
for obj_dn, obj_attrs in ldap_data.items():
|
||||
objects[obj_dn] = self.get_attrs(obj_dn, obj_attrs)
|
||||
objects[obj_dn] = self._get_obj(obj_dn, obj_attrs)
|
||||
self._cached_objects[name] = objects
|
||||
if not key_attr or key_attr == 'dn':
|
||||
return self._cached_objects[name]
|
||||
|
@ -401,8 +495,24 @@ class LdapClient:
|
|||
)
|
||||
|
||||
def get_object(self, type_name, object_name, filterstr, basedn, attrs, warn=True):
|
||||
"""
|
||||
Retrieve an object from LDAP specified using LDAP search parameters
|
||||
|
||||
Only one object is excepted to be returned by the LDAP search, otherwise, one
|
||||
LdapClientException will be raised.
|
||||
|
||||
:param type_name: The object type name
|
||||
:param object_name: The object name (only use in log messages)
|
||||
:param filterstr: The LDAP filter to use to search the object on LDAP directory
|
||||
:param basedn: The base DN of the search
|
||||
:param attrs: The list of attribute names to retreive
|
||||
:param warn: If True, a warning message will be logged if no object is found
|
||||
in LDAP directory (otherwise, it will be just a debug message)
|
||||
(optional, default: True)
|
||||
"""
|
||||
assert self._conn or self.initialize()
|
||||
log.debug('Looking for LDAP %s "%s" with (filter="%s" / basedn="%s")', type_name, object_name, filterstr, basedn)
|
||||
ldap_data = self.cnx.search(
|
||||
ldap_data = self._conn.search(
|
||||
basedn=basedn, filterstr=filterstr,
|
||||
attrs=attrs
|
||||
)
|
||||
|
@ -418,9 +528,21 @@ class LdapClient:
|
|||
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])
|
||||
return self._get_obj(dn, ldap_data[dn])
|
||||
|
||||
def get_object_by_dn(self, type_name, dn, populate_cache_method=None, warn=True):
|
||||
"""
|
||||
Retrieve an LDAP object specified by its DN from cache
|
||||
|
||||
:param type_name: The object type name
|
||||
:param dn: The object DN
|
||||
:param populate_cache_method: The method to use is cache of LDAP object type
|
||||
is not already populated (optional, default,
|
||||
False is returned)
|
||||
:param warn: If True, a warning message will be logged if object is not found
|
||||
in cache (otherwise, it will be just a debug message)
|
||||
(optional, default: True)
|
||||
"""
|
||||
if type_name not in self._cached_objects:
|
||||
if not populate_cache_method:
|
||||
return False
|
||||
|
@ -441,11 +563,35 @@ class LdapClient:
|
|||
|
||||
@classmethod
|
||||
def object_attr_mached(cls, obj, attr, value, case_sensitive=False):
|
||||
"""
|
||||
Determine if object's attribute matched with specified value
|
||||
|
||||
:param obj: The LDAP object (as returned by get_object/get_objects)
|
||||
:param attr: The attribute name
|
||||
:param value: The value for the match test
|
||||
:param case_sensitive: If True, the match test will be case-sensitive
|
||||
(optional, default: 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):
|
||||
"""
|
||||
Retrieve an LDAP object specified by one of its attribute
|
||||
|
||||
:param type_name: The object type name
|
||||
:param attr: The attribute name
|
||||
:param value: The value for the match test
|
||||
:param populate_cache_method: The method to use is cache of LDAP object type
|
||||
is not already populated (optional, default,
|
||||
False is returned)
|
||||
:param case_sensitive: If True, the match test will be case-sensitive
|
||||
(optional, default: False)
|
||||
:param warn: If True, a warning message will be logged if object is not found
|
||||
in cache (otherwise, it will be just a debug message)
|
||||
(optional, default: True)
|
||||
"""
|
||||
if type_name not in self._cached_objects:
|
||||
if not populate_cache_method:
|
||||
return False
|
||||
|
@ -468,7 +614,7 @@ class LdapClient:
|
|||
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())))
|
||||
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]
|
||||
|
@ -504,7 +650,15 @@ class LdapClient:
|
|||
return (old, new)
|
||||
|
||||
def format_changes(self, changes, protected_attrs=None, prefix=None):
|
||||
return self.cnx.format_changes(
|
||||
"""
|
||||
Format changes as string
|
||||
|
||||
:param changes: The changes as returned by get_changes
|
||||
:param protected_attrs: Optional list of protected attributes
|
||||
:param prefix: Optional prefix string for each line of the returned string
|
||||
"""
|
||||
assert self._conn or self.initialize()
|
||||
return self._conn.format_changes(
|
||||
changes[0], changes[1],
|
||||
ignore_attrs=protected_attrs, prefix=prefix
|
||||
)
|
||||
|
@ -521,10 +675,11 @@ class LdapClient:
|
|||
for attr, values in attrs.items()
|
||||
)
|
||||
try:
|
||||
if self.options.just_try:
|
||||
if self.__get_option('just_try', default=False):
|
||||
log.debug('Just-try mode : do not really add object in LDAP')
|
||||
return True
|
||||
return self.cnx.add_object(dn, attrs)
|
||||
assert self._conn or self.initialize()
|
||||
return self._conn.add_object(dn, attrs)
|
||||
except LdapServerException:
|
||||
log.error(
|
||||
"An error occurred adding object %s in LDAP:\n%s\n",
|
||||
|
@ -589,10 +744,11 @@ class LdapClient:
|
|||
log.debug('%s: No change detected on RDN attibute %s', ldap_obj['dn'], rdn_attr)
|
||||
|
||||
try:
|
||||
if self.options.just_try:
|
||||
if self.__get_option('just_try', default=False):
|
||||
log.debug('Just-try mode : do not really update object in LDAP')
|
||||
return True
|
||||
return self.cnx.update_object(
|
||||
assert self._conn or self.initialize()
|
||||
return self._conn.update_object(
|
||||
ldap_obj['dn'],
|
||||
changes[0],
|
||||
changes[1],
|
||||
|
@ -614,10 +770,11 @@ class LdapClient:
|
|||
:param new_dn_or_rdn: The new LDAP object's DN (or RDN)
|
||||
"""
|
||||
try:
|
||||
if self.options.just_try:
|
||||
if self.__get_option('just_try', default=False):
|
||||
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)
|
||||
assert self._conn or self.initialize()
|
||||
return self._conn.rename_object(ldap_obj['dn'], new_dn_or_rdn)
|
||||
except LdapServerException:
|
||||
log.error(
|
||||
"An error occurred moving object %s in LDAP (destination: %s)",
|
||||
|
@ -632,10 +789,11 @@ class LdapClient:
|
|||
:param ldap_obj: The original LDAP object to delete/drop
|
||||
"""
|
||||
try:
|
||||
if self.options.just_try:
|
||||
if self.__get_option('just_try', default=False):
|
||||
log.debug('Just-try mode : do not really drop object in LDAP')
|
||||
return True
|
||||
return self.cnx.drop_object(ldap_obj['dn'])
|
||||
assert self._conn or self.initialize()
|
||||
return self._conn.drop_object(ldap_obj['dn'])
|
||||
except LdapServerException:
|
||||
log.error(
|
||||
"An error occurred removing object %s in LDAP",
|
||||
|
|
Loading…
Reference in a new issue