# -*- coding: <utf-8> -*-
###########################################################################
# Copyright (C) 2004, 2005 by Wido Depping
# <widod@users.sourceforge.net>
#
# Copyright: See COPYING file that comes with this distribution
#
###########################################################################
import ldap
import ldif
import dsml
import copy
import base64
from cStringIO import StringIO
from sets import Set
from base.backend.ObjectClassAttributeInfo import ObjectClassAttributeInfo
from base.utils import stripSpecialChars
from base.utils import escapeSpecialChars
from base.utils import isBinaryAttribute
from base.utils import explodeDN
class SmartDataObject (object):
passwordList = ["userpassword", "ntpassword", "lmpassword", "clearpassword",
"sambalmpassword", "sambantpassword", "goimappassword",
"gokrbpassword", "gofaxpassword", "gologpassword", "gofonpassword",
"solarisbindpassword"]
def __init__(self, data, serverMeta):
self.doSchemaChecks = True
self.isValid = False
self.checkErrorMessageList = ["No error checking done yet."]
self.dn = data[0]
self.data = data[1]
# This is the string representing our key for the objectclasses.
# Important for lower- and uppercase variants
self.objectClassName = None
for x in self.data.keys():
if "objectclass" == x.lower():
self.objectClassName = x
break
# Set server meta information
self.serverMeta = serverMeta
# Set schema for current server
self.serverSchema = ObjectClassAttributeInfo(self.serverMeta)
#self.checkIntegrity()
###############################################################################
def __cmp__(self, otherItem):
""" Custom sort function for comparing SmartDataObjects.
Sorting is based on the distinguished name of the object. The higher
the object in the tree, the more it will be at the beginning of the
sorted list. Leaves should be at the end of the list.
"""
if type(otherItem) == SmartDataObject:
currentList = explodeDN(self.getDN())
otherList = explodeDN(otherItem.getDN())
curLength = len(currentList)
otherLength = len(otherList)
if curLength < otherLength:
return -1
elif curLength == otherLength:
return 0
elif curLength > otherLength:
return 1
else:
return 0
else:
return 1
###############################################################################
def getServerAlias(self):
""" Returns a string representing the alias name of the server.
"""
return copy.deepcopy(self.serverMeta.name)
###############################################################################
def getServerMeta(self):
""" Returns the meta information about the server.
"""
return copy.deepcopy(self.serverMeta)
###############################################################################
def getDN(self):
""" Returns the DN of the current object as a string.
"""
return self.dn
###############################################################################
def getObjectClasses(self):
""" Returns a list of the current objectClasses. Returns None if data
has no values for objectClass.
"""
if None == self.objectClassName:
#return None
return []
else:
return self.data[self.objectClassName][:]
###############################################################################
def getStructuralClasses(self):
""" Returns a list of classes which are structural in this object.
"""
structList = []
for x in self.getObjectClasses():
if self.isObjectclassStructural(x):
structList.append(x)
return structList
###############################################################################
def hasStructuralClass(self):
""" Returns boolean value if current object has a structural class.
"""
for x in self.getObjectClasses():
if self.isObjectclassStructural(x):
return True
return False
###############################################################################
def getAttributeList(self):
""" Returns a list of currently used attributes.
"""
tmpList = self.data.keys()
# Remove attribute 'objectClass'. We don't want that.
if not (None == self.objectClassName):
tmpList.remove(self.objectClassName)
return tmpList
###############################################################################
def getAttributeValueList(self, attributeName=None):
"""Returns a list of values for the given attribute.
"""
if None == attributeName:
raise FunctionArgumentException("Function getAttributeValueList( attributeName ) called without a parameter.")
if self.data.has_key(attributeName):
# Binary values are returned normally.
# String values have to be decoded from utf-8 to unicode.
if self.isAttributeBinary(attributeName):
return self.data[attributeName]
else:
tmpList = []
for x in self.data[attributeName]:
if None == x:
tmpList.append(None)
else:
tmpList.append(x.decode('utf-8'))
return tmpList
else:
return None
###############################################################################
def getAttributeValue(self, attributeName=None, valueIndex=None):
"""Returns the values for the given attribute at index valueIndex.
"""
if (None == attributeName) or (None == valueIndex):
raise FunctionArgumentException("Function getAttributeValue( attributeName, valueIndex ) called without correct parameters.")
if self.data.has_key(attributeName):
# Is the data length of the attribute compatible with the given index?
if valueIndex < len(self.data[attributeName]):
# Binary values are returned normally.
# String values have to be decoded from utf-8 to unicode.
if self.isAttributeBinary(attributeName):
return self.data[attributeName][valueIndex]
else:
tmpValue = self.data[attributeName][valueIndex]
# Do we have an unset value?
if None == tmpValue:
return None
else:
return tmpValue.decode('utf-8')
# Index is out of range.
else:
errorList = []
errorList.append("Can't get value for attribute. Index for attribute " + attributeName + " is out of range.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
else:
return None
###############################################################################
def getAttributeListForObjectClass(self, objectClass):
return self.serverSchema.getAttributeListForObjectClass(objectClass)
###############################################################################
def addAttributeValue(self, attributeName=None, valueList=None, replaceOldValues=False):
""" Adds the values given by valueList to the attribute attributeName.
"""
if None == attributeName:
raise FunctionArgumentException("Function addAttributeValue( attributeName, valueList) called without correct parameters.")
# Do we work on an existing attribute?
if self.data.has_key(attributeName):
# Do we want values to a single valued attribute?
if self.isAttributeSingle(attributeName):
# Do we replace old values?
if replaceOldValues:
if len(valueList) > 1:
errorList = []
errorList.append("Can't add value to attribute. Attribute " + attributeName + " is single valued and the valueList to add has more than one value.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
if self.isAttributeBinary(attributeName):
errorList.append(" To add: Attribute= " + attributeName + " Values= BINARY FORMAT" )
else:
errorList.append(" To add: Attribute= " + attributeName + " Values=" + repr(valueList))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
# We don't replace old values.
else:
# We want to add a value altough attribute is single valued and has a value.
if len(self.data[attributeName]) >= 1:
errorList = []
errorList.append("Can't add value to attribute. Attribute " + attributeName + " is single valued.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
if self.isAttributeBinary(attributeName):
errorList.append(" To add: Attribute= " + attributeName + " Values= BINARY FORMAT" )
else:
errorList.append(" To add: Attribute= " + attributeName + " Values=" + repr(valueList))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
# Attribute values are empty. This is important for other components.
# An empty list indicates if the attribute is fresh and needs a value.
if None == valueList:
self.data[attributeName].append(None)
else:
# Binary values added normally.
# String values have to be encoded in utf-8.
if self.isAttributeBinary(attributeName):
if replaceOldValues:
self.data[attributeName] = valueList
else:
self.data[attributeName].extend(valueList)
else:
if replaceOldValues:
self.data[attributeName] = map(lambda x: x.encode('utf-8'), valueList)
else:
self.data[attributeName].extend(map(lambda x: x.encode('utf-8'), valueList))
# We're adding a new attribute.
else:
# Is attribute allowed?
if self.serverSchema.attributeAllowed(attributeName, self.getObjectClasses()):
# Attribute values are empty. This is important for other components.
# An empty list indicates if the attribute is fresh and needs a value.
if None == valueList:
self.data[attributeName] = [None]
else:
# Is attribute single?
if self.isAttributeSingle(attributeName):
# Has valueList more than one value?
if len(valueList) > 1:
errorList = []
errorList.append("Can't add value to attribute. Attribute " + attributeName + " is single valued.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
if self.isAttributeBinary(attributeName):
errorList.append(" To add: Attribute= " + attributeName + " Values= BINARY FORMAT" )
else:
errorList.append(" To add: Attribute= " + attributeName + " Values=" + repr(valueList))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
# valueList has only one value
else:
if self.isAttributeBinary(attributeName):
self.data[attributeName] = valueList
else:
self.data[attributeName] = map(lambda x: x.encode('utf-8'), valueList)
# The attribute is not single
else:
# Since attribute is new to the object, we have no need to
# join the values with the old list.
if self.isAttributeBinary(attributeName):
self.data[attributeName] = valueList
else:
self.data[attributeName] = map(lambda x: x.encode('utf-8'), valueList)
# Attribute is not allowed with the current list of objectclasses.
else:
errorList = []
errorList.append("Can't add value to attribute. Attribute " + attributeName + " not allowed with current objectClasses.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
if self.isAttributeBinary(attributeName):
errorList.append(" To add: Attribute= " + attributeName + " Values= BINARY FORMAT" )
else:
errorList.append(" To add: Attribute= " + attributeName + " Values=" + repr(valueList))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
###############################################################################
def setAttributeValue(self, attributeName=None, valueIndex=None, newValue=None):
"""Sets the value for the given attribute at index valueIndex.
"""
if (None == attributeName) or (None == valueIndex) or (None == newValue):
raise FunctionArgumentException("Function setAttributeValue( attributeName, valueIndex, newValue ) called without correct parameters.")
if self.data.has_key(attributeName):
# Is the data length of the attribute compatible with the given index?
if valueIndex < len(self.data[attributeName]):
# Binary values are returned normally.
# String values have to be decoded from utf-8 to unicode.
if self.isAttributeBinary(attributeName):
self.data[attributeName][valueIndex] = newValue
else:
tmpValue = newValue.encode('utf-8')
self.data[attributeName][valueIndex] = tmpValue
# Index is out of range.
else:
errorList = []
errorList.append("Can't get value for attribute. Index for attribute " + attributeName + " is out of range.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
elif "rdn" == attributeName.lower():
self.setDN(newValue)
else:
errorList = []
errorList.append("Can't set value for attribute. Object has no attribute " + attributeName + ".")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
###############################################################################
def isAttributeAllowed(self, attributeName=None):
""" Returns True if the attribute attributeName is allowed with the
current set of objectclasses.
"""
if None == attributeName:
raise FunctionArgumentException("Function isAttributeAllowed(attributeName) called without correct parameters.")
mustSet, maySet = self.getPossibleAttributes()
attributeList = mustSet.union(maySet)
attributeName = attributeName.lower()
for x in attributeList:
if attributeName == x.lower():
return True
return False
###############################################################################
def isAttributeMust(self, attributeName=None, classList=None):
""" Returns a boolean if a given attribute is must for a list of objectClasses.
classList can be None. Then the check is performed on the current classes of the object.
Otherwise the check is done on the given list of classes.
"""
if None == attributeName:
raise FunctionArgumentException("Function isAttributeMust(attributeName, classList) called without correct parameters.")
if None == classList:
return self.serverSchema.isMust(attributeName, self.getObjectClasses())
else:
return self.serverSchema.isMust(attributeName, classList)
###############################################################################
def isAttributeSingle(self, attributeName=None):
""" Returns a boolean if a given attribute is single valued.
"""
if None == attributeName:
raise FunctionArgumentException("Function isAttributeSingle(attributeName) called without correct parameters.")
return self.serverSchema.isSingle(attributeName)
###############################################################################
def isAttributeBinary(self, attributeName=None):
""" Returns a boolean if a given attribute is of type binary.
"""
if None == attributeName:
raise FunctionArgumentException("Function isAttributeBinary(attributeName) called without correct parameters.")
return self.serverSchema.isBinary(attributeName)
###############################################################################
def isAttributeImage(self, attributeName=None):
""" Returns a boolean if a given attribute contains image data.
"""
if None == attributeName:
raise FunctionArgumentException("Function isAttributeImage(attributeName) called without correct parameters.")
if "jpegphoto" == attributeName.lower():
return True
else:
return False
###############################################################################
def isAttributePassword(self, attributeName=None):
""" Returns a boolean if a given attribute contains password data.
"""
if None == attributeName:
raise FunctionArgumentException("Function isAttributePassword(attributeName) called without correct parameters.")
tmpName = attributeName.lower()
if tmpName in self.passwordList:
return True
else:
return False
###############################################################################
def isAliasObject(self):
""" Returns a boolean if the current object is an alias object.
The object is from type alias if it has the alias objectClass.
"""
for x in self.getObjectClasses():
if "alias" == x.lower():
return True
return False
###############################################################################
def isAttributeValueRDN(self, attributeName=None, attributeValue=None):
""" Returns a boolean if the given attribute and it's value are the RDN of the object.
The given parameters have to be in unicode format and special characters have
to be decoded.
"""
if None == attributeName:
raise FunctionArgumentException("Function isAttributeValueRDN(attributeName, attributeValue) called without correct parameters.")
# The attribute value is not yet initialized. We can safely ignore it.
if None == attributeValue:
return False
# Is our attribute binary?
if self.isAttributeBinary(attributeName):
return False
# No binary attribute
else:
tmpRDN = attributeName + "=" + attributeValue
# Does the created RDN match the actual RDN?
if tmpRDN == self.getPrettyRDN():
return True
else:
return False
###############################################################################
def getPrettyDN(self):
""" Returns unicode string for the DN. All encoded special characters are
converted to their real repesentations.
"""
tmpList = explodeDN(self.dn)
newList = map(stripSpecialChars, tmpList)
tmpString = ",".join(newList)
return tmpString.decode('utf-8')
###############################################################################
def setDN(self, tmpString):
""" Set the dn of the current object.
"""
tmpList = explodeDN(tmpString)
newList = map(escapeSpecialChars, tmpList)
tmpString = ",".join(newList)
tmpString = unicode(tmpString)
self.dn = tmpString.encode('utf-8')
###############################################################################
def getPrettyRDN(self):
""" Returns unicode string for the RDN. All encoded special characters are
converted to their real repesentations.
"""
rdn = explodeDN(self.dn)[0]
tmpString = stripSpecialChars(rdn)
return tmpString.decode('utf-8')
###############################################################################
def getPrettyParentDN(self):
""" Returns a string with the parent of the current object. All encoded special
characters are converted to their real repesentations.
"""
tmpList = explodeDN(self.dn)
newList = map(stripSpecialChars, tmpList)
tmpString = ",".join(newList[1:])
return tmpString.decode('utf-8')
###############################################################################
def checkIntegrity(self):
isOk = True
errorList = []
# First check for objectClasses
objectClasses = self.getObjectClasses()
for x in objectClasses:
if not self.serverSchema.hasObjectClass(x):
isOk = False
errorList.append("ObjectClass " + x + " not present in server schema.")
# Now check if all attributes are present in schema
attributes = self.getAttributeList()
for x in attributes:
if not self.serverSchema.hasAttribute(x):
isOk = False
errorList.append("Attribute " + x + " not present in server schema.")
self.isValid = isOk
self.checkErrorMessageList = errorList
###############################################################################
def deleteAttributeValue(self, attributeName, index):
""" Delete value for attribute 'attributeName' at index 'index'.
"""
# Is attribute present?
if self.data.has_key(attributeName):
valueLength = len(self.data[attributeName])
# Is index lower or equal to the maximum length of the data?
if index <= (valueLength - 1):
# Is the value we want to delete the RDN of the object?
if self.isAttributeValueRDN(attributeName, self.data[attributeName][index]):
errorList = []
errorList.append("Can't delete attribute. Attribute is RDN of this object.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" To delete: Attribute=" + attributeName + " Index=" + str(index))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
# Is the attribute must and only one value present?
elif self.isAttributeMust(attributeName) and (valueLength == 1):
errorList = []
errorList.append("Can't delete attribute. Attribute is must and has no other value.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" To delete: Attribute=" + attributeName + " Index=" + str(index))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
# Delete value.
del(self.data[attributeName][index])
# we have deleted the last entry. so this is a shortcut.
# no more need to call len() angain.
if 1 == valueLength:
del(self.data[attributeName])
# Index for deletion is higher than length of current data.
else:
errorList = []
errorList.append("Can't delete attribute. Index for deletion is higher than actual data length.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" To delete: Attribute=" + attributeName + " Index=" + str(index))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
# We want to delete an attribute value for which no attribute entry exists.
else:
errorList = []
errorList.append("Can't delete attribute. Attribute is not present in current object.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" To delete: Attribute=" + attributeName + " Index=" + str(index))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
###############################################################################
def deleteAttribute(self, attributeName=None):
""" Delete attribute 'attributeName' from current object.
"""
# Has object the given attribute?
if self.data.has_key(attributeName):
# Is attribute must?
if self.isAttributeMust(attributeName):
errorList = []
errorList.append("Can't delete attribute. Attribute must be present in object.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" To delete: Attribute=" + attributeName + " Index=" + str(index))
errorList.append(". Current data: " + repr(self.data))
raise LdapDataException("".join(errorList))
pass
# Attribute is not must
else:
del self.data[attributeName]
###############################################################################
def addObjectClass(self, className):
""" Add an objectClass to the object.
"""
className = className.lower()
className = className.encode('utf-8')
if self.hasObjectClass(className):
return
if self.isObjectclassStructural(className):
structList = self.getStructuralClasses()
for x in structList:
if not self.serverSchema.sameObjectClassChain(className, x):
errorList = []
errorList.append("Can't add objectClass to object. ObjectClass " + className + " is structural and conflicts with other classes.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" Current objectClasses: " + repr(self.getObjectClasses()))
raise LdapDataException("".join(errorList))
self.data[self.objectClassName].append(className)
###############################################################################
def hasObjectClass(self, className):
""" Checks if the current object has the objectClass className.
"""
className = className.lower()
for x in self.getObjectClasses():
if className == x.lower():
return True
return False
###############################################################################
def hasAttribute(self, attributeName):
""" Check if the current object has the attribute attributeName.
"""
attributeName = attributeName.lower()
for x in self.getAttributeList():
if attributeName == x.lower():
return True
return False
###############################################################################
def deleteObjectClass(self, className):
className = className.lower()
found = False
for x in self.getObjectClasses():
if className == x.lower():
className = x
found = True
break
if not found:
return
# We have to make sure that one structural class remains after deleting
if self.isObjectclassStructural(className):
classList = self.getObjectClasses()
classList.remove(className)
tmpList = self.getObjectClassChain(className, classList)
if len(tmpList) == 0:
errorList = []
errorList.append("Can't delete objectClass to object. ObjectClass " + className + " is the last structural Class and can't be deleted.")
errorList.append(" DN: " + self.getPrettyDN() + ".")
errorList.append(" Current objectClasses: " + repr(self.getObjectClasses()))
raise LdapDataException("".join(errorList))
self.data[self.objectClassName].remove(className)
must, may = self.getPossibleAttributes()
all = Set(self.getAttributeList())
rest = all - must.union(may)
for x in rest:
self.deleteAttribute(x)
###############################################################################
def getObsoleteAttributes(self, className):
""" Returns list of attributes which will be removed when className
is removed from the list of objectClasses.
"""
attributeList = self.serverSchema.getAttributeListForObjectClass(className)
currentList = self.getAttributeList()
tmpList = []
for x in currentList:
if x in attributeList:
tmpList.append(x)
classList = self.getObjectClasses()
classList.remove(className)
attributeList = tmpList[:]
for x in tmpList:
removeAttribute = False
for y in classList:
# Attribute is provided by another objectclass and there is no
# need to remove it.
if x in self.serverSchema.getAttributeListForObjectClass(y):
removeAttribute = True
if removeAttribute:
attributeList.remove(x)
return attributeList
###############################################################################
def missingAttributes(self):
pass
###############################################################################
def checkSingleAttributes(self):
pass
###############################################################################
def checkMustAttributes(self):
pass
###############################################################################
def checkObjectClassCombination(self):
pass
###############################################################################+
def postaladdressDecode(self, address):
"""RFC 2252 requires a specific syntax for attributes
of type 'Postal Address'. This method ensures that
already wrong values in LDAP will can be shown correctly
in the GUI.
"""
address=address.replace('$','\n')
address=address.replace('\\24','$')
address=address.replace('\\5C','\\5c')
address=address.replace('\\5c','\\')
return address
###############################################################################+
def postaladdressEncode(self, address):
"""RFC 2252 requires a specific syntax for attributes
of type 'Postal Address'. This method ensures that
it will be converted correctly before saving to LDAP.
"""
address=address.replace('\\','\\5c')
address=address.replace('$','\\24')
address=address.replace('\n','$')
return address
###############################################################################
def convertToLdif(self):
""" Return the current object into LDIF format.
"""
tmpString = StringIO()
ldifWriter = ldif.LDIFWriter(tmpString)
ldifWriter.unparse(self.dn, self.data)
return tmpString.getvalue()
###############################################################################
def importFromLdif(self):
pass
###############################################################################
def convertToDsml(self):
""" Return the current object into DSML format.
"""
tmpString = StringIO()
dsmlWriter = dsml.DSMLWriter(tmpString)
dsmlWriter.writeRecord(self.dn, self.data)
return tmpString.getvalue()
###############################################################################
def importFromDsml(self):
pass
###############################################################################
def getCompatibleAttributes(self):
""" Returns all attributes which are supported by the server and can be
added without violating the objectclass chain (keyword: structural).
"""
pass
###############################################################################
def getPossibleAttributes(self):
""" Returns all attributes which are supported by the current objectclasses.
The result is one set with must attributes and one set with may attributes.
"""
return self.serverSchema.getAllAttributes(self.getObjectClasses())
###############################################################################
def isObjectclassStructural(self, objclass):
""" Returns True if the objectclass is structural"""
return self.serverSchema.isStructural(objclass)
###############################################################################
def getObjectClassChain(self, className, classList):
""" Returns a list of objectClasses which belong to the same chain
as className, given by classList.
"""
return self.serverSchema.getObjectClassChain(className, classList)
###############################################################################
###############################################################################
###############################################################################
class LdapDataException(Exception):
"""This exception class will be raised if invalid operations are done on
ldap data.
"""
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
###############################################################################
###############################################################################
###############################################################################
class FunctionArgumentException(Exception):
"""This exception class will be raised if a function is called with the wrong arguments.
"""
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
|