summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Gerard DeRose <jderose@redhat.com>2008-09-12 16:36:04 +0000
committerJason Gerard DeRose <jderose@redhat.com>2008-09-12 16:36:04 +0000
commit0e60036bb4db8cf505a3f1009023a09ca2ffe0a1 (patch)
tree46507541065213acb0720d8326c0fadb7713bfec
parentc1ef2d05e881c620d3565d717cfb23029e6e9f4e (diff)
downloadfreeipa-0e60036bb4db8cf505a3f1009023a09ca2ffe0a1.tar.gz
freeipa-0e60036bb4db8cf505a3f1009023a09ca2ffe0a1.tar.xz
freeipa-0e60036bb4db8cf505a3f1009023a09ca2ffe0a1.zip
290: Applyied Rob's patch
-rw-r--r--ipalib/conn.py72
-rw-r--r--ipalib/ipaldap.py627
-rw-r--r--ipalib/ipautil.py190
-rw-r--r--ipalib/plugins/example.py9
-rw-r--r--ipalib/public.py2
-rw-r--r--ipalib/servercore.py148
-rwxr-xr-xserver/test_client16
-rwxr-xr-xserver/test_server133
8 files changed, 1195 insertions, 2 deletions
diff --git a/ipalib/conn.py b/ipalib/conn.py
new file mode 100644
index 00000000..f8f5306f
--- /dev/null
+++ b/ipalib/conn.py
@@ -0,0 +1,72 @@
+# 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 threading
+import ldap
+import ldap.dn
+from ipalib import ipaldap
+
+context = threading.local()
+
+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/ipalib/ipaldap.py b/ipalib/ipaldap.py
new file mode 100644
index 00000000..c1d134a0
--- /dev/null
+++ b/ipalib/ipaldap.py
@@ -0,0 +1,627 @@
+# 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 base64
+import urllib
+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
+import ipautil
+
+# 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):
+ """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."""
+ return self.data.items()
+
+ 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 LDIFConn(ldif.LDIFParser):
+ def __init__(
+ self,
+ input_file,
+ ignored_attr_types=None,max_entries=0,process_url_schemes=None
+ ):
+ """
+ See LDIFParser.__init__()
+
+ Additional Parameters:
+ all_records
+ List instance for storing parsed records
+ """
+ self.dndict = {} # maps dn to Entry
+ self.dnlist = [] # contains entries in order read
+ myfile = input_file
+ if isinstance(input_file,str) or isinstance(input_file,unicode):
+ myfile = open(input_file, "r")
+ ldif.LDIFParser.__init__(self,myfile,ignored_attr_types,max_entries,process_url_schemes)
+ self.parse()
+ if isinstance(input_file,str) or isinstance(input_file,unicode):
+ myfile.close()
+
+ def handle(self,dn,entry):
+ """
+ Append single record to dictionary of all records.
+ """
+ if not dn:
+ dn = ''
+ newentry = Entry((dn, entry))
+ self.dndict[IPAdmin.normalizeDN(dn)] = newentry
+ self.dnlist.append(newentry)
+
+ def get(self,dn):
+ ndn = IPAdmin.normalizeDN(dn)
+ return self.dndict.get(ndn, Entry(None))
+
+class IPAdmin(SimpleLDAPObject):
+
+ def getDseAttr(self,attrname):
+ conffile = self.confdir + '/dse.ldif'
+ dseldif = LDIFConn(conffile)
+ cnconfig = dseldif.get("cn=config")
+ if cnconfig:
+ return cnconfig.getValue(attrname)
+ return None
+
+ def __initPart2(self):
+ if self.binddn and len(self.binddn) and not hasattr(self,'sroot'):
+ try:
+ ent = self.getEntry('cn=config', ldap.SCOPE_BASE, '(objectclass=*)',
+ [ 'nsslapd-instancedir', 'nsslapd-errorlog',
+ 'nsslapd-certdir', 'nsslapd-schemadir' ])
+ self.errlog = ent.getValue('nsslapd-errorlog')
+ self.confdir = ent.getValue('nsslapd-certdir')
+ if not self.confdir:
+ self.confdir = ent.getValue('nsslapd-schemadir')
+ if self.confdir:
+ self.confdir = os.path.dirname(self.confdir)
+ ent = self.getEntry('cn=config,cn=ldbm database,cn=plugins,cn=config',
+ ldap.SCOPE_BASE, '(objectclass=*)',
+ [ 'nsslapd-directory' ])
+ self.dbdir = os.path.dirname(ent.getValue('nsslapd-directory'))
+ except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR):
+ pass # usually means
+ except ldap.LDAPError, e:
+ print "caught exception ", e
+ raise
+
+ 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 and __initPart2 - these are separated
+ out this way so that we can call them from places other than
+ instance creation e.g. when we just need to reconnect, not create a
+ new instance"""
+ 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)
+ self.__initPart2()
+
+ 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 e
+ except ldap.LDAPError, e:
+ raise e
+
+ if not obj:
+ raise ldap.NO_SUCH_OBJECT
+
+ 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 ldap.NO_SUCH_OBJECT
+
+ 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 ldap.NO_SUCH_OBJECT
+
+ 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:
+ # duplicate value
+ raise e
+ except ldap.LDAPError, e:
+ raise 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 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:
+ # FIXME: better error
+ raise SyntaxError("empty modlist")
+
+ 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:
+ # FIXME: better error
+ raise SyntaxError("mid-air collision")
+ except ldap.LDAPError, e:
+ raise 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 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.LDAPError, e:
+ raise 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 addSchema(self, attr, val):
+ dn = "cn=schema"
+ self.modify_s(dn, [(ldap.MOD_ADD, attr, val)])
+
+ def addAttr(self, *args):
+ return self.addSchema('attributeTypes', args)
+
+ def addObjClass(self, *args):
+ return self.addSchema('objectClasses', args)
+
+ ###########################
+ # Static methods start here
+ ###########################
+ 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 getfqdn(name=''):
+ return socket.getfqdn(name)
+ getfqdn = staticmethod(getfqdn)
+
+ def getdomainname(name=''):
+ fqdn = IPAdmin.getfqdn(name)
+ index = fqdn.find('.')
+ if index >= 0:
+ return fqdn[index+1:]
+ else:
+ return fqdn
+ getdomainname = staticmethod(getdomainname)
+
+ def getdefaultsuffix(name=''):
+ dm = IPAdmin.getdomainname(name)
+ if dm:
+ return "dc=" + dm.replace('.', ', dc=')
+ else:
+ return 'dc=localdomain'
+ getdefaultsuffix = staticmethod(getdefaultsuffix)
+
+ def is_a_dn(dn):
+ """Returns True if the given string is a DN, False otherwise."""
+ return (dn.find("=") > 0)
+ is_a_dn = staticmethod(is_a_dn)
+
+
+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:
+ target = re.match(r'\(.*=(.*)\)', searchfilter).group(1)
+ except:
+ target = searchfilter
+ return "%s not found" % str(target)
+ else:
+ return args[0]
diff --git a/ipalib/ipautil.py b/ipalib/ipautil.py
new file mode 100644
index 00000000..6b0e2c89
--- /dev/null
+++ b/ipalib/ipautil.py
@@ -0,0 +1,190 @@
+# 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])
diff --git a/ipalib/plugins/example.py b/ipalib/plugins/example.py
index 4c62a5de..92ef95d5 100644
--- a/ipalib/plugins/example.py
+++ b/ipalib/plugins/example.py
@@ -24,7 +24,8 @@ Some example plugins.
from ipalib import public
from ipalib import api
-
+from ipalib import servercore
+import ldap
# Hypothetical functional commands (not associated with any object):
class krbtest(public.Command):
@@ -39,8 +40,11 @@ api.register(discover)
# Register some methods for the 'user' object:
class user_add(public.Method):
'Add a new user.'
+ def execute(self, **kw):
+ return 1
api.register(user_add)
+
class user_del(public.Method):
'Delete an existing user.'
api.register(user_del)
@@ -51,6 +55,9 @@ api.register(user_mod)
class user_find(public.Method):
'Search the users.'
+ def execute(self, **kw):
+ result = servercore.get_sub_entry(servercore.basedn, "uid=%s" % kw['uid'], ["*"])
+ return result
api.register(user_find)
diff --git a/ipalib/public.py b/ipalib/public.py
index 088e65c5..31270742 100644
--- a/ipalib/public.py
+++ b/ipalib/public.py
@@ -338,7 +338,7 @@ class Command(plugable.Plugin):
kw = self.normalize(**kw)
kw.update(self.get_default(**kw))
self.validate(**kw)
- self.execute(**kw)
+ return self.execute(**kw)
def smart_option_order(self):
def get_key(option):
diff --git a/ipalib/servercore.py b/ipalib/servercore.py
new file mode 100644
index 00000000..8626c04b
--- /dev/null
+++ b/ipalib/servercore.py
@@ -0,0 +1,148 @@
+# 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 sys
+sys.path.insert(0, ".")
+sys.path.insert(0, "..")
+import ldap
+from ipalib.conn import context
+from ipalib import ipautil
+
+# temporary
+import krbV
+
+krbctx = krbV.default_context()
+realm = krbctx.default_realm
+basedn = ipautil.realm_to_suffix(realm)
+
+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
+
+
+# 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_list (base, searchfilter, sattrs=None):
+ """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, ldap.SCOPE_SUBTREE, searchfilter, sattrs)
+
+ return map(convert_entry, entries)
+
+def update_entry (oldentry, newentry):
+ """Update an LDAP entry
+
+ oldentry is a dict
+ newentry is a dict
+ """
+ oldentry = convert_scalar_values(oldentry)
+ newentry = convert_scalar_values(newentry)
+
+ # 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
+
+ res = context.conn.getConn().updateEntry(moddn, oldentry, newentry)
+ return res
+
+def uniq_list(x):
+ """Return a unique list, preserving order and ignoring case"""
+ myset = {}
+ return [set.setdefault(e.lower(),e) for e in x if e.lower() not in myset]
+
+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
diff --git a/server/test_client b/server/test_client
new file mode 100755
index 00000000..c6cb7eeb
--- /dev/null
+++ b/server/test_client
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+
+import xmlrpclib
+
+server = xmlrpclib.ServerProxy("http://localhost:8888/")
+
+print server.system.listMethods()
+#print server.system.methodHelp("user_add")
+
+user = {'givenname':'Joe', 'sn':'Smith'}
+result = server.user_add(user)
+print "returned %s" % result
+
+user = {'givenname':'Joe', 'sn':'Smith', 'uid':'admin'}
+result = server.user_find(user)
+print "returned %s" % result
diff --git a/server/test_server b/server/test_server
new file mode 100755
index 00000000..c955d87a
--- /dev/null
+++ b/server/test_server
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+
+import sys
+sys.path.insert(0, "..")
+sys.path.insert(0, ".")
+import SimpleXMLRPCServer
+import logging
+import xmlrpclib
+import re
+import threading
+import commands
+from ipalib import api, conn
+from ipalib.conn import context
+import ipalib.load_plugins
+
+PORT=8888
+
+class StoppableXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
+ """Override of TIME_WAIT"""
+ allow_reuse_address = True
+
+ def serve_forever(self):
+ self.stop = False
+ while not self.stop:
+ self.handle_request()
+
+class LoggingSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+ """Overides the default SimpleXMLRPCRequestHander to support logging.
+ Logs client IP and the XML request and response.
+ """
+
+ def parse(self, given):
+ """Convert the incoming arguments into the format IPA expects"""
+ args = []
+ kw = {}
+ for g in given:
+ kw[g] = unicode(given[g])
+ return (args, kw)
+
+ def _dispatch(self, method, params):
+ """Dispatches the XML-RPC method.
+
+ Methods beginning with an '_' are considered private and will
+ not be called.
+ """
+ if (params):
+ (args, kw) = self.parse(*params)
+ else:
+ args = []
+ kw = {}
+
+ # this is fine for our test server
+ uid = commands.getoutput('/usr/bin/id -u')
+ krbccache = "FILE:/tmp/krb5cc_" + uid
+
+ func = None
+ try:
+ # FIXME: don't hardcode host and port
+ context.conn = conn.IPAConn("localhost", 389, krbccache)
+ try:
+ # check to see if a matching function has been registered
+ func = funcs[method]
+ except KeyError:
+ raise Exception('method "%s" is not supported' % method)
+ return func(**kw)
+ finally:
+ # Clean up any per-request data and connections
+ for k in context.__dict__.keys():
+ del context.__dict__[k]
+
+ def do_POST(self):
+ clientIP, port = self.client_address
+ # Log client IP and Port
+ logger.info('Client IP: %s - Port: %s' % (clientIP, port))
+ try:
+ # get arguments
+ data = self.rfile.read(int(self.headers["content-length"]))
+
+ # unmarshal the XML data
+ params, method = xmlrpclib.loads(data)
+
+ # Log client request
+ logger.info('Client request: \n%s\n' % data)
+
+ response = self.server._marshaled_dispatch(
+ data, getattr(self, '_dispatch', None))
+
+ # Log server response
+ logger.info('Server response: \n%s\n' % response)
+ except:
+ # This should only happen if the module is buggy
+ # internal error, report as HTTP server error
+ self.send_response(500)
+ self.end_headers()
+ else:
+ # got a valid XML-RPC response
+ self.send_response(200)
+ self.send_header("Content-type", "text/xml")
+ self.send_header("Content-length", str(len(response)))
+ self.end_headers()
+ self.wfile.write(response)
+
+ # shut down the connection
+ self.wfile.flush()
+ self.connection.shutdown(1)
+
+# Set up our logger
+logger = logging.getLogger('xmlrpcserver')
+hdlr = logging.FileHandler('xmlrpcserver.log')
+formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
+hdlr.setFormatter(formatter)
+logger.addHandler(hdlr)
+logger.setLevel(logging.INFO)
+
+# Set up the server
+XMLRPCServer = StoppableXMLRPCServer(("",PORT), LoggingSimpleXMLRPCRequestHandler)
+
+XMLRPCServer.register_introspection_functions()
+
+# Get and register all the methods
+api.finalize()
+for cmd in api.Method:
+ logger.info("registering %s" % cmd)
+ XMLRPCServer.register_function(api.Method[cmd], cmd)
+
+funcs = XMLRPCServer.funcs
+
+print "Listening on port %d" % PORT
+try:
+ XMLRPCServer.serve_forever()
+except KeyboardInterrupt:
+ XMLRPCServer.server_close()
+ print "Server shutdown."