diff --git a/services/webapp/code/rosetta/core_app/api.py b/services/webapp/code/rosetta/core_app/api.py index 0b5062c064c251e04272d0e7d006c07eaf9b4c79..edb9e8db3defd37336960fbaf79603938fe62550 100644 --- a/services/webapp/code/rosetta/core_app/api.py +++ b/services/webapp/code/rosetta/core_app/api.py @@ -13,7 +13,7 @@ from rest_framework import status, serializers, viewsets from rest_framework.views import APIView from .utils import format_exception, send_email, os_shell, now_t, get_ssh_access_mode_credentials, get_or_create_container_from_repository, booleanize from .models import Profile, Task, TaskStatuses, Computing, Storage, KeyPair -from .exceptions import ConsistencyException +from .exceptions import PermissionDenied import json # Setup logging @@ -153,8 +153,12 @@ class PrivatePOSTAPI(APIView): # Call API logic return self._post(request) except Exception as e: - logger.error(format_exception(e)) - return error500('Got error in processing request: {}'.format(e)) + # TODO: refactor me + if isinstance(e, PermissionDenied): + return error400(format(e)) + else: + logger.error(format_exception(e)) + return error500('Got error in processing request: {}'.format(e)) class PrivateGETAPI(APIView): '''Base private GET API class''' @@ -174,9 +178,12 @@ class PrivateGETAPI(APIView): # Call API logic return self._get(request) except Exception as e: - logger.error(format_exception(e)) - return error500('Got error in processing request: {}'.format(e)) - + # TODO: refactor me + if isinstance(e, PermissionDenied): + return error400(format(e)) + else: + logger.error(format_exception(e)) + return error500('Got error in processing request: {}'.format(e)) #============================== @@ -689,6 +696,9 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): def delete(self, path, user, storage): + if storage.read_only: + raise PermissionDenied('This storage is read-only') + if storage.type == 'generic_posix': shell_path = self.sanitize_and_prepare_shell_path(path, user, storage) @@ -707,6 +717,9 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): def mkdir(self, path, user, storage, force=False): + if storage.read_only: + raise PermissionDenied('This storage is read-only') + path = self.sanitize_and_prepare_shell_path(path, user, storage) if storage.type == 'generic_posix': @@ -751,6 +764,9 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): def rename(self, old, new, user, storage): + if storage.read_only: + raise PermissionDenied('This storage is read-only') + if storage.type == 'generic_posix': old = self.sanitize_and_prepare_shell_path(old, user, storage) @@ -772,6 +788,9 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): def copy(self, source, target, user, storage): + if storage.read_only: + raise PermissionDenied('This storage is read-only') + if storage.type == 'generic_posix': source = self.sanitize_and_prepare_shell_path(source, user, storage) @@ -805,6 +824,9 @@ class FileManagerAPI(PrivateGETAPI, PrivatePOSTAPI): def scp_to(self, source, target, user, storage, mode='get'): + if storage.read_only: + raise PermissionDenied('This storage is read-only') + source = self.sanitize_shell_path(source) # This is a folder on Rosetta (/tmp) target = self.sanitize_and_prepare_shell_path(target, user, storage) diff --git a/services/webapp/code/rosetta/core_app/computing_managers.py b/services/webapp/code/rosetta/core_app/computing_managers.py index 9d78a26ca39b730233be8c190466fc61546dcc52..4aad4d42a4988f23383291e7f32c6b4c4ede0716 100644 --- a/services/webapp/code/rosetta/core_app/computing_managers.py +++ b/services/webapp/code/rosetta/core_app/computing_managers.py @@ -147,11 +147,17 @@ class InternalStandaloneComputingManager(StandaloneComputingManager): if '$USER' in expanded_bind_path: expanded_bind_path = expanded_bind_path.replace('$USER', task.user.username) + # Read only? + if storage.read_only: + mode_string = ':ro' + else: + mode_string = '' + # Add the bind if not binds: - binds = '-v{}:{}'.format(expanded_base_path, expanded_bind_path) + binds = '-v{}:{}{}'.format(expanded_base_path, expanded_bind_path, mode_string) else: - binds += ' -v{}:{}'.format(expanded_base_path, expanded_bind_path) + binds += ' -v{}:{}{}'.format(expanded_base_path, expanded_bind_path, mode_string) # Host name, image entry command run_command += ' {} -h task-{} --name task-{} -d -t {}/{}:{}'.format(binds, task.short_uuid, task.short_uuid, task.container.registry, task.container.image_name, task.container.image_tag) @@ -348,11 +354,17 @@ class SSHStandaloneComputingManager(StandaloneComputingManager, SSHComputingMana if '$USER' in expanded_bind_path: expanded_bind_path = expanded_bind_path.replace('$USER', task.user.username) + # Read only? + if storage.read_only: + mode_string = ':ro' + else: + mode_string = '' + # Add the bind if not binds: - binds = '-v{}:{}'.format(expanded_base_path, expanded_bind_path) + binds = '-v{}:{}{}'.format(expanded_base_path, expanded_bind_path, mode_string) else: - binds += ' -v{}:{}'.format(expanded_base_path, expanded_bind_path) + binds += ' -v{}:{}{}'.format(expanded_base_path, expanded_bind_path, mode_string) # TODO: remove this hardcoding prefix = 'sudo' if (computing_host == 'slurmclusterworker' and container_engine=='docker') else '' diff --git a/services/webapp/code/rosetta/core_app/exceptions.py b/services/webapp/code/rosetta/core_app/exceptions.py index 12a3bd8d8a1522a1868e875847d4c8f64cbf2ea0..cbb94230cf28148a79bf109fd4a3739d7fdcf09c 100644 --- a/services/webapp/code/rosetta/core_app/exceptions.py +++ b/services/webapp/code/rosetta/core_app/exceptions.py @@ -4,3 +4,6 @@ class ErrorMessage(Exception): class ConsistencyException(Exception): pass + +class PermissionDenied(Exception): + pass \ No newline at end of file diff --git a/services/webapp/code/rosetta/core_app/migrations/0037_storage_read_only.py b/services/webapp/code/rosetta/core_app/migrations/0037_storage_read_only.py new file mode 100644 index 0000000000000000000000000000000000000000..cd05fb70714574b950b23ecfdbfa8d1a4ffdf4bf --- /dev/null +++ b/services/webapp/code/rosetta/core_app/migrations/0037_storage_read_only.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2025-03-05 09:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core_app', '0036_container_disable_http_basicauth_embedding'), + ] + + operations = [ + migrations.AddField( + model_name='storage', + name='read_only', + field=models.BooleanField(default=False, verbose_name='Read only? (if supported)'), + ), + ] diff --git a/services/webapp/code/rosetta/core_app/models.py b/services/webapp/code/rosetta/core_app/models.py index 9d6cb5f4e4e4b6bbd3487224d886484f51dd0d6e..8efcb05bee0c18d7670f3077cc380d83fcbd4fc2 100644 --- a/services/webapp/code/rosetta/core_app/models.py +++ b/services/webapp/code/rosetta/core_app/models.py @@ -387,6 +387,9 @@ class Storage(models.Model): base_path = models.CharField('Base path', max_length=4096, blank=False, null=False) bind_path = models.CharField('Bind path', max_length=4096, blank=True, null=True) + # Read only? + read_only = models.BooleanField('Read only? (if supported)', default=False) + # Link with a computing resource computing = models.ForeignKey(Computing, related_name='storages', on_delete=models.CASCADE, blank=True, null=True) # Make optional? access_through_computing = models.BooleanField('Access through linked computing resource?', default=False)