From 7442ad2e27afa7719bfd7de16ac8b0b44cb418de Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Sun, 4 Jan 2009 18:44:16 -0700 Subject: Renamed ipa_server/ to ipaserver/ and tests/test_ipa_server/ to tests/test_ipaserver --- ipa_server/__init__.py | 22 -- ipa_server/conn.py | 69 ----- ipa_server/context.py | 32 -- ipa_server/ipaldap.py | 546 --------------------------------- ipa_server/ipautil.py | 201 ------------ ipa_server/mod_python_xmlrpc.py | 367 ---------------------- ipa_server/plugins/__init__.py | 24 -- ipa_server/plugins/b_ldap.py | 327 -------------------- ipa_server/plugins/b_ra.py | 406 ------------------------ ipa_server/rpc.py | 60 ---- ipa_server/servercore.py | 464 ---------------------------- ipa_server/test_client | 28 -- ipa_server/updates/automount.update | 54 ---- ipa_server/updates/groupofhosts.update | 5 - ipa_server/updates/host.update | 22 -- ipaserver/__init__.py | 22 ++ ipaserver/conn.py | 69 +++++ ipaserver/context.py | 32 ++ ipaserver/ipaldap.py | 546 +++++++++++++++++++++++++++++++++ ipaserver/ipautil.py | 201 ++++++++++++ ipaserver/mod_python_xmlrpc.py | 367 ++++++++++++++++++++++ ipaserver/plugins/__init__.py | 24 ++ ipaserver/plugins/b_ldap.py | 327 ++++++++++++++++++++ ipaserver/plugins/b_ra.py | 406 ++++++++++++++++++++++++ ipaserver/rpc.py | 60 ++++ ipaserver/servercore.py | 464 ++++++++++++++++++++++++++++ ipaserver/test_client | 28 ++ ipaserver/updates/automount.update | 54 ++++ ipaserver/updates/groupofhosts.update | 5 + ipaserver/updates/host.update | 22 ++ tests/test_ipa_server/__init__.py | 22 -- tests/test_ipa_server/test_rpc.py | 80 ----- tests/test_ipaserver/__init__.py | 22 ++ tests/test_ipaserver/test_rpc.py | 80 +++++ 34 files changed, 2729 insertions(+), 2729 deletions(-) delete mode 100644 ipa_server/__init__.py delete mode 100644 ipa_server/conn.py delete mode 100644 ipa_server/context.py delete mode 100644 ipa_server/ipaldap.py delete mode 100644 ipa_server/ipautil.py delete mode 100644 ipa_server/mod_python_xmlrpc.py delete mode 100644 ipa_server/plugins/__init__.py delete mode 100644 ipa_server/plugins/b_ldap.py delete mode 100644 ipa_server/plugins/b_ra.py delete mode 100644 ipa_server/rpc.py delete mode 100644 ipa_server/servercore.py delete mode 100755 ipa_server/test_client delete mode 100644 ipa_server/updates/automount.update delete mode 100644 ipa_server/updates/groupofhosts.update delete mode 100644 ipa_server/updates/host.update create mode 100644 ipaserver/__init__.py create mode 100644 ipaserver/conn.py create mode 100644 ipaserver/context.py create mode 100644 ipaserver/ipaldap.py create mode 100644 ipaserver/ipautil.py create mode 100644 ipaserver/mod_python_xmlrpc.py create mode 100644 ipaserver/plugins/__init__.py create mode 100644 ipaserver/plugins/b_ldap.py create mode 100644 ipaserver/plugins/b_ra.py create mode 100644 ipaserver/rpc.py create mode 100644 ipaserver/servercore.py create mode 100755 ipaserver/test_client create mode 100644 ipaserver/updates/automount.update create mode 100644 ipaserver/updates/groupofhosts.update create mode 100644 ipaserver/updates/host.update delete mode 100644 tests/test_ipa_server/__init__.py delete mode 100644 tests/test_ipa_server/test_rpc.py create mode 100644 tests/test_ipaserver/__init__.py create mode 100644 tests/test_ipaserver/test_rpc.py diff --git a/ipa_server/__init__.py b/ipa_server/__init__.py deleted file mode 100644 index b0be96bd..00000000 --- a/ipa_server/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -Package containing server backend. -""" diff --git a/ipa_server/conn.py b/ipa_server/conn.py deleted file mode 100644 index fb00ad99..00000000 --- a/ipa_server/conn.py +++ /dev/null @@ -1,69 +0,0 @@ -# Authors: Rob Crittenden -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import krbV -import ldap -import ldap.dn -import ipaldap - -class IPAConn: - def __init__(self, host, port, krbccache, debug=None): - self._conn = None - - # Save the arguments - self._host = host - self._port = port - self._krbccache = krbccache - self._debug = debug - - self._ctx = krbV.default_context() - - ccache = krbV.CCache(name=krbccache, context=self._ctx) - cprinc = ccache.principal() - - self._conn = ipaldap.IPAdmin(host,port,None,None,None,debug) - - # This will bind the connection - try: - self._conn.set_krbccache(krbccache, cprinc.name) - except ldap.UNWILLING_TO_PERFORM, e: - raise e - except Exception, e: - raise e - - def __del__(self): - # take no chances on unreleased connections - self.releaseConn() - - def getConn(self): - return self._conn - - def releaseConn(self): - if self._conn is None: - return - - self._conn.unbind_s() - self._conn = None - - return - -if __name__ == "__main__": - ipaconn = IPAConn("localhost", 389, "FILE:/tmp/krb5cc_500") - x = ipaconn.getConn().getEntry("dc=example,dc=com", ldap.SCOPE_SUBTREE, "uid=admin", ["cn"]) - print "%s" % x diff --git a/ipa_server/context.py b/ipa_server/context.py deleted file mode 100644 index 15dd7d90..00000000 --- a/ipa_server/context.py +++ /dev/null @@ -1,32 +0,0 @@ -# Authors: Rob Crittenden -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -# This should only be imported once. Importing again will cause the -# a new instance to be created in the same thread - -# To use: -# from ipaserver.context import context -# context.foo = "bar" - -# FIXME: This module is depreciated and code should switch to using -# ipalib.request instead - -import threading - -context = threading.local() diff --git a/ipa_server/ipaldap.py b/ipa_server/ipaldap.py deleted file mode 100644 index 19fd40ef..00000000 --- a/ipa_server/ipaldap.py +++ /dev/null @@ -1,546 +0,0 @@ -# Authors: Rich Megginson -# Rob Crittenden 0 - - def hasAttr(self,name): - """Return True if this entry has an attribute named name, False otherwise""" - return self.data and self.data.has_key(name) - - def __getattr__(self,name): - """If name is the name of an LDAP attribute, return the first value for that - attribute - equivalent to getValue - this allows the use of - entry.cn - instead of - entry.getValue('cn') - This also allows us to return None if an attribute is not found rather than - throwing an exception""" - return self.getValue(name) - - def getValues(self,name): - """Get the list (array) of values for the attribute named name""" - return self.data.get(name) - - def getValue(self,name): - """Get the first value for the attribute named name""" - return self.data.get(name,[None])[0] - - def setValue(self, name, *value): - """ - Set a value on this entry. - - The value passed in may be a single value, several values, or a - single sequence. For example: - - * ent.setValue('name', 'value') - * ent.setValue('name', 'value1', 'value2', ..., 'valueN') - * ent.setValue('name', ['value1', 'value2', ..., 'valueN']) - * ent.setValue('name', ('value1', 'value2', ..., 'valueN')) - - Since value is a tuple, we may have to extract a list or tuple from - that tuple as in the last two examples above. - """ - if isinstance(value[0],list) or isinstance(value[0],tuple): - self.data[name] = value[0] - else: - self.data[name] = value - - setValues = setValue - - def toTupleList(self): - """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.""" - 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""" - return self.__repr__() - - # the ldif class base64 encodes some attrs which I would rather see in - # raw form - to encode specific attrs as base64, add them to the list below - ldif.safe_string_re = re.compile('^$') - base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData'] - - def __repr__(self): - """Convert the Entry to its LDIF representation""" - sio = cStringIO.StringIO() - # what's all this then? the unparse method will currently only accept - # a list or a dict, not a class derived from them. self.data is a - # cidict, so unparse barfs on it. I've filed a bug against python-ldap, - # but in the meantime, we have to convert to a plain old dict for - # printing - # I also don't want to see wrapping, so set the line width really high - # (1000) - newdata = {} - newdata.update(self.data) - ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata) - return sio.getvalue() - -def wrapper(f,name): - """This is the method that wraps all of the methods of the superclass. - This seems to need to be an unbound method, that's why it's outside - of IPAdmin. Perhaps there is some way to do this with the new - classmethod or staticmethod of 2.4. Basically, we replace every call - to a method in SimpleLDAPObject (the superclass of IPAdmin) with a - call to inner. The f argument to wrapper is the bound method of - IPAdmin (which is inherited from the superclass). Bound means that it - will implicitly be called with the self argument, it is not in the - args list. name is the name of the method to call. If name is a - method that returns entry objects (e.g. result), we wrap the data - returned by an Entry class. If name is a method that takes an entry - argument, we extract the raw data from the entry object to pass in. - """ - def inner(*args, **kargs): - if name == 'result': - objtype, data = f(*args, **kargs) - # data is either a 2-tuple or a list of 2-tuples - # print data - if data: - if isinstance(data,tuple): - return objtype, Entry(data) - elif isinstance(data,list): - return objtype, [Entry(x) for x in data] - else: - raise TypeError, "unknown data type %s returned by result" % type(data) - else: - return objtype, data - elif name.startswith('add'): - # the first arg is self - # the second and third arg are the dn and the data to send - # We need to convert the Entry into the format used by - # python-ldap - ent = args[0] - if isinstance(ent,Entry): - return f(ent.dn, ent.toTupleList(), *args[2:]) - else: - return f(*args, **kargs) - else: - return f(*args, **kargs) - return inner - -class IPAdmin(SimpleLDAPObject): - - def __localinit(self): - """If a CA certificate is provided then it is assumed that we are - doing SSL client authentication with proxy auth. - - If a CA certificate is not present then it is assumed that we are - using a forwarded kerberos ticket for SASL auth. SASL provides - its own encryption. - """ - if self.cacert is not None: - SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port)) - else: - SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port)) - - def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None): - """We just set our instance variables and wrap the methods - the real - work is done in __localinit. This is separated out this way so - that we can call it from places other than instance creation - e.g. when we just need to reconnect - """ - if debug and debug.lower() == "on": - ldap.set_option(ldap.OPT_DEBUG_LEVEL,255) - if cacert is not None: - ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert) - if bindcert is not None: - ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert) - if bindkey is not None: - ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey) - - self.__wrapmethods() - self.port = port - self.host = host - self.cacert = cacert - self.bindcert = bindcert - self.bindkey = bindkey - self.proxydn = proxydn - self.suffixes = {} - self.__localinit() - - def __str__(self): - return self.host + ":" + str(self.port) - - def __get_server_controls(self): - """Create the proxy user server control. The control has the form - 0x04 = Octet String - 4|0x80 sets the length of the string length field at 4 bytes - the struct() gets us the length in bytes of string self.proxydn - self.proxydn is the proxy dn to send""" - - if self.proxydn is not None: - proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn; - - # Create the proxy control - sctrl=[] - sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn)) - else: - sctrl=None - - return sctrl - - def toLDAPURL(self): - return "ldap://%s:%d/" % (self.host,self.port) - - def set_proxydn(self, proxydn): - self.proxydn = proxydn - - def set_krbccache(self, krbccache, principal): - if krbccache is not None: - os.environ["KRB5CCNAME"] = krbccache - self.sasl_interactive_bind_s("", sasl_auth) - self.principal = principal - self.proxydn = None - - def do_simple_bind(self, binddn="cn=directory manager", bindpw=""): - self.binddn = binddn - self.bindpwd = bindpw - self.simple_bind_s(binddn, bindpw) - - def getEntry(self,*args): - """This wraps the search function. It is common to just get one entry""" - - sctrl = self.__get_server_controls() - - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - - try: - res = self.search(*args) - objtype, obj = self.result(res) - except ldap.NO_SUCH_OBJECT, e: - raise errors.NotFound, notfound(args) - except ldap.LDAPError, e: - raise errors.DatabaseError, e - - if not obj: - raise errors.NotFound, notfound(args) - - elif isinstance(obj,Entry): - return obj - else: # assume list/tuple - return obj[0] - - def getList(self,*args): - """This wraps the search function to find multiple entries.""" - - sctrl = self.__get_server_controls() - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - - try: - res = self.search(*args) - objtype, obj = self.result(res) - except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e: - # Too many results returned by search - raise e - except ldap.LDAPError, e: - raise e - - if not obj: - raise errors.NotFound, notfound(args) - - entries = [] - for s in obj: - entries.append(s) - - return entries - - def getListAsync(self,*args): - """This version performs an asynchronous search, to allow - results even if we hit a limit. - - It returns a list: counter followed by the results. - If the results are truncated, counter will be set to -1. - """ - - sctrl = self.__get_server_controls() - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - - entries = [] - partial = 0 - - try: - msgid = self.search_ext(*args) - objtype, result_list = self.result(msgid, 0) - while result_list: - for result in result_list: - entries.append(result) - objtype, result_list = self.result(msgid, 0) - except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, - ldap.TIMELIMIT_EXCEEDED), e: - partial = 1 - except ldap.LDAPError, e: - raise e - - if not entries: - raise errors.NotFound, notfound(args) - - if partial == 1: - counter = -1 - else: - counter = len(entries) - - return [counter] + entries - - def addEntry(self,*args): - """This wraps the add function. It assumes that the entry is already - populated with all of the desired objectclasses and attributes""" - - sctrl = self.__get_server_controls() - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.add_s(*args) - except ldap.ALREADY_EXISTS, e: - raise errors.DuplicateEntry, "Entry already exists" - except ldap.LDAPError, e: - raise DatabaseError, e - return True - - def updateRDN(self, dn, newrdn): - """Wrap the modrdn function.""" - - sctrl = self.__get_server_controls() - - if dn == newrdn: - # no need to report an error - return True - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.modrdn_s(dn, newrdn, delold=1) - except ldap.LDAPError, e: - raise DatabaseError, e - return True - - def updateEntry(self,dn,oldentry,newentry): - """This wraps the mod function. It assumes that the entry is already - populated with all of the desired objectclasses and attributes""" - - sctrl = self.__get_server_controls() - - modlist = self.generateModList(oldentry, newentry) - - if len(modlist) == 0: - raise errors.EmptyModlist - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.modify_s(dn, modlist) - # this is raised when a 'delete' attribute isn't found. - # it indicates the previous attribute was removed by another - # update, making the oldentry stale. - except ldap.NO_SUCH_ATTRIBUTE: - raise errors.MidairCollision - except ldap.LDAPError, e: - raise errors.DatabaseError, e - return True - - def generateModList(self, old_entry, new_entry): - """A mod list generator that computes more precise modification lists - than the python-ldap version. This version purposely generates no - REPLACE operations, to deal with multi-user updates more properly.""" - modlist = [] - - old_entry = ipautil.CIDict(old_entry) - new_entry = ipautil.CIDict(new_entry) - - keys = set(map(string.lower, old_entry.keys())) - keys.update(map(string.lower, new_entry.keys())) - - for key in keys: - new_values = new_entry.get(key, []) - if not(isinstance(new_values,list) or isinstance(new_values,tuple)): - new_values = [new_values] - new_values = filter(lambda value:value!=None, new_values) - new_values = set(new_values) - - old_values = old_entry.get(key, []) - if not(isinstance(old_values,list) or isinstance(old_values,tuple)): - old_values = [old_values] - old_values = filter(lambda value:value!=None, old_values) - old_values = set(old_values) - - adds = list(new_values.difference(old_values)) - removes = list(old_values.difference(new_values)) - - if len(removes) > 0: - modlist.append((ldap.MOD_DELETE, key, removes)) - if len(adds) > 0: - modlist.append((ldap.MOD_ADD, key, adds)) - - return modlist - - def inactivateEntry(self,dn,has_key): - """Rather than deleting entries we mark them as inactive. - has_key defines whether the entry already has nsAccountlock - set so we can determine which type of mod operation to run.""" - - sctrl = self.__get_server_controls() - modlist=[] - - if has_key: - operation = ldap.MOD_REPLACE - else: - operation = ldap.MOD_ADD - - modlist.append((operation, "nsAccountlock", "true")) - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.modify_s(dn, modlist) - except ldap.LDAPError, e: - raise DatabaseError, e - return True - - def deleteEntry(self,*args): - """This wraps the delete function. Use with caution.""" - - sctrl = self.__get_server_controls() - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.delete_s(*args) - except ldap.INSUFFICIENT_ACCESS, e: - raise errors.InsufficientAccess, e - except ldap.LDAPError, e: - raise errors.DatabaseError, e - return True - - def modifyPassword(self,dn,oldpass,newpass): - """Set the user password using RFC 3062, LDAP Password Modify Extended - Operation. This ends up calling the IPA password slapi plugin - handler so the Kerberos password gets set properly. - - oldpass is not mandatory - """ - - sctrl = self.__get_server_controls() - - try: - if sctrl is not None: - self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) - self.passwd_s(dn, oldpass, newpass) - except ldap.LDAPError, e: - raise e - return True - - def __wrapmethods(self): - """This wraps all methods of SimpleLDAPObject, so that we can intercept - the methods that deal with entries. Instead of using a raw list of tuples - of lists of hashes of arrays as the entry object, we want to wrap entries - in an Entry class that provides some useful methods""" - for name in dir(self.__class__.__bases__[0]): - attr = getattr(self, name) - if callable(attr): - setattr(self, name, wrapper(attr, name)) - - def normalizeDN(dn): - # not great, but will do until we use a newer version of python-ldap - # that has DN utilities - ary = ldap.explode_dn(dn.lower()) - return ",".join(ary) - normalizeDN = staticmethod(normalizeDN) - -def notfound(args): - """Return a string suitable for displaying as an error when a - search returns no results. - - This just returns whatever is after the equals sign""" - if len(args) > 2: - searchfilter = args[2] - try: - # Python re doesn't do paren counting so the string could - # have a trailing paren "foo)" - target = re.match(r'\(.*=(.*)\)', searchfilter).group(1) - target = target.replace(")","") - except: - target = searchfilter - return "%s not found" % str(target) - else: - return args[0] diff --git a/ipa_server/ipautil.py b/ipa_server/ipautil.py deleted file mode 100644 index 6422fe5a..00000000 --- a/ipa_server/ipautil.py +++ /dev/null @@ -1,201 +0,0 @@ -# Authors: Simo Sorce -# -# Copyright (C) 2007 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import string -import xmlrpclib -import re - -def realm_to_suffix(realm_name): - s = realm_name.split(".") - terms = ["dc=" + x.lower() for x in s] - return ",".join(terms) - -class CIDict(dict): - """ - Case-insensitive but case-respecting dictionary. - - This code is derived from python-ldap's cidict.py module, - written by stroeder: http://python-ldap.sourceforge.net/ - - This version extends 'dict' so it works properly with TurboGears. - If you extend UserDict, isinstance(foo, dict) returns false. - """ - - def __init__(self,default=None): - super(CIDict, self).__init__() - self._keys = {} - self.update(default or {}) - - def __getitem__(self,key): - return super(CIDict,self).__getitem__(string.lower(key)) - - def __setitem__(self,key,value): - lower_key = string.lower(key) - self._keys[lower_key] = key - return super(CIDict,self).__setitem__(string.lower(key),value) - - def __delitem__(self,key): - lower_key = string.lower(key) - del self._keys[lower_key] - return super(CIDict,self).__delitem__(string.lower(key)) - - def update(self,dict): - for key in dict.keys(): - self[key] = dict[key] - - def has_key(self,key): - return super(CIDict, self).has_key(string.lower(key)) - - def get(self,key,failobj=None): - try: - return self[key] - except KeyError: - return failobj - - def keys(self): - return self._keys.values() - - def items(self): - result = [] - for k in self._keys.values(): - result.append((k,self[k])) - return result - - def copy(self): - copy = {} - for k in self._keys.values(): - copy[k] = self[k] - return copy - - def iteritems(self): - return self.copy().iteritems() - - def iterkeys(self): - return self.copy().iterkeys() - - def setdefault(self,key,value=None): - try: - return self[key] - except KeyError: - self[key] = value - return value - - def pop(self, key, *args): - try: - value = self[key] - del self[key] - return value - except KeyError: - if len(args) == 1: - return args[0] - raise - - def popitem(self): - (lower_key,value) = super(CIDict,self).popitem() - key = self._keys[lower_key] - del self._keys[lower_key] - - return (key,value) - - -# -# The safe_string_re regexp and needs_base64 function are extracted from the -# python-ldap ldif module, which was -# written by Michael Stroeder -# http://python-ldap.sourceforge.net -# -# It was extracted because ipaldap.py is naughtily reaching into the ldif -# module and squashing this regexp. -# -SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)' -safe_string_re = re.compile(SAFE_STRING_PATTERN) - -def needs_base64(s): - """ - returns 1 if s has to be base-64 encoded because of special chars - """ - return not safe_string_re.search(s) is None - - -def wrap_binary_data(data): - """Converts all binary data strings into Binary objects for transport - back over xmlrpc.""" - if isinstance(data, str): - if needs_base64(data): - return xmlrpclib.Binary(data) - else: - return data - elif isinstance(data, list) or isinstance(data,tuple): - retval = [] - for value in data: - retval.append(wrap_binary_data(value)) - return retval - elif isinstance(data, dict): - retval = {} - for (k,v) in data.iteritems(): - retval[k] = wrap_binary_data(v) - return retval - else: - return data - - -def unwrap_binary_data(data): - """Converts all Binary objects back into strings.""" - if isinstance(data, xmlrpclib.Binary): - # The data is decoded by the xmlproxy, but is stored - # in a binary object for us. - return str(data) - elif isinstance(data, str): - return data - elif isinstance(data, list) or isinstance(data,tuple): - retval = [] - for value in data: - retval.append(unwrap_binary_data(value)) - return retval - elif isinstance(data, dict): - retval = {} - for (k,v) in data.iteritems(): - retval[k] = unwrap_binary_data(v) - return retval - else: - return data - -def get_gsserror(e): - """A GSSError exception looks differently in python 2.4 than it does - in python 2.5, deal with it.""" - - try: - primary = e[0] - secondary = e[1] - except: - primary = e[0][0] - 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/mod_python_xmlrpc.py b/ipa_server/mod_python_xmlrpc.py deleted file mode 100644 index 9a2960f9..00000000 --- a/ipa_server/mod_python_xmlrpc.py +++ /dev/null @@ -1,367 +0,0 @@ -# mod_python script - -# ipaxmlrpc - an XMLRPC interface for ipa. -# Copyright (c) 2007 Red Hat -# -# IPA is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; -# version 2.1 of the License. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this software; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -# -# Based on kojixmlrpc - an XMLRPC interface for koji by -# Mike McLean -# -# Authors: -# Rob Crittenden - -""" -Production XML-RPC server using mod_python. -""" - -import sys - - -import time -import traceback -import pprint -from xmlrpclib import Marshaller,loads,dumps,Fault -try: - from mod_python import apache -except ImportError: - pass -import logging - -import ldap -from ipalib import api -from ipalib import config -from ipaserver import conn -from ipaserver.servercore import context -from ipaserver.servercore import ipautil -from ipalib.util import xmlrpc_unmarshal - -import string - -api.load_plugins() - -# Global list of available functions -gfunctions = {} - -def register_function(function, name = None): - if name is None: - name = function.__name__ - gfunctions[name] = function - -class ModXMLRPCRequestHandler(object): - """Simple XML-RPC handler for mod_python environment""" - - def __init__(self): - global gfunctions - - self.funcs = gfunctions - self.traceback = False - #introspection functions - self.register_function(self.ping, name="ping") - self.register_function(self.list_api, name="_listapi") - self.register_function(self.system_listMethods, name="system.listMethods") - self.register_function(self.system_methodSignature, name="system.methodSignature") - self.register_function(self.system_methodHelp, name="system.methodHelp") - self.register_function(self.multiCall) - - def register_function(self, function, name = None): - if name is None: - name = function.__name__ - self.funcs[name] = function - - def register_module(self, instance, prefix=None): - """Register all the public functions in an instance with prefix prepended - - For example - h.register_module(exports,"pub.sys") - will register the methods of exports with names like - pub.sys.method1 - pub.sys.method2 - ...etc - """ - for name in dir(instance): - if name.startswith('_'): - continue - function = getattr(instance, name) - if not callable(function): - continue - if prefix is not None: - name = "%s.%s" %(prefix,name) - self.register_function(function, name=name) - - def register_instance(self,instance): - self.register_module(instance) - - def _marshaled_dispatch(self, data, req): - """Dispatches an XML-RPC method from marshalled (XML) data.""" - - params, method = loads(data) - pythonopts = req.get_options() - - # Populate the Apache environment variables - req.add_common_vars() - - context.opts['remoteuser'] = req.user - - if req.subprocess_env.get("KRB5CCNAME") is not None: - krbccache = req.subprocess_env.get("KRB5CCNAME") - else: - response = dumps(Fault(5, "Did not receive Kerberos credentials.")) - return response - - debuglevel = logging.INFO - if pythonopts.get("IPADebug"): - context.opts['ipadebug'] = pythonopts.get("IPADebug").lower() - - if context.opts['ipadebug'] == "on": - debuglevel = logging.DEBUG - - if not context.opts.get('ipadebug'): - context.opts['ipadebug'] = "off" - - logging.basicConfig(level=debuglevel, - format='[%(asctime)s] [%(levelname)s] %(message)s', - datefmt='%a %b %d %H:%M:%S %Y', - stream=sys.stderr) - - logging.info("Interpreter: %s" % req.interpreter) - - -# if opts['ipadebug'] == "on": -# for o in opts: -# logging.debug("IPA: setting option %s: %s" % (o, opts[o])) -# for e in req.subprocess_env: -# logging.debug("IPA: environment %s: %s" % (e, req.subprocess_env[e])) - - context.conn = conn.IPAConn(api.env.ldaphost, api.env.ldapport, krbccache, context.opts.get('ipadebug')) - - start = time.time() - # generate response - try: - response = self._dispatch(method, params) - # wrap response in a singleton tuple - response = (response,) - response = dumps(response, methodresponse=1, allow_none=1) - except Fault, e: - response = dumps(Fault(e.faultCode, e.faultString)) - except: - self.traceback = True - # report exception back to server - e_class, e = sys.exc_info()[:2] - faultCode = getattr(e_class,'faultCode',1) - tb_str = ''.join(traceback.format_exception(*sys.exc_info())) - faultString = tb_str - response = dumps(Fault(faultCode, faultString)) - - return response - - def _dispatch(self,method,params): - func = self.funcs.get(method,None) - if func is None: - raise Fault(1, "Invalid method: %s" % method) - - params = list(ipautil.unwrap_binary_data(params)) - (args, kw) = xmlrpc_unmarshal(*params) - - ret = func(*args, **kw) - - return ipautil.wrap_binary_data(ret) - - def multiCall(self, calls): - """Execute a multicall. Execute each method call in the calls list, collecting results and errors, and return those as a list.""" - results = [] - for call in calls: - try: - result = self._dispatch(call['methodName'], call['params']) - except Fault, fault: - results.append({'faultCode': fault.faultCode, 'faultString': fault.faultString}) - except: - # transform unknown exceptions into XML-RPC Faults - # don't create a reference to full traceback since this creates - # a circular reference. - exc_type, exc_value = sys.exc_info()[:2] - faultCode = getattr(exc_type, 'faultCode', 1) - faultString = ', '.join(exc_value.args) - trace = traceback.format_exception(*sys.exc_info()) - # traceback is not part of the multicall spec, but we include it for debugging purposes - results.append({'faultCode': faultCode, 'faultString': faultString, 'traceback': trace}) - else: - results.append([result]) - - return results - - def list_api(self): - funcs = [] - for name,func in self.funcs.items(): - #the keys in self.funcs determine the name of the method as seen over xmlrpc - #func.__name__ might differ (e.g. for dotted method names) - args = self._getFuncArgs(func) - doc = None - try: - doc = func.doc - except AttributeError: - doc = func.__doc__ - funcs.append({'name': name, - 'doc': doc, - 'args': args}) - return funcs - - def ping(self): - """Simple test to see if the XML-RPC is up and active.""" - return "pong" - - def _getFuncArgs(self, func): - try: - # Plugins have this - args = list(func.args) - args.append("kw") - except: - # non-plugin functions such as the introspective ones - args = [] - for x in range(0, func.func_code.co_argcount): - if x == 0 and func.func_code.co_varnames[x] == "self": - continue - # opts is a name we tack on internally. Don't publish it. - if func.func_code.co_varnames[x] == "opts": - continue - if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults): - args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)])) - else: - args.append(func.func_code.co_varnames[x]) - return args - - def system_listMethods(self): - """List all available XML-RPC methods""" - return self.funcs.keys() - - def system_methodSignature(self, method): - """signatures are not supported""" - #it is not possible to autogenerate this data - return 'signatures not supported' - - def system_methodHelp(self, method): - """Return help on a specific method""" - func = self.funcs.get(method) - if func is None: - return "" - arglist = [] - for arg in self._getFuncArgs(func): - if isinstance(arg,str): - arglist.append(arg) - else: - arglist.append('%s=%s' % (arg[0], arg[1])) - ret = '%s(%s)' % (method, ", ".join(arglist)) - doc = None - try: - doc = func.doc - except AttributeError: - doc = func.__doc__ - if doc: - ret += "\ndescription: %s" % func.__doc__ - return ret - - def handle_request(self,req): - """Handle a single XML-RPC request""" - - # XMLRPC uses POST only. Reject anything else - if req.method != 'POST': - req.allow_methods(['POST'],1) - raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED - - # The LDAP connection pool is not thread-safe. Avoid problems and - # force the forked model for now. - if apache.mpm_query(apache.AP_MPMQ_IS_THREADED): - response = dumps(Fault(3, "Apache must use the forked model")) - else: - response = self._marshaled_dispatch(req.read(), req) - - req.content_type = "text/xml" - req.set_content_length(len(response)) - req.write(response) - - -# -# mod_python handler -# - -def handler(req, profiling=False): - h = ModXMLRPCRequestHandler() - - if profiling: - import profile, pstats, StringIO, tempfile - global _profiling_req - _profiling_req = req - temp = tempfile.NamedTemporaryFile() - profile.run("import ipxmlrpc; ipaxmlrpc.handler(ipaxmlrpc._profiling_req, False)", temp.name) - stats = pstats.Stats(temp.name) - strstream = StringIO.StringIO() - sys.stdout = strstream - stats.sort_stats("time") - stats.print_stats() - req.write("
" + strstream.getvalue() + "
") - _profiling_req = None - else: - context.opts = req.get_options() - context.reqs = req - try: - h.handle_request(req) - finally: - # Clean up any per-request data and connections - for k in context.__dict__.keys(): - del context.__dict__[k] - - return apache.OK - -def setup_logger(level): - """Make a global logging object.""" - l = logging.getLogger() - l.setLevel(level) - h = logging.StreamHandler() - f = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s") - h.setFormatter(f) - l.addHandler(h) - - return - -def load_modules(): - """Load all plugins and register the XML-RPC functions we provide. - - Called by mod_python PythonImport - - PythonImport /path/to/ipaxmlrpc.py::load_modules main_interpreter - ... - PythonInterpreter main_interpreter - PythonHandler ipaxmlrpc - """ - - # setup up the logger with a DEBUG level. It may get reset to INFO - # once we start processing requests. We don't have access to the - # Apache configuration yet. - setup_logger(logging.DEBUG) - - api.finalize() - - # Initialize our environment - config.set_default_env(api.env) - env_dict = config.read_config() - env_dict['server_context'] = True - api.env.update(env_dict) - - # Get and register all the methods - for cmd in api.Command: - logging.debug("registering XML-RPC call %s" % cmd) - register_function(api.Command[cmd], cmd) - - return diff --git a/ipa_server/plugins/__init__.py b/ipa_server/plugins/__init__.py deleted file mode 100644 index 5737dcb7..00000000 --- a/ipa_server/plugins/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Authors: Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -Sub-package containing all server plugins. - -By convention, modules with frontend plugins are named f_*.py and modules -with backend plugins are named b_*.py. -""" diff --git a/ipa_server/plugins/b_ldap.py b/ipa_server/plugins/b_ldap.py deleted file mode 100644 index 071bf52e..00000000 --- a/ipa_server/plugins/b_ldap.py +++ /dev/null @@ -1,327 +0,0 @@ -# Authors: -# Rob Crittenden -# Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -Backend plugin for LDAP. - -This wraps the python-ldap bindings. -""" - -import ldap as _ldap -from ipalib import api, Context -from ipalib import errors -from ipalib.crud import CrudBackend -from ipaserver import servercore -from ipaserver import ipaldap - - -class conn(Context): - """ - Thread-local LDAP connection. - """ - - def get_value(self): - return 'it worked' - -api.register(conn) - - -class ldap(CrudBackend): - """ - LDAP backend plugin. - """ - - def __init__(self): - self.dn = _ldap.dn - - def make_user_dn(self, uid): - """ - Construct user dn from uid. - """ - return 'uid=%s,%s,%s' % ( - self.dn.escape_dn_chars(uid), - self.api.env.container_user, - self.api.env.basedn, - ) - - def make_group_dn(self, cn): - """ - Construct group dn from cn. - """ - return 'cn=%s,%s,%s' % ( - self.dn.escape_dn_chars(cn), - self.api.env.container_group, - self.api.env.basedn, - ) - - def make_hostgroup_dn(self, cn): - """ - Construct group of hosts dn from cn. - """ - return 'cn=%s,%s,%s' % ( - self.dn.escape_dn_chars(cn), - self.api.env.container_hostgroup, - self.api.env.basedn, - ) - - def make_service_dn(self, principal): - """ - Construct service principal dn from principal name - """ - return 'krbprincipalname=%s,%s,%s' % ( - self.dn.escape_dn_chars(principal), - self.api.env.container_service, - self.api.env.basedn, - ) - - def make_host_dn(self, hostname): - """ - Construct host dn from hostname - """ - return 'cn=%s,%s,%s' % ( - self.dn.escape_dn_chars(hostname), - self.api.env.container_host, - self.api.env.basedn, - ) - - def get_object_type(self, attribute): - """ - Based on attribute, make an educated guess as to the type of - object we're looking for. - """ - attribute = attribute.lower() - object_type = None - if attribute == "uid": # User - object_type = "posixAccount" - elif attribute == "cn": # Group - object_type = "posixGroup" - elif attribute == "krbprincipalname": # Service - object_type = "krbPrincipal" - - return object_type - - def find_entry_dn(self, key_attribute, primary_key, object_type=None, base=None): - """ - Find an existing entry's dn from an attribute - """ - key_attribute = key_attribute.lower() - if not object_type: - object_type = self.get_object_type(key_attribute) - if not object_type: - return None - - search_filter = "(&(objectclass=%s)(%s=%s))" % ( - object_type, - key_attribute, - self.dn.escape_dn_chars(primary_key) - ) - - if not base: - base = self.api.env.container_accounts - - search_base = "%s, %s" % (base, self.api.env.basedn) - - entry = servercore.get_sub_entry(search_base, search_filter, ['dn', 'objectclass']) - - return entry.get('dn') - - def get_base_entry(self, searchbase, searchfilter, attrs): - return servercore.get_base_entry(searchbase, searchfilter, attrs) - - def get_sub_entry(self, searchbase, searchfilter, attrs): - return servercore.get_sub_entry(searchbase, searchfilter, attrs) - - def get_one_entry(self, searchbase, searchfilter, attrs): - return servercore.get_one_entry(searchbase, searchfilter, attrs) - - def get_ipa_config(self): - """Return a dictionary of the IPA configuration""" - return servercore.get_ipa_config() - - def mark_entry_active(self, dn): - return servercore.mark_entry_active(dn) - - def mark_entry_inactive(self, dn): - return servercore.mark_entry_inactive(dn) - - def _generate_search_filters(self, **kw): - """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)...) - exact_pattern = "(|" - for field in kw.keys(): - exact_pattern += "(%s=%s)" % (field, kw[field]) - exact_pattern += ")" - - sub_pattern = "(|" - for field in kw.keys(): - sub_pattern += "(%s=*%s*)" % (field, kw[field]) - sub_pattern += ")" - - # construct the giant match for all words - exact_match_filter = "(&" + exact_pattern + ")" - partial_match_filter = "(|" + sub_pattern + ")" - - return (exact_match_filter, partial_match_filter) - - def modify_password(self, dn, **kw): - return servercore.modify_password(dn, kw.get('oldpass'), kw.get('newpass')) - - def add_member_to_group(self, memberdn, groupdn): - """ - Add a new member to a group. - - :param memberdn: the DN of the member to add - :param groupdn: the DN of the group to add a member to - """ - return servercore.add_member_to_group(memberdn, groupdn) - - def remove_member_from_group(self, memberdn, groupdn): - """ - Remove a new member from a group. - - :param memberdn: the DN of the member to remove - :param groupdn: the DN of the group to remove a member from - """ - return servercore.remove_member_from_group(memberdn, groupdn) - - # The CRUD operations - - def strip_none(self, kw): - """ - Remove any None values present in the LDAP attribute dict. - """ - for (key, value) in kw.iteritems(): - if value is None: - continue - if type(value) in (list, tuple): - value = filter( - lambda v: type(v) in (str, unicode, bool, int, float), - value - ) - if len(value) > 0: - yield (key, value) - else: - assert type(value) in (str, unicode, bool, int, float) - yield (key, value) - - def create(self, **kw): - if servercore.entry_exists(kw['dn']): - raise errors.DuplicateEntry("entry already exists") - kw = dict(self.strip_none(kw)) - - - entry = ipaldap.Entry(kw['dn']) - - # dn isn't allowed to be in the entry itself - del kw['dn'] - - # Fill in our new entry - for k in kw: - entry.setValues(k, kw[k]) - - servercore.add_entry(entry) - return self.retrieve(entry.dn) - - def retrieve(self, dn, attributes=None): - return servercore.get_entry_by_dn(dn, attributes) - - def update(self, dn, **kw): - result = self.retrieve(dn, ["*"]) - - entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result))) - kw = dict(self.strip_none(kw)) - for k in kw: - entry.setValues(k, kw[k]) - - servercore.update_entry(entry.toDict()) - - return self.retrieve(dn) - - def delete(self, dn): - return servercore.delete_entry(dn) - - def search(self, **kw): - objectclass = kw.get('objectclass') - sfilter = kw.get('filter') - attributes = kw.get('attributes') - base = kw.get('base') - if attributes: - del kw['attributes'] - else: - attributes = ['*'] - if objectclass: - del kw['objectclass'] - if base: - del kw['base'] - if sfilter: - del kw['filter'] - (exact_match_filter, partial_match_filter) = self._generate_search_filters(**kw) - if objectclass: - exact_match_filter = "(&(objectClass=%s)%s)" % (objectclass, exact_match_filter) - partial_match_filter = "(&(objectClass=%s)%s)" % (objectclass, partial_match_filter) - if sfilter: - exact_match_filter = "(%s%s)" % (sfilter, exact_match_filter) - partial_match_filter = "(%s%s)" % (sfilter, partial_match_filter) - - if not base: - base = self.api.env.container_accounts - - search_base = "%s, %s" % (base, self.api.env.basedn) - try: - exact_results = servercore.search(search_base, - exact_match_filter, attributes) - except errors.NotFound: - exact_results = [0] - - try: - partial_results = servercore.search(search_base, - partial_match_filter, attributes) - except errors.NotFound: - 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.get('dn'), exact_results)) - partial_results = filter(lambda e: e.get('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) - - results = [counter] - for r in exact_results + partial_results: - results.append(r) - - return results - -api.register(ldap) diff --git a/ipa_server/plugins/b_ra.py b/ipa_server/plugins/b_ra.py deleted file mode 100644 index bf119b81..00000000 --- a/ipa_server/plugins/b_ra.py +++ /dev/null @@ -1,406 +0,0 @@ -# Authors: -# Andrew Wnuk -# Jason Gerard DeRose -# -# Copyright (C) 2009 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -Backend plugin for IPA-RA. - -IPA-RA provides an access to CA to issue, retrieve, and revoke certificates. -IPA-RA plugin provides CA interface via the following methods: - check_request_status to check certificate request status - get_certificate to retrieve an existing certificate - request_certificate to request certificate - revoke_certificate to revoke certificate - take_certificate_off_hold to take certificate off hold -""" - -import os, stat, subprocess -import array -import errno -import binascii -import httplib, urllib -from socket import gethostname - -from ipalib import api, Backend -from ipalib import errors -from ipaserver import servercore -from ipaserver import ipaldap - - -class ra(Backend): - - - def __init__(self): - self.sec_dir = api.env.dot_ipa + os.sep + 'alias' - self.pwd_file = self.sec_dir + os.sep + '.pwd' - self.noise_file = self.sec_dir + os.sep + '.noise' - - self.ca_host = None - self.ca_port = None - self.ca_ssl_port = None - - self.__get_ca_location() - - self.ipa_key_size = "2048" - self.ipa_certificate_nickname = "ipaCert" - self.ca_certificate_nickname = "caCert" - - if not os.path.isdir(self.sec_dir): - os.mkdir(self.sec_dir) - self.__create_pwd_file() - self.__create_nss_db() - self.__import_ca_chain() - self.__request_ipa_certificate(self.__generate_ipa_request()) - - - def check_request_status(self, request_id=None): - """ - Check certificate request status - :param request_id: request ID - """ - self.log.debug("IPA-RA: check_request_status") - return_values = {} - if request_id is not None: - params = urllib.urlencode({'requestId': request_id, 'xmlOutput': 'true'}) - headers = {"Content-type": "application/x-www-form-urlencoded"} - conn = httplib.HTTPConnection(self.ca_host, self.ca_port) - conn.request("POST", "/ca/ee/ca/checkRequest", params, headers) - response = conn.getresponse() - api.log.debug("IPA-RA: response.status: %d response.reason: %s" % (response.status, response.reason)) - data = response.read() - conn.close() - self.log.debug(data) - if data is not None: - request_status = self.__find_substring(data, 'header.status = "', '"') - if request_status is not None: - return_values["status"] = "0" - return_values["request_status"] = request_status - self.log.debug("IPA-RA: request_status: '%s'" % request_status) - serial_number = self.__find_substring(data, 'record.serialNumber="', '"') - if serial_number is not None: - return_values["serial_number"] = "0x"+serial_number - request_id = self.__find_substring(data, 'header.requestId = "', '"') - if request_id is not None: - return_values["request_id"] = request_id - error = self.__find_substring(data, 'fixed.unexpectedError = "', '"') - if error is not None: - return_values["error"] = error - if return_values.has_key("status") is False: - return_values["status"] = "2" - else: - return_values["status"] = "1" - return return_values - - - def get_certificate(self, serial_number=None): - """ - Retrieve an existing certificate - :param serial_number: certificate serial number - """ - self.log.debug("IPA-RA: get_certificate") - issued_certificate = None - return_values = {} - if serial_number is not None: - request_info = ("serialNumber=%s" % serial_number) - self.log.debug("request_info: '%s'" % request_info) - returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/displayBySerial", self.ca_host+":"+str(self.ca_ssl_port)]) - self.log.debug("IPA-RA: returncode: %d" % returncode) - if (returncode == 0): - issued_certificate = self.__find_substring(stdout, 'header.certChainBase64 = "', '"') - if issued_certificate is not None: - return_values["status"] = "0" - issued_certificate = issued_certificate.replace("\\r", "") - issued_certificate = issued_certificate.replace("\\n", "") - self.log.debug("IPA-RA: issued_certificate: '%s'" % issued_certificate) - return_values["certificate"] = issued_certificate - else: - return_values["status"] = "1" - revocation_reason = self.__find_substring(stdout, 'header.revocationReason = ', ';') - if revocation_reason is not None: - return_values["revocation_reason"] = revocation_reason - else: - return_values["status"] = str(-returncode) - else: - return_values["status"] = "1" - return return_values - - - def request_certificate(self, certificate_request=None, request_type="pkcs10"): - """ - Submit certificate request - :param certificate_request: certificate request - :param request_type: request type - """ - self.log.debug("IPA-RA: request_certificate") - certificate = None - return_values = {} - if request_type is None: - request_type="pkcs10" - if certificate_request is not None: - request = urllib.quote(certificate_request) - request_info = "profileId=caRAserverCert&cert_request_type="+request_type+"&cert_request="+request+"&xmlOutput=true" - returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/ee/ca/profileSubmit", self.ca_host+":"+str(self.ca_ssl_port)]) - self.log.debug("IPA-RA: returncode: %d" % returncode) - if (returncode == 0): - status = self.__find_substring(stdout, "", "") - if status is not None: - self.log.debug ("status=%s" % status) - return_values["status"] = status - request_id = self.__find_substring(stdout, "", "") - if request_id is not None: - self.log.debug ("request_id=%s" % request_id) - return_values["request_id"] = request_id - serial_number = self.__find_substring(stdout, "", "") - if serial_number is not None: - self.log.debug ("serial_number=%s" % serial_number) - return_values["serial_number"] = ("0x%s" % serial_number) - subject = self.__find_substring(stdout, "", "") - if subject is not None: - self.log.debug ("subject=%s" % subject) - return_values["subject"] = subject - certificate = self.__find_substring(stdout, "", "") - if certificate is not None: - self.log.debug ("certificate=%s" % certificate) - return_values["certificate"] = certificate - if return_values.has_key("status") is False: - return_values["status"] = "2" - else: - return_values["status"] = str(-returncode) - else: - return_values["status"] = "1" - return return_values - - - def revoke_certificate(self, serial_number=None, revocation_reason=0): - """ - Revoke a certificate - :param serial_number: certificate serial number - :param revocation_reason: revocation reason - revocationr reasons: 0 - unspecified - 1 - key compromise - 2 - ca compromise - 3 - affiliation changed - 4 - superseded - 5 - cessation of operation - 6 - certificate hold - 7 - value 7 is not used - 8 - remove from CRL - 9 - privilege withdrawn - 10 - aa compromise - see RFC 5280 for more details - """ - return_values = {} - self.log.debug("IPA-RA: revoke_certificate") - if revocation_reason is None: - revocation_reason = 0 - if serial_number is not None: - if isinstance(serial_number, int): - serial_number = str(serial_number) - if isinstance(revocation_reason, int): - revocation_reason = str(revocation_reason) - request_info = "op=revoke&revocationReason="+revocation_reason+"&revokeAll=(certRecordId%3D"+serial_number+")&totalRecordCount=1" - returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/doRevoke", self.ca_host+":"+str(self.ca_ssl_port)]) - api.log.debug("IPA-RA: returncode: %d" % returncode) - if (returncode == 0): - return_values["status"] = "0" - if (stdout.find('revoked = "yes"') > -1): - return_values["revoked"] = True - else: - return_values["revoked"] = False - else: - return_values["status"] = str(-returncode) - else: - return_values["status"] = "1" - return return_values - - - def take_certificate_off_hold(self, serial_number=None): - """ - Take revoked certificate off hold - :param serial_number: certificate serial number - """ - return_values = {} - self.log.debug("IPA-RA: revoke_certificate") - if serial_number is not None: - if isinstance(serial_number, int): - serial_number = str(serial_number) - request_info = "serialNumber="+serial_number - returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/doUnrevoke", self.ca_host+":"+str(self.ca_ssl_port)]) - api.log.debug("IPA-RA: returncode: %d" % returncode) - if (returncode == 0): - if (stdout.find('unrevoked = "yes"') > -1): - return_values["taken_off_hold"] = True - else: - return_values["taken_off_hold"] = False - else: - return_values["status"] = str(-returncode) - else: - return_values["status"] = "1" - return return_values - - - def __find_substring(self, str, str1, str2): - sub_str = None - k0 = len(str) - k1 = str.find(str1) - k2 = len(str1) - if (k0 > 0 and k1 > -1 and k2 > 0 and k0 > k1 + k2): - sub_str = str[k1+k2:] - k3 = len(sub_str) - k4 = sub_str.find(str2) - if (k3 > 0 and k4 > -1 and k3 > k4): - sub_str = sub_str[:k4] - return sub_str - - - def __get_ca_location(self): - if 'ca_host' in api.env: - api.log.debug("ca_host configuration found") - if api.env.ca_host is not None: - self.ca_host = api.env.ca_host - else: - api.log.debug("ca_host configuration not found") - # if CA is not hosted with IPA on the same system and there is no configuration support for 'api.env.ca_host', then set ca_host below - # self.ca_host = "example.com" - if self.ca_host is None: - self.ca_host = gethostname() - api.log.debug("ca_host: %s" % self.ca_host) - - if 'ca_ssl_port' in api.env: - api.log.debug("ca_ssl_port configuration found") - if api.env.ca_ssl_port is not None: - self.ca_ssl_port = api.env.ca_ssl_port - else: - api.log.debug("ca_ssl_port configuration not found") - if self.ca_ssl_port is None: - self.ca_ssl_port = 9443 - api.log.debug("ca_ssl_port: %d" % self.ca_ssl_port) - - if 'ca_port' in api.env: - api.log.debug("ca_port configuration found") - if api.env.ca_port is not None: - self.ca_port = api.env.ca_port - else: - api.log.debug("ca_port configuration not found") - if self.ca_port is None: - self.ca_port = 9080 - api.log.debug("ca_port: %d" % self.ca_port) - - - def __generate_ipa_request(self): - certificate_request = None - if not os.path.isfile(self.noise_file): - self.__create_noise_file() - returncode, stdout, stderr = self.__run_certutil(["-R", "-k", "rsa", "-g", self.ipa_key_size, "-s", "CN=IPA-Subsystem-Certificate,OU=pki-ipa,O=UsersysRedhat-Domain", "-z", self.noise_file, "-a"]) - if os.path.isfile(self.noise_file): - os.unlink(self.noise_file) - if (returncode == 0): - api.log.info("IPA-RA: IPA certificate request generated") - certificate_request = self.__find_substring(stdout, "-----BEGIN NEW CERTIFICATE REQUEST-----", "-----END NEW CERTIFICATE REQUEST-----") - if certificate_request is not None: - api.log.debug("certificate_request=%s" % certificate_request) - else: - api.log.warn("IPA-RA: Error parsing certificate request." % returncode) - else: - api.log.warn("IPA-RA: Error (%d) generating IPA certificate request." % returncode) - return certificate_request - - def __request_ipa_certificate(self, certificate_request=None): - ipa_certificate = None - if certificate_request is not None: - params = urllib.urlencode({'profileId': 'caServerCert', 'cert_request_type': 'pkcs10', 'requestor_name': 'freeIPA', 'cert_request': self.__generate_ipa_request(), 'xmlOutput': 'true'}) - headers = {"Content-type": "application/x-www-form-urlencoded"} - conn = httplib.HTTPConnection(self.ca_host+":"+elf.ca_port) - conn.request("POST", "/ca/ee/ca/profileSubmit", params, headers) - response = conn.getresponse() - api.log.debug("IPA-RA: response.status: %d response.reason: '%s'" % (response.status, response.reason)) - data = response.read() - conn.close() - api.log.info("IPA-RA: IPA certificate request submitted to CA: %s" % data) - return ipa_certificate - - def __get_ca_chain(self): - headers = {"Content-type": "application/x-www-form-urlencoded"} - conn = httplib.HTTPConnection(self.ca_host+":"+elf.ca_port) - conn.request("POST", "/ca/ee/ca/getCertChain", None, headers) - response = conn.getresponse() - api.log.debug("IPA-RA: response.status: %d response.reason: '%s'" % (response.status, response.reason)) - data = response.read() - conn.close() - certificate_chain = self.__find_substring(data, "", "") - if certificate_chain is not None: - api.log.info(("IPA-RA: CA chain obtained from CA: %s" % certificate_chain)) - else: - api.log.warn("IPA-RA: Error parsing certificate chain.") - return certificate_chain - - def __import_ca_chain(self): - returncode, stdout, stderr = self.__run_certutil(["-A", "-t", "CT,C,C", "-n", self.ca_certificate_nickname, "-a"], self.__get_ca_chain()) - if (returncode == 0): - api.log.info("IPA-RA: CA chain imported to IPA's NSS DB") - else: - api.log.warn("IPA-RA: Error (%d) importing CA chain to IPA's NSS DB." % returncode) - - def __create_noise_file(self): - noise = array.array('B', os.urandom(128)) - f = open(self.noise_file, "wb") - noise.tofile(f) - f.close() - - def __create_pwd_file(self): - hex_str = binascii.hexlify(os.urandom(10)) - print "urandom: %s" % hex_str - f = os.open(self.pwd_file, os.O_CREAT | os.O_RDWR) - os.write(f, hex_str) - os.close(f) - - def __create_nss_db(self): - returncode, stdout, stderr = self.__run_certutil(["-N"]) - if (returncode == 0): - api.log.info("IPA-RA: NSS DB created") - else: - api.log.warn("IPA-RA: Error (%d) creating NSS DB." % returncode) - - """ - sslget and certutil utilities are used only till Python-NSS completion. - """ - def __run_sslget(self, args, stdin=None): - new_args = ["/usr/bin/sslget", "-d", self.sec_dir, "-w", self.pwd_file, "-n", self.ipa_certificate_nickname] - new_args = new_args + args - return self.__run(new_args, stdin) - - def __run_certutil(self, args, stdin=None): - new_args = ["/usr/bin/certutil", "-d", self.sec_dir, "-f", self.pwd_file] - new_args = new_args + args - return self.__run(new_args, stdin) - - def __run(self, args, stdin=None): - if stdin: - p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) - stdout,stderr = p.communicate(stdin) - else: - p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) - stdout,stderr = p.communicate() - - api.log.debug("IPA-RA: returncode: %d args: '%s'" % (p.returncode, ' '.join(args))) - # api.log.debug("IPA-RA: stdout: '%s'" % stdout) - # api.log.debug("IPA-RA: stderr: '%s'" % stderr) - return (p.returncode, stdout, stderr) - -api.register(ra) diff --git a/ipa_server/rpc.py b/ipa_server/rpc.py deleted file mode 100644 index 34de915c..00000000 --- a/ipa_server/rpc.py +++ /dev/null @@ -1,60 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -Execute an RPC request. -""" - -from xmlrpclib import dumps, loads, Fault -from ipalib import Backend -from ipalib.errors import HandledError, CommandError -from ipalib.rpc import xmlrpc_wrap, xmlrpc_unwrap - - -def params_2_args_options(params): - assert type(params) is tuple - if len(params) == 0: - return (tuple(), dict()) - if type(params[-1]) is dict: - return (params[:-1], params[-1]) - return (params, dict()) - - -class xmlrpc(Backend): - - def dispatch(self, method, params): - assert type(method) is str - assert type(params) is tuple - self.info('Received RPC call to %r', method) - if method not in self.Command: - raise CommandError(name=method) - (args, options) = params_2_args_options(xmlrpc_unwrap(params)) - result = self.Command[method](*args, **options) - return (xmlrpc_wrap(result),) - - def execute(self, data, ccache=None, client_ip=None, locale=None): - try: - (params, method) = loads(data) - response = self.dispatch(method, params) - except Exception, e: - if not isinstance(e, HandledError): - e = UnknownError() - assert isinstance(e, HandledError) - response = Fault(e.code, e.message) - return dumps(response) diff --git a/ipa_server/servercore.py b/ipa_server/servercore.py deleted file mode 100644 index 6991989e..00000000 --- a/ipa_server/servercore.py +++ /dev/null @@ -1,464 +0,0 @@ -# Authors: Rob Crittenden -# -# Copyright (C) 2007 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# - -import ldap -import string -import re -from ipaserver.context import context -from ipaserver import ipaldap -import ipautil -from ipalib import errors -from ipalib import api - -def convert_entry(ent): - entry = dict(ent.data) - entry['dn'] = ent.dn - # For now convert single entry lists to a string for the ui. - # TODO: we need to deal with multi-values better - for key,value in entry.iteritems(): - if isinstance(value,list) or isinstance(value,tuple): - if len(value) == 0: - entry[key] = '' - elif len(value) == 1: - entry[key] = value[0] - return entry - -def convert_scalar_values(orig_dict): - """LDAP update dicts expect all values to be a list (except for dn). - This method converts single entries to a list.""" - new_dict={} - for (k,v) in orig_dict.iteritems(): - if not isinstance(v, list) and k != 'dn': - v = [v] - new_dict[k] = v - - 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 -# restricting scope. For now adding a get_base/sub_entry() -# calls, but the API isn't great. -def get_entry (base, scope, searchfilter, sattrs=None): - """Get a specific entry (with a parametized scope). - Return as a dict of values. - Multi-valued fields are represented as lists. - """ - ent="" - - ent = context.conn.getConn().getEntry(base, scope, searchfilter, sattrs) - - return convert_entry(ent) - -def get_base_entry (base, searchfilter, sattrs=None): - """Get a specific entry (with a scope of BASE). - Return as a dict of values. - Multi-valued fields are represented as lists. - """ - return get_entry(base, ldap.SCOPE_BASE, searchfilter, sattrs) - -def get_sub_entry (base, searchfilter, sattrs=None): - """Get a specific entry (with a scope of SUB). - Return as a dict of values. - Multi-valued fields are represented as lists. - """ - return get_entry(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs) - -def get_one_entry (base, searchfilter, sattrs=None): - """Get the children of an entry (with a scope of ONE). - Return as a list of dict of values. - Multi-valued fields are represented as lists. - """ - return get_list(base, searchfilter, sattrs, ldap.SCOPE_ONELEVEL) - -def get_list (base, searchfilter, sattrs=None, scope=ldap.SCOPE_SUBTREE): - """Gets a list of entries. Each is converted to a dict of values. - Multi-valued fields are represented as lists. - """ - entries = [] - - entries = context.conn.getConn().getList(base, scope, searchfilter, sattrs) - - return map(convert_entry, entries) - -def has_nsaccountlock(dn): - """Check to see if an entry has the nsaccountlock attribute. - This attribute is provided by the Class of Service plugin so - doing a search isn't enough. It is provided by the two - entries cn=inactivated and cn=activated. So if the entry has - the attribute and isn't in either cn=activated or cn=inactivated - then the attribute must be in the entry itself. - - Returns True or False - """ - # First get the entry. If it doesn't have nsaccountlock at all we - # can exit early. - entry = get_entry_by_dn(dn, ['dn', 'nsaccountlock', 'memberof']) - if not entry.get('nsaccountlock'): - return False - - # Now look to see if they are in activated or inactivated - # entry is a member - memberof = entry.get('memberof') - if isinstance(memberof, basestring): - memberof = [memberof] - for m in memberof: - inactivated = m.find("cn=inactivated") - activated = m.find("cn=activated") - # if they are in either group that means that the nsaccountlock - # value comes from there, otherwise it must be in this entry. - if inactivated >= 0 or activated >= 0: - return False - - return True - -# 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=*)" - api.log.info("IPA: get_entry_by_dn '%s'" % dn) - return get_base_entry(dn, searchfilter, sattrs) - -def get_entry_by_cn (cn, sattrs): - """Get a specific entry by cn. Return as a dict of values. - Multi-valued fields are represented as lists. - """ - api.log.info("IPA: get_entry_by_cn '%s'" % cn) -# cn = self.__safe_filter(cn) - searchfilter = "(cn=%s)" % cn - return get_sub_entry("cn=accounts," + api.env.basedn, searchfilter, sattrs) - -def get_user_by_uid(uid, sattrs): - """Get a specific user's entry.""" - # FIXME: should accept a container to look in -# uid = self.__safe_filter(uid) - searchfilter = "(&(uid=%s)(objectclass=posixAccount))" % uid - - return get_sub_entry("cn=accounts," + api.env.basedn, searchfilter, sattrs) - -# User support - -def entry_exists(dn): - """Return True if the entry exists, False otherwise.""" - try: - get_base_entry(dn, "objectclass=*", ['dn','objectclass']) - return True - except errors.NotFound: - return False - -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) - api.log.info("IPA: get_user_by_uid '%s'" % uid) -# uid = self.__safe_filter(uid) - searchfilter = "(uid=" + uid + ")" - return get_sub_entry("cn=accounts," + api.env.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 - api.log.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: - api.log.debug("There was a problem " + str(e)) - pass - - return False - -def update_entry (entry): - """Update an LDAP entry - - entry is a dict - - This refreshes the record from LDAP in order to obtain the list of - attributes that has changed. - """ - 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 - # original - try: - moddn = oldentry['dn'] - except KeyError, e: - # FIXME: return a missing DN error message - raise e - - return context.conn.getConn().updateEntry(moddn, oldentry, newentry) - -def add_entry(entry): - """Add a new entry""" - return context.conn.getConn().addEntry(entry) - -def delete_entry(dn): - """Remove an entry""" - return context.conn.getConn().deleteEntry(dn) - -# FIXME, get time and search limit from cn=ipaconfig -def search(base, filter, attributes, timelimit=1, sizelimit=3000): - """Perform an LDAP query""" - try: - timelimit = float(timelimit) - results = context.conn.getConn().getListAsync(base, ldap.SCOPE_SUBTREE, - filter, attributes, 0, None, None, timelimit, sizelimit) - except ldap.NO_SUCH_OBJECT: - raise errors.NotFound - - counter = results[0] - entries = [counter] - for r in results[1:]: - entries.append(convert_entry(r)) - - return entries - -def uniq_list(x): - """Return a unique list, preserving order and ignoring case""" - 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.""" - - schema_entry = get_base_entry("", "objectclass=*", ['dn','subschemasubentry']) - schema_cn = schema_entry.get('subschemasubentry') - schema = get_base_entry(schema_cn, "objectclass=*", ['*']) - - return schema - -def get_objectclasses(): - """Returns a list of available objectclasses that the LDAP - server supports. This parses out the syntax, attributes, etc - and JUST returns a lower-case list of the names.""" - - schema = get_schema() - - objectclasses = schema.get('objectclasses') - - # Convert this list into something more readable - result = [] - for i in range(len(objectclasses)): - oc = objectclasses[i].lower().split(" ") - 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," + api.env.basedn, searchfilter) - except ldap.NO_SUCH_OBJECT, e: - # FIXME - raise errors.NotFound - - return config - -def modify_password(dn, oldpass, newpass): - return context.conn.getConn().modifyPassword(dn, oldpass, newpass) - -def mark_entry_active (dn): - """Mark an entry as active in LDAP.""" - - # This can be tricky. The entry itself can be marked inactive - # by being in the inactivated group. It can also be inactivated by - # being the member of an inactive group. - # - # First we try to remove the entry from the inactivated group. Then - # if it is still inactive we have to add it to the activated group - # which will override the group membership. - - res = "" - # First, check the entry status - entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock']) - - if entry.get('nsaccountlock', 'false').lower() == "false": - api.log.debug("IPA: already active") - raise errors.AlreadyActiveError - - if has_nsaccountlock(dn): - api.log.debug("IPA: appears to have the nsaccountlock attribute") - raise errors.HasNSAccountLock - - group = get_entry_by_cn("inactivated", None) - try: - remove_member_from_group(entry.get('dn'), group.get('dn')) - except errors.NotGroupMember: - # Perhaps the user is there as a result of group membership - pass - - # Now they aren't a member of inactivated directly, what is the status - # now? - entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock']) - - if entry.get('nsaccountlock', 'false').lower() == "false": - # great, we're done - api.log.debug("IPA: removing from inactivated did it.") - return True - - # So still inactive, add them to activated - group = get_entry_by_cn("activated", None) - res = add_member_to_group(dn, group.get('dn')) - api.log.debug("IPA: added to activated.") - - return res - -def mark_entry_inactive (dn): - """Mark an entry as inactive in LDAP.""" - - entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock', 'memberOf']) - - if entry.get('nsaccountlock', 'false').lower() == "true": - api.log.debug("IPA: already marked as inactive") - raise errors.AlreadyInactiveError - - if has_nsaccountlock(dn): - api.log.debug("IPA: appears to have the nsaccountlock attribute") - raise errors.HasNSAccountLock - - # First see if they are in the activated group as this will override - # the our inactivation. - group = get_entry_by_cn("activated", None) - try: - remove_member_from_group(dn, group.get('dn')) - except errors.NotGroupMember: - # this is fine, they may not be explicitly in this group - pass - - # Now add them to inactivated - group = get_entry_by_cn("inactivated", None) - res = add_member_to_group(dn, group.get('dn')) - - return res - -def add_member_to_group(member_dn, group_dn): - """ - Add a member to an existing group. - """ - api.log.info("IPA: add_member_to_group '%s' to '%s'" % (member_dn, group_dn)) - if member_dn.lower() == group_dn.lower(): - # You can't add a group to itself - raise errors.SameGroupError - - group = get_entry_by_dn(group_dn, None) - if group is None: - raise errors.NotFound - - # check to make sure member_dn exists - member_entry = get_base_entry(member_dn, "(objectClass=*)", ['dn','objectclass']) - if not member_entry: - raise errors.NotFound - - # Add the new member to the group member attribute - members = group.get('member', []) - if isinstance(members, basestring): - members = [members] - members.append(member_dn) - group['member'] = members - - try: - return update_entry(group) - except errors.EmptyModlist: - raise - -def remove_member_from_group(member_dn, group_dn=None): - """Remove a member_dn from an existing group.""" - - group = get_entry_by_dn(group_dn, None) - if group is None: - raise errors.NotFound - """ - if group.get('cn') == "admins": - member = get_entry_by_dn(member_dn, ['dn','uid']) - if member.get('uid') == "admin": - raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS) - """ - api.log.info("IPA: remove_member_from_group '%s' from '%s'" % (member_dn, group_dn)) - - members = group.get('member', False) - if not members: - raise errors.NotGroupMember - - if isinstance(members,basestring): - members = [members] - for i in range(len(members)): - members[i] = ipaldap.IPAdmin.normalizeDN(members[i]) - try: - members.remove(member_dn) - except ValueError: - # member is not in the group - # FIXME: raise more specific error? - raise errors.NotGroupMember - except Exception, e: - raise e - - group['member'] = members - - try: - return update_entry(group) - except errors.EmptyModlist: - raise diff --git a/ipa_server/test_client b/ipa_server/test_client deleted file mode 100755 index 3b4794d9..00000000 --- a/ipa_server/test_client +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/python - -import xmlrpclib - -def user_find(uid): - try: - args=uid - result = server.user_find(args) - print "returned %s" % result - except xmlrpclib.Fault, e: - print e.faultString - -# main -server = xmlrpclib.ServerProxy("http://localhost:8888/") - -#print server.system.listMethods() -#print server.system.methodHelp("user_add") - -try: - args="jsmith1" - kw = {'givenname':'Joe', 'sn':'Smith'} - result = server.user_add(kw, args) - print "returned %s" % result -except xmlrpclib.Fault, e: - print e.faultString - -#user_find("admin") -#user_find("notfound") diff --git a/ipa_server/updates/automount.update b/ipa_server/updates/automount.update deleted file mode 100644 index 13d9a6df..00000000 --- a/ipa_server/updates/automount.update +++ /dev/null @@ -1,54 +0,0 @@ -# -# An automount schema based on RFC 2307-bis. -# -# This schema defines new automount and automountMap objectclasses to represent -# the automount maps and their entries. -# -dn: cn=schema -add:attributeTypes: - ( 1.3.6.1.1.1.1.31 NAME 'automountMapName' - DESC 'automount Map Name' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE - X-ORIGIN 'RFC 2307bis' ) -add:attributeTypes: - ( 1.3.6.1.1.1.1.32 NAME 'automountKey' - DESC 'Automount Key value' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE - X-ORIGIN 'RFC 2307bis' ) -add:attributeTypes: - ( 1.3.6.1.1.1.1.33 NAME 'automountInformation' - DESC 'Automount information' - EQUALITY caseExactIA5Match - SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE - X-ORIGIN 'RFC 2307bis' ) -add:objectClasses: - ( 1.3.6.1.1.1.2.16 NAME 'automountMap' - DESC 'Automount Map information' SUP top - STRUCTURAL MUST automountMapName MAY description - X-ORIGIN 'RFC 2307bis' ) -add:objectClasses: - ( 1.3.6.1.1.1.2.17 NAME 'automount' - DESC 'Automount information' SUP top STRUCTURAL - MUST ( automountKey $ automountInformation ) MAY description - X-ORIGIN 'RFC 2307bis' ) - -# Add the default automount entries - -dn: cn=automount,$SUFFIX -add:objectClass: nsContainer -add:cn: automount - -dn: automountmapname=auto.master,cn=automount,$SUFFIX -add:objectClass: automountMap -add:automountMapName: auto.master - -dn: automountkey=/-,automountmapname=auto.master,cn=automount,$SUFFIX -add:objectClass: automount -add:automountKey: '/-' -add:automountInformation: auto.direct - -dn: automountmapname=auto.direct,cn=automount,$SUFFIX -add:objectClass: automountMap -add:automountMapName: auto.direct diff --git a/ipa_server/updates/groupofhosts.update b/ipa_server/updates/groupofhosts.update deleted file mode 100644 index fb39c5e2..00000000 --- a/ipa_server/updates/groupofhosts.update +++ /dev/null @@ -1,5 +0,0 @@ -dn: cn=hostgroups,cn=accounts,$SUFFIX -add:objectClass: top -add:objectClass: nsContainer -add:cn: hostgroups - diff --git a/ipa_server/updates/host.update b/ipa_server/updates/host.update deleted file mode 100644 index dfc9723c..00000000 --- a/ipa_server/updates/host.update +++ /dev/null @@ -1,22 +0,0 @@ -# -# Schema for IPA Hosts -# -dn: cn=schema -add: attributeTypes: - ( 2.16.840.1.113730.3.8.3.10 NAME 'ipaClientVersion' - DESC 'Text string describing client version of the IPA software installed' - SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 - X-ORIGIN 'IPA v2' ) - -add: attributeTypes: - ( 2.16.840.1.113730.3.8.3.11 NAME 'enrolledBy' - DESC 'DN of administrator who performed manual enrollment of the host' - SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 - X-ORIGIN 'IPA v2' ) -add: objectClasses: - ( 2.16.840.1.113730.3.8.4.2 NAME 'ipaHost' - AUXILIARY - MAY ( userPassword $ ipaClientVersion $ enrolledBy) - X-ORIGIN 'IPA v2' ) - - diff --git a/ipaserver/__init__.py b/ipaserver/__init__.py new file mode 100644 index 00000000..b0be96bd --- /dev/null +++ b/ipaserver/__init__.py @@ -0,0 +1,22 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Package containing server backend. +""" diff --git a/ipaserver/conn.py b/ipaserver/conn.py new file mode 100644 index 00000000..fb00ad99 --- /dev/null +++ b/ipaserver/conn.py @@ -0,0 +1,69 @@ +# Authors: Rob Crittenden +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import krbV +import ldap +import ldap.dn +import ipaldap + +class IPAConn: + def __init__(self, host, port, krbccache, debug=None): + self._conn = None + + # Save the arguments + self._host = host + self._port = port + self._krbccache = krbccache + self._debug = debug + + self._ctx = krbV.default_context() + + ccache = krbV.CCache(name=krbccache, context=self._ctx) + cprinc = ccache.principal() + + self._conn = ipaldap.IPAdmin(host,port,None,None,None,debug) + + # This will bind the connection + try: + self._conn.set_krbccache(krbccache, cprinc.name) + except ldap.UNWILLING_TO_PERFORM, e: + raise e + except Exception, e: + raise e + + def __del__(self): + # take no chances on unreleased connections + self.releaseConn() + + def getConn(self): + return self._conn + + def releaseConn(self): + if self._conn is None: + return + + self._conn.unbind_s() + self._conn = None + + return + +if __name__ == "__main__": + ipaconn = IPAConn("localhost", 389, "FILE:/tmp/krb5cc_500") + x = ipaconn.getConn().getEntry("dc=example,dc=com", ldap.SCOPE_SUBTREE, "uid=admin", ["cn"]) + print "%s" % x diff --git a/ipaserver/context.py b/ipaserver/context.py new file mode 100644 index 00000000..15dd7d90 --- /dev/null +++ b/ipaserver/context.py @@ -0,0 +1,32 @@ +# Authors: Rob Crittenden +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +# This should only be imported once. Importing again will cause the +# a new instance to be created in the same thread + +# To use: +# from ipaserver.context import context +# context.foo = "bar" + +# FIXME: This module is depreciated and code should switch to using +# ipalib.request instead + +import threading + +context = threading.local() diff --git a/ipaserver/ipaldap.py b/ipaserver/ipaldap.py new file mode 100644 index 00000000..19fd40ef --- /dev/null +++ b/ipaserver/ipaldap.py @@ -0,0 +1,546 @@ +# Authors: Rich Megginson +# Rob Crittenden 0 + + def hasAttr(self,name): + """Return True if this entry has an attribute named name, False otherwise""" + return self.data and self.data.has_key(name) + + def __getattr__(self,name): + """If name is the name of an LDAP attribute, return the first value for that + attribute - equivalent to getValue - this allows the use of + entry.cn + instead of + entry.getValue('cn') + This also allows us to return None if an attribute is not found rather than + throwing an exception""" + return self.getValue(name) + + def getValues(self,name): + """Get the list (array) of values for the attribute named name""" + return self.data.get(name) + + def getValue(self,name): + """Get the first value for the attribute named name""" + return self.data.get(name,[None])[0] + + def setValue(self, name, *value): + """ + Set a value on this entry. + + The value passed in may be a single value, several values, or a + single sequence. For example: + + * ent.setValue('name', 'value') + * ent.setValue('name', 'value1', 'value2', ..., 'valueN') + * ent.setValue('name', ['value1', 'value2', ..., 'valueN']) + * ent.setValue('name', ('value1', 'value2', ..., 'valueN')) + + Since value is a tuple, we may have to extract a list or tuple from + that tuple as in the last two examples above. + """ + if isinstance(value[0],list) or isinstance(value[0],tuple): + self.data[name] = value[0] + else: + self.data[name] = value + + setValues = setValue + + def toTupleList(self): + """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.""" + 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""" + return self.__repr__() + + # the ldif class base64 encodes some attrs which I would rather see in + # raw form - to encode specific attrs as base64, add them to the list below + ldif.safe_string_re = re.compile('^$') + base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData'] + + def __repr__(self): + """Convert the Entry to its LDIF representation""" + sio = cStringIO.StringIO() + # what's all this then? the unparse method will currently only accept + # a list or a dict, not a class derived from them. self.data is a + # cidict, so unparse barfs on it. I've filed a bug against python-ldap, + # but in the meantime, we have to convert to a plain old dict for + # printing + # I also don't want to see wrapping, so set the line width really high + # (1000) + newdata = {} + newdata.update(self.data) + ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata) + return sio.getvalue() + +def wrapper(f,name): + """This is the method that wraps all of the methods of the superclass. + This seems to need to be an unbound method, that's why it's outside + of IPAdmin. Perhaps there is some way to do this with the new + classmethod or staticmethod of 2.4. Basically, we replace every call + to a method in SimpleLDAPObject (the superclass of IPAdmin) with a + call to inner. The f argument to wrapper is the bound method of + IPAdmin (which is inherited from the superclass). Bound means that it + will implicitly be called with the self argument, it is not in the + args list. name is the name of the method to call. If name is a + method that returns entry objects (e.g. result), we wrap the data + returned by an Entry class. If name is a method that takes an entry + argument, we extract the raw data from the entry object to pass in. + """ + def inner(*args, **kargs): + if name == 'result': + objtype, data = f(*args, **kargs) + # data is either a 2-tuple or a list of 2-tuples + # print data + if data: + if isinstance(data,tuple): + return objtype, Entry(data) + elif isinstance(data,list): + return objtype, [Entry(x) for x in data] + else: + raise TypeError, "unknown data type %s returned by result" % type(data) + else: + return objtype, data + elif name.startswith('add'): + # the first arg is self + # the second and third arg are the dn and the data to send + # We need to convert the Entry into the format used by + # python-ldap + ent = args[0] + if isinstance(ent,Entry): + return f(ent.dn, ent.toTupleList(), *args[2:]) + else: + return f(*args, **kargs) + else: + return f(*args, **kargs) + return inner + +class IPAdmin(SimpleLDAPObject): + + def __localinit(self): + """If a CA certificate is provided then it is assumed that we are + doing SSL client authentication with proxy auth. + + If a CA certificate is not present then it is assumed that we are + using a forwarded kerberos ticket for SASL auth. SASL provides + its own encryption. + """ + if self.cacert is not None: + SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port)) + else: + SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port)) + + def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None): + """We just set our instance variables and wrap the methods - the real + work is done in __localinit. This is separated out this way so + that we can call it from places other than instance creation + e.g. when we just need to reconnect + """ + if debug and debug.lower() == "on": + ldap.set_option(ldap.OPT_DEBUG_LEVEL,255) + if cacert is not None: + ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert) + if bindcert is not None: + ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert) + if bindkey is not None: + ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey) + + self.__wrapmethods() + self.port = port + self.host = host + self.cacert = cacert + self.bindcert = bindcert + self.bindkey = bindkey + self.proxydn = proxydn + self.suffixes = {} + self.__localinit() + + def __str__(self): + return self.host + ":" + str(self.port) + + def __get_server_controls(self): + """Create the proxy user server control. The control has the form + 0x04 = Octet String + 4|0x80 sets the length of the string length field at 4 bytes + the struct() gets us the length in bytes of string self.proxydn + self.proxydn is the proxy dn to send""" + + if self.proxydn is not None: + proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn; + + # Create the proxy control + sctrl=[] + sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn)) + else: + sctrl=None + + return sctrl + + def toLDAPURL(self): + return "ldap://%s:%d/" % (self.host,self.port) + + def set_proxydn(self, proxydn): + self.proxydn = proxydn + + def set_krbccache(self, krbccache, principal): + if krbccache is not None: + os.environ["KRB5CCNAME"] = krbccache + self.sasl_interactive_bind_s("", sasl_auth) + self.principal = principal + self.proxydn = None + + def do_simple_bind(self, binddn="cn=directory manager", bindpw=""): + self.binddn = binddn + self.bindpwd = bindpw + self.simple_bind_s(binddn, bindpw) + + def getEntry(self,*args): + """This wraps the search function. It is common to just get one entry""" + + sctrl = self.__get_server_controls() + + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + + try: + res = self.search(*args) + objtype, obj = self.result(res) + except ldap.NO_SUCH_OBJECT, e: + raise errors.NotFound, notfound(args) + except ldap.LDAPError, e: + raise errors.DatabaseError, e + + if not obj: + raise errors.NotFound, notfound(args) + + elif isinstance(obj,Entry): + return obj + else: # assume list/tuple + return obj[0] + + def getList(self,*args): + """This wraps the search function to find multiple entries.""" + + sctrl = self.__get_server_controls() + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + + try: + res = self.search(*args) + objtype, obj = self.result(res) + except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e: + # Too many results returned by search + raise e + except ldap.LDAPError, e: + raise e + + if not obj: + raise errors.NotFound, notfound(args) + + entries = [] + for s in obj: + entries.append(s) + + return entries + + def getListAsync(self,*args): + """This version performs an asynchronous search, to allow + results even if we hit a limit. + + It returns a list: counter followed by the results. + If the results are truncated, counter will be set to -1. + """ + + sctrl = self.__get_server_controls() + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + + entries = [] + partial = 0 + + try: + msgid = self.search_ext(*args) + objtype, result_list = self.result(msgid, 0) + while result_list: + for result in result_list: + entries.append(result) + objtype, result_list = self.result(msgid, 0) + except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED, + ldap.TIMELIMIT_EXCEEDED), e: + partial = 1 + except ldap.LDAPError, e: + raise e + + if not entries: + raise errors.NotFound, notfound(args) + + if partial == 1: + counter = -1 + else: + counter = len(entries) + + return [counter] + entries + + def addEntry(self,*args): + """This wraps the add function. It assumes that the entry is already + populated with all of the desired objectclasses and attributes""" + + sctrl = self.__get_server_controls() + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.add_s(*args) + except ldap.ALREADY_EXISTS, e: + raise errors.DuplicateEntry, "Entry already exists" + except ldap.LDAPError, e: + raise DatabaseError, e + return True + + def updateRDN(self, dn, newrdn): + """Wrap the modrdn function.""" + + sctrl = self.__get_server_controls() + + if dn == newrdn: + # no need to report an error + return True + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modrdn_s(dn, newrdn, delold=1) + except ldap.LDAPError, e: + raise DatabaseError, e + return True + + def updateEntry(self,dn,oldentry,newentry): + """This wraps the mod function. It assumes that the entry is already + populated with all of the desired objectclasses and attributes""" + + sctrl = self.__get_server_controls() + + modlist = self.generateModList(oldentry, newentry) + + if len(modlist) == 0: + raise errors.EmptyModlist + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modify_s(dn, modlist) + # this is raised when a 'delete' attribute isn't found. + # it indicates the previous attribute was removed by another + # update, making the oldentry stale. + except ldap.NO_SUCH_ATTRIBUTE: + raise errors.MidairCollision + except ldap.LDAPError, e: + raise errors.DatabaseError, e + return True + + def generateModList(self, old_entry, new_entry): + """A mod list generator that computes more precise modification lists + than the python-ldap version. This version purposely generates no + REPLACE operations, to deal with multi-user updates more properly.""" + modlist = [] + + old_entry = ipautil.CIDict(old_entry) + new_entry = ipautil.CIDict(new_entry) + + keys = set(map(string.lower, old_entry.keys())) + keys.update(map(string.lower, new_entry.keys())) + + for key in keys: + new_values = new_entry.get(key, []) + if not(isinstance(new_values,list) or isinstance(new_values,tuple)): + new_values = [new_values] + new_values = filter(lambda value:value!=None, new_values) + new_values = set(new_values) + + old_values = old_entry.get(key, []) + if not(isinstance(old_values,list) or isinstance(old_values,tuple)): + old_values = [old_values] + old_values = filter(lambda value:value!=None, old_values) + old_values = set(old_values) + + adds = list(new_values.difference(old_values)) + removes = list(old_values.difference(new_values)) + + if len(removes) > 0: + modlist.append((ldap.MOD_DELETE, key, removes)) + if len(adds) > 0: + modlist.append((ldap.MOD_ADD, key, adds)) + + return modlist + + def inactivateEntry(self,dn,has_key): + """Rather than deleting entries we mark them as inactive. + has_key defines whether the entry already has nsAccountlock + set so we can determine which type of mod operation to run.""" + + sctrl = self.__get_server_controls() + modlist=[] + + if has_key: + operation = ldap.MOD_REPLACE + else: + operation = ldap.MOD_ADD + + modlist.append((operation, "nsAccountlock", "true")) + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.modify_s(dn, modlist) + except ldap.LDAPError, e: + raise DatabaseError, e + return True + + def deleteEntry(self,*args): + """This wraps the delete function. Use with caution.""" + + sctrl = self.__get_server_controls() + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.delete_s(*args) + except ldap.INSUFFICIENT_ACCESS, e: + raise errors.InsufficientAccess, e + except ldap.LDAPError, e: + raise errors.DatabaseError, e + return True + + def modifyPassword(self,dn,oldpass,newpass): + """Set the user password using RFC 3062, LDAP Password Modify Extended + Operation. This ends up calling the IPA password slapi plugin + handler so the Kerberos password gets set properly. + + oldpass is not mandatory + """ + + sctrl = self.__get_server_controls() + + try: + if sctrl is not None: + self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) + self.passwd_s(dn, oldpass, newpass) + except ldap.LDAPError, e: + raise e + return True + + def __wrapmethods(self): + """This wraps all methods of SimpleLDAPObject, so that we can intercept + the methods that deal with entries. Instead of using a raw list of tuples + of lists of hashes of arrays as the entry object, we want to wrap entries + in an Entry class that provides some useful methods""" + for name in dir(self.__class__.__bases__[0]): + attr = getattr(self, name) + if callable(attr): + setattr(self, name, wrapper(attr, name)) + + def normalizeDN(dn): + # not great, but will do until we use a newer version of python-ldap + # that has DN utilities + ary = ldap.explode_dn(dn.lower()) + return ",".join(ary) + normalizeDN = staticmethod(normalizeDN) + +def notfound(args): + """Return a string suitable for displaying as an error when a + search returns no results. + + This just returns whatever is after the equals sign""" + if len(args) > 2: + searchfilter = args[2] + try: + # Python re doesn't do paren counting so the string could + # have a trailing paren "foo)" + target = re.match(r'\(.*=(.*)\)', searchfilter).group(1) + target = target.replace(")","") + except: + target = searchfilter + return "%s not found" % str(target) + else: + return args[0] diff --git a/ipaserver/ipautil.py b/ipaserver/ipautil.py new file mode 100644 index 00000000..6422fe5a --- /dev/null +++ b/ipaserver/ipautil.py @@ -0,0 +1,201 @@ +# Authors: Simo Sorce +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import string +import xmlrpclib +import re + +def realm_to_suffix(realm_name): + s = realm_name.split(".") + terms = ["dc=" + x.lower() for x in s] + return ",".join(terms) + +class CIDict(dict): + """ + Case-insensitive but case-respecting dictionary. + + This code is derived from python-ldap's cidict.py module, + written by stroeder: http://python-ldap.sourceforge.net/ + + This version extends 'dict' so it works properly with TurboGears. + If you extend UserDict, isinstance(foo, dict) returns false. + """ + + def __init__(self,default=None): + super(CIDict, self).__init__() + self._keys = {} + self.update(default or {}) + + def __getitem__(self,key): + return super(CIDict,self).__getitem__(string.lower(key)) + + def __setitem__(self,key,value): + lower_key = string.lower(key) + self._keys[lower_key] = key + return super(CIDict,self).__setitem__(string.lower(key),value) + + def __delitem__(self,key): + lower_key = string.lower(key) + del self._keys[lower_key] + return super(CIDict,self).__delitem__(string.lower(key)) + + def update(self,dict): + for key in dict.keys(): + self[key] = dict[key] + + def has_key(self,key): + return super(CIDict, self).has_key(string.lower(key)) + + def get(self,key,failobj=None): + try: + return self[key] + except KeyError: + return failobj + + def keys(self): + return self._keys.values() + + def items(self): + result = [] + for k in self._keys.values(): + result.append((k,self[k])) + return result + + def copy(self): + copy = {} + for k in self._keys.values(): + copy[k] = self[k] + return copy + + def iteritems(self): + return self.copy().iteritems() + + def iterkeys(self): + return self.copy().iterkeys() + + def setdefault(self,key,value=None): + try: + return self[key] + except KeyError: + self[key] = value + return value + + def pop(self, key, *args): + try: + value = self[key] + del self[key] + return value + except KeyError: + if len(args) == 1: + return args[0] + raise + + def popitem(self): + (lower_key,value) = super(CIDict,self).popitem() + key = self._keys[lower_key] + del self._keys[lower_key] + + return (key,value) + + +# +# The safe_string_re regexp and needs_base64 function are extracted from the +# python-ldap ldif module, which was +# written by Michael Stroeder +# http://python-ldap.sourceforge.net +# +# It was extracted because ipaldap.py is naughtily reaching into the ldif +# module and squashing this regexp. +# +SAFE_STRING_PATTERN = '(^(\000|\n|\r| |:|<)|[\000\n\r\200-\377]+|[ ]+$)' +safe_string_re = re.compile(SAFE_STRING_PATTERN) + +def needs_base64(s): + """ + returns 1 if s has to be base-64 encoded because of special chars + """ + return not safe_string_re.search(s) is None + + +def wrap_binary_data(data): + """Converts all binary data strings into Binary objects for transport + back over xmlrpc.""" + if isinstance(data, str): + if needs_base64(data): + return xmlrpclib.Binary(data) + else: + return data + elif isinstance(data, list) or isinstance(data,tuple): + retval = [] + for value in data: + retval.append(wrap_binary_data(value)) + return retval + elif isinstance(data, dict): + retval = {} + for (k,v) in data.iteritems(): + retval[k] = wrap_binary_data(v) + return retval + else: + return data + + +def unwrap_binary_data(data): + """Converts all Binary objects back into strings.""" + if isinstance(data, xmlrpclib.Binary): + # The data is decoded by the xmlproxy, but is stored + # in a binary object for us. + return str(data) + elif isinstance(data, str): + return data + elif isinstance(data, list) or isinstance(data,tuple): + retval = [] + for value in data: + retval.append(unwrap_binary_data(value)) + return retval + elif isinstance(data, dict): + retval = {} + for (k,v) in data.iteritems(): + retval[k] = unwrap_binary_data(v) + return retval + else: + return data + +def get_gsserror(e): + """A GSSError exception looks differently in python 2.4 than it does + in python 2.5, deal with it.""" + + try: + primary = e[0] + secondary = e[1] + except: + primary = e[0][0] + 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/ipaserver/mod_python_xmlrpc.py b/ipaserver/mod_python_xmlrpc.py new file mode 100644 index 00000000..9a2960f9 --- /dev/null +++ b/ipaserver/mod_python_xmlrpc.py @@ -0,0 +1,367 @@ +# mod_python script + +# ipaxmlrpc - an XMLRPC interface for ipa. +# Copyright (c) 2007 Red Hat +# +# IPA is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; +# version 2.1 of the License. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this software; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Based on kojixmlrpc - an XMLRPC interface for koji by +# Mike McLean +# +# Authors: +# Rob Crittenden + +""" +Production XML-RPC server using mod_python. +""" + +import sys + + +import time +import traceback +import pprint +from xmlrpclib import Marshaller,loads,dumps,Fault +try: + from mod_python import apache +except ImportError: + pass +import logging + +import ldap +from ipalib import api +from ipalib import config +from ipaserver import conn +from ipaserver.servercore import context +from ipaserver.servercore import ipautil +from ipalib.util import xmlrpc_unmarshal + +import string + +api.load_plugins() + +# Global list of available functions +gfunctions = {} + +def register_function(function, name = None): + if name is None: + name = function.__name__ + gfunctions[name] = function + +class ModXMLRPCRequestHandler(object): + """Simple XML-RPC handler for mod_python environment""" + + def __init__(self): + global gfunctions + + self.funcs = gfunctions + self.traceback = False + #introspection functions + self.register_function(self.ping, name="ping") + self.register_function(self.list_api, name="_listapi") + self.register_function(self.system_listMethods, name="system.listMethods") + self.register_function(self.system_methodSignature, name="system.methodSignature") + self.register_function(self.system_methodHelp, name="system.methodHelp") + self.register_function(self.multiCall) + + def register_function(self, function, name = None): + if name is None: + name = function.__name__ + self.funcs[name] = function + + def register_module(self, instance, prefix=None): + """Register all the public functions in an instance with prefix prepended + + For example + h.register_module(exports,"pub.sys") + will register the methods of exports with names like + pub.sys.method1 + pub.sys.method2 + ...etc + """ + for name in dir(instance): + if name.startswith('_'): + continue + function = getattr(instance, name) + if not callable(function): + continue + if prefix is not None: + name = "%s.%s" %(prefix,name) + self.register_function(function, name=name) + + def register_instance(self,instance): + self.register_module(instance) + + def _marshaled_dispatch(self, data, req): + """Dispatches an XML-RPC method from marshalled (XML) data.""" + + params, method = loads(data) + pythonopts = req.get_options() + + # Populate the Apache environment variables + req.add_common_vars() + + context.opts['remoteuser'] = req.user + + if req.subprocess_env.get("KRB5CCNAME") is not None: + krbccache = req.subprocess_env.get("KRB5CCNAME") + else: + response = dumps(Fault(5, "Did not receive Kerberos credentials.")) + return response + + debuglevel = logging.INFO + if pythonopts.get("IPADebug"): + context.opts['ipadebug'] = pythonopts.get("IPADebug").lower() + + if context.opts['ipadebug'] == "on": + debuglevel = logging.DEBUG + + if not context.opts.get('ipadebug'): + context.opts['ipadebug'] = "off" + + logging.basicConfig(level=debuglevel, + format='[%(asctime)s] [%(levelname)s] %(message)s', + datefmt='%a %b %d %H:%M:%S %Y', + stream=sys.stderr) + + logging.info("Interpreter: %s" % req.interpreter) + + +# if opts['ipadebug'] == "on": +# for o in opts: +# logging.debug("IPA: setting option %s: %s" % (o, opts[o])) +# for e in req.subprocess_env: +# logging.debug("IPA: environment %s: %s" % (e, req.subprocess_env[e])) + + context.conn = conn.IPAConn(api.env.ldaphost, api.env.ldapport, krbccache, context.opts.get('ipadebug')) + + start = time.time() + # generate response + try: + response = self._dispatch(method, params) + # wrap response in a singleton tuple + response = (response,) + response = dumps(response, methodresponse=1, allow_none=1) + except Fault, e: + response = dumps(Fault(e.faultCode, e.faultString)) + except: + self.traceback = True + # report exception back to server + e_class, e = sys.exc_info()[:2] + faultCode = getattr(e_class,'faultCode',1) + tb_str = ''.join(traceback.format_exception(*sys.exc_info())) + faultString = tb_str + response = dumps(Fault(faultCode, faultString)) + + return response + + def _dispatch(self,method,params): + func = self.funcs.get(method,None) + if func is None: + raise Fault(1, "Invalid method: %s" % method) + + params = list(ipautil.unwrap_binary_data(params)) + (args, kw) = xmlrpc_unmarshal(*params) + + ret = func(*args, **kw) + + return ipautil.wrap_binary_data(ret) + + def multiCall(self, calls): + """Execute a multicall. Execute each method call in the calls list, collecting results and errors, and return those as a list.""" + results = [] + for call in calls: + try: + result = self._dispatch(call['methodName'], call['params']) + except Fault, fault: + results.append({'faultCode': fault.faultCode, 'faultString': fault.faultString}) + except: + # transform unknown exceptions into XML-RPC Faults + # don't create a reference to full traceback since this creates + # a circular reference. + exc_type, exc_value = sys.exc_info()[:2] + faultCode = getattr(exc_type, 'faultCode', 1) + faultString = ', '.join(exc_value.args) + trace = traceback.format_exception(*sys.exc_info()) + # traceback is not part of the multicall spec, but we include it for debugging purposes + results.append({'faultCode': faultCode, 'faultString': faultString, 'traceback': trace}) + else: + results.append([result]) + + return results + + def list_api(self): + funcs = [] + for name,func in self.funcs.items(): + #the keys in self.funcs determine the name of the method as seen over xmlrpc + #func.__name__ might differ (e.g. for dotted method names) + args = self._getFuncArgs(func) + doc = None + try: + doc = func.doc + except AttributeError: + doc = func.__doc__ + funcs.append({'name': name, + 'doc': doc, + 'args': args}) + return funcs + + def ping(self): + """Simple test to see if the XML-RPC is up and active.""" + return "pong" + + def _getFuncArgs(self, func): + try: + # Plugins have this + args = list(func.args) + args.append("kw") + except: + # non-plugin functions such as the introspective ones + args = [] + for x in range(0, func.func_code.co_argcount): + if x == 0 and func.func_code.co_varnames[x] == "self": + continue + # opts is a name we tack on internally. Don't publish it. + if func.func_code.co_varnames[x] == "opts": + continue + if func.func_defaults and func.func_code.co_argcount - x <= len(func.func_defaults): + args.append((func.func_code.co_varnames[x], func.func_defaults[x - func.func_code.co_argcount + len(func.func_defaults)])) + else: + args.append(func.func_code.co_varnames[x]) + return args + + def system_listMethods(self): + """List all available XML-RPC methods""" + return self.funcs.keys() + + def system_methodSignature(self, method): + """signatures are not supported""" + #it is not possible to autogenerate this data + return 'signatures not supported' + + def system_methodHelp(self, method): + """Return help on a specific method""" + func = self.funcs.get(method) + if func is None: + return "" + arglist = [] + for arg in self._getFuncArgs(func): + if isinstance(arg,str): + arglist.append(arg) + else: + arglist.append('%s=%s' % (arg[0], arg[1])) + ret = '%s(%s)' % (method, ", ".join(arglist)) + doc = None + try: + doc = func.doc + except AttributeError: + doc = func.__doc__ + if doc: + ret += "\ndescription: %s" % func.__doc__ + return ret + + def handle_request(self,req): + """Handle a single XML-RPC request""" + + # XMLRPC uses POST only. Reject anything else + if req.method != 'POST': + req.allow_methods(['POST'],1) + raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED + + # The LDAP connection pool is not thread-safe. Avoid problems and + # force the forked model for now. + if apache.mpm_query(apache.AP_MPMQ_IS_THREADED): + response = dumps(Fault(3, "Apache must use the forked model")) + else: + response = self._marshaled_dispatch(req.read(), req) + + req.content_type = "text/xml" + req.set_content_length(len(response)) + req.write(response) + + +# +# mod_python handler +# + +def handler(req, profiling=False): + h = ModXMLRPCRequestHandler() + + if profiling: + import profile, pstats, StringIO, tempfile + global _profiling_req + _profiling_req = req + temp = tempfile.NamedTemporaryFile() + profile.run("import ipxmlrpc; ipaxmlrpc.handler(ipaxmlrpc._profiling_req, False)", temp.name) + stats = pstats.Stats(temp.name) + strstream = StringIO.StringIO() + sys.stdout = strstream + stats.sort_stats("time") + stats.print_stats() + req.write("
" + strstream.getvalue() + "
") + _profiling_req = None + else: + context.opts = req.get_options() + context.reqs = req + try: + h.handle_request(req) + finally: + # Clean up any per-request data and connections + for k in context.__dict__.keys(): + del context.__dict__[k] + + return apache.OK + +def setup_logger(level): + """Make a global logging object.""" + l = logging.getLogger() + l.setLevel(level) + h = logging.StreamHandler() + f = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s") + h.setFormatter(f) + l.addHandler(h) + + return + +def load_modules(): + """Load all plugins and register the XML-RPC functions we provide. + + Called by mod_python PythonImport + + PythonImport /path/to/ipaxmlrpc.py::load_modules main_interpreter + ... + PythonInterpreter main_interpreter + PythonHandler ipaxmlrpc + """ + + # setup up the logger with a DEBUG level. It may get reset to INFO + # once we start processing requests. We don't have access to the + # Apache configuration yet. + setup_logger(logging.DEBUG) + + api.finalize() + + # Initialize our environment + config.set_default_env(api.env) + env_dict = config.read_config() + env_dict['server_context'] = True + api.env.update(env_dict) + + # Get and register all the methods + for cmd in api.Command: + logging.debug("registering XML-RPC call %s" % cmd) + register_function(api.Command[cmd], cmd) + + return diff --git a/ipaserver/plugins/__init__.py b/ipaserver/plugins/__init__.py new file mode 100644 index 00000000..5737dcb7 --- /dev/null +++ b/ipaserver/plugins/__init__.py @@ -0,0 +1,24 @@ +# Authors: Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Sub-package containing all server plugins. + +By convention, modules with frontend plugins are named f_*.py and modules +with backend plugins are named b_*.py. +""" diff --git a/ipaserver/plugins/b_ldap.py b/ipaserver/plugins/b_ldap.py new file mode 100644 index 00000000..071bf52e --- /dev/null +++ b/ipaserver/plugins/b_ldap.py @@ -0,0 +1,327 @@ +# Authors: +# Rob Crittenden +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Backend plugin for LDAP. + +This wraps the python-ldap bindings. +""" + +import ldap as _ldap +from ipalib import api, Context +from ipalib import errors +from ipalib.crud import CrudBackend +from ipaserver import servercore +from ipaserver import ipaldap + + +class conn(Context): + """ + Thread-local LDAP connection. + """ + + def get_value(self): + return 'it worked' + +api.register(conn) + + +class ldap(CrudBackend): + """ + LDAP backend plugin. + """ + + def __init__(self): + self.dn = _ldap.dn + + def make_user_dn(self, uid): + """ + Construct user dn from uid. + """ + return 'uid=%s,%s,%s' % ( + self.dn.escape_dn_chars(uid), + self.api.env.container_user, + self.api.env.basedn, + ) + + def make_group_dn(self, cn): + """ + Construct group dn from cn. + """ + return 'cn=%s,%s,%s' % ( + self.dn.escape_dn_chars(cn), + self.api.env.container_group, + self.api.env.basedn, + ) + + def make_hostgroup_dn(self, cn): + """ + Construct group of hosts dn from cn. + """ + return 'cn=%s,%s,%s' % ( + self.dn.escape_dn_chars(cn), + self.api.env.container_hostgroup, + self.api.env.basedn, + ) + + def make_service_dn(self, principal): + """ + Construct service principal dn from principal name + """ + return 'krbprincipalname=%s,%s,%s' % ( + self.dn.escape_dn_chars(principal), + self.api.env.container_service, + self.api.env.basedn, + ) + + def make_host_dn(self, hostname): + """ + Construct host dn from hostname + """ + return 'cn=%s,%s,%s' % ( + self.dn.escape_dn_chars(hostname), + self.api.env.container_host, + self.api.env.basedn, + ) + + def get_object_type(self, attribute): + """ + Based on attribute, make an educated guess as to the type of + object we're looking for. + """ + attribute = attribute.lower() + object_type = None + if attribute == "uid": # User + object_type = "posixAccount" + elif attribute == "cn": # Group + object_type = "posixGroup" + elif attribute == "krbprincipalname": # Service + object_type = "krbPrincipal" + + return object_type + + def find_entry_dn(self, key_attribute, primary_key, object_type=None, base=None): + """ + Find an existing entry's dn from an attribute + """ + key_attribute = key_attribute.lower() + if not object_type: + object_type = self.get_object_type(key_attribute) + if not object_type: + return None + + search_filter = "(&(objectclass=%s)(%s=%s))" % ( + object_type, + key_attribute, + self.dn.escape_dn_chars(primary_key) + ) + + if not base: + base = self.api.env.container_accounts + + search_base = "%s, %s" % (base, self.api.env.basedn) + + entry = servercore.get_sub_entry(search_base, search_filter, ['dn', 'objectclass']) + + return entry.get('dn') + + def get_base_entry(self, searchbase, searchfilter, attrs): + return servercore.get_base_entry(searchbase, searchfilter, attrs) + + def get_sub_entry(self, searchbase, searchfilter, attrs): + return servercore.get_sub_entry(searchbase, searchfilter, attrs) + + def get_one_entry(self, searchbase, searchfilter, attrs): + return servercore.get_one_entry(searchbase, searchfilter, attrs) + + def get_ipa_config(self): + """Return a dictionary of the IPA configuration""" + return servercore.get_ipa_config() + + def mark_entry_active(self, dn): + return servercore.mark_entry_active(dn) + + def mark_entry_inactive(self, dn): + return servercore.mark_entry_inactive(dn) + + def _generate_search_filters(self, **kw): + """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)...) + exact_pattern = "(|" + for field in kw.keys(): + exact_pattern += "(%s=%s)" % (field, kw[field]) + exact_pattern += ")" + + sub_pattern = "(|" + for field in kw.keys(): + sub_pattern += "(%s=*%s*)" % (field, kw[field]) + sub_pattern += ")" + + # construct the giant match for all words + exact_match_filter = "(&" + exact_pattern + ")" + partial_match_filter = "(|" + sub_pattern + ")" + + return (exact_match_filter, partial_match_filter) + + def modify_password(self, dn, **kw): + return servercore.modify_password(dn, kw.get('oldpass'), kw.get('newpass')) + + def add_member_to_group(self, memberdn, groupdn): + """ + Add a new member to a group. + + :param memberdn: the DN of the member to add + :param groupdn: the DN of the group to add a member to + """ + return servercore.add_member_to_group(memberdn, groupdn) + + def remove_member_from_group(self, memberdn, groupdn): + """ + Remove a new member from a group. + + :param memberdn: the DN of the member to remove + :param groupdn: the DN of the group to remove a member from + """ + return servercore.remove_member_from_group(memberdn, groupdn) + + # The CRUD operations + + def strip_none(self, kw): + """ + Remove any None values present in the LDAP attribute dict. + """ + for (key, value) in kw.iteritems(): + if value is None: + continue + if type(value) in (list, tuple): + value = filter( + lambda v: type(v) in (str, unicode, bool, int, float), + value + ) + if len(value) > 0: + yield (key, value) + else: + assert type(value) in (str, unicode, bool, int, float) + yield (key, value) + + def create(self, **kw): + if servercore.entry_exists(kw['dn']): + raise errors.DuplicateEntry("entry already exists") + kw = dict(self.strip_none(kw)) + + + entry = ipaldap.Entry(kw['dn']) + + # dn isn't allowed to be in the entry itself + del kw['dn'] + + # Fill in our new entry + for k in kw: + entry.setValues(k, kw[k]) + + servercore.add_entry(entry) + return self.retrieve(entry.dn) + + def retrieve(self, dn, attributes=None): + return servercore.get_entry_by_dn(dn, attributes) + + def update(self, dn, **kw): + result = self.retrieve(dn, ["*"]) + + entry = ipaldap.Entry((dn, servercore.convert_scalar_values(result))) + kw = dict(self.strip_none(kw)) + for k in kw: + entry.setValues(k, kw[k]) + + servercore.update_entry(entry.toDict()) + + return self.retrieve(dn) + + def delete(self, dn): + return servercore.delete_entry(dn) + + def search(self, **kw): + objectclass = kw.get('objectclass') + sfilter = kw.get('filter') + attributes = kw.get('attributes') + base = kw.get('base') + if attributes: + del kw['attributes'] + else: + attributes = ['*'] + if objectclass: + del kw['objectclass'] + if base: + del kw['base'] + if sfilter: + del kw['filter'] + (exact_match_filter, partial_match_filter) = self._generate_search_filters(**kw) + if objectclass: + exact_match_filter = "(&(objectClass=%s)%s)" % (objectclass, exact_match_filter) + partial_match_filter = "(&(objectClass=%s)%s)" % (objectclass, partial_match_filter) + if sfilter: + exact_match_filter = "(%s%s)" % (sfilter, exact_match_filter) + partial_match_filter = "(%s%s)" % (sfilter, partial_match_filter) + + if not base: + base = self.api.env.container_accounts + + search_base = "%s, %s" % (base, self.api.env.basedn) + try: + exact_results = servercore.search(search_base, + exact_match_filter, attributes) + except errors.NotFound: + exact_results = [0] + + try: + partial_results = servercore.search(search_base, + partial_match_filter, attributes) + except errors.NotFound: + 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.get('dn'), exact_results)) + partial_results = filter(lambda e: e.get('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) + + results = [counter] + for r in exact_results + partial_results: + results.append(r) + + return results + +api.register(ldap) diff --git a/ipaserver/plugins/b_ra.py b/ipaserver/plugins/b_ra.py new file mode 100644 index 00000000..bf119b81 --- /dev/null +++ b/ipaserver/plugins/b_ra.py @@ -0,0 +1,406 @@ +# Authors: +# Andrew Wnuk +# Jason Gerard DeRose +# +# Copyright (C) 2009 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Backend plugin for IPA-RA. + +IPA-RA provides an access to CA to issue, retrieve, and revoke certificates. +IPA-RA plugin provides CA interface via the following methods: + check_request_status to check certificate request status + get_certificate to retrieve an existing certificate + request_certificate to request certificate + revoke_certificate to revoke certificate + take_certificate_off_hold to take certificate off hold +""" + +import os, stat, subprocess +import array +import errno +import binascii +import httplib, urllib +from socket import gethostname + +from ipalib import api, Backend +from ipalib import errors +from ipaserver import servercore +from ipaserver import ipaldap + + +class ra(Backend): + + + def __init__(self): + self.sec_dir = api.env.dot_ipa + os.sep + 'alias' + self.pwd_file = self.sec_dir + os.sep + '.pwd' + self.noise_file = self.sec_dir + os.sep + '.noise' + + self.ca_host = None + self.ca_port = None + self.ca_ssl_port = None + + self.__get_ca_location() + + self.ipa_key_size = "2048" + self.ipa_certificate_nickname = "ipaCert" + self.ca_certificate_nickname = "caCert" + + if not os.path.isdir(self.sec_dir): + os.mkdir(self.sec_dir) + self.__create_pwd_file() + self.__create_nss_db() + self.__import_ca_chain() + self.__request_ipa_certificate(self.__generate_ipa_request()) + + + def check_request_status(self, request_id=None): + """ + Check certificate request status + :param request_id: request ID + """ + self.log.debug("IPA-RA: check_request_status") + return_values = {} + if request_id is not None: + params = urllib.urlencode({'requestId': request_id, 'xmlOutput': 'true'}) + headers = {"Content-type": "application/x-www-form-urlencoded"} + conn = httplib.HTTPConnection(self.ca_host, self.ca_port) + conn.request("POST", "/ca/ee/ca/checkRequest", params, headers) + response = conn.getresponse() + api.log.debug("IPA-RA: response.status: %d response.reason: %s" % (response.status, response.reason)) + data = response.read() + conn.close() + self.log.debug(data) + if data is not None: + request_status = self.__find_substring(data, 'header.status = "', '"') + if request_status is not None: + return_values["status"] = "0" + return_values["request_status"] = request_status + self.log.debug("IPA-RA: request_status: '%s'" % request_status) + serial_number = self.__find_substring(data, 'record.serialNumber="', '"') + if serial_number is not None: + return_values["serial_number"] = "0x"+serial_number + request_id = self.__find_substring(data, 'header.requestId = "', '"') + if request_id is not None: + return_values["request_id"] = request_id + error = self.__find_substring(data, 'fixed.unexpectedError = "', '"') + if error is not None: + return_values["error"] = error + if return_values.has_key("status") is False: + return_values["status"] = "2" + else: + return_values["status"] = "1" + return return_values + + + def get_certificate(self, serial_number=None): + """ + Retrieve an existing certificate + :param serial_number: certificate serial number + """ + self.log.debug("IPA-RA: get_certificate") + issued_certificate = None + return_values = {} + if serial_number is not None: + request_info = ("serialNumber=%s" % serial_number) + self.log.debug("request_info: '%s'" % request_info) + returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/displayBySerial", self.ca_host+":"+str(self.ca_ssl_port)]) + self.log.debug("IPA-RA: returncode: %d" % returncode) + if (returncode == 0): + issued_certificate = self.__find_substring(stdout, 'header.certChainBase64 = "', '"') + if issued_certificate is not None: + return_values["status"] = "0" + issued_certificate = issued_certificate.replace("\\r", "") + issued_certificate = issued_certificate.replace("\\n", "") + self.log.debug("IPA-RA: issued_certificate: '%s'" % issued_certificate) + return_values["certificate"] = issued_certificate + else: + return_values["status"] = "1" + revocation_reason = self.__find_substring(stdout, 'header.revocationReason = ', ';') + if revocation_reason is not None: + return_values["revocation_reason"] = revocation_reason + else: + return_values["status"] = str(-returncode) + else: + return_values["status"] = "1" + return return_values + + + def request_certificate(self, certificate_request=None, request_type="pkcs10"): + """ + Submit certificate request + :param certificate_request: certificate request + :param request_type: request type + """ + self.log.debug("IPA-RA: request_certificate") + certificate = None + return_values = {} + if request_type is None: + request_type="pkcs10" + if certificate_request is not None: + request = urllib.quote(certificate_request) + request_info = "profileId=caRAserverCert&cert_request_type="+request_type+"&cert_request="+request+"&xmlOutput=true" + returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/ee/ca/profileSubmit", self.ca_host+":"+str(self.ca_ssl_port)]) + self.log.debug("IPA-RA: returncode: %d" % returncode) + if (returncode == 0): + status = self.__find_substring(stdout, "", "") + if status is not None: + self.log.debug ("status=%s" % status) + return_values["status"] = status + request_id = self.__find_substring(stdout, "", "") + if request_id is not None: + self.log.debug ("request_id=%s" % request_id) + return_values["request_id"] = request_id + serial_number = self.__find_substring(stdout, "", "") + if serial_number is not None: + self.log.debug ("serial_number=%s" % serial_number) + return_values["serial_number"] = ("0x%s" % serial_number) + subject = self.__find_substring(stdout, "", "") + if subject is not None: + self.log.debug ("subject=%s" % subject) + return_values["subject"] = subject + certificate = self.__find_substring(stdout, "", "") + if certificate is not None: + self.log.debug ("certificate=%s" % certificate) + return_values["certificate"] = certificate + if return_values.has_key("status") is False: + return_values["status"] = "2" + else: + return_values["status"] = str(-returncode) + else: + return_values["status"] = "1" + return return_values + + + def revoke_certificate(self, serial_number=None, revocation_reason=0): + """ + Revoke a certificate + :param serial_number: certificate serial number + :param revocation_reason: revocation reason + revocationr reasons: 0 - unspecified + 1 - key compromise + 2 - ca compromise + 3 - affiliation changed + 4 - superseded + 5 - cessation of operation + 6 - certificate hold + 7 - value 7 is not used + 8 - remove from CRL + 9 - privilege withdrawn + 10 - aa compromise + see RFC 5280 for more details + """ + return_values = {} + self.log.debug("IPA-RA: revoke_certificate") + if revocation_reason is None: + revocation_reason = 0 + if serial_number is not None: + if isinstance(serial_number, int): + serial_number = str(serial_number) + if isinstance(revocation_reason, int): + revocation_reason = str(revocation_reason) + request_info = "op=revoke&revocationReason="+revocation_reason+"&revokeAll=(certRecordId%3D"+serial_number+")&totalRecordCount=1" + returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/doRevoke", self.ca_host+":"+str(self.ca_ssl_port)]) + api.log.debug("IPA-RA: returncode: %d" % returncode) + if (returncode == 0): + return_values["status"] = "0" + if (stdout.find('revoked = "yes"') > -1): + return_values["revoked"] = True + else: + return_values["revoked"] = False + else: + return_values["status"] = str(-returncode) + else: + return_values["status"] = "1" + return return_values + + + def take_certificate_off_hold(self, serial_number=None): + """ + Take revoked certificate off hold + :param serial_number: certificate serial number + """ + return_values = {} + self.log.debug("IPA-RA: revoke_certificate") + if serial_number is not None: + if isinstance(serial_number, int): + serial_number = str(serial_number) + request_info = "serialNumber="+serial_number + returncode, stdout, stderr = self.__run_sslget(["-e", request_info, "-r", "/ca/agent/ca/doUnrevoke", self.ca_host+":"+str(self.ca_ssl_port)]) + api.log.debug("IPA-RA: returncode: %d" % returncode) + if (returncode == 0): + if (stdout.find('unrevoked = "yes"') > -1): + return_values["taken_off_hold"] = True + else: + return_values["taken_off_hold"] = False + else: + return_values["status"] = str(-returncode) + else: + return_values["status"] = "1" + return return_values + + + def __find_substring(self, str, str1, str2): + sub_str = None + k0 = len(str) + k1 = str.find(str1) + k2 = len(str1) + if (k0 > 0 and k1 > -1 and k2 > 0 and k0 > k1 + k2): + sub_str = str[k1+k2:] + k3 = len(sub_str) + k4 = sub_str.find(str2) + if (k3 > 0 and k4 > -1 and k3 > k4): + sub_str = sub_str[:k4] + return sub_str + + + def __get_ca_location(self): + if 'ca_host' in api.env: + api.log.debug("ca_host configuration found") + if api.env.ca_host is not None: + self.ca_host = api.env.ca_host + else: + api.log.debug("ca_host configuration not found") + # if CA is not hosted with IPA on the same system and there is no configuration support for 'api.env.ca_host', then set ca_host below + # self.ca_host = "example.com" + if self.ca_host is None: + self.ca_host = gethostname() + api.log.debug("ca_host: %s" % self.ca_host) + + if 'ca_ssl_port' in api.env: + api.log.debug("ca_ssl_port configuration found") + if api.env.ca_ssl_port is not None: + self.ca_ssl_port = api.env.ca_ssl_port + else: + api.log.debug("ca_ssl_port configuration not found") + if self.ca_ssl_port is None: + self.ca_ssl_port = 9443 + api.log.debug("ca_ssl_port: %d" % self.ca_ssl_port) + + if 'ca_port' in api.env: + api.log.debug("ca_port configuration found") + if api.env.ca_port is not None: + self.ca_port = api.env.ca_port + else: + api.log.debug("ca_port configuration not found") + if self.ca_port is None: + self.ca_port = 9080 + api.log.debug("ca_port: %d" % self.ca_port) + + + def __generate_ipa_request(self): + certificate_request = None + if not os.path.isfile(self.noise_file): + self.__create_noise_file() + returncode, stdout, stderr = self.__run_certutil(["-R", "-k", "rsa", "-g", self.ipa_key_size, "-s", "CN=IPA-Subsystem-Certificate,OU=pki-ipa,O=UsersysRedhat-Domain", "-z", self.noise_file, "-a"]) + if os.path.isfile(self.noise_file): + os.unlink(self.noise_file) + if (returncode == 0): + api.log.info("IPA-RA: IPA certificate request generated") + certificate_request = self.__find_substring(stdout, "-----BEGIN NEW CERTIFICATE REQUEST-----", "-----END NEW CERTIFICATE REQUEST-----") + if certificate_request is not None: + api.log.debug("certificate_request=%s" % certificate_request) + else: + api.log.warn("IPA-RA: Error parsing certificate request." % returncode) + else: + api.log.warn("IPA-RA: Error (%d) generating IPA certificate request." % returncode) + return certificate_request + + def __request_ipa_certificate(self, certificate_request=None): + ipa_certificate = None + if certificate_request is not None: + params = urllib.urlencode({'profileId': 'caServerCert', 'cert_request_type': 'pkcs10', 'requestor_name': 'freeIPA', 'cert_request': self.__generate_ipa_request(), 'xmlOutput': 'true'}) + headers = {"Content-type": "application/x-www-form-urlencoded"} + conn = httplib.HTTPConnection(self.ca_host+":"+elf.ca_port) + conn.request("POST", "/ca/ee/ca/profileSubmit", params, headers) + response = conn.getresponse() + api.log.debug("IPA-RA: response.status: %d response.reason: '%s'" % (response.status, response.reason)) + data = response.read() + conn.close() + api.log.info("IPA-RA: IPA certificate request submitted to CA: %s" % data) + return ipa_certificate + + def __get_ca_chain(self): + headers = {"Content-type": "application/x-www-form-urlencoded"} + conn = httplib.HTTPConnection(self.ca_host+":"+elf.ca_port) + conn.request("POST", "/ca/ee/ca/getCertChain", None, headers) + response = conn.getresponse() + api.log.debug("IPA-RA: response.status: %d response.reason: '%s'" % (response.status, response.reason)) + data = response.read() + conn.close() + certificate_chain = self.__find_substring(data, "", "") + if certificate_chain is not None: + api.log.info(("IPA-RA: CA chain obtained from CA: %s" % certificate_chain)) + else: + api.log.warn("IPA-RA: Error parsing certificate chain.") + return certificate_chain + + def __import_ca_chain(self): + returncode, stdout, stderr = self.__run_certutil(["-A", "-t", "CT,C,C", "-n", self.ca_certificate_nickname, "-a"], self.__get_ca_chain()) + if (returncode == 0): + api.log.info("IPA-RA: CA chain imported to IPA's NSS DB") + else: + api.log.warn("IPA-RA: Error (%d) importing CA chain to IPA's NSS DB." % returncode) + + def __create_noise_file(self): + noise = array.array('B', os.urandom(128)) + f = open(self.noise_file, "wb") + noise.tofile(f) + f.close() + + def __create_pwd_file(self): + hex_str = binascii.hexlify(os.urandom(10)) + print "urandom: %s" % hex_str + f = os.open(self.pwd_file, os.O_CREAT | os.O_RDWR) + os.write(f, hex_str) + os.close(f) + + def __create_nss_db(self): + returncode, stdout, stderr = self.__run_certutil(["-N"]) + if (returncode == 0): + api.log.info("IPA-RA: NSS DB created") + else: + api.log.warn("IPA-RA: Error (%d) creating NSS DB." % returncode) + + """ + sslget and certutil utilities are used only till Python-NSS completion. + """ + def __run_sslget(self, args, stdin=None): + new_args = ["/usr/bin/sslget", "-d", self.sec_dir, "-w", self.pwd_file, "-n", self.ipa_certificate_nickname] + new_args = new_args + args + return self.__run(new_args, stdin) + + def __run_certutil(self, args, stdin=None): + new_args = ["/usr/bin/certutil", "-d", self.sec_dir, "-f", self.pwd_file] + new_args = new_args + args + return self.__run(new_args, stdin) + + def __run(self, args, stdin=None): + if stdin: + p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + stdout,stderr = p.communicate(stdin) + else: + p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) + stdout,stderr = p.communicate() + + api.log.debug("IPA-RA: returncode: %d args: '%s'" % (p.returncode, ' '.join(args))) + # api.log.debug("IPA-RA: stdout: '%s'" % stdout) + # api.log.debug("IPA-RA: stderr: '%s'" % stderr) + return (p.returncode, stdout, stderr) + +api.register(ra) diff --git a/ipaserver/rpc.py b/ipaserver/rpc.py new file mode 100644 index 00000000..34de915c --- /dev/null +++ b/ipaserver/rpc.py @@ -0,0 +1,60 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Execute an RPC request. +""" + +from xmlrpclib import dumps, loads, Fault +from ipalib import Backend +from ipalib.errors import HandledError, CommandError +from ipalib.rpc import xmlrpc_wrap, xmlrpc_unwrap + + +def params_2_args_options(params): + assert type(params) is tuple + if len(params) == 0: + return (tuple(), dict()) + if type(params[-1]) is dict: + return (params[:-1], params[-1]) + return (params, dict()) + + +class xmlrpc(Backend): + + def dispatch(self, method, params): + assert type(method) is str + assert type(params) is tuple + self.info('Received RPC call to %r', method) + if method not in self.Command: + raise CommandError(name=method) + (args, options) = params_2_args_options(xmlrpc_unwrap(params)) + result = self.Command[method](*args, **options) + return (xmlrpc_wrap(result),) + + def execute(self, data, ccache=None, client_ip=None, locale=None): + try: + (params, method) = loads(data) + response = self.dispatch(method, params) + except Exception, e: + if not isinstance(e, HandledError): + e = UnknownError() + assert isinstance(e, HandledError) + response = Fault(e.code, e.message) + return dumps(response) diff --git a/ipaserver/servercore.py b/ipaserver/servercore.py new file mode 100644 index 00000000..6991989e --- /dev/null +++ b/ipaserver/servercore.py @@ -0,0 +1,464 @@ +# Authors: Rob Crittenden +# +# Copyright (C) 2007 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +import ldap +import string +import re +from ipaserver.context import context +from ipaserver import ipaldap +import ipautil +from ipalib import errors +from ipalib import api + +def convert_entry(ent): + entry = dict(ent.data) + entry['dn'] = ent.dn + # For now convert single entry lists to a string for the ui. + # TODO: we need to deal with multi-values better + for key,value in entry.iteritems(): + if isinstance(value,list) or isinstance(value,tuple): + if len(value) == 0: + entry[key] = '' + elif len(value) == 1: + entry[key] = value[0] + return entry + +def convert_scalar_values(orig_dict): + """LDAP update dicts expect all values to be a list (except for dn). + This method converts single entries to a list.""" + new_dict={} + for (k,v) in orig_dict.iteritems(): + if not isinstance(v, list) and k != 'dn': + v = [v] + new_dict[k] = v + + 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 +# restricting scope. For now adding a get_base/sub_entry() +# calls, but the API isn't great. +def get_entry (base, scope, searchfilter, sattrs=None): + """Get a specific entry (with a parametized scope). + Return as a dict of values. + Multi-valued fields are represented as lists. + """ + ent="" + + ent = context.conn.getConn().getEntry(base, scope, searchfilter, sattrs) + + return convert_entry(ent) + +def get_base_entry (base, searchfilter, sattrs=None): + """Get a specific entry (with a scope of BASE). + Return as a dict of values. + Multi-valued fields are represented as lists. + """ + return get_entry(base, ldap.SCOPE_BASE, searchfilter, sattrs) + +def get_sub_entry (base, searchfilter, sattrs=None): + """Get a specific entry (with a scope of SUB). + Return as a dict of values. + Multi-valued fields are represented as lists. + """ + return get_entry(base, ldap.SCOPE_SUBTREE, searchfilter, sattrs) + +def get_one_entry (base, searchfilter, sattrs=None): + """Get the children of an entry (with a scope of ONE). + Return as a list of dict of values. + Multi-valued fields are represented as lists. + """ + return get_list(base, searchfilter, sattrs, ldap.SCOPE_ONELEVEL) + +def get_list (base, searchfilter, sattrs=None, scope=ldap.SCOPE_SUBTREE): + """Gets a list of entries. Each is converted to a dict of values. + Multi-valued fields are represented as lists. + """ + entries = [] + + entries = context.conn.getConn().getList(base, scope, searchfilter, sattrs) + + return map(convert_entry, entries) + +def has_nsaccountlock(dn): + """Check to see if an entry has the nsaccountlock attribute. + This attribute is provided by the Class of Service plugin so + doing a search isn't enough. It is provided by the two + entries cn=inactivated and cn=activated. So if the entry has + the attribute and isn't in either cn=activated or cn=inactivated + then the attribute must be in the entry itself. + + Returns True or False + """ + # First get the entry. If it doesn't have nsaccountlock at all we + # can exit early. + entry = get_entry_by_dn(dn, ['dn', 'nsaccountlock', 'memberof']) + if not entry.get('nsaccountlock'): + return False + + # Now look to see if they are in activated or inactivated + # entry is a member + memberof = entry.get('memberof') + if isinstance(memberof, basestring): + memberof = [memberof] + for m in memberof: + inactivated = m.find("cn=inactivated") + activated = m.find("cn=activated") + # if they are in either group that means that the nsaccountlock + # value comes from there, otherwise it must be in this entry. + if inactivated >= 0 or activated >= 0: + return False + + return True + +# 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=*)" + api.log.info("IPA: get_entry_by_dn '%s'" % dn) + return get_base_entry(dn, searchfilter, sattrs) + +def get_entry_by_cn (cn, sattrs): + """Get a specific entry by cn. Return as a dict of values. + Multi-valued fields are represented as lists. + """ + api.log.info("IPA: get_entry_by_cn '%s'" % cn) +# cn = self.__safe_filter(cn) + searchfilter = "(cn=%s)" % cn + return get_sub_entry("cn=accounts," + api.env.basedn, searchfilter, sattrs) + +def get_user_by_uid(uid, sattrs): + """Get a specific user's entry.""" + # FIXME: should accept a container to look in +# uid = self.__safe_filter(uid) + searchfilter = "(&(uid=%s)(objectclass=posixAccount))" % uid + + return get_sub_entry("cn=accounts," + api.env.basedn, searchfilter, sattrs) + +# User support + +def entry_exists(dn): + """Return True if the entry exists, False otherwise.""" + try: + get_base_entry(dn, "objectclass=*", ['dn','objectclass']) + return True + except errors.NotFound: + return False + +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) + api.log.info("IPA: get_user_by_uid '%s'" % uid) +# uid = self.__safe_filter(uid) + searchfilter = "(uid=" + uid + ")" + return get_sub_entry("cn=accounts," + api.env.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 + api.log.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: + api.log.debug("There was a problem " + str(e)) + pass + + return False + +def update_entry (entry): + """Update an LDAP entry + + entry is a dict + + This refreshes the record from LDAP in order to obtain the list of + attributes that has changed. + """ + 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 + # original + try: + moddn = oldentry['dn'] + except KeyError, e: + # FIXME: return a missing DN error message + raise e + + return context.conn.getConn().updateEntry(moddn, oldentry, newentry) + +def add_entry(entry): + """Add a new entry""" + return context.conn.getConn().addEntry(entry) + +def delete_entry(dn): + """Remove an entry""" + return context.conn.getConn().deleteEntry(dn) + +# FIXME, get time and search limit from cn=ipaconfig +def search(base, filter, attributes, timelimit=1, sizelimit=3000): + """Perform an LDAP query""" + try: + timelimit = float(timelimit) + results = context.conn.getConn().getListAsync(base, ldap.SCOPE_SUBTREE, + filter, attributes, 0, None, None, timelimit, sizelimit) + except ldap.NO_SUCH_OBJECT: + raise errors.NotFound + + counter = results[0] + entries = [counter] + for r in results[1:]: + entries.append(convert_entry(r)) + + return entries + +def uniq_list(x): + """Return a unique list, preserving order and ignoring case""" + 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.""" + + schema_entry = get_base_entry("", "objectclass=*", ['dn','subschemasubentry']) + schema_cn = schema_entry.get('subschemasubentry') + schema = get_base_entry(schema_cn, "objectclass=*", ['*']) + + return schema + +def get_objectclasses(): + """Returns a list of available objectclasses that the LDAP + server supports. This parses out the syntax, attributes, etc + and JUST returns a lower-case list of the names.""" + + schema = get_schema() + + objectclasses = schema.get('objectclasses') + + # Convert this list into something more readable + result = [] + for i in range(len(objectclasses)): + oc = objectclasses[i].lower().split(" ") + 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," + api.env.basedn, searchfilter) + except ldap.NO_SUCH_OBJECT, e: + # FIXME + raise errors.NotFound + + return config + +def modify_password(dn, oldpass, newpass): + return context.conn.getConn().modifyPassword(dn, oldpass, newpass) + +def mark_entry_active (dn): + """Mark an entry as active in LDAP.""" + + # This can be tricky. The entry itself can be marked inactive + # by being in the inactivated group. It can also be inactivated by + # being the member of an inactive group. + # + # First we try to remove the entry from the inactivated group. Then + # if it is still inactive we have to add it to the activated group + # which will override the group membership. + + res = "" + # First, check the entry status + entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock']) + + if entry.get('nsaccountlock', 'false').lower() == "false": + api.log.debug("IPA: already active") + raise errors.AlreadyActiveError + + if has_nsaccountlock(dn): + api.log.debug("IPA: appears to have the nsaccountlock attribute") + raise errors.HasNSAccountLock + + group = get_entry_by_cn("inactivated", None) + try: + remove_member_from_group(entry.get('dn'), group.get('dn')) + except errors.NotGroupMember: + # Perhaps the user is there as a result of group membership + pass + + # Now they aren't a member of inactivated directly, what is the status + # now? + entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock']) + + if entry.get('nsaccountlock', 'false').lower() == "false": + # great, we're done + api.log.debug("IPA: removing from inactivated did it.") + return True + + # So still inactive, add them to activated + group = get_entry_by_cn("activated", None) + res = add_member_to_group(dn, group.get('dn')) + api.log.debug("IPA: added to activated.") + + return res + +def mark_entry_inactive (dn): + """Mark an entry as inactive in LDAP.""" + + entry = get_entry_by_dn(dn, ['dn', 'nsAccountlock', 'memberOf']) + + if entry.get('nsaccountlock', 'false').lower() == "true": + api.log.debug("IPA: already marked as inactive") + raise errors.AlreadyInactiveError + + if has_nsaccountlock(dn): + api.log.debug("IPA: appears to have the nsaccountlock attribute") + raise errors.HasNSAccountLock + + # First see if they are in the activated group as this will override + # the our inactivation. + group = get_entry_by_cn("activated", None) + try: + remove_member_from_group(dn, group.get('dn')) + except errors.NotGroupMember: + # this is fine, they may not be explicitly in this group + pass + + # Now add them to inactivated + group = get_entry_by_cn("inactivated", None) + res = add_member_to_group(dn, group.get('dn')) + + return res + +def add_member_to_group(member_dn, group_dn): + """ + Add a member to an existing group. + """ + api.log.info("IPA: add_member_to_group '%s' to '%s'" % (member_dn, group_dn)) + if member_dn.lower() == group_dn.lower(): + # You can't add a group to itself + raise errors.SameGroupError + + group = get_entry_by_dn(group_dn, None) + if group is None: + raise errors.NotFound + + # check to make sure member_dn exists + member_entry = get_base_entry(member_dn, "(objectClass=*)", ['dn','objectclass']) + if not member_entry: + raise errors.NotFound + + # Add the new member to the group member attribute + members = group.get('member', []) + if isinstance(members, basestring): + members = [members] + members.append(member_dn) + group['member'] = members + + try: + return update_entry(group) + except errors.EmptyModlist: + raise + +def remove_member_from_group(member_dn, group_dn=None): + """Remove a member_dn from an existing group.""" + + group = get_entry_by_dn(group_dn, None) + if group is None: + raise errors.NotFound + """ + if group.get('cn') == "admins": + member = get_entry_by_dn(member_dn, ['dn','uid']) + if member.get('uid') == "admin": + raise ipaerror.gen_exception(ipaerror.INPUT_ADMIN_REQUIRED_IN_ADMINS) + """ + api.log.info("IPA: remove_member_from_group '%s' from '%s'" % (member_dn, group_dn)) + + members = group.get('member', False) + if not members: + raise errors.NotGroupMember + + if isinstance(members,basestring): + members = [members] + for i in range(len(members)): + members[i] = ipaldap.IPAdmin.normalizeDN(members[i]) + try: + members.remove(member_dn) + except ValueError: + # member is not in the group + # FIXME: raise more specific error? + raise errors.NotGroupMember + except Exception, e: + raise e + + group['member'] = members + + try: + return update_entry(group) + except errors.EmptyModlist: + raise diff --git a/ipaserver/test_client b/ipaserver/test_client new file mode 100755 index 00000000..3b4794d9 --- /dev/null +++ b/ipaserver/test_client @@ -0,0 +1,28 @@ +#!/usr/bin/python + +import xmlrpclib + +def user_find(uid): + try: + args=uid + result = server.user_find(args) + print "returned %s" % result + except xmlrpclib.Fault, e: + print e.faultString + +# main +server = xmlrpclib.ServerProxy("http://localhost:8888/") + +#print server.system.listMethods() +#print server.system.methodHelp("user_add") + +try: + args="jsmith1" + kw = {'givenname':'Joe', 'sn':'Smith'} + result = server.user_add(kw, args) + print "returned %s" % result +except xmlrpclib.Fault, e: + print e.faultString + +#user_find("admin") +#user_find("notfound") diff --git a/ipaserver/updates/automount.update b/ipaserver/updates/automount.update new file mode 100644 index 00000000..13d9a6df --- /dev/null +++ b/ipaserver/updates/automount.update @@ -0,0 +1,54 @@ +# +# An automount schema based on RFC 2307-bis. +# +# This schema defines new automount and automountMap objectclasses to represent +# the automount maps and their entries. +# +dn: cn=schema +add:attributeTypes: + ( 1.3.6.1.1.1.1.31 NAME 'automountMapName' + DESC 'automount Map Name' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE + X-ORIGIN 'RFC 2307bis' ) +add:attributeTypes: + ( 1.3.6.1.1.1.1.32 NAME 'automountKey' + DESC 'Automount Key value' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE + X-ORIGIN 'RFC 2307bis' ) +add:attributeTypes: + ( 1.3.6.1.1.1.1.33 NAME 'automountInformation' + DESC 'Automount information' + EQUALITY caseExactIA5Match + SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE + X-ORIGIN 'RFC 2307bis' ) +add:objectClasses: + ( 1.3.6.1.1.1.2.16 NAME 'automountMap' + DESC 'Automount Map information' SUP top + STRUCTURAL MUST automountMapName MAY description + X-ORIGIN 'RFC 2307bis' ) +add:objectClasses: + ( 1.3.6.1.1.1.2.17 NAME 'automount' + DESC 'Automount information' SUP top STRUCTURAL + MUST ( automountKey $ automountInformation ) MAY description + X-ORIGIN 'RFC 2307bis' ) + +# Add the default automount entries + +dn: cn=automount,$SUFFIX +add:objectClass: nsContainer +add:cn: automount + +dn: automountmapname=auto.master,cn=automount,$SUFFIX +add:objectClass: automountMap +add:automountMapName: auto.master + +dn: automountkey=/-,automountmapname=auto.master,cn=automount,$SUFFIX +add:objectClass: automount +add:automountKey: '/-' +add:automountInformation: auto.direct + +dn: automountmapname=auto.direct,cn=automount,$SUFFIX +add:objectClass: automountMap +add:automountMapName: auto.direct diff --git a/ipaserver/updates/groupofhosts.update b/ipaserver/updates/groupofhosts.update new file mode 100644 index 00000000..fb39c5e2 --- /dev/null +++ b/ipaserver/updates/groupofhosts.update @@ -0,0 +1,5 @@ +dn: cn=hostgroups,cn=accounts,$SUFFIX +add:objectClass: top +add:objectClass: nsContainer +add:cn: hostgroups + diff --git a/ipaserver/updates/host.update b/ipaserver/updates/host.update new file mode 100644 index 00000000..dfc9723c --- /dev/null +++ b/ipaserver/updates/host.update @@ -0,0 +1,22 @@ +# +# Schema for IPA Hosts +# +dn: cn=schema +add: attributeTypes: + ( 2.16.840.1.113730.3.8.3.10 NAME 'ipaClientVersion' + DESC 'Text string describing client version of the IPA software installed' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 + X-ORIGIN 'IPA v2' ) + +add: attributeTypes: + ( 2.16.840.1.113730.3.8.3.11 NAME 'enrolledBy' + DESC 'DN of administrator who performed manual enrollment of the host' + SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 + X-ORIGIN 'IPA v2' ) +add: objectClasses: + ( 2.16.840.1.113730.3.8.4.2 NAME 'ipaHost' + AUXILIARY + MAY ( userPassword $ ipaClientVersion $ enrolledBy) + X-ORIGIN 'IPA v2' ) + + diff --git a/tests/test_ipa_server/__init__.py b/tests/test_ipa_server/__init__.py deleted file mode 100644 index 56a6c533..00000000 --- a/tests/test_ipa_server/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -Sub-package containing unit tests for `ipaserver` package. -""" diff --git a/tests/test_ipa_server/test_rpc.py b/tests/test_ipa_server/test_rpc.py deleted file mode 100644 index 07191fda..00000000 --- a/tests/test_ipa_server/test_rpc.py +++ /dev/null @@ -1,80 +0,0 @@ -# Authors: -# Jason Gerard DeRose -# -# Copyright (C) 2008 Red Hat -# see file 'COPYING' for use and warranty information -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; version 2 only -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -""" -Test the `ipaserver.rpc` module. -""" - -from tests.util import create_test_api, raises, PluginTester -from tests.data import unicode_str -from ipalib import errors, Command -from ipaserver import rpc - - -def test_params_2_args_options(): - """ - Test the `ipaserver.rpc.params_2_args_options` function. - """ - f = rpc.params_2_args_options - args = ('Hello', u'world!') - options = dict(one=1, two=u'Two', three='Three') - assert f(tuple()) == (tuple(), dict()) - assert f(args) == (args, dict()) - assert f((options,)) == (tuple(), options) - assert f(args + (options,)) == (args, options) - assert f((options,) + args) == ((options,) + args, dict()) - - -class test_xmlrpc(PluginTester): - """ - Test the `ipaserver.rpc.xmlrpc` plugin. - """ - - _plugin = rpc.xmlrpc - - def test_dispatch(self): - """ - Test the `ipaserver.rpc.xmlrpc.dispatch` method. - """ - (o, api, home) = self.instance('Backend', in_server=True) - e = raises(errors.CommandError, o.dispatch, 'echo', tuple()) - assert str(e) == "Unknown command 'echo'" - assert e.kw['name'] == 'echo' - - class echo(Command): - takes_args = ['arg1', 'arg2+'] - takes_options = ['option1?', 'option2?'] - def execute(self, *args, **options): - assert type(args[1]) is tuple - return args + (options,) - - (o, api, home) = self.instance('Backend', echo, in_server=True) - def call(params): - response = o.dispatch('echo', params) - assert type(response) is tuple and len(response) == 1 - return response[0] - arg1 = unicode_str - arg2 = (u'Hello', unicode_str, u'world!') - options = dict(option1=u'How are you?', option2=unicode_str) - assert call((arg1, arg2, options)) == (arg1, arg2, options) - assert call((arg1,) + arg2 + (options,)) == (arg1, arg2, options) - - - def test_execute(self): - (o, api, home) = self.instance('Backend', in_server=True) diff --git a/tests/test_ipaserver/__init__.py b/tests/test_ipaserver/__init__.py new file mode 100644 index 00000000..56a6c533 --- /dev/null +++ b/tests/test_ipaserver/__init__.py @@ -0,0 +1,22 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Sub-package containing unit tests for `ipaserver` package. +""" diff --git a/tests/test_ipaserver/test_rpc.py b/tests/test_ipaserver/test_rpc.py new file mode 100644 index 00000000..07191fda --- /dev/null +++ b/tests/test_ipaserver/test_rpc.py @@ -0,0 +1,80 @@ +# Authors: +# Jason Gerard DeRose +# +# Copyright (C) 2008 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; version 2 only +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" +Test the `ipaserver.rpc` module. +""" + +from tests.util import create_test_api, raises, PluginTester +from tests.data import unicode_str +from ipalib import errors, Command +from ipaserver import rpc + + +def test_params_2_args_options(): + """ + Test the `ipaserver.rpc.params_2_args_options` function. + """ + f = rpc.params_2_args_options + args = ('Hello', u'world!') + options = dict(one=1, two=u'Two', three='Three') + assert f(tuple()) == (tuple(), dict()) + assert f(args) == (args, dict()) + assert f((options,)) == (tuple(), options) + assert f(args + (options,)) == (args, options) + assert f((options,) + args) == ((options,) + args, dict()) + + +class test_xmlrpc(PluginTester): + """ + Test the `ipaserver.rpc.xmlrpc` plugin. + """ + + _plugin = rpc.xmlrpc + + def test_dispatch(self): + """ + Test the `ipaserver.rpc.xmlrpc.dispatch` method. + """ + (o, api, home) = self.instance('Backend', in_server=True) + e = raises(errors.CommandError, o.dispatch, 'echo', tuple()) + assert str(e) == "Unknown command 'echo'" + assert e.kw['name'] == 'echo' + + class echo(Command): + takes_args = ['arg1', 'arg2+'] + takes_options = ['option1?', 'option2?'] + def execute(self, *args, **options): + assert type(args[1]) is tuple + return args + (options,) + + (o, api, home) = self.instance('Backend', echo, in_server=True) + def call(params): + response = o.dispatch('echo', params) + assert type(response) is tuple and len(response) == 1 + return response[0] + arg1 = unicode_str + arg2 = (u'Hello', unicode_str, u'world!') + options = dict(option1=u'How are you?', option2=unicode_str) + assert call((arg1, arg2, options)) == (arg1, arg2, options) + assert call((arg1,) + arg2 + (options,)) == (arg1, arg2, options) + + + def test_execute(self): + (o, api, home) = self.instance('Backend', in_server=True) -- cgit