diff options
Diffstat (limited to 'ipa_server')
-rw-r--r-- | ipa_server/ipaldap.py | 18 | ||||
-rw-r--r-- | ipa_server/ipautil.py | 11 | ||||
-rw-r--r-- | ipa_server/servercore.py | 209 | ||||
-rwxr-xr-x | ipa_server/test_server | 162 |
4 files changed, 225 insertions, 175 deletions
diff --git a/ipa_server/ipaldap.py b/ipa_server/ipaldap.py index c1d134a0..4ab0d759 100644 --- a/ipa_server/ipaldap.py +++ b/ipa_server/ipaldap.py @@ -33,7 +33,8 @@ import struct import ldap.sasl from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples from ldap.ldapobject import SimpleLDAPObject -import ipautil +from ipa_server import ipautil + # Global variable to define SASL auth sasl_auth = ldap.sasl.sasl({},'GSSAPI') @@ -108,7 +109,20 @@ class Entry: """Convert the attrs and values to a list of 2-tuples. The first element of the tuple is the attribute name. The second element is either a single value or a list of values.""" - return self.data.items() + r = [] + for i in self.data.iteritems(): + n = ipautil.utf8_encode_values(i[1]) + r.append((i[0], n)) + return r + + def toDict(self): + """Convert the attrs and values to a dict. The dict is keyed on the + attribute name. The value is either single value or a list of values.""" + result = ipautil.CIDict(self.data) + for i in result.keys(): + result[i] = ipautil.utf8_encode_values(result[i]) + result['dn'] = self.dn + return result def __str__(self): """Convert the Entry to its LDIF representation""" diff --git a/ipa_server/ipautil.py b/ipa_server/ipautil.py index 6b0e2c89..6422fe5a 100644 --- a/ipa_server/ipautil.py +++ b/ipa_server/ipautil.py @@ -188,3 +188,14 @@ def get_gsserror(e): secondary = e[0][1] return (primary[0], secondary[0]) + +def utf8_encode_value(value): + if isinstance(value,unicode): + return value.encode('utf-8') + return value + +def utf8_encode_values(values): + if isinstance(values,list) or isinstance(values,tuple): + return map(utf8_encode_value, values) + else: + return utf8_encode_value(values) diff --git a/ipa_server/servercore.py b/ipa_server/servercore.py index eeecd4b0..f0d3dd9a 100644 --- a/ipa_server/servercore.py +++ b/ipa_server/servercore.py @@ -17,10 +17,9 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # -import sys -sys.path.insert(0, ".") -sys.path.insert(0, "..") import ldap +import string +import re from ipa_server.context import context import ipautil @@ -31,6 +30,10 @@ krbctx = krbV.default_context() realm = krbctx.default_realm basedn = ipautil.realm_to_suffix(realm) +DefaultUserContainer = "cn=users,cn=accounts" +DefaultGroupContainer = "cn=groups,cn=accounts" +DefaultServiceContainer = "cn=services,cn=accounts" + def convert_entry(ent): entry = dict(ent.data) entry['dn'] = ent.dn @@ -55,6 +58,30 @@ def convert_scalar_values(orig_dict): return new_dict +def generate_match_filters(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) # TODO: rethink the get_entry vs get_list API calls. # they currently restrict the data coming back without @@ -95,14 +122,160 @@ def get_list (base, searchfilter, sattrs=None): return map(convert_entry, entries) -def update_entry (oldentry, newentry): +# General searches + +def get_entry_by_dn (dn, sattrs=None): + """Get a specific entry. Return as a dict of values. + Multi-valued fields are represented as lists. + """ + searchfilter = "(objectClass=*)" +# logging.info("IPA: get_entry_by_dn '%s'" % dn) + return get_base_entry(dn, searchfilter, sattrs) + +# User support + +def is_user_unique(uid): + """Return True if the uid is unique in the tree, False otherwise.""" + # FIXME +# uid = self.__safe_filter(uid) + searchfilter = "(&(uid=%s)(objectclass=posixAccount))" % uid + + try: + entry = get_sub_entry("cn=accounts," + basedn, searchfilter, ['dn','uid']) + return False +# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + except Exception: + return True + +def get_user_by_uid (uid, sattrs): + """Get a specific user's entry. Return as a dict of values. + Multi-valued fields are represented as lists. + """ + + if not isinstance(uid,basestring) or len(uid) == 0: + raise SyntaxError("uid is not a string") +# raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER) + if sattrs is not None and not isinstance(sattrs,list): + raise SyntaxError("sattrs is not a list") +# raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER) +# logging.info("IPA: get_user_by_uid '%s'" % uid) +# uid = self.__safe_filter(uid) + searchfilter = "(uid=" + uid + ")" + return get_sub_entry("cn=accounts," + basedn, searchfilter, sattrs) + +def uid_too_long(uid): + """Verify that the new uid is within the limits we set. This is a + very narrow test. + + Returns True if it is longer than allowed + False otherwise + """ + if not isinstance(uid,basestring) or len(uid) == 0: + # It is bad, but not too long + return False +# logging.debug("IPA: __uid_too_long(%s)" % uid) + try: + config = get_ipa_config() + maxlen = int(config.get('ipamaxusernamelength', 0)) + if maxlen > 0 and len(uid) > maxlen: + return True + except Exception, e: +# logging.debug("There was a problem " + str(e)) + pass + + return False + +def find_users (criteria, sattrs, sizelimit=-1, timelimit=-1): + """Returns a list: counter followed by the results. + If the results are truncated, counter will be set to -1.""" + + """ + if not isinstance(criteria,basestring) or len(criteria) == 0: + raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER) + if sattrs is not None and not isinstance(sattrs, list): + raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER) + if not isinstance(sizelimit,int): + raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER) + if not isinstance(timelimit,int): + raise ipaerror.gen_exception(ipaerror.INPUT_INVALID_PARAMETER) + """ + +# logging.info("IPA: find_users '%s'" % criteria) + config = get_ipa_config() + if timelimit < 0: + timelimit = float(config.get('ipasearchtimelimit')) + if sizelimit < 0: + sizelimit = int(config.get('ipasearchrecordslimit')) + + # 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 = config.get('ipausersearchfields') + 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 [0] + + (exact_match_filter, partial_match_filter) = generate_match_filters( + search_fields, criteria_words) + + # + # further constrain search to just the objectClass + # TODO - need to parameterize this into generate_match_filters, + # and work it into the field-specification search feature + # + exact_match_filter = "(&(objectClass=person)%s)" % exact_match_filter + partial_match_filter = "(&(objectClass=person)%s)" % partial_match_filter + + try: + exact_results = context.conn.getConn().getListAsync("cn=accounts," + basedn, ldap.SCOPE_SUBTREE, exact_match_filter, sattrs, 0, None, None, timelimit, sizelimit) +# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + except Exception: + exact_results = [0] + + try: + partial_results = context.conn.getConn().getListAsync("cn=accounts," + basedn, ldap.SCOPE_SUBTREE, partial_match_filter, sattrs, 0, None, None, timelimit, sizelimit) +# except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND): + except Exception: + partial_results = [0] + + exact_counter = exact_results[0] + partial_counter = partial_results[0] + + exact_results = exact_results[1:] + partial_results = partial_results[1:] + + # 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) + + if (exact_counter == -1) or (partial_counter == -1): + counter = -1 + else: + counter = len(exact_results) + len(partial_results) + + users = [counter] + for u in exact_results + partial_results: + users.append(convert_entry(u)) + + return users + +def update_entry (entry): """Update an LDAP entry - oldentry is a dict - newentry is a dict + entry is a dict + + This refreshes the record from LDAP in order to obtain the list of + attributes that has changed. """ - oldentry = convert_scalar_values(oldentry) - newentry = convert_scalar_values(newentry) + attrs = entry.keys() + o = get_base_entry(entry['dn'], "objectclass=*", attrs) + oldentry = convert_scalar_values(o) + newentry = convert_scalar_values(entry) # Should be able to get this from either the old or new entry # but just in case someone has decided to try changing it, use the @@ -113,13 +286,16 @@ def update_entry (oldentry, newentry): # FIXME: return a missing DN error message raise e - res = context.conn.getConn().updateEntry(moddn, oldentry, newentry) - return res + return context.conn.getConn().updateEntry(moddn, oldentry, newentry) + +def add_entry(entry): + """Add a new entry""" + return context.conn.getConn().addEntry(entry) def uniq_list(x): """Return a unique list, preserving order and ignoring case""" myset = {} - return [set.setdefault(e.lower(),e) for e in x if e.lower() not in myset] + return [myset.setdefault(e.lower(),e) for e in x if e.lower() not in myset] def get_schema(): """Retrieves the current LDAP schema from the LDAP server.""" @@ -146,3 +322,14 @@ def get_objectclasses(): result.append(oc[3].replace("'","")) return result + +def get_ipa_config(): + """Retrieve the IPA configuration""" + searchfilter = "cn=ipaconfig" + try: + config = get_sub_entry("cn=etc," + basedn, searchfilter) + except ldap.NO_SUCH_OBJECT, e: + # FIXME + raise e + + return config diff --git a/ipa_server/test_server b/ipa_server/test_server deleted file mode 100755 index a6726369..00000000 --- a/ipa_server/test_server +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python - -import sys -sys.path.insert(0, "..") -sys.path.insert(0, ".") -import SimpleXMLRPCServer -import logging -import xmlrpclib -import re -import threading -import commands -from ipalib import api -import conn -from ipa_server.servercore import context -import ipalib.load_plugins -import traceback - -PORT=8888 - -class StoppableXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer): - """Override of TIME_WAIT""" - allow_reuse_address = True - - def serve_forever(self): - self.stop = False - while not self.stop: - self.handle_request() - -class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler): - """Overides the default SimpleXMLRPCRequestHander to support logging. - Logs client IP and the XML request and response. - """ - - def parse(self, given): - """Convert the incoming arguments into the format IPA expects""" - args = [] - kw = {} - for g in given: - kw[g] = unicode(given[g]) - return (args, kw) - - def _dispatch(self, method, params): - """Dispatches the XML-RPC method. - - Methods beginning with an '_' are considered private and will - not be called. - """ - - # this is fine for our test server - uid = commands.getoutput('/usr/bin/id -u') - krbccache = "FILE:/tmp/krb5cc_" + uid - - func = None - try: - # FIXME: don't hardcode host and port - context.conn = conn.IPAConn("localhost", 389, krbccache) - try: - # check to see if a matching function has been registered - func = funcs[method] - except KeyError: - raise Exception('method "%s" is not supported' % method) - if len(params) > 1 and isinstance(params[-1], dict): - kw = params[-1] - params = params[:-1] - return func(*params, **kw) - else: - return func(*params) - finally: - # Clean up any per-request data and connections -# for k in context.__dict__.keys(): -# del context.__dict__[k] - pass - - def _marshaled_dispatch(self, data, dispatch_method = None): - try: - params, method = xmlrpclib.loads(data) - - # generate response - if dispatch_method is not None: - response = dispatch_method(method, params) - else: - response = self._dispatch(method, params) - # wrap response in a singleton tuple - response = (response,) - response = xmlrpclib.dumps(response, methodresponse=1) - except: - # report exception back to client. This is needed to report - # tracebacks found in server code. - e_class, e = sys.exc_info()[:2] - # FIXME, need to get this number from somewhere... - faultCode = getattr(e_class,'faultCode',1) - tb_str = ''.join(traceback.format_exception(*sys.exc_info())) - faultString = tb_str - response = xmlrpclib.dumps(xmlrpclib.Fault(faultCode, faultString)) - - return response - - def do_POST(self): - clientIP, port = self.client_address - # Log client IP and Port - logger.info('Client IP: %s - Port: %s' % (clientIP, port)) - try: - # get arguments - data = self.rfile.read(int(self.headers["content-length"])) - - # unmarshal the XML data - params, method = xmlrpclib.loads(data) - - # Log client request - logger.info('Client request: \n%s\n' % data) - -# response = self.server._marshaled_dispatch( - response = self._marshaled_dispatch( - data, getattr(self, '_dispatch', None)) - - # Log server response - logger.info('Server response: \n%s\n' % response) - except Exception, e: - # This should only happen if the module is buggy - # internal error, report as HTTP server error - print e - self.send_response(500) - self.end_headers() - else: - # got a valid XML-RPC response - self.send_response(200) - self.send_header("Content-type", "text/xml") - self.send_header("Content-length", str(len(response))) - self.end_headers() - self.wfile.write(response) - - # shut down the connection - self.wfile.flush() - self.connection.shutdown(1) - -# Set up our logger -logger = logging.getLogger('xmlrpcserver') -hdlr = logging.FileHandler('xmlrpcserver.log') -formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s") -hdlr.setFormatter(formatter) -logger.addHandler(hdlr) -logger.setLevel(logging.INFO) - -# Set up the server -XMLRPCServer = StoppableXMLRPCServer(("",PORT), LoggingSimpleXMLRPCRequestHandler) - -XMLRPCServer.register_introspection_functions() - -# Get and register all the methods -api.finalize() -for cmd in api.Method: - logger.info("registering %s" % cmd) - XMLRPCServer.register_function(api.Method[cmd], cmd) - -funcs = XMLRPCServer.funcs - -print "Listening on port %d" % PORT -try: - XMLRPCServer.serve_forever() -except KeyboardInterrupt: - XMLRPCServer.server_close() - print "Server shutdown." |