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