summaryrefslogtreecommitdiffstats
path: root/ipa_server
diff options
context:
space:
mode:
Diffstat (limited to 'ipa_server')
-rw-r--r--ipa_server/ipaldap.py18
-rw-r--r--ipa_server/ipautil.py11
-rw-r--r--ipa_server/servercore.py209
-rwxr-xr-xipa_server/test_server162
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."