[CalendarServer-changes] [339] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Tue Oct 31 08:16:40 PST 2006
Revision: 339
http://trac.macosforge.org/projects/calendarserver/changeset/339
Author: cdaboo at apple.com
Date: 2006-10-31 08:16:40 -0800 (Tue, 31 Oct 2006)
Log Message:
-----------
Support for proxies: users who can authorize as another user. This is needed to support web-calendar applications that do not want to
require plain text passwords from users. The proxy users must be explicitly configured via the repository.xml - only those users will
be allowed to proxy. To proxy the user does a regular HTTP auth, and also includes an X-Authorize-As header with the user id of the user
they wish to proxy as.
Modified Paths:
--------------
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
CalendarServer/trunk/twistedcaldav/authkerb.py
CalendarServer/trunk/twistedcaldav/directory.py
CalendarServer/trunk/twistedcaldav/repository.py
CalendarServer/trunk/twistedcaldav/resource.py
Added Paths:
-----------
CalendarServer/trunk/lib-patches/Twisted/twisted.dav.auth.patch
CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch
Added: CalendarServer/trunk/lib-patches/Twisted/twisted.dav.auth.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.dav.auth.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.dav.auth.patch 2006-10-31 16:16:40 UTC (rev 339)
@@ -0,0 +1,66 @@
+Index: twisted/web2/dav/auth.py
+===================================================================
+--- twisted/web2/dav/auth.py (revision 18545)
++++ twisted/web2/dav/auth.py (working copy)
+@@ -40,7 +40,7 @@
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+ if IPrincipal in interfaces:
+- return IPrincipal, davxml.Principal(davxml.HRef(avatarId))
++ return IPrincipal, davxml.Principal(davxml.HRef(avatarId[0])), davxml.Principal(davxml.HRef(avatarId[1]))
+
+ raise NotImplementedError("Only IPrincipal interface is supported")
+
+@@ -52,9 +52,23 @@
+ class PrincipalCredentials(object):
+ implements(IPrincipalCredentials)
+
+- def __init__(self, principal, principalURI, credentials):
+- self.principal = principal
+- self.principalURI = principalURI
++ def __init__(self, authnPrincipal, authnURI, authzPrincipal, authzURI, credentials):
++ """
++ Initialize with both authentication and authorization values. Note that in most cases theses will be the same
++ since HTTP auth makes no distinction between the two - but we may be layering some addition auth on top of this
++ (.e.g.. proxy auth, cookies, forms etc) that make result in authentication and authorization being different.
++
++ @param authnPrincipal: L{IDAVPrincipalResource} for the authenticated principal.
++ @param authnURI: C{str} containing the URI of the authenticated principal.
++ @param authzPrincipal: L{IDAVPrincipalResource} for the authorized principal.
++ @param authzURI: C{str} containing the URI of the authorized principal.
++ @param credentials: L{IPrincipalCredentials} for the authentication credentials.
++ """
++
++ self.authnPrincipal = authnPrincipal
++ self.authnURI = authnURI
++ self.authzPrincipal = authzPrincipal
++ self.authzURI = authzURI
+ self.credentials = credentials
+
+ def checkPassword(self, password):
+@@ -66,19 +80,20 @@
+
+ credentialInterfaces = (IPrincipalCredentials,)
+
+- def _cbPasswordMatch(self, matched, principalURI):
++ def _cbPasswordMatch(self, matched, principalURIs):
+ if matched:
+- return principalURI
++ # We return both URIs
++ return principalURIs
+ else:
+ raise error.UnauthorizedLogin(
+- "Bad credentials for: %s" % (principalURI,))
++ "Bad credentials for: %s" % (principalURIs[0],))
+
+ def requestAvatarId(self, credentials):
+ pcreds = IPrincipalCredentials(credentials)
+- pswd = str(pcreds.principal.readDeadProperty(TwistedPasswordProperty))
++ pswd = str(pcreds.authnPrincipal.readDeadProperty(TwistedPasswordProperty))
+
+ d = defer.maybeDeferred(credentials.checkPassword, pswd)
+- d.addCallback(self._cbPasswordMatch, pcreds.principalURI)
++ d.addCallback(self._cbPasswordMatch, (pcreds.authnURI, pcreds.authzURI,))
+ return d
+
+ ##
Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2006-10-31 16:13:44 UTC (rev 338)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.resource.patch 2006-10-31 16:16:40 UTC (rev 339)
@@ -98,7 +98,212 @@
child.addCallback(checkPrivileges)
child.addCallbacks(gotChild, checkPrivilegesError, (childpath,))
child.addErrback(completionDeferred.errback)
-@@ -1511,6 +1561,265 @@
+@@ -574,9 +624,9 @@
+ 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
+- if request.user == davxml.Principal(davxml.Unauthenticated()):
++ if request.authnUser == davxml.Principal(davxml.Unauthenticated()):
+ response = UnauthorizedResponse(request.credentialFactories,
+ request.remoteAddr)
+ else:
+@@ -600,8 +650,13 @@
+
+ def authenticate(self, request):
+ 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
+@@ -608,8 +663,9 @@
+ hasattr(request, 'credentialFactories') and
+ hasattr(request, 'loginInterfaces')
+ ):
+- 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,)
+
+ authHeader = request.headers.getHeader('authorization')
+
+@@ -625,9 +681,11 @@
+
+ # Try to match principals in each principal collection on the resource
+ def gotDetails(details):
+- principal = IDAVPrincipalResource(details[0])
+- principalURI = details[1]
+- return PrincipalCredentials(principal, principalURI, creds)
++ authnPrincipal = IDAVPrincipalResource(details[0][0])
++ authnURI = details[0][1]
++ authzPrincipal = IDAVPrincipalResource(details[1][0])
++ authzURI = details[1][1]
++ return PrincipalCredentials(authnPrincipal, authnURI, authzPrincipal, authzURI, creds)
+
+ def login(pcreds):
+ d = request.portal.login(pcreds, None, *request.loginInterfaces)
+@@ -635,7 +693,7 @@
+
+ return d
+
+- d = self.findPrincipalForAuthID(request, creds.username)
++ d = self.principalsForAuthID(request, creds.username)
+ d.addCallback(gotDetails).addCallback(login)
+
+ return d
+@@ -640,8 +698,9 @@
+
+ return d
+ 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,)
+
+ ##
+ # ACL
+@@ -650,10 +709,10 @@
+ def currentPrincipal(self, request):
+ """
+ @param request: the request being processed.
+- @return: the current principal, as derived from the given request.
++ @return: the current authorized principal, as derived from the given request.
+ """
+- if hasattr(request, "user"):
+- return request.user
++ if hasattr(request, "authzUser"):
++ return request.authzUser
+ else:
+ return unauthenticatedPrincipal
+
+@@ -1149,8 +1208,12 @@
+
+ return []
+
+- def findPrincipalForAuthID(self, request, authid):
++ def principalsForAuthID(self, request, authid):
+ """
++ Return authentication and authorization prinicipal identifiers for the
++ authentication identifer passed in. In this implementation authn and authz
++ principals are the same.
++
+ @param request: the L{IRequest} for the request in progress.
+ @param authid: a string containing the
+ authentication/authorization identifier for the principal
+@@ -1155,12 +1218,55 @@
+ @param authid: a string containing the
+ authentication/authorization identifier for the principal
+ to lookup.
+- @return: a deferred tuple of C{(principal, principalURI)}
+- where: C{principal} is the L{Principal} that is found;
+- C{principalURI} is the C{str} URI of the principal.
++ @return: a deferred tuple of two tuples. Each tuple is
++ C{(principal, principalURI)} where: C{principal} is the L{Principal}
++ that is found; {principalURI} is the C{str} URI of the principal.
++ The first tuple corresponds to authentication identifiers,
++ the second to authorization identifiers.
+ It will errback with an HTTPError(responsecode.FORBIDDEN) if
+ the principal isn't found.
+ """
++
++ # Try to match principals in each principal collection on the resource
++ d = waitForDeferred(self.findPrincipalForAuthID(request, authid))
++ yield d
++ result = d.getResult()
++
++ if result is not None:
++ authnPrincipal = result[0]
++ authnURI = result[1]
++ d = waitForDeferred(self.authorizationPrincipal(request, authid, authnPrincipal, authnURI))
++ yield d
++ authzPrincipal, authzURI = d.getResult()
++ yield ((authnPrincipal, authnURI), (authzPrincipal, authzURI),)
++ return
++ else:
++ principalCollections = waitForDeferred(self.principalCollections(request))
++ yield principalCollections
++ principalCollections = principalCollections.getResult()
++
++ if len(principalCollections) == 0:
++ log.msg("DAV:principal-collection-set property cannot be found on the resource being authorized: %s" % self)
++ else:
++ log.msg("Could not find principal matching user id: %s" % authid)
++ raise HTTPError(responsecode.FORBIDDEN)
++
++ principalsForAuthID = deferredGenerator(principalsForAuthID)
++
++ def findPrincipalForAuthID(self, request, authid):
++ """
++ Return authentication and authoirization prinicipal identifiers for the
++ authentication identifer passed in. In this implementation authn and authz
++ principals are the same.
++
++ @param request: the L{IRequest} for the request in progress.
++ @param authid: a string containing the
++ authentication/authorization identifier for the principal
++ to lookup.
++ @return: a tuple of C{(principal, principalURI)} where: C{principal} is the L{Principal}
++ that is found; {principalURI} is the C{str} URI of the principal.
++ If not found return None.
++ """
+ # Try to match principals in each principal collection on the resource
+ collections = waitForDeferred(self.principalCollections(request))
+ yield collections
+@@ -1173,22 +1279,30 @@
+ yield principal
+ principal = principal.getResult()
+
+- if isPrincipalResource(principal):
+- yield (principal, principalURI)
++ if isPrincipalResource(principal) and principal.exists():
++ yield principal, principalURI
+ return
+ else:
+- principalCollections = waitForDeferred(self.principalCollections(request))
+- yield principalCollections
+- principalCollections = principalCollections.getResult()
+-
+- if len(principalCollections) == 0:
+- log.msg("DAV:principal-collection-set property cannot be found on the resource being authorized: %s" % self)
+- else:
+- log.msg("Could not find principal matching user id: %s" % authid)
+- raise HTTPError(responsecode.FORBIDDEN)
++ yield None
++ return
+
+ findPrincipalForAuthID = deferredGenerator(findPrincipalForAuthID)
+
++ def authorizationPrincipal(self, request, authid, authnPrincipal, authnURI):
++ """
++ Determine the authorization principal for the given request and authentication principal.
++ This implementation simply uses aht authentication principalk as the authoization principal.
++
++ @param request: the L{IRequest} for the request in progress.
++ @param authid: a string containing the uthentication/authorization identifier
++ for the principal to lookup.
++ @param authnPrincipal: the L{IDAVPrincipal} for the authenticated principal
++ @param authnURI: a C{str} containing the URI of the authenticated principal
++ @return: a deferred result C{tuple} of (L{IDAVPrincipal}, C{str}) containing the authorization principal
++ resource and URI respectively.
++ """
++ return succeed((authnPrincipal, authnURI,))
++
+ def samePrincipal(self, principal1, principal2):
+ """
+ Check whether the two prinicpals are exactly the same in terms of
+@@ -1511,6 +1625,265 @@
return None
##
@@ -364,7 +569,7 @@
# HTTP
##
-@@ -1558,7 +1867,7 @@
+@@ -1558,7 +1931,7 @@
"""
DAV resource with no children.
"""
@@ -373,7 +578,7 @@
return succeed(None)
class DAVPrincipalResource (DAVLeafResource):
-@@ -1712,6 +2021,37 @@
+@@ -1712,6 +2085,37 @@
davxml.registerElement(TwistedACLInheritable)
davxml.ACE.allowed_children[(twisted_dav_namespace, "inheritable")] = (0, 1)
Added: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.test.test_resource.patch 2006-10-31 16:16:40 UTC (rev 339)
@@ -0,0 +1,25 @@
+Index: twisted/web2/dav/test/test_resource.py
+===================================================================
+--- twisted/web2/dav/test/test_resource.py (revision 18545)
++++ twisted/web2/dav/test/test_resource.py (working copy)
+@@ -282,7 +282,8 @@
+ # Has auth; should allow
+
+ request = SimpleRequest(site, "GET", "/")
+- request.user = davxml.Principal(davxml.HRef("/users/d00d"))
++ request.authnUser = davxml.Principal(davxml.HRef("/users/d00d"))
++ request.authzUser = davxml.Principal(davxml.HRef("/users/d00d"))
+ d = request.locateResource('/')
+ d.addCallback(_checkPrivileges)
+ d.addCallback(expectOK)
+@@ -380,8 +381,8 @@
+ return succeed(davPrivilegeSet)
+
+ def currentPrincipal(self, request):
+- if hasattr(request, "user"):
+- return request.user
++ if hasattr(request, "authzUser"):
++ return request.authzUser
+ else:
+ return davxml.Principal(davxml.Unauthenticated())
+
Modified: CalendarServer/trunk/twistedcaldav/authkerb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/authkerb.py 2006-10-31 16:13:44 UTC (rev 338)
+++ CalendarServer/trunk/twistedcaldav/authkerb.py 2006-10-31 16:16:40 UTC (rev 339)
@@ -113,11 +113,11 @@
kerberos.checkPassword(creds.username, creds.password, creds.service, creds.default_realm)
except kerberos.BasicAuthError, ex:
logging.err("%s" % (ex[0],), system="BasicKerberosCredentialsChecker")
- raise error.UnauthorizedLogin("Bad credentials for: %s (%s)" % (pcreds.principalURI, ex[0],))
+ raise error.UnauthorizedLogin("Bad credentials for: %s (%s)" % (pcreds.authnURI, ex[0],))
else:
- return succeed(pcreds.principalURI)
+ return succeed((pcreds.authnURI, pcreds.authzURI,))
- raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.principalURI,))
+ raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.authnURI,))
class NegotiateCredentials:
"""
@@ -207,14 +207,14 @@
def requestAvatarId(self, credentials):
# NB If we get here authentication has already succeeded as it is done in NegotiateCredentialsFactory.decode
- # So all we need to do is return the principal URI from the credentials.
+ # So all we need to do is return the principal URIs from the credentials.
- # If there is no calendar principal URI then the calendar user is disabled.
+ # Look for proper credential type.
pcreds = IPrincipalCredentials(credentials)
creds = pcreds.credentials
if isinstance(creds, NegotiateCredentials):
- return succeed(pcreds.principalURI)
+ return succeed((pcreds.authnURI, pcreds.authzURI,))
- raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.principalURI,))
+ raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.authnURI,))
Modified: CalendarServer/trunk/twistedcaldav/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory.py 2006-10-31 16:13:44 UTC (rev 338)
+++ CalendarServer/trunk/twistedcaldav/directory.py 2006-10-31 16:16:40 UTC (rev 339)
@@ -39,6 +39,7 @@
from twisted.web2 import responsecode
from twisted.web2.dav import davxml
from twisted.web2.dav.auth import IPrincipalCredentials
+from twisted.web2.dav.auth import TwistedPropertyChecker
from twisted.web2.dav.static import DAVFile
from twisted.web2.dav.util import joinURL
from twisted.web2.http import HTTPError
@@ -57,26 +58,24 @@
import os
import unicodedata
-class DirectoryCredentialsChecker:
- implements(checkers.ICredentialsChecker)
+class DirectoryCredentialsChecker (TwistedPropertyChecker):
- credentialInterfaces = (IPrincipalCredentials,)
-
def requestAvatarId(self, credentials):
# If there is no calendar principal URI then the calendar user is disabled.
pcreds = IPrincipalCredentials(credentials)
- if not pcreds.principal.hasDeadProperty(customxml.TwistedCalendarPrincipalURI):
- raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.principalURI,))
+ if not pcreds.authnPrincipal.hasDeadProperty(customxml.TwistedCalendarPrincipalURI):
+ # Try regular password check
+ return TwistedPropertyChecker.requestAvatarId(self, credentials)
creds = pcreds.credentials
if isinstance(creds, UsernamePassword):
user = creds.username
pswd = creds.password
- if opendirectory.authenticateUser(pcreds.principal.directory(), user, pswd):
- return succeed(pcreds.principalURI)
+ if opendirectory.authenticateUser(pcreds.authnPrincipal.directory(), user, pswd):
+ return succeed((pcreds.authnURI, pcreds.authzURI,))
- raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.principalURI,))
+ raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.authnURI,))
class DirectoryPrincipalFile (CalendarPrincipalFile):
"""
Modified: CalendarServer/trunk/twistedcaldav/repository.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/repository.py 2006-10-31 16:13:44 UTC (rev 338)
+++ CalendarServer/trunk/twistedcaldav/repository.py 2006-10-31 16:16:40 UTC (rev 339)
@@ -118,6 +118,7 @@
ELEMENT_CALENDAR = "calendar"
ELEMENT_QUOTA = "quota"
ELEMENT_AUTORESPOND = "autorespond"
+ELEMENT_CANPROXY = "canproxy"
ATTRIBUTE_REPEAT = "repeat"
def startServer(docroot, repo, doacct, doacl, dossl, keyfile, certfile, onlyssl, port, sslport, maxsize, quota, serverlogfile, manhole):
@@ -903,6 +904,8 @@
self.acl.parseXML(child)
elif child._get_localName() == ELEMENT_AUTORESPOND:
self.autorespond = True
+ elif child._get_localName() == ELEMENT_CANPROXY:
+ CalDAVResource.proxyUsers.add(self.uid)
class Authentication:
"""
Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2006-10-31 16:13:44 UTC (rev 338)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2006-10-31 16:16:40 UTC (rev 339)
@@ -40,6 +40,7 @@
from twisted.internet import reactor
from twisted.internet.defer import Deferred, maybeDeferred, succeed
from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.python import log
from twisted.web2 import responsecode
from twisted.web2.dav import davxml
from twisted.web2.dav.resource import AccessDeniedError, DAVPrincipalResource
@@ -77,6 +78,10 @@
# resources to that size, or C{None} for no limit.
sizeLimit = None
+ # Set containing user ids of all the users who have been given
+ # the right to authorize as someone else.
+ proxyUsers = set()
+
##
# HTTP
##
@@ -227,6 +232,61 @@
return super(CalDAVResource, self).accessControlList(*args, **kwargs)
+ def authorizationPrincipal(self, request, authid, authnPrincipal, authnURI):
+ """
+ Determine the authorization principal for the given request and authentication principal.
+ This implementation looks for an X-Authorize-As header value to use as the authoization principal.
+
+ @param request: the L{IRequest} for the request in progress.
+ @param authid: a string containing the uthentication/authorization identifier
+ for the principal to lookup.
+ @param authnPrincipal: the L{IDAVPrincipal} for the authenticated principal
+ @param authnURI: a C{str} containing the URI of the authenticated principal
+ @return: a deferred result C{tuple} of (L{IDAVPrincipal}, C{str}) containing the authorization principal
+ resource and URI respectively.
+ """
+
+ # Look for X-Authorize-As Header
+ authz = request.headers.getRawHeaders("x-authorize-as")
+ if authz is not None and (len(authz) == 1):
+ # Substitute the authz value for principal look up
+ authz = authz[0]
+
+ # See if authenticated uid is a proxy user
+ if authid in CalDAVResource.proxyUsers:
+ if authz:
+ if authz in CalDAVResource.proxyUsers:
+ log.msg("Cannot proxy as another proxy: user '%s' as user '%s'" % (authid, authz))
+ raise HTTPError(responsecode.UNAUTHORIZED)
+ else:
+ d = waitForDeferred(self.findPrincipalForAuthID(request, authz))
+ yield d
+ result = d.getResult()
+
+ if result is not None:
+ log.msg("Allow proxy: user '%s' as '%s'" % (authid, authz,))
+ authzPrincipal = result[0]
+ authzURI = result[1]
+ yield authzPrincipal, authzURI
+ return
+ else:
+ log.msg("Could not find proxy user id: '%s'" % authid)
+ raise HTTPError(responsecode.UNAUTHORIZED)
+ else:
+ log.msg("Cannot authenticate proxy user '%s' without X-Authorize-As header" % (authid, ))
+ raise HTTPError(responsecode.UNAUTHORIZED)
+ elif authz:
+ log.msg("Cannot proxy: user '%s' as '%s'" % (authid, authz,))
+ raise HTTPError(responsecode.UNAUTHORIZED)
+ else:
+ # No proxy - do default behavior
+ d = waitForDeferred(super(CalDAVResource, self).authorizationPrincipal(request, authid, authnPrincipal, authnURI))
+ yield d
+ yield d.getResult()
+ return
+
+ authorizationPrincipal = deferredGenerator(authorizationPrincipal)
+
##
# CalDAV
##
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20061031/083e88ad/attachment.html
More information about the calendarserver-changes
mailing list