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 --- 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 ++ 15 files changed, 2627 insertions(+) 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 (limited to 'ipaserver') 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' ) + + -- cgit From 5e6ea11178f3a784c9cd589e958ef752890f8a21 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 8 Jan 2009 15:35:01 -0700 Subject: Fixed ldap and ra plugin 'name'e' problem --- ipaserver/plugins/b_ldap.py | 1 + ipaserver/plugins/b_ra.py | 1 + 2 files changed, 2 insertions(+) (limited to 'ipaserver') diff --git a/ipaserver/plugins/b_ldap.py b/ipaserver/plugins/b_ldap.py index 071bf52e..2d6ad625 100644 --- a/ipaserver/plugins/b_ldap.py +++ b/ipaserver/plugins/b_ldap.py @@ -50,6 +50,7 @@ class ldap(CrudBackend): def __init__(self): self.dn = _ldap.dn + super(ldap, self).__init__() def make_user_dn(self, uid): """ diff --git a/ipaserver/plugins/b_ra.py b/ipaserver/plugins/b_ra.py index bf119b81..544b163c 100644 --- a/ipaserver/plugins/b_ra.py +++ b/ipaserver/plugins/b_ra.py @@ -67,6 +67,7 @@ class ra(Backend): self.__create_nss_db() self.__import_ca_chain() self.__request_ipa_certificate(self.__generate_ipa_request()) + super(ra, self).__init__() def check_request_status(self, request_id=None): -- cgit From 1d1a44bd703073ef735b51cb7fb4fc0dbd92a80f Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Mon, 12 Jan 2009 19:31:42 +0100 Subject: Fix typo in b_ra: elf.ca_port -> self.ca_port --- ipaserver/plugins/b_ra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ipaserver') diff --git a/ipaserver/plugins/b_ra.py b/ipaserver/plugins/b_ra.py index 544b163c..e6a9b63f 100644 --- a/ipaserver/plugins/b_ra.py +++ b/ipaserver/plugins/b_ra.py @@ -327,7 +327,7 @@ class ra(Backend): 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 = httplib.HTTPConnection(self.ca_host+":"+self.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)) @@ -338,7 +338,7 @@ class ra(Backend): def __get_ca_chain(self): headers = {"Content-type": "application/x-www-form-urlencoded"} - conn = httplib.HTTPConnection(self.ca_host+":"+elf.ca_port) + conn = httplib.HTTPConnection(self.ca_host+":"+self.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)) -- cgit From 6be5e4a0a55c1ba048444430afc0e01b3048d8b9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Thu, 15 Jan 2009 23:52:50 -0700 Subject: ipalib.rpc: now using allow_none=True after conversation with Rob; added xml_dumps() and xml_loads() functions; some name cleanup --- ipaserver/rpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ipaserver') diff --git a/ipaserver/rpc.py b/ipaserver/rpc.py index 34de915c..c2cb0bd5 100644 --- a/ipaserver/rpc.py +++ b/ipaserver/rpc.py @@ -24,7 +24,7 @@ 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 +from ipalib.rpc import xml_wrap, xml_unwrap def params_2_args_options(params): @@ -44,9 +44,9 @@ class xmlrpc(Backend): 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)) + (args, options) = params_2_args_options(xml_unwrap(params)) result = self.Command[method](*args, **options) - return (xmlrpc_wrap(result),) + return (xml_wrap(result),) def execute(self, data, ccache=None, client_ip=None, locale=None): try: -- cgit From a04857a23947d5a520204480ed344e69d0c8cf6a Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 16 Jan 2009 00:00:15 -0700 Subject: Renamed ipaserver.rpc to ipaserver.rpcserver --- ipaserver/rpc.py | 60 -------------------------------------------------- ipaserver/rpcserver.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 60 deletions(-) delete mode 100644 ipaserver/rpc.py create mode 100644 ipaserver/rpcserver.py (limited to 'ipaserver') diff --git a/ipaserver/rpc.py b/ipaserver/rpc.py deleted file mode 100644 index c2cb0bd5..00000000 --- a/ipaserver/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 xml_wrap, xml_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(xml_unwrap(params)) - result = self.Command[method](*args, **options) - return (xml_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/rpcserver.py b/ipaserver/rpcserver.py new file mode 100644 index 00000000..c2cb0bd5 --- /dev/null +++ b/ipaserver/rpcserver.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 xml_wrap, xml_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(xml_unwrap(params)) + result = self.Command[method](*args, **options) + return (xml_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) -- cgit From f2e479c33e84367f29d5063dc00c80c49f25f3c9 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 16 Jan 2009 01:47:03 -0700 Subject: rpcserver now uses xml_dumps() and xml_loads() functions --- ipaserver/rpcserver.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) (limited to 'ipaserver') diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index c2cb0bd5..55d6f216 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -18,13 +18,13 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ -Execute an RPC request. +RPC server. """ -from xmlrpclib import dumps, loads, Fault +from xmlrpclib import Fault from ipalib import Backend -from ipalib.errors import HandledError, CommandError -from ipalib.rpc import xml_wrap, xml_unwrap +from ipalib.errors2 import PublicError, InternalError, CommandError +from ipalib.rpc import xml_dumps, xml_loads def params_2_args_options(params): @@ -36,25 +36,28 @@ def params_2_args_options(params): return (params, dict()) -class xmlrpc(Backend): +class xmlserver(Backend): + """ + Execution backend for XML-RPC server. + """ def dispatch(self, method, params): assert type(method) is str assert type(params) is tuple - self.info('Received RPC call to %r', method) + self.debug('Received RPC call to %r', method) if method not in self.Command: raise CommandError(name=method) - (args, options) = params_2_args_options(xml_unwrap(params)) + (args, options) = params_2_args_options(params) result = self.Command[method](*args, **options) - return (xml_wrap(result),) + return (result,) # Must wrap XML-RPC response in a tuple singleton - def execute(self, data, ccache=None, client_ip=None, locale=None): + def execute(self, data, ccache=None, client_ip=None, languages=None): try: - (params, method) = loads(data) + (params, method) = xml_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) + if not isinstance(e, PublicError): + e = InternalError() + assert isinstance(e, PublicError) + response = Fault(e.errno, e.strerror) return dumps(response) -- cgit From 462bac3c13de92511085b080ee5b9999f275a1e3 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 16 Jan 2009 01:56:39 -0700 Subject: Added docstring cross-references between rpc and rpcserver modules --- ipaserver/rpcserver.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'ipaserver') diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 55d6f216..d7f2ee1a 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -19,6 +19,8 @@ """ RPC server. + +Also see the `ipalib.rpc` module. """ from xmlrpclib import Fault -- cgit From 7514f96173575c42c2f5592034211d19ff711a02 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Fri, 16 Jan 2009 11:07:21 -0700 Subject: New Param: fixed metavar bug in cli.py --- ipaserver/rpcserver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'ipaserver') diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index d7f2ee1a..8b16eda9 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -53,7 +53,11 @@ class xmlserver(Backend): result = self.Command[method](*args, **options) return (result,) # Must wrap XML-RPC response in a tuple singleton - def execute(self, data, ccache=None, client_ip=None, languages=None): + def execute(self, data, ccache=None, client_version=None, + client_ip=None, languages=None): + """ + Execute the XML-RPC request in contained in ``data``. + """ try: (params, method) = xml_loads(data) response = self.dispatch(method, params) -- cgit From e4b9be209ef6349966cf1aeaff0f2438cee2e9a9 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 10:20:23 -0500 Subject: Make the membership attribute an argument and add new method entry.delAttr() We need a way to say "this attribute is blank, delete it." delAttr does this. There are now several attributes to which we add "members" to so make the attribute for storing members configurable, defaulting to 'member' --- ipaserver/plugins/b_ldap.py | 16 +++++++++++----- ipaserver/servercore.py | 21 ++++++++++++--------- 2 files changed, 23 insertions(+), 14 deletions(-) (limited to 'ipaserver') diff --git a/ipaserver/plugins/b_ldap.py b/ipaserver/plugins/b_ldap.py index 2d6ad625..9e06ce51 100644 --- a/ipaserver/plugins/b_ldap.py +++ b/ipaserver/plugins/b_ldap.py @@ -190,23 +190,23 @@ class ldap(CrudBackend): 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): + def add_member_to_group(self, memberdn, groupdn, memberattr='member'): """ 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) + return servercore.add_member_to_group(memberdn, groupdn, memberattr) - def remove_member_from_group(self, memberdn, groupdn): + def remove_member_from_group(self, memberdn, groupdn, memberattr='member'): """ 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) + return servercore.remove_member_from_group(memberdn, groupdn, memberattr) # The CRUD operations @@ -227,6 +227,7 @@ class ldap(CrudBackend): else: assert type(value) in (str, unicode, bool, int, float) yield (key, value) + yield (key, value) def create(self, **kw): if servercore.entry_exists(kw['dn']): @@ -251,13 +252,18 @@ class ldap(CrudBackend): def update(self, dn, **kw): result = self.retrieve(dn, ["*"]) + start_keys = kw.keys() 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()) + remove_keys = list(set(start_keys) - set(kw.keys())) + for k in remove_keys: + entry.delAttr(k) + + servercore.update_entry(entry.toDict(), remove_keys) return self.retrieve(dn) diff --git a/ipaserver/servercore.py b/ipaserver/servercore.py index 6991989e..36201340 100644 --- a/ipaserver/servercore.py +++ b/ipaserver/servercore.py @@ -227,16 +227,19 @@ def uid_too_long(uid): return False -def update_entry (entry): +def update_entry (entry, remove_keys=[]): """Update an LDAP entry entry is a dict + remove_keys is a list of attributes to remove from this entry This refreshes the record from LDAP in order to obtain the list of - attributes that has changed. + attributes that has changed. It only retrieves the attributes that + are in the update so attributes aren't inadvertantly lost. """ + assert type(remove_keys) is list attrs = entry.keys() - o = get_base_entry(entry['dn'], "objectclass=*", attrs) + o = get_base_entry(entry['dn'], "objectclass=*", attrs + remove_keys) oldentry = convert_scalar_values(o) newentry = convert_scalar_values(entry) @@ -395,7 +398,7 @@ def mark_entry_inactive (dn): return res -def add_member_to_group(member_dn, group_dn): +def add_member_to_group(member_dn, group_dn, memberattr='member'): """ Add a member to an existing group. """ @@ -414,18 +417,18 @@ def add_member_to_group(member_dn, group_dn): raise errors.NotFound # Add the new member to the group member attribute - members = group.get('member', []) + members = group.get(memberattr, []) if isinstance(members, basestring): members = [members] members.append(member_dn) - group['member'] = members + group[memberattr] = members try: return update_entry(group) except errors.EmptyModlist: raise -def remove_member_from_group(member_dn, group_dn=None): +def remove_member_from_group(member_dn, group_dn, memberattr='member'): """Remove a member_dn from an existing group.""" group = get_entry_by_dn(group_dn, None) @@ -439,7 +442,7 @@ def remove_member_from_group(member_dn, group_dn=None): """ api.log.info("IPA: remove_member_from_group '%s' from '%s'" % (member_dn, group_dn)) - members = group.get('member', False) + members = group.get(memberattr, False) if not members: raise errors.NotGroupMember @@ -456,7 +459,7 @@ def remove_member_from_group(member_dn, group_dn=None): except Exception, e: raise e - group['member'] = members + group[memberattr] = members try: return update_entry(group) -- cgit From 785851bf66bc6f044b520c6830aab1ce250fc1c4 Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 10:24:32 -0500 Subject: Add new method, delAttr(), to completely remove an attribute Fix some errors that weren't being raised properly --- ipaserver/ipaldap.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'ipaserver') diff --git a/ipaserver/ipaldap.py b/ipaserver/ipaldap.py index 19fd40ef..4a2e4e31 100644 --- a/ipaserver/ipaldap.py +++ b/ipaserver/ipaldap.py @@ -111,6 +111,13 @@ class Entry: setValues = setValue + def delAttr(self, name): + """ + Entirely remove an attribute of this entry. + """ + if self.hasAttr(name): + del self.data[name] + 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 @@ -375,7 +382,7 @@ class IPAdmin(SimpleLDAPObject): except ldap.ALREADY_EXISTS, e: raise errors.DuplicateEntry, "Entry already exists" except ldap.LDAPError, e: - raise DatabaseError, e + raise errors.DatabaseError, e return True def updateRDN(self, dn, newrdn): @@ -392,7 +399,7 @@ class IPAdmin(SimpleLDAPObject): self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) self.modrdn_s(dn, newrdn, delold=1) except ldap.LDAPError, e: - raise DatabaseError, e + raise errors.DatabaseError, e return True def updateEntry(self,dn,oldentry,newentry): @@ -474,7 +481,7 @@ class IPAdmin(SimpleLDAPObject): self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl) self.modify_s(dn, modlist) except ldap.LDAPError, e: - raise DatabaseError, e + raise errors.DatabaseError, e return True def deleteEntry(self,*args): -- cgit From c892051006ed27de8833822431b1f9adc976942c Mon Sep 17 00:00:00 2001 From: Rob Crittenden Date: Fri, 16 Jan 2009 10:25:21 -0500 Subject: Add the pkiUser objectclass to hosts --- ipaserver/updates/host.update | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'ipaserver') diff --git a/ipaserver/updates/host.update b/ipaserver/updates/host.update index dfc9723c..f5ecda5a 100644 --- a/ipaserver/updates/host.update +++ b/ipaserver/updates/host.update @@ -18,5 +18,8 @@ add: objectClasses: AUXILIARY MAY ( userPassword $ ipaClientVersion $ enrolledBy) X-ORIGIN 'IPA v2' ) - - +add: objectClasses: + ( 2.5.6.21 NAME 'pkiUser' + SUP top AUXILIARY + MAY ( userCertificate ) + X-ORIGIN 'RFC 2587' ) -- cgit From 55fba5420d8ea57931937728102094492ca73d86 Mon Sep 17 00:00:00 2001 From: Jason Gerard DeRose Date: Mon, 19 Jan 2009 21:10:42 -0700 Subject: Added rpc.xmlclient backend plugin for forwarding; added corresponding unit tests --- ipaserver/rpcserver.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'ipaserver') diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 8b16eda9..22517367 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -44,8 +44,6 @@ class xmlserver(Backend): """ def dispatch(self, method, params): - assert type(method) is str - assert type(params) is tuple self.debug('Received RPC call to %r', method) if method not in self.Command: raise CommandError(name=method) -- cgit