Checking rights only once. Also taking care of mistakenly checking

ownership of events. xmlutils is now unaware of rights.
This commit is contained in:
Matthias Jordan 2012-08-15 22:36:42 +02:00
parent 0722db04fb
commit db708a0853
2 changed files with 151 additions and 123 deletions

View File

@ -181,27 +181,51 @@ class Application(object):
""" Collect those items from the request that the user """ Collect those items from the request that the user
is actually allowed to access """ is actually allowed to access """
last_collection_allowed = None read_last_collection_allowed = None
allowed_items = [] write_last_collection_allowed = None
read_allowed_items = []
write_allowed_items = []
for item in items: for item in items:
if isinstance(item, ical.Collection): if isinstance(item, ical.Collection):
if rights.read_authorized(user, item) or rights.write_authorized(user, item): if rights.read_authorized(user, item):
log.LOGGER.info("%s has access to collection %s" % (user, item.url or "/")) log.LOGGER.info("%s has read access to collection %s" % (user, item.url or "/"))
last_collection_allowed = True read_last_collection_allowed = True
allowed_items.append(item) read_allowed_items.append(item)
else: else:
log.LOGGER.info("%s has NO access to collection %s" % (user, item.url or "/")) log.LOGGER.info("%s has NO read access to collection %s" % (user, item.url or "/"))
last_collection_allowed = False read_last_collection_allowed = False
if rights.write_authorized(user, item):
log.LOGGER.info("%s has write access to collection %s" % (user, item.url or "/"))
write_last_collection_allowed = True
write_allowed_items.append(item)
else:
log.LOGGER.info("%s has NO write access to collection %s" % (user, item.url or "/"))
write_last_collection_allowed = False
# item is not a collection, it's the child of the last # item is not a collection, it's the child of the last
# collection we've met in the loop. Only add this item # collection we've met in the loop. Only add this item
# if this last collection was allowed. # if this last collection was allowed.
elif last_collection_allowed:
log.LOGGER.info("%s has access to item %s" % (user, item.name or "/"))
allowed_items.append(item)
else: else:
if read_last_collection_allowed:
log.LOGGER.info("%s has read access to item %s" % (user, item.name or "/"))
read_allowed_items.append(item)
if write_last_collection_allowed:
log.LOGGER.info("%s has write access to item %s" % (user, item.name or "/"))
write_allowed_items.append(item)
if (not write_last_collection_allowed) and (not read_last_collection_allowed):
log.LOGGER.info("%s has NO access to item %s" % (user, item.name or "/")) log.LOGGER.info("%s has NO access to item %s" % (user, item.name or "/"))
return allowed_items return read_allowed_items, write_allowed_items
def _union(self, list1, list2):
out = []
out.extend(list1)
for thing in list2:
if not thing in list1:
list1.append(thing)
return out
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
@ -247,25 +271,15 @@ class Application(object):
if not items or function == self.options or \ if not items or function == self.options or \
auth.is_authenticated(user, password): auth.is_authenticated(user, password):
allowed_items = self.collect_allowed_items(items, user) read_allowed_items, write_allowed_items = self.collect_allowed_items(items, user)
if allowed_items or function == self.options: if read_allowed_items or write_allowed_items or function == self.options:
# Collections found # Collections found
status, headers, answer = function( status, headers, answer = function(
environ, allowed_items, content, user) environ, read_allowed_items, write_allowed_items, content, user)
else: else:
# Good user and no collections found, redirect user to home # Good user but has no rights to any of the given collections
location = "/%s/" % str(quote(user)) status, headers, answer = NOT_ALLOWED
if path == location:
# Send answer anyway since else we're getting into a
# redirect loop
status, headers, answer = function(
environ, allowed_items, content, user)
else:
log.LOGGER.info("redirecting to %s" % location)
status = client.FOUND
headers = {"Location": location}
answer = "Redirecting to %s" % location
else: else:
# Unknown or unauthorized user # Unknown or unauthorized user
log.LOGGER.info( log.LOGGER.info(
@ -293,9 +307,12 @@ class Application(object):
# 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
def delete(self, environ, collections, content, user): def delete(self, environ, read_collections, write_collections, content, user):
"""Manage DELETE request.""" """Manage DELETE request."""
collection = collections[0] if not len(write_collections):
return NOT_ALLOWED
collection = write_collections[0]
if collection.path == environ["PATH_INFO"].strip("/"): if collection.path == environ["PATH_INFO"].strip("/"):
# Path matching the collection, the collection must be deleted # Path matching the collection, the collection must be deleted
@ -310,16 +327,13 @@ 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 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:
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
def get(self, environ, collections, content, user): def get(self, environ, read_collections, write_collections, content, user):
"""Manage GET request. """Manage GET request.
In Radicale, GET requests create collections when the URL is not In Radicale, GET requests create collections when the URL is not
@ -333,36 +347,37 @@ class Application(object):
answer = b"<!DOCTYPE html>\n<title>Radicale</title>Radicale works!" answer = b"<!DOCTYPE html>\n<title>Radicale</title>Radicale works!"
return client.OK, headers, answer return client.OK, headers, answer
collection = collections[0] if not len(read_collections):
return NOT_ALLOWED
collection = read_collections[0]
item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection) item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
if item_name: if item_name:
# Get collection item # Get collection item
item = collection.get_item(item_name) item = collection.get_item(item_name)
if item: if item:
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:
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:
rights.write_authorized(user, collection): if collection in write_collections:
log.LOGGER.debug("Creating collection %s" % collection.name) log.LOGGER.debug("Creating collection %s" % collection.name)
collection.write() collection.write()
else:
log.LOGGER.debug("Collection %s not available and could not be created due to missing write rights" % collection.name)
return NOT_ALLOWED
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
else:
return NOT_ALLOWED
headers = { headers = {
"Content-Type": collection.mimetype, "Content-Type": collection.mimetype,
@ -371,15 +386,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, read_collections, write_collections, content, user):
"""Manage HEAD request.""" """Manage HEAD request."""
status, headers, answer = self.get(environ, collections, content, user) status, headers, answer = self.get(environ, read_collections, write_collections, content, user)
return status, headers, None return status, headers, None
def mkcalendar(self, environ, collections, content, user): def mkcalendar(self, environ, read_collections, write_collections, content, user):
"""Manage MKCALENDAR request.""" """Manage MKCALENDAR request."""
collection = collections[0] if not len(write_collections):
if rights.write_authorized(user, collection): return NOT_ALLOWED
collection = write_collections[0]
props = xmlutils.props_from_request(content) props = xmlutils.props_from_request(content)
timezone = props.get("C:calendar-timezone") timezone = props.get("C:calendar-timezone")
if timezone: if timezone:
@ -390,25 +408,28 @@ class Application(object):
collection_props[key] = value collection_props[key] = value
collection.write() collection.write()
return client.CREATED, {}, None return client.CREATED, {}, None
else:
def mkcol(self, environ, read_collections, write_collections, content, user):
"""Manage MKCOL request."""
if not len(write_collections):
return NOT_ALLOWED return NOT_ALLOWED
def mkcol(self, environ, collections, content, user): collection = write_collections[0]
"""Manage MKCOL request."""
collection = collections[0]
if rights.write_authorized(user, collection):
props = xmlutils.props_from_request(content) props = xmlutils.props_from_request(content)
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
collection.write() collection.write()
return client.CREATED, {}, None return client.CREATED, {}, None
else:
def move(self, environ, read_collections, write_collections, content, user):
"""Manage MOVE request."""
if not len(write_collections):
return NOT_ALLOWED return NOT_ALLOWED
def move(self, environ, collections, content, user): from_collection = write_collections[0]
"""Manage MOVE request."""
from_collection = collections[0]
from_name = xmlutils.name_from_path( from_name = xmlutils.name_from_path(
environ["PATH_INFO"], from_collection) environ["PATH_INFO"], from_collection)
if from_name: if from_name:
@ -421,8 +442,7 @@ 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 rights.write_authorized(user, to_collection) and \ if to_collection in write_collections:
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
@ -438,7 +458,7 @@ 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, read_collections, write_collections, content, user):
"""Manage OPTIONS request.""" """Manage OPTIONS request."""
headers = { headers = {
"Allow": ("DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, " "Allow": ("DELETE, HEAD, GET, MKCALENDAR, MKCOL, MOVE, "
@ -446,32 +466,38 @@ 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, read_collections, write_collections, content, user):
"""Manage PROPFIND request.""" """Manage PROPFIND request."""
# Rights is handled by collection in xmlutils.propfind # 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"}
collections = self._union(read_collections, write_collections)
answer = xmlutils.propfind( answer = xmlutils.propfind(
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, read_collections, write_collections, content, user):
"""Manage PROPPATCH request.""" """Manage PROPPATCH request."""
collection = collections[0] if not len(write_collections):
if rights.write_authorized(user, collection): return NOT_ALLOWED
collection = write_collections[0]
answer = xmlutils.proppatch( answer = xmlutils.proppatch(
environ["PATH_INFO"], content, collection) environ["PATH_INFO"], content, collection)
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"}
return client.MULTI_STATUS, headers, answer return client.MULTI_STATUS, headers, answer
else:
def put(self, environ, read_collections, write_collections, content, user):
"""Manage PUT request."""
if not len(write_collections):
return NOT_ALLOWED return NOT_ALLOWED
def put(self, environ, collections, content, user): collection = write_collections[0]
"""Manage PUT request."""
collection = collections[0]
collection.set_mimetype(environ.get("CONTENT_TYPE")) collection.set_mimetype(environ.get("CONTENT_TYPE"))
headers = {} headers = {}
item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection) item_name = xmlutils.name_from_path(environ["PATH_INFO"], collection)
@ -485,7 +511,6 @@ 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 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.
@ -495,21 +520,21 @@ 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 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, read_collections, write_collections, content, user):
"""Manage REPORT request.""" """Manage REPORT request."""
collection = collections[0] if not len(read_collections):
headers = {"Content-Type": "text/xml"}
if rights.read_authorized(user, collection):
answer = xmlutils.report(environ["PATH_INFO"], content, collection)
return client.MULTI_STATUS, headers, answer
else:
return NOT_ALLOWED return NOT_ALLOWED
collection = read_collections[0]
headers = {"Content-Type": "text/xml"}
answer = xmlutils.report(environ["PATH_INFO"], content, collection)
return client.MULTI_STATUS, headers, answer
# pylint: enable=W0612,W0613,R0201 # pylint: enable=W0612,W0613,R0201

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 . import client, config, ical, rights from . import client, config, ical
NAMESPACES = { NAMESPACES = {
@ -189,6 +189,10 @@ def propfind(path, xml_request, collections, user=None):
Read rfc4918-9.1 for info. Read rfc4918-9.1 for info.
The collections parameter is a list of collections that are
to be included in the output. Rights checking has to be done
by the caller.
""" """
# Reading request # Reading request
root = ET.fromstring(xml_request.encode("utf8")) root = ET.fromstring(xml_request.encode("utf8"))
@ -200,7 +204,6 @@ 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 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)