<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[12391] twext/trunk/twext/who/ldap</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.calendarserver.org//changeset/12391">12391</a></dd>
<dt>Author</dt> <dd>wsanchez@apple.com</dd>
<dt>Date</dt> <dd>2014-01-17 19:10:38 -0800 (Fri, 17 Jan 2014)</dd>
</dl>

<h3>Log Message</h3>
<pre>We no longer describe a record type to LDAP as a set of values for a specific attribute (eg. objectType).

Instead, we describe it as a relative distinguished name (presently unused), and a sequence of (attribute, value) pairs to match against.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#twexttrunktwextwholdap_constantspy">twext/trunk/twext/who/ldap/_constants.py</a></li>
<li><a href="#twexttrunktwextwholdap_servicepy">twext/trunk/twext/who/ldap/_service.py</a></li>
<li><a href="#twexttrunktwextwholdap_utilpy">twext/trunk/twext/who/ldap/_util.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_servicepy">twext/trunk/twext/who/ldap/test/test_service.py</a></li>
<li><a href="#twexttrunktwextwholdaptesttest_utilpy">twext/trunk/twext/who/ldap/test/test_util.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="twexttrunktwextwholdap_constantspy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/_constants.py (12390 => 12391)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_constants.py        2014-01-17 23:34:52 UTC (rev 12390)
+++ twext/trunk/twext/who/ldap/_constants.py        2014-01-18 03:10:38 UTC (rev 12391)
</span><span class="lines">@@ -125,6 +125,7 @@
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     LDAP match flags.
</span><span class="cx">     &quot;&quot;&quot;
</span><ins>+    none = ValueConstant(u&quot;&quot;)
</ins><span class="cx">     NOT = ValueConstant(u&quot;!&quot;)
</span><span class="cx"> 
</span><span class="cx"> 
</span></span></pre></div>
<a id="twexttrunktwextwholdap_servicepy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/_service.py (12390 => 12391)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_service.py        2014-01-17 23:34:52 UTC (rev 12390)
+++ twext/trunk/twext/who/ldap/_service.py        2014-01-18 03:10:38 UTC (rev 12391)
</span><span class="lines">@@ -50,25 +50,6 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-# Maps field name -&gt; LDAP attribute names
-DEFAULT_FIELDNAME_ATTRIBUTE_MAP = MappingProxyType({
-    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 -&gt; LDAP object class names
-DEFAULT_RECORDTYPE_OBJECTCLASS_MAP = MappingProxyType({
-    BaseRecordType.user: (LDAPObjectClass.inetOrgPerson.value,),
-    BaseRecordType.group: (LDAPObjectClass.groupOfNames.value,),
-})
-
-
-
</del><span class="cx"> #
</span><span class="cx"> # Exceptions
</span><span class="cx"> #
</span><span class="lines">@@ -124,6 +105,77 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> #
</span><ins>+# LDAP schema descriptions
+#
+
+class RecordTypeSchema(object):
+    &quot;&quot;&quot;
+    Describes the LDAP schema for a record type.
+    &quot;&quot;&quot;
+    def __init__(self, relativeDN, attributes):
+        &quot;&quot;&quot;
+        @param relativeDN: The relative distinguished name for the record type.
+            This is prepended to the service's base distinguished name when
+            searching for records of this type.
+        @type relativeDN: L{unicode}
+
+        @param attributes: Attribute/value pairs that are expected for records
+            of this type.
+        @type attributes: iterable of sequences containing two L{unicode}s
+        &quot;&quot;&quot;
+        self.relativeDN = relativeDN
+        self.attributes = tuple(tuple(pair) for pair in attributes)
+
+
+
+# We use strings (constant.value) instead of constants for the values in
+# these mappings because it's meant to be configurable by application users,
+# and user input forms such as config files aren't going to be able to use
+# the constants.
+
+# Maps field name -&gt; LDAP attribute names
+DEFAULT_FIELDNAME_ATTRIBUTE_MAP = MappingProxyType({
+    BaseFieldName.guid: (LDAPAttribute.generatedUUID.value,),
+    BaseFieldName.shortNames: (LDAPAttribute.uid.value,),
+    BaseFieldName.fullNames: (LDAPAttribute.cn.value,),
+    BaseFieldName.emailAddresses: (LDAPAttribute.mail.value,),
+    BaseFieldName.password: (LDAPAttribute.userPassword.value,),
+})
+
+# Information about record types
+DEFAULT_RECORDTYPE_SCHEMAS = MappingProxyType({
+
+    BaseRecordType.user: RecordTypeSchema(
+        # ou=person
+        relativeDN=u&quot;ou={0}&quot;.format(LDAPObjectClass.person.value),
+
+        # (objectClass=inetOrgPerson)
+        attributes=(
+            (
+                LDAPAttribute.objectClass.value,
+                LDAPObjectClass.inetOrgPerson.value,
+            ),
+        ),
+    ),
+
+    BaseRecordType.group: RecordTypeSchema(
+        # ou=groupOfNames
+        relativeDN=u&quot;ou={0}&quot;.format(LDAPObjectClass.groupOfNames.value),
+
+        # (objectClass=groupOfNames)
+        attributes=(
+            (
+                LDAPAttribute.objectClass.value,
+                LDAPObjectClass.groupOfNames.value,
+            ),
+        ),
+    ),
+
+})
+
+
+
+#
</ins><span class="cx"> # Directory Service
</span><span class="cx"> #
</span><span class="cx"> 
</span><span class="lines">@@ -148,8 +200,8 @@
</span><span class="cx">         tlsCACertificateFile=None,
</span><span class="cx">         tlsCACertificateDirectory=None,
</span><span class="cx">         useTLS=False,
</span><del>-        fieldNameToAttributeMap=DEFAULT_FIELDNAME_ATTRIBUTE_MAP,
-        recordTypeToObjectClassMap=DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
</del><ins>+        fieldNameToAttributesMap=DEFAULT_FIELDNAME_ATTRIBUTE_MAP,
+        recordTypeSchemas=DEFAULT_RECORDTYPE_SCHEMAS,
</ins><span class="cx">         uidField=BaseFieldName.uid,
</span><span class="cx">         _debug=False,
</span><span class="cx">     ):
</span><span class="lines">@@ -176,15 +228,14 @@
</span><span class="cx">         @param useTLS: Enable the use of TLS.
</span><span class="cx">         @type useTLS: L{bool}
</span><span class="cx"> 
</span><del>-        @param fieldNameToAttributeMap: A mapping of field names to LDAP
</del><ins>+        @param fieldNameToAttributesMap: A mapping of field names to LDAP
</ins><span class="cx">             attribute names.
</span><del>-        @type fieldNameToAttributeMap: mapping with L{NamedConstant} keys and
</del><ins>+        @type fieldNameToAttributesMap: mapping with L{NamedConstant} keys and
</ins><span class="cx">             sequence of L{unicode} values
</span><span class="cx"> 
</span><del>-        @param recordTypeToObjectClassMap: A mapping of record types to LDAP
-            object classes.
-        @type recordTypeToObjectClassMap: mapping with L{NamedConstant} keys
-            and sequence of L{unicode} values
</del><ins>+        @param recordTypeSchemas: Schema information for record types.
+        @type recordTypeSchemas: mapping from L{NamedConstant} to
+            L{RecordTypeSchema}
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         self.url = url
</span><span class="lines">@@ -209,30 +260,14 @@
</span><span class="cx">         else:
</span><span class="cx">             self._debug = None
</span><span class="cx"> 
</span><del>-        def reverseDict(sourceName, source):
-            new = {}
</del><ins>+        if self.fieldName.recordType in fieldNameToAttributesMap:
+            raise TypeError(&quot;Record type field may not be mapped&quot;)
</ins><span class="cx"> 
</span><del>-            for key, values in source.iteritems():
-                for value in values:
-                    if value in new:
-                        raise LDAPConfigurationError(
-                            u&quot;{0} map has duplicate values: {1}&quot;
-                            .format(sourceName, value)
-                        )
-                    new[value] = key
-
-            return new
-
-        self._fieldNameToAttributeMap = fieldNameToAttributeMap
</del><ins>+        self._fieldNameToAttributesMap = fieldNameToAttributesMap
</ins><span class="cx">         self._attributeToFieldNameMap = reverseDict(
</span><del>-            &quot;Field name&quot;, fieldNameToAttributeMap
</del><ins>+            &quot;Field name&quot;, fieldNameToAttributesMap
</ins><span class="cx">         )
</span><del>-
-        self._recordTypeToObjectClassMap = recordTypeToObjectClassMap
-        self._objectClassToRecordTypeMap = reverseDict(
-            &quot;Record type&quot;, recordTypeToObjectClassMap
-        )
-
</del><ins>+        self._recordTypeSchemas = recordTypeSchemas
</ins><span class="cx">         self._uidField = uidField
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -312,69 +347,68 @@
</span><span class="cx">         self.log.debug(&quot;Performing LDAP query: {query}&quot;, query=queryString)
</span><span class="cx"> 
</span><span class="cx">         reply = connection.search_s(
</span><del>-            self._baseDN, ldap.SCOPE_SUBTREE, queryString  # attrs
</del><ins>+            self._baseDN, ldap.SCOPE_SUBTREE, queryString  # FIXME: attrs
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         records = []
</span><span class="cx"> 
</span><del>-        # Note: self._uidField is the name of the field in
-        # self._fieldNameToAttributeMap that tells us which LDAP attribute
</del><ins>+        # self._uidField is the name of the field in
+        # self._fieldNameToAttributesMap that tells us which LDAP attribute
</ins><span class="cx">         # we are using to determine the UID of the record.
</span><span class="cx"> 
</span><del>-        uidField = self.fieldName.uid
-        uidAttribute = self._fieldNameToAttributeMap[self._uidField][0]
</del><ins>+        uidAttribute = self._fieldNameToAttributesMap[self._uidField][0]
</ins><span class="cx"> 
</span><del>-        recordTypeField = self.fieldName.recordType
-        recordTypeAttributes = (
-            self._fieldNameToAttributeMap[self.fieldName.recordType]
-        )
</del><ins>+        # recordTypeAttributes = set(chain(*[
+        #     info[u&quot;attributes&quot;].iterkeys()
+        #     for info in self._recordTypeInfo.itervalues()
+        # ]))
</ins><span class="cx"> 
</span><span class="cx">         for dn, recordData in reply:
</span><span class="cx"> 
</span><del>-            # Attributes used to determine the record type are required, since
-            # record type is very much required.
</del><ins>+            # Fetch the UID
</ins><span class="cx"> 
</span><del>-            for recordTypeAttribute in recordTypeAttributes:
-                if recordTypeAttribute not in recordData:
-                    self.log.debug(
-                        &quot;Ignoring LDAP record data without record type &quot;
-                        &quot;attribute {attribute!r}: &quot;
-                        &quot;{recordData!r}&quot;,
-                        attribute=recordTypeAttribute, recordData=recordData,
-                    )
-                    continue
-
-            # Make a dict of fields -&gt; values from the incoming dict of
-            # attributes -&gt; values.
-
-            fields = dict([
-                (self._attributeToFieldNameMap[k], v)
-                for k, v in recordData.iteritems()
-                if k in self._attributeToFieldNameMap
-            ])
-
-            # Make sure the UID is populated
-
</del><span class="cx">             try:
</span><del>-                fields[uidField] = recordData[uidAttribute]
</del><ins>+                uid = recordData[uidAttribute]
</ins><span class="cx">             except KeyError:
</span><span class="cx">                 self.log.debug(
</span><del>-                    &quot;Ignoring LDAP record data with no UID attribute &quot;
-                    &quot;{source._uidField!r}: {recordData!r}&quot;,
</del><ins>+                    &quot;Ignoring LDAP record data; no UID attribute &quot;
+                    &quot;({source._uidField}): {recordData!r}&quot;,
</ins><span class="cx">                     recordData=recordData
</span><span class="cx">                 )
</span><span class="cx">                 continue
</span><span class="cx"> 
</span><ins>+            # Determine the record type
</ins><span class="cx"> 
</span><del>-            # Coerce data to the correct type
</del><ins>+            recordType = recordTypeForRecordData(
+                self._recordTypeSchemas, recordData
+            )
</ins><span class="cx"> 
</span><del>-            for fieldName, value in fields.iteritems():
</del><ins>+            if recordType is None:
+                self.log.debug(
+                    &quot;Ignoring LDAP record data; unable to determine record &quot;
+                    &quot;type: {recordData!r}&quot;,
+                    recordData=recordData,
+                )
+                continue
+
+            # Populate a fields dictionary
+
+            fields = {}
+
+            for attribute, value in recordData.iteritems():
+                fieldName = self._attributeToFieldNameMap.get(attribute, None)
+                if fieldName is None:
+                    self.log.debug(
+                        &quot;Unmapped LDAP attribute {attribute!r} in record &quot;
+                        &quot;data: {recordData!r}&quot;,
+                        attribute=attribute, recordData=recordData,
+                    )
+
</ins><span class="cx">                 valueType = self.fieldName.valueType(fieldName)
</span><span class="cx"> 
</span><del>-                if fieldName is recordTypeField:
-                    value = self._objectClassToRecordTypeMap[value]
-                elif valueType in (unicode, UUID):
-                    value = valueType(value)
</del><ins>+                if valueType in (unicode, UUID):
+                    fields[fieldName] = valueType(value)
+
</ins><span class="cx">                 else:
</span><span class="cx">                     raise LDAPConfigurationError(
</span><span class="cx">                         &quot;Unknown value type {0} for field {1}&quot;.format(
</span><span class="lines">@@ -382,8 +416,11 @@
</span><span class="cx">                         )
</span><span class="cx">                     )
</span><span class="cx"> 
</span><del>-                fields[fieldName] = value
</del><ins>+            # Set record type and uid fields
</ins><span class="cx"> 
</span><ins>+            fields[self.fieldName.recordType] = recordType
+            fields[self.fieldName.uid] = uid
+
</ins><span class="cx">             # Make a record object from fields.
</span><span class="cx"> 
</span><span class="cx">             record = DirectoryRecord(self, fields)
</span><span class="lines">@@ -398,7 +435,7 @@
</span><span class="cx">         if isinstance(expression, MatchExpression):
</span><span class="cx">             queryString = ldapQueryStringFromMatchExpression(
</span><span class="cx">                 expression,
</span><del>-                self._fieldNameToAttributeMap, self._recordTypeToObjectClassMap
</del><ins>+                self._fieldNameToAttributesMap, self._recordTypeSchemas
</ins><span class="cx">             )
</span><span class="cx">             return self._recordsFromQueryString(queryString)
</span><span class="cx"> 
</span><span class="lines">@@ -413,7 +450,7 @@
</span><span class="cx"> 
</span><span class="cx">         queryString = ldapQueryStringFromCompoundExpression(
</span><span class="cx">             expression,
</span><del>-            self._fieldNameToAttributeMap, self._recordTypeToObjectClassMap
</del><ins>+            self._fieldNameToAttributesMap, self._recordTypeSchemas
</ins><span class="cx">         )
</span><span class="cx">         return self._recordsFromQueryString(queryString)
</span><span class="cx"> 
</span><span class="lines">@@ -423,3 +460,42 @@
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     LDAP directory record.
</span><span class="cx">     &quot;&quot;&quot;
</span><ins>+
+
+
+def reverseDict(sourceName, source):
+    new = {}
+
+    for key, values in source.iteritems():
+        for value in values:
+            if value in new:
+                raise LDAPConfigurationError(
+                    u&quot;{0} map has duplicate values: {1}&quot;
+                    .format(sourceName, value)
+                )
+            new[value] = key
+
+    return new
+
+
+def recordTypeForRecordData(recordTypeSchemas, recordData):
+    &quot;&quot;&quot;
+    Given info about record types, determine the record type for a blob of
+    LDAP record data.
+
+    @param recordTypeSchemas: Schema information for record types.
+    @type recordTypeSchemas: mapping from L{NamedConstant} to
+        L{RecordTypeSchema}
+
+    @param recordData: LDAP record data.
+    @type recordData: mapping
+    &quot;&quot;&quot;
+
+    for recordType, schema in recordTypeSchemas.iteritems():
+        for attribute, value in schema.attributes:
+            if recordData.get(attribute, None) != value:
+                break
+        else:
+            return recordType
+
+    return None
</ins></span></pre></div>
<a id="twexttrunktwextwholdap_utilpy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/_util.py (12390 => 12391)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/_util.py        2014-01-17 23:34:52 UTC (rev 12390)
+++ twext/trunk/twext/who/ldap/_util.py        2014-01-18 03:10:38 UTC (rev 12391)
</span><span class="lines">@@ -33,7 +33,7 @@
</span><span class="cx">     @type operand: L{unicode}
</span><span class="cx"> 
</span><span class="cx">     @param queryStrings: LDAP query strings.
</span><del>-    @type queryStrings: iterable of L{unicode}
</del><ins>+    @type queryStrings: sequence of L{unicode}
</ins><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     if len(queryStrings) == 1:
</span><span class="cx">         return queryStrings[0]
</span><span class="lines">@@ -51,7 +51,7 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> def ldapQueryStringFromMatchExpression(
</span><del>-    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</del><ins>+    expression, fieldNameToAttributesMap, recordTypeSchemas
</ins><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Generates an LDAP query string from a match expression.
</span><span class="lines">@@ -59,15 +59,14 @@
</span><span class="cx">     @param expression: A match expression.
</span><span class="cx">     @type expression: L{MatchExpression}
</span><span class="cx"> 
</span><del>-    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
</del><ins>+    @param fieldNameToAttributesMap: A mapping from field names to native LDAP
</ins><span class="cx">         attribute names.
</span><del>-    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
</del><ins>+    @type fieldNameToAttributesMap: L{dict} with L{FieldName} keys and sequence
</ins><span class="cx">         of L{unicode} values.
</span><span class="cx"> 
</span><del>-    @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.
</del><ins>+    @param recordTypeSchemas: Schema information for record types.
+    @type recordTypeSchemas: mapping from L{NamedConstant} to
+        L{RecordTypeSchema}
</ins><span class="cx"> 
</span><span class="cx">     @return: An LDAP query string.
</span><span class="cx">     @rtype: L{unicode}
</span><span class="lines">@@ -89,7 +88,7 @@
</span><span class="cx">         notOp = LDAPMatchFlags.NOT.value
</span><span class="cx">         operand = LDAPOperand.AND.value
</span><span class="cx">     else:
</span><del>-        notOp = u&quot;&quot;
</del><ins>+        notOp = LDAPMatchFlags.none.value
</ins><span class="cx">         operand = LDAPOperand.OR.value
</span><span class="cx"> 
</span><span class="cx">     # FIXME: It doesn't look like LDAP queries can be case sensitive.
</span><span class="lines">@@ -100,53 +99,63 @@
</span><span class="cx">     #     raise NotImplementedError(&quot;Need to handle case sensitive&quot;)
</span><span class="cx"> 
</span><span class="cx">     fieldName = expression.fieldName
</span><del>-    try:
-        attributes = fieldNameToAttributeMap[fieldName]
-    except KeyError:
-        raise QueryNotSupportedError(
-            &quot;Unmapped field name: {0}&quot;.format(expression.fieldName)
-        )
</del><span class="cx"> 
</span><span class="cx">     if fieldName is FieldName.recordType:
</span><ins>+        if matchType is not LDAPMatchType.equals:
+            raise QueryNotSupportedError(
+                &quot;Match type for record type must be equals, not {0!r}&quot;
+                .format(expression.matchType)
+            )
+
</ins><span class="cx">         try:
</span><del>-            values = recordTypeToObjectClassMap[expression.fieldValue]
</del><ins>+            schema = recordTypeSchemas[expression.fieldValue]
</ins><span class="cx">         except KeyError:
</span><span class="cx">             raise QueryNotSupportedError(
</span><span class="cx">                 &quot;Unmapped record type: {0}&quot;.format(expression.fieldValue)
</span><span class="cx">             )
</span><del>-    else:
-        values = (unicode(expression.fieldValue),)
</del><span class="cx"> 
</span><del>-    # 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
</del><ins>+        if notOp:
+            valueOperand = LDAPOperand.OR.value
+        else:
+            valueOperand = LDAPOperand.AND.value
</ins><span class="cx"> 
</span><del>-    # Compose an query using each of the LDAP attributes cooresponding to the
-    # target field name.
</del><ins>+        queryStrings = [
+            matchType.queryString.format(
+                notOp=notOp, attribute=attribute, value=value,
+            )
+            for attribute, value in schema.attributes
+        ]
</ins><span class="cx"> 
</span><del>-    if notOp:
-        valueOperand = LDAPOperand.OR.value
</del><ins>+        return ldapQueryStringFromQueryStrings(valueOperand, queryStrings)
+
</ins><span class="cx">     else:
</span><del>-        valueOperand = LDAPOperand.AND.value
</del><ins>+        try:
+            attributes = fieldNameToAttributesMap[fieldName]
+        except KeyError:
+            raise QueryNotSupportedError(
+                &quot;Unmapped field name: {0}&quot;.format(expression.fieldName)
+            )
</ins><span class="cx"> 
</span><del>-    queryStrings = [
-        ldapQueryStringFromQueryStrings(
-            valueOperand,
-            [
-                matchType.queryString.format(
-                    notOp=notOp, attribute=attribute, value=value
-                )
-                for value in values
-            ]
-        )
-        for attribute in attributes
-    ]
</del><ins>+        # Covert to unicode and escape special LDAP query characters
+        value = unicode(expression.fieldValue).translate(LDAP_QUOTING_TABLE)
</ins><span class="cx"> 
</span><del>-    return ldapQueryStringFromQueryStrings(operand, queryStrings)
</del><ins>+        # Compose an query using each of the LDAP attributes cooresponding to
+        # the target field name.
</ins><span class="cx"> 
</span><ins>+        queryStrings = [
+            matchType.queryString.format(
+                notOp=notOp, attribute=attribute, value=value
+            )
+            for attribute in attributes
+        ]
</ins><span class="cx"> 
</span><ins>+        return ldapQueryStringFromQueryStrings(operand, queryStrings)
+
+    raise AssertionError(&quot;We shouldn't be here.&quot;)
+
+
</ins><span class="cx"> def ldapQueryStringFromCompoundExpression(
</span><del>-    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</del><ins>+    expression, fieldNameToAttributesMap, recordTypeSchemas
</ins><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Generates an LDAP query string from a compound expression.
</span><span class="lines">@@ -154,15 +163,14 @@
</span><span class="cx">     @param expression: A compound expression.
</span><span class="cx">     @type expression: L{MatchExpression}
</span><span class="cx"> 
</span><del>-    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
</del><ins>+    @param fieldNameToAttributesMap: A mapping from field names to native LDAP
</ins><span class="cx">         attribute names.
</span><del>-    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
</del><ins>+    @type fieldNameToAttributesMap: L{dict} with L{FieldName} keys and sequence
</ins><span class="cx">         of L{unicode} values.
</span><span class="cx"> 
</span><del>-    @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.
</del><ins>+    @param recordTypeSchemas: Schema information for record types.
+    @type recordTypeSchemas: mapping from L{NamedConstant} to
+        L{RecordTypeSchema}
</ins><span class="cx"> 
</span><span class="cx">     @return: An LDAP query string.
</span><span class="cx">     @rtype: L{unicode}
</span><span class="lines">@@ -179,7 +187,7 @@
</span><span class="cx">     queryStrings = [
</span><span class="cx">         ldapQueryStringFromExpression(
</span><span class="cx">             subExpression,
</span><del>-            fieldNameToAttributeMap, recordTypeToObjectClassMap
</del><ins>+            fieldNameToAttributesMap, recordTypeSchemas
</ins><span class="cx">         )
</span><span class="cx">         for subExpression in expression.expressions
</span><span class="cx">     ]
</span><span class="lines">@@ -188,7 +196,7 @@
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="cx"> def ldapQueryStringFromExpression(
</span><del>-    expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</del><ins>+    expression, fieldNameToAttributesMap, recordTypeSchemas
</ins><span class="cx"> ):
</span><span class="cx">     &quot;&quot;&quot;
</span><span class="cx">     Converts an expression into an LDAP query string.
</span><span class="lines">@@ -196,15 +204,14 @@
</span><span class="cx">     @param expression: An expression.
</span><span class="cx">     @type expression: L{MatchExpression} or L{CompoundExpression}
</span><span class="cx"> 
</span><del>-    @param fieldNameToAttributeMap: A mapping from field names to native LDAP
</del><ins>+    @param fieldNameToAttributesMap: A mapping from field names to native LDAP
</ins><span class="cx">         attribute names.
</span><del>-    @type fieldNameToAttributeMap: L{dict} with L{FieldName} keys and sequence
</del><ins>+    @type fieldNameToAttributesMap: L{dict} with L{FieldName} keys and sequence
</ins><span class="cx">         of L{unicode} values.
</span><span class="cx"> 
</span><del>-    @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.
</del><ins>+    @param recordTypeSchemas: Schema information for record types.
+    @type recordTypeSchemas: mapping from L{NamedConstant} to
+        L{RecordTypeSchema}
</ins><span class="cx"> 
</span><span class="cx">     @return: An LDAP query string.
</span><span class="cx">     @rtype: L{unicode}
</span><span class="lines">@@ -215,12 +222,12 @@
</span><span class="cx"> 
</span><span class="cx">     if isinstance(expression, MatchExpression):
</span><span class="cx">         return ldapQueryStringFromMatchExpression(
</span><del>-            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</del><ins>+            expression, fieldNameToAttributesMap, recordTypeSchemas
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">     if isinstance(expression, CompoundExpression):
</span><span class="cx">         return ldapQueryStringFromCompoundExpression(
</span><del>-            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</del><ins>+            expression, fieldNameToAttributesMap, recordTypeSchemas
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">     raise QueryNotSupportedError(
</span></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_servicepy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/test/test_service.py (12390 => 12391)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_service.py        2014-01-17 23:34:52 UTC (rev 12390)
+++ twext/trunk/twext/who/ldap/test/test_service.py        2014-01-18 03:10:38 UTC (rev 12391)
</span><span class="lines">@@ -33,7 +33,7 @@
</span><span class="cx"> 
</span><span class="cx"> from ...idirectory import QueryNotSupportedError, FieldName as BaseFieldName
</span><span class="cx"> from .._service import (
</span><del>-    DEFAULT_FIELDNAME_ATTRIBUTE_MAP, DEFAULT_RECORDTYPE_OBJECTCLASS_MAP,
</del><ins>+    DEFAULT_FIELDNAME_ATTRIBUTE_MAP, DEFAULT_RECORDTYPE_SCHEMAS,
</ins><span class="cx">     LDAPBindAuthError,
</span><span class="cx">     DirectoryService, DirectoryRecord,
</span><span class="cx"> )
</span><span class="lines">@@ -85,7 +85,7 @@
</span><span class="cx">         return DirectoryService(
</span><span class="cx">             url=self.url,
</span><span class="cx">             baseDN=self.baseDN,
</span><del>-            fieldNameToAttributeMap=TEST_FIELDNAME_MAP,
</del><ins>+            fieldNameToAttributesMap=TEST_FIELDNAME_MAP,
</ins><span class="cx">             **kwargs
</span><span class="cx">         )
</span><span class="cx"> 
</span><span class="lines">@@ -238,16 +238,22 @@
</span><span class="cx">         return unicode(obj)
</span><span class="cx"> 
</span><span class="cx">     def tuplify(record, fieldName):
</span><ins>+        fieldValue = record.fields[fieldName]
+
</ins><span class="cx">         if fieldName is BaseFieldName.recordType:
</span><del>-            values = DEFAULT_RECORDTYPE_OBJECTCLASS_MAP[
-                record.fields[fieldName]
-            ]
</del><ins>+            schema = DEFAULT_RECORDTYPE_SCHEMAS[fieldValue]
+
+            return schema.attributes
+
</ins><span class="cx">         else:
</span><del>-            values = (toUnicode(record.fields[fieldName]),)
</del><ins>+            value = toUnicode(fieldValue)
</ins><span class="cx"> 
</span><del>-        for name in TEST_FIELDNAME_MAP.get(fieldName, fieldName.name):
-            for value in values:
-                yield (name, value)
</del><ins>+            return (
+                (name, value)
+                for name in TEST_FIELDNAME_MAP.get(
+                    fieldName, (u&quot;__&quot; + fieldName.name,)
+                )
+            )
</ins><span class="cx"> 
</span><span class="cx">     for records in service.index[service.fieldName.uid].itervalues():
</span><span class="cx">         for record in records:
</span></span></pre></div>
<a id="twexttrunktwextwholdaptesttest_utilpy"></a>
<div class="modfile"><h4>Modified: twext/trunk/twext/who/ldap/test/test_util.py (12390 => 12391)</h4>
<pre class="diff"><span>
<span class="info">--- twext/trunk/twext/who/ldap/test/test_util.py        2014-01-17 23:34:52 UTC (rev 12390)
+++ twext/trunk/twext/who/ldap/test/test_util.py        2014-01-18 03:10:38 UTC (rev 12391)
</span><span class="lines">@@ -25,7 +25,7 @@
</span><span class="cx">     CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
</span><span class="cx"> )
</span><span class="cx"> from .._constants import LDAPOperand
</span><del>-from .._service import DirectoryService
</del><ins>+from .._service import DirectoryService, RecordTypeSchema
</ins><span class="cx"> from .._util import (
</span><span class="cx">     ldapQueryStringFromQueryStrings,
</span><span class="cx">     ldapQueryStringFromMatchExpression,
</span><span class="lines">@@ -59,7 +59,7 @@
</span><span class="cx">         ])
</span><span class="cx"> 
</span><span class="cx"> 
</span><del>-    def recordTypeMap(self, service):
</del><ins>+    def recordTypeSchemas(self, service):
</ins><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         Create a mapping from record types to LDAP object class names.
</span><span class="cx">         The object class names returned here are not real LDAP object class
</span><span class="lines">@@ -67,7 +67,15 @@
</span><span class="cx">         connecting to LDAP.
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx">         return dict([
</span><del>-            (c, (c.name,))
</del><ins>+            (
+                c,
+                RecordTypeSchema(
+                    relativeDN=NotImplemented,  # Don't expect this to be used
+                    attributes=(
+                        (u&quot;recordTypeAttribute&quot;, c.name),
+                    )
+                )
+            )
</ins><span class="cx">             for c in service.recordType.iterconstants()
</span><span class="cx">         ])
</span><span class="cx"> 
</span><span class="lines">@@ -127,7 +135,7 @@
</span><span class="cx">             )
</span><span class="cx">             queryString = ldapQueryStringFromMatchExpression(
</span><span class="cx">                 expression,
</span><del>-                self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+                self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">             )
</span><span class="cx">             expected = u&quot;({attribute}{expected})&quot;.format(
</span><span class="cx">                 attribute=u&quot;shortNames&quot;, expected=expected
</span><span class="lines">@@ -147,7 +155,7 @@
</span><span class="cx">         )
</span><span class="cx">         queryString = ldapQueryStringFromMatchExpression(
</span><span class="cx">             expression,
</span><del>-            self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+            self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">         )
</span><span class="cx">         expected = u&quot;(!{attribute}=xyzzy)&quot;.format(
</span><span class="cx">             attribute=u&quot;shortNames&quot;,
</span><span class="lines">@@ -168,7 +176,7 @@
</span><span class="cx">         )
</span><span class="cx">         queryString = ldapQueryStringFromMatchExpression(
</span><span class="cx">             expression,
</span><del>-            self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+            self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">         )
</span><span class="cx">         expected = u&quot;???????({attribute}=xyzzy)&quot;.format(
</span><span class="cx">             attribute=u&quot;shortNames&quot;,
</span><span class="lines">@@ -193,7 +201,7 @@
</span><span class="cx">         )
</span><span class="cx">         queryString = ldapQueryStringFromMatchExpression(
</span><span class="cx">             expression,
</span><del>-            self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+            self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">         )
</span><span class="cx">         expected = u&quot;({attribute}={expected})&quot;.format(
</span><span class="cx">             attribute=u&quot;fullNames&quot;,
</span><span class="lines">@@ -219,7 +227,7 @@
</span><span class="cx">             QueryNotSupportedError,
</span><span class="cx">             ldapQueryStringFromMatchExpression,
</span><span class="cx">             expression,
</span><del>-            self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+            self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -238,7 +246,7 @@
</span><span class="cx">             QueryNotSupportedError,
</span><span class="cx">             ldapQueryStringFromMatchExpression,
</span><span class="cx">             expression,
</span><del>-            self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+            self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -257,7 +265,8 @@
</span><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         queryString = ldapQueryStringFromMatchExpression(
</span><del>-            expression, fieldNameToAttributeMap, self.recordTypeMap(service)
</del><ins>+            expression,
+            fieldNameToAttributeMap, self.recordTypeSchemas(service)
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         self.assertEquals(queryString, expected)
</span><span class="lines">@@ -305,17 +314,37 @@
</span><span class="cx"> 
</span><span class="cx">         fieldNameToAttributeMap = self.fieldNameMap(service)
</span><span class="cx"> 
</span><del>-        recordTypeToObjectClassMap = {
-            service.recordType.user: (u&quot;person&quot;, u&quot;account&quot;),
</del><ins>+        recordTypeAttr = fieldNameToAttributeMap[recordTypeField][0]
+        type1 = u&quot;person&quot;
+        type2 = u&quot;coolPerson&quot;
+
+        statusAttr = u&quot;accountStatus&quot;
+        status1 = u&quot;active&quot;
+
+        recordTypeSchemas = {
+            service.recordType.user: RecordTypeSchema(
+                relativeDN=NotImplemented,  # Don't expect this to be used.
+                attributes=(
+                    (recordTypeAttr, type1),
+                    (recordTypeAttr, type2),
+                    (statusAttr, status1),
+                )
+            )
</ins><span class="cx">         }
</span><span class="cx"> 
</span><span class="cx">         queryString = ldapQueryStringFromMatchExpression(
</span><del>-            expression, fieldNameToAttributeMap, recordTypeToObjectClassMap
</del><ins>+            expression, fieldNameToAttributeMap, recordTypeSchemas
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx">         self.assertEquals(
</span><span class="cx">             queryString,
</span><del>-            expected.format(attr=fieldNameToAttributeMap[recordTypeField][0])
</del><ins>+            expected.format(
+                recordType=recordTypeAttr,
+                type1=type1,
+                type2=type2,
+                accountStatus=statusAttr,
+                status1=status1,
+            )
</ins><span class="cx">         )
</span><span class="cx"> 
</span><span class="cx"> 
</span><span class="lines">@@ -326,7 +355,11 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # We want a match for both values.
</span><del>-        expected = u&quot;(&amp;({attr}=person)({attr}=account))&quot;
</del><ins>+        expected = (
+            u&quot;(&amp;({recordType}={type1})&quot;
+            u&quot;({recordType}={type2})&quot;
+            u&quot;({accountStatus}={status1}))&quot;
+        )
</ins><span class="cx"> 
</span><span class="cx">         return self._test_queryStringFromMatchExpression_multiRecordType(
</span><span class="cx">             MatchFlags.none, expected
</span><span class="lines">@@ -339,7 +372,11 @@
</span><span class="cx">         &quot;&quot;&quot;
</span><span class="cx"> 
</span><span class="cx">         # We want a NOT match for either value.
</span><del>-        expected = u&quot;(|(!{attr}=person)(!{attr}=account))&quot;
</del><ins>+        expected = (
+            u&quot;(|(!{recordType}={type1})&quot;
+            u&quot;(!{recordType}={type2})&quot;
+            u&quot;(!{accountStatus}={status1}))&quot;
+        )
</ins><span class="cx"> 
</span><span class="cx">         return self._test_queryStringFromMatchExpression_multiRecordType(
</span><span class="cx">             MatchFlags.NOT, expected
</span><span class="lines">@@ -370,12 +407,13 @@
</span><span class="cx">             )
</span><span class="cx">             queryString = queryFunction(
</span><span class="cx">                 compoundExpression,
</span><del>-                self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+                self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">             )
</span><span class="cx">             expected = u&quot;{match}&quot;.format(
</span><span class="cx">                 match=ldapQueryStringFromMatchExpression(
</span><span class="cx">                     matchExpression,
</span><del>-                    self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+                    self.fieldNameMap(service),
+                    self.recordTypeSchemas(service),
</ins><span class="cx">                 )
</span><span class="cx">             )
</span><span class="cx">             self.assertEquals(queryString, expected)
</span><span class="lines">@@ -404,17 +442,19 @@
</span><span class="cx">             )
</span><span class="cx">             queryString = queryFunction(
</span><span class="cx">                 compoundExpression,
</span><del>-                self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+                self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">             )
</span><span class="cx">             expected = u&quot;({op}{match1}{match2})&quot;.format(
</span><span class="cx">                 op=token,
</span><span class="cx">                 match1=ldapQueryStringFromMatchExpression(
</span><span class="cx">                     matchExpression1,
</span><del>-                    self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+                    self.fieldNameMap(service),
+                    self.recordTypeSchemas(service),
</ins><span class="cx">                 ),
</span><span class="cx">                 match2=ldapQueryStringFromMatchExpression(
</span><span class="cx">                     matchExpression2,
</span><del>-                    self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+                    self.fieldNameMap(service),
+                    self.recordTypeSchemas(service),
</ins><span class="cx">                 ),
</span><span class="cx">             )
</span><span class="cx">             self.assertEquals(queryString, expected)
</span><span class="lines">@@ -431,11 +471,11 @@
</span><span class="cx">         )
</span><span class="cx">         queryString = ldapQueryStringFromExpression(
</span><span class="cx">             matchExpression,
</span><del>-            self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+            self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">         )
</span><span class="cx">         expected = ldapQueryStringFromMatchExpression(
</span><span class="cx">             matchExpression,
</span><del>-            self.fieldNameMap(service), self.recordTypeMap(service),
</del><ins>+            self.fieldNameMap(service), self.recordTypeSchemas(service),
</ins><span class="cx">         )
</span><span class="cx">         self.assertEquals(queryString, expected)
</span><span class="cx"> 
</span><span class="lines">@@ -461,5 +501,6 @@
</span><span class="cx">         self.assertRaises(
</span><span class="cx">             QueryNotSupportedError,
</span><span class="cx">             ldapQueryStringFromExpression,
</span><del>-            object(), self.fieldNameMap(service), self.recordTypeMap(service)
</del><ins>+            object(),
+            self.fieldNameMap(service), self.recordTypeSchemas(service)
</ins><span class="cx">         )
</span></span></pre>
</div>
</div>

</body>
</html>