summaryrefslogtreecommitdiffstats
path: root/ipaserver
diff options
context:
space:
mode:
Diffstat (limited to 'ipaserver')
-rw-r--r--ipaserver/__init__.py22
-rw-r--r--ipaserver/conn.py69
-rw-r--r--ipaserver/context.py32
-rw-r--r--ipaserver/ipaldap.py546
-rw-r--r--ipaserver/ipautil.py201
-rw-r--r--ipaserver/mod_python_xmlrpc.py367
-rw-r--r--ipaserver/plugins/__init__.py24
-rw-r--r--ipaserver/plugins/b_ldap.py327
-rw-r--r--ipaserver/plugins/b_ra.py406
-rw-r--r--ipaserver/rpc.py60
-rw-r--r--ipaserver/servercore.py464
-rwxr-xr-xipaserver/test_client28
-rw-r--r--ipaserver/updates/automount.update54
-rw-r--r--ipaserver/updates/groupofhosts.update5
-rw-r--r--ipaserver/updates/host.update22
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' )
+
+