[CalendarServer-changes] [3470] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Fri Dec 5 19:56:15 PST 2008
Revision: 3470
http://trac.macosforge.org/projects/calendarserver/changeset/3470
Author: cdaboo at apple.com
Date: 2008-12-05 19:56:15 -0800 (Fri, 05 Dec 2008)
Log Message:
-----------
Merged digest creds cache branch to trunk.
Modified Paths:
--------------
CalendarServer/trunk/calendarserver/tap/caldav.py
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
CalendarServer/trunk/twistedcaldav/authkerb.py
CalendarServer/trunk/twistedcaldav/directory/digest.py
CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py
CalendarServer/trunk/twistedcaldav/directory/wiki.py
CalendarServer/trunk/twistedcaldav/root.py
CalendarServer/trunk/twistedcaldav/test/test_kerberos.py
CalendarServer/trunk/twistedcaldav/test/test_root.py
Added Paths:
-----------
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch
Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -557,7 +557,6 @@
schemeConfig["Algorithm"],
schemeConfig["Qop"],
realm,
- config.DataRoot,
)
elif scheme == "basic":
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch (from rev 3469, CalendarServer/branches/users/cdaboo/digestdb-fix-3445/lib-patches/Twisted/twisted.web2.auth.basic.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.basic.patch 2008-12-06 03:56:15 UTC (rev 3470)
@@ -0,0 +1,29 @@
+Index: twisted/web2/auth/basic.py
+===================================================================
+--- twisted/web2/auth/basic.py (revision 19773)
++++ twisted/web2/auth/basic.py (working copy)
+@@ -1,6 +1,7 @@
+ # -*- test-case-name: twisted.web2.test.test_httpauth -*-
+
+ from twisted.cred import credentials, error
++from twisted.internet.defer import succeed
+ from twisted.web2.auth.interfaces import ICredentialFactory
+
+ from zope.interface import implements
+@@ -18,7 +19,7 @@
+ self.realm = realm
+
+ def getChallenge(self, peer):
+- return {'realm': self.realm}
++ return succeed({'realm': self.realm})
+
+ def decode(self, response, request):
+ try:
+@@ -28,6 +29,6 @@
+
+ creds = creds.split(':', 1)
+ if len(creds) == 2:
+- return credentials.UsernamePassword(*creds)
++ return succeed(credentials.UsernamePassword(*creds))
+ else:
+ raise error.LoginFailed('Invalid credentials')
Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.digest.patch 2008-12-06 03:56:15 UTC (rev 3470)
@@ -2,7 +2,11 @@
===================================================================
--- twisted/web2/auth/digest.py (revision 19773)
+++ twisted/web2/auth/digest.py (working copy)
-@@ -11,16 +11,24 @@
+@@ -8,19 +8,28 @@
+ import time
+
+ from twisted.cred import credentials, error
++from twisted.internet.defer import succeed
from zope.interface import implements, Interface
from twisted.web2.auth.interfaces import ICredentialFactory
@@ -31,7 +35,7 @@
}
# DigestCalcHA1
-@@ -228,7 +236,7 @@
+@@ -228,7 +237,7 @@
# Now, what we do is encode the nonce, client ip and a timestamp
# in the opaque value with a suitable digest
key = "%s,%s,%s" % (nonce, clientip, str(int(self._getTime())))
@@ -40,7 +44,7 @@
ekey = key.encode('base64')
return "%s-%s" % (digest, ekey.strip('\n'))
-@@ -274,7 +282,7 @@
+@@ -274,7 +283,7 @@
'Invalid response, incompatible opaque/nonce too old')
# Verify the digest
@@ -49,7 +53,25 @@
if digest != opaqueParts[0]:
raise error.LoginFailed('Invalid response, invalid opaque value')
-@@ -315,18 +323,18 @@
+@@ -293,11 +302,12 @@
+ c = self.generateNonce()
+ o = self.generateOpaque(c, peer.host)
+
+- return {'nonce': c,
+- 'opaque': o,
+- 'qop': 'auth',
+- 'algorithm': self.algorithm,
+- 'realm': self.realm}
++ return succeed({'nonce': c,
++ 'opaque': o,
++ 'qop': 'auth',
++ 'algorithm': self.algorithm,
++ 'realm': self.realm,
++ })
+
+ def decode(self, response, request):
+ """
+@@ -315,18 +325,18 @@
@raise: L{error.LoginFailed} if the response does not contain a
username, a nonce, an opaque, or if the opaque is invalid.
"""
@@ -79,3 +101,13 @@
username = auth.get('username')
if not username:
raise error.LoginFailed('Invalid response, no username given.')
+@@ -342,7 +352,7 @@
+ auth.get('nonce'),
+ request.remoteAddr.host):
+
+- return DigestedCredentials(username,
++ return succeed(DigestedCredentials(username,
+ request.method,
+ self.realm,
+- auth)
++ auth))
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch (from rev 3469, CalendarServer/branches/users/cdaboo/digestdb-fix-3445/lib-patches/Twisted/twisted.web2.auth.interfaces.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.interfaces.patch 2008-12-06 03:56:15 UTC (rev 3470)
@@ -0,0 +1,22 @@
+Index: twisted/web2/auth/interfaces.py
+===================================================================
+--- twisted/web2/auth/interfaces.py (revision 19773)
++++ twisted/web2/auth/interfaces.py (working copy)
+@@ -18,7 +18,7 @@
+ @param peer: The client's address
+
+ @rtype: C{dict}
+- @return: dictionary of challenge arguments
++ @return: deferred returning dictionary of challenge arguments
+ """
+
+ def decode(response, request):
+@@ -32,7 +32,7 @@
+ @type request: L{twisted.web2.server.Request}
+ @param request: the request being processed
+
+- @return: ICredentials
++ @return: deferred returning ICredentials
+ """
+
+
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch (from rev 3469, CalendarServer/branches/users/cdaboo/digestdb-fix-3445/lib-patches/Twisted/twisted.web2.auth.wrapper.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.auth.wrapper.patch 2008-12-06 03:56:15 UTC (rev 3470)
@@ -0,0 +1,205 @@
+Index: twisted/web2/auth/wrapper.py
+===================================================================
+--- twisted/web2/auth/wrapper.py (revision 19773)
++++ twisted/web2/auth/wrapper.py (working copy)
+@@ -5,32 +5,45 @@
+ """
+ from zope.interface import implements, directlyProvides
+ from twisted.cred import error, credentials
+-from twisted.python import failure
+ from twisted.web2 import responsecode
+ from twisted.web2 import http
+ from twisted.web2 import iweb
+ from twisted.web2.auth.interfaces import IAuthenticatedRequest
++from twisted.internet.defer import inlineCallbacks, returnValue
+
+ class UnauthorizedResponse(http.StatusResponse):
+ """A specialized response class for generating www-authenticate headers
+ from the given L{CredentialFactory} instances
+ """
+
+- def __init__(self, factories, remoteAddr=None):
++ @staticmethod
++ def makeResponse(factories, remoteAddr=None):
++
++ response = UnauthorizedResponse()
++ d = response.generateHeaders(factories, remoteAddr)
++ d.addCallback(lambda _:response)
++ return d
++
++ def __init__(self):
++
++ super(UnauthorizedResponse, self).__init__(
++ responsecode.UNAUTHORIZED,
++ "You are not authorized to access this resource."
++ )
++
++ @inlineCallbacks
++ def generateHeaders(self, factories, remoteAddr=None):
+ """
+ @param factories: A L{dict} of {'scheme': ICredentialFactory}
+
+ @param remoteAddr: An L{IAddress} for the connecting client.
+ """
+
+- super(UnauthorizedResponse, self).__init__(
+- responsecode.UNAUTHORIZED,
+- "You are not authorized to access this resource.")
+-
+ authHeaders = []
+ for factory in factories.itervalues():
+- authHeaders.append((factory.scheme,
+- factory.getChallenge(remoteAddr)))
++ scheme = factory.scheme
++ challenge = (yield factory.getChallenge(remoteAddr))
++ authHeaders.append((scheme, challenge,))
+
+ self.headers.setHeader('www-authenticate', authHeaders)
+
+@@ -71,8 +84,6 @@
+
+ def _loginSucceeded(self, avatar, request):
+ """
+- Callback for successful login.
+-
+ @param avatar: A tuple of the form (interface, avatar) as
+ returned by your realm.
+
+@@ -85,6 +96,7 @@
+
+ directlyProvides(request, IAuthenticatedRequest)
+
++ @inlineCallbacks
+ def _addAuthenticateHeaders(request, response):
+ """
+ A response filter that adds www-authenticate headers
+@@ -93,14 +105,16 @@
+ """
+ if response.code == responsecode.UNAUTHORIZED:
+ if not response.headers.hasHeader('www-authenticate'):
+- newResp = UnauthorizedResponse(self.credentialFactories,
+- request.remoteAddr)
++ newResp = (yield UnauthorizedResponse.makeResponse(
++ self.credentialFactories,
++ request.remoteAddr
++ ))
+
+ response.headers.setHeader(
+ 'www-authenticate',
+ newResp.headers.getHeader('www-authenticate'))
+
+- return response
++ returnValue(response)
+
+ _addAuthenticateHeaders.handleErrors = True
+
+@@ -108,27 +122,22 @@
+
+ return self.wrappedResource
+
+- def _loginFailed(self, result, request):
++ @inlineCallbacks
++ def _loginFailed(self, request):
+ """
+- Errback for failed login.
+-
+- @param result: L{Failure} returned by portal.login
+-
+ @param request: L{IRequest} that encapsulates this auth
+ attempt.
+
+- @return: A L{Failure} containing an L{HTTPError} containing the
+- L{UnauthorizedResponse} if C{result} is an L{UnauthorizedLogin}
+- or L{UnhandledCredentials} error
++ @raise: always rais HTTPError
+ """
+- result.trap(error.UnauthorizedLogin, error.UnhandledCredentials)
+
+- return failure.Failure(
+- http.HTTPError(
+- UnauthorizedResponse(
+- self.credentialFactories,
+- request.remoteAddr)))
++ response = (yield UnauthorizedResponse.makeResponse(
++ self.credentialFactories,
++ request.remoteAddr
++ ))
++ raise http.HTTPError(response)
+
++ @inlineCallbacks
+ def login(self, factory, response, request):
+ """
+ @param factory: An L{ICredentialFactory} that understands the given
+@@ -142,50 +151,48 @@
+ or a failure containing an L{UnauthorizedResponse}
+ """
+ try:
+- creds = factory.decode(response, request)
++ creds = (yield factory.decode(response, request))
+ except error.LoginFailed:
+- raise http.HTTPError(UnauthorizedResponse(
+- self.credentialFactories,
+- request.remoteAddr))
++ yield self._loginFailed(request)
+
++ try:
++ avatar = (yield self.portal.login(creds, None, *self.interfaces))
++ except (error.UnauthorizedLogin, error.UnhandledCredentials):
++ yield self._loginFailed(request)
++ resource = self._loginSucceeded(avatar, request)
++ returnValue(resource)
+
+- return self.portal.login(creds, None, *self.interfaces
+- ).addCallbacks(self._loginSucceeded,
+- self._loginFailed,
+- (request,), None,
+- (request,), None)
+-
++ @inlineCallbacks
+ def authenticate(self, request):
+ """
+- Attempt to authenticate the givin request
++ Attempt to authenticate the giving request
+
+ @param request: An L{IRequest} to be authenticated.
+ """
+ authHeader = request.headers.getHeader('authorization')
+
+ if authHeader is None:
+- return self.portal.login(credentials.Anonymous(),
+- None,
+- *self.interfaces
+- ).addCallbacks(self._loginSucceeded,
+- self._loginFailed,
+- (request,), None,
+- (request,), None)
++ try:
++ avatar = (yield self.portal.login(credentials.Anonymous(), None, *self.interfaces))
++ except:
++ yield self._loginFailed(request)
++ resource = self._loginSucceeded(avatar, request)
++ returnValue(resource)
+
+ elif authHeader[0] not in self.credentialFactories:
+- raise http.HTTPError(UnauthorizedResponse(
+- self.credentialFactories,
+- request.remoteAddr))
++ yield self._loginFailed(request)
+ else:
+- return self.login(self.credentialFactories[authHeader[0]],
+- authHeader[1], request)
++ result = (yield self.login(self.credentialFactories[authHeader[0]], authHeader[1], request))
++ returnValue(result)
+
+ def locateChild(self, request, seg):
+ """
+ Authenticate the request then return the C{self.wrappedResource}
+ and the unmodified segments.
+ """
+- return self.authenticate(request), seg
++ d = self.authenticate(request)
++ d.addCallback(lambda result:(result, seg,))
++ return d
+
+ def renderHTTP(self, request):
+ """
Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2008-12-06 03:56:15 UTC (rev 3470)
@@ -2,7 +2,7 @@
===================================================================
--- twisted/web2/dav/resource.py (revision 19773)
+++ twisted/web2/dav/resource.py (working copy)
-@@ -31,19 +31,28 @@
+@@ -31,20 +31,30 @@
"DAVResource",
"DAVLeafResource",
"DAVPrincipalResource",
@@ -27,12 +27,15 @@
from zope.interface import implements
from twisted.python import log
+-from twisted.internet.defer import Deferred, maybeDeferred, succeed
+from twisted.python.failure import Failure
+from twisted.cred.error import LoginFailed, UnauthorizedLogin
- from twisted.internet.defer import Deferred, maybeDeferred, succeed
++from twisted.internet.defer import Deferred, maybeDeferred, succeed,\
++ inlineCallbacks, returnValue
from twisted.internet.defer import waitForDeferred, deferredGenerator
from twisted.internet import reactor
-@@ -52,12 +61,13 @@
+ from twisted.web2 import responsecode
+@@ -52,12 +62,13 @@
from twisted.web2.http_headers import generateContentType
from twisted.web2.iweb import IResponse
from twisted.web2.resource import LeafResource
@@ -47,7 +50,7 @@
from twisted.web2.dav.http import NeedPrivilegesResponse
from twisted.web2.dav.noneprops import NonePropertyStore
from twisted.web2.dav.util import unimplemented, parentForURL, joinURL
-@@ -126,10 +136,13 @@
+@@ -126,10 +137,13 @@
#(dav_namespace, "group" ), # RFC 3744, section 5.2
(dav_namespace, "supported-privilege-set" ), # RFC 3744, section 5.3
(dav_namespace, "current-user-privilege-set"), # RFC 3744, section 5.4
@@ -61,7 +64,7 @@
(twisted_dav_namespace, "resource-class"),
)
-@@ -166,6 +179,14 @@
+@@ -166,6 +180,14 @@
if qname[0] == twisted_private_namespace:
return succeed(False)
@@ -76,7 +79,7 @@
return succeed(qname in self.liveProperties or self.deadProperties().contains(qname))
def readProperty(self, property, request):
-@@ -201,7 +222,6 @@
+@@ -201,7 +223,6 @@
mimeType = self.contentType()
if mimeType is None:
return None
@@ -84,7 +87,7 @@
return davxml.GETContentType(generateContentType(mimeType))
if name == "getcontentlength":
-@@ -239,8 +259,10 @@
+@@ -239,8 +260,10 @@
)
if name == "supported-report-set":
@@ -97,7 +100,7 @@
if name == "supported-privilege-set":
return self.supportedPrivileges(request)
-@@ -252,9 +274,10 @@
+@@ -252,9 +275,10 @@
return davxml.InheritedACLSet(*self.inheritedACLSet())
if name == "principal-collection-set":
@@ -111,7 +114,7 @@
def ifAllowed(privileges, callback):
def onError(failure):
-@@ -286,7 +309,36 @@
+@@ -286,7 +310,36 @@
d.addCallback(gotACL)
return d
return ifAllowed((davxml.ReadACL(),), callback)
@@ -148,7 +151,7 @@
elif namespace == twisted_dav_namespace:
if name == "resource-class":
class ResourceClass (davxml.WebDAVTextElement):
-@@ -309,10 +361,7 @@
+@@ -309,10 +362,7 @@
"""
See L{IDAVResource.writeProperty}.
"""
@@ -160,7 +163,7 @@
def defer():
if property.protected:
-@@ -363,15 +412,28 @@
+@@ -363,15 +413,28 @@
"""
See L{IDAVResource.listProperties}.
"""
@@ -193,7 +196,7 @@
def listAllprop(self, request):
"""
Some DAV properties should not be returned to a C{DAV:allprop} query.
-@@ -465,8 +527,22 @@
+@@ -465,8 +528,22 @@
return super(DAVPropertyMixIn, self).displayName()
class DAVResource (DAVPropertyMixIn, StaticRenderMixin):
@@ -216,7 +219,7 @@
##
# DAV
##
-@@ -553,12 +629,13 @@
+@@ -553,69 +630,65 @@
def supportedReports(self):
"""
See L{IDAVResource.supportedReports}.
@@ -231,54 +234,79 @@
return result
##
-@@ -570,19 +647,21 @@
+ # Authentication
+ ##
+
++ @inlineCallbacks
+ def authorize(self, request, privileges, recurse=False):
+ """
See L{IDAVResource.authorize}.
"""
- def onError(failure):
+- def onError(failure):
- log.err("Invalid authentication details: %s" % (request,))
- raise HTTPError(UnauthorizedResponse(
-+ failure.trap(UnauthorizedLogin, LoginFailed)
+
-+ log.msg("Authentication failed: %s" % (failure.value,))
-+ return Failure(HTTPError(UnauthorizedResponse(
++ try:
++ yield self.authenticate(request)
++ except (UnauthorizedLogin, LoginFailed), e:
++ log.msg("Authentication failed: %s" % (e,))
++ response = (yield UnauthorizedResponse.makeResponse(
request.credentialFactories,
request.remoteAddr
-- ))
-+ )))
+ ))
++ raise HTTPError(response)
- def onAuth(result):
- def onErrors(failure):
- failure.trap(AccessDeniedError)
-
+- def onAuth(result):
+- def onErrors(failure):
+- failure.trap(AccessDeniedError)
+-
- # If we were unauthorized to start with (no Authorization header from client) then
-+ # If we were unauthenticated to start with (no Authorization header from client) then
- # we should return an unauthorized response instead to force the client to login if it can
+- # we should return an unauthorized response instead to force the client to login if it can
- if request.user == davxml.Principal(davxml.Unauthenticated()):
-+ if request.authnUser == davxml.Principal(davxml.Unauthenticated()):
- response = UnauthorizedResponse(request.credentialFactories,
- request.remoteAddr)
- else:
-@@ -593,7 +672,7 @@
- # class is supposed to be a FORBIDDEN status code and
- # "Authorization will not help" according to RFC2616
- #
+- response = UnauthorizedResponse(request.credentialFactories,
+- request.remoteAddr)
+- else:
+- response = NeedPrivilegesResponse(request.uri,
+- failure.value.errors)
+- #
+- # We're not adding the headers here because this response
+- # class is supposed to be a FORBIDDEN status code and
+- # "Authorization will not help" according to RFC2616
+- #
- raise HTTPError(response)
-+ return Failure(HTTPError(response))
++ try:
++ yield self.checkPrivileges(request, privileges, recurse)
++ except AccessDeniedError, e:
++ # If we were unauthenticated to start with (no Authorization header from client) then
++ # we should return an unauthorized response instead to force the client to login if it can
++ if request.authnUser == davxml.Principal(davxml.Unauthenticated()):
++ response = (yield UnauthorizedResponse.makeResponse(
++ request.credentialFactories,
++ request.remoteAddr
++ ))
++ else:
++ response = NeedPrivilegesResponse(request.uri, e.errors)
++ #
++ # We're not adding the headers here because this response
++ # class is supposed to be a FORBIDDEN status code and
++ # "Authorization will not help" according to RFC2616
++ #
++ raise HTTPError(response)
- d = self.checkPrivileges(request, privileges, recurse)
- d.addErrback(onErrors)
-@@ -606,16 +685,21 @@
-
+- d = self.checkPrivileges(request, privileges, recurse)
+- d.addErrback(onErrors)
+- return d
+-
+- d = maybeDeferred(self.authenticate, request)
+- d.addCallbacks(onAuth, onError)
+-
+- return d
+-
++ @inlineCallbacks
def authenticate(self, request):
- def loginSuccess(result):
+- def loginSuccess(result):
- request.user = result[1]
- return request.user
-+ """
-+ @param result: returned tuple from auth.DAVRealm.requestAvatar.
-+ """
-+ request.authnUser = result[1]
-+ request.authzUser = result[2]
-+ return (request.authnUser, request.authzUser,)
if not (
hasattr(request, 'portal') and
@@ -289,45 +317,51 @@
- return request.user
+ request.authnUser = davxml.Principal(davxml.Unauthenticated())
+ request.authzUser = davxml.Principal(davxml.Unauthenticated())
-+ return (request.authnUser, request.authzUser,)
++ returnValue((request.authnUser, request.authzUser,))
authHeader = request.headers.getHeader('authorization')
-@@ -631,9 +715,10 @@
+@@ -627,27 +700,23 @@
+ else:
+ factory = request.credentialFactories[authHeader[0]]
+- creds = factory.decode(authHeader[1], request)
++ creds = (yield factory.decode(authHeader[1], request))
+
# Try to match principals in each principal collection on the resource
- def gotDetails(details):
+- def gotDetails(details):
- principal = IDAVPrincipalResource(details[0])
- principalURI = details[1]
- return PrincipalCredentials(principal, principalURI, creds)
-+ authnPrincipal, authzPrincipal = details
-+ authnPrincipal = IDAVPrincipalResource(authnPrincipal)
-+ authzPrincipal = IDAVPrincipalResource(authzPrincipal)
-+ return PrincipalCredentials(authnPrincipal, authzPrincipal, creds)
++ authnPrincipal, authzPrincipal = (yield self.principalsForAuthID(request, creds.username))
++ authnPrincipal = IDAVPrincipalResource(authnPrincipal)
++ authzPrincipal = IDAVPrincipalResource(authzPrincipal)
- def login(pcreds):
- d = request.portal.login(pcreds, None, *request.loginInterfaces)
-@@ -641,13 +726,15 @@
+- def login(pcreds):
+- d = request.portal.login(pcreds, None, *request.loginInterfaces)
+- d.addCallback(loginSuccess)
++ pcreds = PrincipalCredentials(authnPrincipal, authzPrincipal, creds)
- return d
-
+- return d
+-
- d = self.findPrincipalForAuthID(request, creds.username)
- d.addCallback(gotDetails).addCallback(login)
-+ d = self.principalsForAuthID(request, creds.username)
-+ d.addCallback(gotDetails)
-+ d.addCallback(login)
-
- return d
+-
+- return d
++ result = (yield request.portal.login(pcreds, None, *request.loginInterfaces))
++ request.authnUser = result[1]
++ request.authzUser = result[2]
++ returnValue((request.authnUser, request.authzUser,))
else:
- request.user = davxml.Principal(davxml.Unauthenticated())
- return request.user
+ request.authnUser = davxml.Principal(davxml.Unauthenticated())
+ request.authzUser = davxml.Principal(davxml.Unauthenticated())
-+ return (request.authnUser, request.authzUser,)
++ returnValue((request.authnUser, request.authzUser,))
##
# ACL
-@@ -656,49 +743,23 @@
+@@ -656,49 +725,23 @@
def currentPrincipal(self, request):
"""
@param request: the request being processed.
@@ -386,7 +420,7 @@
"""
@return: the L{davxml.ACL} element containing the default access control
list for this resource.
-@@ -710,6 +771,17 @@
+@@ -710,6 +753,17 @@
#
return readonlyACL
@@ -404,7 +438,7 @@
def setAccessControlList(self, acl):
"""
See L{IDAVResource.setAccessControlList}.
-@@ -748,13 +820,16 @@
+@@ -748,13 +802,16 @@
# 10. Verify that new acl is not in conflict with itself
# 11. Update acl on the resource
@@ -422,7 +456,7 @@
# Need to get list of supported privileges
supported = []
-@@ -773,10 +848,7 @@
+@@ -773,10 +830,7 @@
yield supportedPrivs
supportedPrivs = supportedPrivs.getResult()
for item in supportedPrivs.children:
@@ -434,7 +468,7 @@
addSupportedPrivilege(item)
# Steps 1 - 6
-@@ -910,8 +982,7 @@
+@@ -910,8 +964,7 @@
supportedPrivs = supportedPrivs.getResult()
# Other principals types don't make sense as actors.
@@ -444,7 +478,7 @@
"Principal is not an actor: %r" % (principal,)
)
-@@ -1019,15 +1090,16 @@
+@@ -1019,15 +1072,16 @@
def getMyURL():
url = request.urlForResource(self)
@@ -464,7 +498,7 @@
"Expected %s response from readDeadProperty() exception, not %s"
% (responsecode.NOT_FOUND, e.response.code)
)
-@@ -1038,9 +1110,9 @@
+@@ -1038,9 +1092,9 @@
if myURL == "/":
# If we get to the root without any ACLs, then use the default.
@@ -476,7 +510,7 @@
# Dynamically update privileges for those ace's that are inherited.
if inheritance:
-@@ -1076,7 +1148,7 @@
+@@ -1076,7 +1130,7 @@
# Adjust ACE for inherit on this resource
children = list(ace.children)
children.remove(TwistedACLInheritable())
@@ -485,7 +519,7 @@
aces.append(davxml.ACE(*children))
else:
aces.extend(inherited_aces)
-@@ -1105,8 +1177,7 @@
+@@ -1105,8 +1159,7 @@
the child resource loop and supply those to the checkPrivileges on each child.
@param request: the L{IRequest} for the request in progress.
@@ -495,7 +529,7 @@
"""
# Get the parent ACLs with inheritance and preserve the <inheritable> element.
-@@ -1128,21 +1199,9 @@
+@@ -1128,21 +1181,9 @@
# Adjust ACE for inherit on this resource
children = list(ace.children)
children.remove(TwistedACLInheritable())
@@ -519,7 +553,7 @@
inheritedACEsforChildren = deferredGenerator(inheritedACEsforChildren)
-@@ -1152,49 +1211,69 @@
+@@ -1152,49 +1193,69 @@
This implementation returns an empty set.
"""
@@ -617,7 +651,7 @@
def samePrincipal(self, principal1, principal2):
"""
Check whether the two prinicpals are exactly the same in terms of
-@@ -1219,7 +1298,6 @@
+@@ -1219,7 +1280,6 @@
return False
def matchPrincipal(self, principal1, principal2, request):
@@ -625,7 +659,7 @@
"""
Check whether the principal1 is a principal in the set defined by
principal2.
-@@ -1244,6 +1322,9 @@
+@@ -1244,6 +1304,9 @@
if isinstance(principal1, davxml.Unauthenticated):
yield False
return
@@ -635,7 +669,7 @@
else:
yield True
return
-@@ -1260,10 +1341,7 @@
+@@ -1260,10 +1323,7 @@
yield False
return
@@ -647,7 +681,7 @@
principal2 = waitForDeferred(self.resolvePrincipal(principal2, request))
yield principal2
-@@ -1271,7 +1349,6 @@
+@@ -1271,7 +1331,6 @@
assert principal2 is not None, "principal2 is None"
@@ -655,7 +689,7 @@
# Compare two HRefs and do group membership test as well
if principal1 == principal2:
yield True
-@@ -1289,6 +1366,7 @@
+@@ -1289,6 +1348,7 @@
matchPrincipal = deferredGenerator(matchPrincipal)
@@ -663,7 +697,7 @@
def principalIsGroupMember(self, principal1, principal2, request):
"""
Check whether one principal is a group member of another.
-@@ -1299,18 +1377,21 @@
+@@ -1299,18 +1359,21 @@
@return: L{Deferred} with result C{True} if principal1 is a member of principal2, C{False} otherwise
"""
@@ -696,7 +730,7 @@
def validPrincipal(self, ace_principal, request):
"""
-@@ -1351,11 +1432,16 @@
+@@ -1351,11 +1414,16 @@
@return C{True} if C{href_principal} is valid, C{False} otherwise.
This implementation tests for a href element that corresponds to
@@ -716,7 +750,7 @@
return d
def resolvePrincipal(self, principal, request):
-@@ -1404,8 +1490,7 @@
+@@ -1404,8 +1472,7 @@
try:
principal = principal.getResult()
except HTTPError, e:
@@ -726,7 +760,7 @@
"Expected %s response from readProperty() exception, not %s"
% (responsecode.NOT_FOUND, e.response.code)
)
-@@ -1432,15 +1517,15 @@
+@@ -1432,15 +1499,15 @@
log.err("DAV:self ACE is set on non-principal resource %r" % (self,))
yield None
return
@@ -745,7 +779,7 @@
"Not a meta-principal: %r" % (principal,)
)
-@@ -1517,6 +1602,270 @@
+@@ -1517,6 +1584,270 @@
return None
##
@@ -1016,7 +1050,7 @@
# HTTP
##
-@@ -1525,15 +1874,11 @@
+@@ -1525,15 +1856,11 @@
#litmus = request.headers.getRawHeaders("x-litmus")
#if litmus: log.msg("*** Litmus test: %s ***" % (litmus,))
@@ -1034,7 +1068,7 @@
def setHeaders(response):
response = IResponse(response)
-@@ -1567,7 +1912,7 @@
+@@ -1567,7 +1894,7 @@
def findChildren(self, depth, request, callback, privileges=None, inherited_aces=None):
return succeed(None)
@@ -1043,7 +1077,7 @@
"""
Resource representing a WebDAV principal. (RFC 3744, section 2)
"""
-@@ -1577,7 +1922,7 @@
+@@ -1577,7 +1904,7 @@
# WebDAV
##
@@ -1052,7 +1086,7 @@
(dav_namespace, "alternate-URI-set"),
(dav_namespace, "principal-URL" ),
(dav_namespace, "group-member-set" ),
-@@ -1585,14 +1930,11 @@
+@@ -1585,14 +1912,11 @@
)
def davComplianceClasses(self):
@@ -1068,7 +1102,7 @@
def readProperty(self, property, request):
def defer():
if type(property) is tuple:
-@@ -1610,10 +1952,20 @@
+@@ -1610,10 +1934,20 @@
return davxml.PrincipalURL(davxml.HRef(self.principalURL()))
if name == "group-member-set":
@@ -1091,7 +1125,7 @@
if name == "resourcetype":
if self.isCollection():
-@@ -1655,7 +2007,7 @@
+@@ -1655,7 +1989,7 @@
principals. Subclasses should override this method to provide member
URLs for this resource if appropriate.
"""
@@ -1100,7 +1134,7 @@
def groupMemberships(self):
"""
-@@ -1666,6 +2018,7 @@
+@@ -1666,6 +2000,7 @@
"""
unimplemented(self)
@@ -1108,7 +1142,7 @@
def principalMatch(self, href):
"""
Check whether the supplied principal matches this principal or is a
-@@ -1675,10 +2028,33 @@
+@@ -1675,10 +2010,33 @@
"""
uri = str(href)
if self.principalURL() == uri:
@@ -1144,7 +1178,7 @@
class AccessDeniedError(Exception):
def __init__(self, errors):
"""
-@@ -1718,6 +2094,37 @@
+@@ -1718,6 +2076,37 @@
davxml.registerElement(TwistedACLInheritable)
davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
Copied: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch (from rev 3469, CalendarServer/branches/users/cdaboo/digestdb-fix-3445/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.test.test_httpauth.patch 2008-12-06 03:56:15 UTC (rev 3470)
@@ -0,0 +1,243 @@
+Index: twisted/web2/test/test_httpauth.py
+===================================================================
+--- twisted/web2/test/test_httpauth.py (revision 19773)
++++ twisted/web2/test/test_httpauth.py (working copy)
+@@ -1,3 +1,4 @@
++from twisted.internet.defer import inlineCallbacks
+ import md5
+ from twisted.internet import address
+ from twisted.trial import unittest
+@@ -41,22 +42,25 @@
+ self.username = 'dreid'
+ self.password = 'S3CuR1Ty'
+
++ @inlineCallbacks
+ def testUsernamePassword(self):
+ response = base64.encodestring('%s:%s' % (
+ self.username,
+ self.password))
+
+- creds = self.credentialFactory.decode(response, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(response, _trivial_GET))
+ self.failUnless(creds.checkPassword(self.password))
+
++ @inlineCallbacks
+ def testIncorrectPassword(self):
+ response = base64.encodestring('%s:%s' % (
+ self.username,
+ 'incorrectPassword'))
+
+- creds = self.credentialFactory.decode(response, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(response, _trivial_GET))
+ self.failIf(creds.checkPassword(self.password))
+
++ @inlineCallbacks
+ def testIncorrectPadding(self):
+ response = base64.encodestring('%s:%s' % (
+ self.username,
+@@ -64,7 +68,7 @@
+
+ response = response.strip('=')
+
+- creds = self.credentialFactory.decode(response, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(response, _trivial_GET))
+ self.failUnless(creds.checkPassword(self.password))
+
+ def testInvalidCredentials(self):
+@@ -135,6 +139,7 @@
+ )
+ return expected
+
++ @inlineCallbacks
+ def test_getChallenge(self):
+ """
+ Test that all the required fields exist in the challenge,
+@@ -142,42 +147,44 @@
+ DigestCredentialFactory
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+ self.assertEquals(challenge['qop'], 'auth')
+ self.assertEquals(challenge['realm'], 'test realm')
+ self.assertEquals(challenge['algorithm'], 'md5')
+ self.assertTrue(challenge.has_key("nonce"))
+ self.assertTrue(challenge.has_key("opaque"))
+
++ @inlineCallbacks
+ def test_response(self):
+ """
+ Test that we can decode a valid response to our challenge
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+ self.failUnless(creds.checkPassword('password'))
+
++ @inlineCallbacks
+ def test_multiResponse(self):
+ """
+ Test that multiple responses to to a single challenge are handled
+ successfully.
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+ self.failUnless(creds.checkPassword('password'))
+
+ clientResponse = authRequest2 % (
+@@ -185,24 +192,25 @@
+ self.getDigestResponse(challenge, "00000002"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+ self.failUnless(creds.checkPassword('password'))
+
++ @inlineCallbacks
+ def test_failsWithDifferentMethod(self):
+ """
+ Test that the response fails if made for a different request method
+ than it is being issued for.
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse,
+- SimpleRequest(None, 'POST', '/'))
++ creds = (yield self.credentialFactory.decode(clientResponse,
++ SimpleRequest(None, 'POST', '/')))
+ self.failIf(creds.checkPassword('password'))
+
+ def test_noUsername(self):
+@@ -247,20 +255,21 @@
+ _trivial_GET)
+ self.assertEquals(str(e), "Invalid response, no opaque given.")
+
++ @inlineCallbacks
+ def test_checkHash(self):
+ """
+ Check that given a hash of the form 'username:realm:password'
+ we can verify the digest challenge
+ """
+
+- challenge = self.credentialFactory.getChallenge(clientAddress)
++ challenge = (yield self.credentialFactory.getChallenge(clientAddress))
+
+ clientResponse = authRequest1 % (
+ challenge['nonce'],
+ self.getDigestResponse(challenge, "00000001"),
+ challenge['opaque'])
+
+- creds = self.credentialFactory.decode(clientResponse, _trivial_GET)
++ creds = (yield self.credentialFactory.decode(clientResponse, _trivial_GET))
+
+ self.failUnless(creds.checkHash(
+ md5.md5('username:test realm:password').hexdigest()))
+@@ -268,6 +277,7 @@
+ self.failIf(creds.checkHash(
+ md5.md5('username:test realm:bogus').hexdigest()))
+
++ @inlineCallbacks
+ def test_invalidOpaque(self):
+ """
+ Test that login fails when the opaque does not contain all the required
+@@ -276,7 +286,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ self.assertRaises(
+ error.LoginFailed,
+@@ -302,6 +312,7 @@
+ challenge['nonce'],
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_incompatibleNonce(self):
+ """
+ Test that login fails when the given nonce from the response, does not
+@@ -310,7 +321,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ badNonceOpaque = credentialFactory.generateOpaque(
+ '1234567890',
+@@ -330,6 +341,7 @@
+ '',
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_incompatibleClientIp(self):
+ """
+ Test that the login fails when the request comes from a client ip
+@@ -338,7 +350,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ badNonceOpaque = credentialFactory.generateOpaque(
+ challenge['nonce'],
+@@ -351,6 +363,7 @@
+ challenge['nonce'],
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_oldNonce(self):
+ """
+ Test that the login fails when the given opaque is older than
+@@ -359,7 +372,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+ key = '%s,%s,%s' % (challenge['nonce'],
+ clientAddress.host,
+@@ -376,6 +389,7 @@
+ challenge['nonce'],
+ clientAddress.host)
+
++ @inlineCallbacks
+ def test_mismatchedOpaqueChecksum(self):
+ """
+ Test that login fails when the opaque checksum fails verification
+@@ -383,7 +397,7 @@
+
+ credentialFactory = FakeDigestCredentialFactory('md5', 'test realm')
+
+- challenge = credentialFactory.getChallenge(clientAddress)
++ challenge = (yield credentialFactory.getChallenge(clientAddress))
+
+
+ key = '%s,%s,%s' % (challenge['nonce'],
Modified: CalendarServer/trunk/twistedcaldav/authkerb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/authkerb.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/twistedcaldav/authkerb.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -139,7 +139,7 @@
super(BasicKerberosCredentialFactory, self).__init__(principal, type, hostname)
def getChallenge(self, _ignore_peer):
- return {'realm': self.realm}
+ return succeed({'realm': self.realm})
def decode(self, response, request): #@UnusedVariable
try:
@@ -150,7 +150,7 @@
creds = creds.split(':', 1)
if len(creds) == 2:
c = BasicKerberosCredentials(creds[0], creds[1], self.service, self.realm)
- return c
+ return succeed(c)
raise error.LoginFailed('Invalid credentials')
class BasicKerberosCredentialsChecker(LoggingMixIn):
@@ -209,7 +209,7 @@
super(NegotiateCredentialFactory, self).__init__(principal, type, hostname)
def getChallenge(self, _ignore_peer):
- return {}
+ return succeed({})
def decode(self, base64data, request):
@@ -270,7 +270,7 @@
request.addResponseFilter(responseFilterAddWWWAuthenticate)
- return NegotiateCredentials(username)
+ return succeed(NegotiateCredentials(username))
class NegotiateCredentialsChecker(object):
Modified: CalendarServer/trunk/twistedcaldav/directory/digest.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/digest.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/twistedcaldav/directory/digest.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -14,29 +14,22 @@
# limitations under the License.
##
-from twistedcaldav.sql import AbstractSQLDatabase
-
from twisted.cred import error
+from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.web2.auth.digest import DigestCredentialFactory
from twisted.web2.auth.digest import DigestedCredentials
-
-from zope.interface import implements, Interface
-
-import cPickle as pickle
-from twisted.web2.http_headers import tokenize
from twisted.web2.http_headers import Token
-from twisted.web2.http_headers import split
from twisted.web2.http_headers import parseKeyValue
-import os
-import time
+from twisted.web2.http_headers import split
+from twisted.web2.http_headers import tokenize
-try:
- from sqlite3 import OperationalError
-except ImportError:
- from pysqlite2.dbapi2 import OperationalError
-
from twistedcaldav.log import Logger
+from twistedcaldav.memcacher import Memcacher
+from zope.interface import implements, Interface
+
+import time
+
log = Logger()
"""
@@ -94,297 +87,40 @@
"""
pass
- def deleteMany(self, keys):
- """
- Remove the records associated with the supplied keys.
+class DigestCredentialsMemcache(Memcacher):
- @param key: the key to remove.
- @type key: C{str}
- """
- pass
-
- def keys(self):
- """
- Return all the keys currently available.
-
- @return: a C{list} of C{str} for each key currently in the database.
- """
- pass
-
- def items(self):
- """
- Return all the key/value pairs currently available.
-
- @return: a C{list} of C{tuple} for each key/value currently in the database.
- """
- pass
-
-class DigestCredentialsMap(object):
-
implements(IDigestCredentialsDatabase)
- def __init__(self, *args):
- self.db = {}
+ def __init__(self, namespace):
+ super(DigestCredentialsMemcache, self).__init__(
+ namespace=namespace,
+ pickle=True,
+ )
def has_key(self, key):
"""
See IDigestCredentialsDatabase.
"""
- return self.db.has_key(key)
+ d = self.get(key)
+ d.addCallback(lambda value:value is not None)
+ return d
def set(self, key, value):
"""
See IDigestCredentialsDatabase.
"""
- self.db[key] = value
-
- def get(self, key):
- """
- See IDigestCredentialsDatabase.
- """
- if self.db.has_key(key):
- return self.db[key]
- else:
- return None
-
- def delete(self, key):
- """
- See IDigestCredentialsDatabase.
- """
- if self.db.has_key(key):
- del self.db[key]
-
- def deleteMany(self, keys):
- """
- See IDigestCredentialsDatabase.
- """
- for key in keys:
- if self.db.has_key(key):
- del self.db[key]
-
- def keys(self):
- """
- See IDigestCredentialsDatabase.
- """
- return self.db.keys()
-
- def items(self):
- """
- See IDigestCredentialsDatabase.
- """
- return self.db.items()
-
-class DigestCredentialsDB(AbstractSQLDatabase):
-
- implements(IDigestCredentialsDatabase)
-
- """
- A database to maintain cached digest credentials.
-
- SCHEMA:
-
- Database: DIGESTCREDENTIALS
-
- ROW: KEY, VALUE
-
- """
-
- dbType = "DIGESTCREDENTIALSCACHE"
- dbFilename = "digest.sqlite"
- dbFormatVersion = "2"
-
- exceptionLimit = 10
-
- def __init__(self, path):
- db_path = os.path.join(path, DigestCredentialsDB.dbFilename)
- super(DigestCredentialsDB, self).__init__(db_path, False, autocommit=False)
- self.exceptions = 0
-
- def has_key(self, key):
- """
- See IDigestCredentialsDatabase.
- """
- try:
- for ignore_key in self._db_execute(
- "select KEY from DIGESTCREDENTIALS where KEY = :1",
- key
- ):
- return True
- else:
- return False
- self.exceptions = 0
- except OperationalError, e:
- self.exceptions += 1
- if self.exceptions >= self.exceptionLimit:
- self._db_close()
- log.err("Reset digest credentials database connection: %s" % (e,))
- raise
-
- def set(self, key, value):
- """
- See IDigestCredentialsDatabase.
- """
- try:
- pvalue = pickle.dumps(value)
- self._set_in_db(key, pvalue)
- self._db_commit()
- self.exceptions = 0
- except OperationalError, e:
- self._db_rollback()
- self.exceptions += 1
- if self.exceptions >= self.exceptionLimit:
- self._db_close()
- log.err("Reset digest credentials database connection: %s" % (e,))
- raise
-
- def get(self, key):
- """
- See IDigestCredentialsDatabase.
- """
- try:
- for pvalue in self._db_execute(
- "select VALUE from DIGESTCREDENTIALS where KEY = :1",
- key
- ):
- self.exceptions = 0
- return pickle.loads(str(pvalue[0]))
- else:
- self.exceptions = 0
- return None
- except OperationalError, e:
- self.exceptions += 1
- if self.exceptions >= self.exceptionLimit:
- self._db_close()
- log.err("Reset digest credentials database connection: %s" % (e,))
- raise
-
- def delete(self, key):
- """
- See IDigestCredentialsDatabase.
- """
- try:
- self._delete_from_db(key)
- self._db_commit()
- self.exceptions = 0
- except OperationalError, e:
- self._db_rollback()
- self.exceptions += 1
- if self.exceptions >= self.exceptionLimit:
- self._db_close()
- log.err("Reset digest credentials database connection: %s" % (e,))
- raise
-
- def deleteMany(self, keys):
- """
- See IDigestCredentialsDatabase.
- """
- try:
- for key in keys:
- self._delete_from_db(key)
- self._db_commit()
- self.exceptions = 0
- except OperationalError, e:
- self._db_rollback()
- self.exceptions += 1
- if self.exceptions >= self.exceptionLimit:
- self._db_close()
- log.err("Reset digest credentials database connection: %s" % (e,))
- raise
-
- def keys(self):
- """
- See IDigestCredentialsDatabase.
- """
- try:
- result = []
- for key in self._db_execute("select KEY from DIGESTCREDENTIALS"):
- result.append(str(key[0]))
-
- self.exceptions = 0
- return result
- except OperationalError, e:
- self.exceptions += 1
- if self.exceptions >= self.exceptionLimit:
- self._db_close()
- log.err("Reset digest credentials database connection: %s" % (e,))
- raise
-
- def items(self):
- """
- See IDigestCredentialsDatabase.
- """
- try:
- result = []
- for key in self._db_execute("select KEY, VALUE from DIGESTCREDENTIALS"):
- result.append((str(key[0]), pickle.loads(str(key[1])),))
-
- self.exceptions = 0
- return result
- except OperationalError, e:
- self.exceptions += 1
- if self.exceptions >= self.exceptionLimit:
- self._db_close()
- log.err("Reset digest credentials database connection: %s" % (e,))
- raise
-
- def _set_in_db(self, key, value):
- """
- Insert the specified entry into the database, replacing any that might already exist.
-
- @param key: the key to add.
- @param value: the value to add.
- """
- self._db().execute(
- """
- insert or replace into DIGESTCREDENTIALS (KEY, VALUE)
- values (:1, :2)
- """, (key, value,)
+ super(DigestCredentialsMemcache, self).set(
+ key,
+ value,
+ expire_time=DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
)
-
- def _delete_from_db(self, key):
- """
- Deletes the specified entry from the database.
- @param key: the key to remove.
- """
- self._db().execute("delete from DIGESTCREDENTIALS where KEY = :1", (key,))
-
- def _db_version(self):
- """
- @return: the schema version assigned to this index.
- """
- return DigestCredentialsDB.dbFormatVersion
-
- def _db_type(self):
- """
- @return: the collection type assigned to this index.
- """
- return DigestCredentialsDB.dbType
-
- def _db_init_data_tables(self, q):
- """
- Initialise the underlying database tables.
- @param q: a database cursor to use.
- """
-
- #
- # DIGESTCREDENTIALS table
- #
- q.execute(
- """
- create table DIGESTCREDENTIALS (
- KEY text unique,
- VALUE text
- )
- """
- )
-
class QopDigestCredentialFactory(DigestCredentialFactory):
"""
See twisted.web2.auth.digest.DigestCredentialFactory
"""
- def __init__(self, algorithm, qop, realm, db_path):
+ def __init__(self, algorithm, qop, realm, namespace="DIGESTCREDENTIALS"):
"""
@type algorithm: C{str}
@param algorithm: case insensitive string that specifies
@@ -399,17 +135,12 @@
@type realm: C{str}
@param realm: case sensitive string that specifies the realm
portion of the challenge
-
- @type db_path: C{str}
- @param db_path: path where the credentials cache is to be stored
"""
super(QopDigestCredentialFactory, self).__init__(algorithm, realm)
self.qop = qop
- self.db = DigestCredentialsDB(db_path)
-
- # Always clean-up when we start-up
- self.cleanup()
+ self.db = DigestCredentialsMemcache(namespace)
+ @inlineCallbacks
def getChallenge(self, peer):
"""
Generate the challenge for use in the WWW-Authenticate header
@@ -425,16 +156,19 @@
c = self.generateNonce()
# Make sure it is not a duplicate
- if self.db.has_key(c):
+ result = (yield self.db.has_key(c))
+ if result:
raise AssertionError("nonce value already cached in credentials database: %s" % (c,))
# The database record is a tuple of (client ip, nonce-count, timestamp)
- self.db.set(c, (peer.host, 0, time.time()))
+ yield self.db.set(c, (peer.host, 0, time.time()))
- challenge = {'nonce': c,
- 'qop': 'auth',
- 'algorithm': self.algorithm,
- 'realm': self.realm}
+ challenge = {
+ 'nonce': c,
+ 'qop': 'auth',
+ 'algorithm': self.algorithm,
+ 'realm': self.realm,
+ }
if self.qop:
challenge['qop'] = self.qop
@@ -445,9 +179,9 @@
if hasattr(peer, 'stale') and peer.stale:
challenge['stale'] = 'true'
- return challenge
-
+ returnValue(challenge)
+ @inlineCallbacks
def decode(self, response, request):
"""
Do the default behavior but then strip out any 'qop' from the credential fields
@@ -490,19 +224,20 @@
raise error.LoginFailed('Invalid response, no nonce given.')
# Now verify the nonce/cnonce values for this client
- if self.validate(auth, request):
-
+ result = (yield self._validate(auth, request))
+ if result:
credentials = DigestedCredentials(username,
request.method,
self.realm,
auth)
if not self.qop and credentials.fields.has_key('qop'):
del credentials.fields['qop']
- return credentials
+ returnValue(credentials)
else:
raise error.LoginFailed('Invalid nonce/cnonce values')
- def validate(self, auth, request):
+ @inlineCallbacks
+ def _validate(self, auth, request):
"""
Check that the parameters in the response represent a valid set of credentials that
may be being re-used.
@@ -521,73 +256,56 @@
nonce_count = auth.get('nc')
# First check we have this nonce
- if not self.db.has_key(nonce):
+ result = (yield self.db.get(nonce))
+ if result is None:
raise error.LoginFailed('Invalid nonce value: %s' % (nonce,))
- db_clientip, db_nonce_count, db_timestamp = self.db.get(nonce)
+ db_clientip, db_nonce_count, db_timestamp = result
# Next check client ip
if db_clientip != clientip:
- self.invalidate(nonce)
+ yield self._invalidate(nonce)
raise error.LoginFailed('Client IPs do not match: %s and %s' % (clientip, db_clientip,))
# cnonce and nonce-count MUST be present if qop is present
if auth.get('qop') is not None:
if auth.get('cnonce') is None:
- self.invalidate(nonce)
+ yield self._invalidate(nonce)
raise error.LoginFailed('cnonce is required when qop is specified')
if nonce_count is None:
- self.invalidate(nonce)
+ yield self._invalidate(nonce)
raise error.LoginFailed('nonce-count is required when qop is specified')
# Next check the nonce-count is one greater than the previous one and update it in the DB
try:
nonce_count = int(nonce_count, 16)
except ValueError:
- self.invalidate(nonce)
+ yield self._invalidate(nonce)
raise error.LoginFailed('nonce-count is not a valid hex string: %s' % (auth.get('nonce-count'),))
if nonce_count != db_nonce_count + 1:
- self.invalidate(nonce)
+ yield self._invalidate(nonce)
raise error.LoginFailed('nonce-count value out of sequence: %s should be one more than %s' % (nonce_count, db_nonce_count,))
- self.db.set(nonce, (db_clientip, nonce_count, db_timestamp))
+ yield self.db.set(nonce, (db_clientip, nonce_count, db_timestamp))
else:
# When not using qop the stored nonce-count must always be zero.
# i.e. we can't allow a qop auth then a non-qop auth with the same nonce
if db_nonce_count != 0:
- self.invalidate(nonce)
+ yield self._invalidate(nonce)
raise error.LoginFailed('nonce-count was sent with this nonce: %s' % (nonce,))
# Now check timestamp
if db_timestamp + DigestCredentialFactory.CHALLENGE_LIFETIME_SECS <= time.time():
- self.invalidate(nonce)
+ yield self._invalidate(nonce)
if request.remoteAddr:
request.remoteAddr.stale = True
raise error.LoginFailed('Digest credentials expired')
- return True
+ returnValue(True)
- def invalidate(self, nonce):
+ def _invalidate(self, nonce):
"""
Invalidate cached credentials for the specified nonce value.
- @param nonce: the nonce for the record to invalidate.
+ @param nonce: the nonce for the record to _invalidate.
@type nonce: C{str}
"""
- self.db.delete(nonce)
-
- def cleanup(self):
- """
- This should be called at regular intervals to remove expired credentials from the cache.
- """
- items = self.db.items()
- oldest_allowed = time.time() - DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
- delete_keys = []
- for key, value in items:
- ignore_clientip, ignore_cnonce, db_timestamp = value
- if db_timestamp <= oldest_allowed:
- delete_keys.append(key)
-
- try:
- self.db.deleteMany(delete_keys)
- except Exception, e:
- # Clean-up errors can be logged but we should ignore them
- log.err("Failed to clean digest credentials: %s" % (e,))
+ return self.db.delete(nonce)
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -1,15 +1,33 @@
+##
+# Copyright (c) 2005-2007 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 twisted.cred import error
from twisted.internet import address
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.python import failure
from twisted.web2.auth import digest
from twisted.web2.auth.wrapper import UnauthorizedResponse
from twisted.web2.test.test_server import SimpleRequest
-from twisted.web2.dav.fileop import rmdir
+
from twistedcaldav.directory.digest import QopDigestCredentialFactory
from twistedcaldav.test.util import TestCase
-import os
+from twistedcaldav.config import config
+
import md5
+import sys
class FakeDigestCredentialFactory(QopDigestCredentialFactory):
"""
@@ -81,28 +99,24 @@
Create a DigestCredentialFactory for testing
"""
TestCase.setUp(self)
- self.path1 = self.mktemp()
- self.path2 = self.mktemp()
- os.mkdir(self.path1)
- os.mkdir(self.path2)
+ config.ProcessType = "Single"
+ self.namespace1 = "DIGEST1"
+ self.namespace2 = "DIGEST2"
+
self.credentialFactories = (QopDigestCredentialFactory(
'md5',
'auth',
'test realm',
- self.path1
+ self.namespace1
),
QopDigestCredentialFactory(
'md5',
'',
'test realm',
- self.path2
+ self.namespace2
))
- def tearDown(self):
- rmdir(self.path1)
- rmdir(self.path2)
-
def getDigestResponse(self, challenge, ncount):
"""
Calculate the response for the given challenge
@@ -163,6 +177,22 @@
)
return expected
+ @inlineCallbacks
+ def assertRaisesDeferred(self, exception, f, *args, **kwargs):
+ try:
+ result = (yield f(*args, **kwargs))
+ except exception, inst:
+ returnValue(inst)
+ except:
+ raise self.failureException('%s raised instead of %s:\n %s'
+ % (sys.exc_info()[0],
+ exception.__name__,
+ failure.Failure().getTraceback()))
+ else:
+ raise self.failureException('%s not raised (%r returned)'
+ % (exception.__name__, result))
+
+ @inlineCallbacks
def test_getChallenge(self):
"""
Test that all the required fields exist in the challenge,
@@ -170,34 +200,36 @@
DigestCredentialFactory
"""
- challenge = self.credentialFactories[0].getChallenge(clientAddress)
+ challenge = (yield self.credentialFactories[0].getChallenge(clientAddress))
self.assertEquals(challenge['qop'], 'auth')
self.assertEquals(challenge['realm'], 'test realm')
self.assertEquals(challenge['algorithm'], 'md5')
self.assertTrue(challenge.has_key("nonce"))
- challenge = self.credentialFactories[1].getChallenge(clientAddress)
+ challenge = (yield self.credentialFactories[1].getChallenge(clientAddress))
self.assertFalse(challenge.has_key('qop'))
self.assertEquals(challenge['realm'], 'test realm')
self.assertEquals(challenge['algorithm'], 'md5')
self.assertTrue(challenge.has_key("nonce"))
+ @inlineCallbacks
def test_response(self):
"""
Test that we can decode a valid response to our challenge
"""
for ctr, factory in enumerate(self.credentialFactories):
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
clientResponse = authRequest1[ctr] % (
challenge['nonce'],
self.getDigestResponse(challenge, "00000001"),
)
- creds = factory.decode(clientResponse, _trivial_GET())
+ creds = (yield factory.decode(clientResponse, _trivial_GET()))
self.failUnless(creds.checkPassword('password'))
+ @inlineCallbacks
def test_multiResponse(self):
"""
Test that multiple responses to to a single challenge are handled
@@ -205,14 +237,14 @@
"""
for ctr, factory in enumerate(self.credentialFactories):
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
clientResponse = authRequest1[ctr] % (
challenge['nonce'],
self.getDigestResponse(challenge, "00000001"),
)
- creds = factory.decode(clientResponse, _trivial_GET())
+ creds = (yield factory.decode(clientResponse, _trivial_GET()))
self.failUnless(creds.checkPassword('password'))
clientResponse = authRequest2[ctr] % (
@@ -220,9 +252,10 @@
self.getDigestResponse(challenge, "00000002"),
)
- creds = factory.decode(clientResponse, _trivial_GET())
+ creds = (yield factory.decode(clientResponse, _trivial_GET()))
self.failUnless(creds.checkPassword('password'))
+ @inlineCallbacks
def test_failsWithDifferentMethod(self):
"""
Test that the response fails if made for a different request method
@@ -230,17 +263,18 @@
"""
for ctr, factory in enumerate(self.credentialFactories):
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
clientResponse = authRequest1[ctr] % (
challenge['nonce'],
self.getDigestResponse(challenge, "00000001"),
)
- creds = factory.decode(clientResponse,
- SimpleRequest(None, 'POST', '/'))
+ creds = (yield factory.decode(clientResponse,
+ SimpleRequest(None, 'POST', '/')))
self.failIf(creds.checkPassword('password'))
+ @inlineCallbacks
def test_noUsername(self):
"""
Test that login fails when our response does not contain a username,
@@ -249,31 +283,33 @@
# Check for no username
for factory in self.credentialFactories:
- e = self.assertRaises(error.LoginFailed,
+ e = (yield self.assertRaisesDeferred(error.LoginFailed,
factory.decode,
namelessAuthRequest,
- _trivial_GET())
+ _trivial_GET()))
self.assertEquals(str(e), "Invalid response, no username given.")
# Check for an empty username
- e = self.assertRaises(error.LoginFailed,
+ e = (yield self.assertRaisesDeferred(error.LoginFailed,
factory.decode,
namelessAuthRequest + ',username=""',
- _trivial_GET())
+ _trivial_GET()))
self.assertEquals(str(e), "Invalid response, no username given.")
+ @inlineCallbacks
def test_noNonce(self):
"""
Test that login fails when our response does not contain a nonce
"""
for factory in self.credentialFactories:
- e = self.assertRaises(error.LoginFailed,
+ e = (yield self.assertRaisesDeferred(error.LoginFailed,
factory.decode,
'realm="Test",username="Foo",opaque="bar"',
- _trivial_GET())
+ _trivial_GET()))
self.assertEquals(str(e), "Invalid response, no nonce given.")
+ @inlineCallbacks
def test_emptyAttribute(self):
"""
Test that login fails when our response contains an attribute
@@ -282,12 +318,13 @@
# Check for no username
for factory in self.credentialFactories:
- e = self.assertRaises(error.LoginFailed,
+ e = (yield self.assertRaisesDeferred(error.LoginFailed,
factory.decode,
emtpyAttributeAuthRequest,
- _trivial_GET())
+ _trivial_GET()))
self.assertEquals(str(e), "Invalid response, no username given.")
+ @inlineCallbacks
def test_checkHash(self):
"""
Check that given a hash of the form 'username:realm:password'
@@ -295,14 +332,14 @@
"""
for ctr, factory in enumerate(self.credentialFactories):
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
clientResponse = authRequest1[ctr] % (
challenge['nonce'],
self.getDigestResponse(challenge, "00000001"),
)
- creds = factory.decode(clientResponse, _trivial_GET())
+ creds = (yield factory.decode(clientResponse, _trivial_GET()))
self.failUnless(creds.checkHash(
md5.md5('username:test realm:password').hexdigest()))
@@ -310,18 +347,19 @@
self.failIf(creds.checkHash(
md5.md5('username:test realm:bogus').hexdigest()))
+ @inlineCallbacks
def test_invalidNonceCount(self):
"""
Test that login fails when the nonce-count is repeated.
"""
credentialFactories = (
- FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.path1),
- FakeDigestCredentialFactory('md5', '', 'test realm', self.path2)
+ FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.namespace1),
+ FakeDigestCredentialFactory('md5', '', 'test realm', self.namespace2)
)
for ctr, factory in enumerate(credentialFactories):
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
clientResponse1 = authRequest1[ctr] % (
challenge['nonce'],
@@ -333,18 +371,18 @@
self.getDigestResponse(challenge, "00000002"),
)
- factory.decode(clientResponse1, _trivial_GET())
- factory.decode(clientResponse2, _trivial_GET())
+ yield factory.decode(clientResponse1, _trivial_GET())
+ yield factory.decode(clientResponse2, _trivial_GET())
if challenge.get('qop') is not None:
- self.assertRaises(
+ yield self.assertRaisesDeferred(
error.LoginFailed,
factory.decode,
clientResponse2,
_trivial_GET()
)
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
clientResponse1 = authRequest1[ctr] % (
challenge['nonce'],
@@ -355,14 +393,15 @@
challenge['nonce'],
self.getDigestResponse(challenge, "00000002"),
)
- factory.decode(clientResponse1, _trivial_GET())
- self.assertRaises(
+ yield factory.decode(clientResponse1, _trivial_GET())
+ yield self.assertRaisesDeferred(
error.LoginFailed,
factory.decode,
clientResponse3,
_trivial_GET()
)
+ @inlineCallbacks
def test_invalidNonce(self):
"""
Test that login fails when the given nonce from the response, does not
@@ -370,12 +409,12 @@
"""
credentialFactories = (
- FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.path1),
- FakeDigestCredentialFactory('md5', '', 'test realm', self.path2)
+ FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.namespace1),
+ FakeDigestCredentialFactory('md5', '', 'test realm', self.namespace2)
)
for ctr, factory in enumerate(credentialFactories):
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
challenge['nonce'] = "noNoncense"
clientResponse = authRequest1[ctr] % (
@@ -384,17 +423,21 @@
)
request = _trivial_GET()
- self.assertRaises(
+ yield self.assertRaisesDeferred(
error.LoginFailed,
factory.decode,
clientResponse,
request
)
- factory.invalidate(factory.generateNonce())
- response = UnauthorizedResponse({"Digest":factory}, request.remoteAddr)
+ factory._invalidate(factory.generateNonce())
+ response = (yield UnauthorizedResponse.makeResponse(
+ {"Digest":factory},
+ request.remoteAddr
+ ))
response.headers.getHeader("www-authenticate")[0][1]
+ @inlineCallbacks
def test_incompatibleClientIp(self):
"""
Test that the login fails when the request comes from a client ip
@@ -402,12 +445,12 @@
"""
credentialFactories = (
- FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.path1),
- FakeDigestCredentialFactory('md5', '', 'test realm', self.path2)
+ FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.namespace1),
+ FakeDigestCredentialFactory('md5', '', 'test realm', self.namespace2)
)
for ctr, factory in enumerate(credentialFactories):
- challenge = factory.getChallenge(address.IPv4Address('TCP', '127.0.0.2', 80))
+ challenge = (yield factory.getChallenge(address.IPv4Address('TCP', '127.0.0.2', 80)))
clientResponse = authRequest1[ctr] % (
challenge['nonce'],
@@ -415,17 +458,21 @@
)
request = _trivial_GET()
- self.assertRaises(
+ yield self.assertRaisesDeferred(
error.LoginFailed,
factory.decode,
clientResponse,
request
)
- response = UnauthorizedResponse({"Digest":factory}, request.remoteAddr)
+ response = (yield UnauthorizedResponse.makeResponse(
+ {"Digest":factory},
+ request.remoteAddr,
+ ))
wwwhdrs = response.headers.getHeader("www-authenticate")[0][1]
self.assertTrue('stale' not in wwwhdrs, msg="Stale parameter in Digest WWW-Authenticate headers: %s" % (wwwhdrs,))
+ @inlineCallbacks
def test_oldNonce(self):
"""
Test that the login fails when the given opaque is older than
@@ -433,13 +480,13 @@
"""
credentialFactories = (
- FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.path1),
- FakeDigestCredentialFactory('md5', '', 'test realm', self.path2)
+ FakeDigestCredentialFactory('md5', 'auth', 'test realm', self.namespace1),
+ FakeDigestCredentialFactory('md5', '', 'test realm', self.namespace2)
)
for ctr, factory in enumerate(credentialFactories):
- challenge = factory.getChallenge(clientAddress)
- clientip, nonce_count, timestamp = factory.db.get(challenge['nonce'])
+ challenge = (yield factory.getChallenge(clientAddress))
+ clientip, nonce_count, timestamp = (yield factory.db.get(challenge['nonce']))
factory.db.set(challenge['nonce'], (clientip, nonce_count, timestamp - 2 * digest.DigestCredentialFactory.CHALLENGE_LIFETIME_SECS))
clientResponse = authRequest1[ctr] % (
@@ -448,14 +495,17 @@
)
request = _trivial_GET()
- self.assertRaises(
+ yield self.assertRaisesDeferred(
error.LoginFailed,
factory.decode,
clientResponse,
request
)
- response = UnauthorizedResponse({"Digest":factory}, request.remoteAddr)
+ response = (yield UnauthorizedResponse.makeResponse(
+ {"Digest":factory},
+ request.remoteAddr,
+ ))
wwwhdrs = response.headers.getHeader("www-authenticate")[0][1]
self.assertTrue('stale' in wwwhdrs, msg="No stale parameter in Digest WWW-Authenticate headers: %s" % (wwwhdrs,))
self.assertEquals(wwwhdrs['stale'], 'true', msg="stale parameter not set to true in Digest WWW-Authenticate headers: %s" % (wwwhdrs,))
@@ -486,20 +536,21 @@
preHA1=preHA1
)
+ @inlineCallbacks
def test_commaURI(self):
"""
Check that commas in valued are parsed out properly.
"""
for ctr, factory in enumerate(self.credentialFactories):
- challenge = factory.getChallenge(clientAddress)
+ challenge = (yield factory.getChallenge(clientAddress))
clientResponse = authRequestComma[ctr] % (
challenge['nonce'],
self.getDigestResponseComma(challenge, "00000001"),
)
- creds = factory.decode(clientResponse, _trivial_GET())
+ creds = (yield factory.decode(clientResponse, _trivial_GET()))
self.failUnless(creds.checkPassword('password'))
Modified: CalendarServer/trunk/twistedcaldav/directory/wiki.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/wiki.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/twistedcaldav/directory/wiki.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -207,12 +207,11 @@
if userID == "unauthenticated":
# Return a 401 so they have an opportunity to log in
- raise HTTPError(
- UnauthorizedResponse(
- request.credentialFactories,
- request.remoteAddr
- )
- )
+ response = (yield UnauthorizedResponse.makeResponse(
+ request.credentialFactories,
+ request.remoteAddr,
+ ))
+ raise HTTPError(response)
raise HTTPError(
StatusResponse(
Modified: CalendarServer/trunk/twistedcaldav/root.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/root.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/twistedcaldav/root.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -19,8 +19,8 @@
"RootResource",
]
-from twisted.internet.defer import maybeDeferred, succeed
-from twisted.python.failure import Failure
+from twisted.internet.defer import succeed, inlineCallbacks,\
+ returnValue
from twisted.cred.error import LoginFailed, UnauthorizedLogin
from twisted.web2 import responsecode
@@ -46,7 +46,7 @@
return config.RootResourceACL
def accessControlList(self, request, inheritance=True, expanding=False, inherited_aces=None):
- # Permissions here are fixed, and are not subject to inherritance rules, etc.
+ # Permissions here are fixed, and are not subject to inheritance rules, etc.
return succeed(self.defaultAccessControlList())
@@ -99,98 +99,59 @@
return self._dead_properties
+ @inlineCallbacks
def checkSacl(self, request):
"""
Check SACLs against the current request
"""
- def _authCb((authnUser, authzUser)):
- # Ensure that the user is not unauthenticated.
- # SACLs are authorization for the use of the service,
- # so unauthenticated access doesn't make any sense.
- if authzUser == davxml.Principal(davxml.Unauthenticated()):
- log.msg("Unauthenticated users not enabled with the '%s' SACL" % (self.saclService,))
- return Failure(HTTPError(UnauthorizedResponse(
- request.credentialFactories,
- request.remoteAddr)))
+ try:
+ authnUser, authzUser = yield self.authenticate(request)
+ except Exception:
+ response = (yield UnauthorizedResponse.makeResponse(
+ request.credentialFactories,
+ request.remoteAddr
+ ))
+ raise HTTPError(response)
- return (authnUser, authzUser)
+ # Ensure that the user is not unauthenticated.
+ # SACLs are authorization for the use of the service,
+ # so unauthenticated access doesn't make any sense.
+ if authzUser == davxml.Principal(davxml.Unauthenticated()):
+ log.msg("Unauthenticated users not enabled with the '%s' SACL" % (self.saclService,))
+ response = (yield UnauthorizedResponse.makeResponse(
+ request.credentialFactories,
+ request.remoteAddr
+ ))
+ raise HTTPError(response)
- def _authEb(failure):
- # Make sure we propogate UnauthorizedLogin errors.
- failure.trap(UnauthorizedLogin, LoginFailed)
+ # Cache the authentication details
+ request.authnUser = authnUser
+ request.authzUser = authzUser
- return Failure(HTTPError(UnauthorizedResponse(
- request.credentialFactories,
- request.remoteAddr)))
+ # Figure out the "username" from the davxml.Principal object
+ request.checkingSACL = True
+ principal = (yield request.locateResource(authzUser.children[0].children[0].data))
+ delattr(request, "checkingSACL")
+ username = principal.record.shortName
- def _checkSACLCb((authnUser, authzUser)):
- # Cache the authentication details
- request.authnUser = authnUser
- request.authzUser = authzUser
+ if RootResource.CheckSACL(username, self.saclService) != 0:
+ log.msg("User '%s' is not enabled with the '%s' SACL" % (username, self.saclService,))
+ raise HTTPError(responsecode.FORBIDDEN)
- # Figure out the "username" from the davxml.Principal object
- request.checkingSACL = True
- d = request.locateResource(authzUser.children[0].children[0].data)
+ # Mark SACL's as having been checked so we can avoid doing it multiple times
+ request.checkedSACL = True
- def _checkedSACLCb(principal):
- delattr(request, "checkingSACL")
- username = principal.record.shortName
- if RootResource.CheckSACL(username, self.saclService) != 0:
- log.msg("User '%s' is not enabled with the '%s' SACL" % (username, self.saclService,))
- return Failure(HTTPError(403))
+ returnValue(True)
- # Mark SACL's as having been checked so we can avoid doing it multiple times
- request.checkedSACL = True
- return True
-
- d.addCallback(_checkedSACLCb)
- return d
-
- d = maybeDeferred(self.authenticate, request)
- d.addCallbacks(_authCb, _authEb)
- d.addCallback(_checkSACLCb)
- return d
-
-
+ @inlineCallbacks
def locateChild(self, request, segments):
- def _authCb((authnUser, authzUser)):
- request.authnUser = authnUser
- request.authzUser = authzUser
- def _authEb(failure):
- # Make sure we propogate UnauthorizedLogin errors.
- failure.trap(UnauthorizedLogin, LoginFailed)
-
- return Failure(HTTPError(UnauthorizedResponse(
- request.credentialFactories,
- request.remoteAddr)))
-
for filter in self.contentFilters:
request.addResponseFilter(filter[0], atEnd=filter[1])
-
# Examine cookies for wiki auth token
-
- def validSessionID(username):
- log.info("Wiki lookup returned user: %s" % (username,))
- directory = request.site.resource.getDirectory()
- record = directory.recordWithShortName("users", username)
- if record is None:
- raise HTTPError(StatusResponse(
- responsecode.FORBIDDEN,
- "The username (%s) corresponding to your sessionID was not found by calendar server." % (username,)
- ))
- request.authnUser = request.authzUser = davxml.Principal(
- davxml.HRef.fromString("/principals/__uids__/%s/" % (record.guid,)))
- def invalidSessionID(error):
- log.info("Wiki lookup returned ERROR: %s" % (error,))
- raise HTTPError(StatusResponse(
- responsecode.FORBIDDEN,
- "Your sessionID was rejected by the authenticating wiki server."
- ))
-
wikiConfig = config.Authentication.Wiki
cookies = request.headers.getHeader('cookie')
if wikiConfig["Enabled"] and cookies is not None:
@@ -204,20 +165,34 @@
if token is not None and token != "unauthenticated":
log.info("Wiki sessionID cookie value: %s" % (token,))
proxy = Proxy(wikiConfig["URL"])
- d = proxy.callRemote(wikiConfig["UserMethod"],
- token).addCallbacks(validSessionID, invalidSessionID)
- d.addCallback(lambda _: super(RootResource, self
- ).locateChild(request, segments))
- return d
+ try:
+ username = (yield proxy.callRemote(wikiConfig["UserMethod"], token))
+ log.info("Wiki lookup returned user: %s" % (username,))
+ directory = request.site.resource.getDirectory()
+ record = directory.recordWithShortName("users", username)
+ if record is None:
+ raise HTTPError(StatusResponse(
+ responsecode.FORBIDDEN,
+ "The username (%s) corresponding to your sessionID was not found by calendar server." % (username,)
+ ))
+ request.authnUser = request.authzUser = davxml.Principal(
+ davxml.HRef.fromString("/principals/__uids__/%s/" % (record.guid,)))
+ child = (yield super(RootResource, self).locateChild(request, segments))
+ returnValue(child)
+ except Exception, e:
+ log.info("Wiki lookup returned ERROR: %s" % (e,))
+ raise HTTPError(StatusResponse(
+ responsecode.FORBIDDEN,
+ "Your sessionID was rejected by the authenticating wiki server."
+ ))
+
if self.useSacls and not hasattr(request, "checkedSACL") and not hasattr(request, "checkingSACL"):
- d = self.checkSacl(request)
- d.addCallback(lambda _: super(RootResource, self
- ).locateChild(request, segments))
+ yield self.checkSacl(request)
+ child = (yield super(RootResource, self).locateChild(request, segments))
+ returnValue(child)
- return d
-
if config.RejectClients:
#
# Filter out unsupported clients
@@ -232,36 +207,33 @@
"Your client software (%s) is not allowed to access this service." % (agent,)
))
- def _getCachedResource(_ign, request):
- if not getattr(request, 'checkingCache', False):
- request.checkingCache = True
- d = self.responseCache.getResponseForRequest(request)
- d.addCallback(_serveResponse)
- return d
+ if request.method == 'PROPFIND' and not getattr(request, 'notInCache', False):
+ try:
+ authnUser, authzUser = (yield self.authenticate(request))
+ request.authnUser = authnUser
+ request.authzUser = authzUser
+ except (UnauthorizedLogin, LoginFailed):
+ response = (yield UnauthorizedResponse.makeResponse(
+ request.credentialFactories,
+ request.remoteAddr
+ ))
+ raise HTTPError(response)
- return super(RootResource, self).locateChild(request, segments)
+ try:
+ if not getattr(request, 'checkingCache', False):
+ request.checkingCache = True
+ response = (yield self.responseCache.getResponseForRequest(request))
+ if response is None:
+ request.notInCache = True
+ raise KeyError("Not found in cache.")
+
+ returnValue((_CachedResponseResource(response), []))
+ except KeyError:
+ pass
- def _serveResponse(response):
- if response is None:
- request.notInCache = True
- raise KeyError("Not found in cache.")
+ child = (yield super(RootResource, self).locateChild(request, segments))
+ returnValue(child)
- return _CachedResponseResource(response), []
-
- def _resourceNotInCacheEb(failure):
- failure.trap(KeyError)
- return super(RootResource, self).locateChild(request,segments)
-
- if request.method == 'PROPFIND' and not getattr(
- request, 'notInCache', False):
- d = maybeDeferred(self.authenticate, request)
- d.addCallbacks(_authCb, _authEb)
- d.addCallback(_getCachedResource, request)
- d.addErrback(_resourceNotInCacheEb)
- return d
-
- return super(RootResource, self).locateChild(request, segments)
-
def http_COPY (self, request): return responsecode.FORBIDDEN
def http_MOVE (self, request): return responsecode.FORBIDDEN
def http_DELETE (self, request): return responsecode.FORBIDDEN
Modified: CalendarServer/trunk/twistedcaldav/test/test_kerberos.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_kerberos.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/twistedcaldav/test/test_kerberos.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -13,9 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
+
from twisted.cred.error import LoginFailed
-
from twisted.cred.error import UnauthorizedLogin
+from twisted.internet.defer import inlineCallbacks
from twisted.web2.test.test_server import SimpleRequest
from twistedcaldav import authkerb
@@ -31,10 +32,11 @@
def test_BasicKerberosCredentials(self):
authkerb.BasicKerberosCredentials("test", "test", "http/example.com at EXAMPLE.COM", "EXAMPLE.COM")
+ @inlineCallbacks
def test_BasicKerberosCredentialFactory(self):
factory = authkerb.BasicKerberosCredentialFactory(principal="http/server.example.com at EXAMPLE.COM")
- challenge = factory.getChallenge("peer")
+ challenge = (yield factory.getChallenge("peer"))
expected_challenge = {'realm': "EXAMPLE.COM"}
self.assertTrue(challenge == expected_challenge,
msg="BasicKerberosCredentialFactory challenge %s != %s" % (challenge, expected_challenge))
@@ -49,17 +51,18 @@
def test_NegotiateCredentials(self):
authkerb.NegotiateCredentials("test")
+ @inlineCallbacks
def test_NegotiateCredentialFactory(self):
factory = authkerb.NegotiateCredentialFactory(principal="http/server.example.com at EXAMPLE.COM")
- challenge = factory.getChallenge("peer")
+ challenge = (yield factory.getChallenge("peer"))
expected_challenge = {}
self.assertTrue(challenge == expected_challenge,
msg="NegotiateCredentialFactory challenge %s != %s" % (challenge, expected_challenge))
request = SimpleRequest(self.site, "GET", "/")
try:
- factory.decode("Bogus Data".encode("base64"), request)
+ yield factory.decode("Bogus Data".encode("base64"), request)
except (UnauthorizedLogin, LoginFailed):
pass
except Exception, ex:
Modified: CalendarServer/trunk/twistedcaldav/test/test_root.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_root.py 2008-12-05 21:38:23 UTC (rev 3469)
+++ CalendarServer/trunk/twistedcaldav/test/test_root.py 2008-12-06 03:56:15 UTC (rev 3470)
@@ -24,7 +24,7 @@
from twisted.cred.portal import Portal
-from twisted.internet import defer
+from twisted.internet.defer import inlineCallbacks, maybeDeferred
from twisted.web2 import http_headers
from twisted.web2 import responsecode
@@ -78,6 +78,7 @@
self.site = server.Site(self.root)
+ @inlineCallbacks
def test_noSacls(self):
"""
Test the behaviour of locateChild when SACLs are not enabled.
@@ -90,10 +91,9 @@
"GET",
"/principals/")
- resrc, segments = self.root.locateChild(request,
- ['principals'])
+ resrc, segments = (yield maybeDeferred(self.root.locateChild, request, ['principals']))
- resrc, segments = resrc.locateChild(request, ['principals'])
+ resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals']))
self.failUnless(
isinstance(resrc, DirectoryPrincipalProvisioningResource),
@@ -102,6 +102,7 @@
self.assertEquals(segments, [])
+ @inlineCallbacks
def test_inSacls(self):
"""
Test the behavior of locateChild when SACLs are enabled and the
@@ -119,26 +120,22 @@
'Authorization': ['basic', '%s' % (
'dreid:dierd'.encode('base64'),)]}))
- resrc, segments = self.root.locateChild(request,
- ['principals'])
+ resrc, segments = (yield maybeDeferred(self.root.locateChild, request, ['principals']))
- def _Cb((resrc, segments)):
- self.failUnless(
- isinstance(resrc, DirectoryPrincipalProvisioningResource),
- "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,)
- )
+ resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals']))
- self.assertEquals(segments, [])
+ self.failUnless(
+ isinstance(resrc, DirectoryPrincipalProvisioningResource),
+ "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,)
+ )
- self.assertEquals(request.authzUser,
- davxml.Principal(
- davxml.HRef('/principals/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/')))
+ self.assertEquals(segments, [])
- d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
- d.addCallback(_Cb)
+ self.assertEquals(request.authzUser,
+ davxml.Principal(
+ davxml.HRef('/principals/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/')))
- return d
-
+ @inlineCallbacks
def test_notInSacls(self):
"""
Test the behavior of locateChild when SACLs are enabled and the
@@ -156,17 +153,14 @@
'Authorization': ['basic', '%s' % (
'wsanchez:zehcnasw'.encode('base64'),)]}))
- resrc, segments = self.root.locateChild(request, ['principals'])
+ resrc, segments = (yield maybeDeferred(self.root.locateChild, request, ['principals']))
- def _Eb(failure):
- failure.trap(HTTPError)
- self.assertEquals(failure.value.response.code, 403)
+ try:
+ resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals']))
+ except HTTPError, e:
+ self.assertEquals(e.response.code, 403)
- d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
- d.addErrback(_Eb)
-
- return d
-
+ @inlineCallbacks
def test_unauthenticated(self):
"""
Test the behavior of locateChild when SACLs are enabled and the request
@@ -180,24 +174,15 @@
"GET",
"/principals/")
- resrc, segments = self.root.locateChild(request,
- ['principals'])
+ resrc, segments = (yield maybeDeferred(self.root.locateChild, request, ['principals']))
- def _Cb(result):
- raise AssertionError(("RootResource.locateChild did not return "
- "an error"))
+ try:
+ resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals']))
+ raise AssertionError(("RootResource.locateChild did not return an error"))
+ except HTTPError, e:
+ self.assertEquals(e.response.code, 401)
- def _Eb(failure):
- failure.trap(HTTPError)
- self.assertEquals(failure.value.response.code, 401)
-
- d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
-
- d.addCallback(_Cb)
- d.addErrback(_Eb)
-
- return d
-
+ @inlineCallbacks
def test_badCredentials(self):
"""
Test the behavior of locateChild when SACLS are enabled, and
@@ -215,18 +200,13 @@
'Authorization': ['basic', '%s' % (
'dreid:dreid'.encode('base64'),)]}))
- resrc, segments = self.root.locateChild(request,
- ['principals'])
+ resrc, segments = (yield maybeDeferred(self.root.locateChild, request, ['principals']))
- def _Eb(failure):
- failure.trap(HTTPError)
- self.assertEquals(failure.value.response.code, 401)
+ try:
+ resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals']))
+ except HTTPError, e:
+ self.assertEquals(e.response.code, 401)
- d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
- d.addErrback(_Eb)
-
- return d
-
def test_DELETE(self):
def do_test(response):
response = IResponse(response)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081205/4b823b3a/attachment-0001.html>
More information about the calendarserver-changes
mailing list