diff --git a/SHARING.md b/SHARING.md index 8200f1e6..42867ce6 100644 --- a/SHARING.md +++ b/SHARING.md @@ -3,7 +3,7 @@ Static collection sharing without permissions filter using soft-links (Unix-only) is supported since storage type `multifilesystem` was implemented, see (Wiki: Sharing Collections)[https://github.com/Kozea/Radicale/wiki/Sharing-Collections] -With 3.7.0 a major extension was implemented using internal mapping configuration stored in a database and a management API. +With 3.7.0 major extension was implemented using internal mapping configuration stored in a database and a management API. ## Sharing Implementation @@ -93,7 +93,6 @@ Permissions are filtered by provided `Permissions`. #### CxDav request "(MOVE)" - * Action * map source * map destination @@ -111,7 +110,6 @@ Permissions are filtered by provided `Permissions`. * `permissions_filter` by `Permissions` (of `to_path`) * `to_permissions_filter` by `Permissions` (of `to_path`) - ## Sharing Configuration Store Types of supported sharing configuration: @@ -181,7 +179,6 @@ In case share should be visible using PROPFIND * unhide map as owner (can be combined with "create") * unhide map as user (explicit required to avoid sudden visible share) - ### Sharing Access via Tokens (_>= 3.7.0_) @@ -190,6 +187,8 @@ Token-based sharing can be accessed after retrieving the token via Token-URI: `/.token/` +Note: requests to not enabled or not even defined tokens will resul tin _401 Not Authorized_ + #### Permission Control * `permit_create_token` diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 6390af0e..b6b5d8a4 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -685,6 +685,14 @@ class BaseSharing: logger.error(api_info + ": missing PathMapped") return httputils.bad_request("Missing PathMapped") + ## check whether collection exists + with self._storage.acquire_lock("r", user, path=PathMapped): + item = next(iter(self._storage.discover(PathMapped)), None) + if not item: + return httputils.NOT_FOUND + if not isinstance(item, storage.BaseCollection): + return httputils.METHOD_NOT_ALLOWED + if Permissions is None: if ShareType == "token": Permissions = self.default_permissions_create_token @@ -733,6 +741,7 @@ class BaseSharing: else: User = user + # v1: create uuid token with 2x 32 bytes = 256 bit token = "v1/" + str(base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), 'utf-8') diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 303f3a79..b2111771 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -394,6 +394,9 @@ class TestSharingApiSanity(BaseTest): form_array: Sequence[str] json_dict: dict + path_base1 = "/owner/collection1.ics/" + path_base2 = "/owner/collection2.ics/" + for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)): logging.info("\n*** test: %s", db_type) self.configure({"sharing": {"type": db_type}}) @@ -406,8 +409,16 @@ class TestSharingApiSanity(BaseTest): json_dict = {} _, headers, answer = self._sharing_api_json("token", "create", 400, login="owner:ownerpw", json_dict=json_dict) - logging.info("\n*** create token#1 (form->text)") - form_array = ["PathMapped=/owner/collection1/"] + logging.info("\n*** create token#1 without existing collection (form->text)") + form_array = ["PathMapped=" + path_base1] + _, headers, answer = self._sharing_api_form("token", "create", check=404, login="owner:ownerpw", form_array=form_array) + + logging.info("\n*** create collection*") + self.mkcalendar(path_base1, login="owner:ownerpw") + self.mkcalendar(path_base2, login="owner:ownerpw") + + logging.info("\n*** create token#1 with existing collection (form->text)") + form_array = ["PathMapped=" + path_base1] _, headers, answer = self._sharing_api_form("token", "create", check=200, login="owner:ownerpw", form_array=form_array) assert "Status='success'" in answer assert "PathOrToken='" in answer @@ -420,7 +431,7 @@ class TestSharingApiSanity(BaseTest): assert False logging.info("\n*** create token#2 (json->text)") - json_dict = {'PathMapped': "/owner/collection2/"} + json_dict = {'PathMapped': path_base2} _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain") assert "Status='success'" in answer assert "Token=" in answer @@ -437,14 +448,14 @@ class TestSharingApiSanity(BaseTest): _, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array) assert "Status='success'" in answer assert "Lines=1" in answer - assert "/owner/collection1/" in answer + assert path_base1 in answer logging.info("\n*** lookup token#2 (json->text") json_dict = {'PathOrToken': token2} _, headers, answer = self._sharing_api_json("token", "list", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain") assert "Status='success'" in answer assert "Lines=1" in answer - assert "/owner/collection2/" in answer + assert path_base2 in answer logging.info("\n*** lookup token#2 (json->json)") json_dict = {'PathOrToken': token2} @@ -452,15 +463,15 @@ class TestSharingApiSanity(BaseTest): answer_dict = json.loads(answer) assert answer_dict['Status'] == "success" assert answer_dict['Lines'] == 1 - assert answer_dict['Content'][0]['PathMapped'] == "/owner/collection2/" + assert answer_dict['Content'][0]['PathMapped'] == path_base2 logging.info("\n*** lookup tokens (form->text)") form_array = [] _, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array) assert "Status='success'" in answer assert "Lines=2" in answer - assert "/owner/collection1/" in answer - assert "/owner/collection2/" in answer + assert path_base1 in answer + assert path_base2 in answer logging.info("\n*** lookup tokens (form->csv)") form_array = [] @@ -468,8 +479,8 @@ class TestSharingApiSanity(BaseTest): assert "Status='success'" not in answer assert "Lines=2" not in answer assert ";".join(sharing.DB_FIELDS_V1) in answer - assert "/owner/collection1/" in answer - assert "/owner/collection2/" in answer + assert path_base1 in answer + assert path_base2 in answer logging.info("\n*** delete token#1 (form->text)") form_array = ["PathOrToken=" + token1] @@ -561,6 +572,10 @@ class TestSharingApiSanity(BaseTest): assert answer_dict['Status'] == "not-found" assert answer_dict['Lines'] == 0 + logging.info("\n*** delete collection*") + self.delete(path_base1, login="owner:ownerpw") + self.delete(path_base2, login="owner:ownerpw") + def test_sharing_api_token_usage(self) -> None: """share-by-token API tests - real usage.""" self.configure({"auth": {"type": "htpasswd",