Skip to content
Snippets Groups Projects

Draft: New plugin: LDAP Search

Open Tom Shnayder requested to merge toms/gajim-plugins:ldap into master
+ 208
0
import logging
import queue
import threading
import time
from queue import Queue
import ldap
log = logging.getLogger('gajim.p.ldap')
class LdapSearcher:
MAX_CONNECTION_IDLE_TIME = 20
def __init__(self):
self._server_address = None
self._bind_dn = None
self._password = None
self._base_dn = None
self._request_queue = Queue()
self._stop = threading.Event()
self._thread = None
self._conn = None
self._rebind = True
self._error = None
self._last_idle_time = time.monotonic()
def search(self, search_filter, attributes, on_result, on_error=None):
if self._thread is None:
if on_error:
on_error('LdapSearcher.start was not called')
self._request_queue.put((search_filter, attributes, on_result, on_error))
def start(self):
assert not self._thread
self._stop.clear()
self._conn = None
self._rebind = True
self._error = None
self._thread = threading.Thread(target=self._worker)
self._thread.daemon = True
self._thread.start()
def stop(self):
self._stop.set()
self._thread.join(timeout=2)
self._thread = None
def _worker(self):
log.info('Worker thread started')
while not self._stop.is_set():
self._check_connection_idle_time()
try:
search_filter, attributes, on_result, on_error = self._request_queue.get(timeout=0.1)
except queue.Empty:
continue
if self._error:
log.error('Not doing search due to previous error: %r', self._error)
if on_error:
on_error(self._format_ldap_error(self._error))
continue
try:
if not self._conn or self._rebind:
self._bind()
self._rebind = False
log.debug('Search filter: "%s"', search_filter)
results = self._conn.search_s(self._base_dn, ldap.SCOPE_SUBTREE, search_filter, attributes)
self._last_idle_time = time.monotonic()
results = self._parse_results(results)
log.debug('Got %d results', len(results))
if on_result:
on_result(results)
except ldap.LDAPError as e:
self._error = e
error_message = self._format_ldap_error(e)
log.error('LDAP error: %s', error_message)
if on_error:
on_error(error_message)
self._unbind()
log.info('Worker thread stopped')
def _check_connection_idle_time(self):
# Microsoft's Active Directory LDAP server has a policy named MaxConnIdleTime (defaults to 900)
# which determines the maximum number of second that an LDAP connection is kept alive when it is idle.
# We refresh our connection (unbind + rebind) to avoid timeout errors.
current_time = time.monotonic()
if (current_time - self._last_idle_time) >= self.MAX_CONNECTION_IDLE_TIME:
if self._conn:
log.info('Connection reached maximum idle time')
self._unbind()
@staticmethod
def _parse_results(raw_results):
results = []
for (dn, attrs) in raw_results:
if dn is None:
continue
attributes = {'dn': dn}
for attr_name, attr_values in attrs.items():
if not attr_values:
continue
attributes[attr_name] = attr_values[0].decode('utf-8', errors='replace')
results.append(attributes)
return results
def _bind(self):
self._unbind()
server_address = self._server_address
if not server_address.startswith('ldap://'):
server_address = 'ldap://' + server_address
log.info('Binding to LDAP server: %s', server_address)
self._conn = ldap.initialize(server_address)
self._conn.protocol_version = ldap.VERSION3
self._conn.set_option(ldap.OPT_REFERRALS, 0)
self._conn.simple_bind_s(self._bind_dn, self._password)
log.info('Bound successfully!')
def _unbind(self):
if self._conn is None:
return
log.info('Unbinding from LDAP server')
try:
self._conn.unbind_s()
except ldap.LDAPError as e:
log.error('LDAP error in close: %s', str(e))
finally:
self._conn = None
@staticmethod
def _format_ldap_error(error: ldap.LDAPError) -> str:
if len(error.args) < 1:
return str(error)
arg = error.args[0]
if not isinstance(arg, dict):
return str(error)
desc = arg.get('desc', None)
if desc is None:
return str(error)
return desc
def _on_config_update(self):
self.clear_error()
self._rebind = True
def clear_error(self):
self._error = None
@property
def server_address(self):
return self._server_address
@server_address.setter
def server_address(self, value):
self._server_address = value
self._on_config_update()
@property
def bind_dn(self):
return self._bind_dn
@bind_dn.setter
def bind_dn(self, value):
self._bind_dn = value
self._on_config_update()
@property
def password(self):
return self._password
@password.setter
def password(self, value):
self._password = value
self._on_config_update()
@property
def base_dn(self):
return self._base_dn
@base_dn.setter
def base_dn(self, value):
self._base_dn = value
self._on_config_update()
ldap_searcher = LdapSearcher()
Loading