From 6a2dd3322571f9bc40abf4644caef04957a9fc37 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 26 Feb 2026 05:21:04 +0100 Subject: [PATCH 1/6] align order --- config | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config b/config index 96f18aca..6728142f 100644 --- a/config +++ b/config @@ -311,12 +311,12 @@ # Sharing database path for type 'files' #database_path = (filesystem_folder)/collection-db/files -# Share collection by map -#collection_by_map = false - # Share collection by token #collection_by_token = false +# Share collection by map +#collection_by_map = false + [web] From a8bceb3ccd560915c66f764e8c98cd7744b4e051 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 26 Feb 2026 05:27:47 +0100 Subject: [PATCH 2/6] align order --- radicale/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/radicale/config.py b/radicale/config.py index bc015ee6..465a882b 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -468,13 +468,13 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "value": "", "help": "database path", "type": filepath}), - ("collection_by_map", { - "value": "false", - "help": "enable sharing of collection by map", - "type": bool}), ("collection_by_token", { "value": "false", "help": "enable sharing of collection by token", + "type": bool}), + ("collection_by_map", { + "value": "false", + "help": "enable sharing of collection by map", "type": bool})])), ("hook", OrderedDict([ ("type", { From cfe2f23d60d1d83d7947d80b4718f70ad2c088f9 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 26 Feb 2026 05:34:37 +0100 Subject: [PATCH 3/6] add forgotten doc for 3.3.0 --- radicale/rights/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/radicale/rights/__init__.py b/radicale/rights/__init__.py index 7aec9d4e..ad5ff9b6 100644 --- a/radicale/rights/__init__.py +++ b/radicale/rights/__init__.py @@ -27,6 +27,10 @@ Permissions: (CalDAV/CardDAV is susceptible to expensive search requests) - W: write collections (excluding address books and calendars) - w: write address book and calendar collections + - D: allow deleting a collection in case permit_delete_collection=False (>= 3.3.0) + - d: deny deleting a collection in case permit_delete_collection=True (>= 3.3.0) + - O: allow overwriting a collection in case permit_overwrite_collection=False (>= 3.3.0) + - o: deny overwriting a collection in case permit_overwrite_collection=True (>= 3.3.0) Take a look at the class ``BaseRights`` if you want to implement your own. From 3ad1b1a12a7c1c6e5db1898e228b3777ddd22275 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 26 Feb 2026 08:12:49 +0100 Subject: [PATCH 4/6] add rights value check --- radicale/config.py | 7 +++++++ radicale/rights/__init__.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/radicale/config.py b/radicale/config.py index 465a882b..54058d32 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -66,6 +66,13 @@ def positive_float(value: Any) -> float: return value +def rights_permission(value: Any) -> str: + for permission in value: + if permission not in rights.INTERNAL_PERMISSIONS: + raise ValueError("unsupported permssion %r found in: %r" % (permission, value)) + return value + + def logging_level(value: Any) -> str: if value not in ("debug", "info", "warning", "error", "critical"): raise ValueError("unsupported level: %r" % value) diff --git a/radicale/rights/__init__.py b/radicale/rights/__init__.py index ad5ff9b6..c70f0f6e 100644 --- a/radicale/rights/__init__.py +++ b/radicale/rights/__init__.py @@ -43,6 +43,8 @@ from radicale import config, utils INTERNAL_TYPES: Sequence[str] = ("authenticated", "owner_write", "owner_only", "from_file") +INTERNAL_PERMISSIONS: str = "RriWwDdOoTtMm" + def load(configuration: "config.Configuration") -> "BaseRights": """Load the rights module chosen in configuration.""" From 1cb622e90305f5a4118b82003a9a4b20f9e3eb91 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 26 Feb 2026 08:14:49 +0100 Subject: [PATCH 5/6] sharing: add global and conditional permissions support --- DOCUMENTATION.md | 38 ++++++++++++++++++++++++++++++++++ config | 16 +++++++++++++++ radicale/config.py | 18 +++++++++++++++- radicale/rights/__init__.py | 4 ++++ radicale/sharing/__init__.py | 40 ++++++++++++++++++++++++++++++++---- 5 files changed, 111 insertions(+), 5 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 37838ed6..781f7c17 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2102,6 +2102,40 @@ Share collection by map Default: `false` +##### permit_create_token + +_(>= 3.7.0)_ + +Permit create of token-based sharing + +Default: `false` + +* If `False` it can be explicitly granted by `permissions: t` +* If `True` it can be explicitly forbidden by `permissions: T` + +##### permit_create_map + +_(>= 3.7.0)_ + +Permit create of map-based sharing + +Default: `false` + +* If `False` it can be explicitly granted by `permissions: m` +* If `True` it can be explicitly forbidden by `permissions: M` + +##### default_permissions_create_token + +Default permissions for create token-based sharing + +Default: `r` + +##### default_permissions_create_map + +Default permissions for map-based sharing + +Default: `r` + ## Supported Clients Radicale has been tested with: @@ -2326,6 +2360,10 @@ The following `permissions` are recognized: * **d:** deny deleting a collection in case `permit_delete_collection=True` _(>= 3.3.0)_ * **O:** allow overwriting a collection in case `permit_overwrite_collection=False` _(>= 3.3.0)_ * **o:** deny overwriting a collection in case `permit_overwrite_collection=True` _(>= 3.3.0)_ +* **T:** permit create of token-based sharing of collection in case `permit_create_token=False` _(>= 3.7.0)_ +* **t:** deny create of token-based sharing of collection in case `permit_create_token=True` _(>= 3.7.0)_ +* **M:** permit create of map-based sharing of collection in case `permit_create_map= False` _(>= 3.7.0)_ +* **m:** deny create of map-based sharing of collection in case `permit_create_map=True` _(>= 3.7.0)_ ### Storage diff --git a/config b/config index 6728142f..8f89e223 100644 --- a/config +++ b/config @@ -317,6 +317,22 @@ # Share collection by map #collection_by_map = false +# Permit create of token-based sharing +# If False it can be explicitly granted by permissions: t +# If True it can be explicitly forbidden by permissions: T +#permit_create_token = false + +# Permit create of map-based sharing +# If False it can be explicitly granted by permissions: m +# If True it can be explicitly forbidden by permissions: M +#permit_create_map = false + +# Default permissions for token-based sharing +#default_permissions_create_token = r + +# Default permissions for map-based sharing +#default_permissions_create_map = r + [web] diff --git a/radicale/config.py b/radicale/config.py index 54058d32..abfa91e7 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -482,7 +482,23 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ ("collection_by_map", { "value": "false", "help": "enable sharing of collection by map", - "type": bool})])), + "type": bool}), + ("permit_create_token", { + "value": "false", + "help": "permit create of token-based sharing", + "type": bool}), + ("permit_create_map", { + "value": "false", + "help": "permit create of map-based sharing", + "type": bool}), + ("default_permissions_create_token", { + "value": "r", + "help": "default permissions for token-based sharing", + "type": rights_permission}), + ("default_permissions_create_map", { + "value": "r", + "help": "default permissions for map-based sharing", + "type": rights_permission})])), ("hook", OrderedDict([ ("type", { "value": "none", diff --git a/radicale/rights/__init__.py b/radicale/rights/__init__.py index c70f0f6e..a232598d 100644 --- a/radicale/rights/__init__.py +++ b/radicale/rights/__init__.py @@ -31,6 +31,10 @@ Permissions: - d: deny deleting a collection in case permit_delete_collection=True (>= 3.3.0) - O: allow overwriting a collection in case permit_overwrite_collection=False (>= 3.3.0) - o: deny overwriting a collection in case permit_overwrite_collection=True (>= 3.3.0) + - T: permit create of token-based sharing of collection in case permit_create_token=False (>= 3.7.0) + - t: deny create of token-based sharing of collection in case permit_create_token=True (>= 3.7.0) + - M: permit create of map-based sharing of collection in case permit_create_map= False (>= 3.7.0) + - m: deny create of map-based sharing of collection in case permit_create_map=True (>= 3.7.0) Take a look at the class ``BaseRights`` if you want to implement your own. diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 71e072b7..d96d3d7d 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -102,8 +102,16 @@ class BaseSharing: # Sharing self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map") self.sharing_collection_by_token = configuration.get("sharing", "collection_by_token") + self.permit_create_token = configuration.get("sharing", "permit_create_token") + self.permit_create_map = configuration.get("sharing", "permit_create_map") + self.default_permissions_create_token = configuration.get("sharing", "default_permissions_create_token") + self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map") logger.info("sharing.collection_by_map : %s", self.sharing_collection_by_map) logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token) + logger.info("sharing.permit_create_token: %s", self.permit_create_token) + logger.info("sharing.permit_create_map : %s", self.permit_create_map) + logger.info("sharing.default_permissions_create_token: %r", self.default_permissions_create_token) + logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map) if ((self.sharing_collection_by_map is False) and (self.sharing_collection_by_token is False)): logger.info("sharing disabled as no feature is enabled") @@ -634,7 +642,13 @@ class BaseSharing: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/" + api_info + ": start") if 'Permissions' not in request_data: - Permissions = "r" + if ShareType == "token": + Permissions = self.default_permissions_create_token + elif ShareType == "map": + Permissions = self.default_permissions_create_map + else: + # default + Permissions = "r" if 'Enabled' in request_data: EnabledByOwner = config._convert_to_bool(request_data['Enabled']) @@ -652,10 +666,19 @@ class BaseSharing: if ShareType == "token": # check access Permissions access = Access(self._rights, user, str(PathMapped)) # PathMapped is mandatory - if not access.check("r") and "i" not in access.permissions: + if not access.check("r"): logger.info("Add sharing-by-token: access to %r not allowed for user %r", PathMapped, user) return httputils.NOT_ALLOWED + if self.permit_create_token is False: + if "t" not in access.permissions: + logger.info("Add sharing-by-token: access to %r not allowed for user %r (permit=False but explict grant misses 't')", PathMapped, user) + return httputils.NOT_ALLOWED + else: + if "T" in access.permissions: + logger.info("Add sharing-by-token: access to %r not allowed for user %r (permit=True but denied by 'T')", PathMapped, user) + return httputils.NOT_ALLOWED + # v1: create uuid token with 2x 32 bytes = 256 bit token = "v1/" + str(base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), 'utf-8') @@ -690,9 +713,18 @@ class BaseSharing: logger.info("Add sharing-by-map: access to path(mapped) %r not allowed for owner %r", PathMapped, Owner) return httputils.NOT_ALLOWED + if self.permit_create_map is False: + if "m" not in access.permissions: + logger.info("Add sharing-by-map: access to %r not allowed for user %r (permit=False but explicit grant misses 'm')", PathMapped, user) + return httputils.NOT_ALLOWED + else: + if "M" in access.permissions: + logger.info("Add sharing-by-map: access to %r not allowed for user %r (permit=True but denied by 'M')", PathMapped, user) + return httputils.NOT_ALLOWED + access = Access(self._rights, str(User), PathOrToken) - if not access.check("r") and "i" not in access.permissions: - logger.info("Add sharing-by-map: access to path %r not allowed for user %r", PathOrToken, user) + if not access.check("r"): + logger.info("Add sharing-by-map: access to path %r not allowed for user %r", PathOrToken, User) return httputils.NOT_ALLOWED # check whether share is already existing as real collection From a0c893839a13f94e9a2fd224785c8a70edfe5829 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 26 Feb 2026 08:15:21 +0100 Subject: [PATCH 6/6] sharing: tests for global and conditional permissions support --- radicale/tests/test_sharing.py | 367 ++++++++++++++++++++++++++++++++- 1 file changed, 366 insertions(+), 1 deletion(-) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index febe4732..2edb07fa 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -185,6 +185,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "true", @@ -272,6 +274,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -458,6 +462,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -613,6 +619,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -800,6 +808,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -892,6 +902,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -1084,6 +1096,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -1183,6 +1197,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -1328,6 +1344,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -1428,6 +1446,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -1596,6 +1616,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -1795,6 +1817,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -1911,6 +1935,8 @@ class TestSharingApiSanity(BaseTest): "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -2048,12 +2074,14 @@ class TestSharingApiSanity(BaseTest): assert answer_dict['Lines'] == 1 def test_sharing_api_create_conflict(self) -> None: - """sharing API usage tests related to update.""" + """sharing API usage tests related to conflicts.""" self.configure({"auth": {"type": "htpasswd", "htpasswd_filename": self.htpasswd_file_path, "htpasswd_encryption": "plain"}, "sharing": { "type": "csv", + "permit_create_map": True, + "permit_create_token": True, "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", @@ -2127,3 +2155,340 @@ class TestSharingApiSanity(BaseTest): json_dict['Enabled'] = "True" json_dict['Hidden'] = "False" _, headers, answer = self._sharing_api_json("map", "create", check=409, login="owner1:owner1pw", json_dict=json_dict) + + def test_sharing_api_permissions_global(self) -> None: + """sharing API usage tests related to global permissions.""" + self.configure({"auth": {"type": "htpasswd", + "htpasswd_filename": self.htpasswd_file_path, + "htpasswd_encryption": "plain"}, + "sharing": { + "type": "csv", + "permit_create_map": True, + "permit_create_token": True, + "collection_by_map": "True", + "collection_by_token": "True"}, + "logging": {"request_header_on_debug": "False", + "response_content_on_debug": "False", + "request_content_on_debug": "True"}, + "rights": {"type": "owner_only"}}) + json_dict: dict + + path_user1 = "/user1/calendarPGu1.ics/" + path_owner1 = "/owner1/calendarPGo1.ics/" + + logging.info("\n*** prepare") + self.mkcalendar(path_owner1, login="owner1:owner1pw") + + for db_type in sharing.INTERNAL_TYPES: + if db_type == "none": + continue + logging.info("\n*** test: %s", db_type) + self.configure({"sharing": {"type": db_type}}) + + # create map + json_dict = {} + json_dict['User'] = "user1" + json_dict['PathMapped'] = path_owner1 + json_dict['PathOrToken'] = path_user1 + + logging.info("\n*** create map user1/owner1 but globally disabled -> 403") + self.configure({"sharing": {"permit_create_map": "False"}}) + _, headers, answer = self._sharing_api_json("map", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1 but globally enabled -> 200") + self.configure({"sharing": {"permit_create_map": "True"}}) + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + # create token + json_dict = {} + json_dict['User'] = "user1" + json_dict['PathMapped'] = path_owner1 + + logging.info("\n*** create token owner1 but globally disabled -> 403") + self.configure({"sharing": {"permit_create_token": "False"}}) + _, headers, answer = self._sharing_api_json("token", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1 but globally enabled -> 200") + self.configure({"sharing": {"permit_create_token": "True"}}) + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + def test_sharing_api_permissions_rights(self) -> None: + """sharing API usage tests related to rights permissions.""" + rights_file_path = os.path.join(self.colpath, "rights") + with open(rights_file_path, "w") as f: + f.write("""\ +[default-collection] +user: .+ +collection: {user} +permissions: RrWw +[owner1-T] +user: owner1 +collection: {user}/cal-T(/.*)? +permissions: RrWwT +[owner1-t] +user: owner1 +collection: {user}/cal-t(/.*)? +permissions: RrWwt +[owner1-M] +user: owner1 +collection: {user}/cal-M(/.*)? +permissions: RrWwM +[owner1-m] +user: owner1 +collection: {user}/cal-m(/.*)? +permissions: RrWwm +[default] +user: .+ +collection: {user}(/.*)? +permissions: RrWw""") + + self.configure({"rights": {"file": rights_file_path}}) + self.configure({"auth": {"type": "htpasswd", + "htpasswd_filename": self.htpasswd_file_path, + "htpasswd_encryption": "plain"}, + "sharing": { + "type": "csv", + "permit_create_map": True, + "permit_create_token": True, + "collection_by_map": "True", + "collection_by_token": "True"}, + "logging": {"request_header_on_debug": "False", + "response_content_on_debug": "False", + "rights_rule_doesnt_match_on_debug": "True", + "request_content_on_debug": "True"}, + "rights": {"type": "from_file"}}) + + json_dict: dict + + path_user1 = "/user1/calendarPGu1.ics/" + path_owner1_T = "/owner1/cal-T/" + path_owner1_t = "/owner1/cal-t/" + path_owner1_M = "/owner1/cal-M/" + path_owner1_m = "/owner1/cal-m/" + + logging.info("\n*** prepare") + self.mkcalendar(path_owner1_T, login="owner1:owner1pw") + self.mkcalendar(path_owner1_t, login="owner1:owner1pw") + self.mkcalendar(path_owner1_M, login="owner1:owner1pw") + self.mkcalendar(path_owner1_m, login="owner1:owner1pw") + + for db_type in sharing.INTERNAL_TYPES: + if db_type == "none": + continue + logging.info("\n*** test: %s", db_type) + self.configure({"sharing": {"type": db_type}}) + + # create map + json_dict = {} + json_dict['User'] = "user1" + json_dict['PathOrToken'] = path_user1 + + logging.info("\n*** create map user1/owner1, globally disabled") + self.configure({"sharing": {"permit_create_map": "False"}}) + + logging.info("\n*** create map user1/owner1, globally disabled / not granted M -> 403") + json_dict['PathMapped'] = path_owner1_M + json_dict['PathOrToken'] = path_user1 + "dM" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1, globally disabled / not granted T -> 403") + json_dict['PathMapped'] = path_owner1_T + json_dict['PathOrToken'] = path_user1 + "dT" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1, globally disabled / not granted t -> 403") + json_dict['PathMapped'] = path_owner1_t + json_dict['PathOrToken'] = path_user1 + "dt" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1, globally disabled / granted m -> 200") + json_dict['PathMapped'] = path_owner1_m + json_dict['PathOrToken'] = path_user1 + "dm" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1, globally enabled") + self.configure({"sharing": {"permit_create_map": "True"}}) + + logging.info("\n*** create map user1/owner1, globally enabled / not granted M -> 403") + json_dict['PathMapped'] = path_owner1_M + json_dict['PathOrToken'] = path_user1 + "eM" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1, globally enabled / ignore T -> 200") + json_dict['PathMapped'] = path_owner1_T + json_dict['PathOrToken'] = path_user1 + "eT" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1, globally enabled / ignore t -> 200") + json_dict['PathMapped'] = path_owner1_t + json_dict['PathOrToken'] = path_user1 + "et" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1, globally enabled / ignore m -> 200") + json_dict['PathMapped'] = path_owner1_m + json_dict['PathOrToken'] = path_user1 + "em" + db_type + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + # create token + json_dict = {} + + logging.info("\n*** create token owner1, globally disabled") + self.configure({"sharing": {"permit_create_token": "False"}}) + + logging.info("\n*** create token owner1, globally disabled / not granted M -> 403") + json_dict['PathMapped'] = path_owner1_M + _, headers, answer = self._sharing_api_json("token", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1, globally disabled / not granted m -> 403") + json_dict['PathMapped'] = path_owner1_m + _, headers, answer = self._sharing_api_json("token", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1, globally disabled / not granted T -> 403") + json_dict['PathMapped'] = path_owner1_T + _, headers, answer = self._sharing_api_json("token", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1, globally disabled / granted t -> 200") + json_dict['PathMapped'] = path_owner1_t + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1, globally enabled") + self.configure({"sharing": {"permit_create_token": "True"}}) + + logging.info("\n*** create token owner1, globally enabled / ignore M -> 200") + json_dict['PathMapped'] = path_owner1_M + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1, globally enabled / ignore m -> 200") + json_dict['PathMapped'] = path_owner1_m + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1, globally enabled / not granted T -> 403") + json_dict['PathMapped'] = path_owner1_T + _, headers, answer = self._sharing_api_json("token", "create", check=403, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create token owner1, globally enabled / ignore t -> 200") + json_dict['PathMapped'] = path_owner1_t + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + def test_sharing_api_permissions_default(self) -> None: + """sharing API usage tests related to global permissions.""" + self.configure({"auth": {"type": "htpasswd", + "htpasswd_filename": self.htpasswd_file_path, + "htpasswd_encryption": "plain"}, + "sharing": { + "type": "csv", + "permit_create_map": True, + "permit_create_token": True, + "collection_by_map": "True", + "collection_by_token": "True"}, + "logging": {"request_header_on_debug": "False", + "response_content_on_debug": "False", + "request_content_on_debug": "True"}, + "rights": {"type": "owner_only"}}) + + json_dict: dict + + path_user1 = "/user1/calendarPGu1.ics/" + path_owner1 = "/owner1/calendarPGo1.ics/" + + logging.info("\n*** prepare") + self.mkcalendar(path_owner1, login="owner1:owner1pw") + + for db_type in sharing.INTERNAL_TYPES: + if db_type == "none": + continue + logging.info("\n*** test: %s", db_type) + self.configure({"sharing": {"type": db_type}}) + + # create map + self.configure({"sharing": {"default_permissions_create_map": "r"}}) + + json_dict = {} + json_dict['User'] = "user1" + json_dict['PathMapped'] = path_owner1 + + logging.info("\n*** create map user1/owner1 r -> 200") + json_dict['PathOrToken'] = path_user1 + "r" + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** list (json->json)") + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert answer_dict['Content'][0]['Permissions'] == "r" + + logging.info("\n*** create map user1/owner1 rw -> 200") + json_dict['PathOrToken'] = path_user1 + "rw" + json_dict['Permissions'] = "rw" + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** list (json->json)") + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert answer_dict['Content'][0]['Permissions'] == "rw" + + logging.info("\n*** create map user1/owner1 with adjusted default permissions -> 200") + self.configure({"sharing": {"default_permissions_create_map": "RrWw"}}) + json_dict['PathOrToken'] = path_user1 + "RrRw" + del json_dict['Permissions'] + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** list (json->json)") + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert answer_dict['Content'][0]['Permissions'] == "RrWw" + + # create token + self.configure({"sharing": {"default_permissions_create_token": "r"}}) + + json_dict = {} + json_dict['PathMapped'] = path_owner1 + + logging.info("\n*** create token user1/owner1 r -> 200") + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + Token = answer_dict['PathOrToken'] + + logging.info("\n*** list (json->json)") + json_dict['PathOrToken'] = Token + _, headers, answer = self._sharing_api_json("token", "list", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert answer_dict['Content'][0]['Permissions'] == "r" + + logging.info("\n*** create token user1/owner1 rw -> 200") + json_dict['Permissions'] = "rw" + del json_dict['PathOrToken'] + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + Token = answer_dict['PathOrToken'] + + logging.info("\n*** list (json->json)") + del json_dict['Permissions'] + json_dict['PathOrToken'] = Token + _, headers, answer = self._sharing_api_json("token", "list", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert answer_dict['Content'][0]['Permissions'] == "rw" + + logging.info("\n*** create token user1/owner1 with adjusted default permissions -> 200") + self.configure({"sharing": {"default_permissions_create_token": "RrWw"}}) + del json_dict['PathOrToken'] + _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + Token = answer_dict['PathOrToken'] + + logging.info("\n*** list (json->json)") + json_dict['PathOrToken'] = Token + _, headers, answer = self._sharing_api_json("token", "list", check=200, login="owner1:owner1pw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert answer_dict['Content'][0]['Permissions'] == "RrWw"