|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
# search.py - functions for searching the LDAP database
|
|
Packit |
6bd9ab |
#
|
|
Packit |
6bd9ab |
# Copyright (C) 2010, 2011, 2012, 2013 Arthur de Jong
|
|
Packit |
6bd9ab |
#
|
|
Packit |
6bd9ab |
# This library is free software; you can redistribute it and/or
|
|
Packit |
6bd9ab |
# modify it under the terms of the GNU Lesser General Public
|
|
Packit |
6bd9ab |
# License as published by the Free Software Foundation; either
|
|
Packit |
6bd9ab |
# version 2.1 of the License, or (at your option) any later version.
|
|
Packit |
6bd9ab |
#
|
|
Packit |
6bd9ab |
# This library is distributed in the hope that it will be useful,
|
|
Packit |
6bd9ab |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
Packit |
6bd9ab |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
Packit |
6bd9ab |
# Lesser General Public License for more details.
|
|
Packit |
6bd9ab |
#
|
|
Packit |
6bd9ab |
# You should have received a copy of the GNU Lesser General Public
|
|
Packit |
6bd9ab |
# License along with this library; if not, write to the Free Software
|
|
Packit |
6bd9ab |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
Packit |
6bd9ab |
# 02110-1301 USA
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
import logging
|
|
Packit |
6bd9ab |
import sys
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
import ldap
|
|
Packit |
6bd9ab |
import ldap.ldapobject
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
import cfg
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
# global indicator that there was some error connection to an LDAP server
|
|
Packit |
6bd9ab |
server_error = False
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
# global indicator of first search operation
|
|
Packit |
6bd9ab |
first_search = True
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
class Connection(ldap.ldapobject.ReconnectLDAPObject):
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def __init__(self):
|
|
Packit |
6bd9ab |
ldap.ldapobject.ReconnectLDAPObject.__init__(self, cfg.uri,
|
|
Packit |
6bd9ab |
retry_max=1, retry_delay=cfg.reconnect_retrytime)
|
|
Packit |
6bd9ab |
# set connection-specific LDAP options
|
|
Packit |
6bd9ab |
if cfg.ldap_version:
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_PROTOCOL_VERSION, cfg.ldap_version)
|
|
Packit |
6bd9ab |
if cfg.deref:
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_DEREF, cfg.deref)
|
|
Packit |
6bd9ab |
if cfg.timelimit:
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_TIMELIMIT, cfg.timelimit)
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_TIMEOUT, cfg.timelimit)
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_NETWORK_TIMEOUT, cfg.timelimit)
|
|
Packit |
6bd9ab |
if cfg.referrals:
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_REFERRALS, cfg.referrals)
|
|
Packit |
6bd9ab |
if cfg.sasl_canonicalize is not None:
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_X_SASL_NOCANON, not cfg.sasl_canonicalize)
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_RESTART, True)
|
|
Packit |
6bd9ab |
# TODO: register a connection callback (like dis?connect_cb() in myldap.c)
|
|
Packit |
6bd9ab |
if cfg.ssl or cfg.uri.startswith('ldaps://'):
|
|
Packit |
6bd9ab |
self.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_HARD)
|
|
Packit |
6bd9ab |
# TODO: the following should probably be done on the first search
|
|
Packit |
6bd9ab |
# together with binding, not when creating the connection object
|
|
Packit |
6bd9ab |
if cfg.ssl == 'STARTTLS':
|
|
Packit |
6bd9ab |
self.start_tls_s()
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def reconnect_after_fail(self):
|
|
Packit |
6bd9ab |
import invalidator
|
|
Packit |
6bd9ab |
logging.info('connected to LDAP server %s', cfg.uri)
|
|
Packit |
6bd9ab |
invalidator.invalidate()
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def search_s(self, *args, **kwargs):
|
|
Packit |
6bd9ab |
# wrapper function to keep the global server_error state
|
|
Packit |
6bd9ab |
global server_error, first_search
|
|
Packit |
6bd9ab |
try:
|
|
Packit |
6bd9ab |
res = ldap.ldapobject.ReconnectLDAPObject.search_s(self, *args, **kwargs)
|
|
Packit |
6bd9ab |
except ldap.SERVER_DOWN:
|
|
Packit |
6bd9ab |
server_error = True
|
|
Packit |
6bd9ab |
raise
|
|
Packit |
6bd9ab |
if server_error or first_search:
|
|
Packit |
6bd9ab |
self.reconnect_after_fail()
|
|
Packit |
6bd9ab |
server_error = False
|
|
Packit |
6bd9ab |
first_search = False
|
|
Packit |
6bd9ab |
return res
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
class LDAPSearch(object):
|
|
Packit |
6bd9ab |
"""
|
|
Packit |
6bd9ab |
Class that performs an LDAP search. Subclasses are expected to define the
|
|
Packit |
6bd9ab |
actual searches and should implement the following members:
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
case_sensitive - check that these attributes are present in the response
|
|
Packit |
6bd9ab |
if they were in the request
|
|
Packit |
6bd9ab |
case_insensitive - check that these attributes are present in the
|
|
Packit |
6bd9ab |
response if they were in the request
|
|
Packit |
6bd9ab |
limit_attributes - override response attributes with request attributes
|
|
Packit |
6bd9ab |
(ensure that only one copy of the value is returned)
|
|
Packit |
6bd9ab |
required - attributes that are required
|
|
Packit |
6bd9ab |
canonical_first - search the DN for these attributes and ensure that
|
|
Packit |
6bd9ab |
they are listed first in the attribute values
|
|
Packit |
6bd9ab |
mk_filter() (optional) - function that returns the LDAP search filter
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
The module that contains the Search class can also contain the following
|
|
Packit |
6bd9ab |
definitions:
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
bases - list of search bases to be used, if absent or empty falls back
|
|
Packit |
6bd9ab |
to cfg.bases
|
|
Packit |
6bd9ab |
scope - search scope, falls back to cfg.scope if absent or empty
|
|
Packit |
6bd9ab |
filter - an LDAP search filter
|
|
Packit |
6bd9ab |
attmap - an attribute mapping definition (using he Attributes class)
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
"""
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
canonical_first = []
|
|
Packit |
6bd9ab |
required = []
|
|
Packit |
6bd9ab |
case_sensitive = []
|
|
Packit |
6bd9ab |
case_insensitive = []
|
|
Packit |
6bd9ab |
limit_attributes = []
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def __init__(self, conn, base=None, scope=None, filter=None,
|
|
Packit |
6bd9ab |
attributes=None, parameters=None):
|
|
Packit |
6bd9ab |
self.conn = conn
|
|
Packit |
6bd9ab |
# load information from module that defines the class
|
|
Packit |
6bd9ab |
module = sys.modules[self.__module__]
|
|
Packit |
6bd9ab |
if base:
|
|
Packit |
6bd9ab |
self.bases = [base]
|
|
Packit |
6bd9ab |
else:
|
|
Packit |
6bd9ab |
self.bases = getattr(module, 'bases', cfg.bases)
|
|
Packit |
6bd9ab |
self.scope = scope or getattr(module, 'scope', cfg.scope)
|
|
Packit |
6bd9ab |
self.filter = filter or getattr(module, 'filter', None)
|
|
Packit |
6bd9ab |
self.attmap = getattr(module, 'attmap', None)
|
|
Packit |
6bd9ab |
self.attributes = attributes or self.attmap.attributes()
|
|
Packit |
6bd9ab |
self.parameters = parameters or {}
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def __iter__(self):
|
|
Packit |
6bd9ab |
return self.items()
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def items(self):
|
|
Packit |
6bd9ab |
"""Return the results from the search."""
|
|
Packit |
6bd9ab |
filter = self.mk_filter()
|
|
Packit |
6bd9ab |
for base in self.bases:
|
|
Packit |
6bd9ab |
logging.debug('LDAPSearch(base=%r, filter=%r)', base, filter)
|
|
Packit |
6bd9ab |
try:
|
|
Packit |
6bd9ab |
for entry in self.conn.search_s(base, self.scope, filter, self.attributes):
|
|
Packit |
6bd9ab |
if entry[0]:
|
|
Packit |
6bd9ab |
entry = self._transform(entry[0], entry[1])
|
|
Packit |
6bd9ab |
if entry:
|
|
Packit |
6bd9ab |
yield entry
|
|
Packit |
6bd9ab |
except ldap.NO_SUCH_OBJECT:
|
|
Packit |
6bd9ab |
# FIXME: log message
|
|
Packit |
6bd9ab |
pass
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def mk_filter(self):
|
|
Packit |
6bd9ab |
"""Return the active search filter (based on the read parameters)."""
|
|
Packit |
6bd9ab |
if self.parameters:
|
|
Packit |
6bd9ab |
return '(&%s%s)' % (
|
|
Packit |
6bd9ab |
self.filter,
|
|
Packit |
6bd9ab |
''.join(self.attmap.mk_filter(attribute, value)
|
|
Packit |
6bd9ab |
for attribute, value in self.parameters.items()))
|
|
Packit |
6bd9ab |
return self.filter
|
|
Packit |
6bd9ab |
|
|
Packit |
6bd9ab |
def _transform(self, dn, attributes):
|
|
Packit |
6bd9ab |
"""Handle a single search result entry filtering it with the request
|
|
Packit |
6bd9ab |
parameters, search options and attribute mapping."""
|
|
Packit |
6bd9ab |
# translate the attributes using the attribute mapping
|
|
Packit |
6bd9ab |
if self.attmap:
|
|
Packit |
6bd9ab |
attributes = self.attmap.translate(attributes)
|
|
Packit |
6bd9ab |
# make sure value from DN is first value
|
|
Packit |
6bd9ab |
for attr in self.canonical_first:
|
|
Packit |
6bd9ab |
primary_value = self.attmap.get_rdn_value(dn, attr)
|
|
Packit |
6bd9ab |
if primary_value:
|
|
Packit |
6bd9ab |
values = attributes[attr]
|
|
Packit |
6bd9ab |
if primary_value in values:
|
|
Packit |
6bd9ab |
values.remove(primary_value)
|
|
Packit |
6bd9ab |
attributes[attr] = [primary_value] + values
|
|
Packit |
6bd9ab |
# check that these attributes have at least one value
|
|
Packit |
6bd9ab |
for attr in self.required:
|
|
Packit |
6bd9ab |
if not attributes.get(attr, None):
|
|
Packit |
6bd9ab |
logging.warning('%s: %s: missing', dn, self.attmap[attr])
|
|
Packit |
6bd9ab |
return
|
|
Packit |
6bd9ab |
# check that requested attribute is present (case sensitive)
|
|
Packit |
6bd9ab |
for attr in self.case_sensitive:
|
|
Packit |
6bd9ab |
value = self.parameters.get(attr, None)
|
|
Packit |
6bd9ab |
if value and str(value) not in attributes[attr]:
|
|
Packit |
6bd9ab |
logging.debug('%s: %s: does not contain %r value', dn, self.attmap[attr], value)
|
|
Packit |
6bd9ab |
return # not found, skip entry
|
|
Packit |
6bd9ab |
# check that requested attribute is present (case insensitive)
|
|
Packit |
6bd9ab |
for attr in self.case_insensitive:
|
|
Packit |
6bd9ab |
value = self.parameters.get(attr, None)
|
|
Packit |
6bd9ab |
if value and str(value).lower() not in (x.lower() for x in attributes[attr]):
|
|
Packit |
6bd9ab |
logging.debug('%s: %s: does not contain %r value', dn, self.attmap[attr], value)
|
|
Packit |
6bd9ab |
return # not found, skip entry
|
|
Packit |
6bd9ab |
# limit attribute values to requested value
|
|
Packit |
6bd9ab |
for attr in self.limit_attributes:
|
|
Packit |
6bd9ab |
if attr in self.parameters:
|
|
Packit |
6bd9ab |
attributes[attr] = [self.parameters[attr]]
|
|
Packit |
6bd9ab |
# return the entry
|
|
Packit |
6bd9ab |
return dn, attributes
|