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
# 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
# it under the terms of the GNU General Public License as published by
@@ -17,14 +17,14 @@
import io
import logging
import posixpath
import sys
import xml.etree.ElementTree as ET
from typing import Optional
from typing import Optional, Union
from radicale import (auth, config, hook, httputils, pathutils, rights,
storage, types, utils, web, xmlutils)
from radicale.log import logger
from radicale.rights import intersect
# HACK: https://github.com/tiran/defusedxml/issues/54
import defusedxml.ElementTree as DefusedET # isort:skip
@@ -106,15 +106,19 @@ class Access:
permissions: str
_rights: rights.BaseRights
_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:
self._rights = rights
self.user = user
self.path = path
self.parent_path = pathutils.unstrip_path(
posixpath.dirname(pathutils.strip_path(path)), True)
self.parent_path = pathutils.parent_path(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
@property
@@ -124,6 +128,9 @@ class Access:
if self._parent_permissions is None:
self._parent_permissions = self._rights.authorization(
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
def check(self, permission: str,

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# 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
# 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,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""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"):
return httputils.NOT_ALLOWED
with self._storage.acquire_lock("w", user, path=path, request="DELETE"):

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# 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
# 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)
# Dispatch /.web path to web module
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:
return httputils.NOT_ALLOWED
with self._storage.acquire_lock("r", user):

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# 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
# 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/>.
import errno
import posixpath
import re
import socket
from http import client
@@ -62,8 +61,7 @@ class ApplicationPartMkcalendar(ApplicationBase):
if item:
return self._webdav_error_response(
client.CONFLICT, "D:resource-must-be-null")
parent_path = pathutils.unstrip_path(
posixpath.dirname(pathutils.strip_path(path)), True)
parent_path = pathutils.parent_path(path)
parent_item = next(iter(self._storage.discover(parent_path)), None)
if not parent_item:
return httputils.CONFLICT

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# 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
# 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/>.
import errno
import posixpath
import re
import socket
from http import client
@@ -66,8 +65,7 @@ class ApplicationPartMkcol(ApplicationBase):
item = next(iter(self._storage.discover(path)), None)
if item:
return httputils.METHOD_NOT_ALLOWED
parent_path = pathutils.unstrip_path(
posixpath.dirname(pathutils.strip_path(path)), True)
parent_path = pathutils.parent_path(path)
parent_item = next(iter(self._storage.discover(parent_path)), None)
if not parent_item:
return httputils.CONFLICT

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# 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
# 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
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"):
return httputils.NOT_ALLOWED
to_path = pathutils.sanitize_path(to_url.path)
@@ -76,7 +77,9 @@ class ApplicationPartMove(ApplicationBase):
"start with base prefix", to_path, path)
return httputils.NOT_ALLOWED
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"):
return httputils.NOT_ALLOWED
@@ -94,8 +97,7 @@ class ApplicationPartMove(ApplicationBase):
to_item = next(iter(self._storage.discover(to_path)), None)
if isinstance(to_item, storage.BaseCollection):
return httputils.FORBIDDEN
to_parent_path = pathutils.unstrip_path(
posixpath.dirname(pathutils.strip_path(to_path)), True)
to_parent_path = pathutils.parent_path(to_path)
to_collection = next(iter(
self._storage.discover(to_parent_path)), None)
if not to_collection:

View File

@@ -3,7 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# 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
# 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,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""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"):
return httputils.NOT_ALLOWED
try:
@@ -419,7 +421,7 @@ class ApplicationPartPropfind(ApplicationBase):
return httputils.REQUEST_TIMEOUT
with self._storage.acquire_lock("r", user):
items_iter = iter(self._storage.discover(
path, environ.get("HTTP_DEPTH", "0"),
path, http_depth,
None, self._rights._user_groups))
# take root item for rights checking
item = next(items_iter, None)
@@ -429,11 +431,11 @@ class ApplicationPartPropfind(ApplicationBase):
return httputils.NOT_ALLOWED
# put item back
items_iter = itertools.chain([item], items_iter)
allowed_items = self._collect_allowed_items(items_iter, user)
headers = {"DAV": httputils.DAV_HEADERS,
"Content-Type": "text/xml; charset=%s" % self._encoding}
xml_answer = xml_propfind(base_prefix, path, xml_content,
allowed_items, user, self._encoding, max_resource_size=self._max_resource_size)
if xml_answer is None:
return httputils.NOT_ALLOWED
return client.MULTI_STATUS, headers, self._xml_response(xml_answer), xmlutils.pretty_xml(xml_content)
allowed_items = list(self._collect_allowed_items(items_iter, user))
headers = {"DAV": httputils.DAV_HEADERS,
"Content-Type": "text/xml; charset=%s" % self._encoding}
xml_answer = xml_propfind(base_prefix, path, xml_content,
allowed_items, user, self._encoding, max_resource_size=self._max_resource_size)
if xml_answer is None:
return httputils.NOT_ALLOWED
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 © 2017-2020 Unrud <unrud@outlook.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
# 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,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""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"):
return httputils.NOT_ALLOWED
try:

View File

@@ -6,7 +6,7 @@
# Copyright © 2024-2024 Pieter Hijma <pieterhijma@users.noreply.github.com>
# Copyright © 2024-2024 Ray <ray@react0r.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>
#
# 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,
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
"""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"):
return httputils.NOT_ALLOWED
try:

View File

@@ -2,7 +2,7 @@
# Copyright © 2014 Jean-Marc Martins
# Copyright © 2012-2017 Guillaume Ayoub
# 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
# 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
def parent_path(path: str) -> str:
return unstrip_path(posixpath.dirname(strip_path(path)), True)
class UnsafePathError(ValueError):
def __init__(self, path: str) -> None: