From e40e68b52873fb6c121d358d5fe40c217454b72f Mon Sep 17 00:00:00 2001 From: Matthias Jordan Date: Fri, 3 Aug 2012 13:10:20 +0200 Subject: [PATCH] Separation of authentication and authorization. Separation of read and write authorization. Static test strategies for authentication. Barely tested. Use at your own risk! --- radicale/__init__.py | 206 ++++++++++++++------- radicale/access.py | 67 +++++++ radicale/acl/IMAP.py | 5 +- radicale/acl/LDAP.py | 6 +- radicale/acl/PAM.py | 7 +- radicale/acl/__init__.py | 20 +- radicale/acl/courier.py | 19 +- radicale/acl/htpasswd.py | 4 +- radicale/authorization/__init__.py | 76 ++++++++ radicale/authorization/allauthenticated.py | 47 +++++ radicale/authorization/owneronly.py | 49 +++++ radicale/authorization/static.py | 65 +++++++ radicale/config.py | 6 +- radicale/xmlutils.py | 33 +--- 14 files changed, 478 insertions(+), 132 deletions(-) create mode 100644 radicale/access.py create mode 100644 radicale/authorization/__init__.py create mode 100644 radicale/authorization/allauthenticated.py create mode 100644 radicale/authorization/owneronly.py create mode 100644 radicale/authorization/static.py diff --git a/radicale/__init__.py b/radicale/__init__.py index 47c649a..7a24859 100644 --- a/radicale/__init__.py +++ b/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,50 +213,49 @@ class Application(object): else: user = password = None - last_allowed = None - collections = [] - for collection in items: - if not isinstance(collection, ical.Collection): - if last_allowed: - collections.append(collection) - continue - if collection.owner in acl.PUBLIC_USERS: - log.LOGGER.info("Public collection") - collections.append(collection) - last_allowed = True + if access.is_authenticated(user, password): + + collections = [] + for collection in items: + log.LOGGER.debug("Testing %s" % (collection.name)) + if not isinstance(collection, ical.Collection): + log.LOGGER.info("not a collection: " + collection.name) + # collections.append(collection) + elif access.may_read(user, collection) or access.may_write(user, collection): + log.LOGGER.info("Has access to " + collection.name) + collections.append(collection) + + if collections: + # Collections found + status, headers, answer = function( + environ, collections, 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")) - collections.append(collection) - last_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_allowed = False - - if collections: - # Collections found - status, headers, answer = function( - environ, collections, content, user) - elif user and last_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, collections, 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: @@ -269,6 +270,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 @@ -290,12 +302,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. @@ -317,21 +337,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, @@ -340,11 +367,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] @@ -356,9 +390,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] @@ -366,9 +406,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] @@ -384,9 +430,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 @@ -397,6 +446,10 @@ class Application(object): # Moving collections, not supported return client.FORBIDDEN, {}, None + + + + def options(self, environ, collections, content, user): """Manage OPTIONS request.""" headers = { @@ -405,6 +458,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 = { @@ -414,6 +471,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] @@ -423,6 +484,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] @@ -439,25 +504,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 diff --git a/radicale/access.py b/radicale/access.py new file mode 100644 index 0000000..bde7706 --- /dev/null +++ b/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 . + +""" +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 diff --git a/radicale/acl/IMAP.py b/radicale/acl/IMAP.py index dd50111..f9d9718 100644 --- a/radicale/acl/IMAP.py +++ b/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,)) diff --git a/radicale/acl/LDAP.py b/radicale/acl/LDAP.py index 2366827..9e77d17 100644 --- a/radicale/acl/LDAP.py +++ b/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: diff --git a/radicale/acl/PAM.py b/radicale/acl/PAM.py index d9c84e1..3f6a199 100644 --- a/radicale/acl/PAM.py +++ b/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" % diff --git a/radicale/acl/__init__.py b/radicale/acl/__init__.py index e38adce..8be22e4 100644 --- a/radicale/acl/__init__.py +++ b/radicale/acl/__init__.py @@ -19,19 +19,20 @@ # along with Radicale. If not, see . """ -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) diff --git a/radicale/acl/courier.py b/radicale/acl/courier.py index 1b1926f..21241c1 100644 --- a/radicale/acl/courier.py +++ b/radicale/acl/courier.py @@ -29,14 +29,11 @@ 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) + line = "%i\n%s" % (len(line), line) try: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.connect(COURIER_SOCKET) @@ -51,13 +48,7 @@ def has_right(owner, user, password): log.LOGGER.debug("Got Courier socket response: %r" % data) - # Address, HOME, GID, and either UID or USERNAME are mandatory in resposne - # see http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto - for line in data.split(): - if 'GID' in line: - return True + if repr(data) == "FAIL": + return False - # default is reject - # this alleviates the problem of a possibly empty reply from courier authlib - # see http://www.courier-mta.org/authlib/README_authlib.html#authpipeproto - return False + return True diff --git a/radicale/acl/htpasswd.py b/radicale/acl/htpasswd.py index abc76a2..4ccccd3 100644 --- a/radicale/acl/htpasswd.py +++ b/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 diff --git a/radicale/authorization/__init__.py b/radicale/authorization/__init__.py new file mode 100644 index 0000000..8f05533 --- /dev/null +++ b/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 . + +""" +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) + + \ No newline at end of file diff --git a/radicale/authorization/allauthenticated.py b/radicale/authorization/allauthenticated.py new file mode 100644 index 0000000..cf991de --- /dev/null +++ b/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 . + +""" +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 + diff --git a/radicale/authorization/owneronly.py b/radicale/authorization/owneronly.py new file mode 100644 index 0000000..d95edb5 --- /dev/null +++ b/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 . + +""" +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 + diff --git a/radicale/authorization/static.py b/radicale/authorization/static.py new file mode 100644 index 0000000..a821b5a --- /dev/null +++ b/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 . + +""" +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 + diff --git a/radicale/config.py b/radicale/config.py index d667771..faa492e 100644 --- a/radicale/config.py +++ b/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", diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index 3399002..d72aa49 100644 --- a/radicale/xmlutils.py +++ b/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) @@ -283,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) @@ -301,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: @@ -434,15 +431,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")) @@ -465,9 +455,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)