[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