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)