Merge pull request #11 from matthiasjordan/master

Separe authentication and authorization
This commit is contained in:
Guillaume Ayoub 2012-08-08 06:49:14 -07:00
commit eee83bb49c
18 changed files with 524 additions and 136 deletions

17
.project Normal file
View File

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

10
.pydevproject Normal file
View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ except ImportError:
from urlparse import urlparse from urlparse import urlparse
# pylint: enable=F0401,E0611 # pylint: enable=F0401,E0611
from radicale import acl, config, ical, log, storage, xmlutils from radicale import config, ical, log, storage, xmlutils, access
VERSION = "git" VERSION = "git"
@ -119,7 +119,7 @@ class Application(object):
def __init__(self): def __init__(self):
"""Initialize application.""" """Initialize application."""
super(Application, self).__init__() super(Application, self).__init__()
self.acl = acl.load() access.load()
storage.load() storage.load()
self.encoding = config.get("encoding", "request") self.encoding = config.get("encoding", "request")
if config.getboolean("logging", "full_environment"): if config.getboolean("logging", "full_environment"):
@ -189,15 +189,17 @@ class Application(object):
else: else:
content = None content = None
path = environ["PATH_INFO"]
# Find collection(s) # Find collection(s)
items = ical.Collection.from_path( items = ical.Collection.from_path(
environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0")) path, environ.get("HTTP_DEPTH", "0"))
# Get function corresponding to method # Get function corresponding to method
function = getattr(self, environ["REQUEST_METHOD"].lower()) function = getattr(self, environ["REQUEST_METHOD"].lower())
# Check rights # 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 # No collection, or no acl, 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:
@ -211,56 +213,59 @@ class Application(object):
else: else:
user = password = None user = password = None
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))
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 if
# this last collection was allowed. # this last collection was allowed. log.LOGGER.info("not a collection: " + collection.name)
# collections.append(collection)
if last_collection_allowed: if last_collection_allowed:
allowed_items.append(item) allowed_items.append(item)
continue
# item is a collection
collection = item
if collection.owner in acl.PUBLIC_USERS:
log.LOGGER.info("Public collection")
allowed_items.append(collection)
last_collection_allowed = True
else: else:
log.LOGGER.info( if access.may_read(user, item) or access.may_write(user, item):
"Checking rights for collection owned by %s" % ( log.LOGGER.info(user + "has access to " + item.name)
collection.owner or "nobody"))
if self.acl.has_right(collection.owner, user, password):
log.LOGGER.info(
"%s allowed" % (user or "Anonymous user"))
allowed_items.append(collection)
last_collection_allowed = True last_collection_allowed = True
allowed_items.append(item)
else: else:
log.LOGGER.info(
"%s refused" % (user or "Anonymous user"))
last_collection_allowed = False last_collection_allowed = False
if allowed_items: if allowed_items:
# Collections and items found # Collections found
status, headers, answer = function( status, headers, answer = function(
environ, allowed_items, content, user) environ, allowed_items, content, user)
elif user and last_collection_allowed is None: 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:
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: 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 # Unknown or unauthorized user
log.LOGGER.info(
"%s refused" % (user or "Anonymous user"))
status = client.UNAUTHORIZED status = client.UNAUTHORIZED
headers = { headers = {
"WWW-Authenticate": "WWW-Authenticate":
"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(
@ -275,6 +280,17 @@ class Application(object):
# Return response content # Return response content
return [answer] if answer else [] 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 # All these functions must have the same parameters, some are useless
# pylint: disable=W0612,W0613,R0201 # pylint: disable=W0612,W0613,R0201
@ -295,12 +311,20 @@ 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):
answer = xmlutils.delete(environ["PATH_INFO"], collection) answer = xmlutils.delete(environ["PATH_INFO"], collection)
return client.OK, {}, answer return client.OK, {}, answer
else:
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.
@ -322,21 +346,28 @@ 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):
items = collection.timezones items = collection.timezones
items.append(item) items.append(item)
answer_text = ical.serialize( answer_text = ical.serialize(
collection.tag, collection.headers, items) collection.tag, collection.headers, items)
etag = item.etag etag = item.etag
else:
return self.response_not_allowed()
else: else:
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: if not collection.exists and access.may_write(user, collection):
log.LOGGER.debug("creating collection " + collection.name)
collection.write() collection.write()
if access.may_read(user, collection):
# Get whole collection # Get whole collection
answer_text = collection.text answer_text = collection.text
etag = collection.etag etag = collection.etag
else:
return self.response_not_allowed()
headers = { headers = {
"Content-Type": collection.mimetype, "Content-Type": collection.mimetype,
@ -345,11 +376,18 @@ 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]
@ -361,9 +399,15 @@ 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):
collection.write() collection.write()
else:
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]
@ -371,9 +415,15 @@ 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):
collection.write() collection.write()
else:
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]
@ -389,9 +439,12 @@ 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):
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
else:
return self.response_not_allowed()
else: else:
# Remote destination server, not supported # Remote destination server, not supported
return client.BAD_GATEWAY, {}, None return client.BAD_GATEWAY, {}, None
@ -402,6 +455,10 @@ 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 = {
@ -410,6 +467,10 @@ class Application(object):
"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 = {
@ -419,6 +480,10 @@ 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]
@ -428,6 +493,10 @@ 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]
@ -444,8 +513,9 @@ 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):
xmlutils.put(environ["PATH_INFO"], content, collection) xmlutils.put(environ["PATH_INFO"], content, collection)
status = client.NO_CONTENT if item else 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 by
# the client, then there's no obvious way to generate an etag, we # the client, then there's no obvious way to generate an etag, we
@ -453,16 +523,26 @@ class Application(object):
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
else:
return self.response_not_allowed()
else: else:
# PUT rejected in all other cases # PUT rejected in all other cases
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):
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:
return self.response_not_allowed()
# pylint: enable=W0612,W0613,R0201 # pylint: enable=W0612,W0613,R0201

67
radicale/access.py Normal file
View File

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

View File

@ -38,11 +38,8 @@ IMAP_SERVER = config.get("acl", "imap_auth_host_name")
IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port") 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.""" """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( log.LOGGER.debug(
"[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,)) "[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))

View File

@ -38,14 +38,10 @@ PASSWORD = config.get("acl", "ldap_password")
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("acl", "ldap_scope").upper()) 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.""" """Check if ``user``/``password`` couple is valid."""
global CONNEXION 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: try:
CONNEXION.whoami_s() CONNEXION.whoami_s()
except: except:

View File

@ -33,11 +33,8 @@ from radicale import acl, config, log
GROUP_MEMBERSHIP = config.get("acl", "pam_group_membership") 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.""" """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 # Check whether the user exists in the PAM system
try: try:
@ -50,7 +47,7 @@ def has_right(owner, user, password):
# Check whether the group exists # Check whether the group exists
try: try:
members = grp.getgrnam(GROUP_MEMBERSHIP).gr_mem members = grp.getgrnam(GROUP_MEMBERSHIP)
except KeyError: except KeyError:
log.LOGGER.debug( log.LOGGER.debug(
"The PAM membership required group (%s) doesn't exist" % "The PAM membership required group (%s) doesn't exist" %

View File

@ -19,19 +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 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 This module loads a list of users with access rights, according to the acl
configuration. configuration.
""" """
from radicale import config from radicale import config, log
PUBLIC_USERS = []
PRIVATE_USERS = []
CONFIG_PREFIX = "acl"
def _config_users(name): def _config_users(name):
"""Get an iterable of strings from the configuraton string [acl] ``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. 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() user = user.strip()
yield None if user == "None" else user 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("acl", "type") acl_type = config.get(CONFIG_PREFIX, "type")
log.LOGGER.debug("acl_type = " + acl_type)
if acl_type == "None": if acl_type == "None":
return None return None
else: 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) module = __import__("acl.%s" % acl_type, globals=globals(), level=2)
return getattr(module, acl_type) return getattr(module, acl_type)

View File

@ -29,11 +29,8 @@ from radicale import acl, config, log
COURIER_SOCKET = config.get("acl", "courier_socket") 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.""" """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 = "%s\nlogin\n%s\n%s" % (sys.argv[0], user, password)
line = "AUTH %i\n%s" % (len(line), line) line = "AUTH %i\n%s" % (len(line), line)

View File

@ -58,11 +58,11 @@ def _sha1(hash_value, password):
return sha1.digest() == base64.b64decode(hash_value) 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.""" """Check if ``user``/``password`` couple is valid."""
for line in open(FILENAME).readlines(): for line in open(FILENAME).readlines():
if line.strip(): if line.strip():
login, hash_value = line.strip().split(":") 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 globals()["_%s" % ENCRYPTION](hash_value, password)
return False return False

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,7 @@ except ImportError:
import re import re
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from radicale import client, config, ical from radicale import client, config, ical, access
NAMESPACES = { NAMESPACES = {
@ -200,6 +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):
response = _propfind_response(path, collection, props, user) response = _propfind_response(path, collection, props, user)
multistatus.append(response) multistatus.append(response)
@ -239,7 +240,8 @@ def _propfind_response(path, item, props, user):
tag = ET.Element(_tag("D", "href")) tag = ET.Element(_tag("D", "href"))
tag.text = path tag.text = path
element.append(tag) element.append(tag)
elif tag in (_tag("D", "principal-collection-set"), elif tag in (
_tag("D", "principal-collection-set"),
_tag("C", "calendar-user-address-set"), _tag("C", "calendar-user-address-set"),
_tag("CR", "addressbook-home-set"), _tag("CR", "addressbook-home-set"),
_tag("C", "calendar-home-set")): _tag("C", "calendar-home-set")):
@ -282,13 +284,11 @@ def _propfind_response(path, item, props, user):
if item.is_principal: if item.is_principal:
tag = ET.Element(_tag("D", "principal")) tag = ET.Element(_tag("D", "principal"))
element.append(tag) element.append(tag)
if item.is_leaf(item.path) or ( if item.is_leaf(item.path):
not item.exists and item.resource_type): tag = ET.Element(_tag("C", item.resource_type))
# 2nd case happens when the collection is not stored yet, element.append(tag)
# but the resource type is guessed if not item.exists and item.resource_type:
if item.resource_type == "addressbook": # Collection not stored yet, but guessed resource type
tag = ET.Element(_tag("CR", item.resource_type))
else:
tag = ET.Element(_tag("C", item.resource_type)) tag = ET.Element(_tag("C", item.resource_type))
element.append(tag) element.append(tag)
tag = ET.Element(_tag("D", "collection")) tag = ET.Element(_tag("D", "collection"))
@ -300,8 +300,6 @@ def _propfind_response(path, item, props, user):
elif tag == _tag("C", "calendar-timezone"): elif tag == _tag("C", "calendar-timezone"):
element.text = ical.serialize( element.text = ical.serialize(
item.tag, item.headers, item.timezones) item.tag, item.headers, item.timezones)
elif tag == _tag("D", "displayname"):
element.text = item.name
else: else:
human_tag = _tag_from_clark(tag) human_tag = _tag_from_clark(tag)
if human_tag in collection_props: if human_tag in collection_props:
@ -432,15 +430,8 @@ def report(path, xml_request, collection):
in root.findall(_tag("D", "href"))) in root.findall(_tag("D", "href")))
else: else:
hreferences = (path,) 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: else:
hreferences = () hreferences = ()
tag_filters = None
# Writing answer # Writing answer
multistatus = ET.Element(_tag("D", "multistatus")) multistatus = ET.Element(_tag("D", "multistatus"))
@ -463,9 +454,6 @@ def report(path, xml_request, collection):
items = collection.components items = collection.components
for item in items: for item in items:
if tag_filters and item.tag not in tag_filters:
continue
response = ET.Element(_tag("D", "response")) response = ET.Element(_tag("D", "response"))
multistatus.append(response) multistatus.append(response)