// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import { dialogActions } from "store/dialog/dialog-actions"; import { withDialog } from "store/dialog/with-dialog"; import { SHARING_DIALOG_NAME, SHARING_INVITATION_FORM_NAME, SharingManagementFormData, SharingInvitationFormData, getSharingMangementFormData, SharingPublicAccessFormData, VisibilityLevel, SHARING_PUBLIC_ACCESS_FORM_NAME, } from './sharing-dialog-types'; import { Dispatch } from 'redux'; import { ServiceRepository } from "services/services"; import { FilterBuilder } from 'services/api/filter-builder'; import { initialize, getFormValues, reset } from 'redux-form'; import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types'; import { RootState } from 'store/store'; import { getDialog } from 'store/dialog/dialog-reducer'; import { PermissionLevel, PermissionResource } from 'models/permission'; import { differenceWith } from "lodash"; import { withProgress } from "store/progress-indicator/with-progress"; import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions'; import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions"; import { extractUuidObjectType, ResourceObjectType } from "models/resource"; import { resourcesActions } from "store/resources/resources-actions"; import { getPublicGroupUuid, getAllUsersGroupUuid } from "store/workflow-panel/workflow-panel-actions"; import { getSharingPublicAccessFormData } from './sharing-dialog-types'; export const openSharingDialog = (resourceUuid: string, refresh?: () => void) => (dispatch: Dispatch) => { dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: { resourceUuid, refresh } })); dispatch(loadSharingDialog); }; export const closeSharingDialog = () => dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }); export const connectSharingDialog = withDialog(SHARING_DIALOG_NAME); export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME); export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: () => RootState) => { dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); await dispatch(savePublicPermissionChanges); await dispatch(saveManagementChanges); await dispatch(sendInvitations); dispatch(reset(SHARING_INVITATION_FORM_NAME)); await dispatch(loadSharingDialog); dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); const dialog = getDialog(getState().dialog, SHARING_DIALOG_NAME); if (dialog && dialog.data.refresh) { dialog.data.refresh(); } }; export interface SharingDialogData { resourceUuid: string; refresh: () => void; } export const createSharingToken = (expDate: Date | undefined) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => { const dialog = getDialog(getState().dialog, SHARING_DIALOG_NAME); if (dialog) { const resourceUuid = dialog.data.resourceUuid; if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) { dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); try { const sharingToken = await apiClientAuthorizationService.createCollectionSharingToken(resourceUuid, expDate); dispatch(resourcesActions.SET_RESOURCES([sharingToken])); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Sharing URL created', hideDuration: 2000, kind: SnackbarKind.SUCCESS, })); } catch (e) { dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Failed to create sharing URL', hideDuration: 2000, kind: SnackbarKind.ERROR, })); } finally { dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); } } } }; export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => { dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); try { await apiClientAuthorizationService.delete(uuid); dispatch(resourcesActions.DELETE_RESOURCES([uuid])); dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Sharing URL removed', hideDuration: 2000, kind: SnackbarKind.SUCCESS, })); } catch (e) { dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Failed to remove sharing URL', hideDuration: 2000, kind: SnackbarKind.ERROR, })); } finally { dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); } }; const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => { const dialog = getDialog(getState().dialog, SHARING_DIALOG_NAME); const sharingURLsDisabled = getState().auth.config.clusterConfig.Workbench.DisableSharingURLsUI; if (dialog) { dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); try { const resourceUuid = dialog.data.resourceUuid; await dispatch(initializeManagementForm); // For collections, we need to load the public sharing tokens if (!sharingURLsDisabled && extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) { const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid); dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items])); } } catch (e) { dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'You do not have access to share this item', hideDuration: 2000, kind: SnackbarKind.ERROR })); dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME })); } finally { dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); } } }; export const initializeManagementForm = async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService, permissionService }: ServiceRepository) => { const dialog = getDialog(getState().dialog, SHARING_DIALOG_NAME); if (!dialog) { return; } dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME)); const resourceUuid = dialog?.data.resourceUuid; const { items: permissionLinks } = await permissionService.listResourcePermissions(resourceUuid); dispatch(initializePublicAccessForm(permissionLinks)); const filters = new FilterBuilder() .addIn('uuid', Array.from(new Set(permissionLinks.map(({ tailUuid }) => tailUuid)))) .getFilters(); const { items: users } = await userService.list({ filters, count: "none", limit: 1000 }); const { items: groups } = await groupsService.list({ filters, count: "none", limit: 1000 }); const getEmail = (tailUuid: string) => { const user = users.find(({ uuid }) => uuid === tailUuid); const group = groups.find(({ uuid }) => uuid === tailUuid); return user ? user.email : group ? group.name : tailUuid; }; const managementPermissions = permissionLinks .map(({ tailUuid, name, uuid }) => ({ email: getEmail(tailUuid), permissions: name as PermissionLevel, permissionUuid: uuid, })); const managementFormData: SharingManagementFormData = { permissions: managementPermissions, initialPermissions: managementPermissions, }; dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData)); dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME)); }; const initializePublicAccessForm = (permissionLinks: PermissionResource[]) => (dispatch: Dispatch, getState: () => RootState,) => { const state = getState(); const [publicPermission] = permissionLinks .filter(item => item.tailUuid === getPublicGroupUuid(state)); const [allUsersPermission] = permissionLinks .filter(item => item.tailUuid === getAllUsersGroupUuid(state)); let publicAccessFormData: SharingPublicAccessFormData; if (publicPermission) { publicAccessFormData = { visibility: VisibilityLevel.PUBLIC, initialVisibility: VisibilityLevel.PUBLIC, permissionUuid: publicPermission.uuid }; } else if (allUsersPermission) { publicAccessFormData = { visibility: VisibilityLevel.ALL_USERS, initialVisibility: VisibilityLevel.ALL_USERS, permissionUuid: allUsersPermission.uuid }; } else if (permissionLinks.length > 0) { publicAccessFormData = { visibility: VisibilityLevel.SHARED, initialVisibility: VisibilityLevel.SHARED, permissionUuid: '' }; } else { publicAccessFormData = { visibility: VisibilityLevel.PRIVATE, initialVisibility: VisibilityLevel.PRIVATE, permissionUuid: '' }; } dispatch(initialize(SHARING_PUBLIC_ACCESS_FORM_NAME, publicAccessFormData)); }; const savePublicPermissionChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => { const state = getState(); const { user } = state.auth; const dialog = getDialog(state.dialog, SHARING_DIALOG_NAME); if (dialog && user) { const { permissionUuid, visibility, initialVisibility } = getSharingPublicAccessFormData(state); // If visibility level changed, delete the previous link to public/all users. // On PRIVATE this link will be deleted by saveManagementChanges // so don't double delete (which would show an error dialog). if (permissionUuid !== "" && visibility !== initialVisibility) { await permissionService.delete(permissionUuid); } if (visibility === VisibilityLevel.ALL_USERS) { await permissionService.create({ ownerUuid: user.uuid, headUuid: dialog.data.resourceUuid, tailUuid: getAllUsersGroupUuid(state), name: PermissionLevel.CAN_READ, }); } else if (visibility === VisibilityLevel.PUBLIC) { await permissionService.create({ ownerUuid: user.uuid, headUuid: dialog.data.resourceUuid, tailUuid: getPublicGroupUuid(state), name: PermissionLevel.CAN_READ, }); } } }; const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => { const state = getState(); const { user } = state.auth; const dialog = getDialog(state.dialog, SHARING_DIALOG_NAME); if (dialog && user) { const { initialPermissions, permissions } = getSharingMangementFormData(state); const { visibility } = getSharingPublicAccessFormData(state); const cancelledPermissions = visibility === VisibilityLevel.PRIVATE ? initialPermissions : differenceWith( initialPermissions, permissions, (a, b) => a.permissionUuid === b.permissionUuid ); const deletions = cancelledPermissions.map(async ({ permissionUuid }) => { try { await permissionService.delete(permissionUuid, false); } catch (e) { } }); const updates = permissions.map(async update => { try { await permissionService.update(update.permissionUuid, { name: update.permissions }, false); } catch (e) { } }); await Promise.all([...deletions, ...updates]); } }; const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => { const state = getState(); const { user } = state.auth; const dialog = getDialog(state.dialog, SHARING_DIALOG_NAME); if (dialog && user) { const invitations = getFormValues(SHARING_INVITATION_FORM_NAME)(state) as SharingInvitationFormData; const data = invitations.invitedPeople.map(invitee => ({ ownerUuid: user.uuid, headUuid: dialog.data.resourceUuid, tailUuid: invitee.uuid, name: invitations.permissions })); const changes = data.map(invitation => permissionService.create(invitation)); await Promise.all(changes); } };