summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipaserver/plugins/ldap2.py741
1 files changed, 741 insertions, 0 deletions
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
new file mode 100644
index 000000000..6df01f2a8
--- /dev/null
+++ b/ipaserver/plugins/ldap2.py
@@ -0,0 +1,741 @@
+# Authors:
+# Pavel Zuna <pzuna@redhat.com>
+#
+# Copyright (C) 2009 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+"""
+Backend plugin for LDAP.
+"""
+
+# Entries are represented as (dn, entry_attrs), where entry_attrs is a dict
+# mapping attribute names to values. Values can be a single value or list/tuple
+# of virtually any type. Each method passing these values to the python-ldap
+# binding encodes them into the appropriate representation. This applies to
+# everything except the CrudBackend methods, where dn is part of the entry dict.
+#
+# TODO: review raised exceptions
+# consider using CIDicts for entry_attrs for convenience
+# cleanup & polishing
+# write some documention
+
+import copy
+import os
+import re
+import socket
+import string
+
+import krbV
+import ldap as _ldap
+import ldap.filter as _ldap_filter
+from ldap.controls import LDAPControl
+from ldap.ldapobject import SimpleLDAPObject
+
+from ipalib import api
+from ipalib import errors, errors2
+from ipalib.crud import CrudBackend
+
+# attribute syntax to python type mapping, 'SYNTAX OID': type
+# everything not in this dict is considered human readable unicode
+# instead of using the whole OID, we can just use the last number
+# for standard syntaxes
+# FIXME: if we're going to use non-standard syntaxes, this needs to change
+_syntax_mapping = {
+ '1': str, # ACI Item
+ '4': str, # Audio
+ '5': str, # Binary
+ '7': bool, # Boolean
+ '8': str, # Certificate
+ '9': str, # Certificate List
+ '10': str, # Certificate Pair
+ '23': str, # Fax
+ '27': int, # Integer
+ '28': str, # JPEG
+ '40': str, # OctetString (same as Binary)
+ '49': str, # Supported Algorithm
+ '51': str, # Teletext Terminal Identifier (not sure about this one)
+}
+
+# used to identify the Uniqueness plugin error message
+_uniqueness_plugin_error = 'Another entry with the same attribute value already exists'
+
+
+# utility function, builds LDAP URL string
+def get_ldap_url(host, port, using_cacert=False):
+ if using_cacert:
+ return 'ldaps://%s:%d' % (host, port)
+ return 'ldap://%s:%d' % (host, port)
+
+# retrieves LDAP schema from server
+def load_schema(host, port):
+ url = get_ldap_url(host, port)
+
+ try:
+ conn = _ldap.initialize(url)
+ # assume anonymous access is enabled
+ conn.simple_bind_s('', '')
+ schema_entry = conn.search_s('cn=schema', _ldap.SCOPE_BASE)[0]
+ conn.unbind_s()
+ except _ldap.LDAPError, e:
+ # TODO: raise a more appropriate exception
+ raise errors.DatabaseError
+ except IndexError:
+ # no 'cn=schema' entry in LDAP? some servers use 'cn=subschema'
+ # TODO: DS uses 'cn=schema', support for other server?
+ # raise a more appropriate exception
+ raise
+
+ return _ldap.schema.SubSchema(schema_entry[1])
+
+
+# cache schema when importing module
+_schema = load_schema(api.env.ldap_host, api.env.ldap_port)
+
+# ldap backend class
+class ldap2(CrudBackend):
+
+ # rules for generating filters from entries
+ MATCH_ANY = '|' # (|(filter1)(filter2))
+ MATCH_ALL = '&' # (&(filter1)(filter2))
+ MATCH_NONE = '!' # (!(filter1)(filter2))
+
+ # search scope for find_entries()
+ SCOPE_BASE = _ldap.SCOPE_BASE
+ SCOPE_ONELEVEL = _ldap.SCOPE_ONELEVEL
+ SCOPE_SUBTREE = _ldap.SCOPE_SUBTREE
+
+ def __init__(self):
+ self._host = api.env.ldap_host
+ self._port = api.env.ldap_port
+ self._schema = _schema
+ super(ldap2, self).__init__()
+
+ def __del__(self):
+ self.disconnect()
+
+ def __str__(self):
+ using_cacert = bool(_ldap.get_option(_ldap.OPT_X_TLS_CACERTFILE))
+ return get_ldap_url(self._host, self._port, using_cacert)
+
+ # encoding values from unicode to utf-8 strings for the ldap bindings
+
+ def _encode_value(self, value):
+ if isinstance(value, unicode):
+ return value.encode('utf-8')
+ if value is None:
+ return None
+ if not isinstance(value, (bool, int, float, long, str)):
+ raise TypeError('scalar value expected, got %s' % (value,))
+ return str(value)
+
+ def _encode_values(self, values):
+ if isinstance(values, (list, tuple)):
+ return map(self._encode_value, values)
+ return self._encode_value(values)
+
+ def _encode_entry_attrs(self, entry_attrs):
+ for (k, v) in entry_attrs.iteritems():
+ entry_attrs[k] = self._encode_values(v)
+
+ # decoding values from the ldap bindings to the appropriate type
+
+ def _decode_value(self, value):
+ return value.decode('utf-8')
+
+ def _decode_values(self, values):
+ if isinstance(values, (list, tuple)):
+ return map(self._decode_value, values)
+ return self._decode_value(values)
+
+ def _decode_entry_attrs(self, entry_attrs):
+ for (k, v) in entry_attrs.iteritems():
+ attr = self._schema.get_obj(_ldap.schema.AttributeType, k)
+ if attr:
+ index = attr.syntax.rindex('.') + 1
+ attr_type = _syntax_mapping.get(attr.syntax[index:], unicode)
+ if attr_type is unicode:
+ entry_attrs[k] = self._decode_values(v)
+ elif isinstance(v, (list, tuple)):
+ entry_attrs[k] = map(attr_type, v)
+ else:
+ entry_attrs[k] = attr_type(v)
+
+ def create_connection(self, host=None, port=None, ccache=None,
+ bind_dn='', bind_pw='', debug_level=255,
+ tls_cacertfile=None, tls_certfile=None, tls_keyfile=None):
+ """
+ Connect to LDAP server.
+
+ Keyword arguments:
+ host -- hostname or IP of the server.
+ port -- port number
+ ccache -- Kerberos V5 ccache name
+ bind_dn -- dn used to bind to the server
+ bind_pw -- password used to bind to the server
+ debug_level -- LDAP debug level option
+ tls_cacertfile -- TLS CA certificate filename
+ tls_certfile -- TLS certificate filename
+ tls_keyfile - TLS bind key filename
+
+ Extends backend.Connectible.create_connection.
+ """
+ if host is not None:
+ self._host = host
+ if port is not None:
+ self._port = port
+
+ # if we don't have this server's schema cached, do it now
+ if self._host != api.env.ldap_host or self._port != api.env.ldap_port:
+ self._schema = load_schema(self._host, self._port)
+
+ if tls_cacertfile is not None:
+ _ldap.set_option(_ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile)
+ if tls_certfile is not None:
+ _ldap.set_option(_ldap.OPT_X_TLS_CERTFILE, tls_certfile)
+ if tls_keyfile is not None:
+ _ldap.set_option(_ldap.OPT_X_TLS_KEYFILE, tls_keyfile)
+
+ conn = _ldap.initialize(str(self))
+ if ccache is not None:
+ os.environ['KRB5CCNAME'] = ccache
+ conn.sasl_interactive_bind_s('', _sasl_auth)
+ else:
+ # no kerberos ccache, use simple bind
+ bind_dn = self._encode_value(bind_dn)
+ bind_pw = self._encode_value(bind_pw)
+ conn.simple_bind_s(bind_dn, bind_pw)
+ return conn
+
+ def destroy_connection(self):
+ """Disconnect from LDAP server."""
+ self.conn.unbind_s()
+
+ # DN manipulation
+ # DN's could be generated for example like this:
+ # def execute(...):
+ # ldap = self.backend.ldap2
+ # entry_attrs = self.args_options_2_entry(*args, *options)
+ # parent = ldap.get_container_rdn('accounts')
+ # dn = ldap.make_dn(entry_attrs, self.obj.primary_key, parent)
+ # # add entry with generated dn
+ # ldap.add_entry(dn, entry_attrs)
+
+ def normalize_dn(self, dn):
+ """
+ Normalize distinguished name.
+
+ Note: You don't have to normalize DN's before passing them to
+ ldap2 methods. It's done internally for you.
+ """
+ rdns = _ldap.dn.explode_dn(dn.lower())
+ if rdns:
+ dn = u','.join(rdns)
+ if not dn.endswith(self.api.env.basedn):
+ dn = u'%s,%s' % (dn, self.api.env.basedn)
+ return dn
+ return self.api.env.basedn
+
+ def get_container_rdn(self, name):
+ """Get relative distinguished name of cotainer."""
+ env_container = 'container_%s' % name
+ if env_container in self.api.env:
+ return self.api.env[env_container]
+ return ''
+
+ def make_rdn_from_attr(self, attr, value):
+ """Make relative distinguished name from attribute."""
+ if isinstance(value, (list, tuple)):
+ value = value[0]
+ attr = _ldap.dn.escape_dn_chars(attr)
+ value = _ldap.dn.escape_dn_chars(value)
+ return u'%s=%s' % (attr, value)
+
+ def make_dn_from_rdn(self, rdn, parent_dn=''):
+ """
+ Make distinguished name from relative distinguished name.
+
+ Keyword arguments:
+ parent_dn -- DN of the parent entry (default '')
+ """
+ parent_dn = self.normalize_dn(parent_dn)
+ return u'%s,%s' % (rdn, parent_dn)
+
+ def make_dn(self, entry_attrs, primary_key='cn', parent_dn=''):
+ """
+ Make distinguished name from entry attributes.
+
+ Keyword arguments:
+ primary_key -- attribute from which to make RDN (default 'cn')
+ parent_dn -- DN of the parent entry (default '')
+ """
+ assert primary_key in entry_attrs
+ rdn = self.make_rdn_from_attr(primary_key, entry_attrs[primary_key])
+ return self.make_dn_from_rdn(rdn, parent_dn)
+
+ def add_entry(self, dn, entry_attrs):
+ """Create a new entry."""
+ # encode/normalize arguments
+ dn = self.normalize_dn(dn)
+ dn = self._encode_value(dn)
+ entry_attrs_copy = dict(entry_attrs)
+ self._encode_entry_attrs(entry_attrs_copy)
+
+ # pass arguments to python-ldap
+ try:
+ self.conn.add_s(dn, list(entry_attrs_copy.iteritems()))
+ except _ldap.ALREADY_EXISTS, e:
+ raise errors2.DuplicateEntry
+ except _ldap.CONSTRAINT_VIOLATION, e:
+ if e.args[0].get('info', '') == _uniqueness_plugin_error:
+ raise errors2.DuplicateEntry
+ else:
+ raise errors.DatabaseError, e
+ except _ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+
+ # generating filters for find_entry
+ # some examples:
+ # f1 = ldap2.make_filter_from_attr(u'firstName', u'Pavel')
+ # f2 = ldap2.make_filter_from_attr(u'lastName', u'Zuna')
+ # f = ldap2.combine_filters([f1, f2], ldap2.MATCH_ALL)
+ # # f should be (&(firstName=Pavel)(lastName=Zuna))
+ # # it should be equivalent to:
+ # entry_attrs = {u'firstName': u'Pavel', u'lastName': u'Zuna'}
+ # f = ldap2.make_filter(entry_attrs, rules=ldap2.MATCH_ALL)
+
+ def combine_filters(self, filters, rules='|'):
+ """
+ Combine filters into one for ldap2.find_entries.
+
+ Keyword arguments:
+ rules -- see ldap2.make_filter
+ """
+ assert isinstance(filters, (list, tuple))
+ if len(filters) > 1:
+ flt = '(%s' % rules
+ else:
+ flt = ''
+ for f in filters:
+ if not f.startswith('('):
+ f = '(%s' % f
+ if not f.endswith(')'):
+ f = '%s)' % f
+ flt = '%s%s' % (flt, f)
+ if len(filters) > 1:
+ flt = '%s)' % flt
+ return flt
+
+ def make_filter_from_attr(self, attr, value, rules='|'):
+ """
+ Make filter for ldap2.find_entries from attribute.
+
+ Keyword arguments:
+ rules -- see ldap2.make_filter
+ """
+ if isinstance(value, (list, tuple)):
+ flts = []
+ for v in value:
+ flts.append(self.make_filter_from_attr(attr, v, rules))
+ return self.combine_filters(flts, rules)
+ else:
+ value = _ldap_filter.escape_filter_chars(value)
+ attr = self._encode_value(attr)
+ value = self._encode_value(value)
+ return '(%s=%s)' % (attr, value)
+
+ def make_filter(self, entry_attrs, attrs_list=None, rules='|'):
+ """
+ Make filter for ldap2.find_entries from entry attributes.
+
+ Keyword arguments:
+ attrs_list -- list of attributes to use, all if None (default None)
+ rules -- specifies how to determine a match (default ldap2.MATCH_ANY)
+
+ rules can be one of the following:
+ ldap2.MATCH_NONE - match entries that do not match any attribute
+ ldap2.MATCH_ALL - match entries that match all attributes
+ ldap2.MATCH_ANY - match entries that match any of attribute
+ """
+ flts = []
+ if attrs_list is None:
+ for (k, v) in entry_attrs.iteritems():
+ flts.append(self.make_filter_from_attr(k, v, rules))
+ else:
+ for a in attrs_list:
+ value = entry_attrs.get(a, None)
+ if value is not None:
+ flts.append(self.make_filter_from_attr(a, value, rules))
+ return self.combine_filters(flts, rules)
+
+ def find_entries(self, filter, attrs_list=None, base_dn='',
+ scope=_ldap.SCOPE_SUBTREE, time_limit=1, size_limit=3000):
+ """
+ Return a list of entries [(dn, entry_attrs)] matching specified
+ search parameters.
+
+ Keyword arguments:
+ attrs_list -- list of attributes to return, all if None (default None)
+ base_dn -- dn of the entry at which to start the search (default '')
+ scope -- search scope, see LDAP docs (default ldap2.SCOPE_SUBTREE)
+ time_limit -- time limit in seconds (default 1)
+ size_limit -- size (number of entries returned) limit (default 3000)
+ """
+ # encode/normalize arguments
+ base_dn = self.normalize_dn(base_dn)
+ filter = self._encode_value(filter)
+ if attrs_list is not None:
+ attrs_list = self._encode_values(attrs_list)
+ base_dn = self._encode_value(base_dn)
+
+ # pass arguments to python-ldap
+ try:
+ res = self.conn.search_ext_s(base_dn, scope, filter, attrs_list,
+ timeout=time_limit, sizelimit=size_limit)
+ except (_ldap.ADMINLIMIT_EXCEEDED, _ldap.TIMELIMIT_EXCEEDED,
+ _ldap.SIZELIMIT_EXCEEDED), e:
+ raise e
+ except _ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+ if not res:
+ raise errors2.NotFound()
+
+ # decode results
+ for i in xrange(len(res)):
+ dn = self._decode_value(res[i][0])
+ self._decode_entry_attrs(res[i][1])
+ res[i] = (dn, res[i][1])
+
+ return res
+
+ def get_entry(self, dn, attrs_list=None):
+ """
+ Get entry (dn, entry_attrs) by dn.
+
+ Keyword arguments:
+ attrs_list - list of attributes to return, all if None (default None)
+ """
+ filter = '(objectClass=*)'
+ return self.find_entries(filter, attrs_list, dn, self.SCOPE_BASE)[0]
+
+ def get_ipa_config(self):
+ """Returns the IPA configuration entry (dn, entry_attrs)."""
+ filter = '(cn=ipaConfig)'
+ return self.find_entries(filter, None, 'cn=etc', self.SCOPE_ONELEVEL)[0]
+
+ def get_schema(self):
+ """Returns a copy of the current LDAP schema."""
+ return copy.deepcopy(self._schema)
+
+ def update_entry_rdn(self, dn, new_rdn, del_old=True):
+ """
+ Update entry's relative distinguished name.
+
+ Keyword arguments:
+ del_old -- delete old RDN value (default True)
+ """
+ # encode/normalize arguments
+ dn = self.normalize_dn(dn)
+ dn = self._encode_value(dn)
+ new_rdn = self._encode_value(new_rdn)
+
+ # pass arguments to python-ldap
+ try:
+ self.conn.rename_s(dn, new_rdn, delold=int(del_old))
+ except _ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+
+ def _generate_modlist(self, dn, entry_attrs):
+ # get original entry
+ (dn, entry_attrs_old) = self.get_entry(dn)
+ # get_entry returns a decoded entry, encode it back
+ # we could call search_s directly, but this saves a lot of code at
+ # the expense of a little bit of performace
+ self._encode_entry_attrs(entry_attrs_old)
+
+ # make a copy of the original entry's attribute dict with all
+ # attribute names converted to lowercase
+ old = dict([(k.lower(), v) for (k, v) in entry_attrs_old.iteritems()])
+
+ # generate modlist, we don't want any MOD_REPLACE operations
+ # to handle simultaneous updates better
+ modlist = []
+ for (k, v) in entry_attrs.iteritems():
+ old_v = set(old.get(k.lower(), []))
+ if v is None:
+ modlist.append((_ldap.MOD_DELETE, k, list(old_v)))
+ else:
+ if not isinstance(v, (list, tuple)):
+ v = [v]
+ v = set(filter(lambda value: value is not None, v))
+
+ adds = list(v.difference(old_v))
+ if adds:
+ modlist.append((_ldap.MOD_ADD, k, adds))
+ rems = list(old_v.difference(v))
+ if rems:
+ modlist.append((_ldap.MOD_DELETE, k, rems))
+
+ return modlist
+
+ def update_entry(self, dn, entry_attrs):
+ """
+ Update entry's attributes.
+
+ An attribute value set to None deletes all current values.
+ """
+ # encode/normalize arguments
+ dn = self.normalize_dn(dn)
+ dn = self._encode_value(dn)
+ entry_attrs_copy = dict(entry_attrs)
+ self._encode_entry_attrs(entry_attrs_copy)
+
+ # generate modlist
+ modlist = self._generate_modlist(dn, entry_attrs_copy)
+ if not modlist:
+ raise errors.EmptyModlist
+
+ # pass arguments to python-ldap
+ try:
+ self.conn.modify_s(dn, modlist)
+ except _ldap.NO_SUCH_ATTRIBUTE:
+ raise errors.MidairCollision
+ except _ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+
+ def delete_entry(self, dn):
+ """Delete entry."""
+ # encode/normalize arguments
+ dn = self.normalize_dn(dn)
+ dn = self._encode_value(dn)
+
+ # pass arguments to python-ldap
+ try:
+ self.conn.delete_s(dn)
+ except _ldap.INSUFFICIENT_ACCESS, e:
+ raise errors.InsuficientAccess, e
+ except _ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+
+ def modify_password(self, dn, old_pass, new_pass):
+ """Set user password."""
+ # encode/normalize arguments
+ dn = self.normalize_dn(dn)
+ dn = self._encode_value(dn)
+ old_pass = self._encode_value(old_pass)
+ new_pass = self._encode_value(new_pass)
+
+ # pass arguments to python-ldap
+ try:
+ self.passwd_s(dn, odl_pass, new_pass)
+ except _ldap.LDAPError, e:
+ raise errors.DatabaseError, e
+
+ def add_entry_to_group(self, dn, group_dn, member_attr='member'):
+ """Add entry to group."""
+ # encode/normalize arguments
+ dn = self.normalize_dn(dn)
+ group_dn = self.normalize_dn(group_dn)
+ # check if we're not trying to add group into itself
+ if dn == group_dn:
+ raise errors.SameGroupError
+ # check if the entry exists
+ (dn, entry_attrs) = self.get_entry(dn, ['objectClass'])
+
+ # get group entry
+ (group_dn, group_entry_attrs) = self.get_entry(group_dn)
+
+ # add dn to group entry's `member_attr` attribute
+ members = group_entry_attrs.get(member_attr, [])
+ members.append(dn)
+ group_entry_attrs[member_attr] = members
+
+ # update group entry
+ # FIXME: raise something like AlreadyGroupMember on EmptyModlist
+ # or add a check if dn is already in `member_attr` attribute
+ self.update_entry(group_dn, group_entry_attrs)
+
+ def remove_entry_from_group(self, dn, group_dn, member_attr='member'):
+ """Remove entry from group."""
+ # encode/normalize arguments
+ dn = self.normalize_dn(dn)
+
+ # get group entry
+ (group_dn, group_entry_attrs) = self.get_entry(group_dn)
+
+ # remove dn from group entry's `member_attr` attribute
+ members = group_entry_attrs.get(member_attr, [])
+ try:
+ members.remove(dn)
+ except ValueError:
+ raise errors.NotGroupMember
+ group_entry_attrs[member_attr] = members
+
+ # update group entry
+ self.update_entry(group_dn, group_entry_attrs)
+
+ def set_entry_active(self, dn, active):
+ """Mark entry active/inactive."""
+ assert isinstance(active, bool)
+ # get the entry in question
+ (dn, entry_attrs) = self.get_entry(dn, ['nsAccountLock', 'memberOf'])
+
+ # check nsAccountLock attribute
+ account_lock_attr = entry_attrs.get('nsAccountLock', ['false'])
+ account_lock_attr = account_lock_attr[0].lower()
+ if active:
+ if account_lock_attr == 'false':
+ raise errors.AlreadyActiveError
+ else:
+ if account_lock_attr == 'true':
+ raise errors.AlreadyInactiveError
+
+ # check if nsAccountLock attribute is in the entry itself
+ is_member = False
+ member_of_attr = entry_attrs.get('memberOf', [])
+ for m in member_of_attr:
+ if m.find('cn=activated') >= 0 or m.find('cn=inactivated') >=0:
+ is_member = True
+ break
+ if not is_member and entry_attrs.has_key('nsAccountLock'):
+ raise errors.HasNSAccountLock
+
+ activated_filter = '(cn=activated)'
+ inactivated_filter = '(cn=inactivated)'
+ parent_rdn = self.get_container_rdn('accounts')
+
+ # try to remove the entry from activated/inactivated group
+ if active:
+ entries = self.find_entries(inactivated_filter, [], parent_rdn)
+ else:
+ entries = self.find_entries(activated_filter, [], parent_rdn)
+ (group_dn, group_entry_attrs) = entries[0]
+ try:
+ self.remove_entry_from_group(dn, group_dn)
+ except errors.NotGroupMember:
+ pass
+
+ # add the entry to the activated/inactivated group if necessary
+ if active:
+ (dn, entry_attrs) = self.get_entry(dn, ['nsAccountLock'])
+
+ # check if we still need to add entry to the activated group
+ account_lock_attr = entry_attrs.get('nsAccountLock', ['false'])
+ account_lock_attr = account_lock_attr[0].lower()
+ if account_lock_attr == 'false':
+ return # we don't
+
+ entries = self.find_entries(activated_filter, [], parent_rdn)
+ else:
+ entries = self.find_entries(inactivated_filter, [], parent_rdn)
+ (group_dn, group_entry_attrs) = entries[0]
+ try:
+ self.add_entry_to_group(dn, group_dn)
+ except errors.EmptyModlist:
+ if active:
+ raise errors.AlreadyActiveError
+ else:
+ raise errors.AlreadyInactiveError
+
+ def activate_entry(self, dn):
+ """Mark entry active."""
+ self.set_entry_active(dn, True)
+
+ def deactivate_entry(self, dn):
+ """Mark entry inactive."""
+ self.set_entry_active(dn, False)
+
+ # CrudBackend methods
+
+ def _get_normalized_entry_for_crud(self, dn, attrs_list=None):
+ (dn, entry_attrs) = self.get_entry(dn, attrs_list)
+ entry_attrs['dn'] = [dn]
+ return entry_attrs
+
+ def create(self, **kw):
+ """
+ Create a new entry and return it as one dict (DN included).
+
+ Extends CrudBackend.create.
+ """
+ assert 'dn' in kw
+ dn = kw['dn']
+ del kw['dn']
+ self.add_entry(dn, kw)
+ return self._get_normalized_entry_for_crud(dn)
+
+ def retrieve(self, primary_key, attributes):
+ """
+ Get entry by primary_key (DN) as one dict (DN included).
+
+ Extends CrudBackend.retrieve.
+ """
+ return self._get_normalized_entry_for_crud(primary_key, attributes)
+
+ def update(self, primary_key, **kw):
+ """
+ Update entry's attributes and return it as one dict (DN included).
+
+ Extends CrudBackend.update.
+ """
+ self.update_entry(primary_key, kw)
+ return self._get_normalized_entry_for_crud(primary_key)
+
+ def delete(self, primary_key):
+ """
+ Delete entry by primary_key (DN).
+
+ Extends CrudBackend.delete.
+ """
+ self.delete_entry(primary_key)
+
+ def search(self, **kw):
+ """
+ Return a list of entries (each entry is one dict, DN included) matching
+ the specified criteria.
+
+ Keyword arguments:
+ filter -- search filter (default: '')
+ attrs_list -- list of attributes to return, all if None (default None)
+ base_dn -- dn of the entry at which to start the search (default '')
+ scope -- search scope, see LDAP docs (default ldap2.SCOPE_SUBTREE)
+
+ Extends CrudBackend.search.
+ """
+ # get keyword arguments
+ filter = kw.pop('filter', None)
+ attrs_list = kw.pop('attrs_list', None)
+ base_dn = kw.pop('base_dn', '')
+ scope = kw.pop('scope', self.SCOPE_SUBTREE)
+
+ # generate filter
+ filter_tmp = self.make_filter(kw)
+ if filter:
+ filter = self.combine_filters((filter, filter_tmp), self.MATCH_ALL)
+ else:
+ filter = filter_tmp
+ if not filter:
+ filter = '(objectClass=*)'
+
+ # find entries and normalize the output for CRUD
+ output = []
+ entries = self.find_entries(filter, attrs_list, base_dn, scope)
+ for (dn, entry_attrs) in entries:
+ entry_attrs['dn'] = [dn]
+ output.append(entry_attrs)
+
+ return output
+
+api.register(ldap2)
+