From e31b526c8174e7c55f69b1fdf31a6ee78197e8bc Mon Sep 17 00:00:00 2001 From: Kevin McCarthy Date: Mon, 27 Aug 2007 11:30:26 -0700 Subject: Enhanced user search: - "configurable" fields to search on - tokenize search words - prioritize exact matches over partial matches - split match filter generation into a re-usable function. Other updates: - use finally block to return ldap connections - update web gui to use new get_user methods --- ipa-server/ipa-gui/ipagui/controllers.py | 4 +- ipa-server/xmlrpc-server/funcs.py | 117 ++++++++++++++++++++++++------- 2 files changed, 95 insertions(+), 26 deletions(-) diff --git a/ipa-server/ipa-gui/ipagui/controllers.py b/ipa-server/ipa-gui/ipagui/controllers.py index eb89e5a6..7dff9c90 100644 --- a/ipa-server/ipa-gui/ipagui/controllers.py +++ b/ipa-server/ipa-gui/ipagui/controllers.py @@ -92,7 +92,7 @@ class Root(controllers.RootController): if tg_errors: turbogears.flash("There was a problem with the form!") - user = client.get_user(uid) + user = client.get_user_by_uid(uid) user_dict = user.toDict() # store a copy of the original user for the update later user_data = b64encode(dumps(user_dict)) @@ -155,7 +155,7 @@ class Root(controllers.RootController): def usershow(self, uid): """Retrieve a single user for display""" try: - user = client.get_user(uid) + user = client.get_user_by_uid(uid) return dict(user=user.toDict(), fields=forms.user.UserFields()) except ipaerror.IPAError, e: turbogears.flash("User show failed: " + str(e)) diff --git a/ipa-server/xmlrpc-server/funcs.py b/ipa-server/xmlrpc-server/funcs.py index 8994be91..a0049833 100644 --- a/ipa-server/xmlrpc-server/funcs.py +++ b/ipa-server/xmlrpc-server/funcs.py @@ -90,8 +90,10 @@ class IPAServer: filter = "(krbPrincipalName=" + princ + ")" # The only anonymous search we should have m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,None) - ent = m1.getEntry(self.basedn, self.scope, filter, ['dn']) - _LDAPPool.releaseConn(m1) + try: + ent = m1.getEntry(self.basedn, self.scope, filter, ['dn']) + finally: + _LDAPPool.releaseConn(m1) return "dn:" + ent.dn @@ -139,8 +141,10 @@ class IPAServer: dn = self.get_dn_from_principal(self.princ) m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) - ent = m1.getEntry(base, self.scope, filter, sattrs) - _LDAPPool.releaseConn(m1) + try: + ent = m1.getEntry(base, self.scope, filter, sattrs) + finally: + _LDAPPool.releaseConn(m1) return self.convert_entry(ent) @@ -169,8 +173,10 @@ class IPAServer: proxydn = self.get_dn_from_principal(self.princ) m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn) - res = m1.updateEntry(moddn, oldentry, newentry) - _LDAPPool.releaseConn(m1) + try: + res = m1.updateEntry(moddn, oldentry, newentry) + finally: + _LDAPPool.releaseConn(m1) return res def __safe_filter(self, criteria): @@ -181,9 +187,34 @@ class IPAServer: # where the second byte in a multi-byte character # is (illegally) ')' and make sure python-ldap # bombs out. - criteria = re.sub(r'[\(\)\\]', ldap_search_escape, criteria) + criteria = re.sub(r'[\(\)\\\*]', ldap_search_escape, criteria) return criteria + + def __generate_match_filters(self, search_fields, criteria_words): + """Generates a search filter based on a list of words and a list + of fields to search against. + + Returns a tuple of two filters: (exact_match, partial_match)""" + + # construct search pattern for a single word + # (|(f1=word)(f2=word)...) + search_pattern = "(|" + for field in search_fields: + search_pattern += "(" + field + "=%(match)s)" + search_pattern += ")" + gen_search_pattern = lambda word: search_pattern % {'match':word} + + # construct the giant match for all words + exact_match_filter = "(&" + partial_match_filter = "(&" + for word in criteria_words: + exact_match_filter += gen_search_pattern(word) + partial_match_filter += gen_search_pattern("*%s*" % word) + exact_match_filter += ")" + partial_match_filter += ")" + + return (exact_match_filter, partial_match_filter) # User support @@ -282,8 +313,10 @@ class IPAServer: dn = self.get_dn_from_principal(self.princ) m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) - res = m1.addEntry(entry) - _LDAPPool.releaseConn(m1) + try: + res = m1.addEntry(entry) + finally: + _LDAPPool.releaseConn(m1) return res def get_add_schema (self): @@ -344,8 +377,10 @@ class IPAServer: filter = "(objectclass=posixAccount)" m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) - all_users = m1.getList(self.basedn, self.scope, filter, None) - _LDAPPool.releaseConn(m1) + try: + all_users = m1.getList(self.basedn, self.scope, filter, None) + finally: + _LDAPPool.releaseConn(m1) users = [] for u in all_users: @@ -364,20 +399,46 @@ class IPAServer: dn = self.get_dn_from_principal(self.princ) + # Assume the list of fields to search will come from a central + # configuration repository. A good format for that would be + # a comma-separated list of fields + search_fields_conf_str = "uid,givenName,sn,telephoneNumber" + search_fields = string.split(search_fields_conf_str, ",") + criteria = self.__safe_filter(criteria) + criteria_words = re.split(r'\s+', criteria) + criteria_words = filter(lambda value:value!="", criteria_words) + if len(criteria_words) == 0: + return [] - filter = "(|(uid=%s)(cn=%s))" % (criteria, criteria) + (exact_match_filter, partial_match_filter) = self.__generate_match_filters( + search_fields, criteria_words) + + m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) try: - m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) - results = m1.getList(self.basedn, self.scope, filter, sattrs) + try: + exact_results = m1.getList(self.basedn, self.scope, + exact_match_filter, sattrs) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + exact_results = [] + + try: + partial_results = m1.getList(self.basedn, self.scope, + partial_match_filter, sattrs) + except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + partial_results = [] + finally: _LDAPPool.releaseConn(m1) - except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): - results = [] + + # Remove exact matches from the partial_match list + exact_dns = set(map(lambda e: e.dn, exact_results)) + partial_results = filter(lambda e: e.dn not in exact_dns, + partial_results) users = [] - for u in results: + for u in exact_results + partial_results: users.append(self.convert_entry(u)) - + return users def convert_scalar_values(self, orig_dict): @@ -416,8 +477,10 @@ class IPAServer: has_key = False m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn) - res = m1.inactivateEntry(user['dn'], has_key) - _LDAPPool.releaseConn(m1) + try: + res = m1.inactivateEntry(user['dn'], has_key) + finally: + _LDAPPool.releaseConn(m1) return res # Group support @@ -484,8 +547,10 @@ class IPAServer: dn = self.get_dn_from_principal(self.princ) m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) - res = m1.addEntry(entry) - _LDAPPool.releaseConn(m1) + try: + res = m1.addEntry(entry) + finally: + _LDAPPool.releaseConn(m1) def find_groups (self, criteria, sattrs=None, opts=None): """Return a list containing a User object for each @@ -501,12 +566,13 @@ class IPAServer: criteria = self.__safe_filter(criteria) filter = "(&(cn=%s)(objectClass=posixGroup))" % criteria + m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) try: - m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn) results = m1.getList(self.basedn, self.scope, filter, sattrs) - _LDAPPool.releaseConn(m1) except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): results = [] + finally: + _LDAPPool.releaseConn(m1) groups = [] for u in results: @@ -645,5 +711,8 @@ def ldap_search_escape(match): return "\\29" elif value == "\\": return "\\5c" + elif value == "*": + # drop '*' from input. search performs its own wildcarding + return "" else: return value -- cgit