diff --git a/cterasdk/asynchronous/core/files/browser.py b/cterasdk/asynchronous/core/files/browser.py index 75af9ad2..4c2e91c9 100644 --- a/cterasdk/asynchronous/core/files/browser.py +++ b/cterasdk/asynchronous/core/files/browser.py @@ -142,7 +142,7 @@ async def public_link(self, path, access='RO', expire_in=30): """ return await Link(io.public_link, self._core, path, access, expire_in).a_execute() - async def copy(self, *paths, destination=None, resolver=None, cursor=None, wait=False): + async def copy(self, *paths, destination=None, resolver=None, cursor=None, wait=False, strict_permission=False): """ Copy one or more files or folders. @@ -156,7 +156,16 @@ async def copy(self, *paths, destination=None, resolver=None, cursor=None, wait= :raises cterasdk.exceptions.io.core.CopyError: Raised on failure copying resources. """ try: - return await Copy(io.copy, self._core, wait, *paths, destination=destination, resolver=resolver, cursor=cursor).a_execute() + return await Copy( + io.copy, + self._core, + wait, + *paths, + destination=destination, + resolver=resolver, + cursor=cursor, + strict_permission=strict_permission + ).a_execute() except ValueError: raise ValueError('Copy destination was not specified.') @@ -175,7 +184,7 @@ async def permalink(self, path): class CloudDrive(FileBrowser): """Async CloudDrive API with upload and sharing functionality.""" - async def upload(self, destination, handle, name=None, size=None): + async def upload(self, destination, handle, name=None, size=None, strict_permission=False): """ Upload from file handle. @@ -187,9 +196,18 @@ async def upload(self, destination, handle, name=None, size=None): :rtype: str :raises cterasdk.exceptions.io.core.UploadError: Raised on upload failure. """ - return await Upload(io.upload, self._core, io.listdir, destination, handle, name, size).a_execute() + return await Upload( + io.upload, + self._core, + io.listdir, + destination, + handle, + name, + size, + strict_permission=strict_permission + ).a_execute() - async def upload_file(self, path, destination): + async def upload_file(self, path, destination, strict_permission=False): """ Upload a file. @@ -201,9 +219,15 @@ async def upload_file(self, path, destination): """ _, name = commonfs.split_file_directory(path) with open(path, 'rb') as handle: - return await self.upload(destination, handle, name, commonfs.properties(path)['size']) + return await self.upload( + destination, + handle, + name, + commonfs.properties(path)['size'], + strict_permission=strict_permission + ) - async def mkdir(self, path): + async def mkdir(self, path, strict_permission=False): """ Create a directory. @@ -212,9 +236,9 @@ async def mkdir(self, path): :rtype: str :raises cterasdk.exceptions.io.core.CreateDirectoryError: Raised on error creating directory. """ - return await CreateDirectory(io.mkdir, self._core, path).a_execute() + return await CreateDirectory(io.mkdir, self._core, path, strict_permission=strict_permission).a_execute() - async def makedirs(self, path): + async def makedirs(self, path, strict_permission=False): """ Recursively create a directory. @@ -223,9 +247,9 @@ async def makedirs(self, path): :rtype: str :raises cterasdk.exceptions.io.core.CreateDirectoryError: Raised on error creating directory. """ - return await CreateDirectory(io.mkdir, self._core, path, True).a_execute() + return await CreateDirectory(io.mkdir, self._core, path, True, strict_permission=strict_permission).a_execute() - async def rename(self, path, name, *, resolver=None, wait=False): + async def rename(self, path, name, *, resolver=None, wait=False, strict_permission=False): """ Rename a file or folder. @@ -237,9 +261,17 @@ async def rename(self, path, name, *, resolver=None, wait=False): :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask` :raises cterasdk.exceptions.io.core.RenameError: Raised on error renaming object. """ - return await Rename(io.move, self._core, wait, path, name, resolver).a_execute() + return await Rename( + io.move, + self._core, + wait, + path, + name, + resolver, + strict_permission=strict_permission + ).a_execute() - async def delete(self, *paths, wait=False): + async def delete(self, *paths, wait=False, strict_permission=False): """ Delete one or more files or folders. @@ -249,7 +281,7 @@ async def delete(self, *paths, wait=False): :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask` :raises cterasdk.exceptions.io.core.DeleteError: Raised on error deleting resources. """ - return await Delete(io.delete, self._core, wait, *paths).a_execute() + return await Delete(io.delete, self._core, wait, *paths, strict_permission=strict_permission).a_execute() async def undelete(self, *paths, wait=False): """ @@ -263,7 +295,7 @@ async def undelete(self, *paths, wait=False): """ return await Recover(io.undelete, self._core, wait, *paths).a_execute() - async def move(self, *paths, destination=None, resolver=None, cursor=None, wait=False): + async def move(self, *paths, destination=None, resolver=None, cursor=None, wait=False, strict_permission=False): """ Move one or more files or folders. @@ -277,7 +309,16 @@ async def move(self, *paths, destination=None, resolver=None, cursor=None, wait= :raises cterasdk.exceptions.io.core.MoveError: Raised on error moving resources. """ try: - return await Move(io.move, self._core, wait, *paths, destination=destination, resolver=resolver, cursor=cursor).a_execute() + return await Move( + io.move, + self._core, + wait, + *paths, + destination=destination, + resolver=resolver, + cursor=cursor, + strict_permission=strict_permission + ).a_execute() except ValueError: raise ValueError('Move destination was not specified.') diff --git a/cterasdk/cio/core/commands.py b/cterasdk/cio/core/commands.py index 183e1396..e2642e1c 100644 --- a/cterasdk/cio/core/commands.py +++ b/cterasdk/cio/core/commands.py @@ -20,6 +20,74 @@ logger = logging.getLogger('cterasdk.core') +def _normalize_rc(rc): + if isinstance(rc, str): + return rc.strip() + return rc + + +def _normalize_msg(msg): + if isinstance(msg, str): + return msg.strip().lower() + return msg + + +def _extract_rc_msg(result): + if result is None: + return None, None + if isinstance(result, str): + return None, result + return getattr(result, 'rc', None), getattr(result, 'msg', None) + + +_STRICT_PERMISSION_ERROR_SET = { + (None, None), + (None, ''), + (0, None), + ('0', None), + (None, 'permission denied'), + (None, 'access denied'), + (None, 'read only'), + (None, 'action is not allowed'), + ('permissiondenied', None), + (None, 'permissiondenied'), + ('permissiondenied', 'permissiondenied'), +} + +_STRICT_PERMISSION_TASK_ERROR_SET = { + (None, None, 'permissiondenied'), + (None, None, 'permission denied'), + (None, None, 'access denied'), + (None, None, 'read only'), + (None, None, 'action is not allowed'), + (None, 'permission denied', None), + (None, 'access denied', None), + (None, 'read only', None), + (None, 'action is not allowed', None), + (0, None, 'permissiondenied'), + ('0', None, 'permissiondenied'), +} + + +def _raise_strict_permission_denied(result, path): + rc, msg = _extract_rc_msg(result) + rc = _normalize_rc(rc) + msg = _normalize_msg(msg) + logger.info( + 'strict_permission response for %s: rc=%r msg=%r raw=%s', + path, rc, msg, type(result).__name__ + ) + if (rc, msg) in _STRICT_PERMISSION_ERROR_SET: + raise exceptions.io.core.PrivilegeError(path) + + +def _extract_task_error_tuple(result): + rc = _normalize_rc(getattr(result, 'rc', None)) + msg = _normalize_msg(getattr(result, 'msg', None)) + error_type = _normalize_msg(getattr(result, 'error_type', None)) + return rc, msg, error_type + + def split_file_directory(listdir, receiver, destination): """ Split a path into its parent directory and final component. @@ -120,12 +188,13 @@ def ensure_writeable(resource, directory): class Upload(PortalCommand): - def __init__(self, function, receiver, listdir, destination, fd, name, size): + def __init__(self, function, receiver, listdir, destination, fd, name, size, strict_permission=False): super().__init__(function, receiver) self.destination = automatic_resolution(destination, receiver.context) self._resolver = PathResolver(listdir, receiver, self.destination, name) self.size = size self.fd = fd + self._strict_permission = strict_permission self._resource = None def get_parameter(self): @@ -156,6 +225,8 @@ async def _a_execute(self): def _handle_response(self, r): path = self.destination.relative + if self._strict_permission: + _raise_strict_permission_denied(r, path) if r.rc: error = exceptions.io.core.UploadError(r.msg, path) logger.error('Upload failed: %s', path) @@ -596,10 +667,11 @@ def _handle_exception(self, e): class CreateDirectory(PortalCommand): """Create Directory""" - def __init__(self, function, receiver, path, parents=False): + def __init__(self, function, receiver, path, parents=False, strict_permission=False): super().__init__(function, receiver) self.path = automatic_resolution(path, receiver.context) self.parents = parents + self._strict_permission = strict_permission def get_parameter(self): param = Object() @@ -613,7 +685,17 @@ def _before_command(self): def _parents_generator(self): if self.parents: parts = self.path.parts - for i in range(1, len(parts)): + start_index = 1 + known_roots = ('My Files', 'Shared With Me', 'Shared', 'Team Portal') + if parts: + if parts[0] in known_roots: + start_index = 2 + elif parts[0] in ('Users', 'Groups') and len(parts) > 1: + if len(parts) > 2 and parts[2] in known_roots: + start_index = 4 + else: + start_index = 3 + for i in range(start_index, len(parts)): yield automatic_resolution('/'.join(parts[:i]), self._receiver.context) else: yield self.path @@ -622,7 +704,12 @@ def _execute(self): if self.parents: for path in self._parents_generator(): try: - CreateDirectory(self._function, self._receiver, path).execute() + CreateDirectory( + self._function, + self._receiver, + path, + strict_permission=self._strict_permission + ).execute() except exceptions.io.core.CreateDirectoryError as e: CreateDirectory._suppress_file_conflict_error(e) with self.trace_execution(): @@ -632,7 +719,12 @@ async def _a_execute(self): if self.parents: for path in self._parents_generator(): try: - await CreateDirectory(self._function, self._receiver, path).a_execute() + await CreateDirectory( + self._function, + self._receiver, + path, + strict_permission=self._strict_permission + ).a_execute() except exceptions.io.core.CreateDirectoryError as e: CreateDirectory._suppress_file_conflict_error(e) with self.trace_execution(): @@ -647,6 +739,8 @@ def _handle_response(self, r): path = self.path.relative if r is None or r == 'Ok': return path + if self._strict_permission: + _raise_strict_permission_denied(r, path) error, cause = exceptions.io.core.CreateDirectoryError(path), None if r == ResourceError.FileWithTheSameNameExist: @@ -903,10 +997,11 @@ def _handle_response(self, r): class TaskCommand(PortalCommand): - def __init__(self, function, receiver, block): + def __init__(self, function, receiver, block, strict_permission=False): super().__init__(function, receiver) self.block = block self.background = True + self._strict_permission = strict_permission @abstractmethod def _progress_str(self): @@ -938,12 +1033,19 @@ async def _a_execute(self): return await function(self.get_parameter()) def _handle_response(self, r): + if self._strict_permission: + rc, msg, error_type = _extract_task_error_tuple(r) + if (rc, msg, error_type) in _STRICT_PERMISSION_TASK_ERROR_SET: + raise exceptions.io.core.PrivilegeError('') if not self.block: return r if r.completed: return self._task_complete(r) + if r.completed_with_warnings: + return r + if r.failed or r.completed_with_warnings: return self._task_error(r) @@ -958,8 +1060,8 @@ def _task_error(self, task): # pylint: disable=no-self-use class MultiResourceCommand(TaskCommand): - def __init__(self, function, receiver, block, *paths): - super().__init__(function, receiver, block) + def __init__(self, function, receiver, block, *paths, strict_permission=False): + super().__init__(function, receiver, block, strict_permission=strict_permission) self.paths = list(automatic_resolution(paths, receiver.context)) def get_parameter(self): @@ -998,8 +1100,9 @@ def _task_error(self, task): class ResolverCommand(TaskCommand): - def __init__(self, function, receiver, block, *paths, destination=None, resolver=None, cursor=None): - super().__init__(function, receiver, block) + def __init__(self, function, receiver, block, *paths, destination=None, resolver=None, cursor=None, + strict_permission=False): + super().__init__(function, receiver, block, strict_permission=strict_permission) self.paths = list(automatic_resolution(paths, receiver.context)) self.destination = automatic_resolution(destination, receiver.context) self.resolver = resolver @@ -1088,11 +1191,13 @@ def _progress_str(self): def _try_with_resolver(self, cursor): return Copy(self._function, self._receiver, self.block, *self.paths, - destination=self.destination, resolver=self.resolver, cursor=cursor).execute() + destination=self.destination, resolver=self.resolver, cursor=cursor, + strict_permission=self._strict_permission).execute() async def _a_try_with_resolver(self, cursor): return await Copy(self._function, self._receiver, self.block, *self.paths, - destination=self.destination, resolver=self.resolver, cursor=cursor).a_execute() + destination=self.destination, resolver=self.resolver, cursor=cursor, + strict_permission=self._strict_permission).a_execute() @property def _error_object(self): @@ -1106,11 +1211,13 @@ def _progress_str(self): def _try_with_resolver(self, cursor): return Move(self._function, self._receiver, self.block, *self.paths, - destination=self.destination, resolver=self.resolver, cursor=cursor).execute() + destination=self.destination, resolver=self.resolver, cursor=cursor, + strict_permission=self._strict_permission).execute() async def _a_try_with_resolver(self, cursor): return await Move(self._function, self._receiver, self.block, *self.paths, - destination=self.destination, resolver=self.resolver, cursor=cursor).a_execute() + destination=self.destination, resolver=self.resolver, cursor=cursor, + strict_permission=self._strict_permission).a_execute() @property def _error_object(self): @@ -1122,21 +1229,26 @@ class Rename(Move): def _progress_str(self): return 'Renaming' - def __init__(self, function, receiver, block, path, new_name, resolver, cursor=None): - super().__init__(function, receiver, block, - *[(path, automatic_resolution(path, receiver.context).parent.join(new_name))], - resolver=resolver, cursor=cursor - ) + def __init__(self, function, receiver, block, path, new_name, resolver, cursor=None, strict_permission=False): + super().__init__( + function, + receiver, + block, + *[(path, automatic_resolution(path, receiver.context).parent.join(new_name))], + resolver=resolver, + cursor=cursor, + strict_permission=strict_permission + ) def _try_with_resolver(self, cursor): source, destination = self.paths[0] return Rename(self._function, self._receiver, self.block, source, destination.name, - resolver=self.resolver, cursor=cursor).execute() + resolver=self.resolver, cursor=cursor, strict_permission=self._strict_permission).execute() async def _a_try_with_resolver(self, cursor): source, destination = self.paths[0] return await Rename(self._function, self._receiver, self.block, source, destination.name, - resolver=self.resolver, cursor=cursor).a_execute() + resolver=self.resolver, cursor=cursor, strict_permission=self._strict_permission).a_execute() @property def _error_object(self): diff --git a/cterasdk/core/files/browser.py b/cterasdk/core/files/browser.py index 2202ecc5..f6c15d39 100644 --- a/cterasdk/core/files/browser.py +++ b/cterasdk/core/files/browser.py @@ -142,7 +142,7 @@ def public_link(self, path, access='RO', expire_in=30): """ return Link(io.public_link, self._core, path, access, expire_in).execute() - def copy(self, *paths, destination=None, resolver=None, cursor=None, wait=True): + def copy(self, *paths, destination=None, resolver=None, cursor=None, wait=True, strict_permission=False): """ Copy one or more files or folders. @@ -156,7 +156,16 @@ def copy(self, *paths, destination=None, resolver=None, cursor=None, wait=True): :raises cterasdk.exceptions.io.core.CopyError: Raised on failure to copy resources. """ try: - return Copy(io.copy, self._core, wait, *paths, destination=destination, resolver=resolver, cursor=cursor).execute() + return Copy( + io.copy, + self._core, + wait, + *paths, + destination=destination, + resolver=resolver, + cursor=cursor, + strict_permission=strict_permission + ).execute() except ValueError: raise ValueError('Copy destination was not specified.') @@ -175,7 +184,7 @@ def permalink(self, path): class CloudDrive(FileBrowser): """CloudDrive extends FileBrowser with upload and share functionality.""" - def upload(self, destination, handle, name=None, size=None): + def upload(self, destination, handle, name=None, size=None, strict_permission=False): """ Upload from file handle. @@ -187,9 +196,18 @@ def upload(self, destination, handle, name=None, size=None): :rtype: str :raises cterasdk.exceptions.io.core.UploadError: Raised on upload failure. """ - return Upload(io.upload, self._core, io.listdir, destination, handle, name, size).execute() + return Upload( + io.upload, + self._core, + io.listdir, + destination, + handle, + name, + size, + strict_permission=strict_permission + ).execute() - def upload_file(self, path, destination): + def upload_file(self, path, destination, strict_permission=False): """ Upload a file. @@ -201,9 +219,15 @@ def upload_file(self, path, destination): """ _, name = commonfs.split_file_directory(path) with open(path, 'rb') as handle: - return self.upload(destination, handle, name, commonfs.properties(path)['size']) + return self.upload( + destination, + handle, + name, + commonfs.properties(path)['size'], + strict_permission=strict_permission + ) - def mkdir(self, path): + def mkdir(self, path, strict_permission=False): """ Create a directory. @@ -212,9 +236,9 @@ def mkdir(self, path): :rtype: str :raises cterasdk.exceptions.io.core.CreateDirectoryError: Raised on error creating directory. """ - return CreateDirectory(io.mkdir, self._core, path).execute() + return CreateDirectory(io.mkdir, self._core, path, strict_permission=strict_permission).execute() - def makedirs(self, path): + def makedirs(self, path, strict_permission=False): """ Recursively create a directory. @@ -223,9 +247,9 @@ def makedirs(self, path): :rtype: str :raises cterasdk.exceptions.io.core.CreateDirectoryError: Raised on error creating directory. """ - return CreateDirectory(io.mkdir, self._core, path, True).execute() + return CreateDirectory(io.mkdir, self._core, path, True, strict_permission=strict_permission).execute() - def rename(self, path, name, *, resolver=None, wait=True): + def rename(self, path, name, *, resolver=None, wait=True, strict_permission=False): """ Rename a file or folder. @@ -237,9 +261,17 @@ def rename(self, path, name, *, resolver=None, wait=True): :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask` :raises cterasdk.exceptions.io.core.RenameError: Raised on error renaming object. """ - return Rename(io.move, self._core, wait, path, name, resolver).execute() + return Rename( + io.move, + self._core, + wait, + path, + name, + resolver, + strict_permission=strict_permission + ).execute() - def delete(self, *paths, wait=True): + def delete(self, *paths, wait=True, strict_permission=False): """ Delete one or more files or folders. @@ -249,7 +281,7 @@ def delete(self, *paths, wait=True): :rtype: cterasdk.common.object.Object or :class:`cterasdk.lib.tasks.AwaitablePortalTask` :raises cterasdk.exceptions.io.core.DeleteError: Raised on error deleting resources. """ - return Delete(io.delete, self._core, wait, *paths).execute() + return Delete(io.delete, self._core, wait, *paths, strict_permission=strict_permission).execute() def undelete(self, *paths, wait=True): """ @@ -263,7 +295,7 @@ def undelete(self, *paths, wait=True): """ return Recover(io.undelete, self._core, wait, *paths).execute() - def move(self, *paths, destination=None, resolver=None, cursor=None, wait=True): + def move(self, *paths, destination=None, resolver=None, cursor=None, wait=True, strict_permission=False): """ Move one or more files or folders. @@ -277,7 +309,16 @@ def move(self, *paths, destination=None, resolver=None, cursor=None, wait=True): :raises cterasdk.exceptions.io.core.MoveError: Raised on error moving resources. """ try: - return Move(io.move, self._core, wait, *paths, destination=destination, resolver=resolver, cursor=cursor).execute() + return Move( + io.move, + self._core, + wait, + *paths, + destination=destination, + resolver=resolver, + cursor=cursor, + strict_permission=strict_permission + ).execute() except ValueError: raise ValueError('Move destination was not specified.') diff --git a/tests/ut/core/user/test_mkdir.py b/tests/ut/core/user/test_mkdir.py index 60c6fc6a..4af36857 100644 --- a/tests/ut/core/user/test_mkdir.py +++ b/tests/ut/core/user/test_mkdir.py @@ -1,6 +1,9 @@ from unittest import mock import munch +from cterasdk.common import Object +from cterasdk import exceptions + from tests.ut.core.user import base_user @@ -21,3 +24,14 @@ def test_mkdir(self): }) actual_param = self._services.api.execute.call_args[0][2] self._assert_equal_objects(actual_param, expected_param) + + def test_mkdir_strict_permission_success(self): + self._init_services(execute_response=None) + ret = self._services.files.mkdir(self._directory, strict_permission=True) + self.assertEqual(ret, self._directory) + + def test_mkdir_strict_permission_denied(self): + execute_response = Object(rc=0) + self._init_services(execute_response=execute_response) + with self.assertRaises(exceptions.io.core.PrivilegeError): + self._services.files.mkdir(self._directory, strict_permission=True) diff --git a/tests/ut/core/user/test_move.py b/tests/ut/core/user/test_move.py index 2ae91115..d35a03f9 100644 --- a/tests/ut/core/user/test_move.py +++ b/tests/ut/core/user/test_move.py @@ -1,5 +1,8 @@ from unittest import mock +from cterasdk.common import Object +from cterasdk import exceptions + from tests.ut.core.user import base_user @@ -21,6 +24,14 @@ def test_move_no_wait(self): self._assert_equal_objects(actual_param, expected_param) self.assertEqual(ret.ref, execute_response) + def test_move_strict_permission_denied(self): + task = Object(msg='permission denied') + self._services.tasks.wait = mock.MagicMock(return_value=task) + self._init_services(execute_response=self._task_reference) + with self.assertRaises(exceptions.io.core.PrivilegeError): + self._services.files.move(self._source, destination=self._dest, strict_permission=True) + self._services.tasks.wait.assert_called_once_with(self._task_reference) + def _create_move_resource_param(self): destinations = [base_user.BaseCoreServicesTest.encode_path(self._dest + '/' + self._filename)] return self._create_action_resource_param([base_user.BaseCoreServicesTest.encode_path(self._source)], destinations)