diff options
-rw-r--r-- | ipa-admintools/Makefile | 7 | ||||
-rw-r--r-- | ipa-admintools/src/Makefile | 10 | ||||
-rw-r--r-- | ipa-admintools/src/ipa-adduser | 80 | ||||
-rw-r--r-- | ipa-admintools/src/ipa-finduser | 58 | ||||
-rw-r--r-- | ipa-server/Makefile | 2 | ||||
-rw-r--r-- | ipa-server/ipa-web/Makefile | 9 | ||||
-rw-r--r-- | ipa-server/ipa-web/api/Makefile | 12 | ||||
-rw-r--r-- | ipa-server/ipa-web/api/funcs.py | 168 | ||||
-rw-r--r-- | ipa-server/ipa-web/api/ipa.conf | 24 | ||||
-rw-r--r-- | ipa-server/ipa-web/api/ipaxmlrpc.py | 288 | ||||
-rw-r--r-- | ipa-server/ipa-web/client/Makefile | 11 | ||||
-rw-r--r-- | ipa-server/ipa-web/client/ipaldap.py | 395 | ||||
-rw-r--r-- | ipa-server/ipa-web/client/rpcclient.py | 102 |
13 files changed, 1164 insertions, 2 deletions
diff --git a/ipa-admintools/Makefile b/ipa-admintools/Makefile index 8005c0b4..a7b276f2 100644 --- a/ipa-admintools/Makefile +++ b/ipa-admintools/Makefile @@ -1,5 +1,8 @@ -all: +all: ; install: + $(MAKE) -C src $@ -clean:
\ No newline at end of file +clean: + $(MAKE) -C src $@ + rm -f *~ diff --git a/ipa-admintools/src/Makefile b/ipa-admintools/src/Makefile new file mode 100644 index 00000000..e0fd405a --- /dev/null +++ b/ipa-admintools/src/Makefile @@ -0,0 +1,10 @@ +SBINDIR = $(DESTDIR)/usr/sbin + +all: ; + +install: + install -m 755 ipa-adduser $(SBINDIR) + install -m 755 ipa-finduser $(SBINDIR) + +clean: + rm -f *~ *.pyc diff --git a/ipa-admintools/src/ipa-adduser b/ipa-admintools/src/ipa-adduser new file mode 100644 index 00000000..94a19dba --- /dev/null +++ b/ipa-admintools/src/ipa-adduser @@ -0,0 +1,80 @@ +#! /usr/bin/python -E +# 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 +from optparse import OptionParser +import ipa +import ipa.rpcclient +import xmlrpclib + +def usage(): + print "ipa-adduser [-c|--gecos STRING] [-d|--directory STRING] [-f|--firstname STRING] [-l|--lastname STRING] user" + sys.exit(1) + +def parse_options(): + parser = OptionParser() + parser.add_option("-c", "--gecos", dest="gecos", + help="Set the GECOS field") + parser.add_option("-d", "--directory", dest="directory", + help="Set the User's home directory") + parser.add_option("-f", "--firstname", dest="gn", + help="User's first name") + parser.add_option("-l", "--lastname", dest="sn", + help="User's last name") + parser.add_option("-s", "--shell", dest="shell", + help="Set user's login shell to shell") + parser.add_option("--usage", action="store_true", + help="Program usage") + + (options, args) = parser.parse_args() + + if not options.gn or not options.sn: + usage() + + return options, args + +def main(): + user={} + (options, args) = parse_options() + + if len(args) != 1: + usage() + + user['gn'] = options.gn + user['sn'] = options.sn + user['uid'] = args[0] + if options.gecos: + user['gecos'] = options.gecos + if options.directory: + user['homedirectory'] = options.directory + if options.shell: + user['loginshell'] = options.shell + else + user['loginshell'] = "/bin/bash" + + try: + ipa.rpcclient.add_user(user) + print args[0] "successfully added" + except xmlrpclib.Fault, f: + print f.faultString + + return 0 + +main()
\ No newline at end of file diff --git a/ipa-admintools/src/ipa-finduser b/ipa-admintools/src/ipa-finduser new file mode 100644 index 00000000..928eff75 --- /dev/null +++ b/ipa-admintools/src/ipa-finduser @@ -0,0 +1,58 @@ +#! /usr/bin/python -E +# 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 +# + +from optparse import OptionParser +import ipa +import ipa.rpcclient +import ipa.ipaldap +import base64 +import sys +import xmlrpclib + +def usage(): + print "ipa-finduser <uid>" + sys.exit() + +def parse_options(): + parser = OptionParser() + + (options, args) = parser.parse_args() + + return options, args + +def main(): + user={} + (options, args) = parse_options() + + if len(args) != 1: + usage() + + try: + ent = ipa.rpcclient.get_user(args[0]) + entry = ipa.ipaldap.Entry(ent['dn']) + for e in ent: + entry.setValues(e, ent[e]) + print entry + except xmlrpclib.Fault, fault: + print fault.faultString + + return 0 + +main()
\ No newline at end of file diff --git a/ipa-server/Makefile b/ipa-server/Makefile index 2a10d892..0976df43 100644 --- a/ipa-server/Makefile +++ b/ipa-server/Makefile @@ -19,3 +19,5 @@ clean: @for subdir in $(SUBDIRS); do \ (cd $$subdir && $(MAKE) $@) || exit 1; \ done + rm -f *~ + rm -f ipaserver/*~ diff --git a/ipa-server/ipa-web/Makefile b/ipa-server/ipa-web/Makefile new file mode 100644 index 00000000..055ee9f4 --- /dev/null +++ b/ipa-server/ipa-web/Makefile @@ -0,0 +1,9 @@ +all: ; + +install: + $(MAKE) -C api $@ + $(MAKE) -C client $@ + +clean: + $(MAKE) -C api $@ + rm -f *~ diff --git a/ipa-server/ipa-web/api/Makefile b/ipa-server/ipa-web/api/Makefile new file mode 100644 index 00000000..6af262ee --- /dev/null +++ b/ipa-server/ipa-web/api/Makefile @@ -0,0 +1,12 @@ +SHAREDIR = $(DESTDIR)/usr/share/ipa +HTTPDIR = $(DESTDIR)/etc/httpd/conf.d/ + +all: ; + +install: + -mkdir -p $(SHAREDIR) + install -m 644 *.py $(SHAREDIR) + install -m 644 ipa.conf $(HTTPDIR) + +clean: + rm -f *~ *.pyc diff --git a/ipa-server/ipa-web/api/funcs.py b/ipa-server/ipa-web/api/funcs.py new file mode 100644 index 00000000..b23a40c9 --- /dev/null +++ b/ipa-server/ipa-web/api/funcs.py @@ -0,0 +1,168 @@ +# 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 ipa +import ipa.dsinstance +import ipa.ipaldap +import pdb +import string +from types import * +import xmlrpclib + +# FIXME, this needs to be auto-discovered +host = 'localhost' +port = 389 +binddn = "cn=directory manager" +bindpw = "freeipa" + +basedn = "dc=greyoak,dc=com" +scope = ldap.SCOPE_SUBTREE + +def get_user (username): + """Get a specific user's entry. Return as a dict of values. + Multi-valued fields are represented as lists. + """ + ent="" + + # FIXME: Is this the filter we want or should it be more specific? + filter = "(uid=" username ")" + try: + m1 = ipa.ipaldap.IPAdmin(host,port,binddn,bindpw) + ent = m1.getEntry(basedn, scope, filter, None) + except ldap.LDAPError, e: + raise xmlrpclib.Fault(1, e) + except ipa.ipaldap.NoSuchEntryError: + raise xmlrpclib.Fault(2, "No such user") + + # Convert to LDIF + entry = str(ent) + + # Strip off any junk + entry = entry.strip() + + # Don't need to identify binary fields and this breaks the parser so + # remove double colons + entry = entry.replace('::', ':') + specs = [spec.split(':') for spec in entry.split('\n')] + + # Convert into a dict. We need to handle multi-valued attributes as well + # so we'll convert those into lists. + user={} + for (k,v) in specs: + k = k.lower() + if user.get(k) is not None: + if isinstance(user[k],list): + user[k].append(v.strip()) + else: + first = user[k] + user[k] = [] + user[k].append(first) + user[k].append(v.strip()) + else: + user[k] = v.strip() + + return user +# return str(ent) # return as LDIF + +def add_user (user): + """Add a user in LDAP""" + dn="uid=%s,ou=users,ou=default,dc=greyoak,dc=com" % user['uid'] + entry = ipa.ipaldap.Entry(dn) + + # some required objectclasses + entry.setValues('objectClass', 'top', 'posixAccount', 'shadowAccount', 'account', 'person', 'inetOrgPerson', 'organizationalPerson', 'krbPrincipalAux', 'krbTicketPolicyAux') + + # Fill in shadow fields + entry.setValue('shadowMin', '0') + entry.setValue('shadowMax', '99999') + entry.setValue('shadowWarning', '7') + entry.setValue('shadowExpire', '-1') + entry.setValue('shadowInactive', '-1') + entry.setValue('shadowFlag', '-1') + + # FIXME: calculate shadowLastChange + + # fill in our new entry with everything sent by the user + for u in user: + entry.setValues(u, user[u]) + + try: + m1 = ipa.ipaldap.IPAdmin(host,port,binddn,bindpw) + res = m1.addEntry(entry) + return res + except ldap.ALREADY_EXISTS: + raise xmlrpclib.Fault(3, "User already exists") + return None + except ldap.LDAPError, e: + raise xmlrpclib.Fault(1, str(e)) + return None + +def get_add_schema (): + """Get the list of fields to be used when adding users in the GUI.""" + + # FIXME: this needs to be pulled from LDAP + fields = [] + + field1 = { + "name": "uid" , + "label": "Login:", + "type": "text", + "validator": "text", + "required": "true" + } + fields.append(field1) + + field1 = { + "name": "userPassword" , + "label": "Password:", + "type": "password", + "validator": "String", + "required": "true" + } + fields.append(field1) + + field1 = { + "name": "gn" , + "label": "First name:", + "type": "text", + "validator": "string", + "required": "true" + } + fields.append(field1) + + field1 = { + "name": "sn" , + "label": "Last name:", + "type": "text", + "validator": "string", + "required": "true" + } + fields.append(field1) + + field1 = { + "name": "mail" , + "label": "E-mail address:", + "type": "text", + "validator": "email", + "required": "true" + } + fields.append(field1) + + return fields diff --git a/ipa-server/ipa-web/api/ipa.conf b/ipa-server/ipa-web/api/ipa.conf new file mode 100644 index 00000000..44c4d25e --- /dev/null +++ b/ipa-server/ipa-web/api/ipa.conf @@ -0,0 +1,24 @@ +# LoadModule auth_kerb_module modules/mod_auth_kerb.so + +Alias /ipa "/usr/share/ipa/XMLRPC" + +<Directory "/usr/share/ipa"> +# AuthType Kerberos +# AuthName "Kerberos Login" +# KrbMethodNegotiate on +# KrbMethodK5Passwd off +# KrbServiceName HTTP +# KrbAuthRealms GREYOAK.COM +# Krb5KeyTab /etc/httpd/conf/ipa.keytab +# KrbSaveCredentials on +# Require valid-user + ErrorDocument 401 /errors/unauthorized.html + + SetHandler mod_python + PythonHandler ipaxmlrpc + + PythonDebug Off + + # this is pointless to use since it would just reload ipaxmlrpc.py + PythonAutoReload Off +</Directory> diff --git a/ipa-server/ipa-web/api/ipaxmlrpc.py b/ipa-server/ipa-web/api/ipaxmlrpc.py new file mode 100644 index 00000000..26cac39a --- /dev/null +++ b/ipa-server/ipa-web/api/ipaxmlrpc.py @@ -0,0 +1,288 @@ +# 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> + +import sys +import time +import traceback +import pprint +from xmlrpclib import Marshaller,loads,dumps,Fault +from mod_python import apache + +import ipa +import funcs +import string +import base64 + +# +# An override so we can base64 encode all outgoing values. +# This is set by calling: Marshaller._Marshaller__dump = xmlrpclib_dump +# +# Not currently used. +# +def xmlrpclib_escape(s, replace = string.replace): + """ + xmlrpclib only handles certain characters. Lets encode the whole + blob + """ + + return base64.encodestring(s) + +def xmlrpclib_dump(self, value, write): + """ + xmlrpclib cannot marshal instances of subclasses of built-in + types. This function overrides xmlrpclib.Marshaller.__dump so that + any value that is an instance of one of its acceptable types is + marshalled as that type. + + xmlrpclib also cannot handle invalid 7-bit control characters. See + above. + """ + + # Use our escape function + args = [self, value, write] + if isinstance(value, (str, unicode)): + args.append(xmlrpclib_escape) + + try: + # Try for an exact match first + f = self.dispatch[type(value)] + except KeyError: + # Try for an isinstance() match + for Type, f in self.dispatch.iteritems(): + if isinstance(value, Type): + f(*args) + return + raise TypeError, "cannot marshal %s objects" % type(value) + else: + f(*args) + + +class ModXMLRPCRequestHandler(object): + """Simple XML-RPC handler for mod_python environment""" + + def __init__(self): + self.funcs = {} + self.traceback = False + #introspection functions + 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): + """Dispatches an XML-RPC method from marshalled (XML) data.""" + + params, method = loads(data) + + # special case +# if method == "get_user": +# Marshaller._Marshaller__dump = xmlrpclib_dump + + 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, fault: + self.traceback = True + response = dumps(fault) + 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,opts = ipa.decode_args(*params) + + ret = func(*params,**opts) + + return 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) + funcs.append({'name': name, + 'doc': func.__doc__, + 'args': args}) + return funcs + + def _getFuncArgs(self, func): + args = [] + for x in range(0, func.func_code.co_argcount): + if x == 0 and func.func_code.co_varnames[x] == "self": + 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): + return self.funcs.keys() + + def system_methodSignature(self, method): + #it is not possible to autogenerate this data + return 'signatures not supported' + + def system_methodHelp(self, 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)) + if func.__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 + + response = self._marshaled_dispatch(req.read()) + + req.content_type = "text/xml" + req.set_content_length(len(response)) + req.write(response) + + +# +# mod_python handler +# + +def handler(req, profiling=False): + 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: + opts = req.get_options() + try: + h = ModXMLRPCRequestHandler() + h.register_function(funcs.get_user) + h.register_function(funcs.add_user) + h.register_function(funcs.get_add_schema) + h.handle_request(req) + finally: + pass + return apache.OK +diff -r 0afcf345979d ipa-server/ipa-web/client/Makefile +--- a/dev/null Thu Jan 01 00:00:00 1970 0000 + b/ipa-server/ipa-web/client/Makefile Wed Jul 19 20:17:24 2007 -0400 +PYTHONLIBDIR ?= $(shell python -c "from distutils.sysconfig import *; print get_python_lib(1)") +PACKAGEDIR ?= $(DESTDIR)/$(PYTHONLIBDIR)/ipa + +all: ; + +install: + -mkdir -p $(PACKAGEDIR) + install -m 644 *.py $(PACKAGEDIR) + +clean: + rm -f *~ *.pyc diff --git a/ipa-server/ipa-web/client/Makefile b/ipa-server/ipa-web/client/Makefile new file mode 100644 index 00000000..bc6554be --- /dev/null +++ b/ipa-server/ipa-web/client/Makefile @@ -0,0 +1,11 @@ +PYTHONLIBDIR ?= $(shell python -c "from distutils.sysconfig import *; print get_python_lib(1)") +PACKAGEDIR ?= $(DESTDIR)/$(PYTHONLIBDIR)/ipa + +all: ; + +install: + -mkdir -p $(PACKAGEDIR) + install -m 644 *.py $(PACKAGEDIR) + +clean: + rm -f *~ *.pyc
\ No newline at end of file diff --git a/ipa-server/ipa-web/client/ipaldap.py b/ipa-server/ipa-web/client/ipaldap.py new file mode 100644 index 00000000..50f88520 --- /dev/null +++ b/ipa-server/ipa-web/client/ipaldap.py @@ -0,0 +1,395 @@ +#! /usr/bin/python -E +# Authors: Rich Megginson <richm@redhat.com> +# Rob Crittenden <rcritten2redhat.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 or later +# +# 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 popen2 +import base64 +import urllib +import urllib2 +import socket +import ldif +import re +import ldap +import cStringIO +import time +import operator + +from ldap.ldapobject import SimpleLDAPObject + +class Error(Exception): pass +class InvalidArgumentError(Error): + def __init__(self,message): self.message = message + def __repr__(self): return message +class NoSuchEntryError(Error): + def __init__(self,message): self.message = message + def __repr__(self): return message + +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 = ldap.cidict.cidict(entrydata[1]) + elif isinstance(entrydata,str): + self.dn = entrydata + self.data = ldap.cidict.cidict() + else: + self.dn = '' + self.data = ldap.cidict.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': + type, 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 type, Entry(data) + elif isinstance(data,list): + return type, [Entry(x) for x in data] + else: + raise TypeError, "unknown data type %s returned by result" % type(data) + else: + return type, 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): + CFGSUFFIX = "o=NetscapeRoot" + DEFAULT_USER_ID = "nobody" + + 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' ]) + instdir = ent.getValue('nsslapd-instancedir') + self.sroot, self.inst = re.match(r'(.*)[\/]slapd-(\w)$', instdir).groups() + self.errlog = ent.getValue('nsslapd-errorlog') + except (ldap.INSUFFICIENT_ACCESS, ldap.CONNECT_ERROR, NoSuchEntryError): + pass # usually means +# print "ignored exception" + except ldap.LDAPError, e: + print "caught exception ", e + raise + + def __localinit__(self): + SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port)) + # see if binddn is a dn or a uid that we need to lookup + if self.binddn and not IPAdmin.is_a_dn(self.binddn): + self.simple_bind("","") # anon + ent = self.getEntry(IPAdmin.CFGSUFFIX, ldap.SCOPE_SUBTREE, + "(uid=%s)" % self.binddn, + ['uid']) + if ent: + self.binddn = ent.dn + else: + print "Error: could not find %s under %s" % (self.binddn, IPAdmin.CFGSUFFIX) + self.simple_bind(self.binddn,self.bindpw) +# self.__initPart2() + + def __init__(self,host,port,binddn,bindpw): + """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 + using the start command, we just need to reconnect, not create a new instance""" + self.__wrapmethods() + self.port = port or 389 + self.sslport = 0 + self.host = host + self.binddn = binddn + self.bindpw = bindpw + # see if is local or not + host1 = IPAdmin.getfqdn(host) + host2 = IPAdmin.getfqdn() + self.isLocal = (host1 == host2) + self.suffixes = {} + self.__localinit__() + + def __str__(self): + return self.host ":" str(self.port) + + def toLDAPURL(self): + return "ldap://%s:%d/" % (self.host,self.port) + + def getEntry(self,*args): + """This wraps the search function. It is common to just get one entry""" + res = self.search(*args) + type, obj = self.result(res) + if not obj: + raise NoSuchEntryError("no such entry for " str(args)) + elif isinstance(obj,Entry): + return obj + else: # assume list/tuple + return obj[0] + + 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""" + try: + self.add_s(*args) + except ldap.ALREADY_EXISTS: + raise ldap.ALREADY_EXISTS + except ldap.LDAPError, e: + raise e + return "Success" + + 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 exportLDIF(self, file, suffix, forrepl=False, verbose=False): + cn = "export" str(int(time.time())) + dn = "cn=%s, cn=export, cn=tasks, cn=config" % cn + entry = Entry(dn) + entry.setValues('objectclass', 'top', 'extensibleObject') + entry.setValues('cn', cn) + entry.setValues('nsFilename', file) + entry.setValues('nsIncludeSuffix', suffix) + if forrepl: + entry.setValues('nsExportReplica', 'true') + + rc = self.startTaskAndWait(entry, verbose) + + if rc: + if verbose: + print "Error: export task %s for file %s exited with %d" % (cn,file,rc) + else: + if verbose: + print "Export task %s for file %s completed successfully" % (cn,file) + return rc + + def waitForEntry(self, dn, timeout=7200, attr='', quiet=False): + scope = ldap.SCOPE_BASE + filter = "(objectclass=*)" + attrlist = [] + if attr: + filter = "(%s=*)" % attr + attrlist.append(attr) + timeout = int(time.time()) + + if isinstance(dn,Entry): + dn = dn.dn + + # wait for entry and/or attr to show up + if not quiet: + sys.stdout.write("Waiting for %s %s:%s " % (self,dn,attr)) + sys.stdout.flush() + entry = None + while not entry and int(time.time()) < timeout: + try: + entry = self.getEntry(dn, scope, filter, attrlist) + except NoSuchEntryError: pass # found entry, but no attr + except ldap.NO_SUCH_OBJECT: pass # no entry yet + except ldap.LDAPError, e: # badness + print "\nError reading entry", dn, e + break + if not entry: + if not quiet: + sys.stdout.write(".") + sys.stdout.flush() + time.sleep(1) + + if not entry and int(time.time()) > timeout: + print "\nwaitForEntry timeout for %s for %s" % (self,dn) + elif entry and not quiet: + print "\nThe waited for entry is:", entry + else: + print "\nError: could not read entry %s from %s" % (dn,self) + + return entry + + 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[index1:] + 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 getnewhost(args): + """One of the arguments to createInstance is newhost. If this is specified, we need + to convert it to the fqdn. If not given, we need to figure out what the fqdn of the + local host is. This method sets newhost in args to the appropriate value and + returns True if newhost is the localhost, False otherwise""" + isLocal = False + if args.has_key('newhost'): + args['newhost'] = IPAdmin.getfqdn(args['newhost']) + myhost = IPAdmin.getfqdn() + if myhost == args['newhost']: + isLocal = True + elif args['newhost'] == 'localhost' or \ + args['newhost'] == 'localhost.localdomain': + isLocal = True + else: + isLocal = True + args['newhost'] = IPAdmin.getfqdn() + return isLocal + getnewhost = staticmethod(getnewhost) + + 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) diff --git a/ipa-server/ipa-web/client/rpcclient.py b/ipa-server/ipa-web/client/rpcclient.py new file mode 100644 index 00000000..41602662 --- /dev/null +++ b/ipa-server/ipa-web/client/rpcclient.py @@ -0,0 +1,102 @@ +#! /usr/bin/python -E +# 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 or later +# +# 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 +# + +#!/usr/bin/python + +try: + import krbV +except ImportError: + pass +import xmlrpclib +import socket +import os +import base64 + +# Some errors to catch +# http://cvs.fedora.redhat.com/viewcvs/ldapserver/ldap/servers/plugins/pam_passthru/README?root=dirsec&rev=1.6&view=auto + +# FIXME: do we want this set somewhere else? +server = xmlrpclib.ServerProxy("http://localhost:80/ipa") + +def get_user(username): + """Get a specific user""" + + try: + result = server.get_user(username) + myuser = result + except xmlrpclib.Fault, fault: + raise xmlrpclib.Fault(fault.faultCode, fault.faultString) + return None + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + return None + + return myuser + +def add_user(user): + """Add a new user""" + + # FIXME: Get the realm from somewhere + realm="GREYOAK.COM" + + # FIXME: This should be dynamic and can include just about anything + # Let us add in some missing attributes + if user.get('homeDirectory') is None: + user['homeDirectory'] ='/home/%s' % user['uid'] + if user.get('gecos') is None: + user['gecos'] = user['uid'] + + # FIXME: This can be removed once the DS plugin is installed + user['uidNumber'] ='501' + + # FIXME: What is the default group for users? + user['gidNumber'] ='501' + user['krbPrincipalName'] = "%s@%s" % (user['uid'], realm) + user['cn'] = "%s %s" % (user['gn'], user['sn']) + if user.get('gn'): + del user['gn'] + + try: + result = server.add_user(user) + return result + except xmlrpclib.Fault, fault: + raise xmlrpclib.Fault(fault.faultCode, fault.faultString) + return None + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + return None + +def get_add_schema(): + """Get the list of attributes we need to ask when adding a new + user. + """ + + # FIXME: Hardcoded and designed for the TurboGears GUI. Do we want + # this for the CLI as well? + try: + result = server.get_add_schema() + except xmlrpclib.Fault, fault: + raise xmlrpclib.Fault(fault,faultCode, fault.faultString) + return None + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + return None + + return result |