Revision: 3598 http://trac.macosforge.org/projects/calendarserver/changeset/3598 Author: wsanchez@apple.com Date: 2009-01-19 15:36:19 -0800 (Mon, 19 Jan 2009) Log Message: ----------- twistedcaldav.root -> calendarserver.provision.root twistedcaldav._sacl -> calendarserver.platform.darwin._sacl Modified Paths: -------------- CalendarServer/trunk/calendarserver/tap/caldav.py CalendarServer/trunk/setup.py Added Paths: ----------- CalendarServer/trunk/calendarserver/platform/ CalendarServer/trunk/calendarserver/platform/__init__.py CalendarServer/trunk/calendarserver/platform/darwin/ CalendarServer/trunk/calendarserver/platform/darwin/__init__.py CalendarServer/trunk/calendarserver/platform/darwin/_sacl.c CalendarServer/trunk/calendarserver/provision/__init__.py CalendarServer/trunk/calendarserver/provision/root.py CalendarServer/trunk/calendarserver/provision/test/ CalendarServer/trunk/calendarserver/provision/test/__init__.py CalendarServer/trunk/calendarserver/provision/test/test_root.py Removed Paths: ------------- CalendarServer/trunk/twistedcaldav/_sacl.c CalendarServer/trunk/twistedcaldav/root.py CalendarServer/trunk/twistedcaldav/test/test_root.py Copied: CalendarServer/trunk/calendarserver/platform/darwin/_sacl.c (from rev 3593, CalendarServer/trunk/twistedcaldav/_sacl.c) =================================================================== --- CalendarServer/trunk/calendarserver/platform/darwin/_sacl.c (rev 0) +++ CalendarServer/trunk/calendarserver/platform/darwin/_sacl.c 2009-01-19 23:36:19 UTC (rev 3598) @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2006-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. + */ + +#include "Python.h" +#include <Security/Security.h> +#include <membership.h> + +int mbr_check_service_membership(const uuid_t user, const char* servicename, int* ismember); + +/* + CheckSACL(userOrGroupName, service) + Checks user or group membership in a service. +*/ +static PyObject *appleauth_CheckSACL(PyObject *self, PyObject *args) { + char *username; + int usernameSize; + char *serviceName; + int serviceNameSize; + + // get the args + if (!PyArg_ParseTuple(args, "s#s#", &username, + &usernameSize, &serviceName, &serviceNameSize)) { + return NULL; + } + + // get a uuid for the user + uuid_t user; + int result = mbr_user_name_to_uuid(username, user); + int isMember = 0; + + if ( result != 0 ) { + // no uuid for the user, we might be a group. + result = mbr_group_name_to_uuid(username, user); + } + + if ( result != 0 ) { + return Py_BuildValue("i", (-1)); + } + + result = mbr_check_service_membership(user, serviceName, &isMember); + + if ( ( result == 0 && isMember == 1 ) || ( result == ENOENT ) ) { + // passed + return Py_BuildValue("i", 0); + } + + return Py_BuildValue("i", (-2)); +} + +/* Method definitions. */ +static struct PyMethodDef _sacl_methods[] = { + {"CheckSACL", appleauth_CheckSACL}, + {NULL, NULL} /* Sentinel */ +}; + +void init_sacl(void) { + Py_InitModule("_sacl", _sacl_methods); +} Copied: CalendarServer/trunk/calendarserver/provision/root.py (from rev 3597, CalendarServer/trunk/twistedcaldav/root.py) =================================================================== --- CalendarServer/trunk/calendarserver/provision/root.py (rev 0) +++ CalendarServer/trunk/calendarserver/provision/root.py 2009-01-19 23:36:19 UTC (rev 3598) @@ -0,0 +1,241 @@ +## +# 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. +## + +__all__ = [ + "RootACLMixIn", + "RootResource", +] + +from twisted.internet.defer import inlineCallbacks, returnValue +from twisted.cred.error import LoginFailed, UnauthorizedLogin + +from twisted.web2 import responsecode +from twisted.web2.dav import davxml +from twisted.web2.http import HTTPError, StatusResponse +from twisted.web2.auth.wrapper import UnauthorizedResponse +from twisted.web.xmlrpc import Proxy + +from twistedcaldav.extensions import DAVFile, CachingXattrPropertyStore +from twistedcaldav.extensions import DirectoryPrincipalPropertySearchMixIn +from twistedcaldav.extensions import ReadOnlyResourceMixIn +from twistedcaldav.config import config +from twistedcaldav.log import Logger +from twistedcaldav.cache import _CachedResponseResource +from twistedcaldav.cache import MemcacheResponseCache, MemcacheChangeNotifier +from twistedcaldav.cache import DisabledCache +from twistedcaldav.static import CalendarHomeFile +from twistedcaldav.directory.principal import DirectoryPrincipalResource + +log = Logger() + + +class RootResource (ReadOnlyResourceMixIn, DirectoryPrincipalPropertySearchMixIn, DAVFile): + """ + A special root resource that contains support checking SACLs + as well as adding responseFilters. + """ + + useSacls = False + saclService = "calendar" + + def __init__(self, path, *args, **kwargs): + super(RootResource, self).__init__(path, *args, **kwargs) + + if config.EnableSACLs: + if RootResource.CheckSACL: + self.useSacls = True + else: + log.msg(("RootResource.CheckSACL is unset but " + "config.EnableSACLs is True, SACLs will not be " + "turned on.")) + + self.contentFilters = [] + + if config.Memcached["ClientEnabled"]: + self.responseCache = MemcacheResponseCache(self.fp) + + CalendarHomeFile.cacheNotifierFactory = MemcacheChangeNotifier + DirectoryPrincipalResource.cacheNotifierFactory = MemcacheChangeNotifier + else: + self.responseCache = DisabledCache() + + if config.ResponseCompression: + from twisted.web2.filter import gzip + self.contentFilters.append((gzip.gzipfilter, True)) + + if not config.EnableKeepAlive: + def addConnectionClose(request, response): + response.headers.setHeader("connection", ("close",)) + request.chanRequest.channel.setReadPersistent(False) + return response + self.contentFilters.append((addConnectionClose, True)) + + + def deadProperties(self): + if not hasattr(self, "_dead_properties"): + self._dead_properties = CachingXattrPropertyStore(self) + + return self._dead_properties + + def defaultAccessControlList(self): + return config.RootResourceACL + + @inlineCallbacks + def checkSacl(self, request): + """ + Check SACLs against the current request + """ + + try: + authnUser, authzUser = yield self.authenticate(request) + except Exception: + response = (yield UnauthorizedResponse.makeResponse( + request.credentialFactories, + request.remoteAddr + )) + raise HTTPError(response) + + # 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 %r SACL" % (self.saclService,)) + response = (yield UnauthorizedResponse.makeResponse( + request.credentialFactories, + request.remoteAddr + )) + raise HTTPError(response) + + # Cache the authentication details + request.authnUser = authnUser + request.authzUser = authzUser + + # 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 + + if RootResource.CheckSACL(username, self.saclService) != 0: + log.msg("User %r is not enabled with the %r SACL" % (username, self.saclService,)) + raise HTTPError(responsecode.FORBIDDEN) + + # Mark SACLs as having been checked so we can avoid doing it multiple times + request.checkedSACL = True + + + returnValue(True) + + @inlineCallbacks + def locateChild(self, request, segments): + + for filter in self.contentFilters: + request.addResponseFilter(filter[0], atEnd=filter[1]) + + # Examine cookies for wiki auth token + wikiConfig = config.Authentication.Wiki + cookies = request.headers.getHeader("cookie") + if wikiConfig["Enabled"] and cookies is not None: + for cookie in cookies: + if cookie.name == wikiConfig["Cookie"]: + token = cookie.value + break + else: + token = None + + if token is not None and token != "unauthenticated": + log.info("Wiki sessionID cookie value: %s" % (token,)) + proxy = Proxy(wikiConfig["URL"]) + 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"): + yield self.checkSacl(request) + child = (yield super(RootResource, self).locateChild(request, segments)) + returnValue(child) + + if config.RejectClients: + # + # Filter out unsupported clients + # + agent = request.headers.getHeader("user-agent") + if agent is not None: + for reject in config.RejectClients: + if reject.search(agent) is not None: + log.info("Rejecting user-agent: %s" % (agent,)) + raise HTTPError(StatusResponse( + responsecode.FORBIDDEN, + "Your client software (%s) is not allowed to access this service." % (agent,) + )) + + 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) + + 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 + + child = (yield super(RootResource, self).locateChild(request, segments)) + returnValue(child) + + def http_COPY (self, request): return responsecode.FORBIDDEN + def http_MOVE (self, request): return responsecode.FORBIDDEN + def http_DELETE (self, request): return responsecode.FORBIDDEN + +# So CheckSACL will be parameterized +# We do this after RootResource is defined +try: + from twistedcaldav._sacl import CheckSACL + RootResource.CheckSACL = CheckSACL +except ImportError: + RootResource.CheckSACL = None Copied: CalendarServer/trunk/calendarserver/provision/test/test_root.py (from rev 3593, CalendarServer/trunk/twistedcaldav/test/test_root.py) =================================================================== --- CalendarServer/trunk/calendarserver/provision/test/test_root.py (rev 0) +++ CalendarServer/trunk/calendarserver/provision/test/test_root.py 2009-01-19 23:36:19 UTC (rev 3598) @@ -0,0 +1,246 @@ +## +# 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. +## + +import os + +from twisted.cred.portal import Portal +from twisted.internet.defer import inlineCallbacks, maybeDeferred +from twisted.web2 import http_headers +from twisted.web2 import responsecode +from twisted.web2 import server +from twisted.web2.auth import basic +from twisted.web2.dav import auth +from twisted.web2.dav import davxml +from twisted.web2.http import HTTPError +from twisted.web2.iweb import IResponse +from twisted.web2.test.test_server import SimpleRequest + +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 calendarserver.provision.root import RootResource + +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() + os.mkdir(self.docroot) + + RootResource.CheckSACL = FakeCheckSACL(sacls={ + 'calendar': ['dreid']}) + + directory = XMLDirectoryService(xmlFile) + + principals = DirectoryPrincipalProvisioningResource('/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) + + @inlineCallbacks + 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) + + resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) + + self.failUnless( + isinstance(resrc, DirectoryPrincipalProvisioningResource), + "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,) + ) + + self.assertEquals(segments, []) + + @inlineCallbacks + 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) + + resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) + + self.failUnless( + isinstance(resrc, DirectoryPrincipalProvisioningResource), + "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,) + ) + + self.assertEquals(segments, []) + + self.assertEquals(request.authzUser, + davxml.Principal( + davxml.HRef('/principals/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/'))) + + @inlineCallbacks + 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) + + try: + resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) + except HTTPError, e: + self.assertEquals(e.response.code, 403) + + @inlineCallbacks + 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) + + 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) + + @inlineCallbacks + 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) + + try: + resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) + except HTTPError, e: + self.assertEquals(e.response.code, 401) + + def test_DELETE(self): + def do_test(response): + response = IResponse(response) + + if response.code != responsecode.FORBIDDEN: + self.fail("Incorrect response for DELETE /: %s" % (response.code,)) + + request = SimpleRequest(self.site, "DELETE", "/") + return self.send(request, do_test) + + def test_COPY(self): + def do_test(response): + response = IResponse(response) + + if response.code != responsecode.FORBIDDEN: + self.fail("Incorrect response for COPY /: %s" % (response.code,)) + + request = SimpleRequest( + self.site, + "COPY", + "/", + headers=http_headers.Headers({"Destination":"/copy/"}) + ) + return self.send(request, do_test) + + def test_MOVE(self): + def do_test(response): + response = IResponse(response) + + if response.code != responsecode.FORBIDDEN: + self.fail("Incorrect response for MOVE /: %s" % (response.code,)) + + request = SimpleRequest( + self.site, + "MOVE", + "/", + headers=http_headers.Headers({"Destination":"/copy/"}) + ) + return self.send(request, do_test) Modified: CalendarServer/trunk/calendarserver/tap/caldav.py =================================================================== --- CalendarServer/trunk/calendarserver/tap/caldav.py 2009-01-19 23:23:00 UTC (rev 3597) +++ CalendarServer/trunk/calendarserver/tap/caldav.py 2009-01-19 23:36:19 UTC (rev 3598) @@ -58,7 +58,6 @@ from twistedcaldav.accesslog import AMPCommonAccessLoggingObserver from twistedcaldav.config import config, defaultConfig, defaultConfigFile from twistedcaldav.config import ConfigurationError -from twistedcaldav.root import RootResource from twistedcaldav.resource import CalDAVResource from twistedcaldav.directory.digest import QopDigestCredentialFactory from twistedcaldav.directory.principal import DirectoryPrincipalProvisioningResource @@ -83,6 +82,8 @@ except ImportError: NegotiateCredentialFactory = None +from calendarserver.provision.root import RootResource + log = Logger() Modified: CalendarServer/trunk/setup.py =================================================================== --- CalendarServer/trunk/setup.py 2009-01-19 23:23:00 UTC (rev 3597) +++ CalendarServer/trunk/setup.py 2009-01-19 23:36:19 UTC (rev 3598) @@ -100,10 +100,10 @@ if sys.platform == "darwin": extensions.append( Extension( - "twistedcaldav._sacl", + "calendarserver.platform.darwin._sacl", extra_compile_args = ["-arch", "ppc", "-arch", "i386"], extra_link_args = ["-framework", "Security", "-arch", "ppc", "-arch", "i386"], - sources = ["twistedcaldav/_sacl.c"] + sources = ["calendarserver/platform/darwin/_sacl.c"] ) ) @@ -126,6 +126,9 @@ platforms = [ "all" ], packages = [ "calendarserver", + "calendarserver.platform", + "calendarserver.platform.darwin", + "calendarserver.provision", "calendarserver.tap", "calendarserver.tap.test", "calendarserver.test", Deleted: CalendarServer/trunk/twistedcaldav/_sacl.c =================================================================== --- CalendarServer/trunk/twistedcaldav/_sacl.c 2009-01-19 23:23:00 UTC (rev 3597) +++ CalendarServer/trunk/twistedcaldav/_sacl.c 2009-01-19 23:36:19 UTC (rev 3598) @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2006-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. - */ - -#include "Python.h" -#include <Security/Security.h> -#include <membership.h> - -int mbr_check_service_membership(const uuid_t user, const char* servicename, int* ismember); - -/* - CheckSACL(userOrGroupName, service) - Checks user or group membership in a service. -*/ -static PyObject *appleauth_CheckSACL(PyObject *self, PyObject *args) { - char *username; - int usernameSize; - char *serviceName; - int serviceNameSize; - - // get the args - if (!PyArg_ParseTuple(args, "s#s#", &username, - &usernameSize, &serviceName, &serviceNameSize)) { - return NULL; - } - - // get a uuid for the user - uuid_t user; - int result = mbr_user_name_to_uuid(username, user); - int isMember = 0; - - if ( result != 0 ) { - // no uuid for the user, we might be a group. - result = mbr_group_name_to_uuid(username, user); - } - - if ( result != 0 ) { - return Py_BuildValue("i", (-1)); - } - - result = mbr_check_service_membership(user, serviceName, &isMember); - - if ( ( result == 0 && isMember == 1 ) || ( result == ENOENT ) ) { - // passed - return Py_BuildValue("i", 0); - } - - return Py_BuildValue("i", (-2)); -} - -/* Method definitions. */ -static struct PyMethodDef _sacl_methods[] = { - {"CheckSACL", appleauth_CheckSACL}, - {NULL, NULL} /* Sentinel */ -}; - -void init_sacl(void) { - Py_InitModule("_sacl", _sacl_methods); -} Deleted: CalendarServer/trunk/twistedcaldav/root.py =================================================================== --- CalendarServer/trunk/twistedcaldav/root.py 2009-01-19 23:23:00 UTC (rev 3597) +++ CalendarServer/trunk/twistedcaldav/root.py 2009-01-19 23:36:19 UTC (rev 3598) @@ -1,239 +0,0 @@ -## -# 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. -## - -__all__ = [ - "RootACLMixIn", - "RootResource", -] - -from twisted.internet.defer import succeed, inlineCallbacks, returnValue -from twisted.cred.error import LoginFailed, UnauthorizedLogin - -from twisted.web2 import responsecode -from twisted.web2.dav import davxml -from twisted.web2.http import HTTPError, StatusResponse -from twisted.web2.auth.wrapper import UnauthorizedResponse -from twisted.web.xmlrpc import Proxy - -from twistedcaldav.extensions import DAVFile, CachingXattrPropertyStore -from twistedcaldav.extensions import DirectoryPrincipalPropertySearchMixIn -from twistedcaldav.config import config -from twistedcaldav.log import Logger -from twistedcaldav.cache import _CachedResponseResource -from twistedcaldav.cache import MemcacheResponseCache, MemcacheChangeNotifier -from twistedcaldav.cache import DisabledCache -from twistedcaldav.static import CalendarHomeFile -from twistedcaldav.directory.principal import DirectoryPrincipalResource - -log = Logger() - -class RootResource (ReadOnlyResourceMixIn, DirectoryPrincipalPropertySearchMixIn, DAVFile): - """ - A special root resource that contains support checking SACLs - as well as adding responseFilters. - """ - - useSacls = False - saclService = "calendar" - - def __init__(self, path, *args, **kwargs): - super(RootResource, self).__init__(path, *args, **kwargs) - - if config.EnableSACLs: - if RootResource.CheckSACL: - self.useSacls = True - else: - log.msg(("RootResource.CheckSACL is unset but " - "config.EnableSACLs is True, SACLs will not be " - "turned on.")) - - self.contentFilters = [] - - if config.Memcached["ClientEnabled"]: - self.responseCache = MemcacheResponseCache(self.fp) - - CalendarHomeFile.cacheNotifierFactory = MemcacheChangeNotifier - DirectoryPrincipalResource.cacheNotifierFactory = MemcacheChangeNotifier - else: - self.responseCache = DisabledCache() - - if config.ResponseCompression: - from twisted.web2.filter import gzip - self.contentFilters.append((gzip.gzipfilter, True)) - - if not config.EnableKeepAlive: - def addConnectionClose(request, response): - response.headers.setHeader("connection", ("close",)) - request.chanRequest.channel.setReadPersistent(False) - return response - self.contentFilters.append((addConnectionClose, True)) - - - def deadProperties(self): - if not hasattr(self, "_dead_properties"): - self._dead_properties = CachingXattrPropertyStore(self) - - return self._dead_properties - - def defaultAccessControlList(self): - return config.RootResourceACL - - @inlineCallbacks - def checkSacl(self, request): - """ - Check SACLs against the current request - """ - - try: - authnUser, authzUser = yield self.authenticate(request) - except Exception: - response = (yield UnauthorizedResponse.makeResponse( - request.credentialFactories, - request.remoteAddr - )) - raise HTTPError(response) - - # 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 %r SACL" % (self.saclService,)) - response = (yield UnauthorizedResponse.makeResponse( - request.credentialFactories, - request.remoteAddr - )) - raise HTTPError(response) - - # Cache the authentication details - request.authnUser = authnUser - request.authzUser = authzUser - - # 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 - - if RootResource.CheckSACL(username, self.saclService) != 0: - log.msg("User %r is not enabled with the %r SACL" % (username, self.saclService,)) - raise HTTPError(responsecode.FORBIDDEN) - - # Mark SACLs as having been checked so we can avoid doing it multiple times - request.checkedSACL = True - - - returnValue(True) - - @inlineCallbacks - def locateChild(self, request, segments): - - for filter in self.contentFilters: - request.addResponseFilter(filter[0], atEnd=filter[1]) - - # Examine cookies for wiki auth token - wikiConfig = config.Authentication.Wiki - cookies = request.headers.getHeader("cookie") - if wikiConfig["Enabled"] and cookies is not None: - for cookie in cookies: - if cookie.name == wikiConfig["Cookie"]: - token = cookie.value - break - else: - token = None - - if token is not None and token != "unauthenticated": - log.info("Wiki sessionID cookie value: %s" % (token,)) - proxy = Proxy(wikiConfig["URL"]) - 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"): - yield self.checkSacl(request) - child = (yield super(RootResource, self).locateChild(request, segments)) - returnValue(child) - - if config.RejectClients: - # - # Filter out unsupported clients - # - agent = request.headers.getHeader("user-agent") - if agent is not None: - for reject in config.RejectClients: - if reject.search(agent) is not None: - log.info("Rejecting user-agent: %s" % (agent,)) - raise HTTPError(StatusResponse( - responsecode.FORBIDDEN, - "Your client software (%s) is not allowed to access this service." % (agent,) - )) - - 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) - - 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 - - child = (yield super(RootResource, self).locateChild(request, segments)) - returnValue(child) - - def http_COPY (self, request): return responsecode.FORBIDDEN - def http_MOVE (self, request): return responsecode.FORBIDDEN - def http_DELETE (self, request): return responsecode.FORBIDDEN - -# So CheckSACL will be parameterized -# We do this after RootResource is defined -try: - from twistedcaldav._sacl import CheckSACL - RootResource.CheckSACL = CheckSACL -except ImportError: - RootResource.CheckSACL = None Deleted: CalendarServer/trunk/twistedcaldav/test/test_root.py =================================================================== --- CalendarServer/trunk/twistedcaldav/test/test_root.py 2009-01-19 23:23:00 UTC (rev 3597) +++ CalendarServer/trunk/twistedcaldav/test/test_root.py 2009-01-19 23:36:19 UTC (rev 3598) @@ -1,248 +0,0 @@ -## -# 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. -## - -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.cred.portal import Portal - -from twisted.internet.defer import inlineCallbacks, maybeDeferred - -from twisted.web2 import http_headers -from twisted.web2 import responsecode -from twisted.web2 import server -from twisted.web2.auth import basic -from twisted.web2.dav import auth -from twisted.web2.dav import davxml -from twisted.web2.http import HTTPError -from twisted.web2.iweb import IResponse - -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() - os.mkdir(self.docroot) - - RootResource.CheckSACL = FakeCheckSACL(sacls={ - 'calendar': ['dreid']}) - - directory = XMLDirectoryService(xmlFile) - - principals = DirectoryPrincipalProvisioningResource('/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) - - @inlineCallbacks - 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) - - resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) - - self.failUnless( - isinstance(resrc, DirectoryPrincipalProvisioningResource), - "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,) - ) - - self.assertEquals(segments, []) - - @inlineCallbacks - 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) - - resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) - - self.failUnless( - isinstance(resrc, DirectoryPrincipalProvisioningResource), - "Did not get a DirectoryPrincipalProvisioningResource: %s" % (resrc,) - ) - - self.assertEquals(segments, []) - - self.assertEquals(request.authzUser, - davxml.Principal( - davxml.HRef('/principals/__uids__/5FF60DAD-0BDE-4508-8C77-15F0CA5C8DD1/'))) - - @inlineCallbacks - 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) - - try: - resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) - except HTTPError, e: - self.assertEquals(e.response.code, 403) - - @inlineCallbacks - 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) - - 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) - - @inlineCallbacks - 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 = (yield maybeDeferred(self.root.locateChild, request, ['principals'])) - - try: - resrc, segments = (yield maybeDeferred(resrc.locateChild, request, ['principals'])) - except HTTPError, e: - self.assertEquals(e.response.code, 401) - - def test_DELETE(self): - def do_test(response): - response = IResponse(response) - - if response.code != responsecode.FORBIDDEN: - self.fail("Incorrect response for DELETE /: %s" % (response.code,)) - - request = SimpleRequest(self.site, "DELETE", "/") - return self.send(request, do_test) - - def test_COPY(self): - def do_test(response): - response = IResponse(response) - - if response.code != responsecode.FORBIDDEN: - self.fail("Incorrect response for COPY /: %s" % (response.code,)) - - request = SimpleRequest( - self.site, - "COPY", - "/", - headers=http_headers.Headers({"Destination":"/copy/"}) - ) - return self.send(request, do_test) - - def test_MOVE(self): - def do_test(response): - response = IResponse(response) - - if response.code != responsecode.FORBIDDEN: - self.fail("Incorrect response for MOVE /: %s" % (response.code,)) - - request = SimpleRequest( - self.site, - "MOVE", - "/", - headers=http_headers.Headers({"Destination":"/copy/"}) - ) - return self.send(request, do_test)
participants (1)
-
source_changes@macosforge.org