18946: Transitional user profile feature
[arvados-workbench2.git] / src / common / config.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import Axios from 'axios';
6
7 export const WORKBENCH_CONFIG_URL =
8     process.env.REACT_APP_ARVADOS_CONFIG_URL || '/config.json';
9
10 interface WorkbenchConfig {
11     API_HOST: string;
12     VOCABULARY_URL?: string;
13     FILE_VIEWERS_CONFIG_URL?: string;
14 }
15
16 export interface ClusterConfigJSON {
17     API: {
18         UnfreezeProjectRequiresAdmin: boolean
19         MaxItemsPerResponse: number
20     },
21     ClusterID: string;
22     RemoteClusters: {
23         [key: string]: {
24             ActivateUsers: boolean
25             Host: string
26             Insecure: boolean
27             Proxy: boolean
28             Scheme: string
29         }
30     };
31     Mail?: {
32         SupportEmailAddress: string;
33     };
34     Services: {
35         Controller: {
36             ExternalURL: string;
37         };
38         Workbench1: {
39             ExternalURL: string;
40         };
41         Workbench2: {
42             ExternalURL: string;
43         };
44         Workbench: {
45             DisableSharingURLsUI: boolean;
46             ArvadosDocsite: string;
47             FileViewersConfigURL: string;
48             WelcomePageHTML: string;
49             InactivePageHTML: string;
50             SSHHelpPageHTML: string;
51             SSHHelpHostSuffix: string;
52             SiteName: string;
53             IdleTimeout: string;
54         };
55         Websocket: {
56             ExternalURL: string;
57         };
58         WebDAV: {
59             ExternalURL: string;
60         };
61         WebDAVDownload: {
62             ExternalURL: string;
63         };
64         WebShell: {
65             ExternalURL: string;
66         };
67     };
68     Workbench: {
69         DisableSharingURLsUI: boolean;
70         ArvadosDocsite: string;
71         FileViewersConfigURL: string;
72         WelcomePageHTML: string;
73         InactivePageHTML: string;
74         SSHHelpPageHTML: string;
75         SSHHelpHostSuffix: string;
76         SiteName: string;
77         IdleTimeout: string;
78         BannerUUID: string;
79         UserProfileFormFields: {};
80     };
81     Login: {
82         LoginCluster: string;
83         Google: {
84             Enable: boolean;
85         };
86         LDAP: {
87             Enable: boolean;
88         };
89         OpenIDConnect: {
90             Enable: boolean;
91         };
92         PAM: {
93             Enable: boolean;
94         };
95         SSO: {
96             Enable: boolean;
97         };
98         Test: {
99             Enable: boolean;
100         };
101     };
102     Collections: {
103         ForwardSlashNameSubstitution: string;
104         ManagedProperties?: {
105             [key: string]: {
106                 Function: string;
107                 Value: string;
108                 Protected?: boolean;
109             };
110         };
111         TrustAllContent: boolean;
112     };
113     Volumes: {
114         [key: string]: {
115             StorageClasses: {
116                 [key: string]: boolean;
117             };
118         };
119     };
120     Users: {
121         AnonymousUserToken: string;
122     };
123 }
124
125 export class Config {
126     baseUrl!: string;
127     keepWebServiceUrl!: string;
128     keepWebInlineServiceUrl!: string;
129     remoteHosts!: {
130         [key: string]: string;
131     };
132     rootUrl!: string;
133     uuidPrefix!: string;
134     websocketUrl!: string;
135     workbenchUrl!: string;
136     workbench2Url!: string;
137     vocabularyUrl!: string;
138     fileViewersConfigUrl!: string;
139     loginCluster!: string;
140     clusterConfig!: ClusterConfigJSON;
141     apiRevision!: number;
142 }
143
144 export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
145     const clusterConfigJSON = removeTrailingSlashes(clusterConfig);
146     const config = new Config();
147     config.rootUrl = clusterConfigJSON.Services.Controller.ExternalURL;
148     config.baseUrl = `${config.rootUrl}/${ARVADOS_API_PATH}`;
149     config.uuidPrefix = clusterConfigJSON.ClusterID;
150     config.websocketUrl = clusterConfigJSON.Services.Websocket.ExternalURL;
151     config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
152     config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
153     config.keepWebServiceUrl =
154         clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
155     config.keepWebInlineServiceUrl =
156         clusterConfigJSON.Services.WebDAV.ExternalURL;
157     config.loginCluster = clusterConfigJSON.Login.LoginCluster;
158     config.clusterConfig = clusterConfigJSON;
159     config.apiRevision = 0;
160     mapRemoteHosts(clusterConfigJSON, config);
161     return config;
162 };
163
164 export const getStorageClasses = (config: Config): string[] => {
165     const classes: Set<string> = new Set(['default']);
166     const volumes = config.clusterConfig.Volumes;
167     Object.keys(volumes).forEach((v) => {
168         Object.keys(volumes[v].StorageClasses || {}).forEach((sc) => {
169             if (volumes[v].StorageClasses[sc]) {
170                 classes.add(sc);
171             }
172         });
173     });
174     return Array.from(classes);
175 };
176
177 const getApiRevision = async (apiUrl: string) => {
178     try {
179         const dd = (await Axios.get<any>(`${apiUrl}/${DISCOVERY_DOC_PATH}`)).data;
180         return parseInt(dd.revision, 10) || 0;
181     } catch {
182         console.warn(
183             'Unable to get API Revision number, defaulting to zero. Some features may not work properly.'
184         );
185         return 0;
186     }
187 };
188
189 const removeTrailingSlashes = (
190     config: ClusterConfigJSON
191 ): ClusterConfigJSON => {
192     const svcs: any = {};
193     Object.keys(config.Services).forEach((s) => {
194         svcs[s] = config.Services[s];
195         if (svcs[s].hasOwnProperty('ExternalURL')) {
196             svcs[s].ExternalURL = svcs[s].ExternalURL.replace(/\/+$/, '');
197         }
198     });
199     return { ...config, Services: svcs };
200 };
201
202 export const fetchConfig = () => {
203     return Axios.get<WorkbenchConfig>(
204         WORKBENCH_CONFIG_URL + '?nocache=' + new Date().getTime()
205     )
206         .then((response) => response.data)
207         .catch(() => {
208             console.warn(
209                 `There was an exception getting the Workbench config file at ${WORKBENCH_CONFIG_URL}. Using defaults instead.`
210             );
211             return Promise.resolve(getDefaultConfig());
212         })
213         .then((workbenchConfig) => {
214             if (workbenchConfig.API_HOST === undefined) {
215                 throw new Error(
216                     `Unable to start Workbench. API_HOST is undefined in ${WORKBENCH_CONFIG_URL} or the environment.`
217                 );
218             }
219             return Axios.get<ClusterConfigJSON>(
220                 getClusterConfigURL(workbenchConfig.API_HOST)
221             ).then(async (response) => {
222                 const apiRevision = await getApiRevision(
223                     response.data.Services.Controller.ExternalURL.replace(/\/+$/, '')
224                 );
225                 const config = { ...buildConfig(response.data), apiRevision };
226                 const warnLocalConfig = (varName: string) =>
227                     console.warn(
228                         `A value for ${varName} was found in ${WORKBENCH_CONFIG_URL}. To use the Arvados centralized configuration instead, \
229 remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`
230                     );
231
232                 // Check if the workbench config has an entry for vocabulary and file viewer URLs
233                 // If so, use these values (even if it is an empty string), but print a console warning.
234                 // Otherwise, use the cluster config.
235                 let fileViewerConfigUrl;
236                 if (workbenchConfig.FILE_VIEWERS_CONFIG_URL !== undefined) {
237                     warnLocalConfig('FILE_VIEWERS_CONFIG_URL');
238                     fileViewerConfigUrl = workbenchConfig.FILE_VIEWERS_CONFIG_URL;
239                 } else {
240                     fileViewerConfigUrl =
241                         config.clusterConfig.Workbench.FileViewersConfigURL ||
242                         '/file-viewers-example.json';
243                 }
244                 config.fileViewersConfigUrl = fileViewerConfigUrl;
245
246                 if (workbenchConfig.VOCABULARY_URL !== undefined) {
247                     console.warn(
248                         `A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`
249                     );
250                 }
251                 config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST);
252
253                 return { config, apiHost: workbenchConfig.API_HOST };
254             });
255         });
256 };
257
258 // Maps remote cluster hosts and removes the default RemoteCluster entry
259 export const mapRemoteHosts = (
260     clusterConfigJSON: ClusterConfigJSON,
261     config: Config
262 ) => {
263     config.remoteHosts = {};
264     Object.keys(clusterConfigJSON.RemoteClusters).forEach((k) => {
265         config.remoteHosts[k] = clusterConfigJSON.RemoteClusters[k].Host;
266     });
267     delete config.remoteHosts['*'];
268 };
269
270 export const mockClusterConfigJSON = (
271     config: Partial<ClusterConfigJSON>
272 ): ClusterConfigJSON => ({
273     API: {
274         UnfreezeProjectRequiresAdmin: false,
275         MaxItemsPerResponse: 1000,
276     },
277     ClusterID: '',
278     RemoteClusters: {},
279     Services: {
280         Controller: { ExternalURL: '' },
281         Workbench1: { ExternalURL: '' },
282         Workbench2: { ExternalURL: '' },
283         Websocket: { ExternalURL: '' },
284         WebDAV: { ExternalURL: '' },
285         WebDAVDownload: { ExternalURL: '' },
286         WebShell: { ExternalURL: '' },
287         Workbench: {
288             DisableSharingURLsUI: false,
289             ArvadosDocsite: "",
290             FileViewersConfigURL: "",
291             WelcomePageHTML: "",
292             InactivePageHTML: "",
293             SSHHelpPageHTML: "",
294             SSHHelpHostSuffix: "",
295             SiteName: "",
296             IdleTimeout: "0s"
297         },
298     },
299     Workbench: {
300         DisableSharingURLsUI: false,
301         ArvadosDocsite: '',
302         FileViewersConfigURL: '',
303         WelcomePageHTML: '',
304         InactivePageHTML: '',
305         SSHHelpPageHTML: '',
306         SSHHelpHostSuffix: '',
307         SiteName: '',
308         IdleTimeout: '0s',
309         BannerUUID: "",
310         UserProfileFormFields: {}
311     },
312     Login: {
313         LoginCluster: '',
314         Google: {
315             Enable: false,
316         },
317         LDAP: {
318             Enable: false,
319         },
320         OpenIDConnect: {
321             Enable: false,
322         },
323         PAM: {
324             Enable: false,
325         },
326         SSO: {
327             Enable: false,
328         },
329         Test: {
330             Enable: false,
331         },
332     },
333     Collections: {
334         ForwardSlashNameSubstitution: '',
335         TrustAllContent: false,
336     },
337     Volumes: {},
338     Users: {
339         AnonymousUserToken: ""
340     },
341     ...config,
342 });
343
344 export const mockConfig = (config: Partial<Config>): Config => ({
345     baseUrl: '',
346     keepWebServiceUrl: '',
347     keepWebInlineServiceUrl: '',
348     remoteHosts: {},
349     rootUrl: '',
350     uuidPrefix: '',
351     websocketUrl: '',
352     workbenchUrl: '',
353     workbench2Url: '',
354     vocabularyUrl: '',
355     fileViewersConfigUrl: '',
356     loginCluster: '',
357     clusterConfig: mockClusterConfigJSON({}),
358     apiRevision: 0,
359     ...config,
360 });
361
362 const getDefaultConfig = (): WorkbenchConfig => {
363     let apiHost = '';
364     const envHost = process.env.REACT_APP_ARVADOS_API_HOST;
365     if (envHost !== undefined) {
366         console.warn(`Using default API host ${envHost}.`);
367         apiHost = envHost;
368     } else {
369         console.warn(
370             `No API host was found in the environment. Workbench may not be able to communicate with Arvados components.`
371         );
372     }
373     return {
374         API_HOST: apiHost,
375         VOCABULARY_URL: undefined,
376         FILE_VIEWERS_CONFIG_URL: undefined,
377     };
378 };
379
380 export const ARVADOS_API_PATH = 'arvados/v1';
381 export const CLUSTER_CONFIG_PATH = 'arvados/v1/config';
382 export const VOCABULARY_PATH = 'arvados/v1/vocabulary';
383 export const DISCOVERY_DOC_PATH = 'discovery/v1/apis/arvados/v1/rest';
384 export const getClusterConfigURL = (apiHost: string) =>
385     `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${new Date().getTime()}`;
386 export const getVocabularyURL = (apiHost: string) =>
387     `https://${apiHost}/${VOCABULARY_PATH}?nocache=${new Date().getTime()}`;