[CalendarServer-changes] [699] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Tue Dec 5 14:05:22 PST 2006


Revision: 699
          http://trac.macosforge.org/projects/calendarserver/changeset/699
Author:   dreid at apple.com
Date:     2006-12-05 14:05:21 -0800 (Tue, 05 Dec 2006)

Log Message:
-----------
merge branches/SACLs provide support for Service Access Control Lists in tiger and leopard via the PyApplAuth module

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/conf/repository.xml
    CalendarServer/trunk/run
    CalendarServer/trunk/twistedcaldav/caldavd.py
    CalendarServer/trunk/twistedcaldav/repository.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/root.py
    CalendarServer/trunk/twistedcaldav/test/test_root.py

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2006-12-05 22:05:04 UTC (rev 698)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2006-12-05 22:05:21 UTC (rev 699)
@@ -162,5 +162,8 @@
   <key>twistdLocation</key>
   <string>../Twisted/bin/twistd</string>
 
+  <key>SACLEnable</key>
+  <false/>
+
 </dict>
 </plist>

Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2006-12-05 22:05:04 UTC (rev 698)
+++ CalendarServer/trunk/conf/caldavd.plist	2006-12-05 22:05:21 UTC (rev 699)
@@ -111,5 +111,8 @@
   <key>twistdLocation</key>
   <string>/usr/share/caldavd/bin/twistd</string>
 
+  <key>SACLEnable</key>
+  <true/>
+
 </dict>
 </plist>

Modified: CalendarServer/trunk/conf/repository.xml
===================================================================
--- CalendarServer/trunk/conf/repository.xml	2006-12-05 22:05:04 UTC (rev 698)
+++ CalendarServer/trunk/conf/repository.xml	2006-12-05 22:05:21 UTC (rev 699)
@@ -22,7 +22,7 @@
 
   <docroot auto-principal-collection-set="yes">
     <collection>
-      <pytype>twisted.web2.dav.static.DAVFile</pytype>
+      <pytype>twistedcaldav.root.RootResource</pytype>
       <properties>
         <acl>
           <ace>

Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run	2006-12-05 22:05:04 UTC (rev 698)
+++ CalendarServer/trunk/run	2006-12-05 22:05:21 UTC (rev 699)
@@ -150,6 +150,12 @@
   install="$(pwd)/${install}";
 fi;
 
+svn_uri_base="$(svn info "${caldav}" --xml 2>/dev/null | sed -n 's|^.*<root>\(.*\)</root>.*$|\1|p')";
+
+if [ -z "${svn_uri_base}" ]; then
+  svn_uri_base="http://svn.macosforge.org/repository/calendarserver";
+fi;
+
 ##
 # Download and set up dependancies
 ##
@@ -372,34 +378,48 @@
 # PyKerberos
 #
 
-svn_uri_base="$(svn info "${caldav}" --xml 2>/dev/null | sed -n 's|^.*<root>\(.*\)</root>.*$|\1|p')";
+if type krb5-config > /dev/null; then
+  if ! py_have_module kerberos; then
+    kerberos="${top}/PyKerberos";
 
-if [ -z "${svn_uri_base}" ]; then
-  svn_uri_base="http://svn.macosforge.org/repository/calendarserver";
+    svn_get "PyKerberos" "${kerberos}" "${svn_uri_base}/PyKerberos/trunk" 202;
+    py_build "PyKerberos" "${kerberos}" false; # FIXME: make optional
+    py_install "PyKerberos" "${kerberos}";
+
+    export PYTHONPATH="${PYTHONPATH}:${kerberos}/build/${py_platform_libdir}";
+  fi;
 fi;
 
-if ! py_have_module kerberos; then
-  kerberos="${top}/PyKerberos";
+#
+# PyOpenDirectory
+#
 
-  svn_get "PyKerberos" "${kerberos}" "${svn_uri_base}/PyKerberos/trunk" 202;
-  py_build "PyKerberos" "${kerberos}" false; # FIXME: make optional
-  py_install "PyKerberos" "${kerberos}";
+if [ "$(uname -s)" == "Darwin" ]; then
+  if ! py_have_module opendirectory; then
+    opendirectory="${top}/PyOpenDirectory";
 
-  export PYTHONPATH="${PYTHONPATH}:${kerberos}/build/${py_platform_libdir}";
+    svn_get "PyOpenDirectory" "${opendirectory}" "${svn_uri_base}/PyOpenDirectory/trunk" 31;
+    py_build "PyOpenDirectory" "${opendirectory}" false;
+    py_install "PyOpenDirectory" "${opendirectory}";
+
+    export PYTHONPATH="${PYTHONPATH}:${opendirectory}/build/${py_platform_libdir}";
+  fi;
 fi;
 
 #
-# PyOpenDirectory
+# PyAppleAuth
 #
 
-if ! py_have_module opendirectory; then
-  opendirectory="${top}/PyOpenDirectory";
+if [ "$(uname -s)" == "Darwin" ]; then
+  if ! py_have_module appleauth; then
+    appleauth="${top}/PyAppleAuth";
 
-  svn_get "PyOpenDirectory" "${opendirectory}" "${svn_uri_base}/PyOpenDirectory/trunk" 31;
-  py_build "PyOpenDirectory" "${opendirectory}" false; # FIXME: make optional
-  py_install "PyOpenDirectory" "${opendirectory}";
+    svn_get "PyAppleAuth" "${appleauth}" "${svn_uri_base}/PyAppleAuth/trunk" 656;
+    py_build "PyAppleAuth" "${appleauth}" false;
+    py_install "PyAppleAuth" "${appleauth}"
 
-  export PYTHONPATH="${PYTHONPATH}:${opendirectory}/build/${py_platform_libdir}";
+    export PYTHONPATH="${PYTHONPATH}:${appleauth}/build/${py_platform_libdir}";
+  fi;
 fi;
 
 #

Modified: CalendarServer/trunk/twistedcaldav/caldavd.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavd.py	2006-12-05 22:05:04 UTC (rev 698)
+++ CalendarServer/trunk/twistedcaldav/caldavd.py	2006-12-05 22:05:21 UTC (rev 699)
@@ -67,9 +67,16 @@
     'ServerStatsFile': '/Library/CalendarServer/Documents/stats.plist',
     'UserQuotaBytes': 104857600,
     'Verbose': False,
-    'twistdLocation': '/usr/share/caldavd/bin/twistd'}
+    'twistdLocation': '/usr/share/caldavd/bin/twistd',
+    'SACLEnable': True,
+    }
 
+# FIXME: This doesn't actually work because the webserver runs in a different
+# python process from the commandline util caldavd that actually parses the 
+# plists the twistd plugin will fix this.
+CONFIG = DEFAULTS.copy()
 
+
 class caldavd(object):
     """
     Runs the caldav server.
@@ -79,7 +86,7 @@
         # Option defaults
         self.plistfile = "/etc/caldavd/caldavd.plist"
 
-        self.config = DEFAULTS.copy()
+        self.config = CONFIG
 
         self.action = None
     
@@ -332,6 +339,8 @@
             else:
                 print "Unknown option: %s" % (k,)
 
+        CONFIG = self.config
+
     def validate(self):
         
         result = True

Modified: CalendarServer/trunk/twistedcaldav/repository.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/repository.py	2006-12-05 22:05:04 UTC (rev 698)
+++ CalendarServer/trunk/twistedcaldav/repository.py	2006-12-05 22:05:21 UTC (rev 699)
@@ -43,6 +43,7 @@
 from twisted.web2.dav.element.parser import lookupElement
 from twisted.web2.dav.resource import TwistedACLInheritable
 from twisted.web2.dav.util import joinURL
+from twisted.web2.dav.idav import IDAVPrincipalCollectionResource
 from twisted.web2.log import LogWrapperResource
 from twisted.web2.server import Site
 
@@ -374,22 +375,11 @@
         """
         Build the entire repository starting at the root resource.
         """
-        self.collection.build(self.path, "/", directory)
+        self.collection.build(self, self.path, "/", directory)
         
-        # Setup the principal-collection-set property if required
-        if self.autoPrincipalCollectionSet:
-            # Check that a principal collection was actually created and 'tagged'
-            if not self.principalCollections:
-                log.msg("Cannot create a DAV:principal-collection-set property on the root resource because there are no principal collections.")
-                return
+        # Cheat        
+        self.collection.resource._principalCollections = self.principalCollections
             
-            # Create the private property
-            hrefs = []
-            for collection in self.principalCollections:
-                hrefs.append(davxml.HRef.fromString(collection.uri))
-            pcs = davxml.PrincipalCollectionSet(*hrefs)
-            self.collection.resource.writeDeadProperty(pcs)
-
 class Collection (object):
     """
     Contains information about a collection in the repository.
@@ -478,14 +468,13 @@
                 self.properties.append(Prop())
                 self.properties[-1].parseXML(child)
 
-    def build(self, docroot, urlroot, directory):
+    def build(self, docroot, mypath, urlroot, directory):
         """
         Create this collection, initialising any properties and then create any child
         collections.
         @param docroot: the file system path to create the collection in.
         @param urlroot: the URI path root to create the collection resource in.
         """
-        mypath = docroot
         myurl = urlroot
         if self.name is not None:
             mypath = os.path.join(mypath, self.name)
@@ -525,13 +514,16 @@
         # Set ACL now
         if self.acl is not None:
             self.resource.setAccessControlList(self.acl.acl)
-
+        
         for member in self.members:
-            child = member.build(mypath, myurl, directory)
+            child = member.build(docroot, mypath, myurl, directory)
             # Only putChild if one does not already exists
             if self.resource.putChildren.get(member.name, None) is None:
                 self.resource.putChild(member.name, child)
 
+            if IDAVPrincipalCollectionResource.providedBy(child):
+                docroot.principalCollections.append(child)
+                
         return self.resource
 
 class Prop (object):

Copied: CalendarServer/trunk/twistedcaldav/root.py (from rev 697, CalendarServer/branches/SACLs/twistedcaldav/root.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/root.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/root.py	2006-12-05 22:05:21 UTC (rev 699)
@@ -0,0 +1,102 @@
+##
+# Copyright (c) 2005-2006 Apple Computer, 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.
+#
+# DRI: David Reid, dreid at apple.com
+##
+
+from twisted.internet import defer
+from twisted.python.failure import Failure
+from twisted.cred.error import UnauthorizedLogin
+
+from twisted.web2.http import HTTPError
+from twisted.web2.auth.wrapper import UnauthorizedResponse
+
+from twisted.web2.dav import davxml
+from twisted.web2.dav.static import DAVFile
+
+
+class RootResource(DAVFile):
+    """
+    A special root resource that contains support checking SACLs
+    """
+
+    useSacls = False
+    saclService = 'calendar'
+
+    def __init__(self, path, *args, **kwargs):
+        super(RootResource, self).__init__(path, *args, **kwargs)
+        
+        from twistedcaldav import caldavd
+
+        if caldavd.CONFIG['SACLEnable'] and RootResource.CheckSACL:
+            self.useSacls = True
+
+    def checkSacl(self, request):
+        """
+        Check SACLs against the current request
+        """
+
+        def _authCb((authnUser, authzUser)):
+            # Insure 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()):
+                return Failure(HTTPError(UnauthorizedResponse(
+                            request.credentialFactories,
+                            request.remoteAddr)))
+
+            return (authnUser, authzUser)
+
+        def _authEb(failure):
+            # Make sure we propogate UnauthorizedLogin errors.
+            failure.trap(UnauthorizedLogin)
+
+            return Failure(HTTPError(UnauthorizedResponse(
+                        request.credentialFactories,
+                        request.remoteAddr)))
+
+        def _checkSACLCb((authnUser, authzUser)):
+            # Figure out the "username" from the davxml.Principal object
+            username = authzUser.children[0].children[0].data
+            username = username.split('/')[-1]
+            
+            if RootResource.CheckSACL(username, self.saclService) != 0:
+                return Failure(HTTPError(403))
+
+            return True
+            
+        d = defer.maybeDeferred(self.authenticate, request)
+        d.addCallbacks(_authCb, _authEb)
+        d.addCallback(_checkSACLCb)
+        return d
+
+    def locateChild(self, request, segments):
+        if self.useSacls:
+            d = self.checkSacl(request)
+            d.addCallback(lambda _: super(RootResource, self
+                                          ).locateChild(request, segments))
+
+            return d
+
+        return super(RootResource, self).locateChild(request, segments)
+
+
+# So CheckSACL will be parameterized
+# We do this after RootResource is defined
+try:
+    from appleauth import CheckSACL
+    RootResource.CheckSACL = CheckSACL
+except ImportError:
+    RootResource.CheckSACL = None

Copied: CalendarServer/trunk/twistedcaldav/test/test_root.py (from rev 697, CalendarServer/branches/SACLs/twistedcaldav/test/test_root.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_root.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_root.py	2006-12-05 22:05:21 UTC (rev 699)
@@ -0,0 +1,234 @@
+##
+# Copyright (c) 2005-2006 Apple Computer, 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.
+#
+# DRI: David Reid, dreid at apple.com
+##
+
+import os
+
+from twistedcaldav.root import RootResource
+
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource
+from twistedcaldav.directory.xmlfile import XMLDirectoryService
+from twistedcaldav.directory.test.test_xmlfile import xmlFile
+
+from twisted.internet import defer
+
+from twisted.web2.http import HTTPError
+
+from twisted.web2.dav import auth
+from twisted.web2.dav import davxml
+
+from twisted.web2 import server
+from twisted.web2.auth import basic
+from twisted.web2 import http_headers
+
+from twisted.cred.portal import Portal
+
+from twisted.web2.test.test_server import SimpleRequest
+
+class FakeCheckSACL(object):
+    def __init__(self, sacls=None):
+        self.sacls = sacls or {}
+
+    def __call__(self, username, service):
+        if service not in self.sacls:
+            return 1
+
+        if username in self.sacls[service]:
+            return 0
+
+        return 1
+
+class RootTests(TestCase):
+    def setUp(self):
+        self.docroot = self.mktemp()
+
+        RootResource.CheckSACL = FakeCheckSACL(sacls={
+                'calendar': ['dreid']})
+
+        directory = XMLDirectoryService(xmlFile)
+
+        principals = DirectoryPrincipalProvisioningResource(
+            os.path.join(self.docroot, 'principals'),
+            '/principals/',
+            directory)
+
+        root = RootResource(self.docroot, 
+                            principalCollections=[principals])
+
+        root.putChild('principals',
+                      principals)
+
+        portal = Portal(auth.DavRealm())
+        portal.registerChecker(directory)
+
+        self.root = auth.AuthenticationWrapper(
+            root, 
+            portal, 
+            credentialFactories=(basic.BasicCredentialFactory("Test realm"),),
+            loginInterfaces=(auth.IPrincipal,))
+
+        self.site = server.Site(self.root)
+
+    def test_noSacls(self):
+        """
+        Test the behaviour of locateChild when SACLs are not enabled.
+        
+        should return a valid resource
+        """
+        self.root.resource.useSacls = False
+
+        request = SimpleRequest(self.site,
+                                "GET",
+                                "/principals/")
+
+        resrc, segments = self.root.locateChild(request,
+                                         ['principals'])
+
+        resrc, segments = resrc.locateChild(request, ['principals'])
+
+        self.failUnless(
+            isinstance(resrc,
+                       DirectoryPrincipalProvisioningResource),
+            "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,))
+
+        self.assertEquals(segments, [])
+
+    def test_inSacls(self):
+        """
+        Test the behavior of locateChild when SACLs are enabled and the 
+        user is in the SACL group
+
+        should return a valid resource
+        """
+        self.root.resource.useSacls = True
+
+        request = SimpleRequest(
+            self.site,
+            "GET",
+            "/principals/",
+            headers=http_headers.Headers({
+                    'Authorization': ['basic', '%s' % (
+                            'dreid:dierd'.encode('base64'),)]}))
+        
+        resrc, segments = self.root.locateChild(request,
+                                         ['principals'])
+
+        def _Cb((resrc, segments)):
+            self.failUnless(
+                isinstance(resrc,
+                           DirectoryPrincipalProvisioningResource),
+                "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,))
+
+            self.assertEquals(segments, [])
+
+            self.assertEquals(request.authzUser, 
+                              davxml.Principal(
+                    davxml.HRef('/principals/user/dreid')))
+            
+        d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
+        d.addCallback(_Cb)
+
+        return d
+
+    def test_notInSacls(self):
+        """
+        Test the behavior of locateChild when SACLs are enabled and the
+        user is not in the SACL group
+        
+        should return a 403 forbidden response
+        """
+        self.root.resource.useSacls = True
+
+        request = SimpleRequest(
+            self.site,
+            "GET",
+            "/principals/",
+            headers=http_headers.Headers({
+                    'Authorization': ['basic', '%s' % (
+                            'wsanchez:zehcnasw'.encode('base64'),)]}))
+        
+        resrc, segments = self.root.locateChild(request,
+                                         ['principals'])
+
+        def _Eb(failure):
+            self.assertEquals(failure.value.response.code, 403)
+            
+        d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
+        d.addErrback(_Eb)
+
+        return d
+
+    def test_unauthenticated(self):
+        """
+        Test the behavior of locateChild when SACLs are enabled and the request
+        is unauthenticated
+        
+        should return a 401 UnauthorizedResponse
+        """
+
+        self.root.resource.useSacls = True
+        request = SimpleRequest(self.site,
+                                "GET",
+                                "/principals/")
+
+        resrc, segments = self.root.locateChild(request,
+                                                ['principals'])
+
+        def _Cb(result):
+            raise AssertionError(("RootResource.locateChild did not return "
+                                  "an error"))
+
+        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
+
+    def test_badCredentials(self):
+        """
+        Test the behavior of locateChild when SACLS are enabled, and 
+        incorrect credentials are given.
+
+        should return a 401 UnauthorizedResponse
+        """
+        self.root.resource.useSacls = True
+
+        request = SimpleRequest(
+            self.site,
+            "GET",
+            "/principals/",
+            headers=http_headers.Headers({
+                    'Authorization': ['basic', '%s' % (
+                            'dreid:dreid'.encode('base64'),)]}))
+        
+        resrc, segments = self.root.locateChild(request,
+                                         ['principals'])
+
+        def _Eb(failure):
+            self.assertEquals(failure.value.response.code, 401)
+            
+        d = defer.maybeDeferred(resrc.locateChild, request, ['principals'])
+        d.addErrback(_Eb)
+
+        return d

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20061205/b9238bdf/attachment.html


More information about the calendarserver-changes mailing list