[CalendarServer-changes] [12219] twext/trunk/twext/who

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:19:13 PDT 2014


Revision: 12219
          http://trac.calendarserver.org//changeset/12219
Author:   wsanchez at apple.com
Date:     2014-01-02 16:20:23 -0800 (Thu, 02 Jan 2014)
Log Message:
-----------
Go go gadget cred.

Modified Paths:
--------------
    twext/trunk/twext/who/directory.py
    twext/trunk/twext/who/idirectory.py
    twext/trunk/twext/who/opendirectory/_service.py
    twext/trunk/twext/who/xml.py

Added Paths:
-----------
    twext/trunk/twext/who/checker.py
    twext/trunk/twext/who/test/auth_resource.rpy

Added: twext/trunk/twext/who/checker.py
===================================================================
--- twext/trunk/twext/who/checker.py	                        (rev 0)
+++ twext/trunk/twext/who/checker.py	2014-01-03 00:20:23 UTC (rev 12219)
@@ -0,0 +1,209 @@
+# -*- test-case-name: twext.who.test.test_checker -*-
+##
+# Copyright (c) 2013 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.
+##
+
+"""
+L{twisted.cred}-style credential checker.
+"""
+
+from zope.interface import implementer, classImplements, Attribute
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.cred.error import UnauthorizedLogin
+from twisted.cred.checkers import ICredentialsChecker
+from twisted.cred.credentials import (
+    ICredentials, IUsernamePassword, IUsernameHashedPassword,
+    DigestedCredentials,
+)
+
+from .idirectory import (
+    IDirectoryService, RecordType,
+    IPlaintextPasswordVerifier, IHTTPDigestVerifier,
+)
+
+
+
+ at implementer(ICredentialsChecker)
+class BaseCredentialChecker(object):
+    # credentialInterfaces = (IUsernamePassword, IUsernameHashedPassword)
+
+    def __init__(self, service):
+        """
+        @param service: The directory service to use to obtain directory
+            records and validate credentials against.
+        @type service: L{IDirectoryService}
+        """
+        if not IDirectoryService.providedBy(service):
+            raise TypeError("Not an IDirectoryService: {0!r}".format(service))
+
+        self.service = service
+
+
+
+class UsernamePasswordCredentialChecker(BaseCredentialChecker):
+    credentialInterfaces = (IUsernamePassword, IUsernameHashedPassword)
+
+
+    @inlineCallbacks
+    def requestAvatarId(self, credentials):
+        if not IUsernamePassword.providedBy(credentials):
+            raise TypeError(
+                "Not an IUsernamePassword: {0!r}".format(credentials)
+            )
+
+        record = yield self.service.recordWithShortName(
+            RecordType.user, credentials.username
+        )
+
+        if record is None:
+            raise UnauthorizedLogin("No such user")
+
+        if not IPlaintextPasswordVerifier.providedBy(record):
+            raise UnauthorizedLogin(
+                "Not an IPlaintextPasswordVerifier: {0!r}".format(record)
+            )
+
+        if record.verifyPlaintextPassword(credentials.password):
+            returnValue(record)
+
+        raise UnauthorizedLogin("Incorrect password")
+
+
+
+class IHTTPDigest(ICredentials):
+    """
+    HTTP digest credentials.
+    """
+    username = Attribute("User (short) name")
+    method = Attribute("...")
+    fields = Attribute("...")  # Attributes would be better.
+
+
+
+classImplements(DigestedCredentials, (IHTTPDigest,))
+
+
+
+class HTTPDigestCredentialChecker(BaseCredentialChecker):
+    credentialInterfaces = (IHTTPDigest,)
+
+
+    @inlineCallbacks
+    def requestAvatarId(self, credentials):
+        if not IHTTPDigest.providedBy(credentials):
+            raise TypeError(
+                "Not an IHTTPDigest: {0!r}".format(credentials)
+            )
+
+        record = yield self.service.recordWithShortName(
+            RecordType.user, credentials.username
+        )
+
+        if record is None:
+            raise UnauthorizedLogin("No such user")
+
+        if not IHTTPDigestVerifier.providedBy(record):
+            raise UnauthorizedLogin(
+                "Not an IHTTPDigestVerifier: {0!r}".format(record)
+            )
+
+        if record.verifyHTTPDigest(
+            credentials.username,
+            credentials.fields.get("realm"),
+            credentials.fields.get("uri"),
+            credentials.fields.get("nonce"),
+            credentials.fields.get("cnonce"),
+            credentials.fields.get("algorithm", "md5"),
+            credentials.fields.get("nc"),
+            credentials.fields.get("qop", "auth"),
+            credentials.fields.get("response"),
+            credentials.method,
+        ):
+            returnValue(record)
+
+        raise UnauthorizedLogin("Incorrect password")
+
+
+
+
+
+# class Yuck(object):
+#     def requestAvatarId(self, credentials):
+#         odRecord = self._getUserRecord(credentials.username)
+
+#         if odRecord is None:
+#             return fail(UnauthorizedLogin("No such user"))
+
+#         if isinstance(credentials, DigestedCredentials):
+#             try:
+#                 credentials.fields.setdefault("algorithm", "md5")
+#                 challenge = (
+#                     'Digest realm="{realm}", nonce="{nonce}", '
+#                     'algorithm={algorithm}'
+#                     .format(**credentials.fields)
+#                 )
+#                 response = credentials.fields["response"]
+
+#             except KeyError as e:
+#                 self.log.error(
+#                     "Error authenticating against OpenDirectory: "
+#                     "missing digest response field {field!r} in "
+#                     "{credentials.fields!r}",
+#                     field=e.args[0], credentials=credentials
+#                 )
+#                 return fail(UnauthorizedLogin("Invalid digest challenge"))
+
+#             result, m1, m2, error = odRecord.verifyExtendedWithAuthenticationType_authenticationItems_continueItems_context_error_(
+#                 u"dsAuthMethodStandard:dsAuthNodeDIGEST-MD5",
+#                 [
+#                     credentials.username,
+#                     challenge,
+#                     response,
+#                     credentials.method,
+#                 ],
+#                 None, None, None
+#             )
+
+#             if error:
+#                 return fail(UnauthorizedLogin(error))
+
+#             if result:
+#                 return succeed(DirectoryRecord(self, odRecord))
+
+#         else:
+#             return fail(UnauthorizedLogin(
+#                 "Unknown credentials type: {0}".format(type(credentials))
+#             ))
+
+#         return fail(UnauthorizedLogin("Unknown authorization failure"))
+
+
+
+
+
+
+
+# from twisted.web.guard import DigestCredentialFactory
+
+# class CustomDigestCredentialFactory(DigestCredentialFactory):
+#     """
+#     DigestCredentialFactory without qop, to interop with OD.
+#     """
+
+#     def getChallenge(self, address):
+#         result = DigestCredentialFactory.getChallenge(self, address)
+#         del result["qop"]
+#         return result

Modified: twext/trunk/twext/who/directory.py
===================================================================
--- twext/trunk/twext/who/directory.py	2014-01-02 22:02:37 UTC (rev 12218)
+++ twext/trunk/twext/who/directory.py	2014-01-03 00:20:23 UTC (rev 12219)
@@ -24,15 +24,17 @@
     "DirectoryRecord",
 ]
 
-from zope.interface import implementer
+from zope.interface import implementer, directlyProvides
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.defer import succeed, fail
+from twisted.cred.credentials import DigestedCredentials
 
 from .idirectory import (
     QueryNotSupportedError, NotAllowedError,
     FieldName, RecordType,
     IDirectoryService, IDirectoryRecord,
+    IPlaintextPasswordVerifier, IHTTPDigestVerifier,
 )
 from .expression import CompoundExpression, Operand, MatchExpression
 from .util import uniqueResult, describe, ConstantsContainer
@@ -374,7 +376,12 @@
         self.service = service
         self.fields = normalizedFields
 
+        if self.service.fieldName.password in self.fields:
+            directlyProvides(
+                self, IPlaintextPasswordVerifier, IHTTPDigestVerifier
+            )
 
+
     def __repr__(self):
         return (
             "<{self.__class__.__name__} ({recordType}){shortName}>".format(
@@ -455,3 +462,35 @@
 
     def groups(self):
         return fail(NotImplementedError("Subclasses must implement groups()"))
+
+
+    #
+    # Verifiers for twext.who.checker stuff.
+    #
+
+    def verifyPlaintextPassword(self, password):
+        if self.password == password:
+            return True
+        else:
+            return False
+
+
+    def verifyHTTPDigest(
+        self, username, realm, uri, nonce, cnonce,
+        algorithm, nc, qop, response, method,
+    ):
+        helperCreds = DigestedCredentials(
+            username, method, realm,
+            dict(
+                realm=realm,
+                uri=uri,
+                nonce=nonce,
+                cnonce=cnonce,
+                algorithm=algorithm,
+                nc=nc,
+                qop=qop,
+                response=response
+            )
+        )
+
+        return helperCreds.checkPassword(self.password)

Modified: twext/trunk/twext/who/idirectory.py
===================================================================
--- twext/trunk/twext/who/idirectory.py	2014-01-02 22:02:37 UTC (rev 12218)
+++ twext/trunk/twext/who/idirectory.py	2014-01-03 00:20:23 UTC (rev 12219)
@@ -72,6 +72,7 @@
     """
     Unknown record type.
     """
+
     def __init__(self, token):
         DirectoryServiceError.__init__(self, token)
         self.token = token
@@ -117,6 +118,7 @@
         Represents a non-person not covered by another record type (eg. a
         projector).
     """
+
     user = NamedConstant()
     user.description  = u"user"
 
@@ -150,9 +152,11 @@
     @cvar emailAddresses: The email addresses for a directory record.
         The associated values must be L{unicodes}.
 
-    @cvar password: The clear text password for a directory record.
+    @cvar password: The clear text password (oh no!) for a directory record.
         The associated value must be a L{unicode} or C{None}.
+        The correct value is C{None}.
     """
+
     uid = NamedConstant()
     uid.description = u"UID"
 
@@ -406,6 +410,7 @@
     C{record.recordType} is equivalent to
     C{record.fields[FieldName.recordType]}.
     """
+
     service = Attribute("The L{IDirectoryService} this record exists in.")
     fields  = Attribute("A mapping with L{NamedConstant} keys.")
 
@@ -429,3 +434,39 @@
         @return: a deferred iterable of L{IDirectoryRecord}s which are
             groups that this record is a member of.
         """
+
+
+
+class IPlaintextPasswordVerifier(Interface):
+    """
+    Provides a way to verify a plaintext password as provided by a client.
+    """
+
+    def verifyPlaintextPassword(password):
+        """
+        Verifies that a given plaintext password authenticates the record.
+
+        @param password: A plaintext password.
+        @type password: L{unicode}
+
+        @return: L{True} if the password matches, L{False} otherwise.
+        @rtype: L{BOOL}
+        """
+
+
+
+class IHTTPDigestVerifier(Interface):
+    """
+    Provides a way to verify HTTP digest credentials as provided by a client.
+    """
+
+    def verifyHTTPDigest(username, realm, nonce, algorithm, response, method):
+        """
+        Verifies that a given plaintext password authenticates the record.
+
+        @param password: A plaintext password.
+        @type password: L{unicode}
+
+        @return: L{True} if the password matches, L{False} otherwise.
+        @rtype: L{BOOL}
+        """

Modified: twext/trunk/twext/who/opendirectory/_service.py
===================================================================
--- twext/trunk/twext/who/opendirectory/_service.py	2014-01-02 22:02:37 UTC (rev 12218)
+++ twext/trunk/twext/who/opendirectory/_service.py	2014-01-03 00:20:23 UTC (rev 12219)
@@ -21,16 +21,8 @@
 OpenDirectory directory service implementation.
 """
 
-from zope.interface import implements
-
 from twisted.python.constants import Names, NamedConstant
 from twisted.internet.defer import succeed, fail
-from twisted.cred.checkers import ICredentialsChecker
-from twisted.cred.credentials import (
-    IUsernamePassword, IUsernameHashedPassword, DigestedCredentials,
-)
-from twisted.cred.error import UnauthorizedLogin
-# from twisted.web.guard import DigestCredentialFactory
 
 from twext.python.log import Logger
 
@@ -115,10 +107,6 @@
     """
     OpenDirectory directory service.
     """
-
-    implements(ICredentialsChecker)
-    credentialInterfaces = (IUsernamePassword, IUsernameHashedPassword)
-
     log = Logger()
 
     recordType = ConstantsContainer((
@@ -414,9 +402,9 @@
                 "Error while executing OpenDirectory query: {error}",
                 error=error
             )
-            raise OpenDirectoryQueryError(
+            return fail(OpenDirectoryQueryError(
                 "Unable to execute OpenDirectory query", error
-            )
+            ))
 
         return succeed(DirectoryRecord(self, odr) for odr in odRecords)
 
@@ -476,93 +464,7 @@
         return record
 
 
-    def requestAvatarId(self, credentials):
-        """
-        Authenticate the credentials against OpenDirectory and return the
-        corresponding directory record.
 
-        @param: credentials: The credentials to authenticate.
-        @type: credentials: L{ICredentials}
-
-        @return: The directory record for the given credentials.
-        @rtype: deferred L{DirectoryRecord}
-
-        @raises: L{UnauthorizedLogin} if the credentials are not valid.
-        """
-
-        odRecord = self._getUserRecord(credentials.username)
-
-        if odRecord is None:
-            return fail(UnauthorizedLogin("No such user"))
-
-        if IUsernamePassword.providedBy(credentials):
-            result, error = odRecord.verifyPassword_error_(
-                credentials.password, None
-            )
-
-            if error:
-                return fail(UnauthorizedLogin(error))
-
-            if result:
-                return succeed(DirectoryRecord(self, odRecord))
-
-        elif isinstance(credentials, DigestedCredentials):
-            try:
-                credentials.fields.setdefault("algorithm", "md5")
-                challenge = (
-                    'Digest realm="{realm}", nonce="{nonce}", '
-                    'algorithm={algorithm}'
-                    .format(**credentials.fields)
-                )
-                response = credentials.fields["response"]
-
-            except KeyError as e:
-                self.log.error(
-                    "Error authenticating against OpenDirectory: "
-                    "missing digest response field {field!r} in "
-                    "{credentials.fields!r}",
-                    field=e.args[0], credentials=credentials
-                )
-                return fail(UnauthorizedLogin("Invalid digest challenge"))
-
-            result, m1, m2, error = odRecord.verifyExtendedWithAuthenticationType_authenticationItems_continueItems_context_error_(
-                u"dsAuthMethodStandard:dsAuthNodeDIGEST-MD5",
-                [
-                    credentials.username,
-                    challenge,
-                    response,
-                    credentials.method,
-                ],
-                None, None, None
-            )
-
-            if error:
-                return fail(UnauthorizedLogin(error))
-
-            if result:
-                return succeed(DirectoryRecord(self, odRecord))
-
-        else:
-            return fail(UnauthorizedLogin(
-                "Unknown credentials type: {0}".format(type(credentials))
-            ))
-
-        return fail(UnauthorizedLogin("Unknown authorization failure"))
-
-
-
-# class CustomDigestCredentialFactory(DigestCredentialFactory):
-#     """
-#     DigestCredentialFactory without qop, to interop with OD.
-#     """
-
-#     def getChallenge(self, address):
-#         result = DigestCredentialFactory.getChallenge(self, address)
-#         del result["qop"]
-#         return result
-
-
-
 class DirectoryRecord(BaseDirectoryRecord):
     """
     OpenDirectory directory record.

Added: twext/trunk/twext/who/test/auth_resource.rpy
===================================================================
--- twext/trunk/twext/who/test/auth_resource.rpy	                        (rev 0)
+++ twext/trunk/twext/who/test/auth_resource.rpy	2014-01-03 00:20:23 UTC (rev 12219)
@@ -0,0 +1,61 @@
+##
+# Copyright (c) 2013 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.
+##
+
+cache()
+
+from twisted.cred.portal import Portal
+from twisted.web.resource import IResource
+from twisted.web.guard import (
+    HTTPAuthSessionWrapper,
+    # BasicCredentialFactory,
+    DigestCredentialFactory,
+)
+from twisted.web.static import Data
+
+from twext.who.test.test_xml import xmlService as DirectoryService
+# from twext.who.checker import UsernamePasswordCredentialChecker
+from twext.who.checker import HTTPDigestCredentialChecker
+
+
+
+class Realm(object):
+    def requestAvatar(self, avatarId, mind, *interfaces):
+        resource = Data(
+            "Hello, {0!r}!".format(avatarId),
+            "text/plain"
+        )
+
+        return IResource, resource, lambda: None
+
+
+
+directory = DirectoryService("/tmp/auth.xml")
+
+checkers = [
+    HTTPDigestCredentialChecker(directory),
+    # UsernamePasswordCredentialChecker(directory),
+]
+
+realm = Realm()
+
+portal = Portal(realm, checkers)
+
+factories = [
+    DigestCredentialFactory("md5", "Digest Realm"),
+    # BasicCredentialFactory("Basic Realm"),
+]
+
+resource = HTTPAuthSessionWrapper(portal, factories)

Modified: twext/trunk/twext/who/xml.py
===================================================================
--- twext/trunk/twext/who/xml.py	2014-01-02 22:02:37 UTC (rev 12218)
+++ twext/trunk/twext/who/xml.py	2014-01-03 00:20:23 UTC (rev 12219)
@@ -15,6 +15,7 @@
 # limitations under the License.
 ##
 
+from __future__ import print_function
 from __future__ import absolute_import
 
 """
@@ -35,6 +36,8 @@
     tostring as etreeToString, Element as XMLElement,
 )
 
+from zope.interface import implementer
+
 from twisted.python.constants import Values, ValueConstant
 from twisted.internet.defer import fail
 
@@ -42,10 +45,12 @@
     DirectoryServiceError,
     NoSuchRecordError, UnknownRecordTypeError,
     RecordType, FieldName as BaseFieldName,
+    IPlaintextPasswordVerifier, IHTTPDigestVerifier,
 )
 from .index import (
     DirectoryService as BaseDirectoryService,
-    DirectoryRecord, FieldName as IndexFieldName,
+    DirectoryRecord,
+    FieldName as IndexFieldName,
 )
 from .util import ConstantsContainer
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/9fc7d893/attachment.html>


More information about the calendarserver-changes mailing list