Merge pull request #11 from matthiasjordan/master
Separe authentication and authorization
This commit is contained in:
commit
eee83bb49c
17
.project
Normal file
17
.project
Normal 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
10
.pydevproject
Normal 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>
|
@ -89,7 +89,7 @@ generated-members=
|
||||
[FORMAT]
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=79
|
||||
max-line-length=80
|
||||
|
||||
# Maximum number of lines in a module
|
||||
max-module-lines=1000
|
||||
|
4
.settings/org.eclipse.core.resources.prefs
Normal file
4
.settings/org.eclipse.core.resources.prefs
Normal 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
|
@ -46,7 +46,7 @@ except ImportError:
|
||||
from urlparse import urlparse
|
||||
# pylint: enable=F0401,E0611
|
||||
|
||||
from radicale import acl, config, ical, log, storage, xmlutils
|
||||
from radicale import config, ical, log, storage, xmlutils, access
|
||||
|
||||
|
||||
VERSION = "git"
|
||||
@ -119,7 +119,7 @@ class Application(object):
|
||||
def __init__(self):
|
||||
"""Initialize application."""
|
||||
super(Application, self).__init__()
|
||||
self.acl = acl.load()
|
||||
access.load()
|
||||
storage.load()
|
||||
self.encoding = config.get("encoding", "request")
|
||||
if config.getboolean("logging", "full_environment"):
|
||||
@ -189,15 +189,17 @@ class Application(object):
|
||||
else:
|
||||
content = None
|
||||
|
||||
path = environ["PATH_INFO"]
|
||||
|
||||
# Find collection(s)
|
||||
items = ical.Collection.from_path(
|
||||
environ["PATH_INFO"], environ.get("HTTP_DEPTH", "0"))
|
||||
path, environ.get("HTTP_DEPTH", "0"))
|
||||
|
||||
# Get function corresponding to method
|
||||
function = getattr(self, environ["REQUEST_METHOD"].lower())
|
||||
|
||||
# Check rights
|
||||
if not items or not self.acl or function == self.options:
|
||||
if not items or not access or function == self.options:
|
||||
# No collection, or no acl, or OPTIONS request: don't check rights
|
||||
status, headers, answer = function(environ, items, content, None)
|
||||
else:
|
||||
@ -211,55 +213,58 @@ class Application(object):
|
||||
else:
|
||||
user = password = None
|
||||
|
||||
last_collection_allowed = None
|
||||
allowed_items = []
|
||||
for item in items:
|
||||
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.
|
||||
if last_collection_allowed:
|
||||
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:
|
||||
log.LOGGER.info(
|
||||
"Checking rights for collection owned by %s" % (
|
||||
collection.owner or "nobody"))
|
||||
if self.acl.has_right(collection.owner, user, password):
|
||||
log.LOGGER.info(
|
||||
"%s allowed" % (user or "Anonymous user"))
|
||||
allowed_items.append(collection)
|
||||
last_collection_allowed = True
|
||||
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)
|
||||
if last_collection_allowed:
|
||||
allowed_items.append(item)
|
||||
else:
|
||||
log.LOGGER.info(
|
||||
"%s refused" % (user or "Anonymous user"))
|
||||
last_collection_allowed = False
|
||||
if access.may_read(user, item) or access.may_write(user, item):
|
||||
log.LOGGER.info(user + "has access to " + item.name)
|
||||
last_collection_allowed = True
|
||||
allowed_items.append(item)
|
||||
else:
|
||||
last_collection_allowed = False
|
||||
|
||||
if allowed_items:
|
||||
# Collections and items found
|
||||
status, headers, answer = function(
|
||||
environ, allowed_items, content, user)
|
||||
elif user and last_collection_allowed is None:
|
||||
# Good user and no collections found, redirect user to home
|
||||
location = "/%s/" % str(quote(user))
|
||||
log.LOGGER.info("redirecting to %s" % location)
|
||||
status = client.FOUND
|
||||
headers = {"Location": location}
|
||||
answer = "Redirecting to %s" % location
|
||||
if allowed_items:
|
||||
# Collections found
|
||||
status, headers, answer = function(
|
||||
environ, allowed_items, content, user)
|
||||
else:
|
||||
# Good user and no collections found, redirect user to home
|
||||
location = "/%s/" % str(quote(user))
|
||||
if path != location:
|
||||
log.LOGGER.info("redirecting to %s" % location)
|
||||
status = client.FOUND
|
||||
headers = {"Location": location}
|
||||
answer = "Redirecting to %s" % location
|
||||
else:
|
||||
# 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"))
|
||||
status = client.UNAUTHORIZED
|
||||
headers = {
|
||||
"WWW-Authenticate":
|
||||
"Basic realm=\"Radicale Server - Password Required\""}
|
||||
answer = None
|
||||
|
||||
|
||||
|
||||
|
||||
# Set content length
|
||||
if answer:
|
||||
@ -274,6 +279,17 @@ class Application(object):
|
||||
|
||||
# Return response content
|
||||
return [answer] if answer else []
|
||||
|
||||
|
||||
|
||||
def response_not_allowed(self):
|
||||
headers = {
|
||||
"WWW-Authenticate":
|
||||
"Basic realm=\"Radicale Server - Password Required\""}
|
||||
return client.FORBIDDEN, headers, None
|
||||
|
||||
|
||||
|
||||
|
||||
# All these functions must have the same parameters, some are useless
|
||||
# pylint: disable=W0612,W0613,R0201
|
||||
@ -295,12 +311,20 @@ class Application(object):
|
||||
etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "")
|
||||
if etag == item.etag:
|
||||
# No ETag precondition or precondition verified, delete item
|
||||
answer = xmlutils.delete(environ["PATH_INFO"], collection)
|
||||
return client.OK, {}, answer
|
||||
if access.may_write(user, collection):
|
||||
answer = xmlutils.delete(environ["PATH_INFO"], collection)
|
||||
return client.OK, {}, answer
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
|
||||
|
||||
# No item or ETag precondition not verified, do not delete item
|
||||
return client.PRECONDITION_FAILED, {}, None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get(self, environ, collections, content, user):
|
||||
"""Manage GET request.
|
||||
|
||||
@ -322,21 +346,28 @@ class Application(object):
|
||||
# Get collection item
|
||||
item = collection.get_item(item_name)
|
||||
if item:
|
||||
items = collection.timezones
|
||||
items.append(item)
|
||||
answer_text = ical.serialize(
|
||||
collection.tag, collection.headers, items)
|
||||
etag = item.etag
|
||||
if access.may_read(user, collection):
|
||||
items = collection.timezones
|
||||
items.append(item)
|
||||
answer_text = ical.serialize(
|
||||
collection.tag, collection.headers, items)
|
||||
etag = item.etag
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
else:
|
||||
return client.GONE, {}, None
|
||||
else:
|
||||
# Create the collection if it does not exist
|
||||
if not collection.exists:
|
||||
if not collection.exists and access.may_write(user, collection):
|
||||
log.LOGGER.debug("creating collection " + collection.name)
|
||||
collection.write()
|
||||
|
||||
# Get whole collection
|
||||
answer_text = collection.text
|
||||
etag = collection.etag
|
||||
if access.may_read(user, collection):
|
||||
# Get whole collection
|
||||
answer_text = collection.text
|
||||
etag = collection.etag
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
|
||||
headers = {
|
||||
"Content-Type": collection.mimetype,
|
||||
@ -345,11 +376,18 @@ class Application(object):
|
||||
answer = answer_text.encode(self.encoding)
|
||||
return client.OK, headers, answer
|
||||
|
||||
|
||||
|
||||
|
||||
def head(self, environ, collections, content, user):
|
||||
"""Manage HEAD request."""
|
||||
status, headers, answer = self.get(environ, collections, content, user)
|
||||
return status, headers, None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def mkcalendar(self, environ, collections, content, user):
|
||||
"""Manage MKCALENDAR request."""
|
||||
collection = collections[0]
|
||||
@ -361,9 +399,15 @@ class Application(object):
|
||||
with collection.props as collection_props:
|
||||
for key, value in props.items():
|
||||
collection_props[key] = value
|
||||
collection.write()
|
||||
if access.may_write(user, collection):
|
||||
collection.write()
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
return client.CREATED, {}, None
|
||||
|
||||
|
||||
|
||||
|
||||
def mkcol(self, environ, collections, content, user):
|
||||
"""Manage MKCOL request."""
|
||||
collection = collections[0]
|
||||
@ -371,9 +415,15 @@ class Application(object):
|
||||
with collection.props as collection_props:
|
||||
for key, value in props.items():
|
||||
collection_props[key] = value
|
||||
collection.write()
|
||||
if access.may_write(user, collection):
|
||||
collection.write()
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
return client.CREATED, {}, None
|
||||
|
||||
|
||||
|
||||
|
||||
def move(self, environ, collections, content, user):
|
||||
"""Manage MOVE request."""
|
||||
from_collection = collections[0]
|
||||
@ -389,9 +439,12 @@ class Application(object):
|
||||
to_path, to_name = to_url.rstrip("/").rsplit("/", 1)
|
||||
to_collection = ical.Collection.from_path(
|
||||
to_path, depth="0")[0]
|
||||
to_collection.append(to_name, item.text)
|
||||
from_collection.remove(from_name)
|
||||
return client.CREATED, {}, None
|
||||
if access.may_write(user, to_collection) and access.may_write(user.from_collection):
|
||||
to_collection.append(to_name, item.text)
|
||||
from_collection.remove(from_name)
|
||||
return client.CREATED, {}, None
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
else:
|
||||
# Remote destination server, not supported
|
||||
return client.BAD_GATEWAY, {}, None
|
||||
@ -402,6 +455,10 @@ class Application(object):
|
||||
# Moving collections, not supported
|
||||
return client.FORBIDDEN, {}, None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def options(self, environ, collections, content, user):
|
||||
"""Manage OPTIONS request."""
|
||||
headers = {
|
||||
@ -410,6 +467,10 @@ class Application(object):
|
||||
"DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol"}
|
||||
return client.OK, headers, None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def propfind(self, environ, collections, content, user):
|
||||
"""Manage PROPFIND request."""
|
||||
headers = {
|
||||
@ -419,6 +480,10 @@ class Application(object):
|
||||
environ["PATH_INFO"], content, collections, user)
|
||||
return client.MULTI_STATUS, headers, answer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def proppatch(self, environ, collections, content, user):
|
||||
"""Manage PROPPATCH request."""
|
||||
collection = collections[0]
|
||||
@ -428,6 +493,10 @@ class Application(object):
|
||||
"Content-Type": "text/xml"}
|
||||
return client.MULTI_STATUS, headers, answer
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def put(self, environ, collections, content, user):
|
||||
"""Manage PUT request."""
|
||||
collection = collections[0]
|
||||
@ -444,25 +513,36 @@ class Application(object):
|
||||
# Case 1: No item and no ETag precondition: Add new item
|
||||
# Case 2: Item and ETag precondition verified: Modify item
|
||||
# Case 3: Item and no Etag precondition: Force modifying item
|
||||
xmlutils.put(environ["PATH_INFO"], content, collection)
|
||||
status = client.NO_CONTENT if item else client.CREATED
|
||||
# Try to return the etag in the header
|
||||
# If the added item does't have the same name as the one given by
|
||||
# the client, then there's no obvious way to generate an etag, we
|
||||
# can safely ignore it.
|
||||
new_item = collection.get_item(item_name)
|
||||
if new_item:
|
||||
headers["ETag"] = new_item.etag
|
||||
if access.may_write(user, collection):
|
||||
xmlutils.put(environ["PATH_INFO"], content, collection)
|
||||
status = client.CREATED
|
||||
# Try to return the etag in the header
|
||||
# If the added item does't have the same name as the one given by
|
||||
# the client, then there's no obvious way to generate an etag, we
|
||||
# can safely ignore it.
|
||||
new_item = collection.get_item(item_name)
|
||||
if new_item:
|
||||
headers["ETag"] = new_item.etag
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
else:
|
||||
# PUT rejected in all other cases
|
||||
status = client.PRECONDITION_FAILED
|
||||
return status, headers, None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def report(self, environ, collections, content, user):
|
||||
"""Manage REPORT request."""
|
||||
collection = collections[0]
|
||||
headers = {"Content-Type": "text/xml"}
|
||||
answer = xmlutils.report(environ["PATH_INFO"], content, collection)
|
||||
return client.MULTI_STATUS, headers, answer
|
||||
if access.may_read(user, collection):
|
||||
answer = xmlutils.report(environ["PATH_INFO"], content, collection)
|
||||
return client.MULTI_STATUS, headers, answer
|
||||
else:
|
||||
return self.response_not_allowed()
|
||||
|
||||
# pylint: enable=W0612,W0613,R0201
|
||||
|
67
radicale/access.py
Normal file
67
radicale/access.py
Normal 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
|
@ -38,11 +38,8 @@ IMAP_SERVER = config.get("acl", "imap_auth_host_name")
|
||||
IMAP_SERVER_PORT = config.get("acl", "imap_auth_host_port")
|
||||
|
||||
|
||||
def has_right(owner, user, password):
|
||||
def is_authenticated(user, password):
|
||||
"""Check if ``user``/``password`` couple is valid."""
|
||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
||||
# No user given, or owner is not private and is not user, forbidden
|
||||
return False
|
||||
|
||||
log.LOGGER.debug(
|
||||
"[IMAP ACL] Connecting to %s:%s." % (IMAP_SERVER, IMAP_SERVER_PORT,))
|
||||
|
@ -38,14 +38,10 @@ PASSWORD = config.get("acl", "ldap_password")
|
||||
SCOPE = getattr(ldap, "SCOPE_%s" % config.get("acl", "ldap_scope").upper())
|
||||
|
||||
|
||||
def has_right(owner, user, password):
|
||||
def is_authenticated(user, password):
|
||||
"""Check if ``user``/``password`` couple is valid."""
|
||||
global CONNEXION
|
||||
|
||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
||||
# No user given, or owner is not private and is not user, forbidden
|
||||
return False
|
||||
|
||||
try:
|
||||
CONNEXION.whoami_s()
|
||||
except:
|
||||
|
@ -33,11 +33,8 @@ from radicale import acl, config, log
|
||||
GROUP_MEMBERSHIP = config.get("acl", "pam_group_membership")
|
||||
|
||||
|
||||
def has_right(owner, user, password):
|
||||
def is_authenticated(user, password):
|
||||
"""Check if ``user``/``password`` couple is valid."""
|
||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
||||
# No user given, or owner is not private and is not user, forbidden
|
||||
return False
|
||||
|
||||
# Check whether the user exists in the PAM system
|
||||
try:
|
||||
@ -50,7 +47,7 @@ def has_right(owner, user, password):
|
||||
|
||||
# Check whether the group exists
|
||||
try:
|
||||
members = grp.getgrnam(GROUP_MEMBERSHIP).gr_mem
|
||||
members = grp.getgrnam(GROUP_MEMBERSHIP)
|
||||
except KeyError:
|
||||
log.LOGGER.debug(
|
||||
"The PAM membership required group (%s) doesn't exist" %
|
||||
|
@ -19,19 +19,20 @@
|
||||
# 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
|
||||
configuration.
|
||||
|
||||
"""
|
||||
|
||||
from radicale import config
|
||||
|
||||
|
||||
PUBLIC_USERS = []
|
||||
PRIVATE_USERS = []
|
||||
from radicale import config, log
|
||||
|
||||
CONFIG_PREFIX = "acl"
|
||||
|
||||
def _config_users(name):
|
||||
"""Get an iterable of strings from the configuraton string [acl] ``name``.
|
||||
@ -40,18 +41,17 @@ def _config_users(name):
|
||||
stripped at the beginning and at the end of the values.
|
||||
|
||||
"""
|
||||
for user in config.get("acl", name).split(","):
|
||||
for user in config.get(CONFIG_PREFIX, name).split(","):
|
||||
user = user.strip()
|
||||
yield None if user == "None" else user
|
||||
|
||||
|
||||
def load():
|
||||
"""Load list of available ACL managers."""
|
||||
acl_type = config.get("acl", "type")
|
||||
acl_type = config.get(CONFIG_PREFIX, "type")
|
||||
log.LOGGER.debug("acl_type = " + acl_type)
|
||||
if acl_type == "None":
|
||||
return None
|
||||
else:
|
||||
PUBLIC_USERS.extend(_config_users("public_users"))
|
||||
PRIVATE_USERS.extend(_config_users("private_users"))
|
||||
module = __import__("acl.%s" % acl_type, globals=globals(), level=2)
|
||||
return getattr(module, acl_type)
|
||||
|
@ -29,11 +29,8 @@ from radicale import acl, config, log
|
||||
COURIER_SOCKET = config.get("acl", "courier_socket")
|
||||
|
||||
|
||||
def has_right(owner, user, password):
|
||||
def is_authenticated(user, password):
|
||||
"""Check if ``user``/``password`` couple is valid."""
|
||||
if not user or (owner not in acl.PRIVATE_USERS and user != owner):
|
||||
# No user given, or owner is not private and is not user, forbidden
|
||||
return False
|
||||
|
||||
line = "%s\nlogin\n%s\n%s" % (sys.argv[0], user, password)
|
||||
line = "AUTH %i\n%s" % (len(line), line)
|
||||
|
@ -58,11 +58,11 @@ def _sha1(hash_value, password):
|
||||
return sha1.digest() == base64.b64decode(hash_value)
|
||||
|
||||
|
||||
def has_right(owner, user, password):
|
||||
def is_authenticated(user, password):
|
||||
"""Check if ``user``/``password`` couple is valid."""
|
||||
for line in open(FILENAME).readlines():
|
||||
if line.strip():
|
||||
login, hash_value = line.strip().split(":")
|
||||
if login == user and (owner in acl.PRIVATE_USERS or owner == user):
|
||||
if login == user:
|
||||
return globals()["_%s" % ENCRYPTION](hash_value, password)
|
||||
return False
|
||||
|
76
radicale/authorization/__init__.py
Normal file
76
radicale/authorization/__init__.py
Normal 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)
|
||||
|
||||
|
47
radicale/authorization/allauthenticated.py
Normal file
47
radicale/authorization/allauthenticated.py
Normal 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
|
||||
|
49
radicale/authorization/owneronly.py
Normal file
49
radicale/authorization/owneronly.py
Normal 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
|
||||
|
65
radicale/authorization/static.py
Normal file
65
radicale/authorization/static.py
Normal 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
|
||||
|
@ -53,10 +53,8 @@ INITIAL_CONFIG = {
|
||||
"type": "None",
|
||||
"public_users": "public",
|
||||
"private_users": "private",
|
||||
"htpasswd_filename": "/etc/radicale/users",
|
||||
"htpasswd_encryption": "crypt",
|
||||
"imap_auth_host_name": "localhost",
|
||||
"imap_auth_host_port": "143",
|
||||
"httpasswd_filename": "/etc/radicale/users",
|
||||
"httpasswd_encryption": "crypt",
|
||||
"ldap_url": "ldap://localhost:389/",
|
||||
"ldap_base": "ou=users,dc=example,dc=com",
|
||||
"ldap_attribute": "uid",
|
||||
@ -68,10 +66,10 @@ INITIAL_CONFIG = {
|
||||
"courier_socket": ""},
|
||||
"storage": {
|
||||
"type": "filesystem",
|
||||
"filesystem_folder": os.path.expanduser(
|
||||
"~/.config/radicale/collections"),
|
||||
"git_folder": os.path.expanduser(
|
||||
"~/.config/radicale/collections")},
|
||||
"filesystem_folder":
|
||||
os.path.expanduser("~/.config/radicale/collections"),
|
||||
"git_folder":
|
||||
os.path.expanduser("~/.config/radicale/collections")},
|
||||
"logging": {
|
||||
"config": "/etc/radicale/logging",
|
||||
"debug": "False",
|
||||
|
@ -35,7 +35,7 @@ except ImportError:
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from radicale import client, config, ical
|
||||
from radicale import client, config, ical, access
|
||||
|
||||
|
||||
NAMESPACES = {
|
||||
@ -200,8 +200,9 @@ def propfind(path, xml_request, collections, user=None):
|
||||
multistatus = ET.Element(_tag("D", "multistatus"))
|
||||
|
||||
for collection in collections:
|
||||
response = _propfind_response(path, collection, props, user)
|
||||
multistatus.append(response)
|
||||
if access.may_read(user, collection):
|
||||
response = _propfind_response(path, collection, props, user)
|
||||
multistatus.append(response)
|
||||
|
||||
return _pretty_xml(multistatus)
|
||||
|
||||
@ -239,10 +240,11 @@ def _propfind_response(path, item, props, user):
|
||||
tag = ET.Element(_tag("D", "href"))
|
||||
tag.text = path
|
||||
element.append(tag)
|
||||
elif tag in (_tag("D", "principal-collection-set"),
|
||||
_tag("C", "calendar-user-address-set"),
|
||||
_tag("CR", "addressbook-home-set"),
|
||||
_tag("C", "calendar-home-set")):
|
||||
elif tag in (
|
||||
_tag("D", "principal-collection-set"),
|
||||
_tag("C", "calendar-user-address-set"),
|
||||
_tag("CR", "addressbook-home-set"),
|
||||
_tag("C", "calendar-home-set")):
|
||||
tag = ET.Element(_tag("D", "href"))
|
||||
tag.text = path
|
||||
element.append(tag)
|
||||
@ -282,14 +284,12 @@ def _propfind_response(path, item, props, user):
|
||||
if item.is_principal:
|
||||
tag = ET.Element(_tag("D", "principal"))
|
||||
element.append(tag)
|
||||
if item.is_leaf(item.path) or (
|
||||
not item.exists and item.resource_type):
|
||||
# 2nd case happens when the collection is not stored yet,
|
||||
# but the resource type is guessed
|
||||
if item.resource_type == "addressbook":
|
||||
tag = ET.Element(_tag("CR", item.resource_type))
|
||||
else:
|
||||
tag = ET.Element(_tag("C", item.resource_type))
|
||||
if item.is_leaf(item.path):
|
||||
tag = ET.Element(_tag("C", item.resource_type))
|
||||
element.append(tag)
|
||||
if not item.exists and item.resource_type:
|
||||
# Collection not stored yet, but guessed resource type
|
||||
tag = ET.Element(_tag("C", item.resource_type))
|
||||
element.append(tag)
|
||||
tag = ET.Element(_tag("D", "collection"))
|
||||
element.append(tag)
|
||||
@ -300,8 +300,6 @@ def _propfind_response(path, item, props, user):
|
||||
elif tag == _tag("C", "calendar-timezone"):
|
||||
element.text = ical.serialize(
|
||||
item.tag, item.headers, item.timezones)
|
||||
elif tag == _tag("D", "displayname"):
|
||||
element.text = item.name
|
||||
else:
|
||||
human_tag = _tag_from_clark(tag)
|
||||
if human_tag in collection_props:
|
||||
@ -432,15 +430,8 @@ def report(path, xml_request, collection):
|
||||
in root.findall(_tag("D", "href")))
|
||||
else:
|
||||
hreferences = (path,)
|
||||
# TODO: handle other filters
|
||||
# TODO: handle the nested comp-filters correctly
|
||||
# Read rfc4791-9.7.1 for info
|
||||
tag_filters = set(
|
||||
element.get("name") for element
|
||||
in root.findall(".//%s" % _tag("C", "comp-filter")))
|
||||
else:
|
||||
hreferences = ()
|
||||
tag_filters = None
|
||||
|
||||
# Writing answer
|
||||
multistatus = ET.Element(_tag("D", "multistatus"))
|
||||
@ -463,9 +454,6 @@ def report(path, xml_request, collection):
|
||||
items = collection.components
|
||||
|
||||
for item in items:
|
||||
if tag_filters and item.tag not in tag_filters:
|
||||
continue
|
||||
|
||||
response = ET.Element(_tag("D", "response"))
|
||||
multistatus.append(response)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user