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) {