Merge pull request #1990 from pbiering/feature-sharing-prepare

Feature sharing prepare
This commit is contained in:
Peter Bieringer
2026-02-22 15:39:50 +01:00
committed by GitHub
10 changed files with 54 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
# This file is part of Radicale - CalDAV and CardDAV server # This file is part of Radicale - CalDAV and CardDAV server
# Copyright © 2020 Unrud <unrud@outlook.com> # Copyright © 2020 Unrud <unrud@outlook.com>
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de> # Copyright © 2024-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -17,14 +17,14 @@
import io import io
import logging import logging
import posixpath
import sys import sys
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Optional from typing import Optional, Union
from radicale import (auth, config, hook, httputils, pathutils, rights, from radicale import (auth, config, hook, httputils, pathutils, rights,
storage, types, utils, web, xmlutils) storage, types, utils, web, xmlutils)
from radicale.log import logger from radicale.log import logger
from radicale.rights import intersect
# HACK: https://github.com/tiran/defusedxml/issues/54 # HACK: https://github.com/tiran/defusedxml/issues/54
import defusedxml.ElementTree as DefusedET # isort:skip import defusedxml.ElementTree as DefusedET # isort:skip
@@ -106,15 +106,19 @@ class Access:
permissions: str permissions: str
_rights: rights.BaseRights _rights: rights.BaseRights
_parent_permissions: Optional[str] _parent_permissions: Optional[str]
_permissions_filter: Union[str, None] = None
def __init__(self, rights: rights.BaseRights, user: str, path: str def __init__(self, rights: rights.BaseRights, user: str, path: str, permissions_filter: Union[str, None] = None
) -> None: ) -> None:
self._rights = rights self._rights = rights
self.user = user self.user = user
self.path = path self.path = path
self.parent_path = pathutils.unstrip_path( self.parent_path = pathutils.parent_path(path)
posixpath.dirname(pathutils.strip_path(path)), True)
self.permissions = self._rights.authorization(self.user, self.path) self.permissions = self._rights.authorization(self.user, self.path)
if permissions_filter is not None:
self._permissions_filter = permissions_filter
permissions_filtered = intersect(self.permissions, permissions_filter)
self.permissions = permissions_filtered
self._parent_permissions = None self._parent_permissions = None
@property @property
@@ -124,6 +128,9 @@ class Access:
if self._parent_permissions is None: if self._parent_permissions is None:
self._parent_permissions = self._rights.authorization( self._parent_permissions = self._rights.authorization(
self.user, self.parent_path) self.user, self.parent_path)
if self._permissions_filter is not None:
parent_permissions_filtered = intersect(self._parent_permissions, self._permissions_filter)
self._parent_permissions = parent_permissions_filtered
return self._parent_permissions return self._parent_permissions
def check(self, permission: str, def check(self, permission: str,

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2020 Unrud <unrud@outlook.com> # Copyright © 2017-2020 Unrud <unrud@outlook.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2024-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -57,7 +57,8 @@ class ApplicationPartDelete(ApplicationBase):
def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str, def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""Manage DELETE request.""" """Manage DELETE request."""
access = Access(self._rights, user, path) permissions_filter = None
access = Access(self._rights, user, path, permissions_filter)
if not access.check("w"): if not access.check("w"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
with self._storage.acquire_lock("w", user, path=path, request="DELETE"): with self._storage.acquire_lock("w", user, path=path, request="DELETE"):

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2023 Unrud <unrud@outlook.com> # Copyright © 2017-2023 Unrud <unrud@outlook.com>
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2025-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -76,7 +76,8 @@ class ApplicationPartGet(ApplicationBase):
return httputils.redirect(location, client.MOVED_PERMANENTLY) return httputils.redirect(location, client.MOVED_PERMANENTLY)
# Dispatch /.web path to web module # Dispatch /.web path to web module
return self._web.get(environ, base_prefix, path, user) return self._web.get(environ, base_prefix, path, user)
access = Access(self._rights, user, path) permissions_filter = None
access = Access(self._rights, user, path, permissions_filter)
if not access.check("r") and "i" not in access.permissions: if not access.check("r") and "i" not in access.permissions:
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
with self._storage.acquire_lock("r", user): with self._storage.acquire_lock("r", user):

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2021 Unrud <unrud@outlook.com> # Copyright © 2017-2021 Unrud <unrud@outlook.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2024-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -19,7 +19,6 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import errno import errno
import posixpath
import re import re
import socket import socket
from http import client from http import client
@@ -62,8 +61,7 @@ class ApplicationPartMkcalendar(ApplicationBase):
if item: if item:
return self._webdav_error_response( return self._webdav_error_response(
client.CONFLICT, "D:resource-must-be-null") client.CONFLICT, "D:resource-must-be-null")
parent_path = pathutils.unstrip_path( parent_path = pathutils.parent_path(path)
posixpath.dirname(pathutils.strip_path(path)), True)
parent_item = next(iter(self._storage.discover(parent_path)), None) parent_item = next(iter(self._storage.discover(parent_path)), None)
if not parent_item: if not parent_item:
return httputils.CONFLICT return httputils.CONFLICT

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2021 Unrud <unrud@outlook.com> # Copyright © 2017-2021 Unrud <unrud@outlook.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2024-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -19,7 +19,6 @@
# along with Radicale. If not, see <http://www.gnu.org/licenses/>. # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
import errno import errno
import posixpath
import re import re
import socket import socket
from http import client from http import client
@@ -66,8 +65,7 @@ class ApplicationPartMkcol(ApplicationBase):
item = next(iter(self._storage.discover(path)), None) item = next(iter(self._storage.discover(path)), None)
if item: if item:
return httputils.METHOD_NOT_ALLOWED return httputils.METHOD_NOT_ALLOWED
parent_path = pathutils.unstrip_path( parent_path = pathutils.parent_path(path)
posixpath.dirname(pathutils.strip_path(path)), True)
parent_item = next(iter(self._storage.discover(parent_path)), None) parent_item = next(iter(self._storage.discover(parent_path)), None)
if not parent_item: if not parent_item:
return httputils.CONFLICT return httputils.CONFLICT

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2023 Unrud <unrud@outlook.com> # Copyright © 2017-2023 Unrud <unrud@outlook.com>
# Copyright © 2023-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2023-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -67,7 +67,8 @@ class ApplicationPartMove(ApplicationBase):
# Remote destination server, not supported # Remote destination server, not supported
return httputils.REMOTE_DESTINATION return httputils.REMOTE_DESTINATION
access = Access(self._rights, user, path) permissions_filter = None
access = Access(self._rights, user, path, permissions_filter)
if not access.check("w"): if not access.check("w"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
to_path = pathutils.sanitize_path(to_url.path) to_path = pathutils.sanitize_path(to_url.path)
@@ -76,7 +77,9 @@ class ApplicationPartMove(ApplicationBase):
"start with base prefix", to_path, path) "start with base prefix", to_path, path)
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
to_path = to_path[len(base_prefix):] to_path = to_path[len(base_prefix):]
to_access = Access(self._rights, user, to_path) to_user = user
to_permissions_filter = None
to_access = Access(self._rights, to_user, to_path, to_permissions_filter)
if not to_access.check("w"): if not to_access.check("w"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
@@ -94,8 +97,7 @@ class ApplicationPartMove(ApplicationBase):
to_item = next(iter(self._storage.discover(to_path)), None) to_item = next(iter(self._storage.discover(to_path)), None)
if isinstance(to_item, storage.BaseCollection): if isinstance(to_item, storage.BaseCollection):
return httputils.FORBIDDEN return httputils.FORBIDDEN
to_parent_path = pathutils.unstrip_path( to_parent_path = pathutils.parent_path(to_path)
posixpath.dirname(pathutils.strip_path(to_path)), True)
to_collection = next(iter( to_collection = next(iter(
self._storage.discover(to_parent_path)), None) self._storage.discover(to_parent_path)), None)
if not to_collection: if not to_collection:

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2021 Unrud <unrud@outlook.com> # Copyright © 2017-2021 Unrud <unrud@outlook.com>
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2025-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -405,7 +405,9 @@ class ApplicationPartPropfind(ApplicationBase):
def do_PROPFIND(self, environ: types.WSGIEnviron, base_prefix: str, def do_PROPFIND(self, environ: types.WSGIEnviron, base_prefix: str,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""Manage PROPFIND request.""" """Manage PROPFIND request."""
access = Access(self._rights, user, path) http_depth = environ.get("HTTP_DEPTH", "0")
permissions_filter = None
access = Access(self._rights, user, path, permissions_filter)
if not access.check("r"): if not access.check("r"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
try: try:
@@ -419,7 +421,7 @@ class ApplicationPartPropfind(ApplicationBase):
return httputils.REQUEST_TIMEOUT return httputils.REQUEST_TIMEOUT
with self._storage.acquire_lock("r", user): with self._storage.acquire_lock("r", user):
items_iter = iter(self._storage.discover( items_iter = iter(self._storage.discover(
path, environ.get("HTTP_DEPTH", "0"), path, http_depth,
None, self._rights._user_groups)) None, self._rights._user_groups))
# take root item for rights checking # take root item for rights checking
item = next(items_iter, None) item = next(items_iter, None)
@@ -429,11 +431,11 @@ class ApplicationPartPropfind(ApplicationBase):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
# put item back # put item back
items_iter = itertools.chain([item], items_iter) items_iter = itertools.chain([item], items_iter)
allowed_items = self._collect_allowed_items(items_iter, user) allowed_items = list(self._collect_allowed_items(items_iter, user))
headers = {"DAV": httputils.DAV_HEADERS, headers = {"DAV": httputils.DAV_HEADERS,
"Content-Type": "text/xml; charset=%s" % self._encoding} "Content-Type": "text/xml; charset=%s" % self._encoding}
xml_answer = xml_propfind(base_prefix, path, xml_content, xml_answer = xml_propfind(base_prefix, path, xml_content,
allowed_items, user, self._encoding, max_resource_size=self._max_resource_size) allowed_items, user, self._encoding, max_resource_size=self._max_resource_size)
if xml_answer is None: if xml_answer is None:
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
return client.MULTI_STATUS, headers, self._xml_response(xml_answer), xmlutils.pretty_xml(xml_content) return client.MULTI_STATUS, headers, self._xml_response(xml_answer), xmlutils.pretty_xml(xml_content)

View File

@@ -4,7 +4,7 @@
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2020 Unrud <unrud@outlook.com> # Copyright © 2017-2020 Unrud <unrud@outlook.com>
# Copyright © 2020-2020 Tuna Celik <tuna@jakpark.com> # Copyright © 2020-2020 Tuna Celik <tuna@jakpark.com>
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2025-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -75,7 +75,8 @@ class ApplicationPartProppatch(ApplicationBase):
def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str, def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""Manage PROPPATCH request.""" """Manage PROPPATCH request."""
access = Access(self._rights, user, path) permissions_filter = None
access = Access(self._rights, user, path, permissions_filter)
if not access.check("w"): if not access.check("w"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
try: try:

View File

@@ -6,7 +6,7 @@
# Copyright © 2024-2024 Pieter Hijma <pieterhijma@users.noreply.github.com> # Copyright © 2024-2024 Pieter Hijma <pieterhijma@users.noreply.github.com>
# Copyright © 2024-2024 Ray <ray@react0r.com> # Copyright © 2024-2024 Ray <ray@react0r.com>
# Copyright © 2024-2025 Georgiy <metallerok@gmail.com> # Copyright © 2024-2025 Georgiy <metallerok@gmail.com>
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2024-2026 Peter Bieringer <pb@bieringer.de>
# Copyright © 2025-2025 David Greaves <david@dgreaves.com> # Copyright © 2025-2025 David Greaves <david@dgreaves.com>
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
@@ -810,7 +810,8 @@ class ApplicationPartReport(ApplicationBase):
def do_REPORT(self, environ: types.WSGIEnviron, base_prefix: str, def do_REPORT(self, environ: types.WSGIEnviron, base_prefix: str,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""Manage REPORT request.""" """Manage REPORT request."""
access = Access(self._rights, user, path) permissions_filter = None
access = Access(self._rights, user, path, permissions_filter)
if not access.check("r"): if not access.check("r"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
try: try:

View File

@@ -2,7 +2,7 @@
# Copyright © 2014 Jean-Marc Martins # Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2012-2017 Guillaume Ayoub
# Copyright © 2017-2022 Unrud <unrud@outlook.com> # Copyright © 2017-2022 Unrud <unrud@outlook.com>
# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de> # Copyright © 2025-2026 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # 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 # it under the terms of the GNU General Public License as published by
@@ -293,6 +293,10 @@ def path_to_filesystem(root: str, sane_path: str) -> str:
return safe_path return safe_path
def parent_path(path: str) -> str:
return unstrip_path(posixpath.dirname(strip_path(path)), True)
class UnsafePathError(ValueError): class UnsafePathError(ValueError):
def __init__(self, path: str) -> None: def __init__(self, path: str) -> None: