1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { dialogActions } from "store/dialog/dialog-actions";
6 import { withDialog } from "store/dialog/with-dialog";
9 SHARING_INVITATION_FORM_NAME,
10 SharingManagementFormData,
11 SharingInvitationFormData,
12 getSharingMangementFormData,
13 SharingPublicAccessFormData,
15 SHARING_PUBLIC_ACCESS_FORM_NAME,
16 } from './sharing-dialog-types';
17 import { Dispatch } from 'redux';
18 import { ServiceRepository } from "services/services";
19 import { FilterBuilder } from 'services/api/filter-builder';
20 import { initialize, getFormValues, reset } from 'redux-form';
21 import { SHARING_MANAGEMENT_FORM_NAME } from 'store/sharing-dialog/sharing-dialog-types';
22 import { RootState } from 'store/store';
23 import { getDialog } from 'store/dialog/dialog-reducer';
24 import { PermissionLevel, PermissionResource } from 'models/permission';
25 import { differenceWith } from "lodash";
26 import { withProgress } from "store/progress-indicator/with-progress";
27 import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
28 import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
30 extractUuidObjectType,
32 } from "models/resource";
33 import { resourcesActions } from "store/resources/resources-actions";
34 import { getPublicGroupUuid, getAllUsersGroupUuid } from "store/workflow-panel/workflow-panel-actions";
35 import { getSharingPublicAccessFormData } from './sharing-dialog-types';
37 export const openSharingDialog = (resourceUuid: string, refresh?: () => void) =>
38 (dispatch: Dispatch) => {
39 dispatch(dialogActions.OPEN_DIALOG({ id: SHARING_DIALOG_NAME, data: { resourceUuid, refresh } }));
40 dispatch<any>(loadSharingDialog);
43 export const closeSharingDialog = () =>
44 dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME });
46 export const connectSharingDialog = withDialog(SHARING_DIALOG_NAME);
47 export const connectSharingDialogProgress = withProgress(SHARING_DIALOG_NAME);
50 export const saveSharingDialogChanges = async (dispatch: Dispatch, getState: () => RootState) => {
51 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
52 await dispatch<any>(savePublicPermissionChanges);
53 await dispatch<any>(saveManagementChanges);
54 await dispatch<any>(sendInvitations);
55 dispatch(reset(SHARING_INVITATION_FORM_NAME));
56 await dispatch<any>(loadSharingDialog);
57 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
59 const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
60 if (dialog && dialog.data.refresh) {
61 dialog.data.refresh();
65 export interface SharingDialogData {
70 export const createSharingToken = (expDate: Date | undefined) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
71 const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
73 const resourceUuid = dialog.data.resourceUuid;
74 if (extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
75 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
77 const sharingToken = await apiClientAuthorizationService.createCollectionSharingToken(resourceUuid, expDate);
78 dispatch(resourcesActions.SET_RESOURCES([sharingToken]));
79 dispatch(snackbarActions.OPEN_SNACKBAR({
80 message: 'Sharing URL created',
82 kind: SnackbarKind.SUCCESS,
85 dispatch(snackbarActions.OPEN_SNACKBAR({
86 message: 'Failed to create sharing URL',
88 kind: SnackbarKind.ERROR,
91 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
97 export const deleteSharingToken = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
98 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
100 await apiClientAuthorizationService.delete(uuid);
101 dispatch(resourcesActions.DELETE_RESOURCES([uuid]));
102 dispatch(snackbarActions.OPEN_SNACKBAR({
103 message: 'Sharing URL removed',
105 kind: SnackbarKind.SUCCESS,
108 dispatch(snackbarActions.OPEN_SNACKBAR({
109 message: 'Failed to remove sharing URL',
111 kind: SnackbarKind.ERROR,
114 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
118 const loadSharingDialog = async (dispatch: Dispatch, getState: () => RootState, { apiClientAuthorizationService }: ServiceRepository) => {
120 const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
121 const sharingURLsDisabled = getState().auth.config.clusterConfig.Workbench.DisableSharingURLsUI;
123 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
125 const resourceUuid = dialog.data.resourceUuid;
126 await dispatch<any>(initializeManagementForm);
127 // For collections, we need to load the public sharing tokens
128 if (!sharingURLsDisabled && extractUuidObjectType(resourceUuid) === ResourceObjectType.COLLECTION) {
129 const sharingTokens = await apiClientAuthorizationService.listCollectionSharingTokens(resourceUuid);
130 dispatch(resourcesActions.SET_RESOURCES([...sharingTokens.items]));
133 dispatch(snackbarActions.OPEN_SNACKBAR({
134 message: 'You do not have access to share this item',
136 kind: SnackbarKind.ERROR
138 dispatch(dialogActions.CLOSE_DIALOG({ id: SHARING_DIALOG_NAME }));
140 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
145 export const initializeManagementForm = async (dispatch: Dispatch, getState: () => RootState, { userService, groupsService, permissionService }: ServiceRepository) => {
147 const dialog = getDialog<SharingDialogData>(getState().dialog, SHARING_DIALOG_NAME);
151 dispatch(progressIndicatorActions.START_WORKING(SHARING_DIALOG_NAME));
152 const resourceUuid = dialog?.data.resourceUuid;
153 const { items: permissionLinks } = await permissionService.listResourcePermissions(resourceUuid);
154 dispatch<any>(initializePublicAccessForm(permissionLinks));
155 const filters = new FilterBuilder()
156 .addIn('uuid', Array.from(new Set(permissionLinks.map(({ tailUuid }) => tailUuid))))
159 const { items: users } = await userService.list({ filters, count: "none", limit: 1000 });
160 const { items: groups } = await groupsService.list({ filters, count: "none", limit: 1000 });
162 const getEmail = (tailUuid: string) => {
163 const user = users.find(({ uuid }) => uuid === tailUuid);
164 const group = groups.find(({ uuid }) => uuid === tailUuid);
172 const managementPermissions = permissionLinks
173 .map(({ tailUuid, name, uuid }) => ({
174 email: getEmail(tailUuid),
175 permissions: name as PermissionLevel,
176 permissionUuid: uuid,
179 const managementFormData: SharingManagementFormData = {
180 permissions: managementPermissions,
181 initialPermissions: managementPermissions,
184 dispatch(initialize(SHARING_MANAGEMENT_FORM_NAME, managementFormData));
185 dispatch(progressIndicatorActions.STOP_WORKING(SHARING_DIALOG_NAME));
188 const initializePublicAccessForm = (permissionLinks: PermissionResource[]) =>
189 (dispatch: Dispatch, getState: () => RootState,) => {
191 const state = getState();
193 const [publicPermission] = permissionLinks
194 .filter(item => item.tailUuid === getPublicGroupUuid(state));
196 const [allUsersPermission] = permissionLinks
197 .filter(item => item.tailUuid === getAllUsersGroupUuid(state));
199 let publicAccessFormData: SharingPublicAccessFormData;
201 if (publicPermission) {
202 publicAccessFormData = {
203 visibility: VisibilityLevel.PUBLIC,
204 initialVisibility: VisibilityLevel.PUBLIC,
205 permissionUuid: publicPermission.uuid
207 } else if (allUsersPermission) {
208 publicAccessFormData = {
209 visibility: VisibilityLevel.ALL_USERS,
210 initialVisibility: VisibilityLevel.ALL_USERS,
211 permissionUuid: allUsersPermission.uuid
213 } else if (permissionLinks.length > 0) {
214 publicAccessFormData = {
215 visibility: VisibilityLevel.SHARED,
216 initialVisibility: VisibilityLevel.SHARED,
220 publicAccessFormData = {
221 visibility: VisibilityLevel.PRIVATE,
222 initialVisibility: VisibilityLevel.PRIVATE,
227 dispatch(initialize(SHARING_PUBLIC_ACCESS_FORM_NAME, publicAccessFormData));
230 const savePublicPermissionChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
231 const state = getState();
232 const { user } = state.auth;
233 const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
234 if (dialog && user) {
235 const { permissionUuid, visibility, initialVisibility } = getSharingPublicAccessFormData(state);
236 // If visibility level changed, delete the previous link to public/all users.
237 // On PRIVATE this link will be deleted by saveManagementChanges
238 // so don't double delete (which would show an error dialog).
239 if (permissionUuid !== "" && visibility !== initialVisibility) {
240 await permissionService.delete(permissionUuid);
242 if (visibility === VisibilityLevel.ALL_USERS) {
243 await permissionService.create({
244 ownerUuid: user.uuid,
245 headUuid: dialog.data.resourceUuid,
246 tailUuid: getAllUsersGroupUuid(state),
247 name: PermissionLevel.CAN_READ,
249 } else if (visibility === VisibilityLevel.PUBLIC) {
250 await permissionService.create({
251 ownerUuid: user.uuid,
252 headUuid: dialog.data.resourceUuid,
253 tailUuid: getPublicGroupUuid(state),
254 name: PermissionLevel.CAN_READ,
260 const saveManagementChanges = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
261 const state = getState();
262 const { user } = state.auth;
263 const dialog = getDialog<string>(state.dialog, SHARING_DIALOG_NAME);
264 if (dialog && user) {
265 const { initialPermissions, permissions } = getSharingMangementFormData(state);
266 const { visibility } = getSharingPublicAccessFormData(state);
267 const cancelledPermissions = visibility === VisibilityLevel.PRIVATE
272 (a, b) => a.permissionUuid === b.permissionUuid
275 const deletions = cancelledPermissions.map(async ({ permissionUuid }) => {
277 await permissionService.delete(permissionUuid, false);
280 const updates = permissions.map(async update => {
282 await permissionService.update(update.permissionUuid, { name: update.permissions }, false);
285 await Promise.all([...deletions, ...updates]);
289 const sendInvitations = async (_: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
290 const state = getState();
291 const { user } = state.auth;
292 const dialog = getDialog<SharingDialogData>(state.dialog, SHARING_DIALOG_NAME);
293 if (dialog && user) {
294 const invitations = getFormValues(SHARING_INVITATION_FORM_NAME)(state) as SharingInvitationFormData;
295 const data = invitations.invitedPeople.map(invitee => ({
296 ownerUuid: user.uuid,
297 headUuid: dialog.data.resourceUuid,
298 tailUuid: invitee.uuid,
299 name: invitations.permissions
301 const changes = data.map(invitation => permissionService.create(invitation));
302 await Promise.all(changes);