[CalendarServer-changes] [12279] twext/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:23:24 PDT 2014


Revision: 12279
          http://trac.calendarserver.org//changeset/12279
Author:   wsanchez at apple.com
Date:     2014-01-09 19:39:29 -0800 (Thu, 09 Jan 2014)
Log Message:
-----------
More LDAP

Modified Paths:
--------------
    twext/trunk/pyflakes
    twext/trunk/setup.py
    twext/trunk/twext/who/opendirectory/__init__.py
    twext/trunk/twext/who/opendirectory/_constants.py
    twext/trunk/twext/who/opendirectory/_service.py

Added Paths:
-----------
    twext/trunk/twext/who/ldap/
    twext/trunk/twext/who/ldap/__init__.py
    twext/trunk/twext/who/ldap/_constants.py
    twext/trunk/twext/who/ldap/_service.py
    twext/trunk/twext/who/ldap/_util.py
    twext/trunk/twext/who/ldap/test/
    twext/trunk/twext/who/ldap/test/__init__.py
    twext/trunk/twext/who/ldap/test/test_service.py
    twext/trunk/twext/who/ldap/test/test_util.py

Removed Paths:
-------------
    twext/trunk/twext/who/ldap.py

Modified: twext/trunk/pyflakes
===================================================================
--- twext/trunk/pyflakes	2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/pyflakes	2014-01-10 03:39:29 UTC (rev 12279)
@@ -23,5 +23,5 @@
 
 . "${wd}/develop";
 
-pip install pyflakes --target="${dev_libdir}" > "${dev_root}/pip_pyflakes.log";
+pip install pyflakes --no-use-wheel --upgrade --target="${dev_libdir}" > "${dev_root}/pip_pyflakes.log" || true;
 exec "${python}" -m pyflakes "$@";

Modified: twext/trunk/setup.py
===================================================================
--- twext/trunk/setup.py	2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/setup.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -116,8 +116,9 @@
 ]
 
 install_requirements = [
+    "twisted>=13.2.0",
     "sqlparse==0.1.2",
-    "twisted>=13.2.0",
+    "python-ldap>=2.4.13",
 ]
 
 

Added: twext/trunk/twext/who/ldap/__init__.py
===================================================================
--- twext/trunk/twext/who/ldap/__init__.py	                        (rev 0)
+++ twext/trunk/twext/who/ldap/__init__.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -0,0 +1,34 @@
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+    "LDAPError",
+    "LDAPConnectionError",
+    # "LDAPQueryError",
+    # "LDAPDataError",
+    "DirectoryService",
+]
+
+
+try:
+    from ._service import (
+        LDAPError, LDAPConnectionError,  # LDAPQueryError, LDAPDataError,
+        DirectoryService,
+    )
+except ImportError:
+    raise
+    LDAPError = LDAPConnectionError = None
+    # LDAPQueryError = LDAPDataError = None

Added: twext/trunk/twext/who/ldap/_constants.py
===================================================================
--- twext/trunk/twext/who/ldap/_constants.py	                        (rev 0)
+++ twext/trunk/twext/who/ldap/_constants.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -0,0 +1,99 @@
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+LDAP constants.
+"""
+
+from twisted.python.constants import (
+    Names, NamedConstant,  # Values, ValueConstant
+)
+
+from ..expression import MatchType
+
+
+
+class LDAPMatchType(Names):
+    """
+    Constants for native LDAP match types.
+
+    @cvar any: Attribute has any value.
+    @cvar equals: Attribute equals value.
+    @cvar startsWith: Attribute starts with value.
+    @cvar endsWith: Attribute ends with value.
+    @cvar contains: Attribute contains value.
+    @cvar lessThan: Attribute is less than value.
+    @cvar greaterThan: Attribute is greater than value.
+    @cvar lessThanOrEqualTo: Attribute is less than or equal to value.
+    @cvar greaterThanOrEqualTo: Attribute is greater than or equal to value.
+
+    """
+
+    any = NamedConstant()
+    any.queryString = u"({notOp}{attribute}=*)"
+
+    equals = NamedConstant()
+    equals.matchType = MatchType.equals
+    equals.queryString = u"({notOp}{attribute}={value})"
+
+    startsWith = NamedConstant()
+    startsWith.matchType = MatchType.startsWith
+    startsWith.queryString = u"({notOp}{attribute}={value}*)"
+
+    endsWith = NamedConstant()
+    endsWith.matchType = MatchType.endsWith
+    endsWith.queryString = u"({notOp}{attribute}=*{value})"
+
+    contains = NamedConstant()
+    contains.matchType = MatchType.contains
+    contains.queryString = u"({notOp}{attribute}=*{value}*)"
+
+    lessThan = NamedConstant()
+    lessThan.matchType = MatchType.lessThan
+    lessThan.queryString = u"({notOp}{attribute}<{value})"
+
+    greaterThan = NamedConstant()
+    greaterThan.matchType = MatchType.greaterThan
+    greaterThan.queryString = u"({notOp}{attribute}>{value})"
+
+    lessThanOrEqualTo = NamedConstant()
+    lessThanOrEqualTo.matchType = MatchType.lessThanOrEqualTo
+    lessThanOrEqualTo.queryString = u"({notOp}{attribute}<={value})"
+
+    greaterThanOrEqualTo = NamedConstant()
+    greaterThanOrEqualTo.matchType = MatchType.greaterThanOrEqualTo
+    greaterThanOrEqualTo.queryString = u"({notOp}{attribute}>={value})"
+
+
+    @classmethod
+    def fromMatchType(cls, matchType):
+        """
+        Look up an L{LDAPMatchType} from a L{MatchType}.
+
+        @param matchType: A match type.
+        @type matchType: L{MatchType}
+
+        @return: The cooresponding LDAP match type.
+        @rtype: L{LDAPMatchType}
+        """
+        if not hasattr(cls, "_matchTypeByMatchType"):
+            cls._matchTypeByMatchType = dict((
+                (matchType.matchType, matchType)
+                for matchType in cls.iterconstants()
+                if hasattr(matchType, "matchType")
+            ))
+
+        return cls._matchTypeByMatchType.get(matchType, None)

Copied: twext/trunk/twext/who/ldap/_service.py (from rev 12278, twext/trunk/twext/who/ldap.py)
===================================================================
--- twext/trunk/twext/who/ldap/_service.py	                        (rev 0)
+++ twext/trunk/twext/who/ldap/_service.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -0,0 +1,201 @@
+# -*- test-case-name: twext.who.ldap.test.test_service -*-
+##
+# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from __future__ import print_function
+
+"""
+LDAP directory service implementation.
+"""
+
+import ldap
+
+# from zope.interface import implementer
+
+# from twisted.internet.defer import succeed, fail
+from twisted.cred.credentials import IUsernamePassword
+# from twisted.web.guard import DigestCredentialFactory
+
+from twext.python.log import Logger
+
+from ..idirectory import (
+    DirectoryServiceError, DirectoryAvailabilityError,
+    # InvalidDirectoryRecordError, QueryNotSupportedError,
+    # FieldName as BaseFieldName,
+    RecordType as BaseRecordType,
+    # IPlaintextPasswordVerifier, IHTTPDigestVerifier,
+)
+from ..directory import (
+    DirectoryService as BaseDirectoryService,
+    DirectoryRecord as BaseDirectoryRecord,
+)
+# from ..expression import (
+#     CompoundExpression, Operand,
+#     MatchExpression, MatchFlags,
+# )
+from ..util import (
+    # iterFlags,
+    ConstantsContainer,
+)
+# from ._constants import LDAP_QUOTING_TABLE
+
+
+
+#
+# Exceptions
+#
+
+class LDAPError(DirectoryServiceError):
+    """
+    LDAP error.
+    """
+
+    def __init__(self, message, ldapError=None):
+        super(LDAPError, self).__init__(message)
+        self.ldapError = ldapError
+
+
+
+class LDAPConnectionError(DirectoryAvailabilityError):
+    """
+    LDAP connection error.
+    """
+
+    def __init__(self, message, ldapError=None):
+        super(LDAPConnectionError, self).__init__(message)
+        self.ldapError = ldapError
+
+
+
+# class LDAPQueryError(LDAPError):
+#     """
+#     LDAP query error.
+#     """
+
+
+
+# class LDAPDataError(LDAPError):
+#     """
+#     LDAP data error.
+#     """
+
+
+
+#
+# Directory Service
+#
+
+class DirectoryService(BaseDirectoryService):
+    """
+    LDAP directory service.
+    """
+
+    log = Logger()
+
+    recordType = ConstantsContainer((
+        BaseRecordType.user, BaseRecordType.group,
+    ))
+
+
+    def __init__(
+        self,
+        url="ldap://localhost/",
+        credentials=None,
+        tlsCACertificateFile=None,
+        tlsCACertificateDirectory=None,
+        useTLS=False,
+    ):
+        self._url = url
+        self.credentials = credentials
+        self._tlsCACertificateFile = tlsCACertificateFile
+        self._tlsCACertificateDirectory = tlsCACertificateDirectory
+        self._useTLS = useTLS,
+
+
+    @property
+    def realmName(self):
+        return u"{self.url}".format(self=self)
+
+
+    @property
+    def connection(self):
+        """
+        Get the underlying LDAP connection.
+        """
+        self._connect()
+        return self._connection
+
+
+    def _connect(self):
+        """
+        Connect to the directory server.
+
+        @raises: L{LDAPConnectionError} if unable to connect.
+        """
+        if not hasattr(self, "_connection"):
+            self.log.info("Connecting to LDAP at {source.url}")
+            connection = ldap.initialize(self._url)
+            # FIXME: Use trace_file option to wire up debug logging when
+            # Twisted adopts the new logging stuff.
+
+            def valueFor(constant):
+                if constant is None:
+                    return None
+                else:
+                    return constant.value
+
+            for option, value in (
+                (ldap.OPT_X_TLS_CACERTFILE, self._tlsCACertificateFile),
+                (ldap.OPT_X_TLS_CACERTDIR, self._tlsCACertificateDirectory),
+            ):
+                if value is not None:
+                    connection.set_option(option, value)
+
+            if self._useTLS:
+                connection.start_tls_s()
+
+            if self.credentials is not None:
+                if IUsernamePassword.providedBy(self.credentials):
+                    try:
+                        self.ldap.simple_bind_s(
+                            self.credentials.username,
+                            self.credentials.password,
+                        )
+                        self.log.info(
+                            "Bound to LDAP as {credentials.username}",
+                            credentials=self.credentials
+                        )
+                    except ldap.INVALID_CREDENTIALS as e:
+                        self.log.info(
+                            "Unable to bind to LDAP as {credentials.username}",
+                            credentials=self.credentials
+                        )
+                        raise LDAPConnectionError(str(e), e)
+
+                else:
+                    raise LDAPConnectionError(
+                        "Unknown credentials type: {0}"
+                        .format(self.credentials)
+                    )
+
+            self._connection = connection
+
+
+
+class DirectoryRecord(BaseDirectoryRecord):
+    """
+    LDAP directory record.
+    """

Added: twext/trunk/twext/who/ldap/_util.py
===================================================================
--- twext/trunk/twext/who/ldap/_util.py	                        (rev 0)
+++ twext/trunk/twext/who/ldap/_util.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -0,0 +1,165 @@
+# -*- test-case-name: twext.who.ldap.test.test_util -*-
+##
+# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from ..idirectory import QueryNotSupportedError
+from ..expression import (
+    CompoundExpression, Operand,
+    MatchExpression, MatchFlags,
+)
+from ..util import iterFlags
+from ._constants import LDAPMatchType
+
+
+
+def ldapQueryStringFromMatchExpression(expression, attrMap):
+    """
+    Generates an LDAP query string from a match expression.
+
+    @param expression: A match expression.
+    @type expression: L{MatchExpression}
+
+    @param attrMap: A mapping from L{FieldName}s to native LDAP attribute
+        names.
+    @type attrMap: L{dict}
+
+    @return: An LDAP query string.
+    @rtype: C{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{attrMap}).
+    """
+    matchType = LDAPMatchType.fromMatchType(expression.matchType)
+    if matchType is None:
+        raise QueryNotSupportedError(
+            "Unknown match type: {0}".format(matchType)
+        )
+
+    flags = tuple(iterFlags(expression.flags))
+
+    if MatchFlags.NOT in flags:
+        notOp = u"!"
+    else:
+        notOp = u""
+
+    # FIXME: It doesn't look like LDAP queries can be case sensitive.
+    # This would mean that it's up to the callers to filter out the false
+    # positives...
+    #
+    # if MatchFlags.caseInsensitive not in flags:
+    #     raise NotImplementedError("Need to handle case sensitive")
+
+    try:
+        attribute = attrMap[expression.fieldName]
+    except KeyError:
+        raise QueryNotSupportedError(
+            "Unknown field name: {0}".format(expression.fieldName)
+        )
+
+    value = unicode(expression.fieldValue)       # We want unicode
+    value = value.translate(LDAP_QUOTING_TABLE)  # Escape special chars
+
+    return matchType.queryString.format(
+        notOp=notOp, attribute=attribute, value=value
+    )
+
+
+def ldapQueryStringFromCompoundExpression(expression, attrMap):
+    """
+    Generates an LDAP query string from a compound expression.
+
+    @param expression: A compound expression.
+    @type expression: L{MatchExpression}
+
+    @param attrMap: A mapping from L{FieldName}s to native LDAP attribute
+        names.
+    @type attrMap: L{dict}
+
+    @return: An LDAP query string.
+    @rtype: C{unicode}
+
+    @raises QueryNotSupportedError: If any sub-expression cannot be converted
+        to an LDAP query.
+    """
+    queryTokens = []
+
+    if len(expression.expressions) > 1:
+        queryTokens.append(u"(")
+
+        if expression.operand is Operand.AND:
+            queryTokens.append(u"&")
+        else:
+            queryTokens.append(u"|")
+
+    for subExpression in expression.expressions:
+        queryTokens.append(
+            ldapQueryStringFromExpression(subExpression, attrMap)
+        )
+
+    if len(expression.expressions) > 1:
+        queryTokens.append(u")")
+
+    return u"".join(queryTokens)
+
+
+def ldapQueryStringFromExpression(expression, attrMap):
+    """
+    Converts an expression into an LDAP query string.
+
+    @param attrMap: A mapping from L{FieldName}s to native LDAP attribute
+        names.
+    @type attrMap: L{dict}
+
+    @param expression: An expression.
+    @type expression: L{MatchExpression} or L{CompoundExpression}
+
+    @return: A native OpenDirectory query string
+    @rtype: C{unicode}
+
+    @raises QueryNotSupportedError: If the expression cannot be converted to an
+        LDAP query.
+    """
+
+    if isinstance(expression, MatchExpression):
+        return ldapQueryStringFromMatchExpression(expression, attrMap)
+
+    if isinstance(expression, CompoundExpression):
+        return ldapQueryStringFromCompoundExpression(expression, attrMap)
+
+    raise QueryNotSupportedError(
+        "Unknown expression type: {0!r}".format(expression)
+    )
+
+
+LDAP_QUOTING_TABLE = {
+    ord(u"\\"): u"\\5C",
+    ord(u"/"): u"\\2F",
+
+    ord(u"("): u"\\28",
+    ord(u")"): u"\\29",
+    ord(u"*"): u"\\2A",
+
+    ord(u"<"): u"\\3C",
+    ord(u"="): u"\\3D",
+    ord(u">"): u"\\3E",
+    ord(u"~"): u"\\7E",
+
+    ord(u"&"): u"\\26",
+    ord(u"|"): u"\\7C",
+
+    ord(u"\0"): u"\\00",
+}

Added: twext/trunk/twext/who/ldap/test/__init__.py
===================================================================
--- twext/trunk/twext/who/ldap/test/__init__.py	                        (rev 0)
+++ twext/trunk/twext/who/ldap/test/__init__.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -0,0 +1,15 @@
+##
+# Copyright (c) 2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##

Added: twext/trunk/twext/who/ldap/test/test_service.py
===================================================================
--- twext/trunk/twext/who/ldap/test/test_service.py	                        (rev 0)
+++ twext/trunk/twext/who/ldap/test/test_service.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -0,0 +1,33 @@
+##
+# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+LDAP directory service tests.
+"""
+
+from twisted.trial import unittest
+
+# from ...expression import (
+#     CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
+# )
+# from ..ldap import DirectoryService
+
+
+
+class LDAPServiceTestCase(unittest.TestCase):
+    """
+    Tests for L{DirectoryService}.
+    """

Added: twext/trunk/twext/who/ldap/test/test_util.py
===================================================================
--- twext/trunk/twext/who/ldap/test/test_util.py	                        (rev 0)
+++ twext/trunk/twext/who/ldap/test/test_util.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -0,0 +1,219 @@
+##
+# Copyright (c) 2010-2014 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+OpenDirectory service tests.
+"""
+
+from twisted.trial import unittest
+
+from ...expression import (
+    CompoundExpression, Operand, MatchExpression, MatchType, MatchFlags
+)
+from .._service import DirectoryService
+from .._util import (
+    ldapQueryStringFromMatchExpression,
+)
+
+
+
+class LDAPQueryTestCase(unittest.TestCase):
+    """
+    Tests for LDAP query generation.
+    """
+
+    def attrMap(self, service):
+        """
+        Create a mapping from field names to LDAP attribute names.
+        The attribute names returned here are not real LDAP attribute 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.fieldName.iterconstants()])
+
+
+    def test_queryStringFromMatchExpression_matchTypes(self):
+        """
+        Match expressions with each match type produces the correct
+        operator=value string.
+        """
+        service = DirectoryService()
+
+        for matchType, expected in (
+            (MatchType.equals, u"=xyzzy"),
+            (MatchType.startsWith, u"=xyzzy*"),
+            (MatchType.endsWith, u"=*xyzzy"),
+            (MatchType.contains, u"=*xyzzy*"),
+            (MatchType.lessThan, u"<xyzzy"),
+            (MatchType.greaterThan, u">xyzzy"),
+            (MatchType.lessThanOrEqualTo, u"<=xyzzy"),
+            (MatchType.greaterThanOrEqualTo, u">=xyzzy"),
+        ):
+            expression = MatchExpression(
+                service.fieldName.shortNames, u"xyzzy",
+                matchType=matchType
+            )
+            queryString = ldapQueryStringFromMatchExpression(
+                expression, self.attrMap(service)
+            )
+            self.assertEquals(
+                queryString,
+                u"({attribute}{expected})".format(
+                    attribute="shortNames", expected=expected
+                )
+            )
+
+
+    def test_queryStringFromMatchExpression_match_not(self):
+        """
+        Match expression with the C{NOT} flag adds the C{!} operator.
+        """
+        service = DirectoryService()
+
+        expression = MatchExpression(
+            service.fieldName.shortNames, u"xyzzy",
+            flags=MatchFlags.NOT
+        )
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, self.attrMap(service)
+        )
+        self.assertEquals(
+            queryString,
+            u"(!{attribute}=xyzzy)".format(
+                attribute="shortNames",
+            )
+        )
+
+
+    def test_queryStringFromMatchExpression_match_caseInsensitive(self):
+        """
+        Match expression with the C{caseInsensitive} flag adds the C{??????}
+        operator.
+        """
+        service = DirectoryService()
+
+        expression = MatchExpression(
+            service.fieldName.shortNames, u"xyzzy",
+            flags=MatchFlags.caseInsensitive
+        )
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, self.attrMap(service)
+        )
+        self.assertEquals(
+            queryString,
+            u"???????({attribute}=xyzzy)".format(
+                attribute="shortNames",
+            )
+        )
+
+    test_queryStringFromMatchExpression_match_caseInsensitive.todo = (
+        "unimplemented"
+    )
+
+
+    def test_queryStringFromMatchExpression_match_quoting(self):
+        """
+        Special characters are quoted properly.
+        """
+        service = DirectoryService()
+
+        expression = MatchExpression(
+            service.fieldName.fullNames,
+            u"\\xyzzy: a/b/(c)* ~~ >=< ~~ &| \0!!"
+        )
+        queryString = ldapQueryStringFromMatchExpression(
+            expression, self.attrMap(service)
+        )
+        self.assertEquals(
+            queryString,
+            u"({attribute}={expected})".format(
+                attribute="fullNames",
+                expected=(
+                    u"\\5Cxyzzy: a\\2Fb\\2F\\28c\\29\\2A "
+                    "\\7E\\7E \\3E\\3D\\3C \\7E\\7E \\26\\7C \\00!!"
+                )
+            )
+        )
+
+
+    # def test_queryStringFromExpression(self):
+    #     service = DirectoryService()
+
+    #     # CompoundExpressions
+
+    #     expression = CompoundExpression(
+    #         [
+    #             MatchExpression(
+    #                 service.fieldName.uid, u"a",
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.guid, u"b",
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.shortNames, u"c",
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.emailAddresses, u"d",
+    #                 matchType=MatchType.startsWith
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.fullNames, u"e",
+    #                 matchType=MatchType.equals
+    #             ),
+    #         ],
+    #         Operand.AND
+    #     )
+    #     queryString = service._queryStringFromExpression(expression)
+    #     self.assertEquals(
+    #         queryString,
+    #         (
+    #             u"(&(dsAttrTypeStandard:GeneratedUID=*a*)"
+    #             u"(dsAttrTypeStandard:GeneratedUID=*b*)"
+    #             u"(dsAttrTypeStandard:RecordName=*c*)"
+    #             u"(dsAttrTypeStandard:EMailAddress=d*)"
+    #             u"(dsAttrTypeStandard:RealName=e))"
+    #         )
+    #     )
+
+    #     expression = CompoundExpression(
+    #         [
+    #             MatchExpression(
+    #                 service.fieldName.shortNames, u"a",
+    #                 matchType=MatchType.contains
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.emailAddresses, u"b",
+    #                 matchType=MatchType.startsWith
+    #             ),
+    #             MatchExpression(
+    #                 service.fieldName.fullNames, u"c",
+    #                 matchType=MatchType.equals
+    #             ),
+    #         ],
+    #         Operand.OR
+    #     )
+    #     queryString = service._queryStringFromExpression(expression)
+    #     self.assertEquals(
+    #         queryString,
+    #         (
+    #             u"(|(dsAttrTypeStandard:RecordName=*a*)"
+    #             u"(dsAttrTypeStandard:EMailAddress=b*)"
+    #             u"(dsAttrTypeStandard:RealName=c))"
+    #         )
+    #     )

Deleted: twext/trunk/twext/who/ldap.py
===================================================================
--- twext/trunk/twext/who/ldap.py	2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/ldap.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -1,198 +0,0 @@
-# -*- test-case-name: twext.who.test.test_ldap -*-
-##
-# Copyright (c) 2013-2014 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from __future__ import print_function
-
-"""
-LDAP directory service implementation.
-"""
-
-import ldap
-
-# from zope.interface import implementer
-
-from twisted.python.constants import Values, ValueConstant
-# from twisted.internet.defer import succeed, fail
-# from twisted.web.guard import DigestCredentialFactory
-
-from twext.python.log import Logger
-
-from ..idirectory import (
-    # DirectoryServiceError, DirectoryAvailabilityError,
-    # InvalidDirectoryRecordError, QueryNotSupportedError,
-    # FieldName as BaseFieldName,
-    RecordType as BaseRecordType,
-    # IPlaintextPasswordVerifier, IHTTPDigestVerifier,
-)
-from ..directory import (
-    DirectoryService as BaseDirectoryService,
-    # DirectoryRecord as BaseDirectoryRecord,
-)
-# from ..expression import (
-#     CompoundExpression, Operand,
-#     MatchExpression, MatchFlags,
-# )
-from ..util import (
-    # iterFlags,
-    ConstantsContainer,
-)
-
-
-
-LDAP_QUOTING_TABLE = {
-    ord(u"\\"): u"\\5C",
-    ord(u"/"): u"\\2F",
-
-    ord(u"("): u"\\28",
-    ord(u")"): u"\\29",
-    ord(u"*"): u"\\2A",
-
-    ord(u"<"): u"\\3C",
-    ord(u"="): u"\\3D",
-    ord(u">"): u"\\3E",
-    ord(u"~"): u"\\7E",
-
-    ord(u"&"): u"\\26",
-    ord(u"|"): u"\\7C",
-
-    ord(u"\0"): u"\\00",
-}
-
-
-
-#
-# Exceptions
-#
-
-# class LDAPError(DirectoryServiceError):
-#     """
-#     LDAP error.
-#     """
-
-#     def __init__(self, message, odError=None):
-#         super(LDAPError, self).__init__(message)
-#         self.odError = odError
-
-
-
-# class LDAPConnectionError(DirectoryAvailabilityError):
-#     """
-#     LDAP connection error.
-#     """
-
-#     def __init__(self, message, odError=None):
-#         super(LDAPConnectionError, self).__init__(message)
-#         self.odError = odError
-
-
-
-# class LDAPQueryError(LDAPError):
-#     """
-#     LDAP query error.
-#     """
-
-
-# class LDAPDataError(LDAPError):
-#     """
-#     LDAP data error.
-#     """
-
-
-
-#
-# LDAP Constants
-#
-
-class TLSRequireCertificate(Values):
-    never   = ValueConstant(ldap.OPT_X_TLS_NEVER)
-    allow   = ValueConstant(ldap.OPT_X_TLS_ALLOW)
-    attempt = ValueConstant(ldap.OPT_X_TLS_TRY)
-    demand  = ValueConstant(ldap.OPT_X_TLS_DEMAND)
-    hard    = ValueConstant(ldap.OPT_X_TLS_HARD)
-
-
-#
-# Directory Service
-#
-
-class DirectoryService(BaseDirectoryService):
-    """
-    LDAP directory service.
-    """
-    log = Logger()
-
-    recordType = ConstantsContainer((
-        BaseRecordType.user, BaseRecordType.group,
-    ))
-
-
-    def __init__(
-        self,
-        url="ldap://localhost/",
-        tlsCACertificateFile=None,
-        tlsCACertificateDirectory=None,
-        tlsRequireCertificate=None,
-        useTLS=False,
-    ):
-        self._url = url
-        self._tlsCACertificateFile = tlsCACertificateFile
-        self._tlsCACertificateDirectory = tlsCACertificateDirectory
-        self._tlsRequireCertificate = tlsRequireCertificate
-        self._useTLS = useTLS,
-
-
-    @property
-    def realmName(self):
-        return u"{self.url}".format(self=self)
-
-
-    @property
-    def connection(self):
-        """
-        Get the underlying LDAP connection.
-        """
-        self._connect()
-        return self._connection
-
-
-    def _connect(self):
-        """
-        Connect to the directory server.
-
-        @raises: L{LDAPConnectionError} if unable to connect.
-        """
-        if not hasattr(self, "_connection"):
-            connection = ldap.initialize(self._url)
-
-            def valueFor(constant):
-                if constant is None:
-                    return None
-                else:
-                    return constant.value
-
-            for option, value in (
-                (ldap.OPT_X_TLS_CACERTFILE, self._tlsCACertificateFile),
-                (ldap.OPT_X_TLS_CACERTDIR, self._tlsCACertificateDirectory),
-                (ldap.OPT_X_TLS, valueFor(self._tlsRequireCertificate)),
-            ):
-                if value is not None:
-                    connection.set_option(option, value)
-
-            if self._useTLS:
-                connection.start_tls_s()
-
-            self._connection = connection

Modified: twext/trunk/twext/who/opendirectory/__init__.py
===================================================================
--- twext/trunk/twext/who/opendirectory/__init__.py	2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/opendirectory/__init__.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -24,7 +24,6 @@
     "OpenDirectoryQueryError",
     "OpenDirectoryDataError",
     "DirectoryService",
-    "DirectoryRecord",
     "NoQOPDigestCredentialFactory",
 ]
 

Modified: twext/trunk/twext/who/opendirectory/_constants.py
===================================================================
--- twext/trunk/twext/who/opendirectory/_constants.py	2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/opendirectory/_constants.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -1,4 +1,3 @@
-# -*- test-case-name: twext.who.opendirectory.test.test_service -*-
 ##
 # Copyright (c) 2013-2014 Apple Inc. All rights reserved.
 #
@@ -388,8 +387,8 @@
 
 
 class ODMatchType(Values):
-    all = ValueConstant(0x0001)
-    all.queryString = u"({notOp}{attribute}=*)"
+    any = ValueConstant(0x0001)
+    any.queryString = u"({notOp}{attribute}=*)"
 
     equals = ValueConstant(0x2001)
     equals.matchType = MatchType.equals

Modified: twext/trunk/twext/who/opendirectory/_service.py
===================================================================
--- twext/trunk/twext/who/opendirectory/_service.py	2014-01-09 22:18:11 UTC (rev 12278)
+++ twext/trunk/twext/who/opendirectory/_service.py	2014-01-10 03:39:29 UTC (rev 12279)
@@ -43,6 +43,7 @@
     CompoundExpression, Operand,
     MatchExpression, MatchFlags,
 )
+from ..ldap._util import LDAP_QUOTING_TABLE
 from ..util import iterFlags, ConstantsContainer
 
 from ._odframework import ODSession, ODNode, ODQuery
@@ -52,27 +53,6 @@
 
 
 
-LDAP_QUOTING_TABLE = {
-    ord(u"\\"): u"\\5C",
-    ord(u"/"): u"\\2F",
-
-    ord(u"("): u"\\28",
-    ord(u")"): u"\\29",
-    ord(u"*"): u"\\2A",
-
-    ord(u"<"): u"\\3C",
-    ord(u"="): u"\\3D",
-    ord(u">"): u"\\3E",
-    ord(u"~"): u"\\7E",
-
-    ord(u"&"): u"\\26",
-    ord(u"|"): u"\\7C",
-
-    ord(u"\0"): u"\\00",
-}
-
-
-
 #
 # Exceptions
 #
@@ -298,7 +278,7 @@
         """
         Generates an LDAP query string from a compound expression.
 
-        @param expression: A match expression.
+        @param expression: A compound expression.
         @type expression: L{MatchExpression}
 
         @return: An LDAP query string.
@@ -425,7 +405,7 @@
             recordTypes = ODRecordType.fromRecordType(
                 expression.fieldValue
             ).value
-            matchType = ODMatchType.all.value
+            matchType = ODMatchType.any.value
             queryAttribute = None
             queryValue = None
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/ea093fab/attachment.html>


More information about the calendarserver-changes mailing list