sharing: cosmetics, check existence of collection before create
This commit is contained in:
@@ -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]
|
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
|
## Sharing Implementation
|
||||||
|
|
||||||
@@ -93,7 +93,6 @@ Permissions are filtered by provided `Permissions`.
|
|||||||
|
|
||||||
#### CxDav request "(MOVE)"
|
#### CxDav request "(MOVE)"
|
||||||
|
|
||||||
|
|
||||||
* Action
|
* Action
|
||||||
* map source
|
* map source
|
||||||
* map destination
|
* map destination
|
||||||
@@ -111,7 +110,6 @@ Permissions are filtered by provided `Permissions`.
|
|||||||
* `permissions_filter` by `Permissions` (of `to_path`)
|
* `permissions_filter` by `Permissions` (of `to_path`)
|
||||||
* `to_permissions_filter` by `Permissions` (of `to_path`)
|
* `to_permissions_filter` by `Permissions` (of `to_path`)
|
||||||
|
|
||||||
|
|
||||||
## Sharing Configuration Store
|
## Sharing Configuration Store
|
||||||
|
|
||||||
Types of supported sharing configuration:
|
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 owner (can be combined with "create")
|
||||||
* unhide map as user (explicit required to avoid sudden visible share)
|
* unhide map as user (explicit required to avoid sudden visible share)
|
||||||
|
|
||||||
|
|
||||||
### Sharing Access via Tokens
|
### Sharing Access via Tokens
|
||||||
|
|
||||||
(_>= 3.7.0_)
|
(_>= 3.7.0_)
|
||||||
@@ -190,6 +187,8 @@ Token-based sharing can be accessed after retrieving the token via
|
|||||||
|
|
||||||
Token-URI: `/.token/<Token>`
|
Token-URI: `/.token/<Token>`
|
||||||
|
|
||||||
|
Note: requests to not enabled or not even defined tokens will resul tin _401 Not Authorized_
|
||||||
|
|
||||||
#### Permission Control
|
#### Permission Control
|
||||||
|
|
||||||
* `permit_create_token`
|
* `permit_create_token`
|
||||||
|
|||||||
@@ -685,6 +685,14 @@ class BaseSharing:
|
|||||||
logger.error(api_info + ": missing PathMapped")
|
logger.error(api_info + ": missing PathMapped")
|
||||||
return httputils.bad_request("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 Permissions is None:
|
||||||
if ShareType == "token":
|
if ShareType == "token":
|
||||||
Permissions = self.default_permissions_create_token
|
Permissions = self.default_permissions_create_token
|
||||||
@@ -733,6 +741,7 @@ class BaseSharing:
|
|||||||
else:
|
else:
|
||||||
User = user
|
User = user
|
||||||
|
|
||||||
|
|
||||||
# v1: create uuid token with 2x 32 bytes = 256 bit
|
# v1: create uuid token with 2x 32 bytes = 256 bit
|
||||||
token = "v1/" + str(base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), 'utf-8')
|
token = "v1/" + str(base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), 'utf-8')
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,9 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
form_array: Sequence[str]
|
form_array: Sequence[str]
|
||||||
json_dict: dict
|
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)):
|
for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)):
|
||||||
logging.info("\n*** test: %s", db_type)
|
logging.info("\n*** test: %s", db_type)
|
||||||
self.configure({"sharing": {"type": db_type}})
|
self.configure({"sharing": {"type": db_type}})
|
||||||
@@ -406,8 +409,16 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
json_dict = {}
|
json_dict = {}
|
||||||
_, headers, answer = self._sharing_api_json("token", "create", 400, login="owner:ownerpw", json_dict=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)")
|
logging.info("\n*** create token#1 without existing collection (form->text)")
|
||||||
form_array = ["PathMapped=/owner/collection1/"]
|
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)
|
_, 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
|
assert "PathOrToken='" in answer
|
||||||
@@ -420,7 +431,7 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert False
|
assert False
|
||||||
|
|
||||||
logging.info("\n*** create token#2 (json->text)")
|
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")
|
_, 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
|
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)
|
_, 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 "Lines=1" in answer
|
||||||
assert "/owner/collection1/" in answer
|
assert path_base1 in answer
|
||||||
|
|
||||||
logging.info("\n*** lookup token#2 (json->text")
|
logging.info("\n*** lookup token#2 (json->text")
|
||||||
json_dict = {'PathOrToken': token2}
|
json_dict = {'PathOrToken': token2}
|
||||||
_, headers, answer = self._sharing_api_json("token", "list", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain")
|
_, 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 "Lines=1" in answer
|
||||||
assert "/owner/collection2/" in answer
|
assert path_base2 in answer
|
||||||
|
|
||||||
logging.info("\n*** lookup token#2 (json->json)")
|
logging.info("\n*** lookup token#2 (json->json)")
|
||||||
json_dict = {'PathOrToken': token2}
|
json_dict = {'PathOrToken': token2}
|
||||||
@@ -452,15 +463,15 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
answer_dict = json.loads(answer)
|
answer_dict = json.loads(answer)
|
||||||
assert answer_dict['Status'] == "success"
|
assert answer_dict['Status'] == "success"
|
||||||
assert answer_dict['Lines'] == 1
|
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)")
|
logging.info("\n*** lookup tokens (form->text)")
|
||||||
form_array = []
|
form_array = []
|
||||||
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=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 "Lines=2" in answer
|
||||||
assert "/owner/collection1/" in answer
|
assert path_base1 in answer
|
||||||
assert "/owner/collection2/" in answer
|
assert path_base2 in answer
|
||||||
|
|
||||||
logging.info("\n*** lookup tokens (form->csv)")
|
logging.info("\n*** lookup tokens (form->csv)")
|
||||||
form_array = []
|
form_array = []
|
||||||
@@ -468,8 +479,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert "Status='success'" not in answer
|
assert "Status='success'" not in answer
|
||||||
assert "Lines=2" not in answer
|
assert "Lines=2" not in answer
|
||||||
assert ";".join(sharing.DB_FIELDS_V1) in answer
|
assert ";".join(sharing.DB_FIELDS_V1) in answer
|
||||||
assert "/owner/collection1/" in answer
|
assert path_base1 in answer
|
||||||
assert "/owner/collection2/" in answer
|
assert path_base2 in answer
|
||||||
|
|
||||||
logging.info("\n*** delete token#1 (form->text)")
|
logging.info("\n*** delete token#1 (form->text)")
|
||||||
form_array = ["PathOrToken=" + token1]
|
form_array = ["PathOrToken=" + token1]
|
||||||
@@ -561,6 +572,10 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert answer_dict['Status'] == "not-found"
|
assert answer_dict['Status'] == "not-found"
|
||||||
assert answer_dict['Lines'] == 0
|
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:
|
def test_sharing_api_token_usage(self) -> None:
|
||||||
"""share-by-token API tests - real usage."""
|
"""share-by-token API tests - real usage."""
|
||||||
self.configure({"auth": {"type": "htpasswd",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
|
|||||||
Reference in New Issue
Block a user