From eb3751e34e1e10ff092e09955355d47810862171 Mon Sep 17 00:00:00 2001 From: Csaky Date: Thu, 5 Mar 2026 17:07:05 -0800 Subject: [PATCH 1/6] Handle access based on IDP permissions --- .../components/bucket/BucketPermission.vue | 2 +- .../bucket/BucketPermissionAddUser.vue | 4 +- .../components/object/ObjectFileDetails.vue | 20 +- frontend/src/components/object/ObjectList.vue | 4 +- .../src/components/object/ObjectTable.vue | 4 + frontend/src/components/object/index.ts | 1 + frontend/src/services/permissionService.ts | 95 +++++++ frontend/src/store/bucketStore.ts | 20 +- frontend/src/store/objectStore.ts | 14 +- frontend/src/store/permissionStore.ts | 242 +++++++++++++++++- frontend/src/types/BucketIdpPermission.ts | 8 + frontend/src/types/COMSObjectIdpPermission.ts | 8 + frontend/src/types/IdpPermissions.ts | 8 + frontend/src/types/UserPermissions.ts | 2 +- frontend/src/types/index.ts | 3 + .../options/BucketSearchPermissionsOptions.ts | 1 + .../options/ObjectSearchPermissionsOptions.ts | 9 +- frontend/src/views/list/ListObjectsView.vue | 5 +- 18 files changed, 411 insertions(+), 39 deletions(-) create mode 100644 frontend/src/types/BucketIdpPermission.ts create mode 100644 frontend/src/types/COMSObjectIdpPermission.ts create mode 100644 frontend/src/types/IdpPermissions.ts diff --git a/frontend/src/components/bucket/BucketPermission.vue b/frontend/src/components/bucket/BucketPermission.vue index 9946ff72..f7dc2036 100644 --- a/frontend/src/components/bucket/BucketPermission.vue +++ b/frontend/src/components/bucket/BucketPermission.vue @@ -156,7 +156,7 @@ onBeforeMount(async () => { aria-labelledby="upload_checkbox" /> { const configuredIdp = getConfig.value.idpList.find((idp: IdentityProvider) => idp.idp === selectedUser.idp); - const idpName = configuredIdp?.name || 'BCSC'; + const idp = configuredIdp?.name || 'BCSC'; const idpElevated = configuredIdp?.elevatedRights || false; permissionStore.addBucketUser({ userId: selectedUser.userId, - idpName: idpName, + idp: idp, elevatedRights: idpElevated, fullName: selectedUser.fullName, create: false, diff --git a/frontend/src/components/object/ObjectFileDetails.vue b/frontend/src/components/object/ObjectFileDetails.vue index 50aa728f..294ae9dd 100644 --- a/frontend/src/components/object/ObjectFileDetails.vue +++ b/frontend/src/components/object/ObjectFileDetails.vue @@ -50,7 +50,7 @@ const permissionStore = usePermissionStore(); const tagStore = useTagStore(); const versionStore = useVersionStore(); -const { getUserId } = storeToRefs(useAuthStore()); +const { getProfile, getUserId } = storeToRefs(useAuthStore()); const { getObject } = storeToRefs(objectStore); const { getIsDeleted, @@ -118,21 +118,27 @@ async function onVersionCreated() { onMounted(async () => { const head = await objectStore.headObject(props.objectId); - - // TODO: sync object? - await objectStore.fetchObjects({ objectId: props.objectId, userId: getUserId.value, bucketPerms: true }); + // fetch object and object permissions + await objectStore.fetchObjects({ + objectId: props.objectId, + userId: getUserId.value, + idp: (getProfile.value as any)?.identity_provider, + bucketPerms: true + }); object.value = getObject.value(props.objectId); bucketId.value = object.value ? object.value.bucketId : ''; - - await permissionStore.fetchBucketPermissions({ - userId: getUserId.value, + // fetch bucket and bucket permissions + await bucketStore.fetchBuckets({ bucketId: bucketId.value, + userId: getUserId.value, + idp: (getProfile.value as any)?.identity_provider, objectPerms: true }); if ( head?.status !== 204 && !isDeleted.value && (!object.value || + // check for permissions in store !permissionStore.isObjectActionAllowed(object.value.id, getUserId.value, Permissions.READ, object.value.bucketId)) ) { router.replace({ name: RouteNames.FORBIDDEN }); diff --git a/frontend/src/components/object/ObjectList.vue b/frontend/src/components/object/ObjectList.vue index d9894f7b..9bb9e526 100644 --- a/frontend/src/components/object/ObjectList.vue +++ b/frontend/src/components/object/ObjectList.vue @@ -90,7 +90,7 @@ const autoSync = async () => { const lastSyncDate = new Date(bucket?.lastSyncRequestedDate as string); const sinceLastSyncDate = differenceInSeconds(now, lastSyncDate); - // if havent synced for 24 hrs trigger autoSync + // if havent synced for 24 hrs trigger autoSync of JUST this folder level (not recursive) if (sinceLastSyncDate > autoMinimum) { await bucketStore.syncBucket(props.bucketId, false); syncStatus.value = 'Sync in progress'; @@ -180,7 +180,7 @@ onBeforeMount(async () => { " > { permissionStore.fetchObjectPermissions({ objectId: objects.map((o: COMSObject) => o.id) }); + + permissionStore.fetchObjectIdpPermissions({ + objectId: objects.map((o: COMSObject) => o.id) + }); } }); }; diff --git a/frontend/src/components/object/index.ts b/frontend/src/components/object/index.ts index a7a9b166..8077fe58 100644 --- a/frontend/src/components/object/index.ts +++ b/frontend/src/components/object/index.ts @@ -5,6 +5,7 @@ export { default as DownloadObjectButton } from './DownloadObjectButton.vue'; export { default as ObjectAccess } from './ObjectAccess.vue'; export { default as ObjectFileDetails } from './ObjectFileDetails.vue'; export { default as ObjectFilters } from './ObjectFilters.vue'; +export { default as ObjectIdpPermission } from './ObjectIdpPermission.vue'; export { default as ObjectList } from './ObjectList.vue'; export { default as ObjectMetadata } from './ObjectMetadata.vue'; export { default as ObjectMetadataTagForm } from './ObjectMetadataTagForm.vue'; diff --git a/frontend/src/services/permissionService.ts b/frontend/src/services/permissionService.ts index 9f64d07d..43fad850 100644 --- a/frontend/src/services/permissionService.ts +++ b/frontend/src/services/permissionService.ts @@ -14,8 +14,10 @@ import type { const PATH = 'permission'; const BUCKET = 'bucket'; const OBJECT = 'object'; +const IDP = 'idp'; export default { + // ------ user permissions ------ /** * @function bucketAddPermissions * Adds the given permissions to the bucket @@ -104,5 +106,98 @@ export default { */ objectGetPermissions(objectId: string, params?: ObjectGetPermissionsOptions) { return comsAxios().get(`${PATH}/${OBJECT}/${objectId}`, { params: params }); + }, + + // ------ IDP permissions ------ + + /** + * function bucketAddIdpPermissions + * Adds the given permissions to the bucket for the specified IDP + * @param bucketId + * @param data + * @param params + * @returns + */ + bucketAddIdpPermissions(bucketId: string, data: any, params = { recursive: false }) { + return comsAxios().put(`${PATH}/${IDP}/${BUCKET}/${bucketId}`, data, { + params: params + }); + }, + + /** + * function bucketDeleteIdpPermission + * Deletes the given permission from the bucket for the specified IDP + * @param bucketId + * @param params + * @returns + */ + bucketDeleteIdpPermission(bucketId: string, params: any) { + return comsAxios().delete(`${PATH}/${IDP}/${BUCKET}/${bucketId}`, { + params: params + }); + }, + + /** + * @function bucketSearchIdpPermissions + * Returns a list of buckets and their IDP permissions + * @param {BucketSearchPermissionsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + bucketSearchIdpPermissions(params?: any) { + return comsAxios().get(`${PATH}/${IDP}/${BUCKET}`, { params: params }); + }, + + /** + * function bucketGetIdpPermissions + * Returns a list of IDP permissions for the bucket + * @param bucketId + * @param params + * @returns + */ + bucketGetIdpPermissions(bucketId: string, params?: any) { + return comsAxios().get(`${PATH}/${IDP}/${BUCKET}/${bucketId}`, { params: params }); + }, + + /** + * function objectAddIdpPermissions + * Adds the given permissions to the object for the specified IDP + * @param objectId + * @param data + * @returns + */ + objectAddIdpPermissions(objectId: string, data: Object) { + return comsAxios().put(`${PATH}/${IDP}/${OBJECT}/${objectId}`, data); + }, + + /** + * @function objectDeleteIdpPermission + * Deletes the given IDP permission from the object + * @param {string} objectId ID of the object to remove permissions from + * @param {ObjectDeletePermissionsOptions} params Object containing the permission to remove + * @returns {Promise} An axios response + */ + objectDeleteIdpPermission(objectId: string, params: Object) { + return comsAxios().delete(`${PATH}/${IDP}/${OBJECT}/${objectId}`, { params: params }); + }, + + /** + * @function objectSearchIdpPermissions + * Returns a list of IDP object permissions + * @param {ObjectSearchPermissionsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + objectSearchIdpPermissions(params?: any) { + return comsAxios().get(`${PATH}/${IDP}/${OBJECT}`, { params: params }); + }, + + /** + * @function objectGetIdpPermissions + * Returns a list of IDP permissions for the object + * @param {string} objectId ID of the object + * @param {ObjectGetPermissionsOptions} params Optional object containing the data to filter against + * @returns {Promise} An axios response + */ + objectGetIdpPermissions(objectId: string, params?: Object) { + return comsAxios().get(`${PATH}/${IDP}/${OBJECT}/${objectId}`, { params: params }); } }; diff --git a/frontend/src/store/bucketStore.ts b/frontend/src/store/bucketStore.ts index f8077948..539f6fd1 100644 --- a/frontend/src/store/bucketStore.ts +++ b/frontend/src/store/bucketStore.ts @@ -95,8 +95,8 @@ export const useBucketStore = defineStore('bucket', () => { /** * function does the following in order: * - fetches bucket permissions - * (fetchBucketPermissions() also add bucket permissions to the permission store) - * - pass bucketId's of buckets with a permission to searchBuckets() + * - adds bucket permissions to the permission store + * - pass bucketId's (from list of permissions) to searchBuckets() * - add buckets to store (skipping existing matches) * @param params search parameters * @returns an array of matching buckets found @@ -106,11 +106,21 @@ export const useBucketStore = defineStore('bucket', () => { appStore.beginIndeterminateLoading(); // Get a unique list of bucket IDs the user has access to - const permResponse = await permissionStore.fetchBucketPermissions(params); + // based on user permissions.. + const permResponse = await permissionStore.fetchBucketPermissions({ ...params, idp: undefined }); + // and IDP permissions (if current user's idp is provided in params) + const IdpPermResponse = params?.idp + ? await permissionStore.fetchBucketIdpPermissions({ ...params, userId: undefined }) + : undefined; + // if permissions found - if (permResponse) { + if (permResponse || IdpPermResponse) { const uniqueIds: Array = [ - ...new Set(permResponse.map((x: { bucketId: string }) => x.bucketId)) + ...new Set( + permResponse + ?.map((x: { bucketId: string }) => x.bucketId) + .concat(IdpPermResponse?.map((x: { bucketId: string }) => x.bucketId) || []) + ) ]; let response = Array(); diff --git a/frontend/src/store/objectStore.ts b/frontend/src/store/objectStore.ts index 199475cc..d559e01c 100644 --- a/frontend/src/store/objectStore.ts +++ b/frontend/src/store/objectStore.ts @@ -144,13 +144,19 @@ export const useObjectStore = defineStore('object', () => { appStore.beginIndeterminateLoading(); // Get a unique list of object IDs the user has access to - const permResponse = await permissionStore.fetchObjectPermissions(params); - - if (permResponse) { + // based on user permissions.. + const permResponse = await permissionStore.fetchObjectPermissions({ ...params, idp: undefined }); + // and IDP permissions (if current user's idp is provided in params) + const IdpPermResponse = params?.idp + ? await permissionStore.fetchObjectIdpPermissions({ ...params, userId: undefined }) + : undefined; + // if permissions found + if (permResponse || IdpPermResponse) { const uniqueIds: Array = [ ...new Set( permResponse - .map((x: { objectId: string }) => x.objectId) + ?.map((x: { objectId: string }) => x.objectId) + .concat(IdpPermResponse?.map((x: { objectId: string }) => x.objectId) || []) // Resolve API returning all objects with bucketPerms=true even when requesting single objectId .filter((objectId: string) => !params.objectId || objectId === params.objectId) ) diff --git a/frontend/src/store/permissionStore.ts b/frontend/src/store/permissionStore.ts index 5bc0aaf8..0d0b3255 100644 --- a/frontend/src/store/permissionStore.ts +++ b/frontend/src/store/permissionStore.ts @@ -10,20 +10,30 @@ import { partition } from '@/utils/utils'; import type { Ref } from 'vue'; import type { BucketPermission, + BucketIdpPermission, BucketSearchPermissionsOptions, COMSObjectPermission, + COMSObjectIdpPermission, ObjectSearchPermissionsOptions, IdentityProvider, Permission, User, - UserPermissions + UserPermissions, + IdpPermissions } from '@/types'; export type PermissionStoreState = { + // user permissions bucketPermissions: Ref>; mappedBucketToUserPermissions: Ref>; mappedObjectToUserPermissions: Ref>; objectPermissions: Ref>; + // IDP permissions + bucketIdpPermissions: Ref; + objectIdpPermissions: Ref; + mappedBucketToIdpPermissions: Ref; + mappedObjectToIdpPermissions: Ref; + // general permissions permissions: Ref>; }; @@ -36,15 +46,23 @@ export const usePermissionStore = defineStore('permission', () => { // State const state: PermissionStoreState = { + // user permissions bucketPermissions: ref([]), mappedBucketToUserPermissions: ref([]), mappedObjectToUserPermissions: ref([]), objectPermissions: ref([]), + // IDP permissions + bucketIdpPermissions: ref([]), + objectIdpPermissions: ref([]), + mappedBucketToIdpPermissions: ref([]), + mappedObjectToIdpPermissions: ref([]), + // general permissions permissions: ref([]) }; // Getters const getters = { + // User permissions getBucketPermissions: computed(() => state.bucketPermissions.value), getMappedBucketToUserPermissions: computed(() => state.mappedBucketToUserPermissions.value), getMappedObjectToUserPermissions: computed(() => state.mappedObjectToUserPermissions.value), @@ -57,7 +75,22 @@ export const usePermissionStore = defineStore('permission', () => { }); return op.concat(bp); }), + + // IDP permissions + getMappedBucketToIdpPermissions: computed(() => state.mappedBucketToIdpPermissions.value), + getMappedObjectToIdpPermissions: computed(() => state.mappedObjectToIdpPermissions.value), + getMappedObjectAndBucketToIdpPermissions: computed(() => { + const op = state.mappedObjectToIdpPermissions.value.map((o: any) => { + return { ...o, resource: 'object' }; + }); + const bp = state.mappedBucketToIdpPermissions.value.map((b: any) => { + return { ...b, resource: 'bucket' }; + }); + return op.concat(bp); + }), getObjectPermissions: computed(() => state.objectPermissions.value), + + // General permissions getPermissions: computed(() => state.permissions.value) }; @@ -97,6 +130,33 @@ export const usePermissionStore = defineStore('permission', () => { } } + async function addBucketIdpPermission(bucketId: string, idp: string, permCode: string): Promise { + try { + appStore.beginIndeterminateLoading(); + // note: permission changes always cascade to subfolders for which current user has MANAGE permission + await permissionService.bucketAddIdpPermissions(bucketId, [{ idp, permCode }], { recursive: true }); + } catch (error: any) { + toast.error('Adding bucket IDP permission', error.response?.data.detail ?? error, { life: 0 }); + } finally { + await fetchBucketIdpPermissions({ bucketId: bucketId }); + await mapBucketToIdpPermissions(bucketId); + appStore.endIndeterminateLoading(); + } + } + + async function addObjectIdpPermission(objectId: string, idp: string, permCode: string): Promise { + try { + appStore.beginIndeterminateLoading(); + await permissionService.objectAddIdpPermissions(objectId, [{ idp, permCode }]); + } catch (error: any) { + toast.error('Adding object IDP permission', error.response?.data.detail ?? error, { life: 0 }); + } finally { + await fetchObjectIdpPermissions({ objectId: objectId }); + await mapObjectToIdpPermissions(objectId); + appStore.endIndeterminateLoading(); + } + } + function addObjectUser(user: UserPermissions): void { try { getters.getMappedObjectToUserPermissions.value.push(user); @@ -132,6 +192,33 @@ export const usePermissionStore = defineStore('permission', () => { } } + async function deleteBucketIdpPermission(bucketId: string, idp: string, permCode: string): Promise { + try { + appStore.beginIndeterminateLoading(); + // note: permission changes always cascade to subfolders for which current user has MANAGE permission + await permissionService.bucketDeleteIdpPermission(bucketId, { idp, permCode, recursive: true }); + } catch (error: any) { + toast.error('Deleting bucket IDP permission', error.response?.data.detail ?? error, { life: 0 }); + } finally { + await fetchBucketPermissions({ bucketId: bucketId }); + await mapBucketToIdpPermissions(bucketId); + appStore.endIndeterminateLoading(); + } + } + + async function deleteObjectIdpPermission(objectId: string, idp: string, permCode: string): Promise { + try { + appStore.beginIndeterminateLoading(); + await permissionService.objectDeleteIdpPermission(objectId, { idp, permCode }); + } catch (error: any) { + toast.error('Deleting object IDP permission', error.response?.data.detail ?? error, { life: 0 }); + } finally { + await fetchObjectPermissions({ objectId: objectId }); + await mapObjectToIdpPermissions(objectId); + appStore.endIndeterminateLoading(); + } + } + async function fetchBucketPermissions( params: BucketSearchPermissionsOptions = {} ): Promise | void> { @@ -186,21 +273,37 @@ export const usePermissionStore = defineStore('permission', () => { } function isBucketActionAllowed(bucketId: string, userId?: string, permCode?: string): boolean { - return state.bucketPermissions.value.some( + // check user permissions + const userPerm = state.bucketPermissions.value.some( (x: BucketPermission) => x.bucketId === bucketId && x.userId === userId && x.permCode === permCode ); + // check idp permissions + const idp = getProfile.value?.identity_provider; + const idpPerm = state.bucketIdpPermissions.value.some( + (x: BucketIdpPermission) => x.bucketId === bucketId && x.idp === idp && x.permCode === permCode + ); + + return userPerm || idpPerm; } function isObjectActionAllowed(objectId: string, userId?: string, permCode?: string, bucketId?: string): boolean { - const bucketPerm = state.bucketPermissions.value.some( + // check user permissions + const bucketUserPerm = state.bucketPermissions.value.some( (x: BucketPermission) => x.bucketId === bucketId && x.userId === userId && x.permCode === permCode ); - - const objectPerm = state.objectPermissions.value.some( + const objectUserPerm = state.objectPermissions.value.some( (x: COMSObjectPermission) => x.objectId === objectId && x.userId === userId && x.permCode === permCode ); + // check idp permissions + const idp = getProfile.value?.identity_provider; + const bucketIdpPerm = state.bucketIdpPermissions.value.some( + (x: BucketIdpPermission) => x.bucketId === bucketId && x.idp === idp && x.permCode === permCode + ); + const objectIdpPerm = state.objectIdpPermissions.value.some( + (x: COMSObjectIdpPermission) => x.objectId === objectId && x.idp === idp && x.permCode === permCode + ); - return bucketPerm || objectPerm; + return bucketUserPerm || objectUserPerm || bucketIdpPerm || objectIdpPerm; } function isUserElevatedRights(): boolean { @@ -226,12 +329,12 @@ export const usePermissionStore = defineStore('permission', () => { uniqueUsers.forEach((user: User) => { const configuredIdp = getConfig.value.idpList.find((idp: IdentityProvider) => idp.idp === user.idp); // If IDP is not specified in our IDP list config, assume it's BC Services Card - const idpName = configuredIdp?.name || 'BCSC'; + const idp = configuredIdp?.name || 'BCSC'; const idpElevated = configuredIdp?.elevatedRights || false; userPermissions.push({ userId: user.userId, - idpName: idpName, + idp: idp, elevatedRights: idpElevated, fullName: user.fullName, create: hasPermission(user.userId, Permissions.CREATE), @@ -265,12 +368,12 @@ export const usePermissionStore = defineStore('permission', () => { uniqueUsers.forEach((user: User) => { const configuredIdp = getConfig.value.idpList.find((idp: IdentityProvider) => idp.idp === user.idp); // If IDP is not specified in our IDP list config, assume it's BC Services Card - const idpName = configuredIdp?.name || 'BCSC'; + const idp = configuredIdp?.name || 'BCSC'; const idpElevated = configuredIdp?.elevatedRights || false; userPermissions.push({ userId: user.userId, - idpName: idpName, + idp: idp, elevatedRights: idpElevated, fullName: user.fullName, create: hasPermission(user.userId, Permissions.CREATE), @@ -289,6 +392,115 @@ export const usePermissionStore = defineStore('permission', () => { } } + async function fetchBucketIdpPermissions(params: any = {}): Promise | void> { + try { + appStore.beginIndeterminateLoading(); + + const response = (await permissionService.bucketSearchIdpPermissions(params)).data; + const newPerms: Array = response.flatMap((x: any) => x.permissions); + + // patch bucketPermissions state with latest perms from COMS + const matches = (x: any) => + (!params.bucketId || x.bucketId === params.bucketId) && + (!params.idp || x.idp === params.idp) && + (!params.permCode || x.permCode === params.permCode); + const [, difference] = partition(state.bucketIdpPermissions.value, matches); + state.bucketIdpPermissions.value = difference.concat(newPerms); + + // Pass response back so bucketStore can handle bucketPerms=true correctly + return response; + } catch (error: any) { + toast.error('Fetching bucket IDP permissions', error.response?.data.detail ?? error, { life: 0 }); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function fetchObjectIdpPermissions(params: any = {}): Promise | undefined> { + try { + appStore.beginIndeterminateLoading(); + + const response = (await permissionService.objectSearchIdpPermissions(params)).data; + + const newPerms: Array = response.flatMap((x: any) => x.permissions); + + // patch objectPermissions state with latest perms from COMS + const matches = (x: any) => + (!params.objectId || x.objectId === params.objectId) && + (!params.idp || x.idp === params.idp) && + (!params.permCode || x.permCode === params.permCode); + const [, difference] = partition(state.objectIdpPermissions.value, matches); + state.objectIdpPermissions.value = difference.concat(newPerms); + + // Pass response back so objectStore can handle bucketPerms=true correctly + return response; + } catch (error: any) { + toast.error('Fetching object permissions', error.response?.data.detail ?? error, { life: 0 }); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function mapBucketToIdpPermissions(bucketId: string): Promise { + try { + appStore.beginIndeterminateLoading(); + const bucketPerms = state.bucketIdpPermissions.value.filter((x: any) => x.bucketId === bucketId); + const uniqueIdpNames = [...new Set(bucketPerms.map((x: any) => x.idp))]; + + const hasPermission = (idp: string, permission: string) => { + return bucketPerms.some((perm: any) => perm.idp === idp && perm.permCode === permission); + }; + + const IdpPermissions: IdpPermissions[] = []; + uniqueIdpNames.forEach((idp: any) => { + IdpPermissions.push({ + idp: idp, + create: hasPermission(idp, Permissions.CREATE), + read: hasPermission(idp, Permissions.READ), + update: hasPermission(idp, Permissions.UPDATE), + delete: hasPermission(idp, Permissions.DELETE), + manage: hasPermission(idp, Permissions.MANAGE) + }); + }); + + state.mappedBucketToIdpPermissions.value = IdpPermissions; + } catch (error: any) { + toast.error('Mapping bucket permissions to IDP permissions', error.response?.data.detail ?? error, { life: 0 }); + } finally { + appStore.endIndeterminateLoading(); + } + } + + async function mapObjectToIdpPermissions(objectId: string): Promise { + try { + appStore.beginIndeterminateLoading(); + const objectPerms = state.objectIdpPermissions.value.filter((x: any) => x.objectId === objectId); + const uniqueIdpNames = [...new Set(objectPerms.map((x: any) => x.idp))]; + + const hasPermission = (idp: string, permission: string) => { + return objectPerms.some((perm: any) => perm.idp === idp && perm.permCode === permission); + }; + + const idpPermissions: IdpPermissions[] = []; + uniqueIdpNames.forEach((idp: any) => { + idpPermissions.push({ + idp: idp, + create: hasPermission(idp, Permissions.CREATE), + read: hasPermission(idp, Permissions.READ), + update: hasPermission(idp, Permissions.UPDATE), + delete: hasPermission(idp, Permissions.DELETE), + manage: hasPermission(idp, Permissions.MANAGE) + }); + }); + + state.mappedObjectToIdpPermissions.value = idpPermissions; + } catch (error: any) { + toast.error('Mapping object permissions to IDP permissions', error.response?.data.detail ?? error, { life: 0 }); + } finally { + appStore.endIndeterminateLoading(); + } + } + async function removeBucketUser(bucketId: string, userId: string): Promise { try { appStore.beginIndeterminateLoading(); @@ -358,7 +570,15 @@ export const usePermissionStore = defineStore('permission', () => { mapBucketToUserPermissions, mapObjectToUserPermissions, removeBucketUser, - removeObjectUser + removeObjectUser, + addBucketIdpPermission, + addObjectIdpPermission, + fetchObjectIdpPermissions, + fetchBucketIdpPermissions, + deleteBucketIdpPermission, + deleteObjectIdpPermission, + mapBucketToIdpPermissions, + mapObjectToIdpPermissions }; }); diff --git a/frontend/src/types/BucketIdpPermission.ts b/frontend/src/types/BucketIdpPermission.ts new file mode 100644 index 00000000..2e2cd2a3 --- /dev/null +++ b/frontend/src/types/BucketIdpPermission.ts @@ -0,0 +1,8 @@ +import type { IAudit } from '@/interfaces'; + +export type BucketIdpPermission = { + bucketId: string; + id: string; + permCode: string; + idp: string; +} & IAudit; diff --git a/frontend/src/types/COMSObjectIdpPermission.ts b/frontend/src/types/COMSObjectIdpPermission.ts new file mode 100644 index 00000000..863ad000 --- /dev/null +++ b/frontend/src/types/COMSObjectIdpPermission.ts @@ -0,0 +1,8 @@ +import type { IAudit } from '@/interfaces'; + +export type COMSObjectIdpPermission = { + id: string; + objectId: string; + permCode: string; + idp: string; +} & IAudit; diff --git a/frontend/src/types/IdpPermissions.ts b/frontend/src/types/IdpPermissions.ts new file mode 100644 index 00000000..515c5cd7 --- /dev/null +++ b/frontend/src/types/IdpPermissions.ts @@ -0,0 +1,8 @@ +export type IdpPermissions = { + idp: string; + create?: boolean; + read: boolean; + update: boolean; + delete: boolean; + manage: boolean; +}; diff --git a/frontend/src/types/UserPermissions.ts b/frontend/src/types/UserPermissions.ts index 2c983114..41e5e95a 100644 --- a/frontend/src/types/UserPermissions.ts +++ b/frontend/src/types/UserPermissions.ts @@ -1,6 +1,6 @@ export type UserPermissions = { userId: string; - idpName?: string; + idp?: string; elevatedRights?: boolean; fullName: string; create?: boolean; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 6bac653d..8d800dd2 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -3,7 +3,10 @@ export type { BucketPermission } from './BucketPermission'; export type { BucketTreeNode, BucketTreeNodeData } from './BucketTreeNode'; export type { COMSObject } from './COMSObject'; export type { COMSObjectPermission } from './COMSObjectPermission'; +export type { COMSObjectIdpPermission } from './COMSObjectIdpPermission'; export type { IdentityProvider } from './IdentityProvider'; +export type { IdpPermissions } from './IdpPermissions'; +export type { BucketIdpPermission } from './BucketIdpPermission'; export type { InviteCreateOptions } from './InviteCreateOptions'; export type { Metadata } from './Metadata'; export type { MetadataPair } from './MetadataPair'; diff --git a/frontend/src/types/options/BucketSearchPermissionsOptions.ts b/frontend/src/types/options/BucketSearchPermissionsOptions.ts index da96a983..653f2214 100644 --- a/frontend/src/types/options/BucketSearchPermissionsOptions.ts +++ b/frontend/src/types/options/BucketSearchPermissionsOptions.ts @@ -3,4 +3,5 @@ export type BucketSearchPermissionsOptions = { objectPerms?: boolean; permCode?: string; userId?: string; + idp?: string; }; diff --git a/frontend/src/types/options/ObjectSearchPermissionsOptions.ts b/frontend/src/types/options/ObjectSearchPermissionsOptions.ts index f04a793d..e0926dd4 100644 --- a/frontend/src/types/options/ObjectSearchPermissionsOptions.ts +++ b/frontend/src/types/options/ObjectSearchPermissionsOptions.ts @@ -4,8 +4,9 @@ export type ObjectSearchPermissionsOptions = { objectId?: string | Array; permCode?: string; userId?: string; - limit?: number, - sort?: string, - order?: string, - page?: number + idp?: string; + limit?: number; + sort?: string; + order?: string; + page?: number; }; diff --git a/frontend/src/views/list/ListObjectsView.vue b/frontend/src/views/list/ListObjectsView.vue index 2c29deb3..bf765b32 100644 --- a/frontend/src/views/list/ListObjectsView.vue +++ b/frontend/src/views/list/ListObjectsView.vue @@ -26,7 +26,7 @@ const props = withDefaults(defineProps(), { // Store const bucketStore = useBucketStore(); -const { getUserId, getIsAuthenticated } = storeToRefs(useAuthStore()); +const { getProfile, getUserId, getIsAuthenticated } = storeToRefs(useAuthStore()); const permissionStore = usePermissionStore(); const toast = useToast(); const { focusedElement } = storeToRefs(useNavStore()); @@ -97,13 +97,14 @@ onErrorCaptured((e: Error) => { onBeforeMount(async () => { const router = useRouter(); - // fetch bucket; populate bucket and permissions in store let bucketResponse: any = []; if (props?.bucketId) { + // if logged in, fetch bucket; populate bucket and permissions in store if (getIsAuthenticated.value) { bucketResponse = await bucketStore.fetchBuckets({ bucketId: props.bucketId, userId: getUserId.value, + idp: (getProfile.value as any)?.identity_provider, objectPerms: true }); } else { From 834692e2f210dd6406d92609ddc226d67c076871 Mon Sep 17 00:00:00 2001 From: Csaky Date: Fri, 6 Mar 2026 16:58:54 -0800 Subject: [PATCH 2/6] Controls for Internal Only sharing Toggle for folder/file 'internal only' setting 'internal' annotation added --- frontend/src/assets/main.scss | 15 ++ .../src/components/bucket/BucketIdpToggle.vue | 138 ++++++++++++++++ frontend/src/components/bucket/BucketList.vue | 10 +- .../components/bucket/BucketPermission.vue | 26 ++- .../src/components/bucket/BucketTable.vue | 20 +++ frontend/src/components/bucket/index.ts | 2 + .../components/object/ObjectFileDetails.vue | 13 ++ .../src/components/object/ObjectIdpToggle.vue | 125 +++++++++++++++ frontend/src/components/object/ObjectList.vue | 9 +- .../components/object/ObjectPermission.vue | 29 +++- .../src/components/object/ObjectTable.vue | 49 ++++-- frontend/src/components/object/index.ts | 2 +- frontend/src/store/bucketStore.ts | 2 +- frontend/src/store/permissionStore.ts | 151 ++++++------------ frontend/src/views/list/ListObjectsView.vue | 22 +++ 15 files changed, 477 insertions(+), 136 deletions(-) create mode 100644 frontend/src/components/bucket/BucketIdpToggle.vue create mode 100644 frontend/src/components/object/ObjectIdpToggle.vue diff --git a/frontend/src/assets/main.scss b/frontend/src/assets/main.scss index af54a33f..1dd2ac32 100644 --- a/frontend/src/assets/main.scss +++ b/frontend/src/assets/main.scss @@ -271,6 +271,21 @@ div:focus-visible { } } +.p-tag-info { + background-color: rgb(224, 242, 254); + outline-style: solid; + outline-color: $bcbox-primary; + outline-width: thin; + + .p-tag-value { + color: $bcbox-primary; + } + + .p-tag-icon { + color: $bcbox-primary + } +} + /* datatable */ .p-datatable, .p-treetable { diff --git a/frontend/src/components/bucket/BucketIdpToggle.vue b/frontend/src/components/bucket/BucketIdpToggle.vue new file mode 100644 index 00000000..bb1338d2 --- /dev/null +++ b/frontend/src/components/bucket/BucketIdpToggle.vue @@ -0,0 +1,138 @@ + + + diff --git a/frontend/src/components/bucket/BucketList.vue b/frontend/src/components/bucket/BucketList.vue index efe7ccca..e463cbd2 100644 --- a/frontend/src/components/bucket/BucketList.vue +++ b/frontend/src/components/bucket/BucketList.vue @@ -43,7 +43,11 @@ const closeBucketConfig = () => { }; onMounted(async () => { - await bucketStore.fetchBuckets({ userId: getUserId.value, objectPerms: true }); + await bucketStore.fetchBuckets({ + userId: getUserId.value, + idp: 'idir', + objectPerms: true + }); }); @@ -103,7 +107,7 @@ onMounted(async () => { {{ bucketConfigTitle }} - + { diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index b4e220be..46af5e5c 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -39,7 +39,7 @@ const { getIsAuthenticated, getProfile } = storeToRefs(useAuthStore()); :to="{ name: RouteNames.LIST_OBJECTS_DELETED }" aria-label="Recycle Bin" > - Recycle Bin + Recycle Bin
  • diff --git a/frontend/src/components/object/ObjectFileDetails.vue b/frontend/src/components/object/ObjectFileDetails.vue index 1b73557e..da64eb20 100644 --- a/frontend/src/components/object/ObjectFileDetails.vue +++ b/frontend/src/components/object/ObjectFileDetails.vue @@ -166,22 +166,15 @@ onMounted(async () => { diff --git a/frontend/src/components/object/ObjectTable.vue b/frontend/src/components/object/ObjectTable.vue index ca8ec202..26812877 100644 --- a/frontend/src/components/object/ObjectTable.vue +++ b/frontend/src/components/object/ObjectTable.vue @@ -3,16 +3,10 @@ import { storeToRefs } from 'pinia'; import { computed, onUnmounted, onMounted, ref } from 'vue'; import { Spinner } from '@/components/layout'; -import { - DeleteObjectButton, - DownloadObjectButton, - ObjectFilters, - ObjectPermission, - ObjectPublicToggle -} from '@/components/object'; +import { DeleteObjectButton, DownloadObjectButton, ObjectFilters, ObjectPermission } from '@/components/object'; import { ShareButton } from '@/components/common'; import { Button, Column, DataTable, Dialog, InputText, Tag } from '@/lib/primevue'; -import { useAuthStore, useObjectStore, useNavStore, usePermissionStore } from '@/store'; +import { useAuthStore, useBucketStore, useObjectStore, useNavStore, usePermissionStore } from '@/store'; import { Permissions, RouteNames } from '@/utils/constants'; import { onDialogHide } from '@/utils/utils'; import { ButtonMode } from '@/utils/enums'; @@ -35,16 +29,13 @@ type DataTableFilter = { }; // Props type Props = { - bucketId?: string; + bucketId: string; isBucketPublic?: boolean; - isBucketInternal?: boolean; objectInfoId?: string; }; const props = withDefaults(defineProps(), { - bucketId: undefined, isBucketPublic: undefined, - isBucketInternal: undefined, objectInfoId: undefined }); @@ -52,6 +43,7 @@ const props = withDefaults(defineProps(), { const emit = defineEmits(['show-object-info']); // Store +const bucketStore = useBucketStore(); const objectStore = useObjectStore(); const permissionStore = usePermissionStore(); const { getUserId, getIsAuthenticated } = storeToRefs(useAuthStore()); @@ -79,6 +71,8 @@ const isObjectInternal = computed(() => { return permissionStore.getObjectInternal(objId); }; }); +const bucket = bucketStore.getBucket(props.bucketId); +const isBucketInternal = permissionStore.getBucketInternal(props.bucketId); // Actions const formatShortUuid = (uuid: string) => uuid?.slice(0, 8) ?? uuid; @@ -290,7 +284,6 @@ async function downloadPublicObject(objectId: string) { field="name" sortable header="Name" - header-style="min-width: 25%" body-class="truncate" >