Blame pynslcd/search.py

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