diff options
Diffstat (limited to 'ipaserver/plugins/ldap2.py')
-rw-r--r-- | ipaserver/plugins/ldap2.py | 1123 |
1 files changed, 758 insertions, 365 deletions
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py index 6a3d2164..a0b91fd5 100644 --- a/ipaserver/plugins/ldap2.py +++ b/ipaserver/plugins/ldap2.py @@ -1,5 +1,6 @@ # Authors: # Pavel Zuna <pzuna@redhat.com> +# John Dennis <jdennis@redhat.com> # # Copyright (C) 2009 Red Hat # see file 'COPYING' for use and warranty information @@ -35,6 +36,8 @@ import tempfile import time import re import pwd +import sys +from decimal import Decimal import krbV from ipapython.ipa_log_manager import * @@ -42,6 +45,12 @@ import ldap as _ldap from ldap.ldapobject import SimpleLDAPObject import ldap.filter as _ldap_filter import ldap.sasl as _ldap_sasl +from ipapython.dn import DN, RDN +from ipapython.ipautil import CIDict +from collections import namedtuple +from ipalib.errors import NetworkError, DatabaseError + + try: from ldap.controls.simple import GetEffectiveRightsControl #pylint: disable=F0401,E0611 except ImportError: @@ -56,17 +65,26 @@ except ImportError: def __init__(self, criticality, authzId=None): LDAPControl.__init__(self, '1.3.6.1.4.1.42.2.27.9.5.2', criticality, authzId) # for backward compatibility -from ldap.functions import explode_dn -from ipalib.dn import DN from ipalib import _ import krbV from ipalib import api, errors from ipalib.crud import CrudBackend -from ipalib.encoder import Encoder, encode_args, decode_retval from ipalib.request import context +_debug_log_ldap = False + +# Make python-ldap tuple style result compatible with Entry and Entity +# objects by allowing access to the dn (tuple index 0) via the 'dn' +# attribute name and the attr dict (tuple index 1) via the 'data' +# attribute name. Thus: +# r = result[0] +# r[0] == r.dn +# r[1] == r.data +LDAPEntry = namedtuple('LDAPEntry', ['dn', 'data']) + + # Group Member types MEMBERS_ALL = 0 MEMBERS_DIRECT = 1 @@ -75,264 +93,541 @@ MEMBERS_INDIRECT = 2 # SASL authentication mechanism SASL_AUTH = _ldap_sasl.sasl({}, 'GSSAPI') -class IPASimpleLDAPObject(SimpleLDAPObject): +DN_SYNTAX_OID = '1.3.6.1.4.1.1466.115.121.1.12' + +def unicode_from_utf8(val): ''' - This is a thin layer over SimpleLDAPObject which allows us to utilize - IPA specific types with the python-ldap API without the IPA caller needing - to perform the type translation, consider this a convenience layer for the - IPA programmer. + val is a UTF-8 encoded string, return a unicode object. + ''' + return val.decode('utf-8') - This subclass performs the following translations: +def value_to_utf8(val): + ''' + Coerce the val parameter to a UTF-8 encoded string representation + of the val. + ''' + + # If val is not a string we need to convert it to a string + # (specifically a unicode string). Naively we might think we need to + # call str(val) to convert to a string. This is incorrect because if + # val is already a unicode object then str() will call + # encode(default_encoding) returning a str object encoded with + # default_encoding. But we don't want to apply the default_encoding! + # Rather we want to guarantee the val object has been converted to a + # unicode string because from a unicode string we want to explicitly + # encode to a str using our desired encoding (utf-8 in this case). + # + # Note: calling unicode on a unicode object simply returns the exact + # same object (with it's ref count incremented). This means calling + # unicode on a unicode object is effectively a no-op, thus it's not + # inefficient. - * DN objects may be passed into any ldap function expecting a dn. The DN - object will be converted to a string before being passed to the python-ldap - function. This allows us to maintain DN objects as DN objects in the rest - of the code (useful for DN manipulation and DN information) and not have - to worry about conversion to a string prior to passing it ldap. + return unicode(val).encode('utf-8') +class _ServerSchema(object): + ''' + Properties of a schema retrieved from an LDAP server. ''' - def __init__(self, *args, **kwds): - SimpleLDAPObject.__init__(self, *args, **kwds) - def add(self, dn, modlist): - return SimpleLDAPObject.add(self, str(dn), modlist) + def __init__(self, server, schema): + self.server = server + self.schema = schema + self.retrieve_timestamp = time.time() - def add_ext(self, dn, modlist, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.add_ext(self, str(dn), modlist, serverctrls, clientctrls) +class SchemaCache(object): + ''' + Cache the schema's from individual LDAP servers. + ''' - def add_ext_s(self, dn, modlist, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.add_ext_s(self, str(dn), modlist, serverctrls, clientctrls) + def __init__(self): + log_mgr.get_logger(self, True) + self.servers = {} - def add_s(self, dn, modlist): - return SimpleLDAPObject.add_s(self, str(dn), modlist) + def get_schema(self, url, conn=None, force_update=False): + ''' + Return schema belonging to a specific LDAP server. - def compare(self, dn, attr, value): - return SimpleLDAPObject.compare(self, str(dn), attr, value) + For performance reasons the schema is retrieved once and + cached unless force_update is True. force_update flushes the + existing schema for the server from the cache and reacquires + it. + ''' - def compare_ext(self, dn, attr, value, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.compare_ext(self, str(dn), attr, value, serverctrls, clientctrls) + if force_update: + self.flush(url) - def compare_ext_s(self, dn, attr, value, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.compare_ext_s(self, str(dn), attr, value, serverctrls, clientctrls) + server_schema = self.servers.get(url) + if server_schema is None: + schema = self._retrieve_schema_from_server(url, conn) + server_schema = _ServerSchema(url, schema) + self.servers[url] = server_schema + return server_schema.schema - def compare_s(self, dn, attr, value): - return SimpleLDAPObject.compare_s(self, str(dn), attr, value) + def flush(self, url): + self.debug('flushing %s from SchemaCache', url) + try: + del self.servers[url] + except KeyError: + pass - def delete(self, dn): - return SimpleLDAPObject.delete(self, str(dn)) + def _retrieve_schema_from_server(self, url, conn=None): + """ + Retrieve the LDAP schema from the provided url and determine if + User-Private Groups (upg) are configured. - def delete_ext(self, dn, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.delete_ext(self, str(dn), serverctrls, clientctrls) + Bind using kerberos credentials. If in the context of the + in-tree "lite" server then use the current ccache. If in the context of + Apache then create a new ccache and bind using the Apache HTTP service + principal. - def delete_ext_s(self, dn, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.delete_ext_s(self, str(dn), serverctrls, clientctrls) + If a connection is provided then it the credentials bound to it are + used. The connection is not closed when the request is done. + """ + tmpdir = None + has_conn = conn is not None - def delete_s(self, dn): - return SimpleLDAPObject.delete_s(self, str(dn)) + self.debug('retrieving schema for SchemaCache url=%s conn=%s', url, conn) - def modify(self, dn, modlist): - return SimpleLDAPObject.modify(self, str(dn), modlist) + try: + if api.env.context == 'server' and conn is None: + # FIXME: is this really what we want to do? + # This seems like this logic is in the wrong place and may conflict with other state. + try: + # Create a new credentials cache for this Apache process + tmpdir = tempfile.mkdtemp(prefix = "tmp-") + ccache_file = 'FILE:%s/ccache' % tmpdir + krbcontext = krbV.default_context() + principal = str('HTTP/%s@%s' % (api.env.host, api.env.realm)) + keytab = krbV.Keytab(name='/etc/httpd/conf/ipa.keytab', context=krbcontext) + principal = krbV.Principal(name=principal, context=krbcontext) + prev_ccache = os.environ.get('KRB5CCNAME') + os.environ['KRB5CCNAME'] = ccache_file + ccache = krbV.CCache(name=ccache_file, context=krbcontext, primary_principal=principal) + ccache.init(principal) + ccache.init_creds_keytab(keytab=keytab, principal=principal) + except krbV.Krb5Error, e: + raise StandardError('Unable to retrieve LDAP schema. Error initializing principal %s in %s: %s' % (principal.name, '/etc/httpd/conf/ipa.keytab', str(e))) + finally: + if prev_ccache is not None: + os.environ['KRB5CCNAME'] = prev_ccache + + + if conn is None: + conn = IPASimpleLDAPObject(url) + if url.startswith('ldapi://'): + conn.set_option(_ldap.OPT_HOST_NAME, api.env.host) + conn.sasl_interactive_bind_s(None, SASL_AUTH) + + schema_entry = conn.search_s('cn=schema', _ldap.SCOPE_BASE, + attrlist=['attributetypes', 'objectclasses'])[0] + if not has_conn: + conn.unbind_s() + except _ldap.SERVER_DOWN: + raise NetworkError(uri=url, + error=u'LDAP Server Down, unable to retrieve LDAP schema') + except _ldap.LDAPError, e: + desc = e.args[0]['desc'].strip() + info = e.args[0].get('info', '').strip() + raise DatabaseError(desc = u'uri=%s' % url, + info = u'Unable to retrieve LDAP schema: %s: %s' % (desc, info)) + 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 + finally: + if tmpdir: + shutil.rmtree(tmpdir) + + return _ldap.schema.SubSchema(schema_entry[1]) + +schema_cache = SchemaCache() + +class IPASimpleLDAPObject(object): + ''' + The purpose of this class is to provide a boundary between IPA and + python-ldap. In IPA we use IPA defined types because they are + richer and are designed to meet our needs. We also require that we + consistently use those types without exception. On the other hand + python-ldap uses different types. The goal is to be able to have + IPA code call python-ldap methods using the types native to + IPA. This class accomplishes that goal by exposing python-ldap + methods which take IPA types, convert them to python-ldap types, + call python-ldap, and then convert the results returned by + python-ldap into IPA types. + + IPA code should never call python-ldap directly, it should only + call python-ldap methods in this class. + ''' - def modify_ext(self, dn, modlist, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.modify_ext(self, str(dn), modlist, serverctrls, clientctrls) + # Note: the oid for dn syntax is: 1.3.6.1.4.1.1466.115.121.1.12 - def modify_ext_s(self, dn, modlist, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.modify_ext_s(self, str(dn), modlist, serverctrls, clientctrls) + _SYNTAX_MAPPING = { + '1.3.6.1.4.1.1466.115.121.1.1' : str, # ACI item + '1.3.6.1.4.1.1466.115.121.1.4' : str, # Audio + '1.3.6.1.4.1.1466.115.121.1.5' : str, # Binary + '1.3.6.1.4.1.1466.115.121.1.8' : str, # Certificate + '1.3.6.1.4.1.1466.115.121.1.9' : str, # Certificate List + '1.3.6.1.4.1.1466.115.121.1.10' : str, # Certificate Pair + '1.3.6.1.4.1.1466.115.121.1.23' : str, # Fax + '1.3.6.1.4.1.1466.115.121.1.28' : str, # JPEG + '1.3.6.1.4.1.1466.115.121.1.40' : str, # OctetString (same as Binary) + '1.3.6.1.4.1.1466.115.121.1.49' : str, # Supported Algorithm + '1.3.6.1.4.1.1466.115.121.1.51' : str, # Teletext Terminal Identifier + + DN_SYNTAX_OID : DN, # DN, member, memberof + '2.16.840.1.113730.3.8.3.3' : DN, # enrolledBy + '2.16.840.1.113730.3.8.3.18' : DN, # managedBy + '2.16.840.1.113730.3.8.3.5' : DN, # memberUser + '2.16.840.1.113730.3.8.3.7' : DN, # memberHost + '2.16.840.1.113730.3.8.3.20' : DN, # memberService + '2.16.840.1.113730.3.8.11.4' : DN, # ipaNTFallbackPrimaryGroup + '2.16.840.1.113730.3.8.11.21' : DN, # ipaAllowToImpersonate + '2.16.840.1.113730.3.8.11.22' : DN, # ipaAllowedTarget + '2.16.840.1.113730.3.8.7.1' : DN, # memberAllowCmd + '2.16.840.1.113730.3.8.7.2' : DN, # memberDenyCmd + + '2.16.840.1.113719.1.301.4.14.1' : DN, # krbRealmReferences + '2.16.840.1.113719.1.301.4.17.1' : DN, # krbKdcServers + '2.16.840.1.113719.1.301.4.18.1' : DN, # krbPwdServers + '2.16.840.1.113719.1.301.4.26.1' : DN, # krbPrincipalReferences + '2.16.840.1.113719.1.301.4.29.1' : DN, # krbAdmServers + '2.16.840.1.113719.1.301.4.36.1' : DN, # krbPwdPolicyReference + '2.16.840.1.113719.1.301.4.40.1' : DN, # krbTicketPolicyReference + '2.16.840.1.113719.1.301.4.41.1' : DN, # krbSubTrees + '2.16.840.1.113719.1.301.4.52.1' : DN, # krbObjectReferences + '2.16.840.1.113719.1.301.4.53.1' : DN, # krbPrincContainerRef + } - def modify_s(self, dn, modlist): - return SimpleLDAPObject.modify_s(self, str(dn), modlist) + # In most cases we lookup the syntax from the schema returned by + # the server. However, sometimes attributes may not be defined in + # the schema (e.g. extensibleObject which permits undefined + # attributes), or the schema was incorrectly defined (i.e. giving + # an attribute the syntax DirectoryString when in fact it's really + # a DN). This (hopefully sparse) table allows us to trap these + # anomalies and force them to be the syntax we know to be in use. + # + # FWIW, many entries under cn=config are undefined :-( + + _SCHEMA_OVERRIDE = CIDict({ + 'managedtemplate': DN_SYNTAX_OID, # DN + 'managedbase': DN_SYNTAX_OID, # DN + 'originscope': DN_SYNTAX_OID, # DN + }) + + def __init__(self, uri): + log_mgr.get_logger(self, True) + self.uri = uri + self.conn = SimpleLDAPObject(uri) + self._schema = None + + def _get_schema(self): + if self._schema is None: + # The schema may be updated during install or during + # updates, make sure we have a current version of the + # schema, not an out of date cached version. + force_update = api.env.context in ('installer', 'updates') + self._schema = schema_cache.get_schema(self.uri, self.conn, force_update=force_update) + return self._schema + + schema = property(_get_schema, None, None, 'schema associated with this LDAP server') + + + def flush_cached_schema(self): + ''' + Force this instance to forget it's cached schema and reacquire + it from the schema cache. + ''' + + # Currently this is called during bind operations to assure + # we're working with valid schema for a specific + # connection. This causes self._get_schema() to query the + # schema cache for the server's schema passing along a flag + # indicating if we're in a context that requires freshly + # loading the schema vs. returning the last cached version of + # the schema. If we're in a mode that permits use of + # previously cached schema the flush and reacquire is a very + # low cost operation. + # + # The schema is reacquired whenever this object is + # instantiated or when binding occurs. The schema is not + # reacquired for operations during a bound connection, it is + # presumed schema cannot change during this interval. This + # provides for maximum efficiency in contexts which do need + # schema refreshing by only peforming the refresh inbetween + # logical operations that have the potential to cause a schema + # change. + + self._schema = None + + def get_syntax(self, attr): + # Is this a special case attribute? + syntax = self._SCHEMA_OVERRIDE.get(attr) + if syntax is not None: + return syntax + + # Try to lookup the syntax in the schema returned by the server + obj = self.schema.get_obj(_ldap.schema.AttributeType, attr) + if obj is not None: + return obj.syntax + else: + return None - def modrdn(self, dn, newrdn, delold=1): - return SimpleLDAPObject.modrdn(self, str(dn), str(newrdn), delold) + def has_dn_syntax(self, attr): + """ + Check the schema to see if the attribute uses DN syntax. - def modrdn_s(self, dn, newrdn, delold=1): - return SimpleLDAPObject.modrdn_s(self, str(dn), str(newrdn), delold) + Returns True/False + """ + syntax = self.get_syntax(attr) + return syntax == DN_SYNTAX_OID + + + def encode(self, val): + ''' + ''' + # Booleans are both an instance of bool and int, therefore + # test for bool before int otherwise the int clause will be + # entered for a boolean value instead of the boolean clause. + if isinstance(val, bool): + if val: + return 'TRUE' + else: + return 'FALSE' + elif isinstance(val, (unicode, float, int, long, Decimal, DN)): + return value_to_utf8(val) + elif isinstance(val, str): + return val + elif isinstance(val, list): + return [self.encode(m) for m in val] + elif isinstance(val, tuple): + return tuple(self.encode(m) for m in val) + elif isinstance(val, dict): + dct = dict((self.encode(k), self.encode(v)) for k, v in val.iteritems()) + return dct + elif val is None: + return None + else: + raise TypeError("attempt to pass unsupported type to ldap, value=%s type=%s" %(val, type(val))) - def read_subschemasubentry_s(self, subschemasubentry_dn, attrs=None): - return SimpleLDAPObject.read_subschemasubentry_s(self, str(subschemasubentry_dn), attrs) + def convert_value_list(self, attr, target_type, values): + ''' + ''' - def rename(self, dn, newrdn, newsuperior=None, delold=1, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.rename(self, str(dn), str(newrdn), newsuperior, delold, serverctrls, clientctrls) + ipa_values = [] - def rename_s(self, dn, newrdn, newsuperior=None, delold=1, serverctrls=None, clientctrls=None): - return SimpleLDAPObject.rename_s(self, str(dn), str(newrdn), newsuperior, delold, serverctrls, clientctrls) + for original_value in values: + if isinstance(target_type, type) and isinstance(original_value, target_type): + ipa_value = original_value + else: + try: + ipa_value = target_type(original_value) + except Exception, e: + msg = 'unable to convert the attribute "%s" value "%s" to type %s' % (attr, original_value, target_type) + self.error(msg) + raise ValueError(msg) - def search(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): - return SimpleLDAPObject.search(self, str(base), scope, filterstr, attrlist, attrsonly) + ipa_values.append(ipa_value) - def search_ext(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, - serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0): - return SimpleLDAPObject.search_ext(self, str(base), scope, filterstr, attrlist, attrsonly, - serverctrls, clientctrls, timeout, sizelimit) + return ipa_values - def search_ext_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, - serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0): - return SimpleLDAPObject.search_ext_s(self, str(base), scope, filterstr, attrlist, attrsonly, - serverctrls, clientctrls, timeout, sizelimit) + def convert_result(self, result): + ''' + result is a python-ldap result tuple of the form (dn, attrs), + where dn is a string containing the dn (distinguished name) of + the entry, and attrs is a dictionary containing the attributes + associated with the entry. The keys of attrs are strings, and + the associated values are lists of strings. - def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): - return SimpleLDAPObject.search_s(self, str(base), scope, filterstr, attrlist, attrsonly) + We convert the dn to a DN object. - def search_st(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, timeout=-1): - return SimpleLDAPObject.search_st(self, str(base), scope, filterstr, attrlist, attrsonly, timeout) + We convert every value associated with an attribute according + to it's syntax into the desired Python type. - def search_subschemasubentry_s(self, dn=''): - return SimpleLDAPObject.search_subschemasubentry_s(self, str(dn)) + returns a IPA result tuple of the same form as a python-ldap + result tuple except everything inside of the result tuple has + been converted to it's preferred IPA python type. + ''' -# universal LDAPError handler -def _handle_errors(e, **kw): - """ - Centralize error handling in one place. + ipa_result = [] + for dn_tuple in result: + original_dn = dn_tuple[0] + original_attrs = dn_tuple[1] - e is the error to be raised - **kw is an exception-specific list of options - """ - if not isinstance(e, _ldap.TIMEOUT): - desc = e.args[0]['desc'].strip() - info = e.args[0].get('info', '').strip() - else: - desc = '' - info = '' - - try: - # re-raise the error so we can handle it - raise e - except _ldap.NO_SUCH_OBJECT: - # args = kw.get('args', '') - # raise errors.NotFound(msg=notfound(args)) - raise errors.NotFound(reason='no such entry') - except _ldap.ALREADY_EXISTS: - raise errors.DuplicateEntry() - except _ldap.CONSTRAINT_VIOLATION: - # This error gets thrown by the uniqueness plugin - if info.startswith('Another entry with the same attribute value already exists'): - raise errors.DuplicateEntry() - else: - raise errors.DatabaseError(desc=desc, info=info) - except _ldap.INSUFFICIENT_ACCESS: - raise errors.ACIError(info=info) - except _ldap.INVALID_CREDENTIALS: - raise errors.ACIError(info="%s %s" % (info, desc)) - except _ldap.NO_SUCH_ATTRIBUTE: - # this is raised when a 'delete' attribute isn't found. - # it indicates the previous attribute was removed by another - # update, making the oldentry stale. - raise errors.MidairCollision() - except _ldap.INVALID_SYNTAX: - raise errors.InvalidSyntax(attr=info) - except _ldap.OBJECT_CLASS_VIOLATION: - raise errors.ObjectclassViolation(info=info) - except _ldap.ADMINLIMIT_EXCEEDED: - raise errors.LimitsExceeded() - except _ldap.SIZELIMIT_EXCEEDED: - raise errors.LimitsExceeded() - except _ldap.TIMELIMIT_EXCEEDED: - raise errors.LimitsExceeded() - except _ldap.NOT_ALLOWED_ON_RDN: - raise errors.NotAllowedOnRDN(attr=info) - except _ldap.FILTER_ERROR: - raise errors.BadSearchFilter(info=info) - except _ldap.SUCCESS: - pass - except _ldap.LDAPError, e: - if 'NOT_ALLOWED_TO_DELEGATE' in info: - raise errors.ACIError(info="KDC returned NOT_ALLOWED_TO_DELEGATE") - root_logger.info('Unhandled LDAPError: %s' % str(e)) - raise errors.DatabaseError(desc=desc, info=info) - - -def get_schema(url, conn=None): - """ - Perform global initialization when the module is loaded. + ipa_dn = DN(original_dn) + ipa_attrs = dict() - Retrieve the LDAP schema from the provided url and determine if - User-Private Groups (upg) are configured. + for attr, original_values in original_attrs.items(): + target_type = self._SYNTAX_MAPPING.get(self.get_syntax(attr), unicode_from_utf8) + ipa_attrs[attr.lower()] = self.convert_value_list(attr, target_type, original_values) - Bind using kerberos credentials. If in the context of the - in-tree "lite" server then use the current ccache. If in the context of - Apache then create a new ccache and bind using the Apache HTTP service - principal. + ipa_result.append(LDAPEntry(ipa_dn, ipa_attrs)) - If a connection is provided then it the credentials bound to it are - used. The connection is not closed when the request is done. - """ - tmpdir = None - has_conn = conn is not None + if _debug_log_ldap: + self.debug('ldap.result: %s', ipa_result) + return ipa_result - if ((not api.env.in_server or api.env.context not in ['lite', 'server']) - and conn is None): - # The schema is only needed on the server side - return None + #---------- python-ldap emulations ---------- - try: - if api.env.context == 'server' and conn is None: - try: - # Create a new credentials cache for this Apache process - tmpdir = tempfile.mkdtemp(prefix = "tmp-") - ccache_file = 'FILE:%s/ccache' % tmpdir - krbcontext = krbV.default_context() - principal = str('HTTP/%s@%s' % (api.env.host, api.env.realm)) - keytab = krbV.Keytab(name='/etc/httpd/conf/ipa.keytab', context=krbcontext) - principal = krbV.Principal(name=principal, context=krbcontext) - os.environ['KRB5CCNAME'] = ccache_file - ccache = krbV.CCache(name=ccache_file, context=krbcontext, primary_principal=principal) - ccache.init(principal) - ccache.init_creds_keytab(keytab=keytab, principal=principal) - except krbV.Krb5Error, e: - raise StandardError('Unable to retrieve LDAP schema. Error initializing principal %s in %s: %s' % (principal.name, '/etc/httpd/conf/ipa.keytab', str(e))) - - if conn is None: - conn = IPASimpleLDAPObject(url) - if url.startswith('ldapi://'): - conn.set_option(_ldap.OPT_HOST_NAME, api.env.host) - conn.sasl_interactive_bind_s('', SASL_AUTH) - - schema_entry = conn.search_s( - 'cn=schema', _ldap.SCOPE_BASE, - attrlist=['attributetypes', 'objectclasses'] - )[0] - if not has_conn: - conn.unbind_s() - except _ldap.SERVER_DOWN: - return None - except _ldap.LDAPError, e: - desc = e.args[0]['desc'].strip() - info = e.args[0].get('info', '').strip() - raise StandardError('Unable to retrieve LDAP schema: %s: %s' % (desc, info)) - 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 - finally: - if tmpdir: - shutil.rmtree(tmpdir) - - return _ldap.schema.SubSchema(schema_entry[1]) - -# Global schema -_schema = None - -class ldap2(CrudBackend, Encoder): + def add(self, dn, modlist): + assert isinstance(dn, DN) + dn = str(dn) + modlist = self.encode(modlist) + return self.conn.add(dn, modlist) + + def add_ext(self, dn, modlist, serverctrls=None, clientctrls=None): + assert isinstance(dn, DN) + dn = str(dn) + modlist = self.encode(modlist) + return self.conn.add_ext(dn, modlist, serverctrls, clientctrls) + + def add_ext_s(self, dn, modlist, serverctrls=None, clientctrls=None): + assert isinstance(dn, DN) + dn = str(dn) + modlist = self.encode(modlist) + return self.conn.add_ext_s(dn, modlist, serverctrls, clientctrls) + + def add_s(self, dn, modlist): + assert isinstance(dn, DN) + dn = str(dn) + modlist = self.encode(modlist) + return self.conn.add_s(dn, modlist) + + def bind(self, who, cred, method=_ldap.AUTH_SIMPLE): + self.flush_cached_schema() + if who is None: + who = DN() + assert isinstance(who, DN) + who = str(who) + cred = self.encode(cred) + return self.conn.bind(who, cred, method) + + def delete(self, dn): + assert isinstance(dn, DN) + dn = str(dn) + return self.conn.delete(dn) + + def delete_s(self, dn): + assert isinstance(dn, DN) + dn = str(dn) + return self.conn.delete_s(dn) + + def get_option(self, option): + return self.conn.get_option(option) + + def modify_s(self, dn, modlist): + assert isinstance(dn, DN) + dn = str(dn) + modlist = [(x[0], self.encode(x[1]), self.encode(x[2])) for x in modlist] + return self.conn.modify_s(dn, modlist) + + def modrdn_s(self, dn, newrdn, delold=1): + assert isinstance(dn, DN) + dn = str(dn) + assert isinstance(newrdn, (DN, RDN)) + newrdn = str(newrdn) + return self.conn.modrdn_s(dn, newrdn, delold) + + def passwd_s(self, dn, oldpw, newpw, serverctrls=None, clientctrls=None): + assert isinstance(dn, DN) + dn = str(dn) + oldpw = self.encode(oldpw) + newpw = self.encode(newpw) + return self.conn.passwd_s(dn, oldpw, newpw, serverctrls, clientctrls) + + def rename_s(self, dn, newrdn, newsuperior=None, delold=1, serverctrls=None, clientctrls=None): + assert isinstance(dn, DN) + dn = str(dn) + assert isinstance(newrdn, (DN, RDN)) + newrdn = str(newrdn) + return self.conn.rename_s(dn, newrdn, newsuperior, delold, serverctrls, clientctrls) + + def result(self, msgid=_ldap.RES_ANY, all=1, timeout=None): + resp_type, resp_data = self.conn.result(msgid, all, timeout) + resp_data = self.convert_result(resp_data) + return resp_type, resp_data + + def sasl_interactive_bind_s(self, who, auth, serverctrls=None, clientctrls=None, sasl_flags=_ldap.SASL_QUIET): + self.flush_cached_schema() + if who is None: + who = DN() + assert isinstance(who, DN) + who = str(who) + return self.conn.sasl_interactive_bind_s(who, auth, serverctrls, clientctrls, sasl_flags) + + def search(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + assert isinstance(base, DN) + base = str(base) + filterstr = self.encode(filterstr) + attrlist = self.encode(attrlist) + return self.conn.search(base, scope, filterstr, attrlist, attrsonly) + + def search_ext(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0): + assert isinstance(base, DN) + base = str(base) + filterstr = self.encode(filterstr) + attrlist = self.encode(attrlist) + + if _debug_log_ldap: + self.debug("ldap.search_ext: dn: %s\nfilter: %s\nattrs_list: %s", base, filterstr, attrlist) + + + return self.conn.search_ext(base, scope, filterstr, attrlist, attrsonly, serverctrls, clientctrls, timeout, sizelimit) + + def search_ext_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0): + assert isinstance(base, DN) + base = str(base) + filterstr = self.encode(filterstr) + attrlist = self.encode(attrlist) + ldap_result = self.conn.search_ext_s(base, scope, filterstr, attrlist, attrsonly, serverctrls, clientctrls, timeout, sizelimit) + ipa_result = self.convert_result(ldap_result) + return ipa_result + + def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0): + assert isinstance(base, DN) + base = str(base) + filterstr = self.encode(filterstr) + attrlist = self.encode(attrlist) + ldap_result = self.conn.search_s(base, scope, filterstr, attrlist, attrsonly) + ipa_result = self.convert_result(ldap_result) + return ipa_result + + def search_st(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, timeout=-1): + assert isinstance(base, DN) + base = str(base) + filterstr = self.encode(filterstr) + attrlist = self.encode(attrlist) + ldap_result = self.conn.search_st(base, scope, filterstr, attrlist, attrsonly, timeout) + ipa_result = self.convert_result(ldap_result) + return ipa_result + + def set_option(self, option, invalue): + return self.conn.set_option(option, invalue) + + def simple_bind_s(self, who=None, cred='', serverctrls=None, clientctrls=None): + self.flush_cached_schema() + if who is None: + who = DN() + assert isinstance(who, DN) + who = str(who) + cred = self.encode(cred) + return self.conn.simple_bind_s(who, cred, serverctrls, clientctrls) + + def start_tls_s(self): + return self.conn.start_tls_s() + + def unbind(self): + self.flush_cached_schema() + return self.conn.unbind() + + def unbind_s(self): + self.flush_cached_schema() + return self.conn.unbind_s() + +class ldap2(CrudBackend): """ LDAP Backend Take 2. """ - # attribute syntax to python type mapping, 'SYNTAX OID': type - # everything not in this dict is considered human readable unicode - _SYNTAX_MAPPING = { - '1.3.6.1.4.1.1466.115.121.1.1': str, # ACI item - '1.3.6.1.4.1.1466.115.121.1.4': str, # Audio - '1.3.6.1.4.1.1466.115.121.1.5': str, # Binary - '1.3.6.1.4.1.1466.115.121.1.8': str, # Certificate - '1.3.6.1.4.1.1466.115.121.1.9': str, # Certificate List - '1.3.6.1.4.1.1466.115.121.1.10': str, # Certificate Pair - '1.3.6.1.4.1.1466.115.121.1.23': str, # Fax - '1.3.6.1.4.1.1466.115.121.1.28': str, # JPEG - '1.3.6.1.4.1.1466.115.121.1.40': str, # OctetString (same as Binary) - '1.3.6.1.4.1.1466.115.121.1.49': str, # Supported Algorithm - '1.3.6.1.4.1.1466.115.121.1.51': str, # Teletext Terminal Identifier - } - # attributes in this list cannot be deleted by update_entry # only MOD_REPLACE operations are generated for them _FORCE_REPLACE_ON_UPDATE_ATTRS = [] @@ -349,27 +644,19 @@ class ldap2(CrudBackend, Encoder): def __init__(self, shared_instance=True, ldap_uri=None, base_dn=None, schema=None): - global _schema + log_mgr.get_logger(self, True) CrudBackend.__init__(self, shared_instance=shared_instance) - Encoder.__init__(self) - self.encoder_settings.encode_dict_keys = True - self.encoder_settings.decode_dict_keys = True - self.encoder_settings.decode_dict_vals_postprocess = False - self.encoder_settings.decode_dict_vals_table = self._SYNTAX_MAPPING - self.encoder_settings.decode_dict_vals_table_keygen = self.get_syntax - self.encoder_settings.decode_postprocessor = lambda x: string.lower(x) try: self.ldap_uri = ldap_uri or api.env.ldap_uri except AttributeError: self.ldap_uri = 'ldap://example.com' try: if base_dn is not None: - self.base_dn = base_dn + self.base_dn = DN(base_dn) else: - self.base_dn = api.env.basedn + self.base_dn = DN(api.env.basedn) except AttributeError: - self.base_dn = '' - self.schema = schema or _schema + self.base_dn = DN() def __del__(self): if self.isconnected(): @@ -378,18 +665,83 @@ class ldap2(CrudBackend, Encoder): def __str__(self): return self.ldap_uri + def _get_schema(self): + return self.conn.schema + schema = property(_get_schema, None, None, 'schema associated with this LDAP server') + + # universal LDAPError handler + def handle_errors(self, e): + """ + Centralize error handling in one place. + + e is the error to be raised + """ + if not isinstance(e, _ldap.TIMEOUT): + desc = e.args[0]['desc'].strip() + info = e.args[0].get('info', '').strip() + else: + desc = '' + info = '' + + try: + # re-raise the error so we can handle it + raise e + except _ldap.NO_SUCH_OBJECT: + raise errors.NotFound(reason='no such entry') + except _ldap.ALREADY_EXISTS: + raise errors.DuplicateEntry() + except _ldap.CONSTRAINT_VIOLATION: + # This error gets thrown by the uniqueness plugin + if info.startswith('Another entry with the same attribute value already exists'): + raise errors.DuplicateEntry() + else: + raise errors.DatabaseError(desc=desc, info=info) + except _ldap.INSUFFICIENT_ACCESS: + raise errors.ACIError(info=info) + except _ldap.INVALID_CREDENTIALS: + raise errors.ACIError(info="%s %s" % (info, desc)) + except _ldap.NO_SUCH_ATTRIBUTE: + # this is raised when a 'delete' attribute isn't found. + # it indicates the previous attribute was removed by another + # update, making the oldentry stale. + raise errors.MidairCollision() + except _ldap.INVALID_SYNTAX: + raise errors.InvalidSyntax(attr=info) + except _ldap.OBJECT_CLASS_VIOLATION: + raise errors.ObjectclassViolation(info=info) + except _ldap.ADMINLIMIT_EXCEEDED: + raise errors.LimitsExceeded() + except _ldap.SIZELIMIT_EXCEEDED: + raise errors.LimitsExceeded() + except _ldap.TIMELIMIT_EXCEEDED: + raise errors.LimitsExceeded() + except _ldap.NOT_ALLOWED_ON_RDN: + raise errors.NotAllowedOnRDN(attr=info) + except _ldap.FILTER_ERROR: + raise errors.BadSearchFilter(info=info) + except _ldap.SUCCESS: + pass + except _ldap.LDAPError, e: + if 'NOT_ALLOWED_TO_DELEGATE' in info: + raise errors.ACIError(info="KDC returned NOT_ALLOWED_TO_DELEGATE") + self.info('Unhandled LDAPError: %s' % str(e)) + raise errors.DatabaseError(desc=desc, info=info) + def get_syntax(self, attr, value): - if not self.schema: - self.get_schema() + if self.schema is None: + return None obj = self.schema.get_obj(_ldap.schema.AttributeType, attr) if obj is not None: return obj.syntax else: return None + def has_dn_syntax(self, attr): + return self.conn.has_dn_syntax(attr) + def get_allowed_attributes(self, objectclasses, raise_on_unknown=False): - if not self.schema: - self.get_schema() + if self.schema is None: + return None allowed_attributes = [] for oc in objectclasses: obj = self.schema.get_obj(_ldap.schema.ObjectClass, oc) @@ -408,13 +760,12 @@ class ldap2(CrudBackend, Encoder): If there is a problem loading the schema or the attribute is not in the schema return None """ - if not self.schema: - self.get_schema() + if self.schema is None: + return None obj = self.schema.get_obj(_ldap.schema.AttributeType, attr) return obj and obj.single_value - @encode_args(2, 3, 'bind_dn', 'bind_pw') - def create_connection(self, ccache=None, bind_dn='', bind_pw='', + def create_connection(self, ccache=None, bind_dn=None, bind_pw='', tls_cacertfile=None, tls_certfile=None, tls_keyfile=None, debug_level=0, autobind=False): """ @@ -433,7 +784,9 @@ class ldap2(CrudBackend, Encoder): Extends backend.Connectible.create_connection. """ - global _schema + if bind_dn is None: + bind_dn = DN() + assert isinstance(bind_dn, DN) if tls_cacertfile is not None: _ldap.set_option(_ldap.OPT_X_TLS_CACERTFILE, tls_cacertfile) if tls_certfile is not None: @@ -445,7 +798,7 @@ class ldap2(CrudBackend, Encoder): _ldap.set_option(_ldap.OPT_DEBUG_LEVEL, debug_level) try: - conn = _ldap.initialize(self.ldap_uri) + conn = IPASimpleLDAPObject(self.ldap_uri) if self.ldap_uri.startswith('ldapi://') and ccache: conn.set_option(_ldap.OPT_HOST_NAME, api.env.host) minssf = conn.get_option(_ldap.OPT_X_SASL_SSF_MIN) @@ -459,7 +812,7 @@ class ldap2(CrudBackend, Encoder): conn.set_option(_ldap.OPT_X_SASL_SSF_MAX, minssf) if ccache is not None: os.environ['KRB5CCNAME'] = ccache - conn.sasl_interactive_bind_s('', SASL_AUTH) + conn.sasl_interactive_bind_s(None, SASL_AUTH) principal = krbV.CCache(name=ccache, context=krbV.default_context()).principal().name setattr(context, 'principal', principal) @@ -468,15 +821,13 @@ class ldap2(CrudBackend, Encoder): if autobind: pent = pwd.getpwuid(os.geteuid()) auth_tokens = _ldap.sasl.external(pent.pw_name) - conn.sasl_interactive_bind_s("", auth_tokens) + conn.sasl_interactive_bind_s(None, auth_tokens) else: conn.simple_bind_s(bind_dn, bind_pw) except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) - if _schema: - object.__setattr__(self, 'schema', _schema) return conn def destroy_connection(self): @@ -489,55 +840,41 @@ class ldap2(CrudBackend, Encoder): def normalize_dn(self, dn): """ - Normalize distinguished name. + Normalize distinguished name by assuring it ends with + the base_dn. Note: You don't have to normalize DN's before passing them to ldap2 methods. It's done internally for you. """ - rdns = explode_dn(dn) - if rdns: - dn = ','.join(rdns) - if not dn.endswith(self.base_dn): - dn = '%s,%s' % (dn, self.base_dn) - return dn - return self.base_dn - - 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 '%s=%s' % (attr, value) + assert isinstance(dn, DN) - def make_dn_from_rdn(self, rdn, parent_dn=''): - """ - Make distinguished name from relative distinguished name. + if not dn.endswith(self.base_dn): + # DN's are mutable, don't use in-place addtion (+=) which would + # modify the dn passed in with unintended side-effects. Addition + # returns a new DN object which is the concatenation of the two. + dn = dn + self.base_dn - Keyword arguments: - parent_dn -- DN of the parent entry (default '') - """ - parent_dn = self.normalize_dn(parent_dn) - return '%s,%s' % (rdn, parent_dn) + return dn - def make_dn_from_attr(self, attr, value, parent_dn=''): + def make_dn_from_attr(self, attr, value, parent_dn=None): """ Make distinguished name from attribute. Keyword arguments: parent_dn -- DN of the parent entry (default '') """ - rdn = self.make_rdn_from_attr(attr, value) - return self.make_dn_from_rdn(rdn, parent_dn) + if parent_dn is None: + parent_dn = DN() + assert isinstance(parent_dn, DN) + parent_dn = self.normalize_dn(parent_dn) + + if isinstance(value, (list, tuple)): + value = value[0] + + return DN((attr, value), parent_dn) - def make_dn(self, entry_attrs, primary_key='cn', parent_dn=''): + def make_dn(self, entry_attrs, primary_key='cn', parent_dn=None): """ Make distinguished name from entry attributes. @@ -545,23 +882,32 @@ class ldap2(CrudBackend, Encoder): 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) - @encode_args(1, 2) + if parent_dn is None: + parent_dn = DN() + + parent_dn = self.normalize_dn(parent_dn) + return DN((primary_key, entry_attrs[primary_key]), parent_dn) + def add_entry(self, dn, entry_attrs, normalize=True): """Create a new entry.""" + + assert isinstance(dn, DN) + if normalize: dn = self.normalize_dn(dn) - # remove all None values, python-ldap hates'em + # remove all None or [] values, python-ldap hates'em entry_attrs = dict( - (k, v) for (k, v) in entry_attrs.iteritems() if v + # FIXME, shouldn't these values be an error? + (k, v) for (k, v) in entry_attrs.iteritems() + if v is not None and v != [] ) try: self.conn.add_s(dn, list(entry_attrs.iteritems())) except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) # generating filters for find_entry # some examples: @@ -580,7 +926,9 @@ class ldap2(CrudBackend, Encoder): Keyword arguments: rules -- see ldap2.make_filter """ + assert isinstance(filters, (list, tuple)) + filters = [f for f in filters if f] if filters and rules == self.MATCH_NONE: # unary operator return '(%s%s)' % (self.MATCH_NONE, @@ -598,7 +946,6 @@ class ldap2(CrudBackend, Encoder): flt = '%s)' % flt return flt - @encode_args(1, 2) def make_filter_from_attr(self, attr, value, rules='|', exact=True, leading_wildcard=True, trailing_wildcard=True): """ @@ -620,7 +967,7 @@ class ldap2(CrudBackend, Encoder): trailing_wildcard=trailing_wildcard) for v in value ] return self.combine_filters(flts, rules) elif value is not None: - value = _ldap_filter.escape_filter_chars(value) + value = _ldap_filter.escape_filter_chars(value_to_utf8(value)) if not exact: template = '%s' if leading_wildcard: @@ -671,9 +1018,7 @@ class ldap2(CrudBackend, Encoder): ) return self.combine_filters(flts, rules) - @encode_args(1, 2, 3) - @decode_retval() - def find_entries(self, filter=None, attrs_list=None, base_dn='', + def find_entries(self, filter=None, attrs_list=None, base_dn=None, scope=_ldap.SCOPE_SUBTREE, time_limit=None, size_limit=None, normalize=True, search_refs=False): """ @@ -691,6 +1036,9 @@ class ldap2(CrudBackend, Encoder): normalize -- normalize the DN (default True) search_refs -- allow search references to be returned (default skips these entries) """ + if base_dn is None: + base_dn = DN() + assert isinstance(base_dn, DN) if normalize: base_dn = self.normalize_dn(base_dn) if not filter: @@ -731,7 +1079,7 @@ class ldap2(CrudBackend, Encoder): _ldap.SIZELIMIT_EXCEEDED), e: truncated = True except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) if not res and not truncated: raise errors.NotFound(reason='no such entry') @@ -756,7 +1104,8 @@ class ldap2(CrudBackend, Encoder): del r[1]['memberOf'] else: continue - (direct, indirect) = self.get_memberof(r[0], memberof, time_limit=time_limit, size_limit=size_limit, normalize=normalize) + (direct, indirect) = self.get_memberof(r[0], memberof, time_limit=time_limit, + size_limit=size_limit, normalize=normalize) if len(direct) > 0: r[1]['memberof'] = direct if len(indirect) > 0: @@ -764,8 +1113,7 @@ class ldap2(CrudBackend, Encoder): return (res, truncated) - def find_entry_by_attr(self, attr, value, object_class, attrs_list=None, - base_dn=''): + def find_entry_by_attr(self, attr, value, object_class, attrs_list=None, base_dn=None): """ Find entry (dn, entry_attrs) by attribute and object class. @@ -773,6 +1121,11 @@ class ldap2(CrudBackend, Encoder): 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 '') """ + + if base_dn is None: + base_dn = DN() + assert isinstance(base_dn, DN) + search_kw = {attr: value, 'objectClass': object_class} filter = self.make_filter(search_kw, rules=self.MATCH_ALL) (entries, truncated) = self.find_entries(filter, attrs_list, base_dn) @@ -793,6 +1146,9 @@ class ldap2(CrudBackend, Encoder): Keyword arguments: attrs_list - list of attributes to return, all if None (default None) """ + + assert isinstance(dn, DN) + (entry, truncated) = self.find_entries( None, attrs_list, dn, self.SCOPE_BASE, time_limit=time_limit, size_limit=size_limit, normalize=normalize @@ -805,7 +1161,12 @@ class ldap2(CrudBackend, Encoder): config_defaults = {'ipasearchtimelimit': [2], 'ipasearchrecordslimit': [0]} def get_ipa_config(self, attrs_list=None): """Returns the IPA configuration entry (dn, entry_attrs).""" - cdn = "%s,%s" % (api.Object.config.get_dn(), api.env.basedn) + + odn = api.Object.config.get_dn() + assert isinstance(odn, DN) + assert isinstance(api.env.basedn, DN) + cdn = DN(odn, api.env.basedn) + try: config_entry = getattr(context, 'config_entry') return (cdn, copy.deepcopy(config_entry)) @@ -828,35 +1189,17 @@ class ldap2(CrudBackend, Encoder): setattr(context, 'config_entry', copy.deepcopy(config_entry)) return (cdn, config_entry) - def get_schema(self, deepcopy=False): - """Returns either a reference to current schema or its deep copy""" - global _schema - if not _schema: - _schema = get_schema(self.ldap_uri, self.conn) - if not _schema: - raise errors.DatabaseError(desc='Unable to retrieve LDAP schema', info='Unable to proceed with request') - # explicitly use setattr here so the schema can be set after - # the object is finalized. - object.__setattr__(self, 'schema', _schema) - - if (deepcopy): - return copy.deepcopy(self.schema) - else: - return self.schema - def has_upg(self): """Returns True/False whether User-Private Groups are enabled. This is determined based on whether the UPG Template exists. """ - upg_dn = str(DN('cn=UPG Definition,cn=Definitions,cn=Managed Entries,cn=etc', api.env.basedn)) + upg_dn = DN(('cn', 'UPG Definition'), ('cn', 'Definitions'), ('cn', 'Managed Entries'), + ('cn', 'etc'), api.env.basedn) try: - upg_entry = self.conn.search_s( - upg_dn, - _ldap.SCOPE_BASE, - attrlist=['*'] - )[0] + upg_entry = self.conn.search_s(upg_dn, _ldap.SCOPE_BASE, + attrlist=['*'])[0] disable_attr = '(objectclass=disable)' if 'originfilter' in upg_entry[1]: org_filter = upg_entry[1]['originfilter'] @@ -866,27 +1209,32 @@ class ldap2(CrudBackend, Encoder): except _ldap.NO_SUCH_OBJECT, e: return False - @encode_args(1, 2) def get_effective_rights(self, dn, entry_attrs): """Returns the rights the currently bound user has for the given DN. Returns 2 attributes, the attributeLevelRights for the given list of attributes and the entryLevelRights for the entry itself. """ + + assert isinstance(dn, DN) + principal = getattr(context, 'principal') (binddn, attrs) = self.find_entry_by_attr("krbprincipalname", principal, "krbPrincipalAux") - sctrl = [GetEffectiveRightsControl(True, "dn: " + binddn.encode('UTF-8'))] + assert isinstance(binddn, DN) + sctrl = [GetEffectiveRightsControl(True, "dn: " + str(binddn))] self.conn.set_option(_ldap.OPT_SERVER_CONTROLS, sctrl) (dn, attrs) = self.get_entry(dn, entry_attrs) # remove the control so subsequent operations don't include GER self.conn.set_option(_ldap.OPT_SERVER_CONTROLS, []) return (dn, attrs) - @encode_args(1, 2) def can_write(self, dn, attr): """Returns True/False if the currently bound user has write permissions on the attribute. This only operates on a single attribute at a time. """ + + assert isinstance(dn, DN) + (dn, attrs) = self.get_effective_rights(dn, [attr]) if 'attributelevelrights' in attrs: attr_rights = attrs.get('attributelevelrights')[0].decode('UTF-8') @@ -896,11 +1244,12 @@ class ldap2(CrudBackend, Encoder): return False - @encode_args(1, 2) def can_read(self, dn, attr): """Returns True/False if the currently bound user has read permissions on the attribute. This only operates on a single attribute at a time. """ + assert isinstance(dn, DN) + (dn, attrs) = self.get_effective_rights(dn, [attr]) if 'attributelevelrights' in attrs: attr_rights = attrs.get('attributelevelrights')[0].decode('UTF-8') @@ -919,11 +1268,13 @@ class ldap2(CrudBackend, Encoder): # v - View the entry # - @encode_args(1) def can_delete(self, dn): """Returns True/False if the currently bound user has delete permissions on the entry. """ + + assert isinstance(dn, DN) + (dn, attrs) = self.get_effective_rights(dn, ["*"]) if 'entrylevelrights' in attrs: entry_rights = attrs['entrylevelrights'][0].decode('UTF-8') @@ -932,11 +1283,11 @@ class ldap2(CrudBackend, Encoder): return False - @encode_args(1) def can_add(self, dn): """Returns True/False if the currently bound user has add permissions on the entry. """ + assert isinstance(dn, DN) (dn, attrs) = self.get_effective_rights(dn, ["*"]) if 'entrylevelrights' in attrs: entry_rights = attrs['entrylevelrights'][0].decode('UTF-8') @@ -945,7 +1296,6 @@ class ldap2(CrudBackend, Encoder): return False - @encode_args(1, 2) def update_entry_rdn(self, dn, new_rdn, del_old=True): """ Update entry's relative distinguished name. @@ -953,22 +1303,25 @@ class ldap2(CrudBackend, Encoder): Keyword arguments: del_old -- delete old RDN value (default True) """ + + assert isinstance(dn, DN) + assert isinstance(new_rdn, RDN) + dn = self.normalize_dn(dn) - if dn.startswith(new_rdn + ","): + if dn[0] == new_rdn: raise errors.EmptyModlist() try: self.conn.rename_s(dn, new_rdn, delold=int(del_old)) time.sleep(.3) # Give memberOf plugin a chance to work except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) def _generate_modlist(self, dn, entry_attrs, normalize): + assert isinstance(dn, DN) + # get original entry (dn, entry_attrs_old) = self.get_entry(dn, entry_attrs.keys(), normalize=normalize) - # 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 - entry_attrs_old = self.encode(entry_attrs_old) + # generate modlist # for multi value attributes: no MOD_REPLACE to handle simultaneous # updates better @@ -1009,13 +1362,14 @@ class ldap2(CrudBackend, Encoder): return modlist - @encode_args(1, 2) def update_entry(self, dn, entry_attrs, normalize=True): """ Update entry's attributes. An attribute value set to None deletes all current values. """ + + assert isinstance(dn, DN) if normalize: dn = self.normalize_dn(dn) @@ -1028,37 +1382,40 @@ class ldap2(CrudBackend, Encoder): try: self.conn.modify_s(dn, modlist) except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) - @encode_args(1) def delete_entry(self, dn, normalize=True): """Delete entry.""" + + assert isinstance(dn, DN) if normalize: dn = self.normalize_dn(dn) + try: self.conn.delete_s(dn) except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) - @encode_args(1, 2, 3) def modify_password(self, dn, new_pass, old_pass=''): """Set user password.""" + + assert isinstance(dn, DN) dn = self.normalize_dn(dn) # The python-ldap passwd command doesn't verify the old password # so we'll do a simple bind to validate it. if old_pass != '': try: - conn = _ldap.initialize(self.ldap_uri) + conn = IPASimpleLDAPObject(self.ldap_uri) conn.simple_bind_s(dn, old_pass) conn.unbind() except _ldap.LDAPError, e: - _handle_errors(e, **{}) + self.handle_errors(e) try: self.conn.passwd_s(dn, old_pass, new_pass) except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) def add_entry_to_group(self, dn, group_dn, member_attr='member', allow_same=False): """ @@ -1068,12 +1425,18 @@ class ldap2(CrudBackend, Encoder): Adding a group as a member of itself is not allowed unless allow_same is True. """ + + assert isinstance(dn, DN) + assert isinstance(group_dn, DN) + + self.debug("add_entry_to_group: dn=%s group_dn=%s member_attr=%s", dn, group_dn, member_attr) # 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, [member_attr]) + self.debug("add_entry_to_group: group_entry_attrs=%s", group_entry_attrs) # check if we're not trying to add group into itself if dn == group_dn and not allow_same: raise errors.SameGroupError() @@ -1091,16 +1454,23 @@ class ldap2(CrudBackend, Encoder): def remove_entry_from_group(self, dn, group_dn, member_attr='member'): """Remove entry from group.""" + + assert isinstance(dn, DN) + assert isinstance(group_dn, DN) + + self.debug("remove_entry_from_group: dn=%s group_dn=%s member_attr=%s", dn, group_dn, member_attr) # get group entry (group_dn, group_entry_attrs) = self.get_entry(group_dn, [member_attr]) + self.debug("remove_entry_from_group: group_entry_attrs=%s", group_entry_attrs) # remove dn from group entry's `member_attr` attribute - members = [DN(m) for m in group_entry_attrs.get(member_attr, [])] + members = group_entry_attrs.get(member_attr, []) + assert all([isinstance(x, DN) for x in members]) try: - members.remove(DN(dn)) + members.remove(dn) except ValueError: raise errors.NotGroupMember() - group_entry_attrs[member_attr] = [str(m) for m in members] + group_entry_attrs[member_attr] = members # update group entry self.update_entry(group_dn, group_entry_attrs) @@ -1118,10 +1488,14 @@ class ldap2(CrudBackend, Encoder): Returns a list of DNs. """ + + assert isinstance(group_dn, DN) + if membertype not in [MEMBERS_ALL, MEMBERS_DIRECT, MEMBERS_INDIRECT]: return None - search_group_dn = _ldap_filter.escape_filter_chars(group_dn) + self.debug("get_members: group_dn=%s members=%s membertype=%s", group_dn, members, membertype) + search_group_dn = _ldap_filter.escape_filter_chars(str(group_dn)) searchfilter = "(memberof=%s)" % search_group_dn attr_list.append("member") @@ -1130,18 +1504,23 @@ class ldap2(CrudBackend, Encoder): results = [] if membertype == MEMBERS_ALL or membertype == MEMBERS_INDIRECT: - checkmembers = copy.deepcopy(members) - for member in checkmembers: + user_container_dn = DN(api.env.container_user, api.env.basedn) # FIXME, initialize once + host_container_dn = DN(api.env.container_host, api.env.basedn) + checkmembers = set(DN(x) for x in members) + checked = set() + while checkmembers: + member_dn = checkmembers.pop() + checked.add(member_dn) + # No need to check entry types that are not nested for # additional members - dn = DN(member) - if dn.endswith(DN(api.env.container_user, api.env.basedn)) or \ - dn.endswith(DN(api.env.container_host, api.env.basedn)): - results.append([member, {}]) + if member_dn.endswith(user_container_dn) or \ + member_dn.endswith(host_container_dn): + results.append([member_dn, {}]) continue try: (result, truncated) = self.find_entries(searchfilter, - attr_list, member, time_limit=time_limit, + attr_list, member_dn, time_limit=time_limit, size_limit=size_limit, scope=_ldap.SCOPE_BASE, normalize=normalize) if truncated: @@ -1150,8 +1529,8 @@ class ldap2(CrudBackend, Encoder): for m in result[0][1].get('member', []): # This member may contain other members, add it to our # candidate list - if m not in checkmembers: - checkmembers.append(m) + if m not in checked: + checkmembers.add(m) except errors.NotFound: pass @@ -1164,21 +1543,18 @@ class ldap2(CrudBackend, Encoder): (dn, group) = self.get_entry(group_dn, ['dn', 'member'], size_limit=size_limit, time_limit=time_limit) - real_members = group.get('member') - if isinstance(real_members, basestring): - real_members = [real_members] - if real_members is None: - real_members = [] + real_members = group.get('member', []) entries = [] for e in results: - if unicode(e[0]) not in real_members and unicode(e[0]) not in entries: + if e[0] not in real_members and e[0] not in entries: if membertype == MEMBERS_INDIRECT: entries.append(e[0]) else: if membertype == MEMBERS_DIRECT: entries.append(e[0]) + self.debug("get_members: result=%s", entries) return entries def get_memberof(self, entry_dn, memberof, time_limit=None, size_limit=None, normalize=True): @@ -1192,12 +1568,15 @@ class ldap2(CrudBackend, Encoder): Returns two memberof lists: (direct, indirect) """ + assert isinstance(entry_dn, DN) + + self.debug("get_memberof: entry_dn=%s memberof=%s", entry_dn, memberof) if not type(memberof) in (list, tuple): return ([], []) if len(memberof) == 0: return ([], []) - search_entry_dn = _ldap_filter.escape_filter_chars(entry_dn) + search_entry_dn = _ldap_filter.escape_filter_chars(str(entry_dn)) attr_list = ["dn", "memberof"] searchfilter = "(|(member=%s)(memberhost=%s)(memberuser=%s))" % ( search_entry_dn, search_entry_dn, search_entry_dn) @@ -1207,6 +1586,7 @@ class ldap2(CrudBackend, Encoder): results = [] for group in memberof: + assert isinstance(group, DN) try: (result, truncated) = self.find_entries(searchfilter, attr_list, group, time_limit=time_limit,size_limit=size_limit, @@ -1219,20 +1599,24 @@ class ldap2(CrudBackend, Encoder): # If there is an exception here, it is likely due to a failure in # referential integrity. All members should have corresponding # memberOf entries. - indirect = [ m.lower() for m in memberof ] + indirect = list(memberof) for r in results: direct.append(r[0]) try: - indirect.remove(r[0].lower()) + indirect.remove(r[0]) except ValueError, e: - root_logger.info('Failed to remove indirect entry %s from %s' % r[0], entry_dn) + self.info('Failed to remove indirect entry %s from %s' % r[0], entry_dn) raise e + self.debug("get_memberof: result direct=%s indirect=%s", direct, indirect) return (direct, indirect) def set_entry_active(self, dn, active): """Mark entry active/inactive.""" + + assert isinstance(dn, DN) assert isinstance(active, bool) + # get the entry in question (dn, entry_attrs) = self.get_entry(dn, ['nsaccountlock']) @@ -1255,15 +1639,20 @@ class ldap2(CrudBackend, Encoder): def activate_entry(self, dn): """Mark entry active.""" + + assert isinstance(dn, DN) self.set_entry_active(dn, True) def deactivate_entry(self, dn): """Mark entry inactive.""" + + assert isinstance(dn, DN) self.set_entry_active(dn, False) def remove_principal_key(self, dn): """Remove a kerberos principal key.""" + assert isinstance(dn, DN) dn = self.normalize_dn(dn) # We need to do this directly using the LDAP library because we @@ -1275,11 +1664,14 @@ class ldap2(CrudBackend, Encoder): try: self.conn.modify_s(dn, mod) except _ldap.LDAPError, e: - _handle_errors(e) + self.handle_errors(e) # CrudBackend methods def _get_normalized_entry_for_crud(self, dn, attrs_list=None): + + assert isinstance(dn, DN) + (dn, entry_attrs) = self.get_entry(dn, attrs_list) entry_attrs['dn'] = dn return entry_attrs @@ -1292,6 +1684,7 @@ class ldap2(CrudBackend, Encoder): """ assert 'dn' in kw dn = kw['dn'] + assert isinstance(dn, DN) del kw['dn'] self.add_entry(dn, kw) return self._get_normalized_entry_for_crud(dn) @@ -1337,7 +1730,8 @@ class ldap2(CrudBackend, Encoder): # get keyword arguments filter = kw.pop('filter', None) attrs_list = kw.pop('attrs_list', None) - base_dn = kw.pop('base_dn', '') + base_dn = kw.pop('base_dn', DN()) + assert isinstance(base_dn, DN) scope = kw.pop('scope', self.SCOPE_SUBTREE) # generate filter @@ -1363,4 +1757,3 @@ class ldap2(CrudBackend, Encoder): return (len(output), output) api.register(ldap2) - |