diff options
Diffstat (limited to 'ipalib/dn.py')
-rw-r--r-- | ipalib/dn.py | 1337 |
1 files changed, 0 insertions, 1337 deletions
diff --git a/ipalib/dn.py b/ipalib/dn.py deleted file mode 100644 index 6f2f7deb5..000000000 --- a/ipalib/dn.py +++ /dev/null @@ -1,1337 +0,0 @@ -# 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 -import codecs -import sys - -utf8_codec = codecs.lookup('utf-8') - -__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 (simple DN's 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 -'#722C77' # 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, which is -equivalent to a sequence of RDN's. The attr and value in the pair MUST -be strings. - -The DN and RDN constructors take a sequence, the constructor parses -the sequence to find items it knows about. - -The DN constructor will accept in it's sequence: - * tuple of 2 strings, converting it to an RDN - * list of 2 strings, converting it to an RDN - * a RDN object - * a DN syntax string (e.g. 'cn=Bob,dc=redhat.com') - -Note DN syntax strings should be avoided if possible when passing to a -constructor because they run afoul of the problems outlined above -which the DN, RDN & AVA classes are meant to overcome. But sometimes a -DN syntax string is all you have to work with. DN strings which come -from a LDAP library or server will be properly formed and it's safe to -use those. However DN strings provided via user input should be -treated suspiciously as they may be improperly formed. You can test -for this by passing the string to the DN constructor and see if it -throws an exception. - -The sequence passed to the DN constructor takes each item in order, -produces one or more RDN's from it and appends those RDN in order to -its internal RDN sequence. - -For example: - - 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. - - DN(['cn', 'Bob'], ['dc', 'redhat.com']) - DN(RDN('cn', 'Bob'), RDN('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 in the constructor parameter -list. - - DN(('cn', 'Bob'), 'dc=redhat.com') - DN(('cn', 'Bob'), RDN('dc', 'redhat.com')) - -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' - -Since RDN's are assumed to be single valued, exactly the same -behavior applies to an RDN. If the RDN is multi-valued then the attr -property returns the attr of the first AVA, likewise for the value. - -# 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:3] = ('cn', 'Bob), ('dc', 'redhat.com') -dn[1:3] = 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 - -DN objects implement startswith(), endswith() and the "in" membership -operator. You may pass a DN or RDN object to these. Examples: - -if dn.endswith(base_dn): -if dn.startswith(rdn1): -if container_dn in dn: - -# 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. The comparision is case insensitive. - -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 += 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. - -''' - -def _adjust_indices(start, end, length): - 'helper to fixup start/end slice values' - - if end > length: - end = length - elif end < 0: - end += length - if end < 0: - end = 0 - - if start < 0: - start += length - if start < 0: - start = 0 - - return start, end - -class AVA(object): - ''' - AVA(arg0, ...) - - 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 is passed a sequence of args and a set of - keyword parameters used for configuration. - - The arg sequence may be: - - 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 comparision is case insensitive (because attr's map - to numeric OID's and their values derive from from the 'name' atribute type - (OID 2.5.4.41) whose EQUALITY MATCH RULE is caseIgnoreMatch. - - The str method of an AVA returns the string representation in RFC 4514 DN - syntax with proper escaping. - ''' - flags = 0 - - def __init__(self, *args, **kwds): - 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 - self.value = value - - def _get_attr(self): - return self._attr_unicode - - def _set_attr(self, new_attr): - if not isinstance(new_attr, basestring): - raise TypeError("attr must be basestring, got %s instead" % new_attr.__class__.__name__) - - if isinstance(new_attr, unicode): - self._attr_unicode = new_attr - else: - self._attr_unicode = utf8_codec.decode(new_attr)[0] - - attr = property(_get_attr, _set_attr) - - def _get_value(self): - return self._value_unicode - - def _set_value(self, new_value): - if not isinstance(new_value, basestring): - raise TypeError("value must be basestring, got %s instead" % new_value.__class__.__name__) - - if isinstance(new_value, unicode): - self._value_unicode = new_value - else: - self._value_unicode = utf8_codec.decode(new_value)[0] - - value = property(_get_value, _set_value) - - def _to_openldap(self): - return [[(self._attr_unicode.encode('utf-8'), self._value_unicode.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_unicode: - return self._value_unicode - raise KeyError("\"%s\" not found in %s" % (key, self.__str__())) - else: - raise TypeError("unsupported type for AVA indexing, must be basestring; not %s" % \ - (key.__class__.__name__)) - - def __eq__(self, other): - ''' - The attr comparison is case insensitive because attr is - really an LDAP attribute type which means it's specified with - an OID (dotted number) and not a string. Since OID's are - numeric the human readable name which maps to the OID is not - significant in case. - - The value comparison is also case insensitive because the all - attribute types used in a DN are derived from the 'name' - atribute type (OID 2.5.4.41) whose EQUALITY MATCH RULE is - caseIgnoreMatch. - ''' - if not isinstance(other, self.__class__): - raise TypeError("expected AVA but got %s" % (other.__class__.__name__)) - - return self._attr_unicode.lower() == other.attr.lower() and \ - self._value_unicode.lower() == other.value.lower() - - def __cmp__(self, other): - 'comparision is case insensitive, see __eq__ doc for explanation' - - if not isinstance(other, self.__class__): - raise TypeError("expected AVA but got %s" % (other.__class__.__name__)) - - result = cmp(self._attr_unicode.lower(), other.attr.lower()) - if result != 0: - return result - result = cmp(self._value_unicode.lower(), other.value.lower()) - return result - -class RDN(object): - ''' - RDN(arg0, ..., first_key_match=True) - - 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 and thus the RDN - constructor has simple syntax for them. - - The RDN constructor is passed a sequence of args and a set of - keyword parameters used for configuration. - - The constructor iterates though the sequence and adds AVA's to the RDN. - - The arg sequence may be: - - * 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. - - * A AVA object, the AVA will be copied into the new RDN respecting - the constructors keyword configuration parameters. - - * A RDN object, the AVA's in the RDN are copied into the new RDN - respecting the constructors keyword configuration parameters. - - Single AVA Examples: - - 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 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') # 2 args, 1st tuple forms 1 AVA, - # 2nd DN syntax string adds 1 AVA, - # 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, **kwds): - self.first_key_match = kwds.get('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 AVA(value.attr, value.value) - elif isinstance(value, RDN): - avas = [] - for ava in value.avas: - avas.append(AVA(ava.attr, ava.value)) - if len(avas) == 1: - return avas[0] - else: - return avas - 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 = [] - - for item in seq: - ava = self._ava_from_value(item) - if isinstance(ava, list): - avas.extend(ava) - else: - avas.append(ava) - 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 RDN indexing, must be int, basestring or slice; not %s" % \ - (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("cannot assign multiple AVA's to single entry") - 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 AVA's 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 RDN indexing, must be int, basestring or slice; not %s" % \ - (key.__class__.__name__)) - self.avas.sort() - - 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 RDN but got %s" % (other.__class__.__name__)) - - return self.avas == other.avas - - def __cmp__(self, other): - if not isinstance(other, self.__class__): - raise TypeError("expected RDN but got %s" % (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 = RDN(self, first_key_match=self.first_key_match) - if isinstance(other, RDN): - for ava in other.avas: - result.avas.append(AVA(ava.attr, ava.value)) - elif isinstance(other, AVA): - result.avas.append(AVA(other.attr, other.value)) - elif isinstance(other, basestring): - rdn = RDN(other) - for ava in rdn.avas: - result.avas.append(AVA(ava.attr, ava.value)) - 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, RDN): - for ava in other.avas: - self.avas.append(AVA(ava.attr, ava.value)) - elif isinstance(other, AVA): - self.avas.append(AVA(other.attr, other.value)) - elif isinstance(other, basestring): - rdn = RDN(other) - for ava in rdn.avas: - self.avas.append(AVA(ava.attr, ava.value)) - else: - raise TypeError("expected RDN, AVA or basestring but got %s" % (other.__class__.__name__)) - - self.avas.sort() - return self - -class DN(object): - ''' - DN(arg0, ..., first_key_match=True) - - A DN is a LDAP Distinguished Name. A DN is an ordered sequence of RDN's. - - The DN constructor is passed a sequence of args and a set of - keyword parameters used for configuration. normalize means the - attr and value will be converted to lower case. - - The constructor iterates through the sequence and adds the RDN's - it finds in order to the DN object. Each item in the sequence may - be: - - * A 2-valued tuple or list. The first member is the attr and the - second member is the value of an RDN, both members must be - strings (or unicode). The tuple or list is passed to the RDN - constructor and the resulting RDN is appended 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 which will be appended in order to - the DN. The parsing recognizes the DN syntax escaping rules. - - * A RDN object, the RDN will copied respecting the constructors - keyword configuration parameters and appended in order. - - * A DN object, the RDN's in the DN are copied respecting the - constructors keyword configuration parameters and appended in - order. - - Single DN Examples: - - DN(('cn', 'Bob')) # tuple yields 1 RDN - DN(['cn', 'Bob']) # list 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 tuples yields 2 RDN's - # 2 RDN's total - DN('cn=Bob,ou=people') # DN syntax with 2 RDN's - # 2 RDN's total - DN(RDN('cn', 'Bob'),RDN('ou', 'people')) # 2 RDN objects - # 2 RDN's total - DN(('cn', 'Bob'), "ou=people') # 1st tuple adds 1 RDN - # 2nd DN syntax string adds 1 RDN - # 2 RDN's total - base_dn = DN('dc=redhat,dc=com') - container_dn = DN('cn=sudorules,cn=sudo') - DN(('cn', 'Bob'), container_dn, base_dn) - # 1st arg adds 1 RDN, cn=Bob - # 2nd arg adds 2 RDN's, cn=sudorules,cn=sudo - # 3rd arg adds 2 RDN's, dc=redhat,dc=com - # 5 RDN's total - - - 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:3] = ('cn', 'Bob'), ('dc', 'redhat.com') - dn[1:3] = [['cn', 'Bob'], ['dc', 'redhat.com']] - dn[1:3] = RDN('cn', 'Bob'), RDN('dc', 'redhat.com') - - DN objects support the insert operation. - - dn.insert(i,x) is exactly equivalent to dn[i:i] = [x], thus the following - are all equivalent: - - dn.insert(i, ('cn','Bob')) - dn.insert(i, ['cn','Bob']) - dn.insert(i, RDN(('cn','Bob'))) - dn[i:i] = [('cn','Bob')] - - DN objects support equality testing and comparision. See RDN for the - definition of the comparision method. - - DN objects implement startswith(), endswith() and the "in" membership - operator. You may pass a DN or RDN object to these. Examples: - - # Test if dn ends with the contents of base_dn - if dn.endswith(base_dn): - # Test if dn starts with a rdn - if dn.startswith(rdn1): - # Test if a container is present in a dn - if container_dn in dn: - - 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, **kwds): - self.first_key_match = kwds.get('first_key_match', True) - self.first_key_match = True - self.rdns = self._rdns_from_sequence(args) - - def _rdn_from_value(self, value): - if isinstance(value, RDN): - return RDN(value, first_key_match=self.first_key_match) - elif isinstance(value, DN): - rdns = [] - for rdn in value.rdns: - rdns.append(RDN(rdn, first_key_match=self.first_key_match)) - if len(rdns) == 1: - return rdns[0] - else: - return rdns - elif isinstance(value, basestring): - rdns = [] - try: - dn_list = str2dn(value.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, first_key_match=self.first_key_match) - rdns.append(rdn) - except DECODING_ERROR: - raise ValueError("malformed RDN string = \"%s\"" % value) - if len(rdns) == 1: - return rdns[0] - else: - return rdns - elif isinstance(value, (tuple, list)): - if len(value) != 2: - raise ValueError("tuple or list must be 2-valued, not \"%s\"" % (value)) - rdn = RDN(value, first_key_match=self.first_key_match) - return rdn - else: - raise TypeError("must be str,unicode,tuple, or RDN, got %s instead" % \ - value.__class__.__name__) - - def _rdns_from_sequence(self, seq): - rdns = [] - - for item in seq: - rdn = self._rdn_from_value(item) - if isinstance(rdn, list): - rdns.extend(rdn) - else: - rdns.append(rdn) - 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 DN indexing, must be int, basestring or slice; not %s" % \ - (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("cannot assign multiple RDN's to single entry") - 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 DN indexing, must be int, basestring or slice; not %s" % \ - (key.__class__.__name__)) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - raise TypeError("expected DN but got %s" % (other.__class__.__name__)) - - return self.rdns == other.rdns - - def __cmp__(self, other): - if not isinstance(other, self.__class__): - raise TypeError("expected DN but got %s" % (other.__class__.__name__)) - - result = cmp(len(self), len(other)) - if result != 0: - return result - return self._cmp_sequence(other, 0, len(self)) - - def _cmp_sequence(self, pattern, self_start, pat_len): - self_idx = self_start - pat_idx = 0 - while pat_idx < pat_len: - result = cmp(self[self_idx], pattern[pat_idx]) - if result != 0: - return result - self_idx += 1 - pat_idx += 1 - return 0 - - def __add__(self, other): - result = DN(self, first_key_match=self.first_key_match) - if isinstance(other, self.__class__): - for rdn in other.rdns: - result.rdns.append(RDN(rdn, first_key_match=self.first_key_match)) - elif isinstance(other, RDN): - result.rdns.append(RDN(other, first_key_match=self.first_key_match)) - elif isinstance(other, basestring): - dn = DN(other, first_key_match=self.first_key_match) - for rdn in dn.rdns: - result.rdns.append(rdn) - else: - raise TypeError("expected DN, RDN or basestring but got %s" % (other.__class__.__name__)) - - return result - - def __iadd__(self, other): - if isinstance(other, DN): - for rdn in other.rdns: - self.rdns.append(RDN(rdn, first_key_match=self.first_key_match)) - elif isinstance(other, RDN): - self.rdns.append(RDN(other, first_key_match=self.first_key_match)) - elif isinstance(other, basestring): - dn = DN(other, first_key_match=self.first_key_match) - self.__iadd__(dn) - else: - raise TypeError("expected DN, RDN or basestring but got %s" % (other.__class__.__name__)) - - return self - - def insert(self, i, x): - ''' - x must be a 2-value tuple or list promotable to an RDN object, - or a RDN object. - - dn.insert(i, x) is the same as s[i:i] = [x] - - When a negative index is passed as the first parameter to the - insert() method, the list length is added, as for slice - indices. If it is still negative, it is truncated to zero, as - for slice indices. - ''' - self.rdns.insert(i, self._rdn_from_value(x)) - - # The implementation of startswith, endswith, tailmatch, adjust_indices - # was based on the Python's stringobject.c implementation - - def startswith(self, prefix, start=0, end=sys.maxsize): - ''' - Return True if the dn starts with the specified prefix (either a DN or - RDN object), False otherwise. With optional start, test dn beginning at - that position. With optional end, stop comparing dn at that position. - prefix can also be a tuple of dn's or rdn's to try. - ''' - if isinstance(prefix, tuple): - for pat in prefix: - if self._tailmatch(pat, start, end, -1): - return True - return False - - return self._tailmatch(prefix, start, end, -1) - - def endswith(self, suffix, start=0, end=sys.maxsize): - ''' - Return True if dn ends with the specified suffix (either a DN or RDN - object), False otherwise. With optional start, test dn beginning at - that position. With optional end, stop comparing dn at that position. - suffix can also be a tuple of dn's or rdn's to try. - ''' - if isinstance(suffix, tuple): - for pat in suffix: - if self._tailmatch(pat, start, end, +1): - return True - return False - - return self._tailmatch(suffix, start, end, +1) - - def _tailmatch(self, pattern, start, end, direction): - ''' - Matches the end (direction >= 0) or start (direction < 0) of self - against pattern (either a DN or RDN), using the start and end - arguments. Returns 0 if not found and 1 if found. - ''' - - if isinstance(pattern, DN): - pat_len = len(pattern) - elif isinstance(pattern, RDN): - pat_len = 1 - else: - raise TypeError("expected DN or RDN but got %s" % (pattern.__class__.__name__)) - - self_len = len(self) - - start, end = _adjust_indices(start, end, self_len) - - if direction < 0: # starswith - if start+pat_len > self_len: - return 0 - else: # endswith - if end-start < pat_len or start > self_len: - return 0 - - if end-pat_len >= start: - start = end - pat_len - - if isinstance(pattern, DN): - if end-start >= pat_len: - return not self._cmp_sequence(pattern, start, pat_len) - return 0 - else: - return self.rdns[start] == pattern - - def __contains__(self, other): - 'Return the outcome of the test other in self. Note the reversed operands.' - - if isinstance(other, DN): - other_len = len(other) - end = len(self) - other_len - i = 0 - while i <= end: - result = self._cmp_sequence(other, i, other_len) - if result == 0: - return True - i += 1 - return False - - elif isinstance(other, RDN): - return other in self.rdns - else: - raise TypeError("expected DN or RDN but got %s" % (other.__class__.__name__)) - - - - - |