pull the "current (principal) resource" out to the same level as "current collection"
authorchrysn <chrysn@fsfe.org>
Fri, 4 Apr 2014 10:19:44 +0000 (12:19 +0200)
committerJelmer Vernooń≥ <jelmer@jelmer.uk>
Sun, 10 Apr 2016 11:43:29 +0000 (11:43 +0000)
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
calypso/principal.py
calypso/xmlutils.py
calypso/xmlutils_generic.py [new file with mode: 0644]

index 10723c1..4aee57c 100644 (file)
@@ -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)
index 42447a9..d395c07 100644 (file)
@@ -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)
index 10b39bf..7c1b821 100644 (file)
@@ -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 (file)
index 0000000..f8849b8
--- /dev/null
@@ -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)