Use the same import mechanisms for auth, storage, rights

This commit is contained in:
Guillaume Ayoub 2012-08-09 15:39:01 +02:00
parent 3ddbb80674
commit 17857654b0
7 changed files with 113 additions and 134 deletions

View File

@ -46,11 +46,17 @@ except ImportError:
from urlparse import urlparse from urlparse import urlparse
# pylint: enable=F0401,E0611 # pylint: enable=F0401,E0611
from radicale import access, config, ical, log, storage, xmlutils from radicale import auth, config, ical, log, rights, storage, xmlutils
VERSION = "git" VERSION = "git"
# Standard "not allowed" response
NOT_ALLOWED = (
client.FORBIDDEN,
{"WWW-Authenticate": "Basic realm=\"Radicale - Password Required\""},
None)
class HTTPServer(wsgiref.simple_server.WSGIServer, object): class HTTPServer(wsgiref.simple_server.WSGIServer, object):
"""HTTP server.""" """HTTP server."""
@ -119,7 +125,8 @@ class Application(object):
def __init__(self): def __init__(self):
"""Initialize application.""" """Initialize application."""
super(Application, self).__init__() super(Application, self).__init__()
access.load() auth.load()
rights.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"):
@ -202,20 +209,21 @@ class Application(object):
authorization = environ.get("HTTP_AUTHORIZATION", None) authorization = environ.get("HTTP_AUTHORIZATION", None)
if authorization: if authorization:
auth = authorization.lstrip("Basic").strip().encode("ascii") authorization = \
authorization.lstrip("Basic").strip().encode("ascii")
user, password = self.decode( user, password = self.decode(
base64.b64decode(auth), environ).split(":") base64.b64decode(authorization), environ).split(":")
else: else:
user = password = None user = password = None
if not items or function == self.options or \ if not items or function == self.options or \
access.is_authenticated(user, password): auth.is_authenticated(user, password):
last_collection_allowed = None last_collection_allowed = None
allowed_items = [] allowed_items = []
for item in items: for item in items:
if isinstance(item, ical.Collection): if isinstance(item, ical.Collection):
if access.read_authorized(user, item) or \ if rights.read_authorized(user, item) or \
access.write_authorized(user, item): rights.write_authorized(user, item):
log.LOGGER.info("%s has access to collection %s" % ( log.LOGGER.info("%s has access to collection %s" % (
user, item.name or "/")) user, item.name or "/"))
last_collection_allowed = True last_collection_allowed = True
@ -277,13 +285,6 @@ class Application(object):
# Return response content # Return response content
return [answer] if answer else [] return [answer] if answer else []
def response_not_allowed(self):
"""Return a standard "not allowed" response."""
headers = {
"WWW-Authenticate":
"Basic realm=\"Radicale Server - Password Required\""}
return client.FORBIDDEN, headers, None
# All these functions must have the same parameters, some are useless # All these functions must have the same parameters, some are useless
# pylint: disable=W0612,W0613,R0201 # pylint: disable=W0612,W0613,R0201
@ -304,11 +305,11 @@ 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.write_authorized(user, collection): if rights.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 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
@ -334,36 +335,35 @@ 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.read_authorized(user, collection): if rights.read_authorized(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: else:
return self.response_not_allowed() return 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 and \ if not collection.exists and \
access.write_authorized(user, collection): rights.write_authorized(user, collection):
log.LOGGER.debug("Creating collection %s" % collection.name) log.LOGGER.debug("Creating collection %s" % collection.name)
collection.write() collection.write()
if access.read_authorized(user, collection): if rights.read_authorized(user, collection):
# Get whole collection # Get whole collection
answer_text = collection.text answer_text = collection.text
etag = collection.etag etag = collection.etag
headers = {
"Content-Type": collection.mimetype,
"Last-Modified": collection.last_modified,
"ETag": etag}
answer = answer_text.encode(self.encoding)
return client.OK, headers, answer
else: else:
return self.response_not_allowed() return NOT_ALLOWED
headers = {
"Content-Type": collection.mimetype,
"Last-Modified": collection.last_modified,
"ETag": etag}
answer = answer_text.encode(self.encoding)
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."""
@ -373,32 +373,32 @@ class Application(object):
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]
props = xmlutils.props_from_request(content) if rights.write_authorized(user, collection):
timezone = props.get("C:calendar-timezone") props = xmlutils.props_from_request(content)
if timezone: timezone = props.get("C:calendar-timezone")
collection.replace("", timezone) if timezone:
del props["C:calendar-timezone"] collection.replace("", timezone)
with collection.props as collection_props: del props["C:calendar-timezone"]
for key, value in props.items(): with collection.props as collection_props:
collection_props[key] = value for key, value in props.items():
if access.write_authorized(user, collection): collection_props[key] = value
collection.write() collection.write()
return client.CREATED, {}, None
else: else:
return self.response_not_allowed() return NOT_ALLOWED
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]
props = xmlutils.props_from_request(content) if rights.write_authorized(user, collection):
with collection.props as collection_props: props = xmlutils.props_from_request(content)
for key, value in props.items(): with collection.props as collection_props:
collection_props[key] = value for key, value in props.items():
if access.write_authorized(user, collection): collection_props[key] = value
collection.write() collection.write()
return client.CREATED, {}, None
else: else:
return self.response_not_allowed() return NOT_ALLOWED
return client.CREATED, {}, None
def move(self, environ, collections, content, user): def move(self, environ, collections, content, user):
"""Manage MOVE request.""" """Manage MOVE request."""
@ -415,13 +415,13 @@ 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.write_authorized(user, to_collection) and \ if rights.write_authorized(user, to_collection) and \
access.write_authorized(user.from_collection): rights.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
else: else:
return self.response_not_allowed() return NOT_ALLOWED
else: else:
# Remote destination server, not supported # Remote destination server, not supported
return client.BAD_GATEWAY, {}, None return client.BAD_GATEWAY, {}, None
@ -442,6 +442,7 @@ class Application(object):
def propfind(self, environ, collections, content, user): def propfind(self, environ, collections, content, user):
"""Manage PROPFIND request.""" """Manage PROPFIND request."""
# Rights is handled by collection in xmlutils.propfind
headers = { headers = {
"DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol", "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
"Content-Type": "text/xml"} "Content-Type": "text/xml"}
@ -452,11 +453,15 @@ class Application(object):
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]
answer = xmlutils.proppatch(environ["PATH_INFO"], content, collection) if rights.write_authorized(user, collection):
headers = { answer = xmlutils.proppatch(
"DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol", environ["PATH_INFO"], content, collection)
"Content-Type": "text/xml"} headers = {
return client.MULTI_STATUS, headers, answer "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol",
"Content-Type": "text/xml"}
return client.MULTI_STATUS, headers, answer
else:
return NOT_ALLOWED
def put(self, environ, collections, content, user): def put(self, environ, collections, content, user):
"""Manage PUT request.""" """Manage PUT request."""
@ -474,7 +479,7 @@ 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.write_authorized(user, collection): if rights.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.
@ -485,7 +490,7 @@ class Application(object):
if new_item: if new_item:
headers["ETag"] = new_item.etag headers["ETag"] = new_item.etag
else: else:
return self.response_not_allowed() return NOT_ALLOWED
else: else:
# PUT rejected in all other cases # PUT rejected in all other cases
status = client.PRECONDITION_FAILED status = client.PRECONDITION_FAILED
@ -495,10 +500,10 @@ class Application(object):
"""Manage REPORT request.""" """Manage REPORT request."""
collection = collections[0] collection = collections[0]
headers = {"Content-Type": "text/xml"} headers = {"Content-Type": "text/xml"}
if access.read_authorized(user, collection): if rights.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:
return self.response_not_allowed() return NOT_ALLOWED
# pylint: enable=W0612,W0613,R0201 # pylint: enable=W0612,W0613,R0201

View File

@ -1,63 +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 access module.
Manage access to collections.
"""
from radicale import auth, rights, log
AUTH = None
RIGHTS = None
def load():
"""Load authentication and rights modules."""
global AUTH, RIGHTS
AUTH = auth.load()
RIGHTS = rights.load()
def is_authenticated(user, password):
"""Check if the user is authenticated."""
if AUTH is None:
return True
return AUTH.is_authenticated(user, password) if user else False
def read_authorized(user, collection):
"""Check if the user is allowed to read the collection."""
if RIGHTS is None:
return True
user_authorized = RIGHTS.read_authorized(user, collection)
log.LOGGER.debug(
"Read %s %s -- %i" % (user, collection.owner, user_authorized))
return user_authorized
def write_authorized(user, collection):
"""Check if the user is allowed to write the collection."""
if RIGHTS is None:
return True
user_authorized = RIGHTS.write_authorized(user, collection)
log.LOGGER.debug(
"Write %s %s -- %i" % (user, collection.owner, user_authorized))
return user_authorized

View File

@ -23,6 +23,8 @@ Authentication management.
""" """
import sys
from radicale import config, log from radicale import config, log
@ -33,6 +35,18 @@ def load():
if auth_type == "None": if auth_type == "None":
return None return None
else: else:
module = __import__( root_module = __import__(
"auth.%s" % auth_type, globals=globals(), level=2) "auth.%s" % auth_type, globals=globals(), level=2)
return getattr(module, auth_type) module = getattr(root_module, auth_type)
# Override auth.is_authenticated
sys.modules[__name__].is_authenticated = module.is_authenticated
return module
def is_authenticated(user, password):
"""Check if the user is authenticated.
This method is overriden if an auth module is loaded.
"""
return True # Default is always True: no authentication

View File

@ -23,6 +23,8 @@ Rights management.
""" """
import sys
from radicale import config, log from radicale import config, log
@ -33,6 +35,28 @@ def load():
if rights_type == "None": if rights_type == "None":
return None return None
else: else:
module = __import__( root_module = __import__(
"rights.%s" % rights_type, globals=globals(), level=2) "rights.%s" % rights_type, globals=globals(), level=2)
return getattr(module, rights_type) module = getattr(root_module, rights_type)
# Override rights.[read|write]_authorized
sys.modules[__name__].read_authorized = module.read_authorized
sys.modules[__name__].write_authorized = module.write_authorized
return module
def read_authorized(user, collection):
"""Check if the user is allowed to read the collection.
This method is overriden if an auth module is loaded.
"""
return True
def write_authorized(user, collection):
"""Check if the user is allowed to write the collection.
This method is overriden if an auth module is loaded.
"""
return True

View File

@ -24,12 +24,14 @@ configuration.
""" """
from radicale import config from radicale import config, ical
def load(): def load():
"""Load list of available storage managers.""" """Load list of available storage managers."""
storage_type = config.get("storage", "type") storage_type = config.get("storage", "type")
module = __import__( root_module = __import__(
"storage.%s" % storage_type, globals=globals(), level=2) "storage.%s" % storage_type, globals=globals(), level=2)
return getattr(module, storage_type) module = getattr(root_module, storage_type)
ical.Collection = module.Collection
return module

View File

@ -112,6 +112,3 @@ class Collection(ical.Collection):
self._create_dirs() self._create_dirs()
with open(self._props_path, "w") as prop_file: with open(self._props_path, "w") as prop_file:
json.dump(properties, prop_file) json.dump(properties, prop_file)
ical.Collection = Collection

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, access from radicale import client, config, ical, rights
NAMESPACES = { NAMESPACES = {
@ -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.read_authorized(user, collection): if rights.read_authorized(user, collection):
response = _propfind_response(path, collection, props, user) response = _propfind_response(path, collection, props, user)
multistatus.append(response) multistatus.append(response)