mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-24 16:10:02 -06:00
785851bf66
Fix some errors that weren't being raised properly
554 lines
20 KiB
Python
554 lines
20 KiB
Python
# Authors: Rich Megginson <richm@redhat.com>
|
|
# Rob Crittenden <rcritten@redhat.com
|
|
#
|
|
# Copyright (C) 2007 Red Hat
|
|
# see file 'COPYING' for use and warranty information
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License as
|
|
# published by the Free Software Foundation; version 2 only
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
|
|
import sys
|
|
import os
|
|
import os.path
|
|
import socket
|
|
import ldif
|
|
import re
|
|
import string
|
|
import ldap
|
|
import cStringIO
|
|
import struct
|
|
import ldap.sasl
|
|
from ldap.controls import LDAPControl,DecodeControlTuples,EncodeControlTuples
|
|
from ldap.ldapobject import SimpleLDAPObject
|
|
from ipaserver import ipautil
|
|
from ipalib import errors
|
|
|
|
# Global variable to define SASL auth
|
|
sasl_auth = ldap.sasl.sasl({},'GSSAPI')
|
|
|
|
class Entry:
|
|
"""
|
|
This class represents an LDAP Entry object. An LDAP entry consists of
|
|
a DN and a list of attributes. Each attribute consists of a name and
|
|
a list of values. In python-ldap, entries are returned as a list of
|
|
2-tuples. Instance variables:
|
|
|
|
* dn - string - the string DN of the entry
|
|
* data - CIDict - case insensitive dict of the attributes and values
|
|
"""
|
|
def __init__(self,entrydata):
|
|
"""data is the raw data returned from the python-ldap result method, which is
|
|
a search result entry or a reference or None.
|
|
If creating a new empty entry, data is the string DN."""
|
|
if entrydata:
|
|
if isinstance(entrydata,tuple):
|
|
self.dn = entrydata[0]
|
|
self.data = ipautil.CIDict(entrydata[1])
|
|
elif isinstance(entrydata,str) or isinstance(entrydata,unicode):
|
|
self.dn = entrydata
|
|
self.data = ipautil.CIDict()
|
|
else:
|
|
self.dn = ''
|
|
self.data = ipautil.CIDict()
|
|
|
|
def __nonzero__(self):
|
|
"""This allows us to do tests like if entry: returns false if there is no data,
|
|
true otherwise"""
|
|
return self.data != None and len(self.data) > 0
|
|
|
|
def hasAttr(self,name):
|
|
"""Return True if this entry has an attribute named name, False otherwise"""
|
|
return self.data and self.data.has_key(name)
|
|
|
|
def __getattr__(self,name):
|
|
"""If name is the name of an LDAP attribute, return the first value for that
|
|
attribute - equivalent to getValue - this allows the use of
|
|
entry.cn
|
|
instead of
|
|
entry.getValue('cn')
|
|
This also allows us to return None if an attribute is not found rather than
|
|
throwing an exception"""
|
|
return self.getValue(name)
|
|
|
|
def getValues(self,name):
|
|
"""Get the list (array) of values for the attribute named name"""
|
|
return self.data.get(name)
|
|
|
|
def getValue(self,name):
|
|
"""Get the first value for the attribute named name"""
|
|
return self.data.get(name,[None])[0]
|
|
|
|
def setValue(self, name, *value):
|
|
"""
|
|
Set a value on this entry.
|
|
|
|
The value passed in may be a single value, several values, or a
|
|
single sequence. For example:
|
|
|
|
* ent.setValue('name', 'value')
|
|
* ent.setValue('name', 'value1', 'value2', ..., 'valueN')
|
|
* ent.setValue('name', ['value1', 'value2', ..., 'valueN'])
|
|
* ent.setValue('name', ('value1', 'value2', ..., 'valueN'))
|
|
|
|
Since value is a tuple, we may have to extract a list or tuple from
|
|
that tuple as in the last two examples above.
|
|
"""
|
|
if isinstance(value[0],list) or isinstance(value[0],tuple):
|
|
self.data[name] = value[0]
|
|
else:
|
|
self.data[name] = value
|
|
|
|
setValues = setValue
|
|
|
|
def delAttr(self, name):
|
|
"""
|
|
Entirely remove an attribute of this entry.
|
|
"""
|
|
if self.hasAttr(name):
|
|
del self.data[name]
|
|
|
|
def toTupleList(self):
|
|
"""Convert the attrs and values to a list of 2-tuples. The first element
|
|
of the tuple is the attribute name. The second element is either a
|
|
single value or a list of values."""
|
|
r = []
|
|
for i in self.data.iteritems():
|
|
n = ipautil.utf8_encode_values(i[1])
|
|
r.append((i[0], n))
|
|
return r
|
|
|
|
def toDict(self):
|
|
"""Convert the attrs and values to a dict. The dict is keyed on the
|
|
attribute name. The value is either single value or a list of values."""
|
|
result = ipautil.CIDict(self.data)
|
|
for i in result.keys():
|
|
result[i] = ipautil.utf8_encode_values(result[i])
|
|
result['dn'] = self.dn
|
|
return result
|
|
|
|
def __str__(self):
|
|
"""Convert the Entry to its LDIF representation"""
|
|
return self.__repr__()
|
|
|
|
# the ldif class base64 encodes some attrs which I would rather see in
|
|
# raw form - to encode specific attrs as base64, add them to the list below
|
|
ldif.safe_string_re = re.compile('^$')
|
|
base64_attrs = ['nsstate', 'krbprincipalkey', 'krbExtraData']
|
|
|
|
def __repr__(self):
|
|
"""Convert the Entry to its LDIF representation"""
|
|
sio = cStringIO.StringIO()
|
|
# what's all this then? the unparse method will currently only accept
|
|
# a list or a dict, not a class derived from them. self.data is a
|
|
# cidict, so unparse barfs on it. I've filed a bug against python-ldap,
|
|
# but in the meantime, we have to convert to a plain old dict for
|
|
# printing
|
|
# I also don't want to see wrapping, so set the line width really high
|
|
# (1000)
|
|
newdata = {}
|
|
newdata.update(self.data)
|
|
ldif.LDIFWriter(sio,Entry.base64_attrs,1000).unparse(self.dn,newdata)
|
|
return sio.getvalue()
|
|
|
|
def wrapper(f,name):
|
|
"""This is the method that wraps all of the methods of the superclass.
|
|
This seems to need to be an unbound method, that's why it's outside
|
|
of IPAdmin. Perhaps there is some way to do this with the new
|
|
classmethod or staticmethod of 2.4. Basically, we replace every call
|
|
to a method in SimpleLDAPObject (the superclass of IPAdmin) with a
|
|
call to inner. The f argument to wrapper is the bound method of
|
|
IPAdmin (which is inherited from the superclass). Bound means that it
|
|
will implicitly be called with the self argument, it is not in the
|
|
args list. name is the name of the method to call. If name is a
|
|
method that returns entry objects (e.g. result), we wrap the data
|
|
returned by an Entry class. If name is a method that takes an entry
|
|
argument, we extract the raw data from the entry object to pass in.
|
|
"""
|
|
def inner(*args, **kargs):
|
|
if name == 'result':
|
|
objtype, data = f(*args, **kargs)
|
|
# data is either a 2-tuple or a list of 2-tuples
|
|
# print data
|
|
if data:
|
|
if isinstance(data,tuple):
|
|
return objtype, Entry(data)
|
|
elif isinstance(data,list):
|
|
return objtype, [Entry(x) for x in data]
|
|
else:
|
|
raise TypeError, "unknown data type %s returned by result" % type(data)
|
|
else:
|
|
return objtype, data
|
|
elif name.startswith('add'):
|
|
# the first arg is self
|
|
# the second and third arg are the dn and the data to send
|
|
# We need to convert the Entry into the format used by
|
|
# python-ldap
|
|
ent = args[0]
|
|
if isinstance(ent,Entry):
|
|
return f(ent.dn, ent.toTupleList(), *args[2:])
|
|
else:
|
|
return f(*args, **kargs)
|
|
else:
|
|
return f(*args, **kargs)
|
|
return inner
|
|
|
|
class IPAdmin(SimpleLDAPObject):
|
|
|
|
def __localinit(self):
|
|
"""If a CA certificate is provided then it is assumed that we are
|
|
doing SSL client authentication with proxy auth.
|
|
|
|
If a CA certificate is not present then it is assumed that we are
|
|
using a forwarded kerberos ticket for SASL auth. SASL provides
|
|
its own encryption.
|
|
"""
|
|
if self.cacert is not None:
|
|
SimpleLDAPObject.__init__(self,'ldaps://%s:%d' % (self.host,self.port))
|
|
else:
|
|
SimpleLDAPObject.__init__(self,'ldap://%s:%d' % (self.host,self.port))
|
|
|
|
def __init__(self,host,port=389,cacert=None,bindcert=None,bindkey=None,proxydn=None,debug=None):
|
|
"""We just set our instance variables and wrap the methods - the real
|
|
work is done in __localinit. This is separated out this way so
|
|
that we can call it from places other than instance creation
|
|
e.g. when we just need to reconnect
|
|
"""
|
|
if debug and debug.lower() == "on":
|
|
ldap.set_option(ldap.OPT_DEBUG_LEVEL,255)
|
|
if cacert is not None:
|
|
ldap.set_option(ldap.OPT_X_TLS_CACERTFILE,cacert)
|
|
if bindcert is not None:
|
|
ldap.set_option(ldap.OPT_X_TLS_CERTFILE,bindcert)
|
|
if bindkey is not None:
|
|
ldap.set_option(ldap.OPT_X_TLS_KEYFILE,bindkey)
|
|
|
|
self.__wrapmethods()
|
|
self.port = port
|
|
self.host = host
|
|
self.cacert = cacert
|
|
self.bindcert = bindcert
|
|
self.bindkey = bindkey
|
|
self.proxydn = proxydn
|
|
self.suffixes = {}
|
|
self.__localinit()
|
|
|
|
def __str__(self):
|
|
return self.host + ":" + str(self.port)
|
|
|
|
def __get_server_controls(self):
|
|
"""Create the proxy user server control. The control has the form
|
|
0x04 = Octet String
|
|
4|0x80 sets the length of the string length field at 4 bytes
|
|
the struct() gets us the length in bytes of string self.proxydn
|
|
self.proxydn is the proxy dn to send"""
|
|
|
|
if self.proxydn is not None:
|
|
proxydn = chr(0x04) + chr(4|0x80) + struct.pack('l', socket.htonl(len(self.proxydn))) + self.proxydn;
|
|
|
|
# Create the proxy control
|
|
sctrl=[]
|
|
sctrl.append(LDAPControl('2.16.840.1.113730.3.4.18',True,proxydn))
|
|
else:
|
|
sctrl=None
|
|
|
|
return sctrl
|
|
|
|
def toLDAPURL(self):
|
|
return "ldap://%s:%d/" % (self.host,self.port)
|
|
|
|
def set_proxydn(self, proxydn):
|
|
self.proxydn = proxydn
|
|
|
|
def set_krbccache(self, krbccache, principal):
|
|
if krbccache is not None:
|
|
os.environ["KRB5CCNAME"] = krbccache
|
|
self.sasl_interactive_bind_s("", sasl_auth)
|
|
self.principal = principal
|
|
self.proxydn = None
|
|
|
|
def do_simple_bind(self, binddn="cn=directory manager", bindpw=""):
|
|
self.binddn = binddn
|
|
self.bindpwd = bindpw
|
|
self.simple_bind_s(binddn, bindpw)
|
|
|
|
def getEntry(self,*args):
|
|
"""This wraps the search function. It is common to just get one entry"""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
|
|
try:
|
|
res = self.search(*args)
|
|
objtype, obj = self.result(res)
|
|
except ldap.NO_SUCH_OBJECT, e:
|
|
raise errors.NotFound, notfound(args)
|
|
except ldap.LDAPError, e:
|
|
raise errors.DatabaseError, e
|
|
|
|
if not obj:
|
|
raise errors.NotFound, notfound(args)
|
|
|
|
elif isinstance(obj,Entry):
|
|
return obj
|
|
else: # assume list/tuple
|
|
return obj[0]
|
|
|
|
def getList(self,*args):
|
|
"""This wraps the search function to find multiple entries."""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
|
|
try:
|
|
res = self.search(*args)
|
|
objtype, obj = self.result(res)
|
|
except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED), e:
|
|
# Too many results returned by search
|
|
raise e
|
|
except ldap.LDAPError, e:
|
|
raise e
|
|
|
|
if not obj:
|
|
raise errors.NotFound, notfound(args)
|
|
|
|
entries = []
|
|
for s in obj:
|
|
entries.append(s)
|
|
|
|
return entries
|
|
|
|
def getListAsync(self,*args):
|
|
"""This version performs an asynchronous search, to allow
|
|
results even if we hit a limit.
|
|
|
|
It returns a list: counter followed by the results.
|
|
If the results are truncated, counter will be set to -1.
|
|
"""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
|
|
entries = []
|
|
partial = 0
|
|
|
|
try:
|
|
msgid = self.search_ext(*args)
|
|
objtype, result_list = self.result(msgid, 0)
|
|
while result_list:
|
|
for result in result_list:
|
|
entries.append(result)
|
|
objtype, result_list = self.result(msgid, 0)
|
|
except (ldap.ADMINLIMIT_EXCEEDED, ldap.SIZELIMIT_EXCEEDED,
|
|
ldap.TIMELIMIT_EXCEEDED), e:
|
|
partial = 1
|
|
except ldap.LDAPError, e:
|
|
raise e
|
|
|
|
if not entries:
|
|
raise errors.NotFound, notfound(args)
|
|
|
|
if partial == 1:
|
|
counter = -1
|
|
else:
|
|
counter = len(entries)
|
|
|
|
return [counter] + entries
|
|
|
|
def addEntry(self,*args):
|
|
"""This wraps the add function. It assumes that the entry is already
|
|
populated with all of the desired objectclasses and attributes"""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
|
|
try:
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
self.add_s(*args)
|
|
except ldap.ALREADY_EXISTS, e:
|
|
raise errors.DuplicateEntry, "Entry already exists"
|
|
except ldap.LDAPError, e:
|
|
raise errors.DatabaseError, e
|
|
return True
|
|
|
|
def updateRDN(self, dn, newrdn):
|
|
"""Wrap the modrdn function."""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
|
|
if dn == newrdn:
|
|
# no need to report an error
|
|
return True
|
|
|
|
try:
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
self.modrdn_s(dn, newrdn, delold=1)
|
|
except ldap.LDAPError, e:
|
|
raise errors.DatabaseError, e
|
|
return True
|
|
|
|
def updateEntry(self,dn,oldentry,newentry):
|
|
"""This wraps the mod function. It assumes that the entry is already
|
|
populated with all of the desired objectclasses and attributes"""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
|
|
modlist = self.generateModList(oldentry, newentry)
|
|
|
|
if len(modlist) == 0:
|
|
raise errors.EmptyModlist
|
|
|
|
try:
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
self.modify_s(dn, modlist)
|
|
# this is raised when a 'delete' attribute isn't found.
|
|
# it indicates the previous attribute was removed by another
|
|
# update, making the oldentry stale.
|
|
except ldap.NO_SUCH_ATTRIBUTE:
|
|
raise errors.MidairCollision
|
|
except ldap.LDAPError, e:
|
|
raise errors.DatabaseError, e
|
|
return True
|
|
|
|
def generateModList(self, old_entry, new_entry):
|
|
"""A mod list generator that computes more precise modification lists
|
|
than the python-ldap version. This version purposely generates no
|
|
REPLACE operations, to deal with multi-user updates more properly."""
|
|
modlist = []
|
|
|
|
old_entry = ipautil.CIDict(old_entry)
|
|
new_entry = ipautil.CIDict(new_entry)
|
|
|
|
keys = set(map(string.lower, old_entry.keys()))
|
|
keys.update(map(string.lower, new_entry.keys()))
|
|
|
|
for key in keys:
|
|
new_values = new_entry.get(key, [])
|
|
if not(isinstance(new_values,list) or isinstance(new_values,tuple)):
|
|
new_values = [new_values]
|
|
new_values = filter(lambda value:value!=None, new_values)
|
|
new_values = set(new_values)
|
|
|
|
old_values = old_entry.get(key, [])
|
|
if not(isinstance(old_values,list) or isinstance(old_values,tuple)):
|
|
old_values = [old_values]
|
|
old_values = filter(lambda value:value!=None, old_values)
|
|
old_values = set(old_values)
|
|
|
|
adds = list(new_values.difference(old_values))
|
|
removes = list(old_values.difference(new_values))
|
|
|
|
if len(removes) > 0:
|
|
modlist.append((ldap.MOD_DELETE, key, removes))
|
|
if len(adds) > 0:
|
|
modlist.append((ldap.MOD_ADD, key, adds))
|
|
|
|
return modlist
|
|
|
|
def inactivateEntry(self,dn,has_key):
|
|
"""Rather than deleting entries we mark them as inactive.
|
|
has_key defines whether the entry already has nsAccountlock
|
|
set so we can determine which type of mod operation to run."""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
modlist=[]
|
|
|
|
if has_key:
|
|
operation = ldap.MOD_REPLACE
|
|
else:
|
|
operation = ldap.MOD_ADD
|
|
|
|
modlist.append((operation, "nsAccountlock", "true"))
|
|
|
|
try:
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
self.modify_s(dn, modlist)
|
|
except ldap.LDAPError, e:
|
|
raise errors.DatabaseError, e
|
|
return True
|
|
|
|
def deleteEntry(self,*args):
|
|
"""This wraps the delete function. Use with caution."""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
|
|
try:
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
self.delete_s(*args)
|
|
except ldap.INSUFFICIENT_ACCESS, e:
|
|
raise errors.InsufficientAccess, e
|
|
except ldap.LDAPError, e:
|
|
raise errors.DatabaseError, e
|
|
return True
|
|
|
|
def modifyPassword(self,dn,oldpass,newpass):
|
|
"""Set the user password using RFC 3062, LDAP Password Modify Extended
|
|
Operation. This ends up calling the IPA password slapi plugin
|
|
handler so the Kerberos password gets set properly.
|
|
|
|
oldpass is not mandatory
|
|
"""
|
|
|
|
sctrl = self.__get_server_controls()
|
|
|
|
try:
|
|
if sctrl is not None:
|
|
self.set_option(ldap.OPT_SERVER_CONTROLS, sctrl)
|
|
self.passwd_s(dn, oldpass, newpass)
|
|
except ldap.LDAPError, e:
|
|
raise e
|
|
return True
|
|
|
|
def __wrapmethods(self):
|
|
"""This wraps all methods of SimpleLDAPObject, so that we can intercept
|
|
the methods that deal with entries. Instead of using a raw list of tuples
|
|
of lists of hashes of arrays as the entry object, we want to wrap entries
|
|
in an Entry class that provides some useful methods"""
|
|
for name in dir(self.__class__.__bases__[0]):
|
|
attr = getattr(self, name)
|
|
if callable(attr):
|
|
setattr(self, name, wrapper(attr, name))
|
|
|
|
def normalizeDN(dn):
|
|
# not great, but will do until we use a newer version of python-ldap
|
|
# that has DN utilities
|
|
ary = ldap.explode_dn(dn.lower())
|
|
return ",".join(ary)
|
|
normalizeDN = staticmethod(normalizeDN)
|
|
|
|
def notfound(args):
|
|
"""Return a string suitable for displaying as an error when a
|
|
search returns no results.
|
|
|
|
This just returns whatever is after the equals sign"""
|
|
if len(args) > 2:
|
|
searchfilter = args[2]
|
|
try:
|
|
# Python re doesn't do paren counting so the string could
|
|
# have a trailing paren "foo)"
|
|
target = re.match(r'\(.*=(.*)\)', searchfilter).group(1)
|
|
target = target.replace(")","")
|
|
except:
|
|
target = searchfilter
|
|
return "%s not found" % str(target)
|
|
else:
|
|
return args[0]
|