def do_MOVE(self, environ, base_prefix, path, user): """Manage MOVE request.""" raw_dest = environ.get("HTTP_DESTINATION", "") to_url = urlparse(raw_dest) if to_url.netloc != environ["HTTP_HOST"]: logger.info("Unsupported destination address: %r", raw_dest) # Remote destination server, not supported return httputils.REMOTE_DESTINATION if not self.access(user, path, "w"): return httputils.NOT_ALLOWED to_path = pathutils.sanitize_path(to_url.path) if not (to_path + "/").startswith(base_prefix + "/"): logger.warning( "Destination %r from MOVE request on %r doesn't " "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"): return httputils.NOT_ALLOWED with self.Collection.acquire_lock("w", user): item = next(self.Collection.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)): return httputils.NOT_ALLOWED if isinstance(item, storage.BaseCollection): # TODO: support moving collections return httputils.METHOD_NOT_ALLOWED to_item = next(self.Collection.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_collection = next(self.Collection.discover(to_parent_path), None) if not to_collection: return httputils.CONFLICT tag = item.collection.get_meta("tag") if not tag or tag != to_collection.get_meta("tag"): return httputils.FORBIDDEN if to_item and environ.get("HTTP_OVERWRITE", "F") != "T": return httputils.PRECONDITION_FAILED if (to_item and item.uid != to_item.uid or not to_item and to_collection.path != item.collection.path and to_collection.has_uid(item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") to_href = posixpath.basename(pathutils.strip_path(to_path)) try: self.Collection.move(item, to_collection, to_href) except ValueError as e: logger.warning("Bad MOVE request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.NO_CONTENT if to_item else client.CREATED, {}, None
def do_MOVE(self, environ, base_prefix, path, user): """Manage MOVE request.""" raw_dest = environ.get("HTTP_DESTINATION", "") to_url = urlparse(raw_dest) if to_url.netloc != environ["HTTP_HOST"]: logger.info("Unsupported destination address: %r", raw_dest) # Remote destination server, not supported return httputils.REMOTE_DESTINATION if not self.access(user, path, "w"): return httputils.NOT_ALLOWED to_path = pathutils.sanitize_path(to_url.path) if not (to_path + "/").startswith(base_prefix + "/"): logger.warning("Destination %r from MOVE request on %r doesn't " "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"): return httputils.NOT_ALLOWED with self.Collection.acquire_lock("w", user): item = next(self.Collection.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)): return httputils.NOT_ALLOWED if isinstance(item, storage.BaseCollection): # TODO: support moving collections return httputils.METHOD_NOT_ALLOWED to_item = next(self.Collection.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_collection = next( self.Collection.discover(to_parent_path), None) if not to_collection: return httputils.CONFLICT tag = item.collection.get_meta("tag") if not tag or tag != to_collection.get_meta("tag"): return httputils.FORBIDDEN if to_item and environ.get("HTTP_OVERWRITE", "F") != "T": return httputils.PRECONDITION_FAILED if (to_item and item.uid != to_item.uid or not to_item and to_collection.path != item.collection.path and to_collection.has_uid(item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") to_href = posixpath.basename(pathutils.strip_path(to_path)) try: self.Collection.move(item, to_collection, to_href) except ValueError as e: logger.warning( "Bad MOVE request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.NO_CONTENT if to_item else client.CREATED, {}, None
def authorization(self, user, path): if not user: return "" sane_path = pathutils.strip_path(path) if not sane_path: return "R" attributes = sane_path.split('/') if user != attributes[0]: return "" if "/" not in sane_path: return "RW" if sane_path.count("/") == 1: journal_uid = attributes[1] with etesync_for_user(user) as (etesync, _): try: journal = etesync.get(journal_uid) except api.exceptions.DoesNotExist: return '' return 'rw' if not journal.read_only else 'r' return ""
def discover(cls, path, depth="0", child_context_manager=( lambda path, href=None: contextlib.ExitStack())): # Path should already be sanitized sane_path = pathutils.strip_path(path) attributes = sane_path.split("/") if sane_path else [] folder = cls._get_collection_root_folder() # Create the root collection cls._makedirs_synced(folder) try: filesystem_path = pathutils.path_to_filesystem(folder, sane_path) except ValueError as e: # Path is unsafe logger.debug("Unsafe path %r requested from storage: %s", sane_path, e, exc_info=True) return # Check if the path exists and if it leads to a collection or an item if not os.path.isdir(filesystem_path): if attributes and os.path.isfile(filesystem_path): href = attributes.pop() else: return else: href = None sane_path = "/".join(attributes) collection = cls(pathutils.unstrip_path(sane_path, True)) if href: yield collection._get(href) return yield collection if depth == "0": return for href in collection._list(): with child_context_manager(sane_path, href): yield collection._get(href) for entry in os.scandir(filesystem_path): if not entry.is_dir(): continue href = entry.name if not pathutils.is_safe_filesystem_path_component(href): if not href.startswith(".Radicale"): logger.debug("Skipping collection %r in %r", href, sane_path) continue sane_child_path = posixpath.join(sane_path, href) child_path = pathutils.unstrip_path(sane_child_path, True) with child_context_manager(sane_child_path): yield cls(child_path)
def create_collection(self, href, collection=None, props=None): stripped_path = strip_path(href) c, created = DBCollection.objects.get_or_create( path=stripped_path, parent_path=os.path.dirname(stripped_path)) return c.as_collection()
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 _cleanup(path): sane_path = pathutils.strip_path(path) attributes = sane_path.split("/") if sane_path else [] if len(attributes) < 2: return "" return attributes[0] + "/" + attributes[1]
def discover(self, path, depth="0", child_context_manager=( lambda path, href=None: contextlib.ExitStack())): collections = list(super().discover(path, depth, child_context_manager)) for collection in collections: yield collection if depth == "0": return attributes = _get_attributes_from_path(path) if len(attributes) == 0: return elif len(attributes) == 1: username = attributes[0] known_paths = [collection.path for collection in collections] for sync_type in ["contacts", "calendars", "tasks", "memos"]: for collection in Decsync.list_collections( self.decsync_dir, sync_type): child_path = "/%s/%s-%s/" % (username, sync_type, collection) if pathutils.strip_path(child_path) in known_paths: continue if Decsync.get_static_info(self.decsync_dir, sync_type, collection, "deleted") == True: continue props = {} if sync_type == "contacts": props["tag"] = "VADDRESSBOOK" else: props["tag"] = "VCALENDAR" if sync_type == "calendars": props[ "C:supported-calendar-component-set"] = "VEVENT" elif sync_type == "tasks": props[ "C:supported-calendar-component-set"] = "VTODO" elif sync_type == "memos": props[ "C:supported-calendar-component-set"] = "VJOURNAL" else: raise RuntimeError("Unknown sync type " + sync_type) child = super().create_collection(child_path, props=props) child.decsync.init_stored_entries() child.decsync.execute_stored_entries_for_path_exact( ["info"], child) child.decsync.execute_stored_entries_for_path_prefix( ["resources"], child) yield child elif len(attributes) == 2: return else: raise ValueError("Invalid number of attributes")
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
def authorization(self, user, path): if self._verify_user and not user: return "" sane_path = pathutils.strip_path(path) if "/" not in sane_path: return "RW" if sane_path.count("/") == 1: return "rw" return ""
def authorized(self, user, path, permissions): if self._verify_user and not user: return "" sane_path = pathutils.strip_path(path) if "/" not in sane_path: return rights.intersect_permissions(permissions, "RW") if sane_path.count("/") == 1: return rights.intersect_permissions(permissions, "rw") return ""
def __init__(self, path, filesystem_path=None): folder = self._get_collection_root_folder() # Path should already be sanitized self.path = pathutils.strip_path(path) self._encoding = self.configuration.get("encoding", "stock") if filesystem_path is None: filesystem_path = pathutils.path_to_filesystem(folder, self.path) self._filesystem_path = filesystem_path self._etag_cache = None super().__init__()
def __init__(self, collection_path=None, collection=None, vobject_item=None, href=None, last_modified=None, text=None, etag=None, uid=None, name=None, component_name=None, time_range=None): """Initialize an item. ``collection_path`` the path of the parent collection (optional if ``collection`` is set). ``collection`` the parent collection (optional). ``href`` the href of the item. ``last_modified`` the HTTP-datetime of when the item was modified. ``text`` the text representation of the item (optional if ``vobject_item`` is set). ``vobject_item`` the vobject item (optional if ``text`` is set). ``etag`` the etag of the item (optional). See ``get_etag``. ``uid`` the UID of the object (optional). See ``get_uid_from_object``. ``name`` the name of the item (optional). See ``vobject_item.name``. ``component_name`` the name of the primary component (optional). See ``find_tag``. ``time_range`` the enclosing time range. See ``find_tag_and_time_range``. """ if text is None and vobject_item is None: raise ValueError( "at least one of 'text' or 'vobject_item' must be set") if collection_path is None: if collection is None: raise ValueError("at least one of 'collection_path' or " "'collection' must be set") collection_path = collection.path assert collection_path == pathutils.strip_path( pathutils.sanitize_path(collection_path)) self._collection_path = collection_path self.collection = collection self.href = href self.last_modified = last_modified self._text = text self._vobject_item = vobject_item self._etag = etag self._uid = uid self._name = name self._component_name = component_name self._time_range = time_range
def authorization(self, user, path): if self._verify_user and not user: return "" sane_path = pathutils.strip_path(path) if not sane_path: return "R" if self._verify_user and user != sane_path.split("/", maxsplit=1)[0]: return "" if "/" not in sane_path: return "RW" if sane_path.count("/") == 1: return "rw" return ""
def exception_cm(path, href=None): nonlocal item_errors, collection_errors sane_path = pathutils.strip_path(path) try: yield except Exception as e: if href: item_errors += 1 name = "item %r in %r" % (href, sane_path) else: collection_errors += 1 name = "collection %r" % sane_path logger.error("Invalid %s: %s", name, e, exc_info=True)
def __init__(self, path, filesystem_path=None): folder = self._get_collection_root_folder() # Path should already be sanitized self.path = pathutils.strip_path(path) self._encoding = self.configuration.get("encoding", "stock") if filesystem_path is None: filesystem_path = pathutils.path_to_filesystem(folder, self.path) self._filesystem_path = filesystem_path self._props_path = os.path.join(self._filesystem_path, ".Radicale.props") self._meta_cache = None self._etag_cache = None self._item_cache_cleaned = False
def do_MKCOL(self, environ, base_prefix, path, user, context=None): """Manage MKCOL request.""" permissions = self._rights.authorization(user, path) if not rights.intersect(permissions, "Ww"): return httputils.NOT_ALLOWED try: xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning("Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("Client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) props = {k: v for k, v in props.items() if v is not None} try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning("Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST if (props.get("tag") and "w" not in permissions or not props.get("tag") and "W" not in permissions): return httputils.NOT_ALLOWED with self._storage.acquire_lock("w", user): item = next(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_item = next(self._storage.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self._storage.create_collection(path, props=props) except ValueError as e: logger.warning("Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def do_MKCALENDAR(self, environ, base_prefix, path, user): """Manage MKCALENDAR request.""" if "w" not in self._rights.authorization(user, path): return httputils.NOT_ALLOWED try: xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning("Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("Client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) props = {k: v for k, v in props.items() if v is not None} props["tag"] = "VCALENDAR" # TODO: use this? # timezone = props.get("C:calendar-timezone") try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning("Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST with self._storage.acquire_lock("w", user): item = next(self._storage.discover(path), None) 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_item = next(self._storage.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self._storage.create_collection(path, props=props) except ValueError as e: logger.warning("Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def do_GET(self, environ, base_prefix, path, user): """Manage GET request.""" # Redirect to .web if the root URL is requested if not pathutils.strip_path(path): web_path = ".web" if not environ.get("PATH_INFO"): web_path = posixpath.join(posixpath.basename(base_prefix), web_path) return (client.FOUND, { "Location": web_path, "Content-Type": "text/plain" }, "Redirected to %s" % web_path) # Dispatch .web URL to web module if path == "/.web" or path.startswith("/.web/"): return self._web.get(environ, base_prefix, path, user) access = app.Access(self._rights, user, path) if not access.check("r") and "i" not in access.permissions: 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 access.check("r", item): limited_access = False elif "i" in access.permissions: limited_access = True else: return httputils.NOT_ALLOWED if isinstance(item, storage.BaseCollection): tag = item.get_meta("tag") if not tag: return (httputils.NOT_ALLOWED if limited_access else httputils.DIRECTORY_LISTING) content_type = xmlutils.MIMETYPES[tag] content_disposition = self._content_disposition_attachement( propose_filename(item)) elif limited_access: return httputils.NOT_ALLOWED else: content_type = xmlutils.OBJECT_MIMETYPES[item.name] content_disposition = "" headers = { "Content-Type": content_type, "Last-Modified": item.last_modified, "ETag": item.etag } if content_disposition: headers["Content-Disposition"] = content_disposition answer = item.serialize() return client.OK, headers, answer
def authorization(self, user, path): if self._verify_user and not user: return "" sane_path = pathutils.strip_path(path) if not sane_path: return "R" if self._verify_user: owned = user == sane_path.split("/", maxsplit=1)[0] else: owned = True if "/" not in sane_path: return "RW" if owned else "R" if sane_path.count("/") == 1: return "rw" if owned else "r" return ""
def authorized(self, user, path, permissions): if self._verify_user and not user: return "" sane_path = pathutils.strip_path(path) if not sane_path: return rights.intersect_permissions(permissions, "R") if self._verify_user: owned = user == sane_path.split("/", maxsplit=1)[0] else: owned = True if "/" not in sane_path: return rights.intersect_permissions(permissions, "RW" if owned else "R") if sane_path.count("/") == 1: return rights.intersect_permissions(permissions, "rw" if owned else "r") return ""
def authorized(self, user, path, permissions): logger.debug( "User %r is trying to access path %r. Permissions: %r", user, path, permissions, ) # everybody can access the root collection if path == "/": logger.debug("Accessing root path. Access granted.") return True user = user or "" sane_path = strip_path(path) full_access = "rw" if ("/" in sane_path) else "RW" pathowner, _ = sane_path.split("/", maxsplit=1) # pathowner can be a user... if user == pathowner: logger.debug("User %r is pathowner. Read & Write Access granted.", user) return full_access # ...or a group maybe_groupname = self.group_prefix + pathowner try: group = grp.getgrnam(maybe_groupname) if user in group.gr_mem: logger.debug( "User %r is in pathowner group %r. Read & Write Access granted.", user, pathowner, ) return full_access except KeyError: logger.debug( "Pathowner %r is neither the user nor a valid group.", pathowner, ) logger.debug("Access to path %r is not granted to user %r.", pathowner, user) return ""
def do_MKCOL(self, environ, base_prefix, path, user): """Manage MKCOL request.""" permissions = self.Rights.authorized(user, path, "Ww") if not permissions: return httputils.NOT_ALLOWED try: xml_content = self.read_xml_content(environ) except RuntimeError as e: logger.warning( "Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning( "Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST if (props.get("tag") and "w" not in permissions or not props.get("tag") and "W" not in permissions): return httputils.NOT_ALLOWED with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) if item: return httputils.METHOD_NOT_ALLOWED parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self.Collection.create_collection(path, props=props) except ValueError as e: logger.warning( "Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def do_MKCALENDAR(self, environ, base_prefix, path, user): """Manage MKCALENDAR request.""" if not self.Rights.authorized(user, path, "w"): return httputils.NOT_ALLOWED try: xml_content = self.read_xml_content(environ) except RuntimeError as e: logger.warning( "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) props["tag"] = "VCALENDAR" # TODO: use this? # timezone = props.get("C:calendar-timezone") try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning( "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) if item: return self.webdav_error_response( "D", "resource-must-be-null") parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self.Collection.create_collection(path, props=props) except ValueError as e: logger.warning( "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def create_collection(self, href, items=None, props=None): folder = self._get_collection_root_folder() # Path should already be sanitized sane_path = pathutils.strip_path(href) filesystem_path = pathutils.path_to_filesystem(folder, sane_path) if not props: self._makedirs_synced(filesystem_path) return self._collection_class( self, pathutils.unstrip_path(sane_path, True)) parent_dir = os.path.dirname(filesystem_path) self._makedirs_synced(parent_dir) # Create a temporary directory with an unsafe name with TemporaryDirectory(prefix=".Radicale.tmp-", dir=parent_dir) as tmp_dir: # The temporary directory itself can't be renamed tmp_filesystem_path = os.path.join(tmp_dir, "collection") os.makedirs(tmp_filesystem_path) col = self._collection_class(self, pathutils.unstrip_path( sane_path, True), filesystem_path=tmp_filesystem_path) col.set_meta(props) if items is not None: if props.get("tag") == "VCALENDAR": col._upload_all_nonatomic(items, suffix=".ics") elif props.get("tag") == "VADDRESSBOOK": col._upload_all_nonatomic(items, suffix=".vcf") # This operation is not atomic on the filesystem level but it's # very unlikely that one rename operations succeeds while the # other fails or that only one gets written to disk. if os.path.exists(filesystem_path): os.rename(filesystem_path, os.path.join(tmp_dir, "delete")) os.rename(tmp_filesystem_path, filesystem_path) self._sync_directory(parent_dir) return self._collection_class(self, pathutils.unstrip_path(sane_path, True))
def do_GET(self, environ, base_prefix, path, user): """Manage GET request.""" # Redirect to .web if the root URL is requested if not pathutils.strip_path(path): web_path = ".web" if not environ.get("PATH_INFO"): web_path = posixpath.join(posixpath.basename(base_prefix), web_path) return (client.FOUND, {"Location": web_path, "Content-Type": "text/plain"}, "Redirected to %s" % web_path) # 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"): return httputils.NOT_ALLOWED with self.Collection.acquire_lock("r", user): item = next(self.Collection.discover(path), None) if not item: return httputils.NOT_FOUND if not self.access(user, path, "r", item): return httputils.NOT_ALLOWED if isinstance(item, storage.BaseCollection): tag = item.get_meta("tag") if not tag: return httputils.DIRECTORY_LISTING content_type = xmlutils.MIMETYPES[tag] content_disposition = self._content_disposition_attachement( propose_filename(item)) else: content_type = xmlutils.OBJECT_MIMETYPES[item.name] content_disposition = "" headers = { "Content-Type": content_type, "Last-Modified": item.last_modified, "ETag": item.etag} if content_disposition: headers["Content-Disposition"] = content_disposition answer = item.serialize() return client.OK, headers, answer
def authorized(self, user, path, permissions): user = user or "" sane_path = pathutils.strip_path(path) # Prevent "regex injection" user_escaped = re.escape(user) sane_path_escaped = re.escape(sane_path) rights_config = configparser.ConfigParser({ "login": user_escaped, "path": sane_path_escaped }) try: if not rights_config.read(self.filename): raise RuntimeError("No such file: %r" % self.filename) except Exception as e: raise RuntimeError("Failed to load rights file %r: %s" % (self.filename, e)) from e for section in rights_config.sections(): try: user_pattern = rights_config.get(section, "user") collection_pattern = rights_config.get(section, "collection") user_match = re.fullmatch(user_pattern, user) collection_match = user_match and re.fullmatch( collection_pattern.format( *map(re.escape, user_match.groups())), sane_path) except Exception as e: raise RuntimeError("Error in section %r of rights file %r: " "%s" % (section, self.filename, e)) from e if user_match and collection_match: logger.debug("Rule %r:%r matches %r:%r from section %r", user, sane_path, user_pattern, collection_pattern, section) return rights.intersect_permissions( permissions, rights_config.get(section, "permissions")) else: logger.debug("Rule %r:%r doesn't match %r:%r from section %r", user, sane_path, user_pattern, collection_pattern, section) logger.info("Rights: %r:%r doesn't match any section", user, sane_path) return ""
def create_collection(self, href, items=None, props=None): folder = self._get_collection_root_folder() # Path should already be sanitized sane_path = pathutils.strip_path(href) filesystem_path = pathutils.path_to_filesystem(folder, sane_path) if not props: self._makedirs_synced(filesystem_path) return self._collection_class( self, pathutils.unstrip_path(sane_path, True)) parent_dir = os.path.dirname(filesystem_path) self._makedirs_synced(parent_dir) # Create a temporary directory with an unsafe name with TemporaryDirectory( prefix=".Radicale.tmp-", dir=parent_dir) as tmp_dir: # The temporary directory itself can't be renamed tmp_filesystem_path = os.path.join(tmp_dir, "collection") os.makedirs(tmp_filesystem_path) col = self._collection_class( self, pathutils.unstrip_path(sane_path, True), filesystem_path=tmp_filesystem_path) col.set_meta(props) if items is not None: if props.get("tag") == "VCALENDAR": col._upload_all_nonatomic(items, suffix=".ics") elif props.get("tag") == "VADDRESSBOOK": col._upload_all_nonatomic(items, suffix=".vcf") if os.path.lexists(filesystem_path): pathutils.rename_exchange(tmp_filesystem_path, filesystem_path) else: os.rename(tmp_filesystem_path, filesystem_path) self._sync_directory(parent_dir) return self._collection_class( self, pathutils.unstrip_path(sane_path, True))
def discover(self, path, depth='0'): stripped_path = strip_path(path) if stripped_path == '': yield Collection('') return for c in DBCollection.objects.filter( path=stripped_path).as_collections(): yield c prefix, _, name = stripped_path.rpartition('/') for i in DBItem.objects.filter(collection__path=prefix, name=name).as_items(): yield i if depth == '0': return for i in DBItem.objects.filter( collection__path=stripped_path).as_items(): yield i
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 self.Rights.authorized(user, path, permissions): return True if parent_permissions: parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) if self.Rights.authorized(user, parent_path, parent_permissions): return True return False
def authorization(self, user, path): user = user or "" sane_path = pathutils.strip_path(path) # Prevent "regex injection" escaped_user = re.escape(user) rights_config = configparser.ConfigParser() try: if not rights_config.read(self._filename): raise RuntimeError("No such file: %r" % self._filename) except Exception as e: raise RuntimeError("Failed to load rights file %r: %s" % (self._filename, e)) from e for section in rights_config.sections(): try: user_pattern = rights_config.get(section, "user") collection_pattern = rights_config.get(section, "collection") # Use empty format() for harmonized handling of curly braces user_match = re.fullmatch(user_pattern.format(), user) collection_match = user_match and re.fullmatch( collection_pattern.format( *map(re.escape, user_match.groups()), user=escaped_user), sane_path) except Exception as e: raise RuntimeError("Error in section %r of rights file %r: " "%s" % (section, self._filename, e)) from e if user_match and collection_match: logger.debug("Rule %r:%r matches %r:%r from section %r", user, sane_path, user_pattern, collection_pattern, section) return rights_config.get(section, "permissions") logger.debug("Rule %r:%r doesn't match %r:%r from section %r", user, sane_path, user_pattern, collection_pattern, section) logger.info("Rights: %r:%r doesn't match any section", user, sane_path) return ""
def prepare(vobject_items, tag=None, write_whole_collection=None): if (write_whole_collection or permissions and not parent_permissions): write_whole_collection = True content_type = environ.get("CONTENT_TYPE", "").split(";")[0] tags = {value: key for key, value in xmlutils.MIMETYPES.items()} tag = radicale_item.predict_tag_of_whole_collection( vobject_items, tags.get(content_type)) if not tag: raise ValueError("Can't determine collection tag") collection_path = pathutils.strip_path(path) elif (write_whole_collection is not None and not write_whole_collection or not permissions and parent_permissions): write_whole_collection = False if tag is None: tag = radicale_item.predict_tag_of_parent_collection( vobject_items) collection_path = posixpath.dirname( pathutils.strip_path(path)) props = None stored_exc_info = None items = [] try: if tag: radicale_item.check_and_sanitize_items( vobject_items, is_collection=write_whole_collection, tag=tag) if write_whole_collection and tag == "VCALENDAR": vobject_components = [] vobject_item, = vobject_items for content in ("vevent", "vtodo", "vjournal"): vobject_components.extend( getattr(vobject_item, "%s_list" % content, [])) vobject_components_by_uid = itertools.groupby( sorted(vobject_components, key=radicale_item.get_uid), radicale_item.get_uid) for uid, components in vobject_components_by_uid: vobject_collection = vobject.iCalendar() for component in components: vobject_collection.add(component) item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_collection) item.prepare() items.append(item) elif write_whole_collection and tag == "VADDRESSBOOK": for vobject_item in vobject_items: item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) elif not write_whole_collection: vobject_item, = vobject_items item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) if write_whole_collection: props = {} if tag: props["tag"] = tag if tag == "VCALENDAR" and vobject_items: if hasattr(vobject_items[0], "x_wr_calname"): calname = vobject_items[0].x_wr_calname.value if calname: props["D:displayname"] = calname if hasattr(vobject_items[0], "x_wr_caldesc"): caldesc = vobject_items[0].x_wr_caldesc.value if caldesc: props["C:calendar-description"] = caldesc radicale_item.check_and_sanitize_props(props) except Exception: stored_exc_info = sys.exc_info() # Use generator for items and delete references to free memory # early def items_generator(): while items: yield items.pop(0) return (items_generator(), tag, write_whole_collection, props, stored_exc_info)
def authorized(self, user, path, permissions): sane_path = pathutils.strip_path(path) if sane_path not in ("tmp", "other"): return "" return rights.intersect_permissions(permissions)
def do_PUT(self, environ, base_prefix, path, user): """Manage PUT request.""" if not self.access(user, path, "w"): return httputils.NOT_ALLOWED try: content = self.read_content(environ) except RuntimeError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) permissions = self.Rights.authorized(user, path, "Ww") parent_permissions = self.Rights.authorized(user, parent_path, "w") def prepare(vobject_items, tag=None, write_whole_collection=None): if (write_whole_collection or permissions and not parent_permissions): write_whole_collection = True content_type = environ.get("CONTENT_TYPE", "").split(";")[0] tags = {value: key for key, value in xmlutils.MIMETYPES.items()} tag = radicale_item.predict_tag_of_whole_collection( vobject_items, tags.get(content_type)) if not tag: raise ValueError("Can't determine collection tag") collection_path = pathutils.strip_path(path) elif (write_whole_collection is not None and not write_whole_collection or not permissions and parent_permissions): write_whole_collection = False if tag is None: tag = radicale_item.predict_tag_of_parent_collection( vobject_items) collection_path = posixpath.dirname( pathutils.strip_path(path)) props = None stored_exc_info = None items = [] try: if tag: radicale_item.check_and_sanitize_items( vobject_items, is_collection=write_whole_collection, tag=tag) if write_whole_collection and tag == "VCALENDAR": vobject_components = [] vobject_item, = vobject_items for content in ("vevent", "vtodo", "vjournal"): vobject_components.extend( getattr(vobject_item, "%s_list" % content, [])) vobject_components_by_uid = itertools.groupby( sorted(vobject_components, key=radicale_item.get_uid), radicale_item.get_uid) for uid, components in vobject_components_by_uid: vobject_collection = vobject.iCalendar() for component in components: vobject_collection.add(component) item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_collection) item.prepare() items.append(item) elif write_whole_collection and tag == "VADDRESSBOOK": for vobject_item in vobject_items: item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) elif not write_whole_collection: vobject_item, = vobject_items item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) if write_whole_collection: props = {} if tag: props["tag"] = tag if tag == "VCALENDAR" and vobject_items: if hasattr(vobject_items[0], "x_wr_calname"): calname = vobject_items[0].x_wr_calname.value if calname: props["D:displayname"] = calname if hasattr(vobject_items[0], "x_wr_caldesc"): caldesc = vobject_items[0].x_wr_caldesc.value if caldesc: props["C:calendar-description"] = caldesc radicale_item.check_and_sanitize_props(props) except Exception: stored_exc_info = sys.exc_info() # Use generator for items and delete references to free memory # early def items_generator(): while items: yield items.pop(0) return (items_generator(), tag, write_whole_collection, props, stored_exc_info) try: vobject_items = tuple(vobject.readComponents(content or "")) except Exception as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare(vobject_items) with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT write_whole_collection = ( isinstance(item, storage.BaseCollection) or not parent_item.get_meta("tag")) if write_whole_collection: tag = prepared_tag else: tag = parent_item.get_meta("tag") if write_whole_collection: if not self.Rights.authorized(user, path, "w" if tag else "W"): return httputils.NOT_ALLOWED elif not self.Rights.authorized(user, parent_path, "w"): return httputils.NOT_ALLOWED etag = environ.get("HTTP_IF_MATCH", "") if not item and etag: # Etag asked but no item found: item has been removed return httputils.PRECONDITION_FAILED if item and etag and item.etag != etag: # Etag asked but item not matching: item has changed return httputils.PRECONDITION_FAILED match = environ.get("HTTP_IF_NONE_MATCH", "") == "*" if item and match: # Creation asked but item found: item can't be replaced return httputils.PRECONDITION_FAILED if (tag != prepared_tag or prepared_write_whole_collection != write_whole_collection): (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare( vobject_items, tag, write_whole_collection) props = prepared_props if prepared_exc_info: logger.warning( "Bad PUT request on %r: %s", path, prepared_exc_info[1], exc_info=prepared_exc_info) return httputils.BAD_REQUEST if write_whole_collection: try: etag = self.Collection.create_collection( path, prepared_items, props).etag except ValueError as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST else: prepared_item, = prepared_items if (item and item.uid != prepared_item.uid or not item and parent_item.has_uid(prepared_item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") href = posixpath.basename(pathutils.strip_path(path)) try: etag = parent_item.upload(href, prepared_item).etag except ValueError as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST headers = {"ETag": etag} return client.CREATED, headers, None
def do_PUT(self, environ, base_prefix, path, user): """Manage PUT request.""" if not self.access(user, path, "w"): return httputils.NOT_ALLOWED try: content = self.read_content(environ) except RuntimeError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) permissions = self.Rights.authorized(user, path, "Ww") parent_permissions = self.Rights.authorized(user, parent_path, "w") def prepare(vobject_items, tag=None, write_whole_collection=None): if (write_whole_collection or permissions and not parent_permissions): write_whole_collection = True content_type = environ.get("CONTENT_TYPE", "").split(";")[0] tags = { value: key for key, value in xmlutils.MIMETYPES.items() } tag = radicale_item.predict_tag_of_whole_collection( vobject_items, tags.get(content_type)) if not tag: raise ValueError("Can't determine collection tag") collection_path = pathutils.strip_path(path) elif (write_whole_collection is not None and not write_whole_collection or not permissions and parent_permissions): write_whole_collection = False if tag is None: tag = radicale_item.predict_tag_of_parent_collection( vobject_items) collection_path = posixpath.dirname(pathutils.strip_path(path)) props = None stored_exc_info = None items = [] try: if tag: radicale_item.check_and_sanitize_items( vobject_items, is_collection=write_whole_collection, tag=tag) if write_whole_collection and tag == "VCALENDAR": vobject_components = [] vobject_item, = vobject_items for content in ("vevent", "vtodo", "vjournal"): vobject_components.extend( getattr(vobject_item, "%s_list" % content, [])) vobject_components_by_uid = itertools.groupby( sorted(vobject_components, key=radicale_item.get_uid), radicale_item.get_uid) for uid, components in vobject_components_by_uid: vobject_collection = vobject.iCalendar() for component in components: vobject_collection.add(component) item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_collection) item.prepare() items.append(item) elif write_whole_collection and tag == "VADDRESSBOOK": for vobject_item in vobject_items: item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) elif not write_whole_collection: vobject_item, = vobject_items item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) if write_whole_collection: props = {} if tag: props["tag"] = tag if tag == "VCALENDAR" and vobject_items: if hasattr(vobject_items[0], "x_wr_calname"): calname = vobject_items[0].x_wr_calname.value if calname: props["D:displayname"] = calname if hasattr(vobject_items[0], "x_wr_caldesc"): caldesc = vobject_items[0].x_wr_caldesc.value if caldesc: props["C:calendar-description"] = caldesc radicale_item.check_and_sanitize_props(props) except Exception: stored_exc_info = sys.exc_info() # Use generator for items and delete references to free memory # early def items_generator(): while items: yield items.pop(0) return (items_generator(), tag, write_whole_collection, props, stored_exc_info) try: vobject_items = tuple(vobject.readComponents(content or "")) except Exception as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare(vobject_items) with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT write_whole_collection = (isinstance(item, storage.BaseCollection) or not parent_item.get_meta("tag")) if write_whole_collection: tag = prepared_tag else: tag = parent_item.get_meta("tag") if write_whole_collection: if not self.Rights.authorized(user, path, "w" if tag else "W"): return httputils.NOT_ALLOWED elif not self.Rights.authorized(user, parent_path, "w"): return httputils.NOT_ALLOWED etag = environ.get("HTTP_IF_MATCH", "") if not item and etag: # Etag asked but no item found: item has been removed return httputils.PRECONDITION_FAILED if item and etag and item.etag != etag: # Etag asked but item not matching: item has changed return httputils.PRECONDITION_FAILED match = environ.get("HTTP_IF_NONE_MATCH", "") == "*" if item and match: # Creation asked but item found: item can't be replaced return httputils.PRECONDITION_FAILED if (tag != prepared_tag or prepared_write_whole_collection != write_whole_collection): (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare(vobject_items, tag, write_whole_collection) props = prepared_props if prepared_exc_info: logger.warning("Bad PUT request on %r: %s", path, prepared_exc_info[1], exc_info=prepared_exc_info) return httputils.BAD_REQUEST if write_whole_collection: try: etag = self.Collection.create_collection( path, prepared_items, props).etag except ValueError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST else: prepared_item, = prepared_items if (item and item.uid != prepared_item.uid or not item and parent_item.has_uid(prepared_item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") href = posixpath.basename(pathutils.strip_path(path)) try: etag = parent_item.upload(href, prepared_item).etag except ValueError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST headers = {"ETag": etag} return client.CREATED, headers, None
def _get_attributes_from_path(path): attributes = pathutils.strip_path(path).split("/") if not attributes[0]: attributes.pop() return attributes
def prepare(vobject_items, tag=None, write_whole_collection=None): if (write_whole_collection or permissions and not parent_permissions): write_whole_collection = True content_type = environ.get("CONTENT_TYPE", "").split(";")[0] tags = { value: key for key, value in xmlutils.MIMETYPES.items() } tag = radicale_item.predict_tag_of_whole_collection( vobject_items, tags.get(content_type)) if not tag: raise ValueError("Can't determine collection tag") collection_path = pathutils.strip_path(path) elif (write_whole_collection is not None and not write_whole_collection or not permissions and parent_permissions): write_whole_collection = False if tag is None: tag = radicale_item.predict_tag_of_parent_collection( vobject_items) collection_path = posixpath.dirname(pathutils.strip_path(path)) props = None stored_exc_info = None items = [] try: if tag: radicale_item.check_and_sanitize_items( vobject_items, is_collection=write_whole_collection, tag=tag) if write_whole_collection and tag == "VCALENDAR": vobject_components = [] vobject_item, = vobject_items for content in ("vevent", "vtodo", "vjournal"): vobject_components.extend( getattr(vobject_item, "%s_list" % content, [])) vobject_components_by_uid = itertools.groupby( sorted(vobject_components, key=radicale_item.get_uid), radicale_item.get_uid) for uid, components in vobject_components_by_uid: vobject_collection = vobject.iCalendar() for component in components: vobject_collection.add(component) item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_collection) item.prepare() items.append(item) elif write_whole_collection and tag == "VADDRESSBOOK": for vobject_item in vobject_items: item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) elif not write_whole_collection: vobject_item, = vobject_items item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) if write_whole_collection: props = {} if tag: props["tag"] = tag if tag == "VCALENDAR" and vobject_items: if hasattr(vobject_items[0], "x_wr_calname"): calname = vobject_items[0].x_wr_calname.value if calname: props["D:displayname"] = calname if hasattr(vobject_items[0], "x_wr_caldesc"): caldesc = vobject_items[0].x_wr_caldesc.value if caldesc: props["C:calendar-description"] = caldesc radicale_item.check_and_sanitize_props(props) except Exception: stored_exc_info = sys.exc_info() # Use generator for items and delete references to free memory # early def items_generator(): while items: yield items.pop(0) return (items_generator(), tag, write_whole_collection, props, stored_exc_info)