Minimize accesses to rights backend

This commit is contained in:
Unrud 2020-04-22 19:20:07 +02:00
parent 99adeb19c1
commit aef58bd55c
9 changed files with 92 additions and 74 deletions

View File

@ -264,12 +264,11 @@ class Application(
# Create principal collection
if user:
principal_path = "/%s/" % user
if "W" in self._rights.authorization(user, principal_path):
with self._storage.acquire_lock("r", user):
principal = next(
self._storage.discover(principal_path, depth="1"),
None)
if not principal:
with self._storage.acquire_lock("r", user):
principal = next(self._storage.discover(
principal_path, depth="1"), None)
if not principal:
if "W" in self._rights.authorization(user, principal_path):
with self._storage.acquire_lock("w", user):
try:
self._storage.create_collection(principal_path)
@ -277,9 +276,9 @@ class Application(
logger.warning("Failed to create principal "
"collection %r: %s", user, e)
user = ""
else:
logger.warning("Access to principal path %r denied by "
"rights backend", principal_path)
else:
logger.warning("Access to principal path %r denied by "
"rights backend", principal_path)
if self.configuration.get("server", "_internal_server"):
# Verify content length
@ -313,32 +312,6 @@ class Application(
return response(status, headers, answer)
def _access(self, user, path, permission, item=None):
if permission not in "rw":
raise ValueError("Invalid permission argument: %r" % permission)
if not item:
permissions = permission + permission.upper()
parent_permissions = permission
elif isinstance(item, storage.BaseCollection):
if item.get_meta("tag"):
permissions = permission
else:
permissions = permission.upper()
parent_permissions = ""
else:
permissions = ""
parent_permissions = permission
if permissions and rights.intersect(
self._rights.authorization(user, path), permissions):
return True
if parent_permissions:
parent_path = pathutils.unstrip_path(
posixpath.dirname(pathutils.strip_path(path)), True)
if rights.intersect(self._rights.authorization(user, parent_path),
parent_permissions):
return True
return False
def _read_raw_content(self, environ):
content_length = int(environ.get("CONTENT_LENGTH") or 0)
if not content_length:
@ -382,3 +355,44 @@ class Application(
headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
content = self._write_xml_content(xmlutils.webdav_error(human_tag))
return status, headers, content
class Access:
"""Helper class to check access rights of an item"""
def __init__(self, rights, user, path):
self._rights = rights
self.user = user
self.path = path
self.parent_path = pathutils.unstrip_path(
posixpath.dirname(pathutils.strip_path(path)), True)
self.permissions = self._rights.authorization(self.user, self.path)
self._parent_permissions = None
@property
def parent_permissions(self):
if self.path == self.parent_path:
return self.permissions
if self._parent_permissions is None:
self._parent_permissions = self._rights.authorization(
self.user, self.parent_path)
return self._parent_permissions
def check(self, permission, item=None):
if permission not in "rw":
raise ValueError("Invalid permission argument: %r" % permission)
if not item:
permissions = permission + permission.upper()
parent_permissions = permission
elif isinstance(item, storage.BaseCollection):
if item.get_meta("tag"):
permissions = permission
else:
permissions = permission.upper()
parent_permissions = ""
else:
permissions = ""
parent_permissions = permission
return bool(rights.intersect(self.permissions, permissions) or (
self.path != self.parent_path and
rights.intersect(self.parent_permissions, parent_permissions)))

View File

@ -20,7 +20,7 @@
from http import client
from xml.etree import ElementTree as ET
from radicale import httputils, storage, xmlutils
from radicale import app, httputils, storage, xmlutils
def xml_delete(base_prefix, path, collection, href=None):
@ -49,13 +49,14 @@ def xml_delete(base_prefix, path, collection, href=None):
class ApplicationDeleteMixin:
def do_DELETE(self, environ, base_prefix, path, user):
"""Manage DELETE request."""
if not self._access(user, path, "w"):
access = app.Access(self._rights, user, path)
if not access.check("w"):
return httputils.NOT_ALLOWED
with self._storage.acquire_lock("w", user):
item = next(self._storage.discover(path), None)
if not item:
return httputils.NOT_FOUND
if not self._access(user, path, "w", item):
if not access.check("w", item):
return httputils.NOT_ALLOWED
if_match = environ.get("HTTP_IF_MATCH", "*")
if if_match not in ("*", item.etag):

View File

@ -21,7 +21,7 @@ import posixpath
from http import client
from urllib.parse import quote
from radicale import httputils, pathutils, storage, xmlutils
from radicale import app, httputils, pathutils, storage, xmlutils
from radicale.log import logger
@ -70,13 +70,14 @@ class ApplicationGetMixin:
# Dispatch .web URL to web module
if path == "/.web" or path.startswith("/.web/"):
return self._web.get(environ, base_prefix, path, user)
if not self._access(user, path, "r"):
access = app.Access(self._rights, user, path)
if not access.check("r"):
return httputils.NOT_ALLOWED
with self._storage.acquire_lock("r", user):
item = next(self._storage.discover(path), None)
if not item:
return httputils.NOT_FOUND
if not self._access(user, path, "r", item):
if not access.check("r", item):
return httputils.NOT_ALLOWED
if isinstance(item, storage.BaseCollection):
tag = item.get_meta("tag")

View File

@ -30,9 +30,8 @@ from radicale.log import logger
class ApplicationMkcolMixin:
def do_MKCOL(self, environ, base_prefix, path, user):
"""Manage MKCOL request."""
permissions = rights.intersect(
self._rights.authorization(user, path), "Ww")
if not permissions:
permissions = self._rights.authorization(user, path)
if not rights.intersect(permissions, "Ww"):
return httputils.NOT_ALLOWED
try:
xml_content = self._read_xml_content(environ)

View File

@ -21,7 +21,7 @@ import posixpath
from http import client
from urllib.parse import urlparse
from radicale import httputils, pathutils, storage
from radicale import app, httputils, pathutils, storage
from radicale.log import logger
@ -34,7 +34,8 @@ class ApplicationMoveMixin:
logger.info("Unsupported destination address: %r", raw_dest)
# Remote destination server, not supported
return httputils.REMOTE_DESTINATION
if not self._access(user, path, "w"):
access = app.Access(self._rights, user, path)
if not access.check("w"):
return httputils.NOT_ALLOWED
to_path = pathutils.sanitize_path(to_url.path)
if not (to_path + "/").startswith(base_prefix + "/"):
@ -42,15 +43,16 @@ class ApplicationMoveMixin:
"start with base prefix", to_path, path)
return httputils.NOT_ALLOWED
to_path = to_path[len(base_prefix):]
if not self._access(user, to_path, "w"):
to_access = app.Access(self._rights, user, to_path)
if not to_access.check("w"):
return httputils.NOT_ALLOWED
with self._storage.acquire_lock("w", user):
item = next(self._storage.discover(path), None)
if not item:
return httputils.NOT_FOUND
if (not self._access(user, path, "w", item) or
not self._access(user, to_path, "w", item)):
if (not access.check("w", item) or
not to_access.check("w", item)):
return httputils.NOT_ALLOWED
if isinstance(item, storage.BaseCollection):
# TODO: support moving collections

View File

@ -24,7 +24,7 @@ import socket
from http import client
from xml.etree import ElementTree as ET
from radicale import httputils, pathutils, rights, storage, xmlutils
from radicale import app, httputils, pathutils, rights, storage, xmlutils
from radicale.log import logger
@ -343,7 +343,8 @@ class ApplicationPropfindMixin:
def do_PROPFIND(self, environ, base_prefix, path, user):
"""Manage PROPFIND request."""
if not self._access(user, path, "r"):
access = app.Access(self._rights, user, path)
if not access.check("r"):
return httputils.NOT_ALLOWED
try:
xml_content = self._read_xml_content(environ)
@ -361,7 +362,7 @@ class ApplicationPropfindMixin:
item = next(items, None)
if not item:
return httputils.NOT_FOUND
if not self._access(user, path, "r", item):
if not access.check("r", item):
return httputils.NOT_ALLOWED
# put item back
items = itertools.chain([item], items)

View File

@ -21,7 +21,7 @@ import socket
from http import client
from xml.etree import ElementTree as ET
from radicale import httputils
from radicale import app, httputils
from radicale import item as radicale_item
from radicale import storage, xmlutils
from radicale.log import logger
@ -87,7 +87,8 @@ def xml_proppatch(base_prefix, path, xml_request, collection):
class ApplicationProppatchMixin:
def do_PROPPATCH(self, environ, base_prefix, path, user):
"""Manage PROPPATCH request."""
if not self._access(user, path, "w"):
access = app.Access(self._rights, user, path)
if not access.check("w"):
return httputils.NOT_ALLOWED
try:
xml_content = self._read_xml_content(environ)
@ -102,7 +103,7 @@ class ApplicationProppatchMixin:
item = next(self._storage.discover(path), None)
if not item:
return httputils.NOT_FOUND
if not self._access(user, path, "w", item):
if not access.check("w", item):
return httputils.NOT_ALLOWED
if not isinstance(item, storage.BaseCollection):
return httputils.FORBIDDEN

View File

@ -25,7 +25,7 @@ from http import client
import vobject
from radicale import httputils
from radicale import app, httputils
from radicale import item as radicale_item
from radicale import pathutils, rights, storage, xmlutils
from radicale.log import logger
@ -114,7 +114,8 @@ def prepare(vobject_items, path, content_type, permissions, parent_permissions,
class ApplicationPutMixin:
def do_PUT(self, environ, base_prefix, path, user):
"""Manage PUT request."""
if not self._access(user, path, "w"):
access = app.Access(self._rights, user, path)
if not access.check("w"):
return httputils.NOT_ALLOWED
try:
content = self._read_content(environ)
@ -126,12 +127,6 @@ class ApplicationPutMixin:
return httputils.REQUEST_TIMEOUT
# Prepare before locking
content_type = environ.get("CONTENT_TYPE", "").split(";")[0]
parent_path = pathutils.unstrip_path(
posixpath.dirname(pathutils.strip_path(path)), True)
permissions = rights.intersect(
self._rights.authorization(user, path), "Ww")
parent_permissions = rights.intersect(
self._rights.authorization(user, parent_path), "w")
try:
vobject_items = tuple(vobject.readComponents(content or ""))
except Exception as e:
@ -140,12 +135,14 @@ class ApplicationPutMixin:
return httputils.BAD_REQUEST
(prepared_items, prepared_tag, prepared_write_whole_collection,
prepared_props, prepared_exc_info) = prepare(
vobject_items, path, content_type, permissions,
parent_permissions)
vobject_items, path, content_type,
bool(rights.intersect(access.permissions, "Ww")),
bool(rights.intersect(access.parent_permissions, "w")))
with self._storage.acquire_lock("w", user):
item = next(self._storage.discover(path), None)
parent_item = next(self._storage.discover(parent_path), None)
parent_item = next(
self._storage.discover(access.parent_path), None)
if not parent_item:
return httputils.CONFLICT
@ -159,10 +156,9 @@ class ApplicationPutMixin:
tag = parent_item.get_meta("tag")
if write_whole_collection:
if ("w" if tag else "W") not in self._rights.authorization(
user, path):
if ("w" if tag else "W") not in access.permissions:
return httputils.NOT_ALLOWED
elif "w" not in self._rights.authorization(user, parent_path):
elif "w" not in access.parent_permissions:
return httputils.NOT_ALLOWED
etag = environ.get("HTTP_IF_MATCH", "")
@ -182,8 +178,10 @@ class ApplicationPutMixin:
prepared_write_whole_collection != write_whole_collection):
(prepared_items, prepared_tag, prepared_write_whole_collection,
prepared_props, prepared_exc_info) = prepare(
vobject_items, path, content_type, permissions,
parent_permissions, tag, write_whole_collection)
vobject_items, path, content_type,
bool(rights.intersect(access.permissions, "Ww")),
bool(rights.intersect(access.parent_permissions, "w")),
tag, write_whole_collection)
props = prepared_props
if prepared_exc_info:
logger.warning(

View File

@ -24,7 +24,7 @@ from http import client
from urllib.parse import unquote, urlparse
from xml.etree import ElementTree as ET
from radicale import httputils, pathutils, storage, xmlutils
from radicale import app, httputils, pathutils, storage, xmlutils
from radicale.item import filter as radicale_filter
from radicale.log import logger
@ -257,7 +257,8 @@ def xml_item_response(base_prefix, href, found_props=(), not_found_props=(),
class ApplicationReportMixin:
def do_REPORT(self, environ, base_prefix, path, user):
"""Manage REPORT request."""
if not self._access(user, path, "r"):
access = app.Access(self._rights, user, path)
if not access.check("r"):
return httputils.NOT_ALLOWED
try:
xml_content = self._read_xml_content(environ)
@ -273,7 +274,7 @@ class ApplicationReportMixin:
item = next(self._storage.discover(path), None)
if not item:
return httputils.NOT_FOUND
if not self._access(user, path, "r", item):
if not access.check("r", item):
return httputils.NOT_ALLOWED
if isinstance(item, storage.BaseCollection):
collection = item