[CalendarServer-changes] [12369] twext/trunk/twext/who/ldap

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:22:02 PDT 2014


Revision: 12369
          http://trac.calendarserver.org//changeset/12369
Author:   wsanchez at apple.com
Date:     2014-01-16 19:02:21 -0800 (Thu, 16 Jan 2014)
Log Message:
-----------
Change the field name map so that it maps to multiple LDAP attributes.

Change the record type map so that it maps to multiple LDAP object classes.

Much fallout ensues.

Modified Paths:
--------------
    twext/trunk/twext/who/ldap/_service.py
    twext/trunk/twext/who/ldap/_util.py
    twext/trunk/twext/who/ldap/test/test_service.py
    twext/trunk/twext/who/ldap/test/test_util.py

Modified: twext/trunk/twext/who/ldap/_service.py
===================================================================
--- twext/trunk/twext/who/ldap/_service.py	2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/_service.py	2014-01-17 03:02:21 UTC (rev 12369)
@@ -49,21 +49,21 @@
 
 
 
-# Maps field name -> LDAP attribute name
-DEFAULT_FIELDNAME_MAP = {
-    BaseFieldName.guid: LDAPAttribute.generatedUUID.value,
-    BaseFieldName.recordType: LDAPAttribute.objectClass.value,
-    BaseFieldName.shortNames: LDAPAttribute.uid.value,
-    BaseFieldName.fullNames: LDAPAttribute.cn.value,
-    BaseFieldName.emailAddresses: LDAPAttribute.mail.value,
-    BaseFieldName.password: LDAPAttribute.userPassword.value,
+# Maps field name -> LDAP attribute names
+DEFAULT_FIELDNAME_ATTRIBUTE_MAP = {
+    BaseFieldName.guid: (LDAPAttribute.generatedUUID.value,),
+    BaseFieldName.recordType: (LDAPAttribute.objectClass.value,),
+    BaseFieldName.shortNames: (LDAPAttribute.uid.value,),
+    BaseFieldName.fullNames: (LDAPAttribute.cn.value,),
+    BaseFieldName.emailAddresses: (LDAPAttribute.mail.value,),
+    BaseFieldName.password: (LDAPAttribute.userPassword.value,),
 }
 
 
-# Maps record type -> LDAP object class name
-DEFAULT_RECORDTYPE_MAP = {
-    BaseRecordType.user: LDAPObjectClass.person.value,
-    BaseRecordType.group: LDAPObjectClass.groupOfNames.value,
+# Maps record type -> LDAP object class names
+DEFAULT_RECORDTYPE_OBJECTCLASS_MAP = {
+    BaseRecordType.user: (LDAPObjectClass.person.value,),
+    BaseRecordType.group: (LDAPObjectClass.groupOfNames.value,),
 }
 
 
@@ -147,8 +147,8 @@
         tlsCACertificateFile=None,
         tlsCACertificateDirectory=None,
         useTLS=False,
-        fieldNameToAttributeMap=DEFAULT_FIELDNAME_MAP,
-        recordTypeToObjectClassMap=DEFAULT_RECORDTYPE_MAP,
+        fieldNameToAttributeMap=DEFAULT_FIELDNAME_ATTRIBUTE_MAP,
+        recordTypeToObjectClassMap=DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
         uidField=BaseFieldName.uid,
         _debug=False,
     ):
@@ -178,12 +178,12 @@
         @param fieldNameToAttributeMap: A mapping of field names to LDAP
             attribute names.
         @type fieldNameToAttributeMap: mapping with L{NamedConstant} keys and
-            L{unicode} values
+            sequence of L{unicode} values
 
         @param recordTypeToObjectClassMap: A mapping of record types to LDAP
             object classes.
         @type recordTypeToObjectClassMap: mapping with L{NamedConstant} keys
-            and L{unicode} values
+            and sequence of L{unicode} values
         """
 
         self.url = url
@@ -208,24 +208,28 @@
         else:
             self._debug = None
 
-        def reverseDict(source):
+        def reverseDict(sourceName, source):
             new = {}
 
-            for k, v in source.iteritems():
-                if v in new:
-                    raise LDAPConfigurationError(
-                        u"Field name map has duplicate values: {0}".format(v)
-                    )
-                new[v] = k
+            for key, values in source.iteritems():
+                for value in values:
+                    if value in new:
+                        raise LDAPConfigurationError(
+                            u"{0} map has duplicate values: {1}"
+                            .format(sourceName, value)
+                        )
+                    new[value] = key
 
             return new
 
         self._fieldNameToAttributeMap = fieldNameToAttributeMap
-        self._attributeToFieldNameMap = reverseDict(fieldNameToAttributeMap)
+        self._attributeToFieldNameMap = reverseDict(
+            "Field name", fieldNameToAttributeMap
+        )
 
         self._recordTypeToObjectClassMap = recordTypeToObjectClassMap
         self._objectClassToRecordTypeMap = reverseDict(
-            recordTypeToObjectClassMap
+            "Record type", recordTypeToObjectClassMap
         )
 
         self._uidField = uidField
@@ -317,23 +321,28 @@
         # we are using to determine the UID of the record.
 
         uidField = self.fieldName.uid
-        uidAttribute = self._fieldNameToAttributeMap[self._uidField]
+        uidAttribute = self._fieldNameToAttributeMap[self._uidField][0]
 
         recordTypeField = self.fieldName.recordType
-        recordTypeAttribute = (
+        recordTypeAttributes = (
             self._fieldNameToAttributeMap[self.fieldName.recordType]
         )
 
         for dn, recordData in reply:
 
-            if recordTypeAttribute not in recordData:
-                self.log.debug(
-                    "Ignoring LDAP record data with no record type attribute "
-                    "{source.fieldName.recordType!r}: {recordData!r}",
-                    self=self, recordData=recordData
-                )
-                continue
+            # Attributes used to determine the record type are required, since
+            # record type is very much required.
 
+            for recordTypeAttribute in recordTypeAttributes:
+                if recordTypeAttribute not in recordData:
+                    self.log.debug(
+                        "Ignoring LDAP record data without record type "
+                        "attribute {attribute!r}: "
+                        "{recordData!r}",
+                        attribute=recordTypeAttribute, recordData=recordData,
+                    )
+                    continue
+
             # Make a dict of fields -> values from the incoming dict of
             # attributes -> values.
 
@@ -351,7 +360,7 @@
                 self.log.debug(
                     "Ignoring LDAP record data with no UID attribute "
                     "{source._uidField!r}: {recordData!r}",
-                    self=self, recordData=recordData
+                    recordData=recordData
                 )
                 continue
 

Modified: twext/trunk/twext/who/ldap/_util.py
===================================================================
--- twext/trunk/twext/who/ldap/_util.py	2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/_util.py	2014-01-17 03:02:21 UTC (rev 12369)
@@ -21,12 +21,37 @@
     MatchExpression, MatchFlags,
 )
 from ..util import iterFlags
-from ._constants import LDAPMatchType
+from ._constants import LDAPOperand, LDAPMatchType, LDAPMatchFlags
 
 
 
+def ldapQueryStringFromQueryStrings(operand, queryStrings):
+    """
+    Combines LDAP query strings into a single query string.
+
+    @param operand: An LDAP operand (C{u"&"} or C{u"|"}).
+    @type operand: L{unicode}
+
+    @param queryStrings: LDAP query strings.
+    @type queryStrings: iterable of L{unicode}
+    """
+    if len(queryStrings) == 1:
+        return queryStrings[0]
+
+    elif len(queryStrings) > 1:
+        queryTokens = []
+        queryTokens.append(u"(")
+        queryTokens.append(operand)
+        queryTokens.extend(queryStrings)
+        queryTokens.append(u")")
+        return u"".join(queryTokens)
+
+    else:
+        return u""
+
+
 def ldapQueryStringFromMatchExpression(
-    expression, fieldNameMap, recordTypeMap
+    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
 ):
     """
     Generates an LDAP query string from a match expression.
@@ -34,21 +59,24 @@
     @param expression: A match expression.
     @type expression: L{MatchExpression}
 
-    @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
-        names.
-    @type fieldNameMap: L{dict}
+    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+        attribute names.
+    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+        of L{unicode} values.
 
-    @param recordTypeMap: A mapping from L{RecordType}s to native LDAP object
-        class names.
-    @type recordTypeMap: L{dict}
+    @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+        LDAP object class names.
+    @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+        sequence of L{unicode} values.
 
     @return: An LDAP query string.
-    @rtype: C{unicode}
+    @rtype: L{unicode}
 
     @raises QueryNotSupportedError: If the expression's match type is unknown,
         or if the expresion references an unknown field name (meaning a field
-        name not in C{fieldNameMap}).
+        name not in C{fieldNameToAttributeMap}).
     """
+
     matchType = LDAPMatchType.fromMatchType(expression.matchType)
     if matchType is None:
         raise QueryNotSupportedError(
@@ -58,7 +86,7 @@
     flags = tuple(iterFlags(expression.flags))
 
     if MatchFlags.NOT in flags:
-        notOp = u"!"
+        notOp = LDAPMatchFlags.NOT.value
     else:
         notOp = u""
 
@@ -71,7 +99,7 @@
 
     fieldName = expression.fieldName
     try:
-        attribute = fieldNameMap[fieldName]
+        attributes = fieldNameToAttributeMap[fieldName]
     except KeyError:
         raise QueryNotSupportedError(
             "Unmapped field name: {0}".format(expression.fieldName)
@@ -79,23 +107,49 @@
 
     if fieldName is FieldName.recordType:
         try:
-            value = recordTypeMap[expression.fieldValue]
+            values = recordTypeToObjectClassMap[expression.fieldValue]
         except KeyError:
             raise QueryNotSupportedError(
                 "Unmapped record type: {0}".format(expression.fieldValue)
             )
     else:
-        value = unicode(expression.fieldValue)
+        values = (unicode(expression.fieldValue),)
 
-    value = value.translate(LDAP_QUOTING_TABLE)  # Escape special chars
+    # Escape special LDAP query characters
+    values = [value.translate(LDAP_QUOTING_TABLE) for value in values]
+    del value  # Symbol used below; ensure non-reuse of data
 
-    return matchType.queryString.format(
-        notOp=notOp, attribute=attribute, value=value
-    )
+    # Compose an query using each of the LDAP attributes cooresponding to the
+    # target field name.
 
+    if notOp:
+        operand = LDAPOperand.AND.value
+    else:
+        operand = LDAPOperand.OR.value
 
+    if notOp:
+        valueOperand = LDAPOperand.OR.value
+    else:
+        valueOperand = LDAPOperand.AND.value
+
+    queryStrings = [
+        ldapQueryStringFromQueryStrings(
+            valueOperand,
+            [
+                matchType.queryString.format(
+                    notOp=notOp, attribute=attribute, value=value
+                )
+                for value in values
+            ]
+        )
+        for attribute in attributes
+    ]
+
+    return ldapQueryStringFromQueryStrings(operand, queryStrings)
+
+
 def ldapQueryStringFromCompoundExpression(
-    expression, fieldNameMap, recordTypeMap
+    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
 ):
     """
     Generates an LDAP query string from a compound expression.
@@ -103,67 +157,73 @@
     @param expression: A compound expression.
     @type expression: L{MatchExpression}
 
-    @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
-        names.
-    @type fieldNameMap: L{dict}
+    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+        attribute names.
+    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+        of L{unicode} values.
 
+    @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+        LDAP object class names.
+    @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+        sequence of L{unicode} values.
+
     @return: An LDAP query string.
-    @rtype: C{unicode}
+    @rtype: L{unicode}
 
     @raises QueryNotSupportedError: If any sub-expression cannot be converted
         to an LDAP query.
     """
-    queryTokens = []
+    if expression.operand is Operand.AND:
+        operand = LDAPOperand.AND.value
 
-    if len(expression.expressions) > 1:
-        queryTokens.append(u"(")
+    elif expression.operand is Operand.OR:
+        operand = LDAPOperand.OR.value
 
-        if expression.operand is Operand.AND:
-            queryTokens.append(u"&")
-        else:
-            queryTokens.append(u"|")
-
-    for subExpression in expression.expressions:
-        queryTokens.append(
-            ldapQueryStringFromExpression(
-                subExpression, fieldNameMap, recordTypeMap
-            )
+    queryStrings = [
+        ldapQueryStringFromExpression(
+            subExpression,
+            fieldNameToAttributeMap, recordTypeToObjectClassMap
         )
+        for subExpression in expression.expressions
+    ]
 
-    if len(expression.expressions) > 1:
-        queryTokens.append(u")")
+    return ldapQueryStringFromQueryStrings(operand, queryStrings)
 
-    return u"".join(queryTokens)
 
-
 def ldapQueryStringFromExpression(
-    expression, fieldNameMap, recordTypeMap
+    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
 ):
     """
     Converts an expression into an LDAP query string.
 
-    @param fieldNameMap: A mapping from L{FieldName}s to native LDAP attribute
-        names.
-    @type fieldNameMap: L{dict}
-
     @param expression: An expression.
     @type expression: L{MatchExpression} or L{CompoundExpression}
 
-    @return: A native OpenDirectory query string
-    @rtype: C{unicode}
+    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
+        attribute names.
+    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
+        of L{unicode} values.
 
+    @param recordTypeToObjectClassMap: A mapping from L{RecordType}s to native
+        LDAP object class names.
+    @type recordTypeToObjectClassMap: L{dict} with L{RecordType} keys and
+        sequence of L{unicode} values.
+
+    @return: An LDAP query string.
+    @rtype: L{unicode}
+
     @raises QueryNotSupportedError: If the expression cannot be converted to an
         LDAP query.
     """
 
     if isinstance(expression, MatchExpression):
         return ldapQueryStringFromMatchExpression(
-            expression, fieldNameMap, recordTypeMap
+            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
         )
 
     if isinstance(expression, CompoundExpression):
         return ldapQueryStringFromCompoundExpression(
-            expression, fieldNameMap, recordTypeMap
+            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
         )
 
     raise QueryNotSupportedError(

Modified: twext/trunk/twext/who/ldap/test/test_service.py
===================================================================
--- twext/trunk/twext/who/ldap/test/test_service.py	2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/test/test_service.py	2014-01-17 03:02:21 UTC (rev 12369)
@@ -20,6 +20,8 @@
 LDAP directory service tests.
 """
 
+from itertools import chain
+
 import ldap
 from mockldap import MockLdap
 
@@ -31,7 +33,7 @@
 
 from ...idirectory import QueryNotSupportedError, FieldName as BaseFieldName
 from .._service import (
-    DEFAULT_FIELDNAME_MAP, DEFAULT_RECORDTYPE_MAP,
+    DEFAULT_FIELDNAME_ATTRIBUTE_MAP, DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
     LDAPBindAuthError,
     DirectoryService, DirectoryRecord,
 )
@@ -45,8 +47,8 @@
 
 
 
-TEST_FIELDNAME_MAP = dict(DEFAULT_FIELDNAME_MAP)
-TEST_FIELDNAME_MAP[BaseFieldName.uid] = u"__who_uid__"
+TEST_FIELDNAME_MAP = dict(DEFAULT_FIELDNAME_ATTRIBUTE_MAP)
+TEST_FIELDNAME_MAP[BaseFieldName.uid] = (u"__who_uid__",)
 
 
 
@@ -236,14 +238,16 @@
         return unicode(obj)
 
     def tuplify(record, fieldName):
-        name = TEST_FIELDNAME_MAP.get(fieldName, fieldName.name)
-
         if fieldName is BaseFieldName.recordType:
-            value = DEFAULT_RECORDTYPE_MAP[record.fields[fieldName]]
+            values = DEFAULT_RECORDTYPE_OBJECTCLASS_MAP[
+                record.fields[fieldName]
+            ]
         else:
-            value = toUnicode(record.fields[fieldName])
+            values = (toUnicode(record.fields[fieldName]),)
 
-        return (name, value)
+        for name in TEST_FIELDNAME_MAP.get(fieldName, fieldName.name):
+            for value in values:
+                yield (name, value)
 
     for records in service.index[service.fieldName.uid].itervalues():
         for record in records:
@@ -253,11 +257,11 @@
                 dc1=dc1, dc0=dc0
             )
 
-            recordData = dict(
-                tuplify(record, fieldName)
+            recordData = dict(chain(*(
+                list(tuplify(record, fieldName))
                 for fieldName in service.fieldName.iterconstants()
                 if fieldName in record.fields
-            )
+            )))
 
             data[dn] = recordData
 

Modified: twext/trunk/twext/who/ldap/test/test_util.py
===================================================================
--- twext/trunk/twext/who/ldap/test/test_util.py	2014-01-17 02:59:50 UTC (rev 12368)
+++ twext/trunk/twext/who/ldap/test/test_util.py	2014-01-17 03:02:21 UTC (rev 12369)
@@ -24,8 +24,10 @@
 from ...expression import (
     CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
 )
+from .._constants import LDAPOperand
 from .._service import DirectoryService
 from .._util import (
+    ldapQueryStringFromQueryStrings,
     ldapQueryStringFromMatchExpression,
     ldapQueryStringFromCompoundExpression,
     ldapQueryStringFromExpression,
@@ -51,7 +53,10 @@
         but we don't care for these tests, since we're not actually connecting
         to LDAP.
         """
-        return dict([(c, c.name) for c in service.fieldName.iterconstants()])
+        return dict([
+            (c, (c.name,))
+            for c in service.fieldName.iterconstants()
+        ])
 
 
     def recordTypeMap(self, service):
@@ -61,9 +66,44 @@
         names, but we don't care for these tests, since we're not actually
         connecting to LDAP.
         """
-        return dict([(c, c.name) for c in service.recordType.iterconstants()])
+        return dict([
+            (c, (c.name,))
+            for c in service.recordType.iterconstants()
+        ])
 
 
+    def _test_ldapQueryStringFromQueryStrings(self, queryStrings, expected):
+        for operand in (LDAPOperand.AND.value, LDAPOperand.OR.value):
+            compound = ldapQueryStringFromQueryStrings(operand, queryStrings)
+            self.assertEquals(compound, expected.format(operand=operand))
+
+
+    def test_ldapQueryStringFromQueryStrings_empty(self):
+        """
+        A single expression should just be returned as-is.
+        """
+        return self._test_ldapQueryStringFromQueryStrings((), u"")
+
+
+    def test_ldapQueryStringFromQueryStrings_single(self):
+        """
+        A single expression should just be returned as-is.
+        """
+        queryStrings = (u"(x=yzzy)",)
+        return self._test_ldapQueryStringFromQueryStrings(
+            queryStrings, queryStrings[0]
+        )
+
+
+    def test_ldapQueryStringFromQueryStrings_multiple(self):
+        """
+        Multiple expressions should just be combined with an operator.
+        """
+        return self._test_ldapQueryStringFromQueryStrings(
+            (u"(x=yzzy)", u"(xy=zzy)"), u"({operand}(x=yzzy)(xy=zzy))"
+        )
+
+
     def test_queryStringFromMatchExpression_matchTypes(self):
         """
         Match expressions with each match type produces the correct
@@ -202,6 +242,110 @@
         )
 
 
+    def _test_queryStringFromMatchExpression_multiAttribute(
+        self, flags, expected
+    ):
+        service = self.service()
+
+        expression = MatchExpression(
+            service.fieldName.emailAddresses, u"xyzzy",
+            flags=flags,
+        )
+
+        fieldNameToAttributeMap = {
+            service.fieldName.emailAddresses: (u"mail", u"alternateMail"),
+        }
+
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, fieldNameToAttributeMap, self.recordTypeMap(service)
+        )
+
+        self.assertEquals(queryString, expected)
+
+
+    def test_queryStringFromMatchExpression_multipleAttribute(self):
+        """
+        Match expression when the queried field name maps to multiple
+        attributes.
+        """
+
+        # We want a match for either attribute.
+        expected = u"(|(mail=xyzzy)(alternateMail=xyzzy))"
+
+        return self._test_queryStringFromMatchExpression_multiAttribute(
+            MatchFlags.none, expected
+        )
+
+
+    def test_queryStringFromMatchExpression_multipleAttribute_not(self):
+        """
+        Match expression when the queried field name maps to multiple
+        attributes and the NOT flag is set.
+        """
+
+        # We want a NOT match for both attributes.
+        expected = u"(&(!mail=xyzzy)(!alternateMail=xyzzy))"
+
+        return self._test_queryStringFromMatchExpression_multiAttribute(
+            MatchFlags.NOT, expected
+        )
+
+
+    def _test_queryStringFromMatchExpression_multiRecordType(
+        self, flags, expected
+    ):
+        service = self.service()
+
+        recordTypeField = service.fieldName.recordType
+
+        expression = MatchExpression(
+            recordTypeField, service.recordType.user,
+            flags=flags,
+        )
+
+        fieldNameToAttributeMap = self.fieldNameMap(service)
+
+        recordTypeToObjectClassMap = {
+            service.recordType.user: (u"person", u"account"),
+        }
+
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
+        )
+
+        self.assertEquals(
+            queryString,
+            expected.format(attr=fieldNameToAttributeMap[recordTypeField][0])
+        )
+
+
+    def test_queryStringFromMatchExpression_multipleRecordType(self):
+        """
+        Match expression when the queried field name is the record type field,
+        which maps to multiple attributes.
+        """
+
+        # We want a match for both values.
+        expected = u"(&({attr}=person)({attr}=account))"
+
+        return self._test_queryStringFromMatchExpression_multiRecordType(
+            MatchFlags.none, expected
+        )
+
+    def test_queryStringFromMatchExpression_multipleRecordType_not(self):
+        """
+        Match expression when the queried field name is the record type field,
+        which maps to multiple attributes and the NOT flag is set.
+        """
+
+        # We want a NOT match for either value.
+        expected = u"(|(!{attr}=person)(!{attr}=account))"
+
+        return self._test_queryStringFromMatchExpression_multiRecordType(
+            MatchFlags.NOT, expected
+        )
+
+
     def test_queryStringFromCompoundExpression_single(
         self, queryFunction=ldapQueryStringFromCompoundExpression
     ):
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/97d15dec/attachment.html>


More information about the calendarserver-changes mailing list