summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJohn Dennis <jdennis@redhat.com>2011-07-20 19:39:05 -0400
committerRob Crittenden <rcritten@redhat.com>2011-07-21 00:29:38 -0400
commitad3cf68ac222bb288945e7e3d1c549ce8b49c02c (patch)
tree7f123ef8012812f4a25eaa8a9f3457ac1f032d58
parentd4310c07a9454289676b17d6224ae7b62c51c207 (diff)
downloadfreeipa-ad3cf68ac222bb288945e7e3d1c549ce8b49c02c.tar.gz
freeipa-ad3cf68ac222bb288945e7e3d1c549ce8b49c02c.tar.xz
freeipa-ad3cf68ac222bb288945e7e3d1c549ce8b49c02c.zip
Ticket 1485 - DN pairwise grouping
The pairwise grouping used to form RDN's and AVA's proved to be confusing in practice, this patch removes that functionality thus requiring programmers to explicitly pair attr,value using a tuple or list. In addition it was discovered additional functionality was needed to support some DN operations in freeipa. DN objects now support startswith(), endswith() and the "in" membership test. These functions and operators will accept either a DN or RDN. The unittest was modified to remove the pairwise tests and add new explicit tests. The unittest was augmented to test the new functionality. In addition the unittest was cleaned up a bit to use common utilty functions for improved readabilty and robustness. The documentation was updated. fix test_role_plugin use of DN to avoid pairwise grouping
-rw-r--r--ipalib/dn.py434
-rw-r--r--[-rwxr-xr-x]tests/test_ipalib/test_dn.py184
-rw-r--r--tests/test_xmlrpc/test_role_plugin.py2
3 files changed, 398 insertions, 222 deletions
diff --git a/ipalib/dn.py b/ipalib/dn.py
index 89248ca8e..47c66198e 100644
--- a/ipalib/dn.py
+++ b/ipalib/dn.py
@@ -20,6 +20,7 @@
from ldap.dn import str2dn, dn2str
from ldap import DECODING_ERROR
from copy import deepcopy
+import sys
__all__ = ['AVA', 'RDN', 'DN']
@@ -104,10 +105,10 @@ 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
+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:
@@ -127,13 +128,13 @@ Examples of where the simple assumptions fail are:
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
+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
+'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
@@ -143,10 +144,10 @@ 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
+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
+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
@@ -162,13 +163,13 @@ other string you MUST:
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
+ cn=r\2cw,cn=privilege
- Is a DN with 2 RDN components: cn=R,W followed by "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
+ cn=r,w,cn=privilege
Which is a malformed DN with 3 RDN's, the 2nd RDN is invalid.
@@ -189,10 +190,10 @@ 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'
+ value = 'r,w'
dn = 'cn=%s,%s' % (value, base)
-Will result in the malformed DN 'cn=R,W,dc=redhat,dc=com'
+Will result in the malformed DN 'cn=r,w,dc=redhat,dc=com'
Syntax Sugar
------------
@@ -216,15 +217,36 @@ 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.
+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 attr and value in the pair MUST be strings (we'll see why in a
-moment).
+The DN and RDN constructors take a sequence, the constructor parses
+the sequence to find items it knows about.
-You can express any part of a DN as an even numbered sequence of
-strings.
+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')
- DN('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:
@@ -237,25 +259,18 @@ And is exactly equal to:
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'])
+ 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.
+You can mix and match any of the forms in the constructor parameter
+list.
- 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.
+ 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
@@ -267,9 +282,9 @@ ava.value -> u'Bob'
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)
+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'
@@ -316,8 +331,8 @@ dn['cn'] -> u'Bob'
# 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] = ['cn', 'Bob']
dn[0] = RDN('cn', 'Bob')
dn[0].attr = 'cn'
@@ -333,9 +348,8 @@ dn[-2:]
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[1:3] = ('cn', 'Bob), ('dc', 'redhat.com')
+dn[1:3] = RDN('cn', 'Bob), RDN('dc', 'redhat.com')
String representations and escapes:
@@ -346,27 +360,34 @@ 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')
+rdn = RDN('cn', 'r,w')
# Thus:
-rdn.value == 'R,W' -> True
+rdn.value == 'r,w' -> True
# But:
-str(rdn) == 'cn=R,W' -> False
+str(rdn) == 'cn=r,w' -> False
# Because:
-str(rdn) -> 'cn=R\2cW' or 'cn='R\,W' # depending on the underlying LDAP library
+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')
+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
@@ -380,7 +401,6 @@ Concatenation and In-Place Addition:
dn3 = dn1 + dn2
# Append a RDN to DN's RDN sequence (all are equivalent)
-dn += 'cn', 'Bob'
dn += ('cn', 'Bob')
dn += RDN('cn', 'Bob')
@@ -492,18 +512,18 @@ class AVA(object):
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__))
+ raise TypeError("unsupported type for AVA indexing, must be basestring; not %s" % \
+ (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__))
+ raise TypeError("expected AVA but got %s" % (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__))
+ raise TypeError("expected AVA but got %s" % (other.__class__.__name__))
result = cmp(self.attr, other.attr)
if result != 0: return result
@@ -519,14 +539,15 @@ class RDN(object):
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.
+ within an RDN). Single valued RDN's are the norm and thus the RDN
+ constructor has simple syntax for them.
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
+ arguments may appear in the argument list, each pair adds one additional AVA
to the RDN.
* A 2-valued tuple or list denotes the <attr,value> pair of an AVA. The
@@ -630,7 +651,7 @@ class RDN(object):
def _ava_from_value(self, value):
if isinstance(value, AVA):
- return value
+ return deepcopy(value)
elif isinstance(value, basestring):
try:
rdns = str2dn(value.encode('utf-8'))
@@ -710,14 +731,14 @@ class RDN(object):
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__))
+ 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("multiple AVA's")
+ 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)
@@ -725,7 +746,7 @@ class RDN(object):
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")
+ raise TypeError("cannot assign multiple AVA's to single entry")
found = False
i = 0
while i < len(self.avas):
@@ -737,8 +758,9 @@ class RDN(object):
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__))
+ 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:
@@ -774,13 +796,13 @@ class RDN(object):
def __eq__(self, other):
if not isinstance(other, self.__class__):
- raise TypeError("expected %s but got %s" % (self.__class__.__name__, other.__class__.__name__))
+ 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 %s but got %s" % (self.__class__.__name__, other.__class__.__name__))
+ raise TypeError("expected RDN but got %s" % (other.__class__.__name__))
result = cmp(len(self), len(other))
if result != 0: return result
@@ -797,7 +819,7 @@ class RDN(object):
for ava in other.avas:
result.avas.append(deepcopy(ava))
elif isinstance(other, AVA):
- result.avas.append(deepcopy(other))
+ result.avas.append(deepcopy(other))
elif isinstance(other, basestring):
rdn = RDN(other)
for ava in rdn.avas:
@@ -813,7 +835,7 @@ class RDN(object):
for ava in other.avas:
self.avas.append(deepcopy(ava))
elif isinstance(other, AVA):
- self.avas.append(deepcopy(other))
+ self.avas.append(deepcopy(other))
elif isinstance(other, basestring):
rdn = RDN(other)
for ava in rdn.avas:
@@ -828,54 +850,54 @@ 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.
+ The DN constructor accepts a sequence. 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:
- * 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. 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 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 which will be appended in order to
+ the DN. The parsing recognizes the DN syntax escaping rules.
- * 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.
+ * A RDN object. Each RDN object in the argument list will be
+ appended to the DN in order.
- 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.
+ * A DN object. Each DN object in the argument list will append in order
+ 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']) # 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 strings pairs yield 2 RDN's
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
- 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
+ # 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).
@@ -912,13 +934,23 @@ class DN(object):
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[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 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).
@@ -946,66 +978,49 @@ class DN(object):
def _rdn_from_value(self, value):
if isinstance(value, RDN):
- return value
+ return deepcopy(value)
+ elif isinstance(value, DN):
+ rdns = []
+ for rdn in value.rdns:
+ rdns.append(deepcopy(rdn))
+ if len(rdns) == 1:
+ return rdns[0]
+ else:
+ return rdns
elif isinstance(value, basestring):
+ rdns = []
try:
- rdns = str2dn(value.encode('utf-8'))
- for rdn_list in rdns:
+ 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)
- return rdn
+ 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\"" % (rdn))
rdn = RDN(value)
return rdn
else:
- raise TypeError("single argument must be str,unicode,tuple, or RDN, got %s instead" % \
+ raise TypeError("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
+ for item in seq:
+ rdn = self._rdn_from_value(item)
+ if isinstance(rdn, list):
+ rdns.extend(rdn)
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__)
+ rdns.append(rdn)
return rdns
def _to_openldap(self):
@@ -1042,14 +1057,14 @@ class DN(object):
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__))
+ 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("multiple RDN's")
+ 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)
@@ -1069,26 +1084,31 @@ class DN(object):
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__))
+ 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 %s but got %s" % (self.__class__.__name__, other.__class__.__name__))
+ 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 %s but got %s" % (self.__class__.__name__, other.__class__.__name__))
+ raise TypeError("expected DN 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])
+ 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
- i += 1
+ self_idx += 1
+ pat_idx += 1
return 0
def __add__(self, other):
@@ -1097,7 +1117,7 @@ class DN(object):
for rdn in other.rdns:
result.rdns.append(deepcopy(rdn))
elif isinstance(other, RDN):
- result.rdns.append(deepcopy(other))
+ result.rdns.append(deepcopy(other))
elif isinstance(other, basestring):
dn = DN(other)
for rdn in dn.rdns:
@@ -1112,7 +1132,7 @@ class DN(object):
for rdn in other.rdns:
self.rdns.append(deepcopy(rdn))
elif isinstance(other, RDN):
- self.rdns.append(deepcopy(other))
+ self.rdns.append(deepcopy(other))
elif isinstance(other, basestring):
dn = DN(other)
self.__iadd__(dn)
@@ -1121,3 +1141,111 @@ class DN(object):
return self
+ # 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 _adjust_indices(self, 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
+
+ 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 = self._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__))
+
+
+
+
+
diff --git a/tests/test_ipalib/test_dn.py b/tests/test_ipalib/test_dn.py
index c47f38827..029297f69 100755..100644
--- a/tests/test_ipalib/test_dn.py
+++ b/tests/test_ipalib/test_dn.py
@@ -3,27 +3,36 @@
import unittest
from ipalib.dn import AVA, RDN, DN
+def default_rdn_attr_arg(i):
+ return 'a%d' % i
+
+def default_rdn_value_arg(i):
+ return str(i)
+
+def alt_rdn_attr_arg(i):
+ return 'b%d' % i
+
+def alt_rdn_value_arg(i):
+ return str(i*10)
+
def make_rdn_args(low, high, kind, attr=None, value=None):
result=[]
for i in range(low, high):
if attr is None:
- new_attr = 'a%d' % i
+ new_attr = default_rdn_attr_arg(i)
elif callable(attr):
new_attr = attr(i)
else:
new_attr = attr
if value is None:
- new_value = str(i)
+ new_value = default_rdn_value_arg(i)
elif callable(value):
new_value = value(i)
else:
new_value = value
- if kind == 'sequence':
- result.append(new_attr)
- result.append(new_value)
- elif kind == 'tuple':
+ if kind == 'tuple':
result.append((new_attr, new_value))
elif kind == 'list':
result.append([new_attr, new_value])
@@ -526,9 +535,21 @@ class TestDN(unittest.TestCase):
self.str_dn3 = '%s,%s' % (self.str_rdn1, self.str_rdn2)
self.dn3 = DN(self.rdn1, self.rdn2)
+ self.base_rdn1 = RDN('dc', 'redhat')
+ self.base_rdn2 = RDN('dc', 'com')
+ self.base_dn = DN(self.base_rdn1, self.base_rdn2)
+
+ self.container_rdn1 = RDN('cn', 'sudorules')
+ self.container_rdn2 = RDN('cn', 'sudo')
+ self.container_dn = DN(self.container_rdn1, self.container_rdn2)
+
+ self.base_container_dn = DN((self.attr1, self.value1),
+ self.container_dn, self.base_dn)
+
+
def test_create(self):
# Create with single attr,value pair
- dn1 = DN(self.attr1, self.value1)
+ dn1 = DN((self.attr1, self.value1))
self.assertEqual(len(dn1), 1)
self.assertIsInstance(dn1[0], RDN)
self.assertIsInstance(dn1[0].attr, unicode)
@@ -543,8 +564,12 @@ class TestDN(unittest.TestCase):
self.assertIsInstance(dn1[0].value, unicode)
self.assertEqual(dn1[0], self.rdn1)
- # Create with multiple attr,value pairs
- dn1 = DN(self.attr1, self.value1, self.attr2, self.value2)
+ # Creation with multiple attr,value string pairs should fail
+ with self.assertRaises(ValueError):
+ dn1 = DN(self.attr1, self.value1, self.attr2, self.value2)
+
+ # Create with multiple attr,value pairs passed as tuples & lists
+ dn1 = DN((self.attr1, self.value1), [self.attr2, self.value2])
self.assertEqual(len(dn1), 2)
self.assertIsInstance(dn1[0], RDN)
self.assertIsInstance(dn1[0].attr, unicode)
@@ -555,8 +580,8 @@ class TestDN(unittest.TestCase):
self.assertIsInstance(dn1[1].value, unicode)
self.assertEqual(dn1[1], self.rdn2)
- # Create with multiple attr,value pairs passed as lists
- dn1 = DN([self.attr1, self.value1], [self.attr2, self.value2])
+ # Create with multiple attr,value pairs passed as tuple and RDN
+ dn1 = DN((self.attr1, self.value1), RDN(self.attr2, self.value2))
self.assertEqual(len(dn1), 2)
self.assertIsInstance(dn1[0], RDN)
self.assertIsInstance(dn1[0].attr, unicode)
@@ -570,7 +595,7 @@ class TestDN(unittest.TestCase):
# Create with multiple attr,value pairs but reverse
# constructor parameter ordering. RDN ordering should also be
# reversed because DN's are a ordered sequence of RDN's
- dn1 = DN(self.attr2, self.value2, self.attr1, self.value1)
+ dn1 = DN((self.attr2, self.value2), (self.attr1, self.value1))
self.assertEqual(len(dn1), 2)
self.assertIsInstance(dn1[0], RDN)
self.assertIsInstance(dn1[0].attr, unicode)
@@ -634,6 +659,14 @@ class TestDN(unittest.TestCase):
self.assertIsInstance(dn1[1].value, unicode)
self.assertEqual(dn1[1], self.rdn2)
+ # Create with RDN, and 2 DN's (e.g. attr + container + base)
+ dn1 = DN((self.attr1, self.value1), self.container_dn, self.base_dn)
+ self.assertEqual(len(dn1), 5)
+ dn_str = ','.join([str(self.rdn1),
+ str(self.container_rdn1), str(self.container_rdn2),
+ str(self.base_rdn1), str(self.base_rdn2)])
+ self.assertEqual(str(dn1), dn_str)
+
def test_str(self):
self.assertEqual(str(self.dn1), self.str_dn1)
self.assertIsInstance(str(self.dn1), str)
@@ -646,7 +679,7 @@ class TestDN(unittest.TestCase):
def test_cmp(self):
# Equality
- dn1 = DN(self.attr1, self.value1)
+ dn1 = DN((self.attr1, self.value1))
self.assertTrue(dn1 == self.dn1)
self.assertFalse(dn1 != self.dn1)
@@ -688,6 +721,40 @@ class TestDN(unittest.TestCase):
result = cmp(self.dn3, self.dn1)
self.assertEqual(result, 1)
+ # Test startswith, endswith
+ self.assertTrue(self.base_container_dn.startswith(self.rdn1))
+ self.assertTrue(self.base_container_dn.startswith(self.dn1))
+ self.assertTrue(self.base_container_dn.startswith(self.dn1 + self.container_dn))
+ self.assertFalse(self.base_container_dn.startswith(self.dn2))
+ self.assertFalse(self.base_container_dn.startswith(self.rdn2))
+ self.assertTrue(self.base_container_dn.startswith((self.dn1)))
+ self.assertTrue(self.base_container_dn.startswith((self.rdn1)))
+ self.assertFalse(self.base_container_dn.startswith((self.rdn2)))
+ self.assertTrue(self.base_container_dn.startswith((self.rdn2, self.rdn1)))
+ self.assertTrue(self.base_container_dn.startswith((self.dn1, self.dn2)))
+
+ self.assertTrue(self.base_container_dn.endswith(self.base_dn))
+ self.assertTrue(self.base_container_dn.endswith(self.container_dn + self.base_dn))
+ self.assertFalse(self.base_container_dn.endswith(DN(self.base_rdn1)))
+ self.assertTrue(self.base_container_dn.endswith(DN(self.base_rdn2)))
+ self.assertTrue(self.base_container_dn.endswith((DN(self.base_rdn1), DN(self.base_rdn2))))
+
+ # Test "in" membership
+ self.assertTrue(self.container_rdn1 in self.container_dn)
+ self.assertTrue(self.container_dn in self.container_dn)
+ self.assertFalse(self.base_rdn1 in self.container_dn)
+
+ self.assertTrue(self.container_rdn1 in self.base_container_dn)
+ self.assertTrue(self.container_dn in self.base_container_dn)
+ self.assertTrue(self.container_dn + self.base_dn in
+ self.base_container_dn)
+ self.assertTrue(self.dn1 + self.container_dn + self.base_dn in
+ self.base_container_dn)
+ self.assertTrue(self.dn1 + self.container_dn + self.base_dn ==
+ self.base_container_dn)
+
+ self.assertFalse(self.container_rdn1 in self.base_dn)
+
def test_indexing(self):
self.assertEqual(self.dn1[0], self.rdn1)
self.assertEqual(self.dn1[self.rdn1.attr], self.rdn1.value)
@@ -713,30 +780,33 @@ class TestDN(unittest.TestCase):
dn_low = 0
dn_high = 6
- rdn_args = make_rdn_args(dn_low, dn_high, 'sequence')
+ rdn_args = make_rdn_args(dn_low, dn_high, 'tuple',
+ default_rdn_attr_arg, default_rdn_value_arg)
dn1 = DN(*rdn_args)
- rdn_args = make_rdn_args(dn_low, dn_high, 'tuple')
+ rdn_args = make_rdn_args(dn_low, dn_high, 'list',
+ default_rdn_attr_arg, default_rdn_value_arg)
dn2 = DN(*rdn_args)
- rdn_args = make_rdn_args(dn_low, dn_high, 'RDN')
+ rdn_args = make_rdn_args(dn_low, dn_high, 'RDN',
+ default_rdn_attr_arg, default_rdn_value_arg)
dn3 = DN(*rdn_args)
self.assertEqual(dn1, dn2)
self.assertEqual(dn1, dn3)
for i in range(dn_low, dn_high):
- attr = 'a%d' % i
- value = str(i)
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
self.assertEqual(dn1[i].attr, attr)
self.assertEqual(dn1[i].value, value)
self.assertEqual(dn1[attr], value)
for i in range(dn_low, dn_high):
if i % 2:
- orig_attr = 'a%d' % i
- attr = 'b%d' % i
- value = str(i*10)
+ orig_attr = default_rdn_attr_arg(i)
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
dn1[i] = attr, value
dn2[orig_attr] = (attr, value)
dn3[i] = RDN(attr, value)
@@ -746,11 +816,11 @@ class TestDN(unittest.TestCase):
for i in range(dn_low, dn_high):
if i % 2:
- attr = 'b%d' % i
- value = str(i*10)
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
else:
- attr = 'a%d' % i
- value = str(i)
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
self.assertEqual(dn1[i].value, dn1[i].value)
self.assertEqual(dn1[attr], value)
@@ -759,47 +829,24 @@ class TestDN(unittest.TestCase):
slice_high = 4
interval = range(slice_low, slice_high)
- # Assign via sequence
- rdn_args = make_rdn_args(dn_low, dn_high, 'sequence')
- dn1 = DN(*rdn_args)
-
- dn_slice = make_rdn_args(slice_low, slice_high, 'sequence',
- lambda i: 'b%d' % i, lambda i: str(i*10))
-
- dn1[slice_low:slice_high] = dn_slice
-
- for i in range(dn_low, dn_high):
- if i in interval:
- attr = 'b%d' % i
- value = str(i*10)
- else:
- attr = 'a%d' % i
- value = str(i)
- self.assertEqual(dn1[i].value, dn1[i].value)
- self.assertEqual(dn1[attr], value)
-
- query_slice = dn1[slice_low:slice_high]
- for i, query_rdn in enumerate(query_slice):
- slice_rdn = RDN(dn_slice[i*2], dn_slice[i*2+1])
- self.assertEqual(slice_rdn, query_rdn)
-
# Slices
# Assign via tuple
- rdn_args = make_rdn_args(dn_low, dn_high, 'sequence')
+ rdn_args = make_rdn_args(dn_low, dn_high, 'tuple',
+ default_rdn_attr_arg, default_rdn_value_arg)
dn1 = DN(*rdn_args)
dn_slice = make_rdn_args(slice_low, slice_high, 'tuple',
- lambda i: 'b%d' % i, lambda i: str(i*10))
+ alt_rdn_attr_arg, alt_rdn_value_arg)
dn1[slice_low:slice_high] = dn_slice
for i in range(dn_low, dn_high):
if i in interval:
- attr = 'b%d' % i
- value = str(i*10)
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
else:
- attr = 'a%d' % i
- value = str(i)
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
self.assertEqual(dn1[i].value, dn1[i].value)
self.assertEqual(dn1[attr], value)
@@ -810,21 +857,22 @@ class TestDN(unittest.TestCase):
# Slices
# Assign via RDN
- rdn_args = make_rdn_args(dn_low, dn_high, 'sequence')
+ rdn_args = make_rdn_args(dn_low, dn_high, 'tuple',
+ default_rdn_attr_arg, default_rdn_value_arg)
dn1 = DN(*rdn_args)
dn_slice = make_rdn_args(slice_low, slice_high, 'RDN',
- lambda i: 'b%d' % i, lambda i: str(i*10))
+ alt_rdn_attr_arg, alt_rdn_value_arg)
dn1[slice_low:slice_high] = dn_slice
for i in range(dn_low, dn_high):
if i in interval:
- attr = 'b%d' % i
- value = str(i*10)
+ attr = alt_rdn_attr_arg(i)
+ value = alt_rdn_value_arg(i)
else:
- attr = 'a%d' % i
- value = str(i)
+ attr = default_rdn_attr_arg(i)
+ value = default_rdn_value_arg(i)
self.assertEqual(dn1[i].value, dn1[i].value)
self.assertEqual(dn1[attr], value)
@@ -863,31 +911,31 @@ class TestDN(unittest.TestCase):
def test_concat(self):
- dn1 = DN(self.attr1, self.value1)
- dn2 = DN(self.attr2, self.value2)
+ dn1 = DN((self.attr1, self.value1))
+ dn2 = DN([self.attr2, self.value2])
# in-place addtion
dn1 += dn2
self.assertEqual(dn1, self.dn3)
- dn1 = DN(self.attr1, self.value1)
+ dn1 = DN((self.attr1, self.value1))
dn1 += self.rdn2
self.assertEqual(dn1, self.dn3)
- dn1 = DN(self.attr1, self.value1)
+ dn1 = DN((self.attr1, self.value1))
dn1 += self.dn2
self.assertEqual(dn1, self.dn3)
- dn1 = DN(self.attr1, self.value1)
+ dn1 = DN((self.attr1, self.value1))
dn1 += self.str_dn2
self.assertEqual(dn1, self.dn3)
# concatenation
- dn1 = DN(self.attr1, self.value1)
+ dn1 = DN((self.attr1, self.value1))
dn3 = dn1 + dn2
self.assertEqual(dn3, self.dn3)
- dn1 = DN(self.attr1, self.value1)
+ dn1 = DN((self.attr1, self.value1))
dn3 = dn1 + self.rdn2
self.assertEqual(dn3, self.dn3)
diff --git a/tests/test_xmlrpc/test_role_plugin.py b/tests/test_xmlrpc/test_role_plugin.py
index 82342c340..e1485cf42 100644
--- a/tests/test_xmlrpc/test_role_plugin.py
+++ b/tests/test_xmlrpc/test_role_plugin.py
@@ -44,7 +44,7 @@ group1 = u'testgroup1'
group1_dn = u'cn=%s,%s,%s' % (group1, api.env.container_group, api.env.basedn)
privilege1 = u'r,w privilege 1'
-privilege1_dn = DN('cn', privilege1, DN(api.env.container_privilege), DN(api.env.basedn))
+privilege1_dn = DN(('cn', privilege1), DN(api.env.container_privilege), DN(api.env.basedn))
def escape_comma(value):
return value.replace(',', '\\,')