From 17857654b0d2e91577e6a12e685746df542f6f86 Mon Sep 17 00:00:00 2001 From: Guillaume Ayoub Date: Thu, 9 Aug 2012 15:39:01 +0200 Subject: [PATCH] Use the same import mechanisms for auth, storage, rights --- radicale/__init__.py | 123 +++++++++++++++++---------------- radicale/access.py | 63 ----------------- radicale/auth/__init__.py | 18 ++++- radicale/rights/__init__.py | 28 +++++++- radicale/storage/__init__.py | 8 ++- radicale/storage/filesystem.py | 3 - radicale/xmlutils.py | 4 +- 7 files changed, 113 insertions(+), 134 deletions(-) delete mode 100644 radicale/access.py diff --git a/radicale/__init__.py b/radicale/__init__.py index 22f4db6..5bac534 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -46,11 +46,17 @@ except ImportError: from urlparse import urlparse # 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" +# Standard "not allowed" response +NOT_ALLOWED = ( + client.FORBIDDEN, + {"WWW-Authenticate": "Basic realm=\"Radicale - Password Required\""}, + None) + class HTTPServer(wsgiref.simple_server.WSGIServer, object): """HTTP server.""" @@ -119,7 +125,8 @@ class Application(object): def __init__(self): """Initialize application.""" super(Application, self).__init__() - access.load() + auth.load() + rights.load() storage.load() self.encoding = config.get("encoding", "request") if config.getboolean("logging", "full_environment"): @@ -202,20 +209,21 @@ class Application(object): authorization = environ.get("HTTP_AUTHORIZATION", None) if authorization: - auth = authorization.lstrip("Basic").strip().encode("ascii") + authorization = \ + authorization.lstrip("Basic").strip().encode("ascii") user, password = self.decode( - base64.b64decode(auth), environ).split(":") + base64.b64decode(authorization), environ).split(":") else: user = password = None if not items or function == self.options or \ - access.is_authenticated(user, password): + auth.is_authenticated(user, password): last_collection_allowed = None allowed_items = [] for item in items: if isinstance(item, ical.Collection): - if access.read_authorized(user, item) or \ - access.write_authorized(user, item): + if rights.read_authorized(user, item) or \ + rights.write_authorized(user, item): log.LOGGER.info("%s has access to collection %s" % ( user, item.name or "/")) last_collection_allowed = True @@ -277,13 +285,6 @@ class Application(object): # Return response content return [answer] if answer else [] - def response_not_allowed(self): - """Return a standard "not allowed" response.""" - headers = { - "WWW-Authenticate": - "Basic realm=\"Radicale Server - Password Required\""} - return client.FORBIDDEN, headers, None - # All these functions must have the same parameters, some are useless # pylint: disable=W0612,W0613,R0201 @@ -304,11 +305,11 @@ class Application(object): etag = environ.get("HTTP_IF_MATCH", item.etag).replace("\\", "") if etag == item.etag: # No ETag precondition or precondition verified, delete item - if access.write_authorized(user, collection): + if rights.write_authorized(user, collection): answer = xmlutils.delete(environ["PATH_INFO"], collection) return client.OK, {}, answer else: - return self.response_not_allowed() + return NOT_ALLOWED # No item or ETag precondition not verified, do not delete item return client.PRECONDITION_FAILED, {}, None @@ -334,36 +335,35 @@ class Application(object): # Get collection item item = collection.get_item(item_name) if item: - if access.read_authorized(user, collection): + if rights.read_authorized(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() + return NOT_ALLOWED else: return client.GONE, {}, None else: # Create the collection if it does not exist if not collection.exists and \ - access.write_authorized(user, collection): + rights.write_authorized(user, collection): log.LOGGER.debug("Creating collection %s" % collection.name) collection.write() - if access.read_authorized(user, collection): + if rights.read_authorized(user, collection): # Get whole collection answer_text = collection.text 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: - return self.response_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 + return NOT_ALLOWED def head(self, environ, collections, content, user): """Manage HEAD request.""" @@ -373,32 +373,32 @@ class Application(object): def mkcalendar(self, environ, collections, content, user): """Manage MKCALENDAR request.""" collection = collections[0] - props = xmlutils.props_from_request(content) - timezone = props.get("C:calendar-timezone") - if timezone: - collection.replace("", timezone) - del props["C:calendar-timezone"] - with collection.props as collection_props: - for key, value in props.items(): - collection_props[key] = value - if access.write_authorized(user, collection): - collection.write() + if rights.write_authorized(user, collection): + props = xmlutils.props_from_request(content) + timezone = props.get("C:calendar-timezone") + if timezone: + collection.replace("", timezone) + del props["C:calendar-timezone"] + with collection.props as collection_props: + for key, value in props.items(): + collection_props[key] = value + collection.write() + return client.CREATED, {}, None else: - return self.response_not_allowed() - return client.CREATED, {}, None + return NOT_ALLOWED def mkcol(self, environ, collections, content, user): """Manage MKCOL request.""" collection = collections[0] - props = xmlutils.props_from_request(content) - with collection.props as collection_props: - for key, value in props.items(): - collection_props[key] = value - if access.write_authorized(user, collection): + if rights.write_authorized(user, collection): + props = xmlutils.props_from_request(content) + with collection.props as collection_props: + for key, value in props.items(): + collection_props[key] = value collection.write() + return client.CREATED, {}, None else: - return self.response_not_allowed() - return client.CREATED, {}, None + return NOT_ALLOWED def move(self, environ, collections, content, user): """Manage MOVE request.""" @@ -415,13 +415,13 @@ class Application(object): to_path, to_name = to_url.rstrip("/").rsplit("/", 1) to_collection = ical.Collection.from_path( to_path, depth="0")[0] - if access.write_authorized(user, to_collection) and \ - access.write_authorized(user.from_collection): + if rights.write_authorized(user, to_collection) and \ + rights.write_authorized(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() + return NOT_ALLOWED else: # Remote destination server, not supported return client.BAD_GATEWAY, {}, None @@ -442,6 +442,7 @@ class Application(object): def propfind(self, environ, collections, content, user): """Manage PROPFIND request.""" + # Rights is handled by collection in xmlutils.propfind headers = { "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol", "Content-Type": "text/xml"} @@ -452,11 +453,15 @@ class Application(object): def proppatch(self, environ, collections, content, user): """Manage PROPPATCH request.""" collection = collections[0] - answer = xmlutils.proppatch(environ["PATH_INFO"], content, collection) - headers = { - "DAV": "1, 2, 3, calendar-access, addressbook, extended-mkcol", - "Content-Type": "text/xml"} - return client.MULTI_STATUS, headers, answer + if rights.write_authorized(user, collection): + answer = xmlutils.proppatch( + environ["PATH_INFO"], content, collection) + headers = { + "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): """Manage PUT request.""" @@ -474,7 +479,7 @@ class Application(object): # Case 1: No item and no ETag precondition: Add new item # Case 2: Item and ETag precondition verified: Modify item # Case 3: Item and no Etag precondition: Force modifying item - if access.write_authorized(user, collection): + if rights.write_authorized(user, collection): xmlutils.put(environ["PATH_INFO"], content, collection) status = client.CREATED # Try to return the etag in the header. @@ -485,7 +490,7 @@ class Application(object): if new_item: headers["ETag"] = new_item.etag else: - return self.response_not_allowed() + return NOT_ALLOWED else: # PUT rejected in all other cases status = client.PRECONDITION_FAILED @@ -495,10 +500,10 @@ class Application(object): """Manage REPORT request.""" collection = collections[0] 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) return client.MULTI_STATUS, headers, answer else: - return self.response_not_allowed() + return NOT_ALLOWED # pylint: enable=W0612,W0613,R0201 diff --git a/radicale/access.py b/radicale/access.py deleted file mode 100644 index 38b5df0..0000000 --- a/radicale/access.py +++ /dev/null @@ -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 . - -""" -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 diff --git a/radicale/auth/__init__.py b/radicale/auth/__init__.py index f21d122..78275e0 100644 --- a/radicale/auth/__init__.py +++ b/radicale/auth/__init__.py @@ -23,6 +23,8 @@ Authentication management. """ +import sys + from radicale import config, log @@ -33,6 +35,18 @@ def load(): if auth_type == "None": return None else: - module = __import__( + root_module = __import__( "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 diff --git a/radicale/rights/__init__.py b/radicale/rights/__init__.py index e401e25..ffb499b 100644 --- a/radicale/rights/__init__.py +++ b/radicale/rights/__init__.py @@ -23,6 +23,8 @@ Rights management. """ +import sys + from radicale import config, log @@ -33,6 +35,28 @@ def load(): if rights_type == "None": return None else: - module = __import__( + root_module = __import__( "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 diff --git a/radicale/storage/__init__.py b/radicale/storage/__init__.py index aa2b33f..e9e1cc4 100644 --- a/radicale/storage/__init__.py +++ b/radicale/storage/__init__.py @@ -24,12 +24,14 @@ configuration. """ -from radicale import config +from radicale import config, ical def load(): """Load list of available storage managers.""" storage_type = config.get("storage", "type") - module = __import__( + root_module = __import__( "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 diff --git a/radicale/storage/filesystem.py b/radicale/storage/filesystem.py index 546a32f..57c03dc 100644 --- a/radicale/storage/filesystem.py +++ b/radicale/storage/filesystem.py @@ -112,6 +112,3 @@ class Collection(ical.Collection): self._create_dirs() with open(self._props_path, "w") as prop_file: json.dump(properties, prop_file) - - -ical.Collection = Collection diff --git a/radicale/xmlutils.py b/radicale/xmlutils.py index 45af81e..16dcb2a 100644 --- a/radicale/xmlutils.py +++ b/radicale/xmlutils.py @@ -35,7 +35,7 @@ except ImportError: import re import xml.etree.ElementTree as ET -from radicale import client, config, ical, access +from radicale import client, config, ical, rights NAMESPACES = { @@ -200,7 +200,7 @@ def propfind(path, xml_request, collections, user=None): multistatus = ET.Element(_tag("D", "multistatus")) for collection in collections: - if access.read_authorized(user, collection): + if rights.read_authorized(user, collection): response = _propfind_response(path, collection, props, user) multistatus.append(response)