diff options
author | John Dennis <jdennis@redhat.com> | 2011-06-15 08:39:21 -0400 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2011-06-22 01:05:44 -0400 |
commit | 1a7d05cc0f8c06a6f59d0db75361fa1a06189bee (patch) | |
tree | 980942db683d845184fdbb1bc1ccd7233580bb08 /ipalib | |
parent | b227208d010bf88a11c46149ac5844c4a55ab9ad (diff) | |
download | freeipa-1a7d05cc0f8c06a6f59d0db75361fa1a06189bee.tar.gz freeipa-1a7d05cc0f8c06a6f59d0db75361fa1a06189bee.tar.xz freeipa-1a7d05cc0f8c06a6f59d0db75361fa1a06189bee.zip |
Module for DN objects plus unit test
Diffstat (limited to 'ipalib')
-rw-r--r-- | ipalib/dn.py | 1123 |
1 files changed, 1123 insertions, 0 deletions
diff --git a/ipalib/dn.py b/ipalib/dn.py new file mode 100644 index 000000000..89248ca8e --- /dev/null +++ b/ipalib/dn.py @@ -0,0 +1,1123 @@ +# Authors: +# John Dennis <jdennis@redhat.com> +# +# Copyright (C) 2011 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, either version 3 of the License, or +# (at your option) any later version. +# +# 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, see <http://www.gnu.org/licenses/>. + +from ldap.dn import str2dn, dn2str +from ldap import DECODING_ERROR +from copy import deepcopy + +__all__ = ['AVA', 'RDN', 'DN'] + +''' + +Goal +---- + +To allow a Python programmer the ability to operate on DN's +(Distinguished Names) in a simple intuitive manner supporting all the +Pythonic mechanisms for manipulating objects such that the simple +majority case remains simple with simple code, yet the corner cases +are fully supported. With the result both simple and complex cases are +100% correct. + +This is achieved with a fair of amount of syntax sugar which is best +described as "Do What I Mean" (i.e. DWIM). The class implementations +take simple expressions and internally convert them to their more +complex full definitions hiding much of the complexity from the +programmer. + +Anatomy of a DN +--------------- + +Some definitions: + +AVA + An AVA is an Attribute Value Assertion. In more simple terms it's + an attribute value pair typically expressed as attr=value + (e.g. cn=Bob). Both the attr and value in an AVA when expressed in + a string representation are subject to encoding rules. + +RDN + A RDN is a Relative Distinguished Name. A RDN is a non-empty set of + AVA's. In the common case a RDN is single valued consisting of 1 + AVA (e.g. cn=Bob). But a RDN may be multi-valued consisting of + more than one AVA. Because the RDN is a set of AVA's the AVA's are + unordered when they appear in a multi-valued RDN. In the string + representation of a RDN AVA's are separated by the plus sign (+). + +DN + A DN is a ordered sequence of 1 or more RDN's. In the string + representation of a DN each RDN is separated by a comma (,) + +Thus a DN is: + +Sequence of set of <encoded attr, encoded value> pairs + +The following are valid DN's + +# 1 RDN with 1 AVA (e.g. cn=Bob) +RDN(AVA) + +# 2 RDN's each with 1 AVA (e.g. cn=Bob,dc=redhat.com) +RDN(AVA),RDN(AVA) + +# 2 RDN's the first RDN is multi-valued with 2 AVA's +# the second RDN is singled valued with 1 AVA +# (e.g. cn=Bob+ou=people,dc=redhat.com +RDN({AVA,AVA}),RDN(AVA) + +Common programming mistakes +--------------------------- + +DN's present a pernicious problem for programmers. They appear to have +a very simple string format in the majority case, a sequence of +attr=value pairs separated by commas. For example: + +dn='cn=Bob,ou=people,dc=redhat,dc=com' + +As such there is a tendency to believe you can form DN's by simple +string manipulations such as: + +dn='%s=%s' % ('cn','Bob') + ',ou=people,dc=redhat,dc=com' + +Or to extract a attr & value by searching the string, for example: + +attr=dn[0 : dn.find('=')] +value=dn[dn.find('=')+1 : dn.find(',')] + +Or compare a value returned by an LDAP query to a known value: + +if value == 'Bob' + +All of these simple coding assumptions are WRONG and will FAIL when +a DN is not one of the simple DN's which are probably the 95% of all +DN's. This is what makes DN handling pernicious. What works in 95% of +the cases and is simple, fails for the 5% of DN's which are not +simple. + +Examples of where the simple assumptions fail are: + +* A RDN may be multi-valued + +* A multi-valued RDN has no ordering on it's components + +* Attr's and values must be UTF-8 encoded + +* String representations of AVA's, RDN's and DN's must be completely UTF-8 + +* An attr or value may have reserved characters which must be escaped. + +* Whitespace needs special handling + +To complicate matters a bit more the RFC for the string representation +of DN's (RFC 4514) permits a variety of different syntax's each of +which can evaluate to exactly the same DN but have different string +representations. For example, the attr "R,W" which contains a reserved +character (the comma) can be encoded as a string in these different +ways: + +'R\,W' # backslash escape +'R\2cW' # hexadecimal ascii escape +'#522C57' # binary encoded + +It should be clear a DN string may NOT be a simple string, rather a DN +string is ENCODED. For simple strings the encoding of the DN is +identical to the simple string value (this common case leads to +erroneous assumptions and bugs because it does not account for +encodings). + +The openldap library we use at the client level uses the backslash +escape form. The LDAP server we use uses the hexadecimal ascii escape +form. Thus 'R,W' appears as 'R\,W' when sent from the client to the +LDAP server as part of a DN. But when it's returned as a DN from the +server in an LDAP search it's returned as 'R\2cW'. Any attempt to +compare 'R\,W' to 'R\2cW' for equality will fail despite the fact they +are indeed equal once decoded. Such a test fails because you're +comparing two different encodings of the same value. In MIME you +wouldn't expect the base64 encoding of a string to be equal to the +same string encoded as quoted-printable would you? + +When you are comparing attrs or values which are part of a DN and +other string you MUST: + +* Know if either of the strings have been encoded and make sure you're + comparing only decoded components component-wise. + +* Extract the component from the DN and decode it. You CANNOT decode + the entire DN as a string and operate on it. Why? Consider a value + with a comma embedded in it. For example: + + cn=R\2cW,cn=privilege + + Is a DN with 2 RDN components: cn=R,W followed by "cn=privilege" + + But if you decode the entire DN string as a whole you would get: + + cn=R,W,cn=privilege + + Which is a malformed DN with 3 RDN's, the 2nd RDN is invalid. + +* Determine if a RDN is multi-valued, if so you must account + for the fact each AVA component in the multi-valued RDN can appear + in any order and still be equivalent. For example the following two + RDN's are equal: + + cn=Bob+ou=people + ou=people+cn=Bob + + In addition each AVA (cn=Bob & ou=people) needs to be + INDEPENDENTLY decoded prior to comparing the unordered set of AVA's + in the multi-valued RDN. + +If you are trying to form a new DN or RDN from a raw string you cannot +simply do string concatenation or string formatting unless you ESCAPE +the components independently prior to concatenation, for example: + + base = 'dc=redhat,dc=com' + value = 'R,W' + dn = 'cn=%s,%s' % (value, base) + +Will result in the malformed DN 'cn=R,W,dc=redhat,dc=com' + +Syntax Sugar +------------ + +The majority of DN's have a simple string form: + +attr=value,attr=value + +We want the programmer to be able to create DN's, compare them, and +operate on their components as simply and concisely as possible so +the classes are implemented to provide a lot of syntax sugar. + +The classes automatically handle UTF-8 <-> Unicode conversions. Every +attr and value which is returned from a class will be Unicode. Every +attr and value assigned into an object will be promoted to +Unicode. All string representations in RFC 4514 format will be UTF-8 +and properly escaped. Thus at the "user" or "API" level every string +is Unicode with the single exception that the str() method returns RFC +compliant escaped UTF-8. + +RDN's are assumed to be single-valued. If you need a multi-valued RDN +(an exception) you must explicitly create a multi-valued RDN. + +Thus DN's are assumed to be a sequence of attr, value pairs. + +The attr and value in the pair MUST be strings (we'll see why in a +moment). + +You can express any part of a DN as an even numbered sequence of +strings. + + DN('cn', 'Bob', 'dc', 'redhat.com') + +This is equivalent to the DN string: + + cn=Bob,dc=redhat.com + +And is exactly equal to: + + DN(RDN(AVA('cn','Bob')),RDN(AVA('dc','redhat.com'))) + +The following are alternative syntax's which are all exactly +equivalent to the above example. + +If you prefer to be more explicit about the pair-wise grouping (or you +have to have a pair) you can use tuples or lists with 2 elements. + + DN(('cn', 'Bob'), ('dc', 'redhat.com')) + DN(['cn', 'Bob'], ['dc', 'redhat.com']) + +You can provide a properly escaped string representation. + + DN('cn=Bob,dc=redhat.com') + +You can mix and match any of the forms. + + DN('cn', 'Bob', ['dc', 'redhat.com']) + DN('cn', 'Bob', 'dc=redhat.com') + DN('cn', 'Bob', RDN('dc', 'redhat.com')) + +Note: this is why attr's and values must be strings, the parsing logic +assumes 2 consecutive strings in a sequence is always a single valued +RDN, everything else is interpreted according to it's type. + +AVA's have an attr and value property, thus if you have an AVA + +# Get the attr and value +ava.attr -> u'cn' +ava.value -> u'Bob' + +# Set the attr and value +ava.attr = 'cn' +ava.value = 'Bob' + +But since RDN's are assumed to be single valued, exactly the same +behavior applies to an RDN (it will throw an exception if the RDN is +not single valued) + +# Get the attr and value +rdn.attr -> u'cn' +rdn.value -> u'Bob' + +# Set the attr and value +rdn.attr = 'cn' +rdn.value = 'Bob' + +Also RDN's can be indexed by name or position (see the RDN class doc +for details). + +rdn['cn'] -> u'Bob' +rdn[0] -> AVA('cn', 'Bob') + +A DN is a sequence of RDN's, as such any of Python's container +operators can be applied to a DN in a intuitive way. + +# How many RDN's in a DN? +len(dn) + +# WARNING, this a count of RDN's not how characters there are in the +# string representation the dn, instead that would be: +len(str(dn)) + +# Iterate over each RDN in a DN +for rdn in dn: + +# Get the first RDN in a DN +dn[0] -> RDN('cn', 'Bob') + +# Get the value of the first RDN in a DN +dn[0].value -> u'Bob' + +# Get the value of the first RDN by indexing by attr name +dn['cn'] -> u'Bob' + +# WARNING, when a string is used as an index key the FIRST RDN's value +# in the sequence whose attr matches the key is returned. Thus if you +# have a DN like this "cn=foo,cn=bar" then dn['cn'] will always return +# 'foo' even though there is another attr with the name 'cn'. This is +# almost always what the programmer wants. See the class doc for how +# you can override this default behavior and get a list of every value +# whose attr matches the key. + +# Set the first RDN in the DN (all are equivalent) +dn[0] = 'cn', 'Bob' +dn[0] = ('cn', 'Bob') +dn[0] = RDN('cn', 'Bob') + +dn[0].attr = 'cn' +dn[0].value = 'Bob' + +# Get the first two RDN's using slices +dn[0:2] + +# Get the last two RDN's using slices +dn[-2:] + +# Get a list of all RDN's using slices +dn[:] + +# Set the 2nd and 3rd RDN using slices (all are equivalent) +dn[1:4] = 'cn', 'Bob, 'dc', 'redhat.com' +dn[1:4] = ('cn', 'Bob), ('dc', 'redhat.com') +dn[1:4] = RDN('cn', 'Bob), RDN('dc', 'redhat.com') + +String representations and escapes: + +# To get an RFC compliant string representation of a DN, RDN or AVA +# simply call str() on it or evaluate it in a string context. +str(dn) -> 'cn=Bob,dc=redhat.com' + +# When working with attr's and values you do not have to worry about +# escapes, simply use the raw unescaped string in a natural fashion. + +rdn = RDN('cn', 'R,W') + +# Thus: +rdn.value == 'R,W' -> True + +# But: +str(rdn) == 'cn=R,W' -> False +# Because: +str(rdn) -> 'cn=R\2cW' or 'cn='R\,W' # depending on the underlying LDAP library + +Equality and Comparing: + +# All DN's, RDN's and AVA's support equality testing in an intuitive +# manner. +dn1 = DN('cn', 'Bob') +dn2 = DN(RDN('cn', 'Bob')) +dn1 == dn2 -> True +dn1[0] == dn2[0] -> True +dn1[0].value = 'Bobby' +dn1 == dn2 -> False + +# See the class doc for how DN's, RDN's and AVA's compare +# (e.g. cmp()). The general rule is for objects supporting multiple +# values first their lengths are compared, then if the lengths match +# the respective components of each are pair-wise compared until one +# is discovered to be non-equal + +Concatenation and In-Place Addition: + +# DN's and RDN's can be concatenated. +# Return a new DN by appending the RDN's of dn2 to dn1 +dn3 = dn1 + dn2 + +# Append a RDN to DN's RDN sequence (all are equivalent) +dn += 'cn', 'Bob' +dn += ('cn', 'Bob') +dn += RDN('cn', 'Bob') + +# Append a DN to an existing DN +dn1 += dn2 + +Finally see the unittest for a more complete set of ways you can +manipulate these objects. + +''' + +class AVA(object): + ''' + An AVA is an LDAP Attribute Value Assertion. It is convenient to think of + AVA's as a <attr,value> pair. AVA's are members of RDN's (Relative + Distinguished Name). + + The AVA constructor may be invoked with any of the following methods: + + 1) With 2 string (or unicode) arguments, the first argument will be the + attr, the 2nd the value. + + 2) With a sigle list or tuple argument containing exactly 2 string (or unicode + members), the first member is the attr and the second is the value. + + 3) With a single string (or unicode) argument, in this case the string will + be interpretted using the DN syntax described in RFC 4514 to yield a AVA + <attr,value> pair. The parsing recognizes the DN syntax escaping rules. + + For example: + + ava = AVA('cn', 'Bob') # case 1: two strings + ava = AVA(('cn', 'Bob')) # case 2: 2-valued tuple + ava = AVA(['cn', 'Bob']) # case 2: 2-valued list + ava = AVA('cn=Bob') # case 3: DN syntax + + AVA object have two properties for accessing their data: + + attr: the attribute name, cn in our exmaple + value: the attribute's value, Bob in our example + + When attr and value are returned they will always be unicode. When + attr or value are set they will be promoted to unicode. + + AVA objects support indexing by name, e.g. + + ava['cn'] + + returns the value (Bob in our example). If the index does key does not match + the attr then a KeyError will be raised. + + AVA objects support equality testing and comparsion (e.g. cmp()). When they + are compared the attr is compared first, if the 2 attr's are equal then the + values are compared. + + The str method of an AVA returns the string representation in RFC 4514 DN + syntax with proper escaping. + ''' + flags = 0 + + def __init__(self, *args): + if len(args) == 1: + arg = args[0] + if isinstance(arg, basestring): + try: + rdns = str2dn(arg.encode('utf-8')) + except DECODING_ERROR: + raise ValueError("malformed AVA string = \"%s\"" % arg) + if len(rdns) != 1: + raise ValueError("multiple RDN's specified by \"%s\"" % (arg)) + rdn = rdns[0] + if len(rdn) != 1: + raise ValueError("multiple AVA's specified by \"%s\"" % (arg)) + ava = rdn[0] + elif isinstance(arg, (tuple, list)): + ava = arg + if len(ava) != 2: + raise ValueError("tuple or list must be 2-valued, not \"%s\"" % (ava)) + else: + raise TypeError("with 1 argument, argument must be str,unicode,tuple or list, got %s instead" % \ + arg.__class__.__name__) + + attr = ava[0] + value = ava[1] + elif len(args) == 2: + attr = args[0] + value = args[1] + else: + raise TypeError("takes 1 or 2 arguments (%d given)" % (len(args))) + + if not isinstance(attr, basestring): + raise TypeError("attr must be basestring, got %s instead" % attr.__class__.__name__) + if not isinstance(value, basestring): + raise TypeError("value must be basestring, got %s instead" % value.__class__.__name__) + + self.attr = attr.decode('utf-8') + self.value = value.decode('utf-8') + + + def _to_openldap(self): + return [[(self.attr.encode('utf-8'), self.value.encode('utf-8'), self.flags)]] + + def __str__(self): + return dn2str(self._to_openldap()) + + def __getitem__(self, key): + if isinstance(key, basestring): + if key == self.attr: + return self.value + raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) + else: + raise TypeError("unsupported type for %s indexing, must be basestring; not %s" % \ + (self.__class__.__name__, key.__class__.__name__)) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("expected %s but got %s" % (self.__class__.__name__, other.__class__.__name__)) + + return self.attr == other.attr and self.value == other.value + + def __cmp__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("expected %s but got %s" % (self.__class__.__name__, other.__class__.__name__)) + + result = cmp(self.attr, other.attr) + if result != 0: return result + result = cmp(self.value, other.value) + return result + +class RDN(object): + ''' + An RDN is a LDAP Relative Distinguished Name. RDN's are members of DN's + (Distinguished Name). An RDN contains 1 or more AVA's. If the RDN contains + more than one AVA it is said to be a multi-valued RDN. When an RDN is + multi-valued the AVA's are unorderd comprising a set. However this + implementation orders the AVA's according to the AVA comparison function to + make equality and comparison testing easier. Think of this a canonical + normalization (however LDAP does not impose any ordering on multiple AVA's + within an RDN). Single valued RDN's are the norm. + + The RDN constructor may be invoked in a variety of different ways. + + * When two adjacent string (or unicode) argument appear together in the + argument list they are taken to be the <attr,value> pair of an AVA. An AVA + object is constructed and inserted into the RDN. Multiple pairs of strings + arguments may appear in the argument list, each pair add one additional AVA + to the RDN. + + * A 2-valued tuple or list denotes the <attr,value> pair of an AVA. The + first member is the attr and the second member is the value, both members + must be strings (or unicode). The tuple or list is passed to the AVA + constructor and the resulting AVA is added to the RDN. Multiple tuples or + lists may appear in the argument list, each adds one additional AVA to the + RDN. + + * A single string (or unicode) argument, in this case the string will + be interpretted using the DN syntax described in RFC 4514 to yield one or + more AVA <attr,value> pairs. The parsing recognizes the DN syntax escaping + rules. + + Note, a DN syntax argument is distguished from AVA string pairs by testing + to see if two strings appear adjacent in the argument list, if so those two + strings are interpretted as an <attr,value> AVA pair and consumed. + + * A AVA object. Each AVA object in the argument list will be added to the RDN. + + Single AVA Examples: + + RDN('cn', 'Bob') # 2 adjacent strings yield 1 AVA + RDN(('cn', 'Bob')) # tuple yields 1 AVA + RDN('cn=Bob') # DN syntax with 1 AVA + RDN(AVA('cn', 'Bob')) # AVA object adds 1 AVA + + Multiple AVA Examples: + + RDN('cn', 'Bob', 'ou', 'people') # 2 strings pairs yield 2 AVA's + RDN(('cn', 'Bob'),('ou', 'people')) # 2 tuples yields 2 AVA's + RDN('cn=Bob+ou=people') # DN syntax with 2 AVA's + RDN(AVA('cn', 'Bob'),AVA('ou', 'people')) # 2 AVA objects adds 2 AVA's + RDN('cn', 'Bob', "ou=people') # 3 strings, 1st two strings form 1 AVA + # 3rd string DN syntax for 1 AVA, + # adds 2 AVA's in total + + Note: The RHS of a slice assignment is interpreted exactly in the + same manner as the constructor argument list (see above examples). + + RDN objects support iteration over their AVA members. You can iterate all + AVA members via any Python iteration syntax. RDN objects support full Python + indexing using bracket [] notation. Examples: + + len(rdn) # return the number of AVA's + rdn[0] # indexing the first AVA + rdn['cn'] # index by AVA attr, returns AVA value + for ava in rdn: # iterate over each AVA + rdn[:] # a slice, in this case a copy of each AVA + + WARNING: When indexing by attr (e.g. rdn['cn']) there is a possibility more + than one AVA has the same attr name as the index key. The default behavior + is to return the value of the first AVA whose attr matches the index + key. This behavior can be modified by setting the first_key_match property + to false in the RDN object. If first_key_match is False a list of all values + will be returned instead. The first_key_match behavior is the default and is + useful because duplicate attr names in multi-valued RDN's are rare. We seek + the most useful common case for programmer friendliness, but you should be + aware of the caveat. + + RDN objects support the AVA attr and value properties as another programmer + convenience because the vast majority of RDN's are single valued. The attr + and value properties return the attr and value properties of the first AVA + in the RDN, for example: + + rdn = RDN('cn', 'Bob') # rdn has 1 AVA whose attr == 'cn' and value == 'Bob' + len(rdn) -> 1 + rdn.attr -> u'cn' # exactly equivalent to rdn[0].attr + rdn.value -> u'Bob' # exactly equivalent to rdn[0].value + + When attr and value are returned they will always be unicode. When + attr or value are set they will be promoted to unicode. + + If an RDN is multi-valued the attr and value properties still return only + the first AVA's properties, programmer beware! Recall the AVA's in the RDN + are sorted according the to AVA collating semantics. + + RDN objects support equality testing and comparision. See AVA for the + definition of the comparision method. + + RDN objects support concatenation and addition with other RDN's or AVA's + + rdn1 + rdn2 # yields a new RDN object with the contents of each RDN. + rdn1 + ava1 # yields a new RDN object with the contents of rdn1 and ava1 + + RDN objects can add AVA's objects via in-place addition. + + rdn1 += rdn2 # rdn1 now contains the sum of rdn1 and rdn2 + rdn1 += ava1 # rdn1 has ava1 added to it. + + The str method of an RDN returns the string representation in RFC 4514 DN + syntax with proper escaping. + ''' + + flags = 0 + + def __init__(self, *args): + self.first_key_match = True + self.avas = self._avas_from_sequence(args) + self.avas.sort() + + def _ava_from_value(self, value): + if isinstance(value, AVA): + return value + elif isinstance(value, basestring): + try: + rdns = str2dn(value.encode('utf-8')) + if len(rdns) != 1: + raise ValueError("multiple RDN's specified by \"%s\"" % (value)) + rdn = rdns[0] + if len(rdn) == 1: + return AVA(rdn[0][0], rdn[0][1]) + else: + avas = [] + for ava_tuple in rdn: + avas.append(AVA(ava_tuple[0], ava_tuple[1])) + return avas + except DECODING_ERROR: + raise ValueError("malformed RDN string = \"%s\"" % value) + elif isinstance(value, (tuple, list)): + if len(value) != 2: + raise ValueError("tuple or list must be 2-valued, not \"%s\"" % (value)) + return AVA(value) + else: + raise TypeError("must be str,unicode,tuple, or AVA, got %s instead" % \ + value.__class__.__name__) + + + def _avas_from_sequence(self, seq): + avas = [] + + i = 0 + while i < len(seq): + if i+1 < len(seq) and \ + isinstance(seq[i], basestring) and \ + isinstance(seq[i+1], basestring): + ava = AVA(seq[i], seq[i+1]) + avas.append(ava) + i += 2 + else: + arg = seq[i] + ava = self._ava_from_value(arg) + if isinstance(ava, list): + avas.extend(ava) + else: + avas.append(ava) + i += 1 + return avas + + def _to_openldap(self): + return [[(ava.attr.encode('utf-8'), ava.value.encode('utf-8'), self.flags) for ava in self.avas]] + + def __str__(self): + return dn2str(self._to_openldap()) + + def _next(self): + for ava in self.avas: + yield ava + + def __iter__(self): + return self._next() + + def __len__(self): + return len(self.avas) + + def __getitem__(self, key): + if isinstance(key, (int, long, slice)): + return self.avas[key] + elif isinstance(key, basestring): + if self.first_key_match: + for ava in self.avas: + if key == ava.attr: + return ava.value + raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) + else: + avas = [] + for ava in self.avas: + if key == ava.attr: + avas.append(ava.value) + if len(avas) > 0: + return avas + raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) + else: + raise TypeError("unsupported type for %s indexing, must be int, basestring or slice; not %s" % \ + (self.__class__.__name__, key.__class__.__name__)) + + def __setitem__(self, key, value): + if isinstance(key, (int, long)): + new_ava = self._ava_from_value(value) + if isinstance(new_ava, list): + raise TypeError("multiple AVA's") + self.avas[key] = new_ava + elif isinstance(key, slice): + avas = self._avas_from_sequence(value) + self.avas[key] = avas + elif isinstance(key, basestring): + new_ava = self._ava_from_value(value) + if isinstance(new_ava, list): + raise TypeError("cannot assign multiple values to single entry") + found = False + i = 0 + while i < len(self.avas): + if key == self.avas[i].attr: + found = True + self.avas[i] = new_ava + if self.first_key_match: break + i += 1 + if not found: + raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) + else: + raise TypeError("unsupported type for %s indexing, must be int, basestring or slice; not %s" % \ + (self.__class__.__name__, key.__class__.__name__)) + + def _get_attr(self): + if len(self.avas) == 0: + raise IndexError("No AVA's in this RDN") + return self.avas[0].attr + + def _set_attr(self, new_attr): + if len(self.avas) == 0: + raise IndexError("No AVA's in this RDN") + + if not isinstance(new_attr, basestring): + raise TypeError("attr must be basestring, got %s instead" % new_attr.__class__.__name__) + + self.avas[0].attr = new_attr + + attr = property(_get_attr, _set_attr) + + def _get_value(self): + if len(self.avas) == 0: + raise IndexError("No AVA's in this RDN") + return self.avas[0].value + + def _set_value(self, new_value): + if len(self.avas) == 0: + raise IndexError("No AVA's in this RDN") + + if not isinstance(new_value, basestring): + raise TypeError("value must be basestring, got %s instead" % new_value.__class__.__name__) + + self.avas[0].value = new_value + + value = property(_get_value, _set_value) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("expected %s but got %s" % (self.__class__.__name__, other.__class__.__name__)) + + return self.avas == other.avas + + def __cmp__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("expected %s but got %s" % (self.__class__.__name__, other.__class__.__name__)) + + result = cmp(len(self), len(other)) + if result != 0: return result + i = 0 + while i < len(self): + result = cmp(self[i], other[i]) + if result != 0: return result + i += 1 + return 0 + + def __add__(self, other): + result = deepcopy(self) + if isinstance(other, self.__class__): + for ava in other.avas: + result.avas.append(deepcopy(ava)) + elif isinstance(other, AVA): + result.avas.append(deepcopy(other)) + elif isinstance(other, basestring): + rdn = RDN(other) + for ava in rdn.avas: + result.avas.append(deepcopy(ava)) + else: + raise TypeError("expected RDN, AVA or basestring but got %s" % (other.__class__.__name__)) + + result.avas.sort() + return result + + def __iadd__(self, other): + if isinstance(other, self.__class__): + for ava in other.avas: + self.avas.append(deepcopy(ava)) + elif isinstance(other, AVA): + self.avas.append(deepcopy(other)) + elif isinstance(other, basestring): + rdn = RDN(other) + for ava in rdn.avas: + self.avas.append(deepcopy(ava)) + else: + raise TypeError("expected RDN, AVA or basestring but got %s" % (other.__class__.__name__)) + + self.avas.sort() + return self + +class DN(object): + ''' + A DN is a LDAP Distinguished Name. A DN is an ordered sequence of RDN's. + + The DN constructor may be invoked in a variety of different ways. + + * When two adjacent string (or unicode) argument appear together in the + argument list they are taken to be the <attr,value> pair of a + singled valued RDN. An RDN + object is constructed and inserted into the DN. Multiple pairs of strings + arguments may appear in the argument list, each pair adds one additional RDN + to the DN. + + * A 2-valued tuple or list denotes the <attr,value> pair of an RDN. The + first member is the attr and the second member is the value, both members + must be strings (or unicode). The tuple or list is passed to the RDN + constructor and the resulting RDN is added to the DN. Multiple tuples or + lists may appear in the argument list, each adds one additional RDN to the + DN. + + * A single string (or unicode) argument, in this case the string will + be interpretted using the DN syntax described in RFC 4514 to yield one or + more RDN's. The parsing recognizes the DN syntax escaping + rules. + + Note, a DN syntax argument is distguished from RDN string pairs by testing + to see if two strings appear adjacent in the argument list, if so those two + strings are interpretted as an <attr,value> RDN pair and consumed. + + * A RDN object. Each RDN object in the argument list will be added to the DN. + + * A DN object. Each DN object in the argument list will add it's RDN's to the DN. + + Single DN Examples: + + DN('cn', 'Bob') # 2 adjacent strings yield 1 RDN + DN(('cn', 'Bob')) # tuple yields 1 RDN + DN('cn=Bob') # DN syntax with 1 RDN + DN(RDN('cn', 'Bob')) # RDN object adds 1 RDN + + Multiple RDN Examples: + + DN('cn', 'Bob', 'ou', 'people') # 2 strings pairs yield 2 RDN's + DN(('cn', 'Bob'),('ou', 'people')) # 2 tuples yields 2 RDN's + DN('cn=Bob,ou=people') # DN syntax with 2 RDN's + DN(RDN('cn', 'Bob'),RDN('ou', 'people')) # 2 RDN objects adds 2 RDN's + DN('cn', 'Bob', "ou=people') # 3 strings, 1st two strings form 1 RDN + # 3rd string DN syntax for 1 RDN, + # adds 2 RDN's in total + DN('cn', 'Bob', DN(container), DN(base)) # 1st two strings form 1 RDN + # then the RDN's from container are added + # followed by the RDN from base + + Note: The RHS of a slice assignment is interpreted exactly in the + same manner as the constructor argument list (see above examples). + + DN objects support iteration over their RDN members. You can iterate all + RDN members via any Python iteration syntax. DN objects support full Python + indexing using bracket [] notation. Examples: + + len(rdn) # return the number of RDN's + rdn[0] # indexing the first RDN + rdn['cn'] # index by RDN attr, returns RDN value + for ava in rdn: # iterate over each RDN + rdn[:] # a slice, in this case a copy of each RDN + + WARNING: When indexing by attr (e.g. rdn['cn']) there is a possibility more + than one RDN has the same attr name as the index key. The default behavior + is to return the value of the first RDN whose attr matches the index + key. This behavior can be modified by setting the first_key_match property + to false in the RDN object. If first_key_match is False a list of all values + will be returned instead. The first_key_match behavior is the default and is + useful because typical usage is to seek the first matching RDN. We seek + the most useful common case for programmer friendliness, but you should be + aware of the caveat. + + DN object support slices. + + # Get the first two RDN's using slices + dn[0:2] + + # Get the last two RDN's using slices + dn[-2:] + + # Get a list of all RDN's using slices + dn[:] + + # Set the 2nd and 3rd RDN using slices (all are equivalent) + dn[1:4] = 'cn', 'Bob, 'dc', 'redhat.com' + dn[1:4] = ('cn', 'Bob), ('dc', 'redhat.com') + dn[1:4] = RDN('cn', 'Bob), RDN('dc', 'redhat.com') + + DN objects support equality testing and comparision. See RDN for the + definition of the comparision method. + + DN objects support concatenation and addition with other DN's or RDN's + or strings (interpreted as RFC 4514 DN syntax). + + # yields a new DN object with the RDN's of dn2 appended to the RDN's of dn1 + dn1 + dn2 + + # yields a new DN object with the rdn1 appended to the RDN's of dn1 + dn1 + rdn1 + + DN objects can add RDN's objects via in-place addition. + + dn1 += dn2 # dn2 RDN's are appended to the dn1's RDN's + dn1 += rdn1 # dn1 has rdn appended to its RDN's + dn1 += "dc=redhat.com" # string is converted to DN, then appended + + The str method of an DN returns the string representation in RFC 4514 DN + syntax with proper escaping. + ''' + + flags = 0 + + def __init__(self, *args): + self.first_key_match = True + self.rdns = self._rdns_from_sequence(args) + + def _rdn_from_value(self, value): + if isinstance(value, RDN): + return value + elif isinstance(value, basestring): + try: + rdns = str2dn(value.encode('utf-8')) + for rdn_list in rdns: + avas = [] + for ava_tuple in rdn_list: + avas.append(AVA(ava_tuple[0], ava_tuple[1])) + rdn = RDN(*avas) + return rdn + except DECODING_ERROR: + raise ValueError("malformed RDN string = \"%s\"" % value) + elif isinstance(value, (tuple, list)): + if len(value) != 2: + raise ValueError("tuple or list must be 2-valued, not \"%s\"" % (rdn)) + rdn = RDN(value) + return rdn + else: + raise TypeError("single argument must be str,unicode,tuple, or RDN, got %s instead" % \ + value.__class__.__name__) + + def _rdns_from_sequence(self, seq): + self.first_key_match = True + rdns = [] + + i = 0 + while i < len(seq): + if i+1 < len(seq) and \ + isinstance(seq[i], basestring) and \ + isinstance(seq[i+1], basestring): + rdn = RDN(seq[i], seq[i+1]) + rdns.append(rdn) + i += 2 + else: + arg = seq[i] + i += 1 + if isinstance(arg, RDN): + rdns.append(arg) + elif isinstance(arg, DN): + for rdn in arg.rdns: + rdns.append(deepcopy(rdn)) + elif isinstance(arg, basestring): + try: + dn_list = str2dn(arg.encode('utf-8')) + for rdn_list in dn_list: + avas = [] + for ava_tuple in rdn_list: + avas.append(AVA(ava_tuple[0], ava_tuple[1])) + rdn = RDN(*avas) + rdns.append(rdn) + except DECODING_ERROR: + raise ValueError("malformed RDN string = \"%s\"" % arg) + elif isinstance(arg, (tuple, list)): + if len(arg) != 2: + raise ValueError("tuple or list must be 2-valued, not \"%s\"" % (rdn)) + rdn = RDN(arg) + rdns.append(rdn) + else: + raise TypeError("single argument must be str,unicode,tuple, or RDN, got %s instead" % \ + arg.__class__.__name__) + return rdns + + def _to_openldap(self): + return [[(ava.attr.encode('utf-8'), ava.value.encode('utf-8'), self.flags) for ava in rdn] for rdn in self.rdns] + + def __str__(self): + return dn2str(self._to_openldap()) + + def _next(self): + for rdn in self.rdns: + yield rdn + + def __iter__(self): + return self._next() + + def __len__(self): + return len(self.rdns) + + def __getitem__(self, key): + if isinstance(key, (int, long, slice)): + return self.rdns[key] + elif isinstance(key, basestring): + if self.first_key_match: + for rdn in self.rdns: + if key == rdn.attr: + return rdn.value + raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) + else: + rdns = [] + for rdn in self.rdns: + if key == rdn.attr: + rdns.append(rdn.value) + if len(rdns) > 0: + return rdns + raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) + else: + raise TypeError("unsupported type for %s indexing, must be int, basestring or slice; not %s" % \ + (self.__class__.__name__, key.__class__.__name__)) + + def __setitem__(self, key, value): + if isinstance(key, (int, long)): + new_rdn = self._rdn_from_value(value) + if isinstance(new_rdn, list): + raise TypeError("multiple RDN's") + self.rdns[key] = new_rdn + elif isinstance(key, slice): + rdns = self._rdns_from_sequence(value) + self.rdns[key] = rdns + elif isinstance(key, basestring): + new_rdn = self._rdn_from_value(value) + if isinstance(new_rdn, list): + raise TypeError("cannot assign multiple values to single entry") + found = False + i = 0 + while i < len(self.rdns): + if key == self.rdns[i].attr: + found = True + self.rdns[i] = new_rdn + if self.first_key_match: break + i += 1 + if not found: + raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) + else: + raise TypeError("unsupported type for %s indexing, must be int, basestring or slice; not %s" % \ + (self.__class__.__name__, key.__class__.__name__)) + + def __eq__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("expected %s but got %s" % (self.__class__.__name__, other.__class__.__name__)) + + return self.rdns == other.rdns + + def __cmp__(self, other): + if not isinstance(other, self.__class__): + raise TypeError("expected %s but got %s" % (self.__class__.__name__, other.__class__.__name__)) + + result = cmp(len(self), len(other)) + if result != 0: return result + i = 0 + while i < len(self): + result = cmp(self[i], other[i]) + if result != 0: return result + i += 1 + return 0 + + def __add__(self, other): + result = deepcopy(self) + if isinstance(other, self.__class__): + for rdn in other.rdns: + result.rdns.append(deepcopy(rdn)) + elif isinstance(other, RDN): + result.rdns.append(deepcopy(other)) + elif isinstance(other, basestring): + dn = DN(other) + for rdn in dn.rdns: + result.rdns.append(deepcopy(rdn)) + else: + raise TypeError("expected DN, RDN or basestring but got %s" % (other.__class__.__name__)) + + return result + + def __iadd__(self, other): + if isinstance(other, self.__class__): + for rdn in other.rdns: + self.rdns.append(deepcopy(rdn)) + elif isinstance(other, RDN): + self.rdns.append(deepcopy(other)) + elif isinstance(other, basestring): + dn = DN(other) + self.__iadd__(dn) + else: + raise TypeError("expected DN, RDN or basestring but got %s" % (other.__class__.__name__)) + + return self + |