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

View File

@ -200,7 +200,7 @@ class Application(object):
# Check rights # Check rights
if not items or not access 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 # No collection, or no auth, or OPTIONS request: don't check rights
status, headers, answer = function(environ, items, content, None) status, headers, answer = function(environ, items, content, None)
else: else:
# Ask authentication backend to check rights # Ask authentication backend to check rights
@ -213,23 +213,22 @@ class Application(object):
else: else:
user = password = None user = password = None
if access.is_authenticated(user, password): if access.is_authenticated(user, password):
last_collection_allowed = None last_collection_allowed = None
allowed_items = [] allowed_items = []
for item in items: for item in items:
log.LOGGER.debug("Testing %s" % (item.name)) log.LOGGER.debug("Testing %s" % (item.name))
if not isinstance(item, ical.Collection): if not isinstance(item, ical.Collection):
# item is not a colleciton, it's the child of the last # item is not a colleciton, it's the child of the last
# collection we've met in the loop. Only add this item if # collection we've met in the loop. Only add this item
# this last collection was allowed. log.LOGGER.info("not a collection: " + collection.name) # if this last collection was allowed.
# collections.append(collection)
if last_collection_allowed: if last_collection_allowed:
allowed_items.append(item) allowed_items.append(item)
else: else:
if access.may_read(user, item) or access.may_write(user, item): if access.read_authorized(user, item) or \
log.LOGGER.info(user + "has access to " + item.name) access.write_authorized(user, item):
log.LOGGER.info("%s has access to %s" % (
user, item.name))
last_collection_allowed = True last_collection_allowed = True
allowed_items.append(item) allowed_items.append(item)
else: else:
@ -242,18 +241,17 @@ class Application(object):
else: else:
# Good user and no collections found, redirect user to home # Good user and no collections found, redirect user to home
location = "/%s/" % str(quote(user)) 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) log.LOGGER.info("redirecting to %s" % location)
status = client.FOUND status = client.FOUND
headers = {"Location": location} headers = {"Location": location}
answer = "Redirecting to %s" % 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: else:
# Unknown or unauthorized user # Unknown or unauthorized user
log.LOGGER.info( log.LOGGER.info(
"%s refused" % (user or "Anonymous user")) "%s refused" % (user or "Anonymous user"))
@ -263,9 +261,6 @@ class Application(object):
"Basic realm=\"Radicale Server - Password Required\""} "Basic realm=\"Radicale Server - Password Required\""}
answer = None answer = None
# Set content length # Set content length
if answer: if answer:
log.LOGGER.debug( log.LOGGER.debug(
@ -280,17 +275,13 @@ class Application(object):
# Return response content # Return response content
return [answer] if answer else [] return [answer] if answer else []
def response_not_allowed(self): def response_not_allowed(self):
"""Return a standard "not allowed" response."""
headers = { headers = {
"WWW-Authenticate": "WWW-Authenticate":
"Basic realm=\"Radicale Server - Password Required\""} "Basic realm=\"Radicale Server - Password Required\""}
return client.FORBIDDEN, headers, None return client.FORBIDDEN, headers, None
# All these functions must have the same parameters, some are useless # All these functions must have the same parameters, some are useless
# pylint: disable=W0612,W0613,R0201 # pylint: disable=W0612,W0613,R0201
@ -311,20 +302,15 @@ class Application(object):
etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "") etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "")
if etag == item.etag: if etag == item.etag:
# No ETag precondition or precondition verified, delete item # 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) answer = xmlutils.delete(environ["PATH_INFO"], collection)
return client.OK, {}, answer return client.OK, {}, answer
else: else:
return self.response_not_allowed() return self.response_not_allowed()
# No item or ETag precondition not verified, do not delete item # No item or ETag precondition not verified, do not delete item
return client.PRECONDITION_FAILED, {}, None return client.PRECONDITION_FAILED, {}, None
def get(self, environ, collections, content, user): def get(self, environ, collections, content, user):
"""Manage GET request. """Manage GET request.
@ -346,7 +332,7 @@ class Application(object):
# Get collection item # Get collection item
item = collection.get_item(item_name) item = collection.get_item(item_name)
if item: if item:
if access.may_read(user, collection): if access.read_authorized(user, collection):
items = collection.timezones items = collection.timezones
items.append(item) items.append(item)
answer_text = ical.serialize( answer_text = ical.serialize(
@ -358,11 +344,11 @@ class Application(object):
return client.GONE, {}, None return client.GONE, {}, None
else: else:
# Create the collection if it does not exist # 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) log.LOGGER.debug("creating collection " + collection.name)
collection.write() collection.write()
if access.may_read(user, collection): if access.read_authorized(user, collection):
# Get whole collection # Get whole collection
answer_text = collection.text answer_text = collection.text
etag = collection.etag etag = collection.etag
@ -376,18 +362,11 @@ class Application(object):
answer = answer_text.encode(self.encoding) answer = answer_text.encode(self.encoding)
return client.OK, headers, answer return client.OK, headers, answer
def head(self, environ, collections, content, user): def head(self, environ, collections, content, user):
"""Manage HEAD request.""" """Manage HEAD request."""
status, headers, answer = self.get(environ, collections, content, user) status, headers, answer = self.get(environ, collections, content, user)
return status, headers, None return status, headers, None
def mkcalendar(self, environ, collections, content, user): def mkcalendar(self, environ, collections, content, user):
"""Manage MKCALENDAR request.""" """Manage MKCALENDAR request."""
collection = collections[0] collection = collections[0]
@ -399,15 +378,12 @@ class Application(object):
with collection.props as collection_props: with collection.props as collection_props:
for key, value in props.items(): for key, value in props.items():
collection_props[key] = value collection_props[key] = value
if access.may_write(user, collection): if access.write_authorized(user, collection):
collection.write() collection.write()
else: else:
return self.response_not_allowed() return self.response_not_allowed()
return client.CREATED, {}, None return client.CREATED, {}, None
def mkcol(self, environ, collections, content, user): def mkcol(self, environ, collections, content, user):
"""Manage MKCOL request.""" """Manage MKCOL request."""
collection = collections[0] collection = collections[0]
@ -415,15 +391,12 @@ class Application(object):
with collection.props as collection_props: with collection.props as collection_props:
for key, value in props.items(): for key, value in props.items():
collection_props[key] = value collection_props[key] = value
if access.may_write(user, collection): if access.write_authorized(user, collection):
collection.write() collection.write()
else: else:
return self.response_not_allowed() return self.response_not_allowed()
return client.CREATED, {}, None return client.CREATED, {}, None
def move(self, environ, collections, content, user): def move(self, environ, collections, content, user):
"""Manage MOVE request.""" """Manage MOVE request."""
from_collection = collections[0] from_collection = collections[0]
@ -439,7 +412,8 @@ class Application(object):
to_path, to_name = to_url.rstrip("/").rsplit("/", 1) to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
to_collection = ical.Collection.from_path( to_collection = ical.Collection.from_path(
to_path, depth="0")[0] 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) to_collection.append(to_name, item.text)
from_collection.remove(from_name) from_collection.remove(from_name)
return client.CREATED, {}, None return client.CREATED, {}, None
@ -455,22 +429,14 @@ class Application(object):
# Moving collections, not supported # Moving collections, not supported
return client.FORBIDDEN, {}, None return client.FORBIDDEN, {}, None
def options(self, environ, collections, content, user): def options(self, environ, collections, content, user):
"""Manage OPTIONS request.""" """Manage OPTIONS request."""
headers = { headers = {
"Allow": "DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " \ "Allow": ("DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, "
"OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT", "OPTIONS, PROPFIND, PROPPATCH, PUT, REPORT"),
"DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"} "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
return client.OK, headers, None return client.OK, headers, None
def propfind(self, environ, collections, content, user): def propfind(self, environ, collections, content, user):
"""Manage PROPFIND request.""" """Manage PROPFIND request."""
headers = { headers = {
@ -480,10 +446,6 @@ class Application(object):
environ["PATH_INFO"], content, collections, user) environ["PATH_INFO"], content, collections, user)
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
def proppatch(self, environ, collections, content, user): def proppatch(self, environ, collections, content, user):
"""Manage PROPPATCH request.""" """Manage PROPPATCH request."""
collection = collections[0] collection = collections[0]
@ -493,10 +455,6 @@ class Application(object):
"Content-Type": "text/xml"} "Content-Type": "text/xml"}
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
def put(self, environ, collections, content, user): def put(self, environ, collections, content, user):
"""Manage PUT request.""" """Manage PUT request."""
collection = collections[0] collection = collections[0]
@ -513,13 +471,13 @@ class Application(object):
# Case 1: No item and no ETag precondition: Add new item # Case 1: No item and no ETag precondition: Add new item
# Case 2: Item and ETag precondition verified: Modify item # Case 2: Item and ETag precondition verified: Modify item
# Case 3: Item and no Etag precondition: Force modifying 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) xmlutils.put(environ["PATH_INFO"], content, collection)
status = client.CREATED status = client.CREATED
# Try to return the etag in the header # Try to return the etag in the header.
# If the added item does't have the same name as the one given by # If the added item does't have the same name as the one given
# the client, then there's no obvious way to generate an etag, we # by the client, then there's no obvious way to generate an
# can safely ignore it. # etag, we can safely ignore it.
new_item = collection.get_item(item_name) new_item = collection.get_item(item_name)
if new_item: if new_item:
headers["ETag"] = new_item.etag headers["ETag"] = new_item.etag
@ -530,16 +488,11 @@ class Application(object):
status = client.PRECONDITION_FAILED status = client.PRECONDITION_FAILED
return status, headers, None return status, headers, None
def report(self, environ, collections, content, user): def report(self, environ, collections, content, user):
"""Manage REPORT request.""" """Manage REPORT request."""
collection = collections[0] collection = collections[0]
headers = {"Content-Type": "text/xml"} 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) answer = xmlutils.report(environ["PATH_INFO"], content, collection)
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
else: else:

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # 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 # 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 # 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/>. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
""" """
Radicale authorization module. Authentication management.
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 from radicale import config, log
import sys
from radicale import authorization, config, log
def load():
"""Load list of available authentication managers."""
def read_authorized(user, collection): auth_type = config.get("auth", "type")
"""Check if the user is allowed to read the collection""" log.LOGGER.debug("Authentication type is %s" % auth_type)
log.LOGGER.debug("read_authorized '" + user + "' in '" + collection.name + "'"); if auth_type == "None":
return True return None
else:
module = __import__(
def write_authorized(user, collection): "auth.%s" % auth_type, globals=globals(), level=2)
"""Check if the user is allowed to write the collection""" return getattr(module, auth_type)
log.LOGGER.debug("write_authorized '" + user + "' in '" + collection.name + "'");
return True

View File

@ -17,16 +17,17 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
""" """
Courier-Authdaemon ACL. Courier-Authdaemon authentication.
""" """
import sys import sys
import socket 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): def is_authenticated(user, password):

View File

@ -19,7 +19,7 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>. # 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 Load the list of login/password couples according a the configuration file
created by Apache ``htpasswd`` command. Plain-text, crypt and sha1 are 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 base64
import hashlib import hashlib
from radicale import acl, config from radicale import config
FILENAME = config.get("acl", "htpasswd_filename") FILENAME = config.get("auth", "htpasswd_filename")
ENCRYPTION = config.get("acl", "htpasswd_encryption") ENCRYPTION = config.get("auth", "htpasswd_encryption")
def _plain(hash_value, password): 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": { "encoding": {
"request": "utf-8", "request": "utf-8",
"stock": "utf-8"}, "stock": "utf-8"},
"acl": { "auth": {
"type": "None", "type": "None",
"public_users": "public", "public_users": "public",
"private_users": "private", "private_users": "private",

View File

@ -345,7 +345,7 @@ class Collection(object):
items = self.items items = self.items
for new_item in self._parse( for new_item in self._parse(
text, (Timezone, Event, Todo, Journal, Card), name): text, (Timezone, Event, Todo, Journal, Card), name):
if new_item.name not in (item.name for item in items): if new_item.name not in (item.name for item in items):
items.append(new_item) items.append(new_item)

View File

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

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# This file is part of Radicale Server - Calendar Server # 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 # 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 # 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/>. # 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. Only owners have read and write access to their own collections.
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): def read_authorized(user, collection):
"""Check if the user is allowed to read the 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 return user == collection.owner
def write_authorized(user, collection): def write_authorized(user, collection):
"""Check if the user is allowed to write the 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 return user == collection.owner

View File

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

View File

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