sharing: replace type bday by conversion
This commit is contained in:
@@ -19,6 +19,7 @@ import base64
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os # TODO: remove/3.7.0-final
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import uuid
|
import uuid
|
||||||
@@ -34,7 +35,7 @@ from radicale.log import logger
|
|||||||
|
|
||||||
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
|
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none")
|
||||||
|
|
||||||
DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Owner', 'User', 'Permissions', 'EnabledByOwner', 'EnabledByUser', 'HiddenByOwner', 'HiddenByUser', 'TimestampCreated', 'TimestampUpdated', 'Properties', 'Conversion', 'Actions')
|
DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Conversion', 'Owner', 'User', 'Permissions', 'EnabledByOwner', 'EnabledByUser', 'HiddenByOwner', 'HiddenByUser', 'TimestampCreated', 'TimestampUpdated', 'Properties', 'Actions')
|
||||||
# ShareType: <token|map>
|
# ShareType: <token|map>
|
||||||
# PathOrToken: <path|token> [PrimaryKey]
|
# PathOrToken: <path|token> [PrimaryKey]
|
||||||
# PathMapped: <path>
|
# PathMapped: <path>
|
||||||
@@ -48,8 +49,8 @@ DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Owner'
|
|||||||
# TimestampCreated: <unixtime> (when created)
|
# TimestampCreated: <unixtime> (when created)
|
||||||
# TimestampUpdated: <unixtime> (last update)
|
# TimestampUpdated: <unixtime> (last update)
|
||||||
# Properties: Overlay of collection properties in JSON
|
# Properties: Overlay of collection properties in JSON
|
||||||
# Conversion: None|bday
|
# Conversion: none|bday
|
||||||
# bday: check VCARD(vcf) for BDAY and convert to reoccuring VEVENT(ics)
|
# bday: check VADDRESSBOOK VCARD(vcf) entries for BDAY and convert to VCALENDAR reoccuring VEVENT(ics)
|
||||||
# Actions: Actions structure in JSON
|
# Actions: Actions structure in JSON
|
||||||
# (future reserved for e.g. "filter", "filter_pre", "filter_post" or anything else, implemented on request)
|
# (future reserved for e.g. "filter", "filter_pre", "filter_post" or anything else, implemented on request)
|
||||||
|
|
||||||
@@ -73,14 +74,17 @@ DB_TYPES_V1: dict[str, type] = {
|
|||||||
|
|
||||||
DB_FIELDS_V1_USER_PERMITTED: Sequence[str] = ('EnabledByUser', 'HiddenByUser', 'Properties')
|
DB_FIELDS_V1_USER_PERMITTED: Sequence[str] = ('EnabledByUser', 'HiddenByUser', 'Properties')
|
||||||
|
|
||||||
SHARE_TYPES: Sequence[str] = ('token', 'map', 'bday', 'all')
|
SHARE_TYPES: Sequence[str] = ('token', 'map', 'all')
|
||||||
|
|
||||||
SHARE_TYPES_V1: Sequence[str] = ('token', 'map', 'bday')
|
|
||||||
# token: share by secret token (does not require authentication)
|
# token: share by secret token (does not require authentication)
|
||||||
# map : share by mapping collection of one user to another as virtual
|
# map : share by mapping collection of one user to another as virtual
|
||||||
# bday : share by mapping addressbook-collection of one user to another as virtual calendar-collection
|
|
||||||
# all : only supported for "list" and "info"
|
# all : only supported for "list" and "info"
|
||||||
|
|
||||||
|
SHARE_TYPES_V1: Sequence[str] = ('token', 'map')
|
||||||
|
|
||||||
|
if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final
|
||||||
|
SHARE_TYPES: Sequence[str] = ('token', 'map', 'bday', 'all') # type: ignore[no-redef] # TODO: remove/3.7.0-final
|
||||||
|
SHARE_TYPES_V1: Sequence[str] = ('token', 'map', 'bday') # type: ignore[no-redef] # TODO: remove/3.7.0-final
|
||||||
|
|
||||||
API_HOOKS_V1: Sequence[str] = ('list', 'create', 'delete', 'update', 'hide', 'unhide', 'enable', 'disable', 'info')
|
API_HOOKS_V1: Sequence[str] = ('list', 'create', 'delete', 'update', 'hide', 'unhide', 'enable', 'disable', 'info')
|
||||||
# list : list sharings (optional filtered)
|
# list : list sharings (optional filtered)
|
||||||
# create : create share by token or map
|
# create : create share by token or map
|
||||||
@@ -115,6 +119,7 @@ API_TYPES_V1: dict[str, type] = {
|
|||||||
"Properties": dict,
|
"Properties": dict,
|
||||||
"Conversion": str,
|
"Conversion": str,
|
||||||
"Actions": dict,
|
"Actions": dict,
|
||||||
|
"SupportedConversions": list,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -126,7 +131,7 @@ USER_PATTERN: str = "([a-zA-Z0-9@]+)" # TODO: extend or find better source
|
|||||||
|
|
||||||
OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname")
|
OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname")
|
||||||
|
|
||||||
CONVERSIONS_WHITELIST: Sequence[str] = ("bday")
|
CONVERSIONS_WHITELIST: Sequence[str] = ("bday", "none")
|
||||||
|
|
||||||
|
|
||||||
def load(configuration: "config.Configuration") -> "BaseSharing":
|
def load(configuration: "config.Configuration") -> "BaseSharing":
|
||||||
@@ -157,20 +162,21 @@ 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.sharing_collection_by_bday = configuration.get("sharing", "collection_by_bday")
|
if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final
|
||||||
|
self.sharing_collection_by_map = self.sharing_collection_by_map or configuration.get("sharing", "collection_by_bday") # TODO: remove/3.7.0-final
|
||||||
self.permit_create_token = configuration.get("sharing", "permit_create_token")
|
self.permit_create_token = configuration.get("sharing", "permit_create_token")
|
||||||
self.permit_create_map = configuration.get("sharing", "permit_create_map")
|
self.permit_create_map = configuration.get("sharing", "permit_create_map")
|
||||||
self.permit_create_bday = configuration.get("sharing", "permit_create_bday")
|
if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final
|
||||||
|
self.permit_create_map = self.permit_create_map or configuration.get("sharing", "permit_create_bday") # TODO: remove/3.7.0-final
|
||||||
self.default_permissions_create_token = configuration.get("sharing", "default_permissions_create_token")
|
self.default_permissions_create_token = configuration.get("sharing", "default_permissions_create_token")
|
||||||
self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map")
|
self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map")
|
||||||
self.permit_properties_overlay = configuration.get("sharing", "permit_properties_overlay")
|
self.permit_properties_overlay = configuration.get("sharing", "permit_properties_overlay")
|
||||||
self.enforce_properties_overlay = configuration.get("sharing", "enforce_properties_overlay")
|
self.enforce_properties_overlay = configuration.get("sharing", "enforce_properties_overlay")
|
||||||
|
|
||||||
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.collection_by_bday : %s", self.sharing_collection_by_bday)
|
|
||||||
logger.info("sharing.permit_create_token: %s", self.permit_create_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.permit_create_map : %s", self.permit_create_map)
|
||||||
logger.info("sharing.permit_create_bday : %s", self.permit_create_bday)
|
|
||||||
logger.info("sharing.default_permissions_create_token: %r", self.default_permissions_create_token)
|
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)
|
logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map)
|
||||||
logger.info("sharing.permit_properties_overlay: %s", self.permit_properties_overlay)
|
logger.info("sharing.permit_properties_overlay: %s", self.permit_properties_overlay)
|
||||||
@@ -180,7 +186,7 @@ class BaseSharing:
|
|||||||
self.sharing_db_type = configuration.get("sharing", "type")
|
self.sharing_db_type = configuration.get("sharing", "type")
|
||||||
logger.info("sharing.database_type: %s", self.sharing_db_type)
|
logger.info("sharing.database_type: %s", self.sharing_db_type)
|
||||||
|
|
||||||
if ((self.sharing_collection_by_map is False) and (self.sharing_collection_by_token is False) and (self.sharing_collection_by_bday 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")
|
||||||
self._enabled = False
|
self._enabled = False
|
||||||
return
|
return
|
||||||
@@ -195,7 +201,7 @@ class BaseSharing:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self.database_init() is False:
|
if self.database_init() is False:
|
||||||
logger.info("sharing disabled as no database is active")
|
logger.warning("sharing disabled as no database is active")
|
||||||
self._enabled = False
|
self._enabled = False
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -247,13 +253,13 @@ class BaseSharing:
|
|||||||
def database_create_sharing(self,
|
def database_create_sharing(self,
|
||||||
ShareType: str,
|
ShareType: str,
|
||||||
PathOrToken: str, PathMapped: str,
|
PathOrToken: str, PathMapped: str,
|
||||||
|
Conversion: str,
|
||||||
Owner: str, User: str,
|
Owner: str, User: str,
|
||||||
Permissions: str = "r",
|
Permissions: str = "r",
|
||||||
EnabledByOwner: bool = False, EnabledByUser: bool = False,
|
EnabledByOwner: bool = False, EnabledByUser: bool = False,
|
||||||
HiddenByOwner: bool = True, HiddenByUser: bool = True,
|
HiddenByOwner: bool = True, HiddenByUser: bool = True,
|
||||||
Timestamp: int = 0,
|
Timestamp: int = 0,
|
||||||
Properties: Union[dict, None] = None,
|
Properties: Union[dict, None] = None,
|
||||||
Conversion: Union[str, None] = None,
|
|
||||||
Actions: Union[dict, None] = None,
|
Actions: Union[dict, None] = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
""" create sharing """
|
""" create sharing """
|
||||||
@@ -350,7 +356,12 @@ class BaseSharing:
|
|||||||
|
|
||||||
# *** sharing functions called by request methods ***
|
# *** sharing functions called by request methods ***
|
||||||
# list sharings
|
# list sharings
|
||||||
def sharing_collection_list(self, User: Union[str, None] = None, Enabled: Union[bool, None] = None, Hidden: Union[bool, None] = None) -> list[dict]:
|
def sharing_collection_list(self,
|
||||||
|
User: Union[str, None] = None,
|
||||||
|
Enabled: Union[bool, None] = None,
|
||||||
|
Hidden: Union[bool, None] = None,
|
||||||
|
Conversion: Union[str, None] = None,
|
||||||
|
) -> list[dict]:
|
||||||
""" returning dict with shared collections by filter(User/Enabled/Hidden) or None if not found"""
|
""" returning dict with shared collections by filter(User/Enabled/Hidden) or None if not found"""
|
||||||
sharing_collection_list = []
|
sharing_collection_list = []
|
||||||
|
|
||||||
@@ -366,27 +377,16 @@ class BaseSharing:
|
|||||||
EnabledByOwner=Enabled,
|
EnabledByOwner=Enabled,
|
||||||
EnabledByUser=Enabled,
|
EnabledByUser=Enabled,
|
||||||
HiddenByOwner=Hidden,
|
HiddenByOwner=Hidden,
|
||||||
HiddenByUser=Hidden)
|
HiddenByUser=Hidden,
|
||||||
|
Conversion=Conversion,
|
||||||
if not self.sharing_collection_by_bday:
|
)
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("TRACE/sharing/bday: not active")
|
|
||||||
else:
|
|
||||||
# retrieve collections depending on filter
|
|
||||||
sharing_collection_list += self.database_list_sharing(
|
|
||||||
ShareType="bday",
|
|
||||||
OwnerOrUser=User,
|
|
||||||
User=User,
|
|
||||||
EnabledByOwner=Enabled,
|
|
||||||
EnabledByUser=Enabled,
|
|
||||||
HiddenByOwner=Hidden,
|
|
||||||
HiddenByUser=Hidden)
|
|
||||||
|
|
||||||
return sharing_collection_list
|
return sharing_collection_list
|
||||||
|
|
||||||
# resolves a path to a share
|
# resolves a path to a share
|
||||||
def sharing_collection_resolver(self, path: str, user: str) -> Union[dict, None]:
|
def sharing_collection_resolver(self, path: str, user: str) -> Union[dict, None]:
|
||||||
""" returning dict with PathMapped, Owner, Permissions or None if not found"""
|
""" returning dict with PathMapped, Owner, Permissions or None if not found"""
|
||||||
|
logger.debug("TRACE/sharing/resolver: lookup path=%r user=%r", path, user)
|
||||||
share = None
|
share = None
|
||||||
|
|
||||||
if path == "/":
|
if path == "/":
|
||||||
@@ -396,6 +396,8 @@ class BaseSharing:
|
|||||||
if self.sharing_collection_by_token:
|
if self.sharing_collection_by_token:
|
||||||
if share is None:
|
if share is None:
|
||||||
share = self.sharing_collection_by_token_resolver(path)
|
share = self.sharing_collection_by_token_resolver(path)
|
||||||
|
if share is not None and 'error' in share:
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/token: not active")
|
logger.debug("TRACE/sharing/token: not active")
|
||||||
@@ -403,17 +405,12 @@ class BaseSharing:
|
|||||||
if self.sharing_collection_by_map:
|
if self.sharing_collection_by_map:
|
||||||
if share is None:
|
if share is None:
|
||||||
share = self.sharing_collection_by_map_resolver(path, user)
|
share = self.sharing_collection_by_map_resolver(path, user)
|
||||||
|
if share is not None and 'error' in share:
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/map: not active")
|
logger.debug("TRACE/sharing/map: not active")
|
||||||
|
|
||||||
if self.sharing_collection_by_bday:
|
|
||||||
if share is None:
|
|
||||||
share = self.sharing_collection_by_bday_resolver(path, user)
|
|
||||||
else:
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("TRACE/sharing/bday: not active")
|
|
||||||
|
|
||||||
if share is not None:
|
if share is not None:
|
||||||
if self.permit_properties_overlay:
|
if self.permit_properties_overlay:
|
||||||
if share['Permissions'] and "p" not in share['Permissions']:
|
if share['Permissions'] and "p" not in share['Permissions']:
|
||||||
@@ -446,31 +443,44 @@ class BaseSharing:
|
|||||||
|
|
||||||
# *** internal sharing functions ***
|
# *** internal sharing functions ***
|
||||||
# resolves a token "path" to a share
|
# resolves a token "path" to a share
|
||||||
|
# dict: share
|
||||||
|
# None: not supported
|
||||||
|
# False: supported but not found
|
||||||
def sharing_collection_by_token_resolver(self, path) -> Union[dict, None]:
|
def sharing_collection_by_token_resolver(self, path) -> Union[dict, None]:
|
||||||
""" returning dict with PathMapped, Owner, Permissions or None if invalid"""
|
""" returning dict with PathMapped, Owner, Permissions or None if invalid"""
|
||||||
if self.sharing_collection_by_token:
|
if self.sharing_collection_by_token:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/token: check path: %r", path)
|
logger.debug("TRACE/sharing/token/resolver: check path: %r", path)
|
||||||
if path.startswith("/.token/"):
|
if path.startswith("/.token/"):
|
||||||
pattern = re.compile('^(/\\.token/' + TOKEN_PATTERN_V1 + '/)$')
|
pattern = re.compile('^(/\\.token/' + TOKEN_PATTERN_V1 + '/)$')
|
||||||
match = pattern.match(path)
|
match = pattern.match(path)
|
||||||
if not match:
|
if not match:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/token: unsupported token: %r", path)
|
logger.debug("TRACE/sharing/token/resolver: unsupported token: %r", path)
|
||||||
return None
|
return {'error': 'token-not-supported'}
|
||||||
else:
|
else:
|
||||||
# TODO add token validity checks
|
# TODO add token validity checks
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/token: supported token found in path: %r (token=%r)", path, match[1])
|
logger.debug("TRACE/sharing/token/resolver: supported token: %r", path)
|
||||||
result = self.database_get_sharing(
|
result = self.database_get_sharing(
|
||||||
ShareType="token",
|
ShareType="token",
|
||||||
|
OnlyEnabled=False,
|
||||||
PathOrToken=match[1])
|
PathOrToken=match[1])
|
||||||
if result is not None:
|
|
||||||
logger.info("Sharing/%s: resolved %r->%r, User=%r, Permissions=%r Conversion=%r", "token", path, result['PathMapped'], result['Owner'], result['Permissions'], result['Conversion'])
|
if result is None:
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("TRACE/sharing/token/resolver: supported token not found: %r", path)
|
||||||
|
return {'error': 'token-not-found'}
|
||||||
|
|
||||||
|
if result['EnabledByOwner'] is not True:
|
||||||
|
logger.info("Sharing/%s: resolved path %r->%r, User=%r not enabled by owner", "token", path, result['PathMapped'], result['Owner'])
|
||||||
|
return {'error': 'token-not-enabled'}
|
||||||
|
|
||||||
|
logger.info("Sharing/%s: resolved %r->%r, User=%r, Permissions=%r Conversion=%r", "token", path, result['PathMapped'], result['Owner'], result['Permissions'], result['Conversion'])
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/token: no supported prefix found in path: %r", path)
|
logger.debug("TRACE/sharing/token/resolver: no supported prefix found in path: %r", path)
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
@@ -483,13 +493,14 @@ class BaseSharing:
|
|||||||
if self.sharing_collection_by_map:
|
if self.sharing_collection_by_map:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/map/resolver: check path: %r", path)
|
logger.debug("TRACE/sharing/map/resolver: check path: %r", path)
|
||||||
|
|
||||||
result = self.database_get_sharing(
|
result = self.database_get_sharing(
|
||||||
ShareType="map",
|
ShareType="map",
|
||||||
PathOrToken=path,
|
PathOrToken=path,
|
||||||
User=user)
|
OnlyEnabled=False,
|
||||||
if result:
|
User=user)
|
||||||
pass
|
|
||||||
else:
|
if not result:
|
||||||
# fallback to parent path
|
# fallback to parent path
|
||||||
parent_path = pathutils.parent_path(path)
|
parent_path = pathutils.parent_path(path)
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
@@ -497,6 +508,7 @@ class BaseSharing:
|
|||||||
result = self.database_get_sharing(
|
result = self.database_get_sharing(
|
||||||
ShareType="map",
|
ShareType="map",
|
||||||
PathOrToken=parent_path,
|
PathOrToken=parent_path,
|
||||||
|
OnlyEnabled=False,
|
||||||
User=user)
|
User=user)
|
||||||
if result:
|
if result:
|
||||||
result['PathMapped'] = path.replace(parent_path, result['PathMapped'])
|
result['PathMapped'] = path.replace(parent_path, result['PathMapped'])
|
||||||
@@ -504,55 +516,26 @@ class BaseSharing:
|
|||||||
logger.debug("TRACE/sharing/map/resolver: PathMapped=%r Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], parent_path)
|
logger.debug("TRACE/sharing/map/resolver: PathMapped=%r Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], parent_path)
|
||||||
else:
|
else:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/map: not found")
|
logger.debug("TRACE/sharing/map/resolver: not found")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r", "map", path, result['PathMapped'], user, result['Owner'], result['Permissions'], result['Conversion'])
|
if result:
|
||||||
return result
|
if result['EnabledByOwner'] is not True:
|
||||||
|
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r not enabled by owner", "map", path, result['PathMapped'], user, result['Owner'])
|
||||||
|
return {'error': 'map-not-enabled'}
|
||||||
|
if result['EnabledByUser'] is not True:
|
||||||
|
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r not enabled by user", "map", path, result['PathMapped'], user, result['Owner'])
|
||||||
|
return {'error': 'map-not-enabled'}
|
||||||
|
|
||||||
|
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r", "map", path, result['PathMapped'], user, result['Owner'], result['Permissions'], result['Conversion'])
|
||||||
|
return result
|
||||||
|
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/map: not active")
|
logger.debug("TRACE/sharing/map: not active")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# resolves a bday "path" to a share
|
|
||||||
def sharing_collection_by_bday_resolver(self, path: str, user: str) -> Union[dict, None]:
|
|
||||||
""" returning dict with PathMapped, Owner, Permissions or None if invalid"""
|
|
||||||
if self.sharing_collection_by_bday:
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("TRACE/sharing/bday/resolver: check path: %r", path)
|
|
||||||
result = self.database_get_sharing(
|
|
||||||
ShareType="bday",
|
|
||||||
PathOrToken=path,
|
|
||||||
User=user)
|
|
||||||
if result:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# fallback to parent path
|
|
||||||
parent_path = pathutils.parent_path(path)
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("TRACE/sharing/bday/resolver: check parent path: %r", parent_path)
|
|
||||||
result = self.database_get_sharing(
|
|
||||||
ShareType="bday",
|
|
||||||
PathOrToken=parent_path,
|
|
||||||
User=user)
|
|
||||||
if result:
|
|
||||||
result['PathMapped'] = path.replace(parent_path, result['PathMapped'])
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("TRACE/sharing/bday/resolver: PathMapped=%r Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], parent_path)
|
|
||||||
else:
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("TRACE/sharing/bday: not found")
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not result['Conversion']:
|
|
||||||
result['Conversion'] = "bday"
|
|
||||||
logger.info("Sharing/%s: resolved path %r->%r, user %r->%r, Permissions=%r Conversion=%r", "bday", path, result['PathMapped'], user, result['Owner'], result['Permissions'], result['Conversion'])
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
|
||||||
logger.debug("TRACE/sharing/bday: not active")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# *** POST API ***
|
# *** POST API ***
|
||||||
def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str) -> types.WSGIResponse:
|
def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str) -> types.WSGIResponse:
|
||||||
# Late import to avoid circular dependency in config
|
# Late import to avoid circular dependency in config
|
||||||
@@ -567,27 +550,28 @@ class BaseSharing:
|
|||||||
``user`` is empty for anonymous users.
|
``user`` is empty for anonymous users.
|
||||||
|
|
||||||
Request:
|
Request:
|
||||||
action: (token|map|bday)/list
|
action: (token|map)/list
|
||||||
PathOrToken: <path|token> (optional for filter)
|
PathOrToken: <path|token> (optional for filter)
|
||||||
|
|
||||||
action: (token|map|bday)/create
|
action: (token|map)/create
|
||||||
PathMapped: <path> (mandatory)
|
PathMapped: <path> (mandatory)
|
||||||
Permissions: <Permissions> (default: r)
|
Permissions: <Permissions> (default: r)
|
||||||
|
|
||||||
token -> returns <token>
|
token -> returns <token>
|
||||||
|
|
||||||
map|bday
|
map
|
||||||
PathOrToken: <path> (mandatory)
|
PathOrToken: <path> (mandatory)
|
||||||
User: <target_user> (mandatory)
|
User: <target_user> (mandatory)
|
||||||
|
Conversion: None|bday (optional)
|
||||||
|
|
||||||
action: (token|map|bday)/update
|
action: (token|map)/update
|
||||||
|
|
||||||
action: (token|map|bday)/(delete|disable|enable|hide|unhide)
|
action: (token|map)/(delete|disable|enable|hide|unhide)
|
||||||
PathOrToken: <path|token> (mandatory)
|
PathOrToken: <path|token> (mandatory)
|
||||||
|
|
||||||
token
|
token
|
||||||
|
|
||||||
map|bday
|
map
|
||||||
PathMapped: <path> (mandatory)
|
PathMapped: <path> (mandatory)
|
||||||
User: <target_user>
|
User: <target_user>
|
||||||
|
|
||||||
@@ -648,11 +632,6 @@ class BaseSharing:
|
|||||||
logger.warning(api_info + ": not enabled by config (collection_by_map)")
|
logger.warning(api_info + ": not enabled by config (collection_by_map)")
|
||||||
return httputils.NOT_FOUND
|
return httputils.NOT_FOUND
|
||||||
|
|
||||||
if not self.sharing_collection_by_bday and ShareType == "bday":
|
|
||||||
# API "token" is not enabled
|
|
||||||
logger.warning(api_info + ": not enabled by config (collection_by_bday)")
|
|
||||||
return httputils.NOT_FOUND
|
|
||||||
|
|
||||||
# check for valid API hooks
|
# check for valid API hooks
|
||||||
if action not in API_HOOKS_V1:
|
if action not in API_HOOKS_V1:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
@@ -830,11 +809,10 @@ class BaseSharing:
|
|||||||
Properties = request_data['Properties']
|
Properties = request_data['Properties']
|
||||||
|
|
||||||
if 'Conversion' in request_data:
|
if 'Conversion' in request_data:
|
||||||
# verify against whitelist
|
|
||||||
for entry in request_data['Conversion']:
|
|
||||||
if entry not in CONVERSIONS_WHITELIST:
|
|
||||||
return httputils.bad_request("Conversion not supported: %r" % entry)
|
|
||||||
Conversion = request_data['Conversion']
|
Conversion = request_data['Conversion']
|
||||||
|
# verify against whitelist
|
||||||
|
if Conversion not in CONVERSIONS_WHITELIST:
|
||||||
|
return httputils.bad_request("Conversion not supported: %r" % Conversion)
|
||||||
|
|
||||||
if 'Actions' in request_data:
|
if 'Actions' in request_data:
|
||||||
return httputils.bad_request("Actions currently not supported (reserved for future needs)")
|
return httputils.bad_request("Actions currently not supported (reserved for future needs)")
|
||||||
@@ -860,7 +838,12 @@ class BaseSharing:
|
|||||||
answer['ApiVersion'] = 1
|
answer['ApiVersion'] = 1
|
||||||
Timestamp = int((datetime.now() - datetime(1970, 1, 1)).total_seconds())
|
Timestamp = int((datetime.now() - datetime(1970, 1, 1)).total_seconds())
|
||||||
|
|
||||||
if not self.sharing_collection_by_map and not self.sharing_collection_by_token and not self.sharing_collection_by_bday:
|
if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final
|
||||||
|
if ShareType == "bday": # TODO: remove/3.7.0-final
|
||||||
|
ShareType = "map" # TODO: remove/3.7.0-final
|
||||||
|
Conversion = "bday" # TODO: remove/3.7.0-final
|
||||||
|
|
||||||
|
if not self.sharing_collection_by_map and not self.sharing_collection_by_token:
|
||||||
if not action == 'info':
|
if not action == 'info':
|
||||||
# API is not enabled
|
# API is not enabled
|
||||||
logger.warning(api_info + ": API is not enabled")
|
logger.warning(api_info + ": API is not enabled")
|
||||||
@@ -880,12 +863,16 @@ class BaseSharing:
|
|||||||
ShareType=ShareType,
|
ShareType=ShareType,
|
||||||
OwnerOrUser=user,
|
OwnerOrUser=user,
|
||||||
PathMapped=PathMapped,
|
PathMapped=PathMapped,
|
||||||
PathOrToken=PathOrToken)
|
PathOrToken=PathOrToken,
|
||||||
|
Conversion=Conversion,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result_array = self.database_list_sharing(
|
result_array = self.database_list_sharing(
|
||||||
OwnerOrUser=user,
|
OwnerOrUser=user,
|
||||||
PathMapped=PathMapped,
|
PathMapped=PathMapped,
|
||||||
PathOrToken=PathOrToken)
|
PathOrToken=PathOrToken,
|
||||||
|
Conversion=Conversion,
|
||||||
|
)
|
||||||
|
|
||||||
answer['Lines'] = len(result_array)
|
answer['Lines'] = len(result_array)
|
||||||
if len(result_array) == 0:
|
if len(result_array) == 0:
|
||||||
@@ -905,6 +892,9 @@ class BaseSharing:
|
|||||||
logger.warning(api_info + ": missing PathMapped")
|
logger.warning(api_info + ": missing PathMapped")
|
||||||
return httputils.bad_request("Missing PathMapped")
|
return httputils.bad_request("Missing PathMapped")
|
||||||
|
|
||||||
|
if Conversion is None:
|
||||||
|
Conversion = "none"
|
||||||
|
|
||||||
# check whether collection exists
|
# check whether collection exists
|
||||||
with self._storage.acquire_lock("r", user, path=PathMapped):
|
with self._storage.acquire_lock("r", user, path=PathMapped):
|
||||||
item = next(iter(self._storage.discover(PathMapped)), None)
|
item = next(iter(self._storage.discover(PathMapped)), None)
|
||||||
@@ -912,7 +902,12 @@ class BaseSharing:
|
|||||||
logger.warning(api_info + ": cannot find PathMapped=%r", PathMapped)
|
logger.warning(api_info + ": cannot find PathMapped=%r", PathMapped)
|
||||||
return httputils.NOT_FOUND
|
return httputils.NOT_FOUND
|
||||||
if not isinstance(item, storage.BaseCollection):
|
if not isinstance(item, storage.BaseCollection):
|
||||||
|
logger.warning(api_info + ": PathMapped=%r is not a collection", PathMapped)
|
||||||
return httputils.METHOD_NOT_ALLOWED
|
return httputils.METHOD_NOT_ALLOWED
|
||||||
|
if Conversion == "bday":
|
||||||
|
if item.tag != "VADDRESSBOOK":
|
||||||
|
logger.warning(api_info + ": PathMapped=%r is not a VADDRESSBOOK collection (mandatory for Conversion=%r)", PathMapped, Conversion)
|
||||||
|
return httputils.METHOD_NOT_ALLOWED
|
||||||
|
|
||||||
if Permissions is None:
|
if Permissions is None:
|
||||||
if ShareType == "token":
|
if ShareType == "token":
|
||||||
@@ -924,6 +919,12 @@ class BaseSharing:
|
|||||||
Permissions = "r"
|
Permissions = "r"
|
||||||
else:
|
else:
|
||||||
Permissions = str(Permissions)
|
Permissions = str(Permissions)
|
||||||
|
if Conversion == "bday":
|
||||||
|
# bday is read-only
|
||||||
|
for permission in Permissions:
|
||||||
|
if permission not in "r":
|
||||||
|
logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion)
|
||||||
|
return httputils.METHOD_NOT_ALLOWED
|
||||||
|
|
||||||
if Enabled is None:
|
if Enabled is None:
|
||||||
Enabled = False # security by default
|
Enabled = False # security by default
|
||||||
@@ -988,7 +989,7 @@ class BaseSharing:
|
|||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/" + api_info + ": result=%r", result)
|
logger.debug("TRACE/" + api_info + ": result=%r", result)
|
||||||
|
|
||||||
elif ShareType in ["map", "bday"]:
|
elif ShareType == "map":
|
||||||
# check preconditions
|
# check preconditions
|
||||||
if PathOrToken is None:
|
if PathOrToken is None:
|
||||||
return httputils.bad_request("Missing PathOrToken")
|
return httputils.bad_request("Missing PathOrToken")
|
||||||
@@ -1006,10 +1007,10 @@ class BaseSharing:
|
|||||||
else:
|
else:
|
||||||
User = str(User)
|
User = str(User)
|
||||||
|
|
||||||
# lookup existing shares with requested PathMapped for same User
|
# lookup existing shares with requested PathMapped for same User and same Conversion
|
||||||
shares = self.database_list_sharing(ShareType=ShareType, PathMapped=PathMapped, User=User)
|
shares = self.database_list_sharing(ShareType=ShareType, PathMapped=PathMapped, User=User, Conversion=Conversion)
|
||||||
if len(shares) > 0:
|
if len(shares) > 0:
|
||||||
logger.warning(api_info + ": share already exists with PathMapped=%r User=%r", PathMapped, User)
|
logger.warning(api_info + ": share already exists with PathMapped=%r User=%r Conversion=%r", PathMapped, User, Conversion)
|
||||||
return httputils.CONFLICT
|
return httputils.CONFLICT
|
||||||
|
|
||||||
# check access Permissions
|
# check access Permissions
|
||||||
@@ -1018,26 +1019,14 @@ class BaseSharing:
|
|||||||
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r", PathMapped, user)
|
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r", PathMapped, user)
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
|
|
||||||
if ShareType == "map":
|
if self.permit_create_map is False:
|
||||||
if self.permit_create_map is False:
|
if "m" not in access.permissions:
|
||||||
if "m" not in access.permissions:
|
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=False but explicit grant misses 'm')", PathMapped, user)
|
||||||
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=False but explicit grant misses 'm')", PathMapped, user)
|
return httputils.NOT_ALLOWED
|
||||||
return httputils.NOT_ALLOWED
|
else:
|
||||||
else:
|
if "M" in access.permissions:
|
||||||
if "M" in access.permissions:
|
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=True but denied by 'M')", PathMapped, user)
|
||||||
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=True but denied by 'M')", PathMapped, user)
|
return httputils.NOT_ALLOWED
|
||||||
return httputils.NOT_ALLOWED
|
|
||||||
|
|
||||||
elif ShareType == "bday":
|
|
||||||
Conversion = "bday"
|
|
||||||
if self.permit_create_bday is False:
|
|
||||||
if "b" not in access.permissions:
|
|
||||||
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=False but explicit grant misses 'b')", PathMapped, user)
|
|
||||||
return httputils.NOT_ALLOWED
|
|
||||||
else:
|
|
||||||
if "B" in access.permissions:
|
|
||||||
logger.warning(api_info + ": access to PathMapped=%r not allowed for owner %r (permit=True but denied by 'B')", PathMapped, user)
|
|
||||||
return httputils.NOT_ALLOWED
|
|
||||||
|
|
||||||
access = Access(self._rights, User, PathOrToken)
|
access = Access(self._rights, User, PathOrToken)
|
||||||
if not access.check("r"):
|
if not access.check("r"):
|
||||||
@@ -1100,7 +1089,7 @@ 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 ShareType not in ["token", "map", "bday"]:
|
if ShareType not in SHARE_TYPES_V1:
|
||||||
logger.warning(api_info + ": unsupported for ShareType=%r", ShareType)
|
logger.warning(api_info + ": unsupported for ShareType=%r", ShareType)
|
||||||
return httputils.bad_request("Invalid share type")
|
return httputils.bad_request("Invalid share type")
|
||||||
|
|
||||||
@@ -1226,7 +1215,7 @@ 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 ShareType not in ["token", "map", "bday"]:
|
if ShareType not in SHARE_TYPES_V1:
|
||||||
logger.warning(api_info + ": unsupported for ShareType=%r", ShareType)
|
logger.warning(api_info + ": unsupported for ShareType=%r", ShareType)
|
||||||
return httputils.bad_request("Invalid share type")
|
return httputils.bad_request("Invalid share type")
|
||||||
|
|
||||||
@@ -1268,19 +1257,21 @@ class BaseSharing:
|
|||||||
if ShareType in ["all", "map"]:
|
if ShareType in ["all", "map"]:
|
||||||
answer['FeatureEnabledCollectionByMap'] = self.sharing_collection_by_map
|
answer['FeatureEnabledCollectionByMap'] = self.sharing_collection_by_map
|
||||||
answer['PermittedCreateCollectionByMap'] = self.permit_create_map
|
answer['PermittedCreateCollectionByMap'] = self.permit_create_map
|
||||||
|
if "SHARING_NO_LEGACY" not in os.environ: # TODO: remove/3.7.0-final
|
||||||
|
answer['FeatureEnabledCollectionByBday'] = self.sharing_collection_by_map # TODO: remove/3.7.0-final
|
||||||
|
answer['PermittedCreateCollectionByBday'] = self.permit_create_map # TODO: remove/3.7.0-final
|
||||||
if ShareType in ["all", "token"]:
|
if ShareType in ["all", "token"]:
|
||||||
answer['FeatureEnabledCollectionByToken'] = self.sharing_collection_by_token
|
answer['FeatureEnabledCollectionByToken'] = self.sharing_collection_by_token
|
||||||
answer['PermittedCreateCollectionByToken'] = self.permit_create_token
|
answer['PermittedCreateCollectionByToken'] = self.permit_create_token
|
||||||
if ShareType in ["all", "bday"]:
|
if ShareType in ["all", "map", "token"]:
|
||||||
answer['FeatureEnabledCollectionByBday'] = self.sharing_collection_by_bday
|
answer['SupportedConversions'] = CONVERSIONS_WHITELIST
|
||||||
answer['PermittedCreateCollectionByBday'] = self.permit_create_bday
|
|
||||||
|
|
||||||
# action: TOGGLE
|
# action: TOGGLE
|
||||||
elif action in API_SHARE_TOGGLES_V1:
|
elif action in API_SHARE_TOGGLES_V1:
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/sharing/API/POST/" + action)
|
logger.debug("TRACE/sharing/API/POST/" + action)
|
||||||
|
|
||||||
if ShareType not in ["token", "map", "bday"]:
|
if ShareType not in SHARE_TYPES_V1:
|
||||||
logger.warning(api_info + ": unsupported for ShareType=%r", ShareType)
|
logger.warning(api_info + ": unsupported for ShareType=%r", ShareType)
|
||||||
return httputils.bad_request("Invalid share type")
|
return httputils.bad_request("Invalid share type")
|
||||||
|
|
||||||
@@ -1365,6 +1356,8 @@ class BaseSharing:
|
|||||||
if key != 'Content':
|
if key != 'Content':
|
||||||
if API_TYPES_V1[key] is bool or API_TYPES_V1[key] is int:
|
if API_TYPES_V1[key] is bool or API_TYPES_V1[key] is int:
|
||||||
answer_array.append(key + '=' + str(answer[key]))
|
answer_array.append(key + '=' + str(answer[key]))
|
||||||
|
elif API_TYPES_V1[key] is list:
|
||||||
|
answer_array.append(key + '=(' + str(" ".join(answer[key])) + ')')
|
||||||
else:
|
else:
|
||||||
answer_array.append(key + "='" + str(answer[key]) + "'")
|
answer_array.append(key + "='" + str(answer[key]) + "'")
|
||||||
if 'Content' in answer and answer['Content'] is not None:
|
if 'Content' in answer and answer['Content'] is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user