Enable LDAP SASL authentication using a forwarded kerberos ticket

Handle both SASL auth and proxied authentication
Refactor LDAP connection code to be simpler
Other small bug fixes
This commit is contained in:
rcritten@redhat.com 2007-09-05 13:14:23 -04:00
parent 82943c31de
commit 9b30f46744
5 changed files with 176 additions and 138 deletions

View File

@ -50,7 +50,9 @@ def main():
client = ipaclient.IPAClient()
users = client.find_users(args[1], sattrs=['dn','uid','cn','homeDirectory'])
if len(users) == 0:
counter = users[0]
users = users[1:]
if counter == 0:
print "No entries found for", args[1]
return 0

View File

@ -59,6 +59,9 @@ def main():
except ipa.ipaerror.IPAError, e:
print "%s" % e.message
return 1
except kerberos.GSSError, e:
print "Could not initialize GSSAPI: %s/%s" % (e[0][0][0], e[0][1][0])
return 1
if options.gecos:
user.setValue('gecos', options.gecos)
@ -79,6 +82,9 @@ def main():
except xmlrpclib.ProtocolError, e:
print "Unable to connect to IPA server: %s" % (e.errmsg)
return 1
except ipa.ipaerror.IPAError, e:
print "%s" % (e.message)
return 1
return 0

View File

@ -35,13 +35,14 @@ import cStringIO
import time
import operator
import struct
import ldap.sasl
from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples
from ldap.modlist import modifyModlist
from ldap.ldapobject import SimpleLDAPObject
from ipa import ipaerror, 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
@ -196,22 +197,34 @@ class IPAdmin(SimpleLDAPObject):
raise ipaerror.gen_exception(ipaerror.LDAP_DATABASE_ERROR, None, e)
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,cacert,bindcert,bindkey,proxydn=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
using the start command, we just need to reconnect, not create a new instance"""
"""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"""
# ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
if cacert is not None:
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert)
ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert)
ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey)
self.__wrapmethods()
self.port = port or 389
self.sslport = 0
self.host = host
self.cacert = cacert
self.bindcert = bindcert
self.bindkey = bindkey
self.proxydn = proxydn
@ -251,6 +264,12 @@ class IPAdmin(SimpleLDAPObject):
def set_proxydn(self, proxydn):
self.proxydn = proxydn
def set_keytab(self, keytab):
if keytab is not None:
os.environ["KRB5CCNAME"] = keytab
self.sasl_interactive_bind_s("", sasl_auth)
self.proxydn = None
def getEntry(self,*args):
"""This wraps the search function. It is common to just get one entry"""
@ -346,6 +365,7 @@ class IPAdmin(SimpleLDAPObject):
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:
@ -366,6 +386,7 @@ class IPAdmin(SimpleLDAPObject):
raise ipaerror.gen_exception(ipaerror.LDAP_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.
@ -428,6 +449,7 @@ class IPAdmin(SimpleLDAPObject):
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:
@ -440,6 +462,7 @@ class IPAdmin(SimpleLDAPObject):
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:

View File

@ -47,19 +47,30 @@ DefaultGroupContainer = "cn=groups,cn=accounts"
# this is not anticipated.
class IPAConnPool:
def __init__(self):
self.numentries = 0
self.freelist = []
def getConn(self, host, port, bindca, bindcert, bindkey, proxydn=None):
self.numentries = self.numentries + 1
def getConn(self, host, port, bindca, bindcert, bindkey, proxydn=None, keytab=None):
conn = None
if len(self.freelist) > 0:
conn = self.freelist.pop()
else:
for i in range(len(self.freelist)):
c = self.freelist[i]
if ((c.host == host) and (c.port == port)):
conn = self.freelist.pop(i)
break
if conn is None:
conn = ipaserver.ipaldap.IPAdmin(host,port,bindca,bindcert,bindkey)
if proxydn is not None:
conn.set_proxydn(proxydn)
else:
conn.set_keytab(keytab)
return conn
def releaseConn(self, conn):
# We can't re-use SASL connections. If proxydn is None it means
# we have a keytab set. See ipaldap.set_keytab
if conn.proxydn is None:
conn.unbind_s()
else:
self.freelist.append(conn)
class IPAServer:
@ -68,7 +79,8 @@ class IPAServer:
global _LDAPPool
# FIXME, this needs to be auto-discovered
self.host = 'localhost'
self.port = 636
self.port = 389
self.sslport = 636
self.bindcert = "/usr/share/ipa/cert.pem"
self.bindkey = "/usr/share/ipa/key.pem"
self.bindca = "/usr/share/ipa/cacert.asc"
@ -79,24 +91,84 @@ class IPAServer:
self.basedn = ipa.ipautil.realm_to_suffix(ipa.config.config.get_realm())
self.scope = ldap.SCOPE_SUBTREE
self.princ = None
self.keytab = None
def set_principal(self, princ):
self.princ = princ
def set_keytab(self, keytab):
self.keytab = keytab
def get_dn_from_principal(self, princ):
"""Given a kerberls principal get the LDAP uid"""
"""Given a kerberos principal get the LDAP uid"""
global _LDAPPool
filter = "(krbPrincipalName=" + princ + ")"
# The only anonymous search we should have
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,None)
conn = _LDAPPool.getConn(self.host,self.sslport,self.bindca,self.bindcert,self.bindkey,None,None)
try:
ent = m1.getEntry(self.basedn, self.scope, filter, ['dn'])
ent = conn.getEntry(self.basedn, self.scope, filter, ['dn'])
finally:
_LDAPPool.releaseConn(m1)
_LDAPPool.releaseConn(conn)
return "dn:" + ent.dn
def __setup_connection(self, opts):
"""Set up common things done in the connection.
If there is a keytab then return None as the proxy dn and the keytab
otherwise return the proxy dn and None as the keytab.
We only want one or the other used at one time and we prefer
the keytab. So if there is a keytab, return that and None for
proxy dn to make calling getConn() easier.
"""
if opts:
if opts.get('keytab'):
self.set_keytab(opts['keytab'])
self.set_principal(None)
else:
self.set_keytab(None)
self.set_principal(opts['remoteuser'])
else:
self.set_keytab(None)
# The caller should have already set the principal
if self.princ is not None:
return self.get_dn_from_principal(self.princ), None
else:
return None, self.keytab
def getConnection(self, opts):
"""Wrapper around IPAConnPool.getConn() so we don't have to pass
around self.* every time a connection is needed.
For SASL connections (where we have a keytab) we can't set
the SSL variables for certificates. It confuses the ldap
module.
"""
global _LDAPPool
(proxy_dn, keytab) = self.__setup_connection(opts)
if keytab is not None:
bindca = None
bindcert = None
bindkey = None
port = self.port
else:
bindca = self.bindca
bindcert = self.bindcert
bindkey = self.bindkey
port = self.sslport
return _LDAPPool.getConn(self.host,port,bindca,bindcert,bindkey,proxy_dn,keytab)
def releaseConnection(self, conn):
global _LDAPPool
_LDAPPool.releaseConn(conn)
def convert_entry(self, ent):
entry = dict(ent.data)
entry['dn'] = ent.dn
@ -110,24 +182,17 @@ class IPAServer:
entry[key] = value[0]
return entry
def __get_entry (self, base, filter, sattrs=None, opts=None):
"""Get a specific entry. Return as a dict of values.
Multi-valued fields are represented as lists.
"""
global _LDAPPool
ent=""
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
ent = m1.getEntry(base, self.scope, filter, sattrs)
ent = conn.getEntry(base, self.scope, filter, sattrs)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return self.convert_entry(ent)
@ -137,8 +202,6 @@ class IPAServer:
oldentry is a dict
newentry is a dict
"""
global _LDAPPool
oldentry = self.convert_scalar_values(oldentry)
newentry = self.convert_scalar_values(newentry)
@ -150,16 +213,11 @@ class IPAServer:
except KeyError, e:
raise ipaerror.gen_exception(ipaerror.LDAP_MISSING_DN)
if opts:
self.set_principal(opts['remoteuser'])
proxydn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn)
conn = self.getConnection(opts)
try:
res = m1.updateEntry(moddn, oldentry, newentry)
res = conn.updateEntry(moddn, oldentry, newentry)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return res
def __safe_filter(self, criteria):
@ -234,8 +292,6 @@ class IPAServer:
attribute name and the value is either a string or in the case
of a multi-valued field a list of values. user_container sets
where in the tree the user is placed."""
global _LDAPPool
if user_container is None:
user_container = DefaultUserContainer
@ -288,16 +344,11 @@ class IPAServer:
for u in user:
entry.setValues(u, user[u])
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
res = m1.addEntry(entry)
res = conn.addEntry(entry)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return res
def get_add_schema (self):
@ -348,20 +399,13 @@ class IPAServer:
"""Return a list containing a User object for each
existing user.
"""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
filter = "(objectclass=posixAccount)"
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
all_users = m1.getList(self.basedn, self.scope, filter, None)
all_users = conn.getList(self.basedn, self.scope, filter, None)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
users = []
for u in all_users:
@ -372,13 +416,6 @@ class IPAServer:
def find_users (self, criteria, sattrs=None, opts=None):
"""Returns a list: counter followed by the results.
If the results are truncated, counter will be set to -1."""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
# Assume the list of fields to search will come from a central
# configuration repository. A good format for that would be
# a comma-separated list of fields
@ -394,21 +431,21 @@ class IPAServer:
(exact_match_filter, partial_match_filter) = self.__generate_match_filters(
search_fields, criteria_words)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
try:
exact_results = m1.getListAsync(self.basedn, self.scope,
exact_results = conn.getListAsync(self.basedn, self.scope,
exact_match_filter, sattrs)
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
exact_results = [0]
try:
partial_results = m1.getListAsync(self.basedn, self.scope,
partial_results = conn.getListAsync(self.basedn, self.scope,
partial_match_filter, sattrs)
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
partial_results = [0]
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
exact_counter = exact_results[0]
partial_counter = partial_results[0]
@ -450,13 +487,6 @@ class IPAServer:
def mark_user_deleted (self, uid, opts=None):
"""Mark a user as inactive in LDAP. We aren't actually deleting
users here, just making it so they can't log in, etc."""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
proxydn = self.get_dn_from_principal(self.princ)
user = self.get_user_by_uid(uid, ['dn', 'uid', 'nsAccountlock'], opts)
# Are we doing an add or replace operation?
@ -467,11 +497,11 @@ class IPAServer:
else:
has_key = False
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,proxydn)
conn = self.getConnection(opts)
try:
res = m1.inactivateEntry(user['dn'], has_key)
res = conn.inactivateEntry(user['dn'], has_key)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
return res
def delete_user (self, uid, opts=None):
@ -483,18 +513,15 @@ class IPAServer:
The memberOf plugin handles removing the user from any other
groups.
"""
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
user_dn = self.get_user_by_uid(uid, ['dn', 'uid', 'objectclass'], opts)
if user_dn is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
res = m1.deleteEntry(user_dn['dn'])
_LDAPPool.releaseConn(m1)
conn = self.getConnection(opts)
try:
res = conn.deleteEntry(user_dn['dn'])
finally:
self.releaseConnection(conn)
return res
# Group support
@ -532,8 +559,6 @@ class IPAServer:
attribute name and the value is either a string or in the case
of a multi-valued field a list of values. group_container sets
where in the tree the group is placed."""
global _LDAPPool
if group_container is None:
group_container = DefaultGroupContainer
@ -554,38 +579,26 @@ class IPAServer:
for g in group:
entry.setValues(g, group[g])
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
res = m1.addEntry(entry)
res = conn.addEntry(entry)
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
def find_groups (self, criteria, sattrs=None, opts=None):
"""Return a list containing a User object for each
existing group that matches the criteria.
"""
global _LDAPPool
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
criteria = self.__safe_filter(criteria)
filter = "(&(cn=%s)(objectClass=posixGroup))" % criteria
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
conn = self.getConnection(opts)
try:
results = m1.getList(self.basedn, self.scope, filter, sattrs)
results = conn.getList(self.basedn, self.scope, filter, sattrs)
except ipaerror.exception_for(ipaerror.LDAP_NOT_FOUND):
results = []
finally:
_LDAPPool.releaseConn(m1)
self.releaseConnection(conn)
groups = []
for u in results:
@ -599,9 +612,6 @@ class IPAServer:
group is the cn of the group to be added to
"""
if opts:
self.set_principal(opts['remoteuser'])
old_group = self.get_group_by_cn(group, None, opts)
if old_group is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
@ -652,9 +662,6 @@ class IPAServer:
group is the cn of the group to be removed from
"""
if opts:
self.set_principal(opts['remoteuser'])
old_group = self.get_group_by_cn(group, None, opts)
if old_group is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
@ -718,19 +725,16 @@ class IPAServer:
The memberOf plugin handles removing the group from any other
groups.
"""
if opts:
self.set_principal(opts['remoteuser'])
dn = self.get_dn_from_principal(self.princ)
group = self.get_group_by_cn(group_cn, ['dn', 'cn'], opts)
if len(group) != 1:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)
m1 = _LDAPPool.getConn(self.host,self.port,self.bindca,self.bindcert,self.bindkey,dn)
res = m1.deleteEntry(group[0]['dn'])
_LDAPPool.releaseConn(m1)
conn = self.getConnection(opts)
try:
res = conn.deleteEntry(group[0]['dn'])
finally:
self.releaseConnection(conn)
return res
def add_group_to_group(self, group, tgroup, opts=None):
@ -739,9 +743,6 @@ class IPAServer:
tgroup is the cn of the group to be added to
"""
if opts:
self.set_principal(opts['remoteuser'])
old_group = self.get_group_by_cn(tgroup, None, opts)
if old_group is None:
raise ipaerror.gen_exception(ipaerror.LDAP_NOT_FOUND)

View File

@ -126,13 +126,19 @@ class ModXMLRPCRequestHandler(object):
def register_instance(self,instance):
self.register_module(instance)
def _marshaled_dispatch(self, data, remoteuser):
def _marshaled_dispatch(self, data, req):
"""Dispatches an XML-RPC method from marshalled (XML) data."""
params, method = loads(data)
# Populate the Apache environment variables
req.add_common_vars()
opts={}
opts['remoteuser'] = remoteuser
opts['remoteuser'] = req.user
if req.subprocess_env.get("KRB5CCNAME") is not None:
opts['keytab'] = req.subprocess_env.get("KRB5CCNAME")
# Tack onto the end of the passed-in arguments any options we also
# need
@ -263,7 +269,7 @@ class ModXMLRPCRequestHandler(object):
req.allow_methods(['POST'],1)
raise apache.SERVER_RETURN, apache.HTTP_METHOD_NOT_ALLOWED
response = self._marshaled_dispatch(req.read(), req.user)
response = self._marshaled_dispatch(req.read(), req)
req.content_type = "text/xml"
req.set_content_length(len(response))