Code cleaned and modules renamed

*Radicale is probably broken now*
This commit is contained in:
Guillaume Ayoub 2012-08-08 18:29:09 +02:00
parent a17ad1b6a3
commit 45afac5353
17 changed files with 131 additions and 360 deletions

10
config
View File

@ -36,8 +36,8 @@ request = utf-8
stock = utf-8
[acl]
# Access method
[auth]
# Authentication method
# Value: None | htpasswd | LDAP | PAM | courier
type = None
@ -78,6 +78,12 @@ pam_group_membership =
courier_socket =
[rights]
# Rights management method
# Value: None | owner_only
type = None
[storage]
# Storage backend
type = filesystem

View File

@ -200,7 +200,7 @@ class Application(object):
# Check rights
if not items or not access or function == self.options:
# No collection, or no acl, or OPTIONS request: don't check rights
# No collection, or no auth, or OPTIONS request: don't check rights
status, headers, answer = function(environ, items, content, None)
else:
# Ask authentication backend to check rights
@ -213,23 +213,22 @@ class Application(object):
else:
user = password = None
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)
# 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)
else:
if access.may_read(user, item) or access.may_write(user, item):
log.LOGGER.info(user + "has access to " + item.name)
if access.read_authorized(user, item) or \
access.write_authorized(user, item):
log.LOGGER.info("%s has access to %s" % (
user, item.name))
last_collection_allowed = True
allowed_items.append(item)
else:
@ -242,18 +241,17 @@ class Application(object):
else:
# Good user and no collections found, redirect user to home
location = "/%s/" % str(quote(user))
if path != location:
if path == location:
# Send answer anyway since else we're getting into a
# redirect loop
status, headers, answer = function(
environ, allowed_items, content, user)
else:
log.LOGGER.info("redirecting to %s" % location)
status = client.FOUND
headers = {"Location": location}
answer = "Redirecting to %s" % location
else:
# 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"))
@ -263,9 +261,6 @@ class Application(object):
"Basic realm=\"Radicale Server - Password Required\""}
answer = None
# Set content length
if answer:
log.LOGGER.debug(
@ -280,17 +275,13 @@ class Application(object):
# Return response content
return [answer] if answer else []
def response_not_allowed(self):
"""Return a standard "not allowed" response."""
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
@ -311,20 +302,15 @@ class Application(object):
etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "")
if etag == item.etag:
# No ETag precondition or precondition verified, delete item
if access.may_write(user, collection):
if access.write_authorized(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.
@ -346,7 +332,7 @@ class Application(object):
# Get collection item
item = collection.get_item(item_name)
if item:
if access.may_read(user, collection):
if access.read_authorized(user, collection):
items = collection.timezones
items.append(item)
answer_text = ical.serialize(
@ -358,11 +344,11 @@ class Application(object):
return client.GONE, {}, None
else:
# Create the collection if it does not exist
if not collection.exists and access.may_write(user, collection):
if not collection.exists and access.write_authorized(user, collection):
log.LOGGER.debug("creating collection " + collection.name)
collection.write()
if access.may_read(user, collection):
if access.read_authorized(user, collection):
# Get whole collection
answer_text = collection.text
etag = collection.etag
@ -376,18 +362,11 @@ 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]
@ -399,15 +378,12 @@ class Application(object):
with collection.props as collection_props:
for key, value in props.items():
collection_props[key] = value
if access.may_write(user, collection):
if access.write_authorized(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]
@ -415,15 +391,12 @@ class Application(object):
with collection.props as collection_props:
for key, value in props.items():
collection_props[key] = value
if access.may_write(user, collection):
if access.write_authorized(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]
@ -439,7 +412,8 @@ class Application(object):
to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
to_collection = ical.Collection.from_path(
to_path, depth="0")[0]
if access.may_write(user, to_collection) and access.may_write(user.from_collection):
if access.write_authorized(user, to_collection) and \
access.write_authorized(user.from_collection):
to_collection.append(to_name, item.text)
from_collection.remove(from_name)
return client.CREATED, {}, None
@ -455,22 +429,14 @@ class Application(object):
# Moving collections, not supported
return client.FORBIDDEN, {}, None
def options(self, environ, collections, content, user):
"""Manage OPTIONS request."""
headers = {
"Allow": "DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " \
"OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT",
"Allow": ("DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, "
"OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT"),
"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 = {
@ -480,10 +446,6 @@ 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]
@ -493,10 +455,6 @@ 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]
@ -513,13 +471,13 @@ 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
if access.may_write(user, collection):
if access.write_authorized(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.
# 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
@ -530,16 +488,11 @@ class Application(object):
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"}
if access.may_read(user, collection):
if access.read_authorized(user, collection):
answer = xmlutils.report(environ["PATH_INFO"], content, collection)
return client.MULTI_STATUS, headers, answer
else:

View File

@ -19,49 +19,43 @@
"""
Radicale access module.
Manages access to collections.
Manage access to collections.
"""
import os
import sys
from radicale import acl, authorization, log
from radicale import auth, rights, log
AUTH = None
RIGHTS = None
def load():
log.LOGGER.debug("access.load()")
global aacl ; aacl = acl.load()
global aauthorization ; aauthorization = authorization.load()
"""Load authentication and rights modules."""
global AUTH, RIGHTS
AUTH = auth.load()
RIGHTS = rights.load()
def is_authenticated(user, password):
if (not user):
# No user given
return False
return aacl.is_authenticated(user, password)
"""Check if the user is authenticated."""
return AUTH.is_authenticated(user, password) if user else False
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))
def read_authorized(user, collection):
"""Check if the user is allowed to read the collection."""
if RIGHTS is None:
return True
user_authorized = RIGHTS.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))
def write_authorized(user, collection):
"""Check if the user is allowed to write the collection."""
if RIGHTS is None:
return True
user_authorized = RIGHTS.write_authorized(user, collection)
log.LOGGER.debug(
"Write %s %s -- %i" % (user, collection.owner, user_authorized))
return user_authorized

View File

@ -17,7 +17,7 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
IMAP ACL.
IMAP authentication.
Secure authentication based on the ``imaplib`` module.
@ -32,17 +32,17 @@ Python 3.2 or newer is required for TLS.
import imaplib
from radicale import acl, config, log
from radicale import config, log
IMAP_SERVER = config.get("acl", "imap_auth_host_name")
IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port")
IMAP_SERVER = config.get("auth", "imap_auth_host_name")
IMAP_SERVER_PORT = config.get("auth", "imap_auth_host_port")
def is_authenticated(user, password):
"""Check if ``user``/``password`` couple is valid."""
log.LOGGER.debug(
"[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
"[IMAP AUTH] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
connection = imaplib.IMAP4(host=IMAP_SERVER, port=IMAP_SERVER_PORT)
server_is_local = (IMAP_SERVER == "localhost")
@ -50,20 +50,20 @@ def is_authenticated(user, password):
connection_is_secure = False
try:
connection.starttls()
log.LOGGER.debug("[IMAP ACL] Server connection changed to TLS.")
log.LOGGER.debug("IMAP server connection changed to TLS.")
connection_is_secure = True
except AttributeError:
if not server_is_local:
log.LOGGER.error(
"[IMAP ACL] Python 3.2 or newer is required for TLS.")
"Python 3.2 or newer is required for IMAP + TLS.")
except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
log.LOGGER.warning(
"[IMAP ACL] Server at %s failed to accept TLS connection "
"IMAP server at %s failed to accept TLS connection "
"because of: %s" % (IMAP_SERVER, exception))
if server_is_local and not connection_is_secure:
log.LOGGER.warning(
"[IMAP ACL] Server is local. "
"IMAP server is local. "
"Will allow transmitting unencrypted credentials.")
if connection_is_secure or server_is_local:
@ -71,16 +71,16 @@ def is_authenticated(user, password):
connection.login(user, password)
connection.logout()
log.LOGGER.debug(
"[IMAP ACL] Authenticated user %s "
"Authenticated IMAP user %s "
"via %s." % (user, IMAP_SERVER))
return True
except (imaplib.IMAP4.error, imaplib.IMAP4.abort) as exception:
log.LOGGER.error(
"[IMAP ACL] Server could not authenticate user %s "
"IMAP server could not authenticate user %s "
"because of: %s" % (user, exception))
else:
log.LOGGER.critical(
"[IMAP ACL] Server did not support TLS and is not ``localhost``. "
"IMAP server did not support TLS and is not ``localhost``. "
"Refusing to transmit passwords under these conditions. "
"Authentication attempt aborted.")
return False # authentication failed

View File

@ -18,7 +18,7 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
LDAP ACL.
LDAP authentication.
Authentication based on the ``python-ldap`` module
(http://www.python-ldap.org/).
@ -26,16 +26,16 @@ Authentication based on the ``python-ldap`` module
"""
import ldap
from radicale import acl, config, log
from radicale import config, log
BASE = config.get("acl", "ldap_base")
ATTRIBUTE = config.get("acl", "ldap_attribute")
FILTER = config.get("acl", "ldap_filter")
CONNEXION = ldap.initialize(config.get("acl", "ldap_url"))
BINDDN = config.get("acl", "ldap_binddn")
PASSWORD = config.get("acl", "ldap_password")
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("acl", "ldap_scope").upper())
BASE = config.get("auth", "ldap_base")
ATTRIBUTE = config.get("auth", "ldap_attribute")
FILTER = config.get("auth", "ldap_filter")
CONNEXION = ldap.initialize(config.get("auth", "ldap_url"))
BINDDN = config.get("auth", "ldap_binddn")
PASSWORD = config.get("auth", "ldap_password")
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("auth", "ldap_scope").upper())
def is_authenticated(user, password):
@ -46,7 +46,7 @@ def is_authenticated(user, password):
CONNEXION.whoami_s()
except:
log.LOGGER.debug("Reconnecting the LDAP server")
CONNEXION = ldap.initialize(config.get("acl", "ldap_url"))
CONNEXION = ldap.initialize(config.get("auth", "ldap_url"))
if BINDDN and PASSWORD:
log.LOGGER.debug("Initial LDAP bind as %s" % BINDDN)

View File

@ -17,7 +17,7 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
PAM ACL.
PAM authentication.
Authentication based on the ``pam-python`` module.
@ -27,10 +27,10 @@ import grp
import pam
import pwd
from radicale import acl, config, log
from radicale import config, log
GROUP_MEMBERSHIP = config.get("acl", "pam_group_membership")
GROUP_MEMBERSHIP = config.get("auth", "pam_group_membership")
def is_authenticated(user, password):

View File

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
# Copyright © 2011-2012 Guillaume Ayoub
# 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
@ -17,31 +19,20 @@
# 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.
Authentication management.
"""
import os
import sys
from radicale import authorization, config, log
from radicale import 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
def load():
"""Load list of available authentication managers."""
auth_type = config.get("auth", "type")
log.LOGGER.debug("Authentication type is %s" % auth_type)
if auth_type == "None":
return None
else:
module = __import__(
"auth.%s" % auth_type, globals=globals(), level=2)
return getattr(module, auth_type)

View File

@ -17,16 +17,17 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
Courier-Authdaemon ACL.
Courier-Authdaemon authentication.
"""
import sys
import socket
from radicale import acl, config, log
from radicale import config, log
COURIER_SOCKET = config.get("acl", "courier_socket")
COURIER_SOCKET = config.get("auth", "courier_socket")
def is_authenticated(user, password):

View File

@ -19,7 +19,7 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
Htpasswd ACL.
Htpasswd authentication.
Load the list of login/password couples according a the configuration file
created by Apache ``htpasswd`` command. Plain-text, crypt and sha1 are
@ -30,11 +30,11 @@ supported, but md5 is not (see ``htpasswd`` man page to understand why).
import base64
import hashlib
from radicale import acl, config
from radicale import config
FILENAME = config.get("acl", "htpasswd_filename")
ENCRYPTION = config.get("acl", "htpasswd_encryption")
FILENAME = config.get("auth", "htpasswd_filename")
ENCRYPTION = config.get("auth", "htpasswd_encryption")
def _plain(hash_value, password):

View File

@ -1,76 +0,0 @@
# -*- 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)

View File

@ -1,65 +0,0 @@
# -*- 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

View File

@ -49,7 +49,7 @@ INITIAL_CONFIG = {
"encoding": {
"request": "utf-8",
"stock": "utf-8"},
"acl": {
"auth": {
"type": "None",
"public_users": "public",
"private_users": "private",

View File

@ -19,39 +19,20 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
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.
Rights management.
"""
from radicale import config, log
CONFIG_PREFIX = "acl"
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(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(CONFIG_PREFIX, "type")
log.LOGGER.debug("acl_type = " + acl_type)
if acl_type == "None":
rights_type = config.get("rights", "type")
log.LOGGER.debug("Rights type is %s" % rights_type)
if rights_type == "None":
return None
else:
module = __import__("acl.%s" % acl_type, globals=globals(), level=2)
return getattr(module, acl_type)
module = __import__(
"rights.%s" % rights_type, globals=globals(), level=2)
return getattr(module, rights_type)

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# This file is part of Radicale Server - Calendar Server
# Copyright © 2011-2012 Guillaume Ayoub
# Copyright © 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
@ -17,33 +17,18 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
"""
Radicale authorization module.
Owner-only based rights.
Manages who is authorized to access a collection.
The policy here is that owners have read and write access
to their own collections.
Only 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 + "'");
"""Check if the user is allowed to read the collection."""
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 + "'");
"""Check if the user is allowed to write the collection."""
return user == collection.owner

View File

@ -200,7 +200,7 @@ def propfind(path, xml_request, collections, user=None):
multistatus = ET.Element(_tag("D", "multistatus"))
for collection in collections:
if access.may_read(user, collection):
if access.read_authorized(user, collection):
response = _propfind_response(path, collection, props, user)
multistatus.append(response)

View File

@ -51,11 +51,12 @@ setup(
author="Guillaume Ayoub",
author_email="guillaume.ayoub@kozea.fr",
url="http://www.radicale.org/",
download_url="http://pypi.python.org/packages/source/R/Radicale/" \
"Radicale-%s.tar.gz" % radicale.VERSION,
download_url=("http://pypi.python.org/packages/source/R/Radicale/"
"Radicale-%s.tar.gz" % radicale.VERSION),
license="GNU GPL v3",
platforms="Any",
packages=["radicale", "radicale.acl", "radicale.storage"],
packages=[
"radicale", "radicale.auth", "radicale.rights", "radicale.storage"],
provides=["radicale"],
scripts=["bin/radicale"],
keywords=["calendar", "addressbook", "CalDAV", "CardDAV"],