From 1ca8250ee0b4880b323470b8e9b8ad087509366b Mon Sep 17 00:00:00 2001 From: chrysn Date: Fri, 4 Apr 2014 12:19:44 +0200 Subject: [PATCH] pull the "current (principal) resource" out to the same level as "current collection" this allows managing privileges on principal resources, which would otherwise not have been available at all in personal mode. the implementation of _resource could be memoizing; not implementing this as i'm not sure of the lifetime of the request objects. --- calypso/__init__.py | 51 +++++++++++++++++++++++++++++++++---- calypso/principal.py | 8 ++++-- calypso/xmlutils.py | 46 +++------------------------------ calypso/xmlutils_generic.py | 11 ++++++++ 4 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 calypso/xmlutils_generic.py diff --git a/calypso/__init__.py b/calypso/__init__.py index 10723c1..4aee57c 100644 --- a/calypso/__init__.py +++ b/calypso/__init__.py @@ -43,6 +43,7 @@ import email.utils import logging import rfc822 import ssl +import functools # Manage Python2/3 different modules # pylint: disable=F0401 @@ -53,7 +54,7 @@ except ImportError: import BaseHTTPServer as server # pylint: enable=F0401 -from . import acl, config, webdav, xmlutils, paths, gssapi +from . import acl, config, webdav, xmlutils, paths, gssapi, principal log = logging.getLogger() ch = logging.StreamHandler() @@ -71,7 +72,9 @@ def _check(request, function): owner = user = password = None negotiate_success = False - if request._collection: + if request._resource: + owner = request._resource.owner + elif request._collection: owner = request._collection.owner authorization = request.headers.get("Authorization", None) @@ -88,10 +91,16 @@ def _check(request, function): ("user-agent", "x-client", "origin") if name in request.headers]) + # bound privilege checker that can be used by principals etc in discovery too + has_right = functools.partial(request.server.acl.has_right, user=user, password=password) + # Also send UNAUTHORIZED if there's no collection. Otherwise one # could probe the server for (non-)existing collections. - if request.server.acl.has_right(owner, user, password) or negotiate_success: - function(request, context={"user": user, "client_info": client_info}) + if has_right(owner) or negotiate_success: + function(request, context={ + "user": user, + "client_info": client_info, + "has_right": has_right}) else: request.send_calypso_response(client.UNAUTHORIZED, 0) if negotiate.enabled(): @@ -140,6 +149,34 @@ def collection_singleton(p): CollectionHTTPHandler.collections[path] = webdav.Collection(path) return CollectionHTTPHandler.collections[path] +def identify_resource(path): + """Return a Resource object corresponding to the path (this is used for + everything that is not a collection, like Principal and HomeSet objects)""" + + try: + left, right = config.get('server', 'user_principal').split('%(user)s') + except ValueError: + raise ValueError("user_principal setting must contain %(user)s.") + + if not path.startswith(left): + return None + + remainder = path[len(left):] + if right not in remainder: + return None + + username = remainder[:remainder.index(right)] + remainder = remainder[remainder.index(right)+len(right):] + + if remainder == principal.AddressbookHomeSet.type_dependent_suffix + "/": + return principal.AddressbookHomeSet(username) + elif remainder == principal.CalendarHomeSet.type_dependent_suffix + "/": + return principal.CalendarHomeSet(username) + elif remainder == "": + return principal.Principal(username) + else: + return None + class CollectionHTTPHandler(server.BaseHTTPRequestHandler): """HTTP requests handler for WebDAV collections.""" _encoding = config.get("encoding", "request") @@ -259,6 +296,10 @@ class CollectionHTTPHandler(server.BaseHTTPRequestHandler): """The ``webdav.Collection`` object corresponding to the given path.""" return collection_singleton(self.path) + @property + def _resource(self): + return identify_resource(self.path) + def _decode(self, text): """Try to decode text according to various parameters.""" # List of charsets to try @@ -406,7 +447,7 @@ class CollectionHTTPHandler(server.BaseHTTPRequestHandler): xml_request = self.xml_request log.debug("PROPFIND %s", xml_request) self._answer = xmlutils.propfind( - self.path, xml_request, self._collection, + self.path, xml_request, self._collection, self._resource, self.headers.get("depth", "infinity"), context) log.debug("PROPFIND ANSWER %s", self._answer) diff --git a/calypso/principal.py b/calypso/principal.py index 42447a9..d395c07 100644 --- a/calypso/principal.py +++ b/calypso/principal.py @@ -14,7 +14,7 @@ class Resource(object): """If self can respond to a propfind request on a tag, update the prepared response element with child nodes.""" - def propfind_children(self, depth): + def propfind_children(self, depth, context): """Return a list of resources / collections / items that are to be responded with to a propfind of a given depth""" return [self] @@ -26,6 +26,8 @@ class Principal(Resource): self.username = username self.urlpath = config.get("server", "user_principal") % {"user": self.username} # it's currently hardcoded anyway + owner = property(lambda self: self.username) + def propfind(self, tag, element): super(Principal, self).propfind(tag, element) @@ -46,7 +48,9 @@ class HomeSet(Resource): self.username = username self.urlpath = config.get("server", "user_principal") % {"user": self.username} + self.type_dependent_suffix + "/" # it's currently hardcoded anyway - def propfind_children(self, depth): + owner = property(lambda self: self.username) + + def propfind_children(self, depth, context): # FIXME ignoring depth collection_name = paths.collection_from_path(self.username + "/" + self.single_collection) diff --git a/calypso/xmlutils.py b/calypso/xmlutils.py index 10b39bf..7c1b821 100644 --- a/calypso/xmlutils.py +++ b/calypso/xmlutils.py @@ -39,22 +39,12 @@ import email.utils import logging from . import client, config, webdav, paths, principal +from .xmlutils_generic import _tag __package__ = 'calypso.xmlutils' -NAMESPACES = { - "C": "urn:ietf:params:xml:ns:caldav", - "A": "urn:ietf:params:xml:ns:carddav", - "D": "DAV:", - "E": "http://apple.com/ns/ical/", - "CS": "http://calendarserver.org/ns/"} - log = logging.getLogger(__name__) -def _tag(short_name, local): - """Get XML Clark notation {uri(``short_name``)}``local``.""" - return "{%s}%s" % (NAMESPACES[short_name], local) - def _response(code): """Return full W3C names from HTTP status codes.""" @@ -84,35 +74,7 @@ def delete(path, collection, context): return ET.tostring(multistatus, config.get("encoding", "request")) -def identify_resource(path): - """Return a Resource object corresponding to the path (this is used for - everything that is not a collection, like Principal and HomeSet objects)""" - - try: - left, right = config.get('server', 'user_principal').split('%(user)s') - except ValueError: - raise ValueError("user_principal setting must contain %(user)s.") - - if not path.startswith(left): - return None - - remainder = path[len(left):] - if right not in remainder: - return None - - username = remainder[:remainder.index(right)] - remainder = remainder[remainder.index(right)+len(right):] - - if remainder == principal.AddressbookHomeSet.type_dependent_suffix + "/": - return principal.AddressbookHomeSet(username) - elif remainder == principal.CalendarHomeSet.type_dependent_suffix + "/": - return principal.CalendarHomeSet(username) - elif remainder == "": - return principal.Principal(username) - else: - return None - -def propfind(path, xml_request, collection, depth, context): +def propfind(path, xml_request, collection, resource, depth, context): """Read and answer PROPFIND requests. Read rfc4918-9.1 for info. @@ -148,10 +110,8 @@ def propfind(path, xml_request, collection, depth, context): # Writing answer multistatus = ET.Element(_tag("D", "multistatus")) - resource = identify_resource(path) - if resource is not None: - items = resource.propfind_children(depth) + items = resource.propfind_children(depth, context) elif collection: if item_name: item = collection.get_item(item_name) diff --git a/calypso/xmlutils_generic.py b/calypso/xmlutils_generic.py new file mode 100644 index 0000000..f8849b8 --- /dev/null +++ b/calypso/xmlutils_generic.py @@ -0,0 +1,11 @@ +NAMESPACES = { + "C": "urn:ietf:params:xml:ns:caldav", + "A": "urn:ietf:params:xml:ns:carddav", + "D": "DAV:", + "E": "http://apple.com/ns/ical/", + "CS": "http://calendarserver.org/ns/"} + + +def _tag(short_name, local): + """Get XML Clark notation {uri(``short_name``)}``local``.""" + return "{%s}%s" % (NAMESPACES[short_name], local) -- 2.34.1