You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by ma...@apache.org on 2023/02/07 15:59:34 UTC

[airavata-django-portal] branch develop updated: AIRAVATA-3682 make shared directory readonly for non-admins

This is an automated email from the ASF dual-hosted git repository.

machristie pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/airavata-django-portal.git


The following commit(s) were added to refs/heads/develop by this push:
     new 4f48ff29 AIRAVATA-3682 make shared directory readonly for non-admins
4f48ff29 is described below

commit 4f48ff29a5f94364e3be262a5ef716d6f9e4ae1e
Author: Marcus Christie <ma...@apache.org>
AuthorDate: Tue Feb 7 10:59:17 2023 -0500

    AIRAVATA-3682 make shared directory readonly for non-admins
---
 django_airavata/apps/api/serializers.py            | 27 +++++++++--
 .../js/models/UserStorageDirectory.js              |  2 +
 .../js/models/UserStorageFile.js                   |  1 +
 .../js/models/UserStoragePath.js                   |  5 ++
 django_airavata/apps/api/view_utils.py             | 55 ++++++++++++++++++++++
 django_airavata/apps/api/views.py                  |  8 +++-
 .../components/storage/UserStorageCreateView.vue   |  5 +-
 .../storage/UserStorageFileSelectionContainer.vue  |  1 +
 .../components/storage/UserStoragePathViewer.vue   | 22 +++++++--
 9 files changed, 116 insertions(+), 10 deletions(-)

diff --git a/django_airavata/apps/api/serializers.py b/django_airavata/apps/api/serializers.py
index d429ae0b..b259c716 100644
--- a/django_airavata/apps/api/serializers.py
+++ b/django_airavata/apps/api/serializers.py
@@ -70,7 +70,7 @@ from django.contrib.auth import get_user_model
 from django.urls import reverse
 from rest_framework import serializers
 
-from . import models, thrift_utils
+from . import models, thrift_utils, view_utils
 
 log = logging.getLogger(__name__)
 
@@ -939,7 +939,22 @@ class ParserSerializer(thrift_utils.create_serializer_class(Parser)):
         lookup_url_kwarg='parser_id')
 
 
-class UserStorageFileSerializer(serializers.Serializer):
+class UserHasWriteAccessToPathSerializer(serializers.Serializer):
+    userHasWriteAccess = serializers.SerializerMethodField()
+
+    def get_userHasWriteAccess(self, instance):
+        request = self.context['request']
+        is_shared_dir = view_utils.is_shared_dir(instance["path"])
+        is_shared_path = view_utils.is_shared_path(instance["path"])
+        if is_shared_dir:
+            return False
+        elif is_shared_path:
+            return request.is_gateway_admin
+        else:
+            return True
+
+
+class UserStorageFileSerializer(UserHasWriteAccessToPathSerializer):
     name = serializers.CharField()
     downloadURL = serializers.SerializerMethodField()
     dataProductURI = serializers.CharField(source='data-product-uri')
@@ -955,7 +970,7 @@ class UserStorageFileSerializer(serializers.Serializer):
         return user_storage.get_lazy_download_url(request, data_product_uri=file['data-product-uri'])
 
 
-class UserStorageDirectorySerializer(serializers.Serializer):
+class UserStorageDirectorySerializer(UserHasWriteAccessToPathSerializer):
     name = serializers.CharField()
     path = serializers.CharField()
     createdTime = serializers.DateTimeField(source='created_time')
@@ -966,9 +981,13 @@ class UserStorageDirectorySerializer(serializers.Serializer):
         view_name='django_airavata_api:user-storage-items',
         lookup_field='path',
         lookup_url_kwarg='path')
+    isSharedDir = serializers.SerializerMethodField()
+
+    def get_isSharedDir(self, directory):
+        return view_utils.is_shared_dir(directory["path"])
 
 
-class UserStoragePathSerializer(serializers.Serializer):
+class UserStoragePathSerializer(UserHasWriteAccessToPathSerializer):
     isDir = serializers.BooleanField()
     directories = UserStorageDirectorySerializer(many=True)
     files = UserStorageFileSerializer(many=True)
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
index 2ff65dfc..94d5ead0 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageDirectory.js
@@ -7,6 +7,8 @@ const FIELDS = [
   { name: "modifiedTime", type: "date" },
   "size",
   "hidden",
+  "userHasWriteAccess",
+  "isSharedDir",
 ];
 
 export default class UserStorageDirectory extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
index ee6ac6a7..2ed73371 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStorageFile.js
@@ -8,6 +8,7 @@ const FIELDS = [
   { name: "modifiedTime", type: "date" },
   "size",
   "mimeType",
+  "userHasWriteAccess",
 ];
 
 export default class UserStorageFile extends BaseModel {
diff --git a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStoragePath.js b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStoragePath.js
index 339b7ddf..d3188201 100644
--- a/django_airavata/apps/api/static/django_airavata_api/js/models/UserStoragePath.js
+++ b/django_airavata/apps/api/static/django_airavata_api/js/models/UserStoragePath.js
@@ -23,6 +23,11 @@ const FIELDS = [
     type: "boolean",
     list: false,
   },
+  {
+    name: "userHasWriteAccess",
+    type: "boolean",
+    default: true,
+  },
 ];
 
 export default class UserStoragePath extends BaseModel {
diff --git a/django_airavata/apps/api/view_utils.py b/django_airavata/apps/api/view_utils.py
index 3f06ad00..c7f4e35b 100644
--- a/django_airavata/apps/api/view_utils.py
+++ b/django_airavata/apps/api/view_utils.py
@@ -1,8 +1,10 @@
 import logging
+import os
 from collections.__init__ import OrderedDict
 from datetime import datetime
 
 import pytz
+from airavata_django_portal_sdk import user_storage
 from django.conf import settings
 from django.http import Http404
 from django.http.request import QueryDict
@@ -226,3 +228,56 @@ class IsInAdminsGroupPermission(permissions.BasePermission):
 class ReadOnly(permissions.BasePermission):
     def has_permission(self, request, view):
         return request.method in permissions.SAFE_METHODS
+
+
+def is_shared_dir(path):
+    shared_dirs: dict = getattr(settings, 'GATEWAY_DATA_SHARED_DIRECTORIES', {})
+    return any(map(lambda n: n == path, shared_dirs.keys()))
+
+
+def is_shared_path(path):
+    shared_dirs: dict = getattr(settings, 'GATEWAY_DATA_SHARED_DIRECTORIES', {})
+    # FIXME: path returned when creating a new directory in user storage is an
+    # absolute path. Assume that when an absolute path is given that it was for
+    # a newly created directory and so it is not a shared path
+    if os.path.isabs(path):
+        return False
+    # check if path starts with a shared directory
+    return any(map(lambda n: os.path.commonpath((n, path)) == n, shared_dirs.keys()))
+
+
+class BaseSharedDirPermission(permissions.BasePermission):
+    def get_path(self, request, view) -> str:
+        raise NotImplementedError()
+
+    def has_permission(self, request, view):
+        if request.method in permissions.SAFE_METHODS:
+            return True
+
+        path = self.get_path(request, view)
+
+        # check if path starts with a shared directory
+        shared_path = is_shared_path(path)
+        shared_dir = is_shared_dir(path)
+        if shared_path:
+            # No user can delete a shared directory
+            if shared_dir and request.method == 'DELETE':
+                return False
+            # Only admins can create/update/delete files/directories in a shared directory
+            return request.is_gateway_admin
+
+        return True
+
+
+class DataProductSharedDirPermission(BaseSharedDirPermission):
+    def get_path(self, request, view) -> str:
+        data_product_uri = request.GET.get('data-product-uri', '')
+        file_metadata = user_storage.get_data_product_metadata(request, data_product_uri=data_product_uri)
+        return file_metadata["path"]
+
+
+class UserStorageSharedDirPermission(BaseSharedDirPermission):
+
+    def get_path(self, request, view):
+        # 'path' can be a url path parameter, query parameter or in the request body (data)
+        return request.query_params.get('path', request.data.get('path', view.kwargs.get('path')))
diff --git a/django_airavata/apps/api/views.py b/django_airavata/apps/api/views.py
index b419a14e..5701a901 100644
--- a/django_airavata/apps/api/views.py
+++ b/django_airavata/apps/api/views.py
@@ -36,7 +36,7 @@ from django.shortcuts import redirect
 from django.urls import reverse
 from django.views.decorators.gzip import gzip_page
 from rest_framework import mixins, pagination, status
-from rest_framework.decorators import action, api_view
+from rest_framework.decorators import action, api_view, permission_classes
 from rest_framework.exceptions import ParseError
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.renderers import JSONRenderer
@@ -47,8 +47,10 @@ from django_airavata.apps.api.view_utils import (
     APIBackedViewSet,
     APIResultIterator,
     APIResultPagination,
+    DataProductSharedDirPermission,
     GenericAPIBackedViewSet,
-    IsInAdminsGroupPermission
+    IsInAdminsGroupPermission,
+    UserStorageSharedDirPermission
 )
 from django_airavata.apps.auth import iam_admin_client
 from django_airavata.apps.auth.models import EmailVerification
@@ -882,6 +884,7 @@ def download_file(request):
 
 
 @api_view(http_method_names=['DELETE'])
+@permission_classes([IsAuthenticated, DataProductSharedDirPermission])
 def delete_file(request):
     # TODO check that user has write access to this file using sharing API
     data_product_uri = request.GET.get('data-product-uri', '')
@@ -1355,6 +1358,7 @@ class ParserViewSet(mixins.CreateModelMixin,
 
 class UserStoragePathView(APIView):
     serializer_class = serializers.UserStoragePathSerializer
+    permission_classes = (IsAuthenticated, UserStorageSharedDirPermission)
 
     def get(self, request, path="/", format=None):
         # AIRAVATA-3460 Allow passing path as a query parameter instead
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageCreateView.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageCreateView.vue
index b8828cc5..7ff525da 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageCreateView.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageCreateView.vue
@@ -10,7 +10,7 @@
         </p>
       </div>
     </div>
-    <div class="row">
+    <div class="row" v-if="userHasWriteAccess">
       <div class="col">
         <uppy
           class="mb-1"
@@ -56,6 +56,9 @@ export default {
     username() {
       return session.Session.username;
     },
+    userHasWriteAccess() {
+      return this.userStoragePath.userHasWriteAccess;
+    },
   },
   data() {
     return {
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageFileSelectionContainer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageFileSelectionContainer.vue
index 1193a414..fa8b24eb 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageFileSelectionContainer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStorageFileSelectionContainer.vue
@@ -9,6 +9,7 @@
       :include-delete-action="false"
       :include-select-file-action="true"
       :include-create-file-action="false"
+      :include-download-action="false"
       :download-in-new-window="true"
       :selected-data-product-uris="selectedDataProductUris"
     >
diff --git a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
index 9f99d059..31cb8d4b 100644
--- a/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
+++ b/django_airavata/apps/workspace/static/django_airavata_workspace/js/components/storage/UserStoragePathViewer.vue
@@ -35,6 +35,9 @@
           @click="directorySelected(data.item)"
         >
           <i class="fa fa-folder-open"></i> {{ data.item.name }}
+          <template v-if="data.item.isSharedDir">
+            <b-badge class="ml-1">shared</b-badge>
+          </template>
         </b-link>
         <user-storage-link
           v-else
@@ -58,7 +61,7 @@
         </b-button>
 
         <b-link
-          v-if="data.item.type === 'file'"
+          v-if="includeDownloadAction && data.item.type === 'file'"
           class="action-link"
           :href="`${data.item.downloadURL}&download`"
         >
@@ -66,14 +69,17 @@
           <i class="fa fa-download" aria-hidden="true"></i>
         </b-link>
         <b-link
-          v-if="data.item.type === 'dir'"
+          v-if="includeDownloadAction && data.item.type === 'dir'"
           class="action-link"
           :href="`/sdk/download-dir/?path=${data.item.path}`"
         >
           Download Zip
           <i class="fa fa-file-archive" aria-hidden="true"></i>
         </b-link>
-        <delete-link v-if="includeDeleteAction" @delete="deleteItem(data.item)">
+        <delete-link
+          v-if="includeDeleteAction && data.item.userHasWriteAccess"
+          @delete="deleteItem(data.item)"
+        >
           Are you sure you want to delete <strong>{{ data.item.name }}</strong
           >?
         </delete-link>
@@ -113,6 +119,10 @@ export default {
       type: Boolean,
       default: true,
     },
+    includeDownloadAction: {
+      type: Boolean,
+      default: true,
+    },
     downloadInNewWindow: {
       type: Boolean,
       default: false,
@@ -179,6 +189,8 @@ export default {
               modifiedTime: d.modifiedTime,
               modifiedTimestamp: d.modifiedTime.getTime(), // for sorting
               size: d.size,
+              userHasWriteAccess: d.userHasWriteAccess,
+              isSharedDir: d.isSharedDir,
             };
           });
         const files = this.userStoragePath.files.map((f) => {
@@ -191,6 +203,7 @@ export default {
             modifiedTime: f.modifiedTime,
             modifiedTimestamp: f.modifiedTime.getTime(), // for sorting
             size: f.size,
+            userHasWriteAccess: f.userHasWriteAccess,
           };
         });
         return dirs.concat(files);
@@ -201,6 +214,9 @@ export default {
     downloadTarget() {
       return this.downloadInNewWindow ? "_blank" : "_self";
     },
+    userHasWriteAccess() {
+      return this.userStoragePath.userHasWriteAccess;
+    },
   },
   methods: {
     getFormattedSize(size) {