diff options
author | Jason Gerard DeRose <jderose@redhat.com> | 2009-01-04 18:44:16 -0700 |
---|---|---|
committer | Jason Gerard DeRose <jderose@redhat.com> | 2009-01-04 18:44:16 -0700 |
commit | 7442ad2e27afa7719bfd7de16ac8b0b44cb418de (patch) | |
tree | 20c4ae7a83676464df3c17d6c10274a48fd5d9e6 /ipaserver | |
parent | 6fe78a4944f11d430b724103f7d8d49c92af9b63 (diff) | |
download | freeipa-7442ad2e27afa7719bfd7de16ac8b0b44cb418de.tar.gz freeipa-7442ad2e27afa7719bfd7de16ac8b0b44cb418de.tar.xz freeipa-7442ad2e27afa7719bfd7de16ac8b0b44cb418de.zip |
Renamed ipa_server/ to ipaserver/ and tests/test_ipa_server/ to tests/test_ipaserver
Diffstat (limited to 'ipaserver')
-rw-r--r-- | ipaserver/__init__.py | 22 | ||||
-rw-r--r-- | ipaserver/conn.py | 69 | ||||
-rw-r--r-- | ipaserver/context.py | 32 | ||||
-rw-r--r-- | ipaserver/ipaldap.py | 546 | ||||
-rw-r--r-- | ipaserver/ipautil.py | 201 | ||||
-rw-r--r-- | ipaserver/mod_python_xmlrpc.py | 367 | ||||
-rw-r--r-- | ipaserver/plugins/__init__.py | 24 | ||||
-rw-r--r-- | ipaserver/plugins/b_ldap.py | 327 | ||||
-rw-r--r-- | ipaserver/plugins/b_ra.py | 406 | ||||
-rw-r--r-- | ipaserver/rpc.py | 60 | ||||
-rw-r--r-- | ipaserver/servercore.py | 464 | ||||
-rwxr-xr-x | ipaserver/test_client | 28 | ||||
-rw-r--r-- | ipaserver/updates/automount.update | 54 | ||||
-rw-r--r-- | ipaserver/updates/groupofhosts.update | 5 | ||||
-rw-r--r-- | ipaserver/updates/host.update | 22 |
15 files changed, 2627 insertions, 0 deletions
diff --git a/ipaserver/__init__.py b/ipaserver/__init__.py new file mode 100644 index 000000000..b0be96bd2 --- /dev/null +++ b/ipaserver/__init__.py @@ -0,0 +1,22 @@ +# Authors: +# Jason Gerard DeRose <jderose@redhat.com> +# +# 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 000000000..fb00ad998 --- /dev/null +++ b/ipaserver/conn.py @@ -0,0 +1,69 @@ +# Authors: Rob Crittenden <rcritten@redhat.com> +# +# 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 000000000..15dd7d908 --- /dev/null +++ b/ipaserver/context.py @@ -0,0 +1,32 @@ +# Authors: Rob Crittenden <rcritten@redhat.com> +# +# 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 000000000..19fd40efd --- /dev/null +++ b/ipaserver/ipaldap.py @@ -0,0 +1,546 @@ +# Authors: Rich Megginson <richm@redhat.com> +# Rob Crittenden <rcritten@redhat.com +# +# 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 sys +import os +import os.path +import socket +import ldif +import re +import string +import ldap +import cStringIO +import struct +import ldap.sasl +from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples +from ldap.ldapobject import SimpleLDAPObject +from ipaserver import ipautil +from ipalib import errors + +# Global variable to define SASL auth +sasl_auth = ldap.sasl.sasl({},'GSSAPI') + +class Entry: + """ + This class represents an LDAP Entry object. An LDAP entry consists of + a DN and a list of attributes. Each attribute consists of a name and + a list of values. In python-ldap, entries are returned as a list of + 2-tuples. Instance variables: + + * dn - string - the string DN of the entry + * data - CIDict - case insensitive dict of the attributes and values + """ + def __init__(self,entrydata): + """data is the raw data returned from the python-ldap result method, which is + a search result entry or a reference or None. + If creating a new empty entry, data is the string DN.""" + if entrydata: + if isinstance(entrydata,tuple): + self.dn = entrydata[0] + self.data = ipautil.CIDict(entrydata[1]) + elif isinstance(entrydata,str) or isinstance(entrydata,unicode): + self.dn = entrydata + self.data = ipautil.CIDict() + else: + self.dn = '' + self.data = ipautil.CIDict() + + def __nonzero__(self): + """This allows us to do tests like if entry: returns false if there is no data, + true otherwise""" + return self.data != None and len(self.data) > 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 000000000..6422fe5a6 --- /dev/null +++ b/ipaserver/ipautil.py @@ -0,0 +1,201 @@ +# Authors: Simo Sorce <ssorce@redhat.com> +# +# 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 <michael@stroeder.com> +# 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 000000000..9a2960f93 --- /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 <mikem@redhat.com> +# +# Authors: +# Rob Crittenden <rcritten@redhat.com> + +""" +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("<pre>" + strstream.getvalue() + "</pre>") + _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 000000000..5737dcb79 --- /dev/null +++ b/ipaserver/plugins/__init__.py @@ -0,0 +1,24 @@ +# Authors: Jason Gerard DeRose <jderose@redhat.com> +# +# 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 000000000..071bf52eb --- /dev/null +++ b/ipaserver/plugins/b_ldap.py @@ -0,0 +1,327 @@ +# Authors: +# Rob Crittenden <rcritten@redhat.com> +# Jason Gerard DeRose <jderose@redhat.com> +# +# 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 000000000..bf119b810 --- /dev/null +++ b/ipaserver/plugins/b_ra.py @@ -0,0 +1,406 @@ +# Authors: +# Andrew Wnuk <awnuk@redhat.com> +# Jason Gerard DeRose <jderose@redhat.com> +# +# 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, "<Status>", "</Status>") + if status is not None: + self.log.debug ("status=%s" % status) + return_values["status"] = status + request_id = self.__find_substring(stdout, "<Id>", "</Id>") + 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, "<serialno>", "</serialno>") + 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, "<SubjectDN>", "</SubjectDN>") + if subject is not None: + self.log.debug ("subject=%s" % subject) + return_values["subject"] = subject + certificate = self.__find_substring(stdout, "<b64>", "</b64>") + 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, "<ChainBase64>", "</ChainBase64>") + 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 000000000..34de915cb --- /dev/null +++ b/ipaserver/rpc.py @@ -0,0 +1,60 @@ +# Authors: +# Jason Gerard DeRose <jderose@redhat.com> +# +# 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 000000000..6991989e5 --- /dev/null +++ b/ipaserver/servercore.py @@ -0,0 +1,464 @@ +# Authors: Rob Crittenden <rcritten@redhat.com> +# +# 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 000000000..3b4794d95 --- /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 000000000..13d9a6df0 --- /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 000000000..fb39c5e25 --- /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 000000000..dfc9723cf --- /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' ) + + |