[MM-22648] basic auth external sites (#1295)
* fix not using babel * wip * added tests, moved to map, polifill-like to convert between object and map * basic structure setup * working, found new bug * change buttons * fix login issue * remove logging code * address CR comments * remove custom function in favor of airbnb shim * fix linting * fix PM requested changes * [MM-25323] fix basic auth cancelling * fix crash when multiple request were made * address UX comments, added external link for user convenience
This commit is contained in:
parent
53f1f40774
commit
36c6106cad
185
package-lock.json
generated
185
package-lock.json
generated
|
@ -95,16 +95,161 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.2.3.tgz",
|
||||
"integrity": "sha512-xO/3Gn+2C7/eOUeb0VRnSP1+yvWHNxlpAot1eMhtoKDCN7POsyQP5excuT5UsV5daHxMWBeIIOeI5cmB8vMRgQ==",
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.6.tgz",
|
||||
"integrity": "sha512-6N9IeuyHvMBRyjNYOMJHrhwtu4WJMrYf8hVbEHD3pbbbmNOk1kmXSQs7bA4dYDUaIx4ZEzdnvo6NwC3WHd/Qow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-function-name": "^7.1.0",
|
||||
"@babel/helper-member-expression-to-functions": "^7.0.0",
|
||||
"@babel/helper-optimise-call-expression": "^7.0.0",
|
||||
"@babel/helper-plugin-utils": "^7.0.0",
|
||||
"@babel/helper-replace-supers": "^7.2.3"
|
||||
"@babel/helper-function-name": "^7.9.5",
|
||||
"@babel/helper-member-expression-to-functions": "^7.8.3",
|
||||
"@babel/helper-optimise-call-expression": "^7.8.3",
|
||||
"@babel/helper-plugin-utils": "^7.8.3",
|
||||
"@babel/helper-replace-supers": "^7.9.6",
|
||||
"@babel/helper-split-export-declaration": "^7.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
|
||||
"integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/highlight": "^7.8.3"
|
||||
}
|
||||
},
|
||||
"@babel/generator": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.9.6.tgz",
|
||||
"integrity": "sha512-+htwWKJbH2bL72HRluF8zumBxzuX0ZZUFl3JLNyoUjM/Ho8wnVpPXM6aUz8cfKDqQ/h7zHqKt4xzJteUosckqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.9.6",
|
||||
"jsesc": "^2.5.1",
|
||||
"lodash": "^4.17.13",
|
||||
"source-map": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-function-name": {
|
||||
"version": "7.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.9.5.tgz",
|
||||
"integrity": "sha512-JVcQZeXM59Cd1qanDUxv9fgJpt3NeKUaqBqUEvfmQ+BCOKq2xUgaWZW2hr0dkbyJgezYuplEoh5knmrnS68efw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-get-function-arity": "^7.8.3",
|
||||
"@babel/template": "^7.8.3",
|
||||
"@babel/types": "^7.9.5"
|
||||
}
|
||||
},
|
||||
"@babel/helper-get-function-arity": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz",
|
||||
"integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.8.3"
|
||||
}
|
||||
},
|
||||
"@babel/helper-member-expression-to-functions": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz",
|
||||
"integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.8.3"
|
||||
}
|
||||
},
|
||||
"@babel/helper-optimise-call-expression": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz",
|
||||
"integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.8.3"
|
||||
}
|
||||
},
|
||||
"@babel/helper-plugin-utils": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
|
||||
"integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-replace-supers": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.9.6.tgz",
|
||||
"integrity": "sha512-qX+chbxkbArLyCImk3bWV+jB5gTNU/rsze+JlcF6Nf8tVTigPJSI1o1oBow/9Resa1yehUO9lIipsmu9oG4RzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-member-expression-to-functions": "^7.8.3",
|
||||
"@babel/helper-optimise-call-expression": "^7.8.3",
|
||||
"@babel/traverse": "^7.9.6",
|
||||
"@babel/types": "^7.9.6"
|
||||
}
|
||||
},
|
||||
"@babel/helper-split-export-declaration": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz",
|
||||
"integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/types": "^7.8.3"
|
||||
}
|
||||
},
|
||||
"@babel/highlight": {
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz",
|
||||
"integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.9.0",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/parser": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.9.6.tgz",
|
||||
"integrity": "sha512-AoeIEJn8vt+d/6+PXDRPaksYhnlbMIiejioBZvvMQsOjW/JYK6k/0dKnvvP3EhK5GfMBWDPtrxRtegWdAcdq9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz",
|
||||
"integrity": "sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@babel/parser": "^7.8.6",
|
||||
"@babel/types": "^7.8.6"
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.9.6.tgz",
|
||||
"integrity": "sha512-b3rAHSjbxy6VEAvlxM8OV/0X4XrG72zoxme6q1MOoe2vd0bEc+TwayhuC1+Dfgqh1QEG+pj7atQqvUprHIccsg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.8.3",
|
||||
"@babel/generator": "^7.9.6",
|
||||
"@babel/helper-function-name": "^7.9.5",
|
||||
"@babel/helper-split-export-declaration": "^7.8.3",
|
||||
"@babel/parser": "^7.9.6",
|
||||
"@babel/types": "^7.9.6",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.13"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.9.6.tgz",
|
||||
"integrity": "sha512-qxXzvBO//jO9ZnoasKF1uJzHd2+M6Q2ZPIVfnFps8JJvXy0ZBbwbNOmE6SGIY5XOY6d1Bo5lb9d9RJ8nv3WSeA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.9.5",
|
||||
"lodash": "^4.17.13",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/helper-define-map": {
|
||||
|
@ -257,6 +402,12 @@
|
|||
"@babel/types": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz",
|
||||
"integrity": "sha512-/8arLKUFq882w4tWGj9JYzRpAlZgiWUJ+dtteNTDqrRBz9Iguck9Rn3ykuBDoUwh2TO4tSAJlrxDUOXWklJe4g==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-wrap-function": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz",
|
||||
|
@ -309,13 +460,21 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-proposal-class-properties": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.2.0.tgz",
|
||||
"integrity": "sha512-pdBj4Hvyt/L1LA0Vvm/Tkp9gNDGLIkSbX8IrYfWcoA5xUZIg8qjSOTChUSaCjYdvoMcB9y2bXVGNVZ5DH62CZg==",
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz",
|
||||
"integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.2.0",
|
||||
"@babel/helper-plugin-utils": "^7.0.0"
|
||||
"@babel/helper-create-class-features-plugin": "^7.8.3",
|
||||
"@babel/helper-plugin-utils": "^7.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": {
|
||||
"version": "7.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz",
|
||||
"integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/plugin-proposal-json-strings": {
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
"devDependencies": {
|
||||
"7zip-bin": "^4.1.0",
|
||||
"@babel/core": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.2.0",
|
||||
"@babel/preset-env": "^7.2.0",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
|
@ -59,11 +59,11 @@
|
|||
"electron-connect": "^0.6.3",
|
||||
"electron-notarize": "^0.1.1",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#070ce792d105482ffb2b27cfc0b7e78b3d20acee",
|
||||
"eslint-plugin-cypress": "^2.7.0",
|
||||
"eslint-plugin-eslint-comments": "^3.1.2",
|
||||
"eslint-plugin-header": "^3.0.0",
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-mattermost": "github:mattermost/eslint-plugin-mattermost#070ce792d105482ffb2b27cfc0b7e78b3d20acee",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"image-webpack-loader": "5.0.0",
|
||||
|
|
|
@ -8,23 +8,36 @@ import {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 're
|
|||
export default class LoginModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.username = '';
|
||||
this.password = '';
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
this.props.onLogin(this.props.request, this.username, this.password);
|
||||
this.username = '';
|
||||
this.password = '';
|
||||
this.props.onLogin(this.props.request, this.state.username, this.state.password);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
}
|
||||
|
||||
handleCancel = (event) => {
|
||||
event.preventDefault();
|
||||
this.props.onCancel(this.props.request);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
}
|
||||
|
||||
setUsername = (e) => {
|
||||
this.username = e.target.value;
|
||||
this.setState({username: e.target.value});
|
||||
}
|
||||
|
||||
setPassword = (e) => {
|
||||
this.password = e.target.value;
|
||||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -60,6 +73,7 @@ export default class LoginModal extends React.Component {
|
|||
type='text'
|
||||
placeholder='User Name'
|
||||
onChange={this.setUsername}
|
||||
value={this.state.username}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
@ -76,6 +90,7 @@ export default class LoginModal extends React.Component {
|
|||
type='password'
|
||||
placeholder='Password'
|
||||
onChange={this.setPassword}
|
||||
value={this.state.password}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
|
@ -90,7 +105,7 @@ export default class LoginModal extends React.Component {
|
|||
bsStyle='primary'
|
||||
>{'Login'}</Button>
|
||||
{ ' ' }
|
||||
<Button onClick={this.props.onCancel}>{'Cancel'}</Button>
|
||||
<Button onClick={this.handleCancel}>{'Cancel'}</Button>
|
||||
</div>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
|
|
|
@ -29,6 +29,7 @@ import HoveringURL from './HoveringURL.jsx';
|
|||
import Finder from './Finder.jsx';
|
||||
import NewTeamModal from './NewTeamModal.jsx';
|
||||
import SelectCertificateModal from './SelectCertificateModal.jsx';
|
||||
import PermissionModal from './PermissionModal.jsx';
|
||||
import ExtraBar from './ExtraBar.jsx';
|
||||
|
||||
export default class MainPage extends React.Component {
|
||||
|
@ -126,17 +127,7 @@ export default class MainPage extends React.Component {
|
|||
}
|
||||
|
||||
ipcRenderer.on('login-request', (event, request, authInfo) => {
|
||||
self.setState({
|
||||
loginRequired: true,
|
||||
});
|
||||
const loginQueue = self.state.loginQueue;
|
||||
loginQueue.push({
|
||||
request,
|
||||
authInfo,
|
||||
});
|
||||
self.setState({
|
||||
loginQueue,
|
||||
});
|
||||
this.loginRequest(event, request, authInfo);
|
||||
});
|
||||
|
||||
ipcRenderer.on('select-user-certificate', (_, origin, certificateList) => {
|
||||
|
@ -367,6 +358,18 @@ export default class MainPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
loginRequest = (event, request, authInfo) => {
|
||||
const loginQueue = this.state.loginQueue;
|
||||
loginQueue.push({
|
||||
request,
|
||||
authInfo,
|
||||
});
|
||||
this.setState({
|
||||
loginRequired: true,
|
||||
loginQueue,
|
||||
});
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.key !== this.state.key) { // i.e. When tab has been changed
|
||||
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
|
||||
|
@ -502,7 +505,9 @@ export default class MainPage extends React.Component {
|
|||
this.setState({loginQueue});
|
||||
}
|
||||
|
||||
handleLoginCancel = () => {
|
||||
handleLoginCancel = (request) => {
|
||||
ipcRenderer.send('login-cancel', request);
|
||||
|
||||
const loginQueue = this.state.loginQueue;
|
||||
loginQueue.shift();
|
||||
this.setState({loginQueue});
|
||||
|
@ -836,6 +841,7 @@ export default class MainPage extends React.Component {
|
|||
onLogin={this.handleLogin}
|
||||
onCancel={this.handleLoginCancel}
|
||||
/>
|
||||
<PermissionModal/>
|
||||
<SelectCertificateModal
|
||||
certificateRequests={this.state.certificateRequests}
|
||||
onSelect={this.handleSelectCertificate}
|
||||
|
|
151
src/browser/components/PermissionModal.jsx
Normal file
151
src/browser/components/PermissionModal.jsx
Normal file
|
@ -0,0 +1,151 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
/* eslint-disable react/no-set-state */
|
||||
|
||||
import React from 'react';
|
||||
import {Modal, Button} from 'react-bootstrap';
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
import {log} from 'electron-log';
|
||||
|
||||
import {BASIC_AUTH_PERMISSION, REQUEST_PERMISSION_CHANNEL, DENY_PERMISSION_CHANNEL, GRANT_PERMISSION_CHANNEL, PERMISSION_DESCRIPTION} from '../../common/permissions';
|
||||
|
||||
import Util from '../../utils/util';
|
||||
|
||||
import ExternalLink from './externalLink.jsx';
|
||||
|
||||
function getKey(request, permission) {
|
||||
return `${request.url}:${permission}`;
|
||||
}
|
||||
|
||||
export default class PermissionModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tracker: new Map(), // permission request order is not preserved, but we won't have repetition of requests.
|
||||
current: null,
|
||||
};
|
||||
|
||||
ipcRenderer.on(REQUEST_PERMISSION_CHANNEL, (event, request, authInfo, permission) => {
|
||||
switch (permission) {
|
||||
case BASIC_AUTH_PERMISSION:
|
||||
this.requestBasicAuthPermission(event, request, authInfo, permission);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown permission request: ${permission}`);
|
||||
ipcRenderer.send(DENY_PERMISSION_CHANNEL, request, permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
requestBasicAuthPermission(event, request, authInfo, permission) {
|
||||
const key = getKey(request, permission);
|
||||
this.requestPermission(key, request.url, permission).then(() => {
|
||||
ipcRenderer.send(GRANT_PERMISSION_CHANNEL, request.url, permission);
|
||||
ipcRenderer.sendTo(remote.getCurrentWindow().webContents.id, 'login-request', request, authInfo);
|
||||
this.loadNext();
|
||||
}).catch((err) => {
|
||||
ipcRenderer.send(DENY_PERMISSION_CHANNEL, request.url, permission, err.message);
|
||||
ipcRenderer.send('login-cancel', request);
|
||||
this.loadNext();
|
||||
});
|
||||
}
|
||||
|
||||
requestPermission(key, url, permission) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tracker = new Map(this.state.tracker);
|
||||
const permissionRequest = {
|
||||
grant: resolve,
|
||||
deny: () => reject(new Error(`User denied ${permission} to ${url}`)),
|
||||
url,
|
||||
permission,
|
||||
};
|
||||
tracker.set(key, permissionRequest);
|
||||
const current = this.state.current ? this.state.current : key;
|
||||
this.setState({
|
||||
tracker,
|
||||
current,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentData() {
|
||||
if (this.state.current) {
|
||||
return this.state.tracker.get(this.state.current);
|
||||
}
|
||||
return {
|
||||
grant: () => {
|
||||
const err = new Error();
|
||||
log.error(`There isn't any permission to grant access to.\n Stack trace:\n${err.stack}`);
|
||||
},
|
||||
deny: () => {
|
||||
const err = new Error();
|
||||
log.error(`There isn't any permission to deny access to.\n Stack trace:\n${err.stack}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
loadNext() {
|
||||
const tracker = new Map(this.state.tracker);
|
||||
tracker.delete(this.state.current);
|
||||
const nextKey = tracker.keys().next();
|
||||
const current = nextKey.done ? null : nextKey.value;
|
||||
this.setState({
|
||||
tracker,
|
||||
current,
|
||||
});
|
||||
}
|
||||
|
||||
getModalTitle() {
|
||||
const {permission} = this.getCurrentData();
|
||||
return `${PERMISSION_DESCRIPTION[permission]} Required`;
|
||||
}
|
||||
|
||||
getModalBody() {
|
||||
const {url, permission} = this.getCurrentData();
|
||||
const originDisplay = url ? Util.getHost(url) : 'unknown origin';
|
||||
const originLink = url ? originDisplay : '';
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission]}.`}
|
||||
</p>
|
||||
<p>
|
||||
<span>{'This request originated from '}</span>
|
||||
<ExternalLink href={originLink}>{`${originDisplay}`}</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {grant, deny} = this.getCurrentData();
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='permission-modal'
|
||||
show={Boolean(this.state.current)}
|
||||
id='requestPermissionModal'
|
||||
enforceFocus={true}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{this.getModalTitle()}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.getModalBody()}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'remove-border'}>
|
||||
<div>
|
||||
<Button
|
||||
onClick={deny}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
onClick={grant}
|
||||
>{'Accept'}</Button>
|
||||
</div>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable react/no-set-state */
|
32
src/browser/components/externalLink.jsx
Normal file
32
src/browser/components/externalLink.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ipcRenderer} from 'electron';
|
||||
|
||||
// this component is used to override some checks from the UI, leaving only to trust the protocol in case it wasn't http/s
|
||||
// it is used the same as an `a` JSX tag
|
||||
export default function ExternalLink(props) {
|
||||
const click = (e) => {
|
||||
e.preventDefault();
|
||||
let parseUrl;
|
||||
try {
|
||||
parseUrl = new URL(props.href);
|
||||
ipcRenderer.send('confirm-protocol', parseUrl.protocol, props.href);
|
||||
} catch (err) {
|
||||
console.error(`invalid url ${props.href} supplied to externallink: ${err}`);
|
||||
}
|
||||
};
|
||||
const options = {
|
||||
onClick: click,
|
||||
...props,
|
||||
};
|
||||
return (
|
||||
<a {...options}/>
|
||||
);
|
||||
}
|
||||
|
||||
ExternalLink.propTypes = {
|
||||
href: PropTypes.string.isRequired,
|
||||
};
|
|
@ -21,7 +21,7 @@
|
|||
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons {
|
||||
margin-bottom: 0;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.PermissionRequestDialog-content .PermissionRequestDialog-content-buttons > * {
|
||||
margin-right: 7px;
|
||||
|
@ -42,3 +42,10 @@
|
|||
.PermissionRequestDialog-content .PermissionRequestDialog-content-close:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.permission-modal .modal-dialog {
|
||||
max-width: 580px;
|
||||
}
|
||||
.permission-modal .remove-border {
|
||||
border: none;
|
||||
}
|
16
src/common/permissions.js
Normal file
16
src/common/permissions.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// channel types for managing permissions
|
||||
export const REQUEST_PERMISSION_CHANNEL = 'request-permission';
|
||||
export const GRANT_PERMISSION_CHANNEL = 'grant-permission';
|
||||
export const DENY_PERMISSION_CHANNEL = 'deny-permission';
|
||||
|
||||
// Permission types that can be requested
|
||||
export const BASIC_AUTH_PERMISSION = 'canBasicAuth';
|
||||
|
||||
// Permission descriptions
|
||||
export const PERMISSION_DESCRIPTION = {
|
||||
[BASIC_AUTH_PERMISSION]: 'Web Authentication',
|
||||
};
|
60
src/main.js
60
src/main.js
|
@ -10,6 +10,7 @@ import electron, {nativeTheme} from 'electron';
|
|||
import isDev from 'electron-is-dev';
|
||||
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
|
||||
import log from 'electron-log';
|
||||
import 'airbnb-js-shims/target/es2015';
|
||||
|
||||
import {protocols} from '../electron-builder.json';
|
||||
|
||||
|
@ -20,6 +21,7 @@ import upgradeAutoLaunch from './main/autoLaunch';
|
|||
import RegistryConfig from './common/config/RegistryConfig';
|
||||
import Config from './common/config';
|
||||
import CertificateStore from './main/certificateStore';
|
||||
import TrustedOriginsStore from './main/trustedOrigins';
|
||||
import createMainWindow from './main/mainWindow';
|
||||
import appMenu from './main/menus/app';
|
||||
import trayMenu from './main/menus/tray';
|
||||
|
@ -32,6 +34,7 @@ import SpellChecker from './main/SpellChecker';
|
|||
import UserActivityMonitor from './main/UserActivityMonitor';
|
||||
import Utils from './utils/util';
|
||||
import parseArgs from './main/ParseArgs';
|
||||
import {REQUEST_PERMISSION_CHANNEL, GRANT_PERMISSION_CHANNEL, DENY_PERMISSION_CHANNEL, BASIC_AUTH_PERMISSION} from './common/permissions';
|
||||
|
||||
// pull out required electron components like this
|
||||
// as not all components can be referenced before the app is ready
|
||||
|
@ -59,6 +62,7 @@ let mainWindow = null;
|
|||
let popupWindow = null;
|
||||
let hideOnStartup = null;
|
||||
let certificateStore = null;
|
||||
let trustedOriginsStore = null;
|
||||
let spellChecker = null;
|
||||
let deeplinkingUrl = null;
|
||||
let scheme = null;
|
||||
|
@ -180,11 +184,13 @@ function initializeAppEventListeners() {
|
|||
|
||||
function initializeBeforeAppReady() {
|
||||
certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
|
||||
trustedOriginsStore = new TrustedOriginsStore(path.resolve(app.getPath('userData'), 'trustedOrigins.json'));
|
||||
trustedOriginsStore.load();
|
||||
|
||||
// prevent using a different working directory, which happens on windows running after installation.
|
||||
const expectedPath = path.dirname(process.execPath);
|
||||
if (process.cwd() !== expectedPath && !isDev) {
|
||||
console.warn(`Current working directory is ${process.cwd()}, changing into ${expectedPath}`);
|
||||
log.warn(`Current working directory is ${process.cwd()}, changing into ${expectedPath}`);
|
||||
process.chdir(expectedPath);
|
||||
}
|
||||
|
||||
|
@ -219,6 +225,7 @@ function initializeBeforeAppReady() {
|
|||
function initializeInterCommunicationEventListeners() {
|
||||
ipcMain.on('reload-config', handleReloadConfig);
|
||||
ipcMain.on('login-credentials', handleLoginCredentialsEvent);
|
||||
ipcMain.on('login-cancel', handleCancelLoginEvent);
|
||||
ipcMain.on('download-url', handleDownloadURLEvent);
|
||||
ipcMain.on('notified', handleNotifiedEvent);
|
||||
ipcMain.on('update-title', handleUpdateTitleEvent);
|
||||
|
@ -229,6 +236,8 @@ function initializeInterCommunicationEventListeners() {
|
|||
ipcMain.on('get-spellchecker-locale', handleGetSpellcheckerLocaleEvent);
|
||||
ipcMain.on('reply-on-spellchecker-is-ready', handleReplyOnSpellcheckerIsReadyEvent);
|
||||
ipcMain.on('selected-client-certificate', handleSelectedCertificate);
|
||||
ipcMain.on(GRANT_PERMISSION_CHANNEL, handlePermissionGranted);
|
||||
ipcMain.on(DENY_PERMISSION_CHANNEL, handlePermissionDenied);
|
||||
|
||||
if (shouldShowTrayIcon()) {
|
||||
ipcMain.on('update-unread', handleUpdateUnreadEvent);
|
||||
|
@ -334,16 +343,16 @@ function handleSelectCertificate(event, webContents, url, list, callback) {
|
|||
function handleSelectedCertificate(event, server, cert) {
|
||||
const callback = certificateRequests.get(server);
|
||||
if (!callback) {
|
||||
console.error(`there was no callback associated with: ${server}`);
|
||||
log.error(`there was no callback associated with: ${server}`);
|
||||
return;
|
||||
}
|
||||
if (typeof cert === 'undefined') {
|
||||
console.log('user canceled certificate selection');
|
||||
log.info('user canceled certificate selection');
|
||||
} else {
|
||||
try {
|
||||
callback(cert);
|
||||
} catch (e) {
|
||||
console.log(`There was a problem using the selected certificate: ${e}`);
|
||||
log.error(`There was a problem using the selected certificate: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -363,7 +372,7 @@ function handleAppCertificateError(event, webContents, url, error, certificate,
|
|||
|
||||
// if we are already showing that error, don't add more dialogs
|
||||
if (certificateErrorCallbacks.has(errorID)) {
|
||||
console.log(`Ignoring already shown dialog for ${errorID}`);
|
||||
log.warn(`Ignoring already shown dialog for ${errorID}`);
|
||||
certificateErrorCallbacks.set(errorID, callback);
|
||||
return;
|
||||
}
|
||||
|
@ -417,11 +426,24 @@ function handleAppGPUProcessCrashed(event, killed) {
|
|||
|
||||
function handleAppLogin(event, webContents, request, authInfo, callback) {
|
||||
event.preventDefault();
|
||||
if (!isTrustedURL(request.url)) {
|
||||
return;
|
||||
const parsedURL = new URL(request.url);
|
||||
const server = Utils.getServer(parsedURL, config.teams);
|
||||
|
||||
loginCallbackMap.set(request.url, typeof callback === 'undefined' ? null : callback); // if callback is undefined set it to null instead so we know we have set it up with no value
|
||||
if (isTrustedURL(request.url) || isCustomLoginURL(parsedURL, server) || trustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
|
||||
mainWindow.webContents.send('login-request', request, authInfo);
|
||||
} else {
|
||||
mainWindow.webContents.send(REQUEST_PERMISSION_CHANNEL, request, authInfo, BASIC_AUTH_PERMISSION);
|
||||
}
|
||||
loginCallbackMap.set(JSON.stringify(request), callback);
|
||||
mainWindow.webContents.send('login-request', request, authInfo);
|
||||
}
|
||||
|
||||
function handlePermissionGranted(event, url, permission) {
|
||||
trustedOriginsStore.addPermission(url, permission);
|
||||
trustedOriginsStore.save();
|
||||
}
|
||||
|
||||
function handlePermissionDenied(event, url, permission, reason) {
|
||||
log.warn(`Permission request denied: ${reason}`);
|
||||
}
|
||||
|
||||
function handleAppWillFinishLaunching() {
|
||||
|
@ -808,10 +830,20 @@ function initializeAfterAppReady() {
|
|||
//
|
||||
|
||||
function handleLoginCredentialsEvent(event, request, user, password) {
|
||||
const callback = loginCallbackMap.get(JSON.stringify(request));
|
||||
const callback = loginCallbackMap.get(request.url);
|
||||
if (typeof callback === 'undefined') {
|
||||
log.error(`Failed to retrieve login callback for ${request.url}`);
|
||||
return;
|
||||
}
|
||||
if (callback != null) {
|
||||
callback(user, password);
|
||||
}
|
||||
loginCallbackMap.delete(request.url);
|
||||
}
|
||||
|
||||
function handleCancelLoginEvent(event, request) {
|
||||
log.info(`Cancelling request for ${request ? request.url : 'unknown'}`);
|
||||
handleLoginCredentialsEvent(event, request); // we use undefined to cancel the request
|
||||
}
|
||||
|
||||
function handleDownloadURLEvent(event, url) {
|
||||
|
@ -821,7 +853,7 @@ function handleDownloadURLEvent(event, url) {
|
|||
type: 'error',
|
||||
message: err.toString(),
|
||||
});
|
||||
console.log(err);
|
||||
log.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -924,11 +956,11 @@ function handleUpdateDictionaryEvent(_, localeSelected) {
|
|||
path.resolve(app.getAppPath(), 'node_modules/simple-spellchecker/dict'),
|
||||
(err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
log.error(err);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('couldn\'t load a spellchecker for locale');
|
||||
log.error('couldn\'t load a spellchecker for locale');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -989,7 +1021,6 @@ function handleMainWindowWebContentsCrashed() {
|
|||
function isTrustedURL(url) {
|
||||
const parsedURL = Utils.parseURL(url);
|
||||
if (!parsedURL) {
|
||||
console.log('not an url');
|
||||
return false;
|
||||
}
|
||||
return Utils.getServer(parsedURL, config.teams) !== null;
|
||||
|
@ -1121,7 +1152,6 @@ function wasUpdated(lastAppVersion) {
|
|||
|
||||
function clearAppCache() {
|
||||
if (mainWindow) {
|
||||
console.log('Clear cache after update');
|
||||
mainWindow.webContents.session.clearCache().then(mainWindow.reload);
|
||||
} else {
|
||||
//Wait for mainWindow
|
||||
|
|
|
@ -91,6 +91,17 @@ const certificateStoreSchema = Joi.object().pattern(
|
|||
})
|
||||
);
|
||||
|
||||
const originPermissionsSchema = Joi.object().keys({
|
||||
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
|
||||
});
|
||||
|
||||
const trustedOriginsSchema = Joi.object({}).pattern(
|
||||
Joi.string().uri(),
|
||||
Joi.object().keys({
|
||||
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
|
||||
}),
|
||||
);
|
||||
|
||||
const allowedProtocolsSchema = Joi.array().items(Joi.string().regex(/^[a-z-]+:$/i));
|
||||
|
||||
// validate bounds_info.json
|
||||
|
@ -165,6 +176,16 @@ export function validateAllowedProtocols(data) {
|
|||
return validateAgainstSchema(data, allowedProtocolsSchema);
|
||||
}
|
||||
|
||||
export function validateTrustedOriginsStore(data) {
|
||||
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
return validateAgainstSchema(jsonData, trustedOriginsSchema);
|
||||
}
|
||||
|
||||
export function validateOriginPermissions(data) {
|
||||
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
return validateAgainstSchema(jsonData, originPermissionsSchema);
|
||||
}
|
||||
|
||||
function validateAgainstSchema(data, schema) {
|
||||
if (typeof data !== 'object') {
|
||||
console.error(`Input 'data' is not an object we can validate: ${typeof data}`);
|
||||
|
|
102
src/main/trustedOrigins.js
Normal file
102
src/main/trustedOrigins.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
'use strict';
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import log from 'electron-log';
|
||||
|
||||
import Utils from '../utils/util.js';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
export default class TrustedOriginsStore {
|
||||
constructor(storeFile) {
|
||||
this.storeFile = storeFile;
|
||||
}
|
||||
|
||||
// don't use this, is for ease of mocking it on testing
|
||||
readFromFile = () => {
|
||||
let storeData;
|
||||
try {
|
||||
storeData = fs.readFileSync(this.storeFile, 'utf-8');
|
||||
} catch (e) {
|
||||
storeData = null;
|
||||
}
|
||||
return storeData;
|
||||
}
|
||||
|
||||
load = () => {
|
||||
const storeData = this.readFromFile();
|
||||
let result = {};
|
||||
if (storeData !== null) {
|
||||
result = Validator.validateTrustedOriginsStore(storeData);
|
||||
if (!result) {
|
||||
throw new Error('Provided TrustedOrigins file does not validate, using defaults instead.');
|
||||
}
|
||||
}
|
||||
this.data = new Map(Object.entries(result));
|
||||
}
|
||||
|
||||
// don't use this, is for ease of mocking it on testing
|
||||
saveToFile(stringMap) {
|
||||
fs.writeFileSync(this.storeFile, stringMap);
|
||||
}
|
||||
|
||||
save = () => {
|
||||
this.saveToFile(JSON.stringify(Object.fromEntries((this.data.entries())), null, ' '));
|
||||
};
|
||||
|
||||
// if permissions or targetUrl are invalid, this function will throw an error
|
||||
// this function stablishes all the permissions at once, overwriting whatever was before
|
||||
// to enable just one permission use addPermission instead.
|
||||
set = (targetURL, permissions) => {
|
||||
const validPermissions = Validator.validateOriginPermissions(permissions);
|
||||
if (!validPermissions) {
|
||||
throw new Error(`Invalid permissions set for trusting ${targetURL}`);
|
||||
}
|
||||
this.data.set(Utils.getHost(targetURL), validPermissions);
|
||||
};
|
||||
|
||||
// enables usage of `targetURL` for `permission`
|
||||
addPermission = (targetURL, permission) => {
|
||||
const origin = Utils.getHost(targetURL);
|
||||
const currentPermissions = this.data.get(origin) || {};
|
||||
currentPermissions[permission] = true;
|
||||
this.set(origin, currentPermissions);
|
||||
}
|
||||
|
||||
delete = (targetURL) => {
|
||||
let host;
|
||||
try {
|
||||
host = Utils.getHost(targetURL);
|
||||
this.data.delete(host);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isExisting = (targetURL) => {
|
||||
return (typeof this.data.get(Utils.getHost(targetURL)) !== 'undefined');
|
||||
};
|
||||
|
||||
// if user hasn't set his preferences, it will return null (falsy)
|
||||
checkPermission = (targetURL, permission) => {
|
||||
if (!permission) {
|
||||
log.error(`Missing permission request on ${targetURL}`);
|
||||
return null;
|
||||
}
|
||||
let origin;
|
||||
try {
|
||||
origin = Utils.getHost(targetURL);
|
||||
} catch (e) {
|
||||
log.error(`invalid host to retrieve permissions: ${targetURL}: ${e}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const urlPermissions = this.data.get(origin);
|
||||
return urlPermissions ? urlPermissions[permission] : null;
|
||||
}
|
||||
}
|
|
@ -34,6 +34,14 @@ function parseURL(inputURL) {
|
|||
}
|
||||
}
|
||||
|
||||
function getHost(inputURL) {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (parsedURL) {
|
||||
return parsedURL.origin;
|
||||
}
|
||||
throw new Error(`Couldn't parse url: ${inputURL}`);
|
||||
}
|
||||
|
||||
// isInternalURL determines if the target url is internal to the application.
|
||||
// - currentURL is the current url inside the webview
|
||||
// - basename is the global export from the Mattermost application defining the subpath, if any
|
||||
|
@ -183,4 +191,5 @@ export default {
|
|||
isPluginUrl,
|
||||
getDisplayBoundaries,
|
||||
dispatchNotification,
|
||||
getHost,
|
||||
};
|
||||
|
|
99
test/specs/main/trusted_origins_test.js
Normal file
99
test/specs/main/trusted_origins_test.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import assert from 'assert';
|
||||
import 'airbnb-js-shims/target/es2015';
|
||||
|
||||
import TrustedOriginsStore from '../../../src/main/trustedOrigins.js';
|
||||
import {BASIC_AUTH_PERMISSION} from '../../../src/common/permissions.js';
|
||||
|
||||
function mockTOS(fileName, returnvalue) {
|
||||
const tos = new TrustedOriginsStore(fileName);
|
||||
tos.readFromFile = () => {
|
||||
return returnvalue;
|
||||
};
|
||||
return tos;
|
||||
}
|
||||
|
||||
describe('Trusted Origins', () => {
|
||||
describe('validate load', () => {
|
||||
it('should be empty if there is no file', () => {
|
||||
const tos = mockTOS('emptyfile', null);
|
||||
tos.load();
|
||||
assert.deepEqual(tos.data.size, 0);
|
||||
});
|
||||
|
||||
it('should throw an error if data isn\'t an object', () => {
|
||||
const tos = mockTOS('notobject', 'this is not my object!');
|
||||
|
||||
assert.throws(tos.load, SyntaxError);
|
||||
});
|
||||
|
||||
it('should throw an error if data isn\'t in the expected format', () => {
|
||||
const tos = mockTOS('badobject', '{"https://mattermost.com": "this is not my object!"}');
|
||||
assert.throws(tos.load, /^Error: Provided TrustedOrigins file does not validate, using defaults instead\.$/);
|
||||
});
|
||||
|
||||
it('should drop keys that aren\'t urls', () => {
|
||||
const tos = mockTOS('badobject2', `{"this is not an uri": {"${BASIC_AUTH_PERMISSION}": true}}`);
|
||||
tos.load();
|
||||
assert.equal(typeof tos.data['this is not an uri'], 'undefined');
|
||||
});
|
||||
|
||||
it('should contain valid data if everything goes right', () => {
|
||||
const value = {
|
||||
'https://mattermost.com': {
|
||||
[BASIC_AUTH_PERMISSION]: true,
|
||||
}};
|
||||
const tos = mockTOS('okfile', JSON.stringify(value));
|
||||
tos.load();
|
||||
assert.deepEqual(Object.fromEntries(tos.data.entries()), value);
|
||||
});
|
||||
});
|
||||
describe('validate testing permissions', () => {
|
||||
const value = {
|
||||
'https://mattermost.com': {
|
||||
[BASIC_AUTH_PERMISSION]: true,
|
||||
},
|
||||
'https://notmattermost.com': {
|
||||
[BASIC_AUTH_PERMISSION]: false,
|
||||
},
|
||||
};
|
||||
const tos = mockTOS('permission_test', JSON.stringify(value));
|
||||
tos.load();
|
||||
it('tos should contain 2 elements', () => {
|
||||
assert.equal(tos.data.size, 2);
|
||||
});
|
||||
it('should say ok if the permission is set', () => {
|
||||
assert.equal(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION), true);
|
||||
});
|
||||
it('should say ko if the permission is set to false', () => {
|
||||
assert.equal(tos.checkPermission('https://notmattermost.com', BASIC_AUTH_PERMISSION), false);
|
||||
});
|
||||
it('should say ko if the uri is not set', () => {
|
||||
assert.equal(tos.checkPermission('https://undefined.com', BASIC_AUTH_PERMISSION), null);
|
||||
});
|
||||
it('should say null if the permission is unknown', () => {
|
||||
assert.equal(tos.checkPermission('https://mattermost.com'), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate deleting permissions', () => {
|
||||
const value = {
|
||||
'https://mattermost.com': {
|
||||
[BASIC_AUTH_PERMISSION]: true,
|
||||
},
|
||||
'https://notmattermost.com': {
|
||||
[BASIC_AUTH_PERMISSION]: false,
|
||||
},
|
||||
};
|
||||
const tos = mockTOS('permission_test', JSON.stringify(value));
|
||||
tos.load();
|
||||
it('deleting revokes access', () => {
|
||||
assert.equal(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION), true);
|
||||
tos.delete('https://mattermost.com');
|
||||
assert.equal(tos.checkPermission('https://mattermost.com', BASIC_AUTH_PERMISSION), null);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -92,4 +92,16 @@ describe('Utils', () => {
|
|||
assert.equal(Utils.isInternalURL(targetURL, currentURL, basename), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHost', () => {
|
||||
it('should return the origin of a well formed url', () => {
|
||||
const myurl = 'https://mattermost.com/download';
|
||||
assert.equal(Utils.getHost(myurl), 'https://mattermost.com');
|
||||
});
|
||||
|
||||
it('shoud raise an error on malformed urls', () => {
|
||||
const myurl = 'http://example.com:-80/';
|
||||
assert.throws(() => Utils.getHost(myurl), Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,6 +18,17 @@ module.exports = merge(base, {
|
|||
path: path.join(__dirname, 'src'),
|
||||
filename: '[name]_bundle.js',
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.js?$/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
include: ['@babel/plugin-proposal-class-properties']
|
||||
}
|
||||
},
|
||||
}],
|
||||
},
|
||||
node: {
|
||||
__filename: true,
|
||||
__dirname: true,
|
||||
|
|
Loading…
Reference in a new issue