Merge pull request #2000 from pbiering/feature-sharing-permissions
Feature sharing global and granular permissions
This commit is contained in:
@@ -2102,6 +2102,40 @@ Share collection by map
|
|||||||
|
|
||||||
Default: `false`
|
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
|
## Supported Clients
|
||||||
|
|
||||||
Radicale has been tested with:
|
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)_
|
* **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:** 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)_
|
* **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
|
### Storage
|
||||||
|
|
||||||
|
|||||||
20
config
20
config
@@ -311,11 +311,27 @@
|
|||||||
# Sharing database path for type 'files'
|
# Sharing database path for type 'files'
|
||||||
#database_path = (filesystem_folder)/collection-db/files
|
#database_path = (filesystem_folder)/collection-db/files
|
||||||
|
|
||||||
|
# Share collection by token
|
||||||
|
#collection_by_token = false
|
||||||
|
|
||||||
# Share collection by map
|
# Share collection by map
|
||||||
#collection_by_map = false
|
#collection_by_map = false
|
||||||
|
|
||||||
# Share collection by token
|
# Permit create of token-based sharing
|
||||||
#collection_by_token = false
|
# 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]
|
[web]
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ def positive_float(value: Any) -> float:
|
|||||||
return value
|
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:
|
def logging_level(value: Any) -> str:
|
||||||
if value not in ("debug", "info", "warning", "error", "critical"):
|
if value not in ("debug", "info", "warning", "error", "critical"):
|
||||||
raise ValueError("unsupported level: %r" % value)
|
raise ValueError("unsupported level: %r" % value)
|
||||||
@@ -468,14 +475,30 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
|||||||
"value": "",
|
"value": "",
|
||||||
"help": "database path",
|
"help": "database path",
|
||||||
"type": filepath}),
|
"type": filepath}),
|
||||||
|
("collection_by_token", {
|
||||||
|
"value": "false",
|
||||||
|
"help": "enable sharing of collection by token",
|
||||||
|
"type": bool}),
|
||||||
("collection_by_map", {
|
("collection_by_map", {
|
||||||
"value": "false",
|
"value": "false",
|
||||||
"help": "enable sharing of collection by map",
|
"help": "enable sharing of collection by map",
|
||||||
"type": bool}),
|
"type": bool}),
|
||||||
("collection_by_token", {
|
("permit_create_token", {
|
||||||
"value": "false",
|
"value": "false",
|
||||||
"help": "enable sharing of collection by token",
|
"help": "permit create of token-based sharing",
|
||||||
"type": bool})])),
|
"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([
|
("hook", OrderedDict([
|
||||||
("type", {
|
("type", {
|
||||||
"value": "none",
|
"value": "none",
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ Permissions:
|
|||||||
(CalDAV/CardDAV is susceptible to expensive search requests)
|
(CalDAV/CardDAV is susceptible to expensive search requests)
|
||||||
- W: write collections (excluding address books and calendars)
|
- W: write collections (excluding address books and calendars)
|
||||||
- w: write address book and calendar collections
|
- 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)
|
||||||
|
- 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.
|
Take a look at the class ``BaseRights`` if you want to implement your own.
|
||||||
|
|
||||||
@@ -39,6 +47,8 @@ from radicale import config, utils
|
|||||||
INTERNAL_TYPES: Sequence[str] = ("authenticated", "owner_write", "owner_only",
|
INTERNAL_TYPES: Sequence[str] = ("authenticated", "owner_write", "owner_only",
|
||||||
"from_file")
|
"from_file")
|
||||||
|
|
||||||
|
INTERNAL_PERMISSIONS: str = "RriWwDdOoTtMm"
|
||||||
|
|
||||||
|
|
||||||
def load(configuration: "config.Configuration") -> "BaseRights":
|
def load(configuration: "config.Configuration") -> "BaseRights":
|
||||||
"""Load the rights module chosen in configuration."""
|
"""Load the rights module chosen in configuration."""
|
||||||
|
|||||||
@@ -102,8 +102,16 @@ class BaseSharing:
|
|||||||
# Sharing
|
# Sharing
|
||||||
self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map")
|
self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map")
|
||||||
self.sharing_collection_by_token = configuration.get("sharing", "collection_by_token")
|
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_map : %s", self.sharing_collection_by_map)
|
||||||
logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token)
|
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)):
|
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")
|
logger.info("sharing disabled as no feature is enabled")
|
||||||
@@ -634,7 +642,13 @@ class BaseSharing:
|
|||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/" + api_info + ": start")
|
logger.debug("TRACE/" + api_info + ": start")
|
||||||
if 'Permissions' not in request_data:
|
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:
|
if 'Enabled' in request_data:
|
||||||
EnabledByOwner = config._convert_to_bool(request_data['Enabled'])
|
EnabledByOwner = config._convert_to_bool(request_data['Enabled'])
|
||||||
@@ -652,10 +666,19 @@ class BaseSharing:
|
|||||||
if ShareType == "token":
|
if ShareType == "token":
|
||||||
# check access Permissions
|
# check access Permissions
|
||||||
access = Access(self._rights, user, str(PathMapped)) # PathMapped is mandatory
|
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)
|
logger.info("Add sharing-by-token: access to %r not allowed for user %r", PathMapped, user)
|
||||||
return httputils.NOT_ALLOWED
|
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
|
# 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')
|
||||||
|
|
||||||
@@ -690,9 +713,18 @@ class BaseSharing:
|
|||||||
logger.info("Add sharing-by-map: access to path(mapped) %r not allowed for owner %r", PathMapped, Owner)
|
logger.info("Add sharing-by-map: access to path(mapped) %r not allowed for owner %r", PathMapped, Owner)
|
||||||
return httputils.NOT_ALLOWED
|
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)
|
access = Access(self._rights, str(User), PathOrToken)
|
||||||
if not access.check("r") and "i" not in access.permissions:
|
if not access.check("r"):
|
||||||
logger.info("Add sharing-by-map: access to path %r not allowed for user %r", PathOrToken, user)
|
logger.info("Add sharing-by-map: access to path %r not allowed for user %r", PathOrToken, User)
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
|
|
||||||
# check whether share is already existing as real collection
|
# check whether share is already existing as real collection
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "true",
|
"logging": {"request_header_on_debug": "true",
|
||||||
@@ -272,6 +274,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -458,6 +462,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -613,6 +619,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -800,6 +808,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -892,6 +902,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -1084,6 +1096,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -1183,6 +1197,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -1328,6 +1344,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -1428,6 +1446,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -1596,6 +1616,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -1795,6 +1817,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -1911,6 +1935,8 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -2048,12 +2074,14 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
assert answer_dict['Lines'] == 1
|
assert answer_dict['Lines'] == 1
|
||||||
|
|
||||||
def test_sharing_api_create_conflict(self) -> None:
|
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",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
"htpasswd_filename": self.htpasswd_file_path,
|
"htpasswd_filename": self.htpasswd_file_path,
|
||||||
"htpasswd_encryption": "plain"},
|
"htpasswd_encryption": "plain"},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"type": "csv",
|
"type": "csv",
|
||||||
|
"permit_create_map": True,
|
||||||
|
"permit_create_token": True,
|
||||||
"collection_by_map": "True",
|
"collection_by_map": "True",
|
||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
@@ -2127,3 +2155,340 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
json_dict['Enabled'] = "True"
|
json_dict['Enabled'] = "True"
|
||||||
json_dict['Hidden'] = "False"
|
json_dict['Hidden'] = "False"
|
||||||
_, headers, answer = self._sharing_api_json("map", "create", check=409, login="owner1:owner1pw", json_dict=json_dict)
|
_, 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user