From 5fff06692effe7b4c95e83100f94b51d96165f1c Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Thu, 31 May 2018 16:30:52 +0100 Subject: [PATCH 01/13] app: prevent new windows out of initial origin Fixes https://github.com/plotly/streambed/issues/11017 --- backend/main.development.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/main.development.js b/backend/main.development.js index dbac65e34..7633c6f6e 100644 --- a/backend/main.development.js +++ b/backend/main.development.js @@ -65,6 +65,12 @@ app.on('ready', () => { if (!url.startsWith(HTTP_URL)) event.preventDefault(); }); + // prevent navigation out of HTTP_URL + // see https://electronjs.org/docs/api/web-contents#event-will-navigate + mainWindow.webContents.on('new-window', (event, url) => { + if (!url.startsWith(HTTP_URL)) event.preventDefault(); + }); + mainWindow.on('closed', () => { mainWindow = null; }); From 90e5f7d1b4ec44f5f416418d22db28ddb566a171 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 09:32:00 +0100 Subject: [PATCH 02/13] app: replace 'new-window` with shell.openExternal * This will help get rid of `shell.openExternal` in the frontend. --- backend/main.development.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/main.development.js b/backend/main.development.js index 7633c6f6e..32bf721a8 100644 --- a/backend/main.development.js +++ b/backend/main.development.js @@ -1,4 +1,4 @@ -import {app, BrowserWindow} from 'electron'; +import {app, BrowserWindow, shell} from 'electron'; import {contains} from 'ramda'; import Logger from './logger'; import {setupMenus} from './menus'; @@ -68,7 +68,8 @@ app.on('ready', () => { // prevent navigation out of HTTP_URL // see https://electronjs.org/docs/api/web-contents#event-will-navigate mainWindow.webContents.on('new-window', (event, url) => { - if (!url.startsWith(HTTP_URL)) event.preventDefault(); + event.preventDefault(); + shell.openExternal(url); }); mainWindow.on('closed', () => { From dbd1e82d68128840a0f93868078b34a6a8eefe6a Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 11:08:10 +0100 Subject: [PATCH 03/13] Settings: silence uncaught promise --- app/components/Settings/Settings.react.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/Settings/Settings.react.js b/app/components/Settings/Settings.react.js index b87c995e5..8559cd7bd 100644 --- a/app/components/Settings/Settings.react.js +++ b/app/components/Settings/Settings.react.js @@ -60,10 +60,14 @@ class Settings extends Component { this.intervals.checkHTTPSEndpointInterval = setInterval(() => { if (this.state.urls.https) { - fetch(this.state.urls.https).then(() => { + fetch(this.state.urls.https) + .then(() => { this.setState({httpsServerIsOK: true}); clearInterval(this.intervals.checkHTTPSEndpointInterval); clearInterval(this.intervals.timeElapsedInterval); + }) + .catch(() => { + // silence fetch errors }); } }, 5 * 1000); From 8b16fc0629743f5904a0cce2c43afa9f492c76fe Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 11:15:46 +0100 Subject: [PATCH 04/13] routes: fix URL to csv file returned by /datacache * Remove extra slash and ensure an absolute path is returned --- backend/routes.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/routes.js b/backend/routes.js index 296f3cff6..615c48fec 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -613,13 +613,15 @@ export default class Servers { } else { const rand = Math.round(Math.random() * 1000).toString(); - const downloadPath = path.join(getSetting('STORAGE_PATH'), `data_export_${rand}.csv`); + const downloadPath = path.resolve( + path.join(getSetting('STORAGE_PATH'), `data_export_${rand}.csv`) + ); fs.writeFile(downloadPath, payload, (err) => { if (err) { res.json({type: 'error', message: err}); return next(); } - res.json({type: 'csv', url: 'file:///'.concat(downloadPath)}); + res.json({type: 'csv', url: 'file://'.concat(downloadPath)}); return next(); }); } From 9778cac9e8c277488d6314350f454edf9b63d665 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 12:44:05 +0100 Subject: [PATCH 05/13] Link: don't use shell.openExternal * Replace call to `shell.openExternal` with an `` tag. * Don't use /datacache to save CSV files locally, because: - it's unnecessary, - and `` can't be used to open `file://` URLs. * Convert CSV file to data URL. * Capture requests to open a data URL and show native a save dialog instead. --- app/components/Link.react.js | 28 +----------- .../Settings/Preview/Preview.react.js | 32 ++++++-------- backend/main.development.js | 44 ++++++++++++++++--- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/app/components/Link.react.js b/app/components/Link.react.js index 7e16ebda7..06bf0aa37 100644 --- a/app/components/Link.react.js +++ b/app/components/Link.react.js @@ -1,31 +1,5 @@ -import R from 'ramda'; import React from 'react'; -import PropTypes from 'prop-types'; -import {dynamicRequireElectron} from '../utils/utils'; - - -let shell; -try { - shell = dynamicRequireElectron().shell; -} catch (e) { - shell = null; -} export function Link(props) { - const {href} = props; - if (shell) { - return ( - {shell.openExternal(href);}} - {...R.omit(['className'], props)} - /> - ); - } - return ; + return ; } - -Link.propTypes = { - href: PropTypes.string, - className: PropTypes.string -}; diff --git a/app/components/Settings/Preview/Preview.react.js b/app/components/Settings/Preview/Preview.react.js index f4155a519..7944220a7 100644 --- a/app/components/Settings/Preview/Preview.react.js +++ b/app/components/Settings/Preview/Preview.react.js @@ -171,7 +171,6 @@ class Preview extends Component { } fetchDatacache(payload, type) { - const {username} = this.props; const payloadJSON = JSON.stringify({ payload: payload, type: type, requestor: username}); @@ -187,7 +186,8 @@ class Preview extends Component { }).then(resp => { return resp.json(); }).then(data => { - const plotlyLinks = this.state.plotlyLinks; + const {plotlyLinks} = this.state; + let link; if (!('error' in data)) { @@ -456,6 +456,15 @@ class Preview extends Component {
+ } -
{link.url}
} - {link.type === 'csv' && -
-
💾 Your CSV has been saved ⬇️
- {link.url} -
- } {link.type === 'plot' &&
📈 Link to your chart on Chart Studio ⬇️
diff --git a/backend/main.development.js b/backend/main.development.js index 32bf721a8..b8b848b94 100644 --- a/backend/main.development.js +++ b/backend/main.development.js @@ -1,8 +1,10 @@ -import {app, BrowserWindow, shell} from 'electron'; +import {app, BrowserWindow, dialog, shell} from 'electron'; +import fs from 'fs'; + import {contains} from 'ramda'; -import Logger from './logger'; -import {setupMenus} from './menus'; +import Logger from './logger.js'; +import {setupMenus} from './menus.js'; import Servers from './routes.js'; Logger.log('Starting application', 2); @@ -62,14 +64,46 @@ app.on('ready', () => { // prevent navigation out of HTTP_URL // see https://electronjs.org/docs/api/web-contents#event-will-navigate mainWindow.webContents.on('will-navigate', (event, url) => { - if (!url.startsWith(HTTP_URL)) event.preventDefault(); + if (!url.startsWith(HTTP_URL)) { + Logger.log(`Preventing navigation to ${url}`, 2); + event.preventDefault(); + } }); // prevent navigation out of HTTP_URL // see https://electronjs.org/docs/api/web-contents#event-will-navigate mainWindow.webContents.on('new-window', (event, url) => { event.preventDefault(); - shell.openExternal(url); + + if (!url.startsWith('data:')) { + Logger.log(`Opening ${url}`, 2); + shell.openExternal(url); + return; + } + + // only download data URLs that contain a CSV file + if (!url.startsWith('data:text/csv;base64,')) { + Logger.log(`Preventing request ${url}`, 2); + return; + } + + dialog.showSaveDialog({ + title: 'Save CSV File', + filters: [{ + name: 'CSV files', + extensions: ['csv'] + }] + }, (filename) => { + if (!filename) { + // silence error thrown when user cancels the save dialog + return; + } + + fs.writeFileSync( + filename, + Buffer.from(url.slice(1 + url.indexOf(',')), 'base64').toString() + ); + }); }); mainWindow.on('closed', () => { From ea0ffa7502756852f821a98a1a3ecc1353a9d119 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 13:10:46 +0100 Subject: [PATCH 06/13] Login: don't use shell.openExternal --- app/components/Login.react.js | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/app/components/Login.react.js b/app/components/Login.react.js index 64168d263..5ee699f6d 100644 --- a/app/components/Login.react.js +++ b/app/components/Login.react.js @@ -4,7 +4,6 @@ import React, {Component} from 'react'; import {connect} from 'react-redux'; import { baseUrl, - dynamicRequireElectron, homeUrl, isOnPrem, plotlyUrl @@ -138,21 +137,11 @@ class Login extends Component { } oauthPopUp() { - try { - const electron = dynamicRequireElectron(); - const oauthUrl = this.buildOauthUrl(); - electron.shell.openExternal(oauthUrl); - } catch (e) { - // eslint-disable-next-line no-console - console.log('Unable to openExternal, opening a popupWindow instead:'); - // eslint-disable-next-line no-console - console.log(e); - const popupWindow = PopupCenter( - this.buildOauthUrl(), 'Authorization', '500', '500' - ); - if (window.focus) { - popupWindow.focus(); - } + const popupWindow = PopupCenter( + this.buildOauthUrl(), 'Authorization', '500', '500' + ); + if (window.focus) { + popupWindow.focus(); } } From 6100bba37fe1ed71788a63336c262df2a4e86047 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 16:13:45 +0100 Subject: [PATCH 07/13] Login: remove build-time global variables * See https://github.com/plotly/streambed/issues/10436 --- Dockerfile | 2 +- app/components/Configuration.react.js | 45 ++++++++++--------- app/components/Login.react.js | 63 ++++++++++++--------------- backend/constants.js | 6 +++ backend/plugins/authorization.js | 21 ++++++--- webpack.config.web.js | 17 -------- 6 files changed, 73 insertions(+), 81 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5b59bedec..d60f22427 100644 --- a/Dockerfile +++ b/Dockerfile @@ -49,4 +49,4 @@ RUN yarn run heroku-postbuild ENV PLOTLY_CONNECTOR_PORT 9494 EXPOSE 9494 -ENTRYPOINT yarn run build-web && yarn run start-headless +ENTRYPOINT yarn run start-headless diff --git a/app/components/Configuration.react.js b/app/components/Configuration.react.js index 85ea491cc..6de3453e7 100644 --- a/app/components/Configuration.react.js +++ b/app/components/Configuration.react.js @@ -2,20 +2,21 @@ import cookie from 'react-cookies'; import React, { Component, PropTypes } from 'react'; import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import Settings from './Settings/Settings.react'; -import {isElectron} from '../utils/utils'; -import * as SessionsActions from '../actions/sessions'; -import Login from './Login.react'; + +import Login from './Login.react.js'; +import * as SessionsActions from '../actions/sessions.js'; +import Settings from './Settings/Settings.react.js'; +import {isElectron} from '../utils/utils.js'; class Configuration extends Component { constructor(props) { super(props); this.state = { + authEnabled: (cookie.load('db-connector-auth-enabled') === 'true'), + clientId: cookie.load('db-connector-client-id'), isMenuOpen: false, - username: cookie.load('db-connector-user'), - /* global PLOTLY_ENV */ - authDisabled: !PLOTLY_ENV.AUTH_ENABLED + username: cookie.load('db-connector-user') }; this.toggle = this.toggle.bind(this); this.close = this.close.bind(this); @@ -44,26 +45,24 @@ class Configuration extends Component { } logOut() { + /* + * Delete all the cookies and reset user state. This does not kill + * any running connections, but user will not be able to access them + * without logging in again. + */ + cookie.remove('db-connector-user'); + cookie.remove('plotly-auth-token'); + cookie.remove('db-connector-auth-token'); + this.setState({ username: ''}); - /* - * Delete all the cookies and reset user state. This does not kill - * any running connections, but user will not be able to access them - * without logging in again. - */ - cookie.remove('db-connector-user'); - cookie.remove('plotly-auth-token'); - cookie.remove('db-connector-auth-token'); - cookie.remove('db-connector-auth-disabled'); - this.setState({ username: ''}); - - // reload page when running in browser: - if (!isElectron()) { - window.location.reload(); - } + // reload page when running in browser: + if (!isElectron()) { + window.location.reload(); + } } render() { - return (isElectron() || this.state.authDisabled || this.state.username) ? ( + return (isElectron() || !this.state.authEnabled || this.state.username) ? (
diff --git a/app/components/Login.react.js b/app/components/Login.react.js index 5ee699f6d..2127e5ce8 100644 --- a/app/components/Login.react.js +++ b/app/components/Login.react.js @@ -22,33 +22,11 @@ const ONPREM = 'onprem'; window.document.title = `${build.productName} v${version}`; let usernameLogged = ''; -// http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen -const PopupCenter = (url, title, w, h) => { - // Fixes dual-screen position - const dualScreenLeft = 'screenLeft' in window ? window.screenLeft : screen.left; - const dualScreenTop = 'screenTop' in window ? window.screenTop : screen.top; - - const width = window.innerWidth - ? window.innerWidth - : document.documentElement.clientWidth - ? document.documentElement.clientWidth - : screen.width; - const height = window.innerHeight - ? window.innerHeight - : document.documentElement.clientHeight - ? document.documentElement.clientHeight - : screen.height; - - const left = ((width / 2) - (w / 2)) + dualScreenLeft; - const top = ((height / 2) - (h / 2)) + dualScreenTop; - const popupWindow = window.open(url, title, `scrollbars=yes, width=${w}, height=${h}, top=${top}, left=${left}`); - return popupWindow; -}; - class Login extends Component { constructor(props) { super(props); this.state = { + clientId: cookie.load('db-connector-client-id'), domain: (isOnPrem() ? plotlyUrl() : 'https://plot.ly'), statusMessage: '', serverType: CLOUD, @@ -100,7 +78,6 @@ class Login extends Component { } saveDomainToSettings() { - const {domain} = this.state; let PLOTLY_API_SSL_ENABLED = true; let PLOTLY_API_DOMAIN = ''; @@ -124,25 +101,41 @@ class Login extends Component { } buildOauthUrl() { - const {domain} = this.state; - /* global PLOTLY_ENV */ - const oauthClientId = PLOTLY_ENV.OAUTH2_CLIENT_ID; - + const {clientId, domain} = this.state; const redirect_uri = baseUrlWrapped; return ( `${domain}/o/authorize/?response_type=token&` + - `client_id=${oauthClientId}&` + + `client_id=${clientId}&` + `redirect_uri=${redirect_uri}/oauth2/callback` ); } oauthPopUp() { - const popupWindow = PopupCenter( - this.buildOauthUrl(), 'Authorization', '500', '500' - ); - if (window.focus) { - popupWindow.focus(); - } + // http://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen + const url = this.buildOauthUrl(); + const title = 'Authorization'; + const w = '600'; + const h = '600'; + + // Fixes dual-screen position + const dualScreenLeft = 'screenLeft' in window ? window.screenLeft : screen.left; + const dualScreenTop = 'screenTop' in window ? window.screenTop : screen.top; + + const width = window.innerWidth + ? window.innerWidth + : document.documentElement.clientWidth + ? document.documentElement.clientWidth + : screen.width; + const height = window.innerHeight + ? window.innerHeight + : document.documentElement.clientHeight + ? document.documentElement.clientHeight + : screen.height; + + const left = ((width / 2) - (w / 2)) + dualScreenLeft; + const top = ((height / 2) - (h / 2)) + dualScreenTop; + + window.open(url, title, `scrollbars=yes, width=${w}, height=${h}, top=${top}, left=${left}`); } logIn () { diff --git a/backend/constants.js b/backend/constants.js index 004b8d4a4..35d19a245 100644 --- a/backend/constants.js +++ b/backend/constants.js @@ -1,5 +1,11 @@ import {getSetting} from './settings'; +export function getUnsecuredCookieOptions() { + return { + path: getSetting('WEB_BASE_PATHNAME') + }; +} + export function getCookieOptions() { return { secure: getSetting('SSL_ENABLED'), diff --git a/backend/plugins/authorization.js b/backend/plugins/authorization.js index 1d6fcc82c..aa70c078c 100644 --- a/backend/plugins/authorization.js +++ b/backend/plugins/authorization.js @@ -1,9 +1,13 @@ +import fetch from 'node-fetch'; import {contains} from 'ramda'; + +import {generateAndSaveAccessToken} from '../utils/authUtils.js'; +import { + getAccessTokenCookieOptions, + getUnsecuredCookieOptions +} from '../constants.js'; +import Logger from '../logger.js'; import {getSetting} from '../settings.js'; -import {getAccessTokenCookieOptions} from '../constants.js'; -import {generateAndSaveAccessToken} from '../utils/authUtils'; -import Logger from '../logger'; -import fetch from 'node-fetch'; /* * backend does not see `/external-data-connector` in on-prem (because it is proxied). @@ -29,7 +33,14 @@ export function PlotlyOAuth(electron) { return function isAuthorized(req, res, next) { const path = req.href(); - if (!getSetting('AUTH_ENABLED')) { + const clientId = process.env.PLOTLY_CONNECTOR_OAUTH2_CLIENT_ID || + 'isFcew9naom2f1khSiMeAtzuOvHXHuLwhPsM7oPt'; + res.setCookie('db-connector-client-id', clientId, getUnsecuredCookieOptions()); + + const authEnabled = getSetting('AUTH_ENABLED'); + res.setCookie('db-connector-auth-enabled', authEnabled, getUnsecuredCookieOptions()); + + if (!authEnabled) { return next(); } diff --git a/webpack.config.web.js b/webpack.config.web.js index 6b2ddafb9..5f63e8523 100644 --- a/webpack.config.web.js +++ b/webpack.config.web.js @@ -2,13 +2,6 @@ import webpack from 'webpack'; import baseConfig from './webpack.config.base'; import path from 'path'; -const AUTH_ENABLED = process.env.PLOTLY_CONNECTOR_AUTH_ENABLED ? - JSON.parse(process.env.PLOTLY_CONNECTOR_AUTH_ENABLED) : true; - -const OAUTH2_CLIENT_ID = process.env.PLOTLY_CONNECTOR_OAUTH2_CLIENT_ID ? - JSON.stringify(process.env.PLOTLY_CONNECTOR_OAUTH2_CLIENT_ID) : - JSON.stringify('isFcew9naom2f1khSiMeAtzuOvHXHuLwhPsM7oPt'); - const config = { ...baseConfig, @@ -31,16 +24,6 @@ const config = { new webpack.DefinePlugin({ __DEV__: false, 'process.env.NODE_ENV': JSON.stringify('production') - }), - - // This is used to pass environment variables to frontend - // detailed discussions on this ticket: - // https://github.com/plotly/streambed/issues/10436 - new webpack.DefinePlugin({ - 'PLOTLY_ENV': { - 'AUTH_ENABLED': AUTH_ENABLED, - 'OAUTH2_CLIENT_ID': OAUTH2_CLIENT_ID - } }) ], From f3629210815f37998baf4af856983d142673a69c Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 17:13:28 +0100 Subject: [PATCH 08/13] oauth: fix authorisation in the web app * Ensure the cookie db-connector-user is also passed to HTTP connections. * Trigger the reload of the web app when the authorisation popup is closed. Fixes #260 --- app/components/Login.react.js | 11 +++++++---- backend/routes.js | 27 ++++++++++++++++----------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/app/components/Login.react.js b/app/components/Login.react.js index 2127e5ce8..7fc78b931 100644 --- a/app/components/Login.react.js +++ b/app/components/Login.react.js @@ -20,7 +20,6 @@ const CLOUD = 'cloud'; const ONPREM = 'onprem'; window.document.title = `${build.productName} v${version}`; -let usernameLogged = ''; class Login extends Component { constructor(props) { @@ -35,6 +34,11 @@ class Login extends Component { this.buildOauthUrl = this.buildOauthUrl.bind(this); this.oauthPopUp = this.oauthPopUp.bind(this); this.logIn = this.logIn.bind(this); + + // the web app: + // - sets this property to the popup window opened for authorization + // - and triggers a reload when `this.popup.closed` becomes true + this.popup = null; } componentDidMount() { @@ -46,8 +50,7 @@ class Login extends Component { * to check for authentication. */ setInterval(() => { - usernameLogged = cookie.load('db-connector-user'); - if (usernameLogged) { + if (this.popup && this.popup.closed) { if (serverType === ONPREM) { this.setState({ status: 'authorized', @@ -135,7 +138,7 @@ class Login extends Component { const left = ((width / 2) - (w / 2)) + dualScreenLeft; const top = ((height / 2) - (h / 2)) + dualScreenTop; - window.open(url, title, `scrollbars=yes, width=${w}, height=${h}, top=${top}, left=${left}`); + this.popup = window.open(url, title, `scrollbars=yes, width=${w}, height=${h}, top=${top}, left=${left}`); } logIn () { diff --git a/backend/routes.js b/backend/routes.js index 615c48fec..935a6d4c3 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -1,13 +1,24 @@ +const fetch = require('node-fetch'); +import {contains, keys, isEmpty, merge, pluck} from 'ramda'; const restify = require('restify'); const CookieParser = require('restify-cookies'); -const fetch = require('node-fetch'); import * as fs from 'fs'; import path from 'path'; -import * as Datastores from './persistent/datastores/Datastores.js'; import {PlotlyOAuth} from './plugins/authorization.js'; -import {getQueries, getQuery, deleteQuery} from './persistent/Queries'; +import {generateAndSaveAccessToken} from './utils/authUtils.js'; +import { + getAccessTokenCookieOptions, + getCookieOptions, + getUnsecuredCookieOptions +} from './constants.js'; +import {getCerts, timeoutFetchAndSaveCerts, setRenewalJob} from './certificates.js'; +import * as Datastores from './persistent/datastores/Datastores.js'; +import init from './init.js'; +import Logger from './logger.js'; +import {checkWritePermissions, newDatacache} from './persistent/plotly-api.js'; +import {getQueries, getQuery, deleteQuery} from './persistent/Queries.js'; import { deleteConnectionById, editConnectionById, @@ -21,13 +32,7 @@ import { } from './persistent/Connections.js'; import QueryScheduler from './persistent/QueryScheduler.js'; import {getSetting, saveSetting} from './settings.js'; -import {generateAndSaveAccessToken} from './utils/authUtils'; -import {getAccessTokenCookieOptions, getCookieOptions} from './constants'; -import {checkWritePermissions, newDatacache} from './persistent/plotly-api.js'; -import {contains, keys, isEmpty, merge, pluck} from 'ramda'; -import {getCerts, timeoutFetchAndSaveCerts, setRenewalJob} from './certificates'; -import Logger from './logger'; -import init from './init.js'; + export default class Servers { /* @@ -309,7 +314,7 @@ export default class Servers { res.setCookie('db-connector-auth-token', db_connector_access_token, getAccessTokenCookieOptions()); - res.setCookie('db-connector-user', username, getCookieOptions()); + res.setCookie('db-connector-user', username, getUnsecuredCookieOptions()); const existingUsers = getSetting('USERS'); const existingUsernames = pluck('username', existingUsers); From ce0be6252c3e650b5755aa227705f12ce8d921c4 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Fri, 1 Jun 2018 20:25:29 +0100 Subject: [PATCH 09/13] app: disable nodeIntegration * Disables nodeIntegration by moving all the use of electron's API in the frontend to the object `window.$falcon` (that is setup inside the preload script). Closes #438 --- app/components/Configuration.react.js | 11 ++-- .../UserConnections/UserConnections.react.js | 13 +---- app/utils/utils.js | 21 +++++--- backend/main.development.js | 6 ++- backend/preload.js | 54 +++++++++++++++++++ 5 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 backend/preload.js diff --git a/app/components/Configuration.react.js b/app/components/Configuration.react.js index 6de3453e7..4fb09f617 100644 --- a/app/components/Configuration.react.js +++ b/app/components/Configuration.react.js @@ -6,7 +6,7 @@ import {connect} from 'react-redux'; import Login from './Login.react.js'; import * as SessionsActions from '../actions/sessions.js'; import Settings from './Settings/Settings.react.js'; -import {isElectron} from '../utils/utils.js'; +import {isElectron, setUsernameListener} from '../utils/utils.js'; class Configuration extends Component { @@ -28,12 +28,9 @@ class Configuration extends Component { * In the browser, the username is set with a cookie but in electron * this is set using electron's ipcRenderer. */ - if (isElectron()) { - window.require('electron').ipcRenderer.once('username', - (event, message) => { - this.setState({username: message}); - }); - } + setUsernameListener((event, message) => { + this.setState({username: message});} + ); } toggle() { diff --git a/app/components/Settings/UserConnections/UserConnections.react.js b/app/components/Settings/UserConnections/UserConnections.react.js index 04058fdb1..13b1cae5e 100644 --- a/app/components/Settings/UserConnections/UserConnections.react.js +++ b/app/components/Settings/UserConnections/UserConnections.react.js @@ -5,21 +5,12 @@ import Filedrop from './filedrop.jsx'; import {contains} from 'ramda'; import {CONNECTION_CONFIG, SAMPLE_DBS} from '../../../constants/constants'; -import {dynamicRequireElectron} from '../../../utils/utils'; - -let dialog; -try { - dialog = dynamicRequireElectron().remote.dialog; -} catch (e) { - dialog = null; -} +import {showOpenDialog} from '../../../utils/utils'; /* * Displays and alters user inputs for `configuration` * username, password, and local port number. */ - - export default class UserConnections extends Component { constructor(props) { super(props); @@ -47,7 +38,7 @@ export default class UserConnections extends Component { getStorageOnClick(setting) { // sqlite requires a path return () => { - dialog.showOpenDialog({ + showOpenDialog({ properties: ['openFile'], filters: [{ name: 'databases', diff --git a/app/utils/utils.js b/app/utils/utils.js index 5b030271e..11b49e1f1 100644 --- a/app/utils/utils.js +++ b/app/utils/utils.js @@ -1,7 +1,20 @@ import {contains} from 'ramda'; -export function dynamicRequireElectron() { - return window.require('electron'); +export function isElectron() { + // the electron's main process preloads a script that sets up `window.$falcon` + return Boolean(window.$falcon); +} + +export function setUsernameListener(callback) { + if (isElectron()) { + window.$falcon.setUsernameListener(callback); + } +} + +export function showOpenDialog(options, callback) { + if (isElectron()) { + return window.$falcon.showOpenDialog(options, callback); + } } export function baseUrl() { @@ -40,10 +53,6 @@ export function plotlyUrl() { return 'https://plot.ly'; } -export function isElectron() { - return window.process && window.process.type === 'renderer'; -} - export function homeUrl() { return (isOnPrem()) ? '/external-data-connector' : diff --git a/backend/main.development.js b/backend/main.development.js index b8b848b94..ec8dfa835 100644 --- a/backend/main.development.js +++ b/backend/main.development.js @@ -1,5 +1,6 @@ import {app, BrowserWindow, dialog, shell} from 'electron'; import fs from 'fs'; +import path from 'path'; import {contains} from 'ramda'; @@ -28,7 +29,10 @@ server.queryScheduler.loadQueries(); app.on('ready', () => { let mainWindow = new BrowserWindow({ - show: true, + webPreferences: { + nodeIntegration: false, + preload: path.resolve(path.join(__dirname, 'preload.js')) + }, width: 1024, height: 1024 }); diff --git a/backend/preload.js b/backend/preload.js new file mode 100644 index 000000000..2442de269 --- /dev/null +++ b/backend/preload.js @@ -0,0 +1,54 @@ +// this file is preloaded by electron in the renderer process +// (it has access to electron's API, even when nodeIntegration has been disabled) + +const {ipcRenderer, remote} = require('electron'); + +const propertyOptions = { + configurable: false, + enumerable: false, + writable: false +}; + +process.once('loaded', () => { + const $falcon = {}; + + Object.defineProperty(window, '$falcon', { + value: $falcon, + ...propertyOptions + }); + + Object.defineProperty($falcon, 'showOpenDialog', { + value: remote.dialog.showOpenDialog, + ...propertyOptions + }); + + let onUsername; + let receivedUsernameBeforeCallback; + let username; + function sendUsername(event, user) { + try { + if (onUsername) { + receivedUsernameBeforeCallback = false; + onUsername(event, user); + } else { + receivedUsernameBeforeCallback = true; + username = user; + } + } catch (error) { + console.error(error); // eslint-disable-line + } + } + + ipcRenderer.on('username', sendUsername); + + Object.defineProperty($falcon, 'setUsernameListener', { + value: (value) => { + onUsername = value; + + if (receivedUsernameBeforeCallback) { + sendUsername(null, username); + } + }, + ...propertyOptions + }); +}); From 2e09698214fe8abed33138d6b1190f5aa2eef7fb Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 5 Jun 2018 16:07:14 +0100 Subject: [PATCH 10/13] Query: enable query panel for non-sql connectors Fixes #451 --- app/components/Settings/Settings.react.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/app/components/Settings/Settings.react.js b/app/components/Settings/Settings.react.js index 8559cd7bd..11d272546 100644 --- a/app/components/Settings/Settings.react.js +++ b/app/components/Settings/Settings.react.js @@ -251,6 +251,7 @@ class Settings extends Component { apacheDrillStorageRequest, apacheDrillS3KeysRequest, connections, + connectRequest, deleteTab, elasticsearchMappingsRequest, getSqlSchema, @@ -287,6 +288,11 @@ class Settings extends Component { const dialect = connections[selectedTab].dialect; + const queryPanelDisabled = ( + connectRequest.status !== 200 || + (!selectedTable && contains(dialect, SQL_DIALECTS_USING_EDITOR)) + ); + return (
Connection - {this.props.connectRequest.status === 200 && selectedTable ? ( - Query - ) : ( - Query - )} + Query {isOnPrem() || @@ -339,7 +341,11 @@ class Settings extends Component { - {this.props.connectRequest.status === 200 && selectedTable ? ( + {queryPanelDisabled ? ( +
+

Please connect to a data store in the Connection tab first.

+
+ ) : ( - ) : ( -
-

Please connect to a data store in the Connection tab first.

-
)}
From 2344f2cbd132401f17cc410b6cbd2a85471717a4 Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Tue, 5 Jun 2018 16:21:09 +0100 Subject: [PATCH 11/13] electron-builder: force dtrace-provider rebuild Closes #421 Closes #422 --- package.json | 1 + webpack.config.base.js | 1 + yarn.lock | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/package.json b/package.json index b3d18c06b..909e0972e 100644 --- a/package.json +++ b/package.json @@ -236,6 +236,7 @@ "dependencies": { "alasql": "^0.4.5", "csv-parse": "^2.0.0", + "dtrace-provider": "^0.8.6", "font-awesome": "^4.6.1", "ibm_db": "^2.3.0", "mysql": "^2.15.0", diff --git a/webpack.config.base.js b/webpack.config.base.js index f42560b58..5e19e5054 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -32,6 +32,7 @@ export default { { 'csv-parse': 'commonjs csv-parse', 'data-urls': 'commonjs data-urls', + 'dtrace-provider': 'commonjs dtrace-provider', 'font-awesome': 'font-awesome', 'ibm_db': 'commonjs ibm_db', 'mysql': 'mysql', diff --git a/yarn.lock b/yarn.lock index 66b223449..9ab787c7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3096,6 +3096,12 @@ dtrace-provider@^0.8.2, dtrace-provider@~0.8: dependencies: nan "^2.3.3" +dtrace-provider@^0.8.6: + version "0.8.7" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04" + dependencies: + nan "^2.10.0" + dtype@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dtype/-/dtype-1.0.0.tgz#ae34ffa282673715203582d61bbdd0aad3cba3e7" @@ -6751,6 +6757,10 @@ mysql@^2.15.0: safe-buffer "5.1.1" sqlstring "2.3.0" +nan@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + nan@^2.3.0, nan@^2.3.3, nan@~2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" From f35bbca6c4f31e8bcd2dd2bbdd267b05e1ddd6ba Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 6 Jun 2018 18:16:41 +0100 Subject: [PATCH 12/13] doc: fix link --- backend/main.development.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/main.development.js b/backend/main.development.js index ec8dfa835..90aa99ee2 100644 --- a/backend/main.development.js +++ b/backend/main.development.js @@ -75,7 +75,7 @@ app.on('ready', () => { }); // prevent navigation out of HTTP_URL - // see https://electronjs.org/docs/api/web-contents#event-will-navigate + // see https://electronjs.org/docs/api/web-contents#event-new-window mainWindow.webContents.on('new-window', (event, url) => { event.preventDefault(); From 0ee49a2817ae0b1cabc80811326545271fc7ef6e Mon Sep 17 00:00:00 2001 From: Nicolas Riesco Date: Wed, 6 Jun 2018 18:21:21 +0100 Subject: [PATCH 13/13] oauth2: rename cookie * Renamed cookie `db-connector-client-id` to `db-connector-oauth2-client-id`. --- app/components/Configuration.react.js | 2 +- app/components/Login.react.js | 2 +- backend/plugins/authorization.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/components/Configuration.react.js b/app/components/Configuration.react.js index 4fb09f617..dbb17d785 100644 --- a/app/components/Configuration.react.js +++ b/app/components/Configuration.react.js @@ -14,7 +14,7 @@ class Configuration extends Component { super(props); this.state = { authEnabled: (cookie.load('db-connector-auth-enabled') === 'true'), - clientId: cookie.load('db-connector-client-id'), + clientId: cookie.load('db-connector-oauth2-client-id'), isMenuOpen: false, username: cookie.load('db-connector-user') }; diff --git a/app/components/Login.react.js b/app/components/Login.react.js index 7fc78b931..b814d5c6d 100644 --- a/app/components/Login.react.js +++ b/app/components/Login.react.js @@ -25,7 +25,7 @@ class Login extends Component { constructor(props) { super(props); this.state = { - clientId: cookie.load('db-connector-client-id'), + clientId: cookie.load('db-connector-oauth2-client-id'), domain: (isOnPrem() ? plotlyUrl() : 'https://plot.ly'), statusMessage: '', serverType: CLOUD, diff --git a/backend/plugins/authorization.js b/backend/plugins/authorization.js index aa70c078c..69eaf1761 100644 --- a/backend/plugins/authorization.js +++ b/backend/plugins/authorization.js @@ -35,7 +35,7 @@ export function PlotlyOAuth(electron) { const clientId = process.env.PLOTLY_CONNECTOR_OAUTH2_CLIENT_ID || 'isFcew9naom2f1khSiMeAtzuOvHXHuLwhPsM7oPt'; - res.setCookie('db-connector-client-id', clientId, getUnsecuredCookieOptions()); + res.setCookie('db-connector-oauth2-client-id', clientId, getUnsecuredCookieOptions()); const authEnabled = getSetting('AUTH_ENABLED'); res.setCookie('db-connector-auth-enabled', authEnabled, getUnsecuredCookieOptions());