From fb8ffb059a9eba216738780f9afe7f73a35ccf68 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de>
Date: Thu, 12 Oct 2023 12:19:41 +0200
Subject: [PATCH] Respect file prefix in bucket permissions in bucket view

#71
---
 .../object-storage/BucketListItem.vue         | 10 +-
 .../object-storage/modals/PermissionModal.vue |  2 +-
 src/stores/s3objects.ts                       |  9 +-
 src/stores/users.ts                           |  2 +-
 src/views/object-storage/BucketView.vue       | 98 +++++++++++++------
 5 files changed, 83 insertions(+), 38 deletions(-)

diff --git a/src/components/object-storage/BucketListItem.vue b/src/components/object-storage/BucketListItem.vue
index b020e6c..0650a4c 100644
--- a/src/components/object-storage/BucketListItem.vue
+++ b/src/components/object-storage/BucketListItem.vue
@@ -11,6 +11,7 @@ import { Tooltip } from "bootstrap";
 import { useBucketStore } from "@/stores/buckets";
 import { useRouter } from "vue-router";
 import { useAuthStore } from "@/stores/users";
+import type { FolderTree } from "@/types/PseudoFolder";
 
 const props = defineProps<{
   active: boolean;
@@ -27,6 +28,13 @@ const router = useRouter();
 const permission = computed<BucketPermissionOut | undefined>(
   () => permissionRepository.ownPermissions[props.bucket.name],
 );
+const subFolder = computed<FolderTree>(() => {
+  const subFolders: Record<string, FolderTree> = {};
+  if (permission.value?.file_prefix != null) {
+    subFolders[permission.value.file_prefix] = { subFolders: {}, files: [] };
+  }
+  return { subFolders: subFolders, files: [] };
+});
 
 const emit = defineEmits<{
   (e: "delete-bucket", bucketName: string): void;
@@ -50,7 +58,7 @@ onMounted(() => {
     v-if="permission != undefined && props.active"
     :modalID="'view-permission-modal' + randomIDSuffix"
     :bucket-name="props.bucket.name"
-    :sub-folders="{ subFolders: {}, files: [] }"
+    :sub-folders="subFolder"
     :edit-user-permission="permission"
     :readonly="true"
     :editable="false"
diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue
index 3ac7ac5..485f1ac 100644
--- a/src/components/object-storage/modals/PermissionModal.vue
+++ b/src/components/object-storage/modals/PermissionModal.vue
@@ -160,7 +160,7 @@ function findSubFolders(
     const subFolderString =
       (parentFolders.length > 0 ? parentFolders.join("/") + "/" : "") +
       subFolder +
-      "/";
+      (subFolder.endsWith("/") ? "" : "/");
     arr.push(
       subFolderString,
       ...findSubFolders(
diff --git a/src/stores/s3objects.ts b/src/stores/s3objects.ts
index c7581cb..c2ed8e1 100644
--- a/src/stores/s3objects.ts
+++ b/src/stores/s3objects.ts
@@ -39,12 +39,12 @@ export const useS3ObjectStore = defineStore({
     },
   getters: {
     getPresignedUrl(): (bucketName: string, key: string) => Promise<string> {
-      return async (bucketName, key) => {
+      return (bucketName, key) => {
         const command = new GetObjectCommand({
           Bucket: bucketName,
           Key: key,
         });
-        return await getSignedUrl(this.client, command, {
+        return getSignedUrl(this.client, command, {
           expiresIn: 30,
         });
       };
@@ -78,14 +78,15 @@ export const useS3ObjectStore = defineStore({
     },
     async fetchS3Objects(
       bucketName: string,
+      prefix?: string,
       onFinally?: () => void,
     ): Promise<S3Object[]> {
-      if ((this.objectMapping[bucketName] ?? []).length > 0) {
+      if (this.objectMapping[bucketName] != undefined) {
         onFinally?.();
       }
       const pag = paginateListObjectsV2(
         { client: this.client },
-        { Bucket: bucketName },
+        { Bucket: bucketName, Prefix: prefix },
       );
       const objs: S3Object[] = [];
       try {
diff --git a/src/stores/users.ts b/src/stores/users.ts
index 9b21599..76e987a 100644
--- a/src/stores/users.ts
+++ b/src/stores/users.ts
@@ -8,7 +8,7 @@ import { useWorkflowExecutionStore } from "@/stores/workflowExecutions";
 import { useBucketStore } from "@/stores/buckets";
 import { useWorkflowStore } from "@/stores/workflows";
 import { useS3KeyStore } from "@/stores/s3keys";
-import {useS3ObjectStore} from "@/stores/s3objects";
+import { useS3ObjectStore } from "@/stores/s3objects";
 
 type DecodedToken = {
   exp: number;
diff --git a/src/views/object-storage/BucketView.vue b/src/views/object-storage/BucketView.vue
index 3bea32b..5966934 100644
--- a/src/views/object-storage/BucketView.vue
+++ b/src/views/object-storage/BucketView.vue
@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { onMounted, reactive, watch, computed } from "vue";
-import type { BucketPermissionOut } from "@/client/s3proxy";
 import type {
   FolderTree,
   S3PseudoFolder,
@@ -34,14 +33,14 @@ const s3KeyRepository = useS3KeyStore();
 const props = defineProps<{
   bucketName: string;
   subFolders: string[] | string;
-  permission?: BucketPermissionOut;
 }>();
+
 const randomIDSuffix = Math.random().toString(16).substring(2, 8);
 let successToast: Toast | null = null;
+let refreshTimeout: NodeJS.Timeout | undefined = undefined;
 
 // Reactive State
 // -----------------------------------------------------------------------------
-
 const deleteObjectsState = reactive<{
   deletedItem: string;
   potentialObjectToDelete: string;
@@ -57,7 +56,6 @@ const objectState = reactive<{
   filterString: string;
   bucketNotFoundError: boolean;
   bucketPermissionError: boolean;
-  createdPermission: undefined | BucketPermissionOut;
   editObjectKey: string;
   copyObject: S3Object;
   viewDetailObject: S3Object;
@@ -66,7 +64,6 @@ const objectState = reactive<{
   filterString: "",
   bucketNotFoundError: false,
   bucketPermissionError: false,
-  createdPermission: undefined,
   editObjectKey: "",
   copyObject: {
     Key: "",
@@ -213,9 +210,18 @@ const subFolderInUrl = computed<boolean>(
 const errorLoadingObjects = computed<boolean>(
   () => objectState.bucketPermissionError || objectState.bucketNotFoundError,
 );
-const writableBucket = computed<boolean>(() =>
-  bucketRepository.writableBucket(props.bucketName),
-);
+const writableBucket = computed<boolean>(() => {
+  // Allow only upload in bucket folder with respect to permission prefix
+  let prefixWritable = true;
+  if (
+    bucketRepository.ownPermissions[props.bucketName]?.file_prefix != undefined
+  ) {
+    prefixWritable =
+      bucketRepository.ownPermissions[props.bucketName]?.file_prefix ===
+      currentSubFolders.value.join("/") + "/";
+  }
+  return bucketRepository.writableBucket(props.bucketName) && prefixWritable;
+});
 const readableBucket = computed<boolean>(() =>
   bucketRepository.readableBucket(props.bucketName),
 );
@@ -227,7 +233,9 @@ watch(
   (newBucketName, oldBucketName) => {
     if (oldBucketName !== newBucketName) {
       // If bucket is changed, update the objects
-      updateObjects(newBucketName);
+      objectState.bucketPermissionError = false;
+      objectState.bucketNotFoundError = false;
+      fetchObjects();
       objectState.filterString = "";
     }
   },
@@ -251,9 +259,17 @@ watch(
 // Lifecycle Hooks
 // -----------------------------------------------------------------------------
 onMounted(() => {
-  s3KeyRepository.fetchS3Keys(authStore.currentUID).then(() => {
-    updateObjects(props.bucketName);
-  });
+  let counter = 0;
+  const onFinally = () => {
+    counter++;
+    if (counter > 1) {
+      fetchObjects();
+    }
+  };
+  // wait till s3keys and ownPermissions are available before fetching objects
+  s3KeyRepository.fetchS3Keys(authStore.currentUID, onFinally);
+  bucketRepository.fetchOwnPermissions(onFinally);
+
   document
     .querySelectorAll(".tooltip-container")
     .forEach(
@@ -303,25 +319,33 @@ function calculateFolderLastModified(folder: FolderTree): string {
 }
 
 /**
- * Load the meta information about objects from a bucket
- * @param bucketName Name of a bucket
+ * Fetch object from bucket with loading animation
  */
-async function updateObjects(bucketName: string) {
+function fetchObjects() {
   objectState.loading = true;
-  objectRepository.fetchS3Objects(bucketName, () => {
-    objectState.loading = false;
-  });
-  /*
-
-  } catch {
-    objectState.bucketNotFoundError = true;
-
-    if (error.status === 404) {
-        objectState.bucketNotFoundError = true;
-      } else if (error.status == 403) {
+  const prefix: string | undefined =
+    bucketRepository.ownPermissions[props.bucketName]?.file_prefix ?? undefined;
+  objectRepository
+    .fetchS3Objects(props.bucketName, prefix, () => {
+      objectState.loading = false;
+    })
+    .catch((error) => {
+      if (error.Code == "AccessDenied") {
         objectState.bucketPermissionError = true;
+      } else {
+        objectState.bucketNotFoundError = true;
       }
-  */
+    });
+}
+
+/**
+ * Fetch the meta information about objects from a bucket
+ */
+function refreshObjects() {
+  clearTimeout(refreshTimeout);
+  refreshTimeout = setTimeout(() => {
+    fetchObjects();
+  }, 500);
 }
 
 function isS3Object(
@@ -465,7 +489,7 @@ function getObjectFileName(key: string): string {
   <!-- Inputs on top -->
   <!-- Search bucket text input -->
   <div class="row">
-    <div class="col-8">
+    <div class="col-5 me-auto">
       <div class="input-group mt-2 rounded shadow-sm">
         <span class="input-group-text" id="objects-search-wrapping"
           ><font-awesome-icon icon="fa-solid fa-magnifying-glass"
@@ -483,6 +507,17 @@ function getObjectFileName(key: string): string {
     </div>
     <!-- Upload object button -->
     <div id="BucketViewButtons" class="col-auto">
+      <button
+        type="button"
+        class="btn btn-light me-4 tooltip-container border shadow-sm"
+        :disabled="errorLoadingObjects"
+        data-bs-toggle="tooltip"
+        data-bs-title="Refresh Objects"
+        @click="refreshObjects"
+      >
+        <font-awesome-icon icon="fa-solid fa-arrow-rotate-right" />
+        <span class="visually-hidden">Refresh Objects</span>
+      </button>
       <button
         type="button"
         class="btn btn-light me-2 tooltip-container border shadow-sm"
@@ -503,7 +538,7 @@ function getObjectFileName(key: string): string {
       <!-- Add folder button -->
       <button
         type="button"
-        class="btn btn-light m-2 tooltip-container border shadow-sm"
+        class="btn btn-light me-4 tooltip-container border shadow-sm"
         :disabled="errorLoadingObjects || !writableBucket"
         data-bs-toggle="modal"
         data-bs-title="Create Folder"
@@ -523,7 +558,7 @@ function getObjectFileName(key: string): string {
         v-if="!authStore.foreignUser"
         :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)"
         type="button"
-        class="btn btn-light m-2 tooltip-container border shadow-sm"
+        class="btn btn-light me-2 tooltip-container border shadow-sm"
         :disabled="errorLoadingObjects"
         data-bs-toggle="modal"
         data-bs-title="Create Bucket Permission"
@@ -547,7 +582,7 @@ function getObjectFileName(key: string): string {
         v-if="!authStore.foreignUser"
         :hidden="!bucketRepository.permissionFeatureAllowed(props.bucketName)"
         type="button"
-        class="btn btn-light m-2 tooltip-container border shadow-sm"
+        class="btn btn-light tooltip-container border shadow-sm"
         :disabled="errorLoadingObjects"
         data-bs-title="List Bucket Permission"
         data-bs-toggle="modal"
@@ -558,6 +593,7 @@ function getObjectFileName(key: string): string {
       </button>
       <permission-list-modal
         v-if="
+          objectState.loading == false &&
           bucketRepository.ownPermissions[props.bucketName] == undefined &&
           !authStore.foreignUser
         "
-- 
GitLab