diff --git a/SHARING.md b/SHARING.md index daebf668..3568dbad 100644 --- a/SHARING.md +++ b/SHARING.md @@ -145,7 +145,7 @@ Shows what kind of ShareTypes are supported ``` curl -u user:pass -H "accept: text/plain" -d "" http://localhost:5232/.sharing/v1/all/info ApiVersion=1 -Status=success +Status='success' FeatureEnabledCollectionByMap=True PermittedCreateCollectionByMap=True FeatureEnabledCollectionByToken=True @@ -183,29 +183,28 @@ Authenticated user as `Owner` has at least read access to `PathMapped` * Input -| Parameter | Owner | -| - | - | -| Owner | implicit(by authentication) | -| PathMapped | mandatory | -| User | optional(default:owner) | -| Permissions | optional(default:r) | -| Enabled | optional(owner) | -| Hidden | optional(owner) | -| Properties | optional | +| Parameter | Type | Requirement | +| - | - | - | +| PathMapped | str | mandatory | +| User | str | optional(default:owner) | +| Permissions | str | optional(default:r) | +| Enabled | bool | optional(owner/default:False) | +| Hidden | bool | optional(owner/default:True) | +| Properties | str | optional | * Output -| Parameter | Value | +| Parameter | Type | Value | | - | - | -| PathOrToken | (autogenerated token) | +| PathOrToken | str | (autogenerated token) | * Example: TEXT ``` curl -u user:pass -d "PathMapped=/user/testcalendar1/" http://localhost:5232/.sharing/v1/token/create ApiVersion=1 -Status=success -PathOrToken=v1/VQR7AmsVRi2ZlFj_JwGpFx-ES5Goyku-gP_YkLh1zUw= +Status='success' +PathOrToken='v1/VQR7AmsVRi2ZlFj_JwGpFx-ES5Goyku-gP_YkLh1zUw=' ``` ###### API Hook "map/create" @@ -220,15 +219,14 @@ Provided `User` has at least read access to `PathOrToken` * Input -| Parameter | Value | +| Parameter | Type | Requirement | | - | - | -| Owner | implicit(by authentication) | -| PathOrToken | mandatory | -| PathMapped | mandatory | -| User | mandatory | -| Permissions | optional(default:r) | -| Enabled | optional(owner) | -| Hidden | optional(owner) | +| PathOrToken | str | mandatory | +| PathMapped | str | mandatory | +| User | str | mandatory | +| Permissions | str | optional(default:r) | +| Enabled | bool | optional(owner/default:False) | +| Hidden | bool | optional(owner/default:True) | | Properties | optional | * Output: result status @@ -252,12 +250,10 @@ Authenticated user as `Owner` or `User` * Input -| Parameter | Filter | -| - | - | -| Owner | implicit(by authentication) | -| User | implicit(by authentication) | -| PathOrToken | optional | -| PathMapped | optional | +| Parameter | Type | Used for | +| - | - | - | +| PathOrToken | str | optional | +| PathMapped | str | optional | * Output: plain/csv/json @@ -265,8 +261,8 @@ Authenticated user as `Owner` or `User` ``` curl -H "accept: text/csv" -u owner:pass -d "" http://localhost:5232/.sharing/v1/map/list -ShareType,PathOrToken,PathMapped,Owner,User,Permissions,EnabledByOwner,EnabledByUser,HiddenByOwner,HiddenByUser,TimestampCreated,TimestampUpdated -map,/user/cal1-from-owner/,/owner/cal1/,owner,user,r,False,False,True,True,1771962120,1771962120 +ShareType,PathOrToken,PathMapped,Owner,User,Permissions,EnabledByOwner,EnabledByUser,HiddenByOwner,HiddenByUser,TimestampCreated,TimestampUpdated,Properties +map,/user/cal1-from-owner/,/owner/cal1/,owner,user,r,False,False,True,True,1771962120,1771962120,{} ``` @@ -280,9 +276,9 @@ Authenticated user is `Owner` * Input -| Parameter | Type | Owner | User | -| - | - | - | - | -| PathOrToken | selector | mandatory | not-permitted | +| Parameter | Type | Used for | as Owner | as User | +| - | - | - | - | - | +| PathOrToken | str | selection | mandatory | not-permitted | * Output: result status @@ -298,16 +294,15 @@ Authenticated user is `Owner` or `User` * Input -| Parameter | Type | Owner | User | -| - | - | - | - | -| PathOrToken | selector | mandatory | mandatory | -| Owner | by authentication | not-permitted | not-permitted | -| PathMapped | adjustable | optional | not-permitted | -| User | adjustable | optional | not-permitted | -| Permissions | adjustable | optional | not-permitted | -| Enabled | adjustable | optional(owner) | optional(user) | -| Hidden | adjustable | optional(owner) | optional(user) | -| Properties | adjustable | optional | optional | +| Parameter | Type | Used for | Owner | User | +| - | - | - | - | - | +| PathOrToken | str | selection | mandatory | mandatory | +| PathMapped | str | adjust | optional | not-permitted | +| User | str | adjust | optional | not-permitted | +| Permissions | str | adjust | optional | not-permitted | +| Enabled | bool | adjust | optional(owner) | optional(user) | +| Hidden | bool | adjust | optional(owner) | optional(user) | +| Properties | str | adjust | optional | optional | * Output: result status @@ -321,9 +316,9 @@ Authenticated user is `Owner` or `User` * Input -| Parameter | Type | Owner | User | -| - | - | - | - | -| PathOrToken | selector | mandatory | mandatory | +| Parameter | Type | Used for | Owner | User | +| - | - | - | - | - | +| PathOrToken | selection | mandatory | mandatory | * Output: result status @@ -340,7 +335,7 @@ Status=success ``` curl -u owner:pass -d '{"PathOrToken": "/user/cal1-from-owner/", "PathMapped": "/owner/cal1/", "User": "user"} http://localhost:5232/.sharing/v1/map/unhide ApiVersion=1 -Status=success +Status='success' ``` ## Properties Overlay diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 8dba8314..6390af0e 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -72,6 +72,20 @@ API_HOOKS_V1: Sequence[str] = ('list', 'create', 'delete', 'update', 'hide', 'un API_SHARE_TOGGLES_V1: Sequence[str] = ('hide', 'unhide', 'enable', 'disable') +API_TYPES_V1: dict[str, type] = { + "ApiVersion": int, + "Status": str, + "Lines": int, + "ShareType": str, + "PathOrToken": str, + "PathMapped:": str, + "Owner": str, + "User": str, + "Permissions": str, + "Enabled": bool, + "Hidden": bool, + "Properties": str} + TOKEN_PATTERN_V1: str = "(v1/[a-zA-Z0-9_=\\-]{44})" PATH_PATTERN: str = "([a-zA-Z0-9/.\\-]+)" # TODO: extend or find better source @@ -1056,7 +1070,10 @@ class BaseSharing: if output_format == "txt": for key in answer: if key != 'Content': - answer_array.append(key + '=' + str(answer[key])) + if API_TYPES_V1[key] is bool or API_TYPES_V1[key] is int: + answer_array.append(key + '=' + str(answer[key])) + else: + answer_array.append(key + "='" + str(answer[key]) + "'") if 'Content' in answer and answer['Content'] is not None: csv = io.StringIO() writer = DictWriter(csv, fieldnames=DB_FIELDS_V1, delimiter=';') diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index aef27c29..41670242 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -319,13 +319,13 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** list (form->csv)") form_array = [] _, headers, answer = self._sharing_api_form(sharing_type, "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=not-found" in answer + assert "Status='not-found'" in answer assert "Lines=0" in answer logging.info("\n*** list (json->text)") json_dict = {} _, headers, answer = self._sharing_api_json(sharing_type, "list", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain") - assert "Status=not-found" in answer + assert "Status='not-found'" in answer assert "Lines=0" in answer logging.info("\n*** list (json->json)") @@ -338,10 +338,10 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** create a token -> 200") form_array = ["PathMapped=/owner/collectionL1/"] _, 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 + assert "Status='success'" in answer + assert "PathOrToken='" in answer # extract token - match = re.search('PathOrToken=(.+)', answer) + match = re.search("PathOrToken='(.+)'", answer) if match: token = match.group(1) logging.info("received token %r", token) @@ -360,20 +360,20 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** list/all (form->csv)") form_array = [] _, headers, answer = self._sharing_api_form("all", "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "Lines=2" in answer logging.info("\n*** delete token -> 200") form_array = ["PathOrToken=" + token] _, headers, answer = self._sharing_api_form("token", "delete", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer logging.info("\n*** delete share -> 200") form_array = [] form_array.append("PathOrToken=/user/collectionL2-shared-by-owner/") form_array.append("PathMapped=/owner/collectionL2/") _, headers, answer = self._sharing_api_form("map", "delete", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer def test_sharing_api_token_basic(self) -> None: """share-by-token API tests.""" @@ -409,10 +409,10 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** create token#1 (form->text)") form_array = ["PathMapped=/owner/collection1/"] _, 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 + assert "Status='success'" in answer + assert "PathOrToken='" in answer # extract token - match = re.search('PathOrToken=(.+)', answer) + match = re.search("PathOrToken='(.+)'", answer) if match: token1 = match.group(1) logging.info("received token %r", token1) @@ -422,10 +422,10 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** create token#2 (json->text)") json_dict = {'PathMapped': "/owner/collection2/"} _, 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 "Status='success'" in answer assert "Token=" in answer # extract token - match = re.search('Token=(.+)', answer) + match = re.search("Token='(.+)'", answer) if match: token2 = match.group(1) logging.info("received token %r", token2) @@ -435,14 +435,14 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** lookup token#1 (form->text)") form_array = ["PathOrToken=" + token1] _, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "Lines=1" in answer assert "/owner/collection1/" 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 "Status='success'" in answer assert "Lines=1" in answer assert "/owner/collection2/" in answer @@ -457,7 +457,7 @@ class TestSharingApiSanity(BaseTest): 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 "Status='success'" in answer assert "Lines=2" in answer assert "/owner/collection1/" in answer assert "/owner/collection2/" in answer @@ -465,7 +465,7 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** lookup tokens (form->csv)") form_array = [] _, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array, accept="text/csv") - assert "Status=success" not in answer + 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 @@ -474,24 +474,24 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** delete token#1 (form->text)") form_array = ["PathOrToken=" + token1] _, headers, answer = self._sharing_api_form("token", "delete", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer logging.info("\n*** lookup token#1 (form->text) -> should not be there anymore") form_array = ["PathOrToken=" + token1] _, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=not-found" in answer + assert "Status='not-found'" in answer assert "Lines=0" in answer logging.info("\n*** lookup tokens (form->text) -> still one should be there") 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 "Status='success'" in answer assert "Lines=1" in answer logging.info("\n*** disable token#2 as owner (form->text)") form_array = ["PathOrToken=" + token2] _, headers, answer = self._sharing_api_form("token", "disable", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer logging.info("\n*** lookup token#2 (json->json) -> check for not enabled") json_dict = {'PathOrToken': token2} @@ -512,7 +512,7 @@ class TestSharingApiSanity(BaseTest): form_array = [] form_array.append("PathOrToken=" + token2) _, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "Lines=1" in answer assert "True;True;True;True" in answer @@ -520,13 +520,13 @@ class TestSharingApiSanity(BaseTest): form_array = [] form_array.append("PathOrToken=" + token2) _, headers, answer = self._sharing_api_form("token", "hide", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer logging.info("\n*** lookup token#2 (form->text) -> check for hidden") form_array = [] form_array.append("PathOrToken=" + token2) _, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "Lines=1" in answer assert "True;True;True;True" in answer @@ -606,10 +606,10 @@ class TestSharingApiSanity(BaseTest): form_array = [] form_array.append("PathMapped=" + path_base) _, headers, answer = self._sharing_api_form("token", "create", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "PathOrToken=" in answer # extract token - match = re.search('PathOrToken=(.+)', answer) + match = re.search("PathOrToken='(.+)'", answer) if match: token = match.group(1) logging.info("received token %r", token) @@ -620,10 +620,10 @@ class TestSharingApiSanity(BaseTest): form_array = [] form_array.append("PathMapped=" + path_base2) _, headers, answer = self._sharing_api_form("token", "create", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "PathOrToken=" in answer # extract token - match = re.search('PathOrToken=(.+)', answer) + match = re.search("PathOrToken='(.+)'", answer) if match: token2 = match.group(1) logging.info("received token %r", token2) @@ -633,7 +633,7 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** enable token (form->text)") form_array = ["PathOrToken=" + token] _, headers, answer = self._sharing_api_form("token", "enable", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer logging.info("\n*** fetch collection using invalid token (without credentials)") _, headers, answer = self.request("GET", path_token + "v1/invalidtoken", check=401) @@ -645,7 +645,7 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** disable token (form->text)") form_array = ["PathOrToken=" + token] _, headers, answer = self._sharing_api_form("token", "disable", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer logging.info("\n*** fetch collection using disabled token (without credentials)") _, headers, answer = self.request("GET", path_token + token, check=401) @@ -653,7 +653,7 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** enable token (form->text)") form_array = ["PathOrToken=" + token] _, headers, answer = self._sharing_api_form("token", "enable", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer logging.info("\n*** fetch collection using token (without credentials)") _, headers, answer = self.request("GET", path_token + token, check=200) @@ -2207,7 +2207,7 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** list/all (form->csv)") form_array = [] _, headers, answer = self._sharing_api_form("map", "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "Lines=1" in answer # read collection @@ -2227,7 +2227,7 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** list/all (form->csv)") form_array = [] _, headers, answer = self._sharing_api_form("map", "list", check=200, login="owner:ownerpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer assert "Lines=1" in answer # read collection @@ -2987,7 +2987,7 @@ permissions: RrWw""") form_array.append("Properties='C:calendar-description'='ICAL-USER-NEW'") form_array.append("Properties='ICAL:calendar-color'='#CCCCCC'") _, headers, answer = self._sharing_api_form("map", "update", check=200, login="user:userpw", form_array=form_array) - assert "Status=success" in answer + assert "Status='success'" in answer # verify overlay as user logging.info("\n*** PROPFIND collection user (overlay) -> ok")