diff options
author | Petr Viktorin <pviktori@redhat.com> | 2013-01-18 06:44:53 -0500 |
---|---|---|
committer | Martin Kosek <mkosek@redhat.com> | 2013-03-01 16:59:43 +0100 |
commit | 0c26913309fc97b59f2312b4e04f063fec6550b1 (patch) | |
tree | 35d7fa61429374e0b88c770d648dfcafe904ec9c | |
parent | ddee3aa900fb0e34cadb1d80842f254ffbe4d575 (diff) | |
download | freeipa-0c26913309fc97b59f2312b4e04f063fec6550b1.tar.gz freeipa-0c26913309fc97b59f2312b4e04f063fec6550b1.tar.xz freeipa-0c26913309fc97b59f2312b4e04f063fec6550b1.zip |
Move entry finding methods to LDAPConnection
The get_ipa_config method relies on IPA being installed.
Leave the implementation in ldap2, and add stub get_ipa_config that
simply returns an empty dictionary to LDAPConnection.
The get_members method contains an optimization that also relies on
the api object. The optimization is disabled in base LDAPConfig.
Part of the work for: https://fedorahosted.org/freeipa/ticket/2660
-rw-r--r-- | ipaserver/ipaldap.py | 321 | ||||
-rw-r--r-- | ipaserver/plugins/ldap2.py | 299 |
2 files changed, 328 insertions, 292 deletions
diff --git a/ipaserver/ipaldap.py b/ipaserver/ipaldap.py index db5fc9880..4ef224659 100644 --- a/ipaserver/ipaldap.py +++ b/ipaserver/ipaldap.py @@ -51,6 +51,12 @@ DEFAULT_TIMEOUT = 10 DN_SYNTAX_OID = '1.3.6.1.4.1.1466.115.121.1.12' _debug_log_ldap = False +# Group Member types +MEMBERS_ALL = 0 +MEMBERS_DIRECT = 1 +MEMBERS_INDIRECT = 2 + + def unicode_from_utf8(val): ''' val is a UTF-8 encoded string, return a unicode object. @@ -801,6 +807,13 @@ class LDAPConnection(object): def _init_connection(self): self.conn = None + def get_api(self): + """Return the API if available, otherwise None + + May be overridden in a subclass. + """ + return None + def handle_errors(self, e, arg_desc=None): """Universal LDAPError handler @@ -1078,6 +1091,314 @@ class LDAPConnection(object): ) return self.combine_filters(flts, rules) + 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): + """ + Return a list of entries and indication of whether the results were + truncated ([(dn, entry_attrs)], truncated) matching specified search + parameters followed by truncated flag. If the truncated flag is True, + search hit a server limit and its results are incomplete. + + 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 use IPA config values) + size_limit -- size (number of entries returned) limit + (default use IPA config values) + 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: + filter = '(objectClass=*)' + res = [] + truncated = False + + if time_limit is None or size_limit is None: + config = self.get_ipa_config() + if time_limit is None: + time_limit = config.get('ipasearchtimelimit', [-1])[0] + if size_limit is None: + size_limit = config.get('ipasearchrecordslimit', [0])[0] + if time_limit == 0: + time_limit = -1 + if not isinstance(size_limit, int): + size_limit = int(size_limit) + if not isinstance(time_limit, float): + time_limit = float(time_limit) + + if attrs_list: + attrs_list = list(set(attrs_list)) + + # pass arguments to python-ldap + try: + id = self.conn.search_ext( + base_dn, scope, filter, attrs_list, timeout=time_limit, + sizelimit=size_limit + ) + while True: + (objtype, res_list) = self.conn.result(id, 0) + if not res_list: + break + if (objtype == _ldap.RES_SEARCH_ENTRY or + (search_refs and + objtype == _ldap.RES_SEARCH_REFERENCE)): + res.append(res_list[0]) + except (_ldap.ADMINLIMIT_EXCEEDED, _ldap.TIMELIMIT_EXCEEDED, + _ldap.SIZELIMIT_EXCEEDED), e: + truncated = True + except _ldap.LDAPError, e: + self.handle_errors(e) + + if not res and not truncated: + raise errors.NotFound(reason='no such entry') + + if attrs_list and ( + 'memberindirect' in attrs_list or '*' in attrs_list): + for r in res: + if not 'member' in r[1]: + continue + else: + members = r[1]['member'] + indirect = self.get_members( + r[0], members, membertype=MEMBERS_INDIRECT, + time_limit=time_limit, size_limit=size_limit, + normalize=normalize) + if len(indirect) > 0: + r[1]['memberindirect'] = indirect + if attrs_list and ( + 'memberofindirect' in attrs_list or '*' in attrs_list): + for r in res: + if 'memberof' in r[1]: + memberof = r[1]['memberof'] + del r[1]['memberof'] + elif 'memberOf' in r[1]: + memberof = r[1]['memberOf'] + del r[1]['memberOf'] + else: + continue + 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: + r[1]['memberofindirect'] = indirect + + return (res, truncated) + + 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. + + 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 '') + """ + + 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) + + if len(entries) > 1: + raise errors.SingleMatchExpected(found=len(entries)) + else: + if truncated: + raise errors.LimitsExceeded() + else: + return entries[0] + + def get_entry(self, dn, attrs_list=None, time_limit=None, + size_limit=None, normalize=True): + """ + Get entry (dn, entry_attrs) by dn. + + 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 + ) + + if truncated: + raise errors.LimitsExceeded() + return entry[0] + + def get_ipa_config(self, attrs_list=None): + """Returns the IPA configuration entry. + + Overriden in the subclasses that have access to IPA configuration. + """ + return {} + + def get_memberof(self, entry_dn, memberof, time_limit=None, + size_limit=None, normalize=True): + """ + Examine the objects that an entry is a member of and determine if they + are a direct or indirect member of that group. + + entry_dn: dn of the entry we want the direct/indirect members of + memberof: the memberOf attribute for entry_dn + + Returns two memberof lists: (direct, indirect) + """ + + assert isinstance(entry_dn, DN) + + self.log.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(str(entry_dn)) + attr_list = ["dn", "memberof"] + searchfilter = "(|(member=%s)(memberhost=%s)(memberuser=%s))" % ( + search_entry_dn, search_entry_dn, search_entry_dn) + + # Search only the groups for which the object is a member to + # determine if it is directly or indirectly associated. + + 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, + scope=_ldap.SCOPE_BASE, normalize=normalize) + results.extend(list(result)) + except errors.NotFound: + pass + + direct = [] + # If there is an exception here, it is likely due to a failure in + # referential integrity. All members should have corresponding + # memberOf entries. + indirect = list(memberof) + for r in results: + direct.append(r[0]) + try: + indirect.remove(r[0]) + except ValueError, e: + self.log.info( + 'Failed to remove indirect entry %s from %s', + r[0], entry_dn) + raise e + + self.log.debug( + "get_memberof: result direct=%s indirect=%s", direct, indirect) + return (direct, indirect) + + def get_members(self, group_dn, members, attr_list=[], + membertype=MEMBERS_ALL, time_limit=None, size_limit=None, + normalize=True): + """Do a memberOf search of groupdn and return the attributes in + attr_list (an empty list returns all attributes). + + membertype = MEMBERS_ALL all members returned + membertype = MEMBERS_DIRECT only direct members are returned + membertype = MEMBERS_INDIRECT only inherited members are returned + + Members may be included in a group as a result of being a member + of a group that is a member of the group being queried. + + Returns a list of DNs. + """ + + assert isinstance(group_dn, DN) + + if membertype not in [MEMBERS_ALL, MEMBERS_DIRECT, MEMBERS_INDIRECT]: + return None + + self.log.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") + + # Verify group membership + + results = [] + if membertype == MEMBERS_ALL or membertype == MEMBERS_INDIRECT: + api = self.get_api() + if api: + user_container_dn = DN(api.env.container_user, api.env.basedn) + host_container_dn = DN(api.env.container_host, api.env.basedn) + else: + user_container_dn = host_container_dn = None + 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 + if user_container_dn and ( + 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_dn, + time_limit=time_limit, size_limit=size_limit, + scope=_ldap.SCOPE_BASE, normalize=normalize) + if truncated: + raise errors.LimitsExceeded() + results.append(list(result[0])) + for m in result[0][1].get('member', []): + # This member may contain other members, add it to our + # candidate list + if m not in checked: + checkmembers.add(m) + except errors.NotFound: + pass + + if membertype == MEMBERS_ALL: + entries = [] + for e in results: + entries.append(e[0]) + + return entries + + dn, group = self.get_entry( + group_dn, ['dn', 'member'], + size_limit=size_limit, time_limit=time_limit) + real_members = group.get('member', []) + + entries = [] + for e in results: + 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.log.debug("get_members: result=%s", entries) + return entries + class IPAdmin(LDAPConnection): diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py index a75e6bbc5..d7cd0a4ec 100644 --- a/ipaserver/plugins/ldap2.py +++ b/ipaserver/plugins/ldap2.py @@ -35,7 +35,6 @@ import pwd import krbV import ldap as _ldap -import ldap.filter as _ldap_filter from ipapython.dn import DN, RDN from ipaserver.ipaldap import ( @@ -60,11 +59,6 @@ from ipalib import api, errors from ipalib.crud import CrudBackend from ipalib.request import context -# Group Member types -MEMBERS_ALL = 0 -MEMBERS_DIRECT = 1 -MEMBERS_INDIRECT = 2 - class ldap2(LDAPConnection, CrudBackend): """ @@ -97,6 +91,9 @@ class ldap2(LDAPConnection, CrudBackend): # do not set it pass + def get_api(self): + return api + def __del__(self): if self.isconnected(): self.disconnect() @@ -225,146 +222,6 @@ class ldap2(LDAPConnection, CrudBackend): except _ldap.LDAPError, e: self.handle_errors(e) - 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): - """ - Return a list of entries and indication of whether the results were - truncated ([(dn, entry_attrs)], truncated) matching specified search - parameters followed by truncated flag. If the truncated flag is True, - search hit a server limit and its results are incomplete. - - 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 use IPA config values) - size_limit -- size (number of entries returned) limit (default use IPA config values) - 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: - filter = '(objectClass=*)' - res = [] - truncated = False - - if time_limit is None or size_limit is None: - (cdn, config) = self.get_ipa_config() - if time_limit is None: - time_limit = config.get('ipasearchtimelimit', [-1])[0] - if size_limit is None: - size_limit = config.get('ipasearchrecordslimit', [0])[0] - if time_limit == 0: - time_limit = -1 - if not isinstance(size_limit, int): - size_limit = int(size_limit) - if not isinstance(time_limit, float): - time_limit = float(time_limit) - - if attrs_list: - attrs_list = list(set(attrs_list)) - - # pass arguments to python-ldap - try: - id = self.conn.search_ext( - base_dn, scope, filter, attrs_list, timeout=time_limit, - sizelimit=size_limit - ) - while True: - (objtype, res_list) = self.conn.result(id, 0) - if not res_list: - break - if objtype == _ldap.RES_SEARCH_ENTRY or \ - (search_refs and objtype == _ldap.RES_SEARCH_REFERENCE): - res.append(res_list[0]) - except (_ldap.ADMINLIMIT_EXCEEDED, _ldap.TIMELIMIT_EXCEEDED, - _ldap.SIZELIMIT_EXCEEDED), e: - truncated = True - except _ldap.LDAPError, e: - self.handle_errors(e) - - if not res and not truncated: - raise errors.NotFound(reason='no such entry') - - if attrs_list and ('memberindirect' in attrs_list or '*' in attrs_list): - for r in res: - if not 'member' in r[1]: - continue - else: - members = r[1]['member'] - indirect = self.get_members(r[0], members, membertype=MEMBERS_INDIRECT, - time_limit=time_limit, size_limit=size_limit, normalize=normalize) - if len(indirect) > 0: - r[1]['memberindirect'] = indirect - if attrs_list and ('memberofindirect' in attrs_list or '*' in attrs_list): - for r in res: - if 'memberof' in r[1]: - memberof = r[1]['memberof'] - del r[1]['memberof'] - elif 'memberOf' in r[1]: - memberof = r[1]['memberOf'] - del r[1]['memberOf'] - else: - continue - (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: - r[1]['memberofindirect'] = indirect - - return (res, truncated) - - 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. - - 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 '') - """ - - 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) - - if len(entries) > 1: - raise errors.SingleMatchExpected(found=len(entries)) - else: - if truncated: - raise errors.LimitsExceeded() - else: - return entries[0] - - def get_entry(self, dn, attrs_list=None, time_limit=None, - size_limit=None, normalize=True): - """ - Get entry (dn, entry_attrs) by dn. - - 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 - ) - - if truncated: - raise errors.LimitsExceeded() - return entry[0] - config_defaults = {'ipasearchtimelimit': [2], 'ipasearchrecordslimit': [0]} def get_ipa_config(self, attrs_list=None): """Returns the IPA configuration entry (dn, entry_attrs).""" @@ -376,7 +233,7 @@ class ldap2(LDAPConnection, CrudBackend): try: config_entry = getattr(context, 'config_entry') - return (cdn, copy.deepcopy(config_entry)) + return copy.deepcopy(config_entry) except AttributeError: # Not in our context yet pass @@ -387,14 +244,14 @@ class ldap2(LDAPConnection, CrudBackend): ) if truncated: raise errors.LimitsExceeded() - (cdn, config_entry) = entry[0] + config_entry = entry[0] except errors.NotFound: config_entry = {} for a in self.config_defaults: if a not in config_entry: config_entry[a] = self.config_defaults[a] - setattr(context, 'config_entry', copy.deepcopy(config_entry)) - return (cdn, config_entry) + context.config_entry = copy.deepcopy(config_entry) + return config_entry def has_upg(self): """Returns True/False whether User-Private Groups are enabled. @@ -706,148 +563,6 @@ class ldap2(LDAPConnection, CrudBackend): # update group entry self.update_entry(group_dn, group_entry_attrs) - def get_members(self, group_dn, members, attr_list=[], membertype=MEMBERS_ALL, time_limit=None, size_limit=None, normalize=True): - """Do a memberOf search of groupdn and return the attributes in - attr_list (an empty list returns all attributes). - - membertype = MEMBERS_ALL all members returned - membertype = MEMBERS_DIRECT only direct members are returned - membertype = MEMBERS_INDIRECT only inherited members are returned - - Members may be included in a group as a result of being a member - of a group that is a member of the group being queried. - - Returns a list of DNs. - """ - - assert isinstance(group_dn, DN) - - if membertype not in [MEMBERS_ALL, MEMBERS_DIRECT, MEMBERS_INDIRECT]: - return None - - self.log.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") - - # Verify group membership - - results = [] - if membertype == MEMBERS_ALL or membertype == MEMBERS_INDIRECT: - 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 - 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_dn, time_limit=time_limit, - size_limit=size_limit, scope=_ldap.SCOPE_BASE, - normalize=normalize) - if truncated: - raise errors.LimitsExceeded() - results.append(list(result[0])) - for m in result[0][1].get('member', []): - # This member may contain other members, add it to our - # candidate list - if m not in checked: - checkmembers.add(m) - except errors.NotFound: - pass - - if membertype == MEMBERS_ALL: - entries = [] - for e in results: - entries.append(e[0]) - - return entries - - (dn, group) = self.get_entry(group_dn, ['dn', 'member'], - size_limit=size_limit, time_limit=time_limit) - real_members = group.get('member', []) - - entries = [] - for e in results: - 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.log.debug("get_members: result=%s", entries) - return entries - - def get_memberof(self, entry_dn, memberof, time_limit=None, size_limit=None, normalize=True): - """ - Examine the objects that an entry is a member of and determine if they - are a direct or indirect member of that group. - - entry_dn: dn of the entry we want the direct/indirect members of - memberof: the memberOf attribute for entry_dn - - Returns two memberof lists: (direct, indirect) - """ - - assert isinstance(entry_dn, DN) - - self.log.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(str(entry_dn)) - attr_list = ["dn", "memberof"] - searchfilter = "(|(member=%s)(memberhost=%s)(memberuser=%s))" % ( - search_entry_dn, search_entry_dn, search_entry_dn) - - # Search only the groups for which the object is a member to - # determine if it is directly or indirectly associated. - - 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, - scope=_ldap.SCOPE_BASE, normalize=normalize) - results.extend(list(result)) - except errors.NotFound: - pass - - direct = [] - # If there is an exception here, it is likely due to a failure in - # referential integrity. All members should have corresponding - # memberOf entries. - indirect = list(memberof) - for r in results: - direct.append(r[0]) - try: - indirect.remove(r[0]) - except ValueError, e: - self.log.info( - 'Failed to remove indirect entry %s from %s', - r[0], entry_dn) - raise e - - self.log.debug( - "get_memberof: result direct=%s indirect=%s", direct, indirect) - return (direct, indirect) - def set_entry_active(self, dn, active): """Mark entry active/inactive.""" |