Просмотр исходного кода

Merge pull request #11 from matthiasjordan/master

Separe authentication and authorization
Guillaume Ayoub 13 лет назад
Родитель
Сommit
eee83bb49c

+ 17 - 0
.project

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>Radicale</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.python.pydev.PyDevBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.python.pydev.pythonNature</nature>
+	</natures>
+</projectDescription>

+ 10 - 0
.pydevproject

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
+<path>/Radicale</path>
+</pydev_pathproperty>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+</pydev_project>

+ 1 - 1
.pylintrc

@@ -89,7 +89,7 @@ generated-members=
 [FORMAT]
 
 # Maximum number of characters on a single line.
-max-line-length=79
+max-line-length=80
 
 # Maximum number of lines in a module
 max-module-lines=1000

+ 4 - 0
.settings/org.eclipse.core.resources.prefs

@@ -0,0 +1,4 @@
+#Sat Aug 04 10:58:22 CEST 2012
+eclipse.preferences.version=1
+encoding//radicale/__init__.py=utf-8
+encoding//radicale/__main__.py=utf-8

+ 151 - 71
radicale/__init__.py

@@ -46,7 +46,7 @@ except ImportError:
     from urlparse import urlparse
 # pylint: enable=F0401,E0611
 
-from radicale import acl, config, ical, log, storage, xmlutils
+from radicale import config, ical, log, storage, xmlutils, access
 
 
 VERSION = "git"
@@ -119,7 +119,7 @@ class Application(object):
     def __init__(self):
         """Initialize application."""
         super(Application, self).__init__()
-        self.acl = acl.load()
+        access.load()
         storage.load()
         self.encoding = config.get("encoding", "request")
         if config.getboolean("logging", "full_environment"):
@@ -189,15 +189,17 @@ class Application(object):
         else:
             content = None
 
+        path = environ["PATH_INFO"]
+
         # Find collection(s)
         items = ical.Collection.from_path(
-            environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0"))
+            path, environ.get("HTTP_DEPTH", "0"))
 
         # Get function corresponding to method
         function = getattr(self, environ["REQUEST_METHOD"].lower())
 
         # Check rights
-        if not items or not self.acl or function == self.options:
+        if not items or not access or function == self.options:
             # No collection, or no acl, or OPTIONS request: don't check rights
             status, headers, answer = function(environ, items, content, None)
         else:
@@ -211,55 +213,58 @@ class Application(object):
             else:
                 user = password = None
 
-            last_collection_allowed = None
-            allowed_items = []
-            for item in items:
-                if not isinstance(item, ical.Collection):
-                    # item is not a colleciton, it's the child of the last
-                    # collection we've met in the loop. Only add this item if
-                    # this last collection was allowed.
-                    if last_collection_allowed:
-                        allowed_items.append(item)
-                    continue
-
-                # item is a collection
-                collection = item
-                if collection.owner in acl.PUBLIC_USERS:
-                    log.LOGGER.info("Public collection")
-                    allowed_items.append(collection)
-                    last_collection_allowed = True
+
+            if access.is_authenticated(user, password):
+
+                last_collection_allowed = None
+                allowed_items = []
+                for item in items:
+                    log.LOGGER.debug("Testing %s" % (item.name))
+                    if not isinstance(item, ical.Collection):
+                        # item is not a colleciton, it's the child of the last
+                        # collection we've met in the loop. Only add this item if
+                        # this last collection was allowed.                        log.LOGGER.info("not a collection: " + collection.name)
+                        #  collections.append(collection)
+                        if last_collection_allowed:
+                            allowed_items.append(item)
+                    else:
+                        if access.may_read(user, item) or access.may_write(user, item):
+                            log.LOGGER.info(user + "has access to " + item.name)
+                            last_collection_allowed = True
+                            allowed_items.append(item)
+                        else:
+                            last_collection_allowed = False
+
+                if allowed_items:
+                    # Collections found
+                    status, headers, answer = function(
+                        environ, allowed_items, content, user)
                 else:
-                    log.LOGGER.info(
-                        "Checking rights for collection owned by %s" % (
-                            collection.owner or "nobody"))
-                    if self.acl.has_right(collection.owner, user, password):
-                        log.LOGGER.info(
-                            "%s allowed" % (user or "Anonymous user"))
-                        allowed_items.append(collection)
-                        last_collection_allowed = True
+                    # Good user and no collections found, redirect user to home
+                    location = "/%s/" % str(quote(user))
+                    if path != location: 
+                        log.LOGGER.info("redirecting to %s" % location)
+                        status = client.FOUND
+                        headers = {"Location": location}
+                        answer = "Redirecting to %s" % location
                     else:
-                        log.LOGGER.info(
-                            "%s refused" % (user or "Anonymous user"))
-                        last_collection_allowed = False
-
-            if allowed_items:
-                # Collections and items found
-                status, headers, answer = function(
-                    environ, allowed_items, content, user)
-            elif user and last_collection_allowed is None:
-                # Good user and no collections found, redirect user to home
-                location = "/%s/" % str(quote(user))
-                log.LOGGER.info("redirecting to %s" % location)
-                status = client.FOUND
-                headers = {"Location": location}
-                answer = "Redirecting to %s" % location
+                        # Send answer anyway since else we're getting into a redirect loop
+                        status, headers, answer = function(
+                            environ, allowed_items, content, user)
+                            
             else:
+                
                 # Unknown or unauthorized user
+                log.LOGGER.info(
+                    "%s refused" % (user or "Anonymous user"))
                 status = client.UNAUTHORIZED
                 headers = {
                     "WWW-Authenticate":
                     "Basic realm=\"Radicale Server - Password Required\""}
                 answer = None
+                    
+                
+                
 
         # Set content length
         if answer:
@@ -274,6 +279,17 @@ class Application(object):
 
         # Return response content
         return [answer] if answer else []
+    
+    
+    
+    def response_not_allowed(self):
+        headers = {
+            "WWW-Authenticate":
+            "Basic realm=\"Radicale Server - Password Required\""}
+        return client.FORBIDDEN, headers, None
+
+
+
 
     # All these functions must have the same parameters, some are useless
     # pylint: disable=W0612,W0613,R0201
@@ -295,12 +311,20 @@ class Application(object):
             etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "")
             if etag == item.etag:
                 # No ETag precondition or precondition verified, delete item
-                answer = xmlutils.delete(environ["PATH_INFO"], collection)
-                return client.OK, {}, answer
+                if access.may_write(user, collection):
+                    answer = xmlutils.delete(environ["PATH_INFO"], collection)
+                    return client.OK, {}, answer
+                else:
+                    return self.response_not_allowed()
+                    
 
         # No item or ETag precondition not verified, do not delete item
         return client.PRECONDITION_FAILED, {}, None
 
+
+
+
+
     def get(self, environ, collections, content, user):
         """Manage GET request.
 
@@ -322,21 +346,28 @@ class Application(object):
             # Get collection item
             item = collection.get_item(item_name)
             if item:
-                items = collection.timezones
-                items.append(item)
-                answer_text = ical.serialize(
-                    collection.tag, collection.headers, items)
-                etag = item.etag
+                if access.may_read(user, collection):
+                    items = collection.timezones
+                    items.append(item)
+                    answer_text = ical.serialize(
+                        collection.tag, collection.headers, items)
+                    etag = item.etag
+                else:
+                    return self.response_not_allowed()                    
             else:
                 return client.GONE, {}, None
         else:
             # Create the collection if it does not exist
-            if not collection.exists:
+            if not collection.exists and access.may_write(user, collection):
+                log.LOGGER.debug("creating collection " + collection.name)
                 collection.write()
 
-            # Get whole collection
-            answer_text = collection.text
-            etag = collection.etag
+            if access.may_read(user, collection):
+                # Get whole collection
+                answer_text = collection.text
+                etag = collection.etag
+            else:
+                return self.response_not_allowed()
 
         headers = {
             "Content-Type": collection.mimetype,
@@ -345,11 +376,18 @@ class Application(object):
         answer = answer_text.encode(self.encoding)
         return client.OK, headers, answer
 
+
+
+
     def head(self, environ, collections, content, user):
         """Manage HEAD request."""
         status, headers, answer = self.get(environ, collections, content, user)
         return status, headers, None
 
+
+
+
+
     def mkcalendar(self, environ, collections, content, user):
         """Manage MKCALENDAR request."""
         collection = collections[0]
@@ -361,9 +399,15 @@ class Application(object):
         with collection.props as collection_props:
             for key, value in props.items():
                 collection_props[key] = value
-        collection.write()
+        if access.may_write(user, collection):
+            collection.write()
+        else:
+            return self.response_not_allowed()
         return client.CREATED, {}, None
 
+
+
+
     def mkcol(self, environ, collections, content, user):
         """Manage MKCOL request."""
         collection = collections[0]
@@ -371,9 +415,15 @@ class Application(object):
         with collection.props as collection_props:
             for key, value in props.items():
                 collection_props[key] = value
-        collection.write()
+        if access.may_write(user, collection):
+            collection.write()
+        else:
+            return self.response_not_allowed()
         return client.CREATED, {}, None
 
+
+
+
     def move(self, environ, collections, content, user):
         """Manage MOVE request."""
         from_collection = collections[0]
@@ -389,9 +439,12 @@ class Application(object):
                     to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
                     to_collection = ical.Collection.from_path(
                         to_path, depth="0")[0]
-                    to_collection.append(to_name, item.text)
-                    from_collection.remove(from_name)
-                    return client.CREATED, {}, None
+                    if access.may_write(user, to_collection) and access.may_write(user.from_collection):
+                        to_collection.append(to_name, item.text)
+                        from_collection.remove(from_name)
+                        return client.CREATED, {}, None
+                    else:
+                        return self.response_not_allowed()
                 else:
                     # Remote destination server, not supported
                     return client.BAD_GATEWAY, {}, None
@@ -402,6 +455,10 @@ class Application(object):
             # Moving collections, not supported
             return client.FORBIDDEN, {}, None
 
+
+
+
+
     def options(self, environ, collections, content, user):
         """Manage OPTIONS request."""
         headers = {
@@ -410,6 +467,10 @@ class Application(object):
             "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
         return client.OK, headers, None
 
+
+
+
+
     def propfind(self, environ, collections, content, user):
         """Manage PROPFIND request."""
         headers = {
@@ -419,6 +480,10 @@ class Application(object):
             environ["PATH_INFO"], content, collections, user)
         return client.MULTI_STATUS, headers, answer
 
+
+
+
+
     def proppatch(self, environ, collections, content, user):
         """Manage PROPPATCH request."""
         collection = collections[0]
@@ -428,6 +493,10 @@ class Application(object):
             "Content-Type": "text/xml"}
         return client.MULTI_STATUS, headers, answer
 
+
+
+
+
     def put(self, environ, collections, content, user):
         """Manage PUT request."""
         collection = collections[0]
@@ -444,25 +513,36 @@ class Application(object):
             # Case 1: No item and no ETag precondition: Add new item
             # Case 2: Item and ETag precondition verified: Modify item
             # Case 3: Item and no Etag precondition: Force modifying item
-            xmlutils.put(environ["PATH_INFO"], content, collection)
-            status = client.NO_CONTENT if item else client.CREATED
-            # Try to return the etag in the header
-            # If the added item does't have the same name as the one given by
-            # the client, then there's no obvious way to generate an etag, we
-            # can safely ignore it.
-            new_item = collection.get_item(item_name)
-            if new_item:
-                headers["ETag"] = new_item.etag
+            if access.may_write(user, collection):
+                xmlutils.put(environ["PATH_INFO"], content, collection)
+                status = client.CREATED
+                # Try to return the etag in the header
+                # If the added item does't have the same name as the one given by
+                # the client, then there's no obvious way to generate an etag, we
+                # can safely ignore it.
+                new_item = collection.get_item(item_name)
+                if new_item:
+                    headers["ETag"] = new_item.etag
+            else:
+                return self.response_not_allowed()
         else:
             # PUT rejected in all other cases
             status = client.PRECONDITION_FAILED
         return status, headers, None
 
+
+
+
+
+
     def report(self, environ, collections, content, user):
         """Manage REPORT request."""
         collection = collections[0]
         headers = {"Content-Type": "text/xml"}
-        answer = xmlutils.report(environ["PATH_INFO"], content, collection)
-        return client.MULTI_STATUS, headers, answer
+        if access.may_read(user, collection):
+            answer = xmlutils.report(environ["PATH_INFO"], content, collection)
+            return client.MULTI_STATUS, headers, answer
+        else:
+            return self.response_not_allowed()
 
     # pylint: enable=W0612,W0613,R0201

+ 67 - 0
radicale/access.py

@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Radicale Server - Calendar Server
+# Copyright © 2011-2012 Guillaume Ayoub
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Radicale access module.
+
+Manages access to collections.
+
+"""
+
+import os
+import sys
+
+from radicale import acl, authorization, log
+
+
+
+def load():
+    log.LOGGER.debug("access.load()")
+    global aacl ; aacl = acl.load()
+    global aauthorization ; aauthorization = authorization.load()
+
+
+
+def is_authenticated(user, password):
+    if (not user): 
+        # No user given
+        return False
+
+    return aacl.is_authenticated(user, password)
+     
+
+
+
+def may_read(user, collection):
+    """Check if the user is allowed to read the collection"""
+    
+    user_authorized = aauthorization.read_authorized(user, collection)
+
+    log.LOGGER.debug("read %s %s -- %i" % (user, collection.owner, user_authorized))
+    return user_authorized
+
+
+
+
+def may_write(user, collection):
+    """Check if the user is allowed to write the collection"""
+    
+    user_authorized = aauthorization.write_authorized(user, collection)
+    
+    log.LOGGER.debug("write %s %s -- %i" % (user, collection.owner, user_authorized))
+    return user_authorized

+ 1 - 4
radicale/acl/IMAP.py

@@ -38,11 +38,8 @@ IMAP_SERVER = config.get("acl", "imap_auth_host_name")
 IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port")
 
 
-def has_right(owner, user, password):
+def is_authenticated(user, password):
     """Check if ``user``/``password`` couple is valid."""
-    if not user or (owner not in acl.PRIVATE_USERS and user != owner):
-        # No user given, or owner is not private and is not user, forbidden
-        return False
 
     log.LOGGER.debug(
         "[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))

+ 1 - 5
radicale/acl/LDAP.py

@@ -38,14 +38,10 @@ PASSWORD = config.get("acl", "ldap_password")
 SCOPE = getattr(ldap, "SCOPE_%s" % config.get("acl", "ldap_scope").upper())
 
 
-def has_right(owner, user, password):
+def is_authenticated(user, password):
     """Check if ``user``/``password`` couple is valid."""
     global CONNEXION
 
-    if not user or (owner not in acl.PRIVATE_USERS and user != owner):
-        # No user given, or owner is not private and is not user, forbidden
-        return False
-
     try:
         CONNEXION.whoami_s()
     except:

+ 2 - 5
radicale/acl/PAM.py

@@ -33,11 +33,8 @@ from radicale import acl, config, log
 GROUP_MEMBERSHIP = config.get("acl", "pam_group_membership")
 
 
-def has_right(owner, user, password):
+def is_authenticated(user, password):
     """Check if ``user``/``password`` couple is valid."""
-    if not user or (owner not in acl.PRIVATE_USERS and user != owner):
-        # No user given, or owner is not private and is not user, forbidden
-        return False
 
     # Check whether the user exists in the PAM system
     try:
@@ -50,7 +47,7 @@ def has_right(owner, user, password):
 
     # Check whether the group exists
     try:
-        members = grp.getgrnam(GROUP_MEMBERSHIP).gr_mem
+        members = grp.getgrnam(GROUP_MEMBERSHIP)
     except KeyError:
         log.LOGGER.debug(
             "The PAM membership required group (%s) doesn't exist" %

+ 10 - 10
radicale/acl/__init__.py

@@ -19,19 +19,20 @@
 # along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-Users and rights management.
+Users management.
+
+ACL is basically the wrong name here since this package deals with authenticating users.
+
+The authorization part is done in the package "authorization".
 
 This module loads a list of users with access rights, according to the acl
 configuration.
 
 """
 
-from radicale import config
-
-
-PUBLIC_USERS = []
-PRIVATE_USERS = []
+from radicale import config, log
 
+CONFIG_PREFIX = "acl"
 
 def _config_users(name):
     """Get an iterable of strings from the configuraton string [acl] ``name``.
@@ -40,18 +41,17 @@ def _config_users(name):
     stripped at the beginning and at the end of the values.
 
     """
-    for user in config.get("acl", name).split(","):
+    for user in config.get(CONFIG_PREFIX, name).split(","):
         user = user.strip()
         yield None if user == "None" else user
 
 
 def load():
     """Load list of available ACL managers."""
-    acl_type = config.get("acl", "type")
+    acl_type = config.get(CONFIG_PREFIX, "type")
+    log.LOGGER.debug("acl_type = "  + acl_type)
     if acl_type == "None":
         return None
     else:
-        PUBLIC_USERS.extend(_config_users("public_users"))
-        PRIVATE_USERS.extend(_config_users("private_users"))
         module = __import__("acl.%s" % acl_type, globals=globals(), level=2)
         return getattr(module, acl_type)

+ 1 - 4
radicale/acl/courier.py

@@ -29,11 +29,8 @@ from radicale import acl, config, log
 COURIER_SOCKET = config.get("acl", "courier_socket")
 
 
-def has_right(owner, user, password):
+def is_authenticated(user, password):
     """Check if ``user``/``password`` couple is valid."""
-    if not user or (owner not in acl.PRIVATE_USERS and user != owner):
-        # No user given, or owner is not private and is not user, forbidden
-        return False
 
     line = "%s\nlogin\n%s\n%s" % (sys.argv[0], user, password)
     line = "AUTH %i\n%s" % (len(line), line)

+ 2 - 2
radicale/acl/htpasswd.py

@@ -58,11 +58,11 @@ def _sha1(hash_value, password):
     return sha1.digest() == base64.b64decode(hash_value)
 
 
-def has_right(owner, user, password):
+def is_authenticated(user, password):
     """Check if ``user``/``password`` couple is valid."""
     for line in open(FILENAME).readlines():
         if line.strip():
             login, hash_value = line.strip().split(":")
-            if login == user and (owner in acl.PRIVATE_USERS or owner == user):
+            if login == user:
                 return globals()["_%s" % ENCRYPTION](hash_value, password)
     return False

+ 76 - 0
radicale/authorization/__init__.py

@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Radicale Server - Calendar Server
+# Copyright © 2008-2012 Guillaume Ayoub
+# Copyright © 2008 Nicolas Kandel
+# Copyright © 2008 Pascal Halter
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Users and rights management.
+
+This module loads a list of users with access rights, according to the acl
+configuration.
+
+"""
+
+from radicale import config, log
+
+
+AUTHORIZATION_PREFIX = "authorization"
+
+PUBLIC_USERS = []
+PRIVATE_USERS = []
+
+
+
+def _config_users(name):
+    """Get an iterable of strings from the configuraton string [acl] ``name``.
+
+    The values must be separated by a comma. The whitespace characters are
+    stripped at the beginning and at the end of the values.
+
+    """
+    for user in config.get(AUTHORIZATION_PREFIX, name).split(","):
+        user = user.strip()
+        yield None if user == "None" else user
+
+
+def load():
+    """Load list of available ACL managers."""
+    
+    PUBLIC_USERS.extend(_config_users("public_users"))
+    PRIVATE_USERS.extend(_config_users("private_users"))
+    
+    authorization_type = config.get(AUTHORIZATION_PREFIX, "type")
+    log.LOGGER.debug("auth type = " + authorization_type)
+    if authorization_type == "None":
+        return None
+    else:
+        module = __import__("authorization.%s" % authorization_type, globals=globals(), level=2)
+        return getattr(module, authorization_type)
+
+
+def may_read(user, collection):
+    if (collection.owner not in PRIVATE_USERS and user != collection.owner):
+        # owner is not private and is not user, forbidden
+        return False
+
+    return read_authorized(user, collection)
+    
+def may_write(user, collection):
+    return write_authorized(user, collection)
+    
+    

+ 47 - 0
radicale/authorization/allauthenticated.py

@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Radicale Server - Calendar Server
+# Copyright © 2011-2012 Guillaume Ayoub
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Radicale authorization module.
+
+Manages who is authorized to access a collection.
+
+The policy here is that all authenticated users
+have read and write access to all collections.
+
+"""
+
+import os
+import sys
+
+from radicale import authorization, config, log
+
+
+
+
+def read_authorized(user, collection):
+    """Check if the user is allowed to read the collection"""
+    log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.name + "'");
+    return True
+
+
+def write_authorized(user, collection):
+    """Check if the user is allowed to write the collection"""
+    log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.name + "'");
+    return True
+    

+ 49 - 0
radicale/authorization/owneronly.py

@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Radicale Server - Calendar Server
+# Copyright © 2011-2012 Guillaume Ayoub
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Radicale authorization module.
+
+Manages who is authorized to access a collection.
+
+The policy here is that owners have read and write access
+to their own collections.
+
+"""
+
+import os
+import sys
+
+from radicale import authorization, config, log
+
+
+
+
+def read_authorized(user, collection):
+    """Check if the user is allowed to read the collection"""
+    log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
+    
+    return user == collection.owner
+
+
+def write_authorized(user, collection):
+    """Check if the user is allowed to write the collection"""
+    log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
+
+    return user == collection.owner
+    

+ 65 - 0
radicale/authorization/static.py

@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+#
+# This file is part of Radicale Server - Calendar Server
+# Copyright © 2011-2012 Guillaume Ayoub
+#
+# This library is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+Radicale authorization module.
+
+Manages who is authorized to access a collection.
+
+The policy is that the owner may read and write in
+all collections and some special rights are hardcoded.
+
+"""
+
+import os
+import sys
+
+from radicale import authorization, config, log
+from radicale.authorization import owneronly
+
+
+
+def read_authorized(user, collection):
+    """Check if the user is allowed to read the collection"""
+    log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
+    
+    if owneronly.read_authorized(user, collection):
+        return True
+    
+    if user == "user1" and collection.owner == "user2" and collection.name == "user2sharedwithuser1":
+        return True
+    if user == "user2" and collection.owner == "user1" and collection.name == "user1sharedwithuser2":
+        return True
+    
+    return False
+
+
+def write_authorized(user, collection):
+    """Check if the user is allowed to write the collection"""
+    log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.owner + "/" + collection.name + "'");
+
+    if owneronly.write_authorized(user, collection):
+        return True
+    
+    if user == "user1" and collection.owner == "user2" and collection.name == "user2sharedwithuser1":
+        return True
+    if user == "user2" and collection.owner == "user1" and collection.name == "user1sharedwithuser2":
+        return False
+
+    return False
+    

+ 6 - 8
radicale/config.py

@@ -53,10 +53,8 @@ INITIAL_CONFIG = {
         "type": "None",
         "public_users": "public",
         "private_users": "private",
-        "htpasswd_filename": "/etc/radicale/users",
-        "htpasswd_encryption": "crypt",
-        "imap_auth_host_name": "localhost",
-        "imap_auth_host_port": "143",
+        "httpasswd_filename": "/etc/radicale/users",
+        "httpasswd_encryption": "crypt",
         "ldap_url": "ldap://localhost:389/",
         "ldap_base": "ou=users,dc=example,dc=com",
         "ldap_attribute": "uid",
@@ -68,10 +66,10 @@ INITIAL_CONFIG = {
         "courier_socket": ""},
     "storage": {
         "type": "filesystem",
-        "filesystem_folder": os.path.expanduser(
-            "~/.config/radicale/collections"),
-        "git_folder": os.path.expanduser(
-            "~/.config/radicale/collections")},
+        "filesystem_folder":
+            os.path.expanduser("~/.config/radicale/collections"),
+        "git_folder":
+            os.path.expanduser("~/.config/radicale/collections")},
     "logging": {
         "config": "/etc/radicale/logging",
         "debug": "False",

+ 15 - 27
radicale/xmlutils.py

@@ -35,7 +35,7 @@ except ImportError:
 import re
 import xml.etree.ElementTree as ET
 
-from radicale import client, config, ical
+from radicale import client, config, ical, access
 
 
 NAMESPACES = {
@@ -200,8 +200,9 @@ def propfind(path, xml_request, collections, user=None):
     multistatus = ET.Element(_tag("D", "multistatus"))
 
     for collection in collections:
-        response = _propfind_response(path, collection, props, user)
-        multistatus.append(response)
+        if access.may_read(user, collection):
+            response = _propfind_response(path, collection, props, user)
+            multistatus.append(response)
 
     return _pretty_xml(multistatus)
 
@@ -239,10 +240,11 @@ def _propfind_response(path, item, props, user):
             tag = ET.Element(_tag("D", "href"))
             tag.text = path
             element.append(tag)
-        elif tag in (_tag("D", "principal-collection-set"),
-                     _tag("C", "calendar-user-address-set"),
-                     _tag("CR", "addressbook-home-set"),
-                     _tag("C", "calendar-home-set")):
+        elif tag in (
+            _tag("D", "principal-collection-set"),
+            _tag("C", "calendar-user-address-set"),
+            _tag("CR", "addressbook-home-set"),
+            _tag("C", "calendar-home-set")):
             tag = ET.Element(_tag("D", "href"))
             tag.text = path
             element.append(tag)
@@ -282,14 +284,12 @@ def _propfind_response(path, item, props, user):
                 if item.is_principal:
                     tag = ET.Element(_tag("D", "principal"))
                     element.append(tag)
-                if item.is_leaf(item.path) or (
-                        not item.exists and item.resource_type):
-                    # 2nd case happens when the collection is not stored yet,
-                    # but the resource type is guessed
-                    if item.resource_type == "addressbook":
-                        tag = ET.Element(_tag("CR", item.resource_type))
-                    else:
-                        tag = ET.Element(_tag("C", item.resource_type))
+                if item.is_leaf(item.path):
+                    tag = ET.Element(_tag("C", item.resource_type))
+                    element.append(tag)
+                if not item.exists and item.resource_type:
+                    # Collection not stored yet, but guessed resource type
+                    tag = ET.Element(_tag("C", item.resource_type))
                     element.append(tag)
                 tag = ET.Element(_tag("D", "collection"))
                 element.append(tag)
@@ -300,8 +300,6 @@ def _propfind_response(path, item, props, user):
             elif tag == _tag("C", "calendar-timezone"):
                 element.text = ical.serialize(
                     item.tag, item.headers, item.timezones)
-            elif tag == _tag("D", "displayname"):
-                element.text = item.name
             else:
                 human_tag = _tag_from_clark(tag)
                 if human_tag in collection_props:
@@ -432,15 +430,8 @@ def report(path, xml_request, collection):
                 in root.findall(_tag("D", "href")))
         else:
             hreferences = (path,)
-        # TODO: handle other filters
-        # TODO: handle the nested comp-filters correctly
-        # Read rfc4791-9.7.1 for info
-        tag_filters = set(
-            element.get("name") for element
-            in root.findall(".//%s" % _tag("C", "comp-filter")))
     else:
         hreferences = ()
-        tag_filters = None
 
     # Writing answer
     multistatus = ET.Element(_tag("D", "multistatus"))
@@ -463,9 +454,6 @@ def report(path, xml_request, collection):
             items = collection.components
 
         for item in items:
-            if tag_filters and item.tag not in tag_filters:
-                continue
-
             response = ET.Element(_tag("D", "response"))
             multistatus.append(response)