diff options
author | Simo Sorce <ssorce@redhat.com> | 2007-08-09 14:52:54 -0400 |
---|---|---|
committer | Simo Sorce <ssorce@redhat.com> | 2007-08-09 14:52:54 -0400 |
commit | a59c36b394dba5f8b371c1a6cdab0317a5507486 (patch) | |
tree | 91cbc96560da1e0aebe3b0dda64c8b3191495a0b /ipa-python | |
parent | 950bddf2a32b765ee97df5442a1ec9c0a358ccb3 (diff) | |
parent | cf93b1bc642a843242cc9aaf585c08c5475ad632 (diff) | |
download | freeipa.git-a59c36b394dba5f8b371c1a6cdab0317a5507486.tar.gz freeipa.git-a59c36b394dba5f8b371c1a6cdab0317a5507486.tar.xz freeipa.git-a59c36b394dba5f8b371c1a6cdab0317a5507486.zip |
merge in changes from upstream
Diffstat (limited to 'ipa-python')
-rw-r--r-- | ipa-python/Makefile | 8 | ||||
-rw-r--r-- | ipa-python/config.py | 106 | ||||
-rwxr-xr-x | ipa-python/freeipa-python.spec | 12 | ||||
-rwxr-xr-x | ipa-python/freeipa-python.spec.in | 12 | ||||
-rw-r--r-- | ipa-python/ipa.conf | 3 | ||||
-rw-r--r-- | ipa-python/ipaclient.py | 87 | ||||
-rw-r--r-- | ipa-python/krbtransport.py | 55 | ||||
-rw-r--r-- | ipa-python/rpcclient.py | 146 | ||||
-rw-r--r-- | ipa-python/user.py | 112 |
9 files changed, 474 insertions, 67 deletions
diff --git a/ipa-python/Makefile b/ipa-python/Makefile index bc6554be..b2e4660f 100644 --- a/ipa-python/Makefile +++ b/ipa-python/Makefile @@ -1,11 +1,17 @@ PYTHONLIBDIR ?= $(shell python -c "from distutils.sysconfig import *; print get_python_lib(1)") PACKAGEDIR ?= $(DESTDIR)/$(PYTHONLIBDIR)/ipa +CONFIGDIR ?= $(DESTDIR)/etc/ipa all: ; install: -mkdir -p $(PACKAGEDIR) install -m 644 *.py $(PACKAGEDIR) + -mkdir -p $(CONFIGDIR) + if ! [ -e $(CONFIGDIR)/ipa.conf ]; then \ + install -m 644 ipa.conf $(CONFIGDIR); \ + fi clean: - rm -f *~ *.pyc
\ No newline at end of file + rm -f *~ *.pyc + diff --git a/ipa-python/config.py b/ipa-python/config.py new file mode 100644 index 00000000..a17e585b --- /dev/null +++ b/ipa-python/config.py @@ -0,0 +1,106 @@ +# Authors: Karl MacMillan <kmacmill@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 +# + +import ConfigParser +from optparse import OptionParser + +class IPAConfigError(Exception): + def __init__(self, msg=''): + self.msg = msg + Exception.__init__(self, msg) + + def __repr__(self): + return self.msg + + __str__ = __repr__ + +class IPAConfig: + def __init__(self): + self.default_realm = None + self.default_server = None + + def get_realm(self): + if self.default_realm: + return self.default_realm + else: + raise IPAConfigError("no default realm") + + def get_server(self): + if self.default_server: + return self.default_server + else: + raise IPAConfigError("no default server") + +# Global library config +config = IPAConfig() + +def __parse_config(): + p = ConfigParser.SafeConfigParser() + p.read("/etc/ipa/ipa.conf") + + try: + config.default_realm = p.get("defaults", "realm") + config.default_server = p.get("defaults", "server") + except: + pass + +def usage(): + return """ --realm\tset the IPA realm + --server\tset the IPA server +""" + +def __parse_args(args): + # Can't use option parser because it doesn't easily leave + # unknown arguments - creating our own seems simpler. + # + # should make this more robust and handle --realm=foo syntax + out_args = [] + i = 0 + while i < len(args): + if args[i] == "--realm": + if i == len(args) - 1: + raise IPAConfigError("missing argument to --realm") + config.default_realm = args[i + 1] + i = i + 2 + continue + if args[i] == "--server": + if i == len(args) - 1: + raise IPAConfigError("missing argument to --server") + config.default_server = args[i + 1] + i = i + 2 + continue + out_args.append(args[i]) + i = i + 1 + + return out_args + + +def init_config(args=None): + __parse_config() + out_args = None + if args: + out_args = __parse_args(args) + + if not config.default_realm: + raise IPAConfigError("realm not specified in config file or on command line") + if not config.default_server: + raise IPAConfigError("server not specified in config file or on command line") + + if out_args: + return out_args diff --git a/ipa-python/freeipa-python.spec b/ipa-python/freeipa-python.spec index 61be3a5d..e9b1e708 100755 --- a/ipa-python/freeipa-python.spec +++ b/ipa-python/freeipa-python.spec @@ -1,6 +1,6 @@ Name: freeipa-python Version: 0.1.0 -Release: 1%{?dist} +Release: 3%{?dist} Summary: FreeIPA authentication server Group: System Environment/Base @@ -15,6 +15,7 @@ Requires: python %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} %define pkgpythondir %{python_sitelib}/ipa +%define configdir /etc/ipa %description FreeIPA is a server for identity, policy, and audit. @@ -25,6 +26,7 @@ FreeIPA is a server for identity, policy, and audit. %install rm -rf %{buildroot} mkdir -p %{buildroot}%{pkgpythondir} +mkdir -p %{buildroot}%{configdir} make install DESTDIR=%{buildroot} @@ -36,9 +38,17 @@ rm -rf %{buildroot} %files %defattr(-,root,root,-) %{pkgpythondir}/* +%config(noreplace) %{configdir}/ipa.conf %changelog +* Mon Aug 5 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-3 +- Abstracted client class to work directly or over RPC + +* Wed Aug 1 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-2 +- Add User class +- Add kerberos authentication to the XML-RPC request made from tools. + * Fri Jul 27 2007 Karl MacMillan <kmacmill@localhost.localdomain> - 0.1.0-1 - Initial rpm version diff --git a/ipa-python/freeipa-python.spec.in b/ipa-python/freeipa-python.spec.in index 90a135b4..b0a37308 100755 --- a/ipa-python/freeipa-python.spec.in +++ b/ipa-python/freeipa-python.spec.in @@ -1,6 +1,6 @@ Name: freeipa-python Version: VERSION -Release: 1%{?dist} +Release: 3%{?dist} Summary: FreeIPA authentication server Group: System Environment/Base @@ -15,6 +15,7 @@ Requires: python %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} %define pkgpythondir %{python_sitelib}/ipa +%define configdir /etc/ipa %description FreeIPA is a server for identity, policy, and audit. @@ -25,6 +26,7 @@ FreeIPA is a server for identity, policy, and audit. %install rm -rf %{buildroot} mkdir -p %{buildroot}%{pkgpythondir} +mkdir -p %{buildroot}%{configdir} make install DESTDIR=%{buildroot} @@ -36,9 +38,17 @@ rm -rf %{buildroot} %files %defattr(-,root,root,-) %{pkgpythondir}/* +%config(noreplace) %{configdir}/ipa.conf %changelog +* Mon Aug 5 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-3 +- Abstracted client class to work directly or over RPC + +* Wed Aug 1 2007 Rob Crittenden <rcritten@redhat.com> - 0.1.0-2 +- Add User class +- Add kerberos authentication to the XML-RPC request made from tools. + * Fri Jul 27 2007 Karl MacMillan <kmacmill@localhost.localdomain> - 0.1.0-1 - Initial rpm version diff --git a/ipa-python/ipa.conf b/ipa-python/ipa.conf new file mode 100644 index 00000000..5243cf22 --- /dev/null +++ b/ipa-python/ipa.conf @@ -0,0 +1,3 @@ +[defaults] +realm = foo.bar +server = realm.foo.bar diff --git a/ipa-python/ipaclient.py b/ipa-python/ipaclient.py new file mode 100644 index 00000000..c75b5bc9 --- /dev/null +++ b/ipa-python/ipaclient.py @@ -0,0 +1,87 @@ +#! /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 + +import sys +sys.path.append("/usr/share/ipa") + +from ipaserver import funcs +import ipa.rpcclient as rpcclient +import user +import ipa +import config + +class IPAClient: + + def __init__(self,local=None): + self.local = local + ipa.config.init_config() + if local: + self.transport = funcs.IPAServer() + # client needs to call set_principal(user@REALM) + else: + self.transport = rpcclient.RPCClient() + + def set_principal(self,princ): + if self.local: + self.transport.set_principal(princ) + + def get_user(self,uid): + result = self.transport.get_user(uid) + return user.User(result) + + def add_user(self,user): + + realm = config.config.get_realm() + + # 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['givenName'], user['sn']) + if user.get('gn'): + del user['gn'] + + result = self.transport.add_user(user) + return result + + def get_all_users(self): + result = self.transport.get_all_users() + + all_users = [] + for (attrs) in result: + if attrs is not None: + all_users.append(user.User(attrs)) + + return all_users + + def get_add_schema(self): + result = self.transport.get_add_schema() + return result diff --git a/ipa-python/krbtransport.py b/ipa-python/krbtransport.py new file mode 100644 index 00000000..dbb8ec34 --- /dev/null +++ b/ipa-python/krbtransport.py @@ -0,0 +1,55 @@ +#! /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 + +import httplib +import xmlrpclib +import kerberos +from kerberos import GSSError + +class KerbTransport(xmlrpclib.Transport): + """Handles Kerberos Negotiation authentication to an XML-RPC server.""" + + def get_host_info(self, host): + + host, extra_headers, x509 = xmlrpclib.Transport.get_host_info(self, host) + + # Set the remote host principal + h = host + hostinfo = h.split(':') + service = "HTTP@" + hostinfo[0] + + try: + rc, vc = kerberos.authGSSClientInit(service); + except kerberos.GSSError, e: + raise GSSError(e) + + try: + kerberos.authGSSClientStep(vc, ""); + except kerberos.GSSError, e: + raise GSSError(e) + + extra_headers = [ + ("Authorization", "negotiate %s" % kerberos.authGSSClientResponse(vc) ) + ] + + return host, extra_headers, x509 + diff --git a/ipa-python/rpcclient.py b/ipa-python/rpcclient.py index 41602662..d4e645e1 100644 --- a/ipa-python/rpcclient.py +++ b/ipa-python/rpcclient.py @@ -20,83 +20,101 @@ #!/usr/bin/python -try: - import krbV -except ImportError: - pass import xmlrpclib import socket +import config +from krbtransport import KerbTransport +from kerberos import GSSError import os import base64 +import user +import ipa # Some errors to catch # http://cvs.fedora.redhat.com/viewcvs/ldapserver/ldap/servers/plugins/pam_passthru/README?root=dirsec&rev=1.6&view=auto + +class RPCClient: + + def __init__(self): + ipa.config.init_config() -# FIXME: do we want this set somewhere else? -server = xmlrpclib.ServerProxy("http://localhost:80/ipa") - -def get_user(username): - """Get a specific user""" + def server_url(self): + return "http://" + config.config.get_server() + "/ipa" - 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 + def setup_server(self): + return xmlrpclib.ServerProxy(self.server_url(), KerbTransport()) - return myuser + def convert_entry(self,ent): + # Convert into a dict. We need to handle multi-valued attributes as well + # so we'll convert those into lists. + user={} + for (k) in ent: + k = k.lower() + if user.get(k) is not None: + if isinstance(user[k],list): + user[k].append(ent[k].strip()) + else: + first = user[k] + user[k] = () + user[k].append(first) + user[k].append(ent[k].strip()) + else: + user[k] = ent[k] -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' + return user + + def get_user(self,username): + """Get a specific user""" + server = self.setup_server() + try: + result = server.get_user(username) + except xmlrpclib.Fault, fault: + raise xmlrpclib.Fault(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) - # 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'] + return result + + + def add_user(self,user): + """Add a new user""" + server = self.setup_server() + + try: + result = server.add_user(user) + except xmlrpclib.Fault, fault: + raise xmlrpclib.Fault(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) - 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(self): + """Get the list of attributes we need to ask when adding a new + user. + """ + server = self.setup_server() + + # 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) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result -def get_add_schema(): - """Get the list of attributes we need to ask when adding a new - user. - """ + def get_all_users (self): + """Return a list containing a User object for each existing 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 + server = self.setup_server() + try: + result = server.get_all_users() + except xmlrpclib.Fault, fault: + raise xmlrpclib.Fault(fault.faultCode, fault.faultString) + except socket.error, (value, msg): + raise xmlrpclib.Fault(value, msg) + + return result diff --git a/ipa-python/user.py b/ipa-python/user.py new file mode 100644 index 00000000..6162354b --- /dev/null +++ b/ipa-python/user.py @@ -0,0 +1,112 @@ +import ldap +import ldif +import re +import cStringIO + +class User: + """This class represents an IPA user. An LDAP entry consists of a DN + and a list of attributes. Each attribute consists of a name and a list of + values. For the time being I will maintain this. + + 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() + elif isinstance(entrydata,dict): + self.dn = entrydata['dn'] + del entrydata['dn'] + self.data = ldap.cidict.cidict(entrydata) + 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""" + value = self.data.get(name,[None]) + if isinstance(value[0],list) or isinstance(value[0],tuple): + return value[0] + else: + return value + + 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 attrList(self): + """Return a list of all attributes in the entry""" + return self.data.keys() + +# 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,User.base64_attrs,1000).unparse(self.dn,newdata) +# return sio.getvalue() |