Skip to content

Commit

Permalink
Check perms on route entry and nav to 403 page if necessary
Browse files Browse the repository at this point in the history
  • Loading branch information
kamorel committed Mar 13, 2023
1 parent 7b05144 commit aa557af
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 27 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/object/ObjectFileDetails.vue
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ watch( [props, getObjects], () => {
>
<ShareObjectButton
v-if="permissionStore.isObjectActionAllowed(
props.objId, getUserId, Permissions.MANAGE, bucketId)"
props.objId, getUserId, Permissions.READ, bucketId)"
:id="props.objId"
/>
<DownloadObjectButton
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/object/ObjectTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ watch( selectedObjects, () => {
<template #body="{ data }">
<ShareObjectButton
v-if="permissionStore.isObjectActionAllowed(
data.id, getUserId, Permissions.MANAGE, props.bucketId as string)"
data.id, getUserId, Permissions.READ, props.bucketId as string)"
:id="data.id"
/>
<DownloadObjectButton
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/object/share/ShareObjectButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const displayShareDialog = ref(false);
// Share link
const bcBoxLink = computed(() => {
return `${window.location.origin}/list/detail/object?objId=${props.id}`;
return `${window.location.origin}/detail/objects?objId=${props.id}`;
});
const comsUrl = computed(() => {
return `${getConfig.value.coms?.apiPath}/object/${props.id}`;
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ const routes: Array<RouteRecordRaw> = [
path: 'objects',
name: RouteNames.DETAIL_OBJECTS,
component: () => import('@/views/detail/DetailObjectsView.vue'),
meta: { requiresAuth: true },
props: createProps
}
]
Expand Down Expand Up @@ -107,6 +106,11 @@ const routes: Array<RouteRecordRaw> = [
// path: '/update',
// children: [{}],
// },
{
path: '/forbidden',
name: RouteNames.FORBIDDEN,
component: () => import('@/views/Forbidden.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/services/objectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ export default {
});
},

/**
* @function headObject
* Get an object details (head call)
* @param objectId The id for the object to get
* @returns {Promise} An axios response
*/
headObject(objectId: string) {
return comsAxios().head(`${PATH}/${objectId}`);
},

/**
* @function searchObjects
* List and search for all objects
Expand All @@ -81,16 +91,6 @@ export default {
return comsAxios().get(PATH, { params: params });
},

/**
* @function readObject
* Get an object details (head call)
* @param objectId The id for the object to get
* @returns {Promise} An axios response
*/
readObject(objectId: string) {
return comsAxios().head(`${PATH}/${objectId}`);
},

/**
* @function togglePublic
* Toggles the public property for an object
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/store/objectStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const useObjectStore = defineStore('object', () => {

// Get a unique list of object IDs the user has access to
const permResponse = await permissionStore.fetchObjectPermissions(params);

if (permResponse) {
const uniqueIds: string[] = [...new Set<string>(permResponse.map((x: { objectId: string }) => x.objectId))];

Expand Down Expand Up @@ -127,6 +128,21 @@ export const useObjectStore = defineStore('object', () => {
return state.objects.value.find((x) => x.id === objectId);
}

async function headObject(objectId: string) {
try {
appStore.beginIndeterminateLoading();

// Return full response as data will always be No Content
return (await objectService.headObject(objectId));
}
catch (error) {
toast.add({ severity: 'error', summary: 'Error fetching head', detail: error, life: 3000 });
}
finally {
appStore.endIndeterminateLoading();
}
}

function setSelectedObjects(selected: Array<COMSObject>) {
state.selectedObjects.value = selected;
}
Expand Down Expand Up @@ -157,6 +173,7 @@ export const useObjectStore = defineStore('object', () => {
downloadObject,
fetchObjects,
findObjectById,
headObject,
setSelectedObjects,
togglePublic
};
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const RouteNames = Object.freeze({
CREATE_BUCKET: 'createBucket',
DETAIL_OBJECTS: 'detailObjects',
DEVELOPER: 'developer',
FORBIDDEN: 'forbidden',
HOME: 'home',
LIST_BUCKETS: 'listBuckets',
LIST_OBJECTS: 'listObjects',
Expand Down
17 changes: 17 additions & 0 deletions frontend/src/views/Forbidden.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts" />

<template>
<h3>
Forbidden.
</h3>
</template>

<style lang="scss" scoped>
h1 {
font-weight: bold;
}
h3 {
font-weight: bold;
}
</style>
72 changes: 66 additions & 6 deletions frontend/src/views/detail/DetailObjectsView.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,81 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { onBeforeMount, ref } from 'vue';
import { useRouter } from 'vue-router';
import ObjectFileDetails from '@/components/object/ObjectFileDetails.vue';
import { useAuthStore, useObjectStore, usePermissionStore } from '@/store';
import { Permissions, RouteNames } from '@/utils/constants';
import type { Ref } from 'vue';
// Props
type Props = {
objId: string
};
const props = withDefaults(defineProps<Props>(), {});
// Store
const objectStore = useObjectStore();
const permissionStore = usePermissionStore();
const { getIsAuthenticated, getUserId } = storeToRefs(useAuthStore());
// State
const ready: Ref<boolean> = ref(false);
const isPublic: Ref<boolean> = ref(false);
// Actions
onBeforeMount( async () => {
const router = useRouter();
if( props.objId ) {
const head = await objectStore.headObject(props.objId);
isPublic.value = head?.status === 204;
if( isPublic.value && !getIsAuthenticated.value ) {
ready.value = true;
}
else {
if( !getIsAuthenticated.value ) {
router.replace({ name: RouteNames.LOGIN });
}
else {
await permissionStore.fetchBucketPermissions({userId: getUserId.value});
await objectStore.fetchObjects({objId: props.objId, userId: getUserId.value});
const obj = objectStore.findObjectById(props.objId);
const bucketId = obj?.bucketId;
if( !isPublic.value &&
( !obj || !permissionStore.isObjectActionAllowed(obj.id, getUserId.value, Permissions.READ, bucketId) ) ) {
router.replace({ name: RouteNames.FORBIDDEN });
}
else {
ready.value = true;
}
}
}
}
else {
router.replace({ name: RouteNames.FORBIDDEN });
}
});
</script>

<template>
<ObjectFileDetails
v-if="objId"
:obj-id="props.objId"
/>
<div v-else>
<h3>No object provided</h3>
<div v-if="ready">
<div v-if="getIsAuthenticated">
<ObjectFileDetails
v-if="objId"
:obj-id="props.objId"
/>
<div v-else>
<h3>No object provided</h3>
</div>
</div>
<div v-else-if="isPublic && !getIsAuthenticated">
<h3>Public file. COMS API does not currently allow unauthenticated users to fetch data.</h3>
</div>
</div>
</template>

Expand Down
34 changes: 27 additions & 7 deletions frontend/src/views/list/ListObjectsView.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script setup lang="ts">
import { useToast } from 'primevue/usetoast';
import { onErrorCaptured, onMounted, ref } from 'vue';
import { storeToRefs } from 'pinia';
import { onBeforeMount, onErrorCaptured, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import ObjectList from '@/components/object/ObjectList.vue';
import { useBucketStore } from '@/store';
import { ObjectList } from '@/components/object';
import { useToast } from '@/lib/primevue';
import { useAuthStore, useBucketStore, usePermissionStore } from '@/store';
import { RouteNames } from '@/utils/constants';
import type { Ref } from 'vue';
import type { Bucket } from '@/types';
import type { Bucket, BucketPermission } from '@/types';
// Props
type Props = {
Expand All @@ -20,8 +22,12 @@ const props = withDefaults(defineProps<Props>(), {
// Store
const bucketStore = useBucketStore();
const permissionStore = usePermissionStore();
const { getBucketPermissions } = storeToRefs(permissionStore);
const { getUserId } = storeToRefs(useAuthStore());
// State
const ready: Ref<boolean> = ref(false);
const bucket: Ref< Bucket | undefined > = ref(undefined);
// Actions
Expand All @@ -34,13 +40,27 @@ onErrorCaptured((e: Error) => {
toast.add({ severity: 'error', summary: 'Unable to load bucket information.', detail: e.message, life: 5000 });
});
onBeforeMount( async () => {
const router = useRouter();
await permissionStore.fetchBucketPermissions({ userId: getUserId.value, objectPerms: true });
if( !getBucketPermissions.value.some( (x: BucketPermission) =>
x.bucketId === props.bucketId && x.userId === getUserId.value ) ) {
router.replace({ name: RouteNames.FORBIDDEN });
}
else {
ready.value = true;
}
});
onMounted(async () => {
await getBucketName();
getBucketName();
});
</script>

<template>
<div>
<div v-if="ready">
<h1>
Files
</h1>
Expand Down

0 comments on commit aa557af

Please sign in to comment.