[MM-21192] allow for certificate selection (#1148)
* wip * first working version * kinda show the cert * fix lint * wip * wip * css fixes, cleanup * fix lint * get back to normal op * get back cert name * wip, added cert info screen * queue cert requests, move user to cert request team * certifications queued * remove unneded import * remove log * remove commented code * cleanup * remove trust certificate dialog for showing it * fix CR comments
This commit is contained in:
parent
9f2c1b6f86
commit
33e24030d1
|
@ -26,6 +26,7 @@ import TabBar from './TabBar.jsx';
|
|||
import HoveringURL from './HoveringURL.jsx';
|
||||
import Finder from './Finder.jsx';
|
||||
import NewTeamModal from './NewTeamModal.jsx';
|
||||
import SelectCertificateModal from './SelectCertificateModal.jsx';
|
||||
|
||||
export default class MainPage extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -50,6 +51,7 @@ export default class MainPage extends React.Component {
|
|||
mentionAtActiveCounts: new Array(this.props.teams.length),
|
||||
loginQueue: [],
|
||||
targetURL: '',
|
||||
certificateRequests: [],
|
||||
maximized: false,
|
||||
};
|
||||
}
|
||||
|
@ -136,6 +138,20 @@ export default class MainPage extends React.Component {
|
|||
});
|
||||
});
|
||||
|
||||
ipcRenderer.on('select-user-certificate', (_, origin, certificateList) => {
|
||||
const certificateRequests = self.state.certificateRequests;
|
||||
certificateRequests.push({
|
||||
server: origin,
|
||||
certificateList,
|
||||
});
|
||||
self.setState({
|
||||
certificateRequests,
|
||||
});
|
||||
if (certificateRequests.length === 1) {
|
||||
self.switchToTabForCertificateRequest(origin);
|
||||
}
|
||||
});
|
||||
|
||||
// can't switch tabs sequentially for some reason...
|
||||
ipcRenderer.on('switch-tab', (event, key) => {
|
||||
this.handleSelect(key);
|
||||
|
@ -341,6 +357,18 @@ export default class MainPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
switchToTabForCertificateRequest = (origin) => {
|
||||
// origin is server name + port, if the port doesn't match the protocol, it is kept by URL
|
||||
const originURL = new URL(`http://${origin.split(':')[0]}`);
|
||||
const secureOriginURL = new URL(`https://${origin.split(':')[0]}`);
|
||||
|
||||
const key = this.props.teams.findIndex((team) => {
|
||||
const parsedURL = new URL(team.url);
|
||||
return (parsedURL.origin === originURL.origin) || (parsedURL.origin === secureOriginURL.origin);
|
||||
});
|
||||
this.handleSelect(key);
|
||||
};
|
||||
|
||||
handleMaximizeState = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
this.setState({maximized: win.isMaximized()});
|
||||
|
@ -534,6 +562,24 @@ export default class MainPage extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
handleSelectCertificate = (certificate) => {
|
||||
const certificateRequests = this.state.certificateRequests;
|
||||
const current = certificateRequests.shift();
|
||||
this.setState({certificateRequests});
|
||||
ipcRenderer.send('selected-client-certificate', current.server, certificate);
|
||||
if (certificateRequests.length > 0) {
|
||||
this.switchToTabForCertificateRequest(certificateRequests[0].server);
|
||||
}
|
||||
}
|
||||
handleCancelCertificate = () => {
|
||||
const certificateRequests = this.state.certificateRequests;
|
||||
const current = certificateRequests.shift();
|
||||
this.setState({certificateRequests});
|
||||
ipcRenderer.send('selected-client-certificate', current.server);
|
||||
if (certificateRequests.length > 0) {
|
||||
this.switchToTabForCertificateRequest(certificateRequests[0].server);
|
||||
}
|
||||
};
|
||||
setDarkMode() {
|
||||
this.setState({
|
||||
isDarkMode: this.props.setDarkMode(),
|
||||
|
@ -727,6 +773,11 @@ export default class MainPage extends React.Component {
|
|||
onLogin={this.handleLogin}
|
||||
onCancel={this.handleLoginCancel}
|
||||
/>
|
||||
<SelectCertificateModal
|
||||
certificateRequests={this.state.certificateRequests}
|
||||
onSelect={this.handleSelectCertificate}
|
||||
onCancel={this.handleCancelCertificate}
|
||||
/>
|
||||
<Grid fluid={true}>
|
||||
{ topRow }
|
||||
{ viewsRow }
|
||||
|
|
177
src/browser/components/SelectCertificateModal.jsx
Normal file
177
src/browser/components/SelectCertificateModal.jsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, Table, Row, Col} from 'react-bootstrap';
|
||||
|
||||
import ShowCertificateModal from './showCertificateModal.jsx';
|
||||
|
||||
const CELL_SIZE = 23;
|
||||
const ELIPSIS_SIZE = 3;
|
||||
|
||||
export default class SelectCertificateModal extends React.Component {
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
certificateRequests: PropTypes.arrayOf(PropTypes.shape({
|
||||
server: PropTypes.string,
|
||||
certificateList: PropTypes.array,
|
||||
})),
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedIndex: null,
|
||||
showCertificate: null,
|
||||
};
|
||||
}
|
||||
|
||||
maxSize = (item, max) => {
|
||||
if (!item || item.length <= max) {
|
||||
return item;
|
||||
}
|
||||
const sub = item.substring(0, max - ELIPSIS_SIZE);
|
||||
return `${sub}...`;
|
||||
}
|
||||
|
||||
selectfn = (index) => {
|
||||
return (() => {
|
||||
this.setState({selectedIndex: index});
|
||||
});
|
||||
};
|
||||
|
||||
renderCert = (cert, index) => {
|
||||
const issuer = cert.issuer && cert.issuer.commonName ? cert.issuer.commonName : '';
|
||||
const subject = cert.subject && cert.subject.commonName ? cert.subject.commonName : '';
|
||||
const serial = cert.serialNumber || '';
|
||||
|
||||
const issuerShort = this.maxSize(cert.issuer.commonName, CELL_SIZE);
|
||||
const subjectShort = this.maxSize(cert.subject.commonName, CELL_SIZE);
|
||||
const serialShort = this.maxSize(cert.serialNumber, CELL_SIZE);
|
||||
|
||||
const style = this.state.selectedIndex === index ? {background: '#457AB2', color: '#FFFFFF'} : {};
|
||||
return (
|
||||
<tr
|
||||
key={`cert-${index}`}
|
||||
onClick={this.selectfn(index)}
|
||||
style={style}
|
||||
>
|
||||
<td
|
||||
style={style}
|
||||
title={issuer}
|
||||
>{issuerShort}</td>
|
||||
<td
|
||||
style={style}
|
||||
title={subject}
|
||||
>{subjectShort}</td>
|
||||
<td
|
||||
style={style}
|
||||
title={serial}
|
||||
>{serialShort}</td>
|
||||
</tr>);
|
||||
};
|
||||
|
||||
renderCerts = (certificateList) => {
|
||||
if (certificateList) {
|
||||
const certs = certificateList.map(this.renderCert);
|
||||
return (
|
||||
<Fragment>
|
||||
{certs}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
return (<Fragment><tr/><tr><td/><td>{'No certificates available'}</td><td/></tr></Fragment>);
|
||||
}
|
||||
|
||||
getSelectedCert = () => {
|
||||
return this.state.selectedIndex === null ? null : this.props.certificateRequests[0].certificateList[this.state.selectedIndex];
|
||||
};
|
||||
|
||||
handleOk = () => {
|
||||
const cert = this.getSelectedCert();
|
||||
if (cert !== null) {
|
||||
this.props.onSelect(cert);
|
||||
}
|
||||
}
|
||||
|
||||
handleCertificateInfo = () => {
|
||||
const certificate = this.getSelectedCert();
|
||||
this.setState({showCertificate: certificate});
|
||||
}
|
||||
|
||||
certificateInfoClose = () => {
|
||||
this.setState({showCertificate: null});
|
||||
}
|
||||
|
||||
render() {
|
||||
const certList = this.props.certificateRequests.length ? this.props.certificateRequests[0].certificateList : [];
|
||||
const server = this.props.certificateRequests.length ? this.props.certificateRequests[0].server : '';
|
||||
if (this.state.showCertificate) {
|
||||
return (
|
||||
<ShowCertificateModal
|
||||
certificate={this.state.showCertificate}
|
||||
onOk={this.certificateInfoClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='certificateModal'
|
||||
show={this.props.certificateRequests.length}
|
||||
>
|
||||
<Modal.Header className={'noBorder'}>
|
||||
<Modal.Title className={'bottomBorder'}>{'Select a certificate'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p className={'subtitle'}>{`Select a certificate to authenticate yourself to ${server}`}</p>
|
||||
<Table
|
||||
stripped={'true'}
|
||||
hover={true}
|
||||
size={'sm'}
|
||||
className='certificateList'
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span className={'divider'}>{'Subject'}</span></th>
|
||||
<th><span className={'divider'}>{'Issuer'}</span></th>
|
||||
<th>{'Serial'}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderCerts(certList)}
|
||||
<tr/* this is to correct table height without affecting real rows *//>
|
||||
</tbody>
|
||||
</Table>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'noBorder'}>
|
||||
<Row className={'topBorder'}>
|
||||
<Col sm={4}>
|
||||
<Button
|
||||
variant={'info'}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
onClick={this.handleCertificateInfo}
|
||||
className={'info'}
|
||||
>{'Certificate Information'}</Button>
|
||||
</Col>
|
||||
<Col sm={8}>
|
||||
<Button
|
||||
onClick={this.props.onCancel}
|
||||
variant={'secondary'}
|
||||
className={'secondary'}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
className={'primary'}
|
||||
>{'Ok'}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
97
src/browser/components/showCertificateModal.jsx
Normal file
97
src/browser/components/showCertificateModal.jsx
Normal file
|
@ -0,0 +1,97 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, Row} from 'react-bootstrap';
|
||||
|
||||
export default class ShowCertificateModal extends React.Component {
|
||||
static propTypes = {
|
||||
certificate: PropTypes.object,
|
||||
onOk: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
certificate: props.certificate,
|
||||
};
|
||||
}
|
||||
|
||||
handleOk = () => {
|
||||
this.setState({certificate: null});
|
||||
this.props.onOk();
|
||||
}
|
||||
|
||||
render() {
|
||||
const certificateItem = (descriptor, value) => {
|
||||
const ddclass = value ? '' : 'emtpyDescriptor';
|
||||
const val = value ? `${value}` : <span/>;
|
||||
return (
|
||||
<Fragment>
|
||||
<dt>{descriptor}</dt>
|
||||
<dd className={ddclass}>{val}</dd>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
if (this.state.certificate === null) {
|
||||
return (
|
||||
<Modal>
|
||||
<Modal.Body>
|
||||
{'No certificate Selected'}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const utcSeconds = (date) => {
|
||||
const d = new Date(0);
|
||||
d.setUTCSeconds(date);
|
||||
return d;
|
||||
};
|
||||
|
||||
const expiration = utcSeconds(this.state.certificate.validExpiry);
|
||||
const creation = utcSeconds(this.state.certificate.validStart);
|
||||
const dateDisplayOptions = {dateStyle: 'full', timeStyle: 'full'};
|
||||
const dateLocale = 'en-US';
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='certificateModal'
|
||||
show={this.state.certificate !== null}
|
||||
scrollable={true}
|
||||
>
|
||||
<Modal.Header className={'noBorder'}>
|
||||
<Modal.Title className={'bottomBorder'}>{'Certificate Information'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<h3 className={'certificateKey'}>{`${this.state.certificate.subject.commonName}`}</h3>
|
||||
<p className={'certInfo'}>{`Issued by: ${this.state.certificate.issuer.commonName}`}</p>
|
||||
<p className={'certInfo'}>{`Expires: ${expiration.toLocaleString(dateLocale, dateDisplayOptions)}`}</p>
|
||||
<p>{'Details'}</p>
|
||||
<dl>
|
||||
{certificateItem('Subject Name')}
|
||||
{certificateItem('Common Name', this.state.certificate.subject.commonName)}
|
||||
{certificateItem('Issuer Name')}
|
||||
{certificateItem('Common Name', this.state.certificate.issuer.commonName)}
|
||||
{certificateItem('Serial Number', this.state.certificate.serialNumber)}
|
||||
{certificateItem('Not Valid Before', creation.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
{certificateItem('Not Valid After', expiration.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
{certificateItem('Public Key Info')}
|
||||
{certificateItem('Algorithm', this.state.certificate.fingerprint.split('/')[0])}
|
||||
</dl>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'noBorder'}>
|
||||
<Row className={'topBorder'}>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
className={'primary'}
|
||||
>{'Close'}</Button>
|
||||
</Row>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
121
src/browser/css/components/CertificateModal.css
Normal file
121
src/browser/css/components/CertificateModal.css
Normal file
|
@ -0,0 +1,121 @@
|
|||
.certificateModal dialog {
|
||||
background-color: aliceblue;
|
||||
}
|
||||
|
||||
.certificateList thead {
|
||||
width: 557.89px;
|
||||
height: 22px;
|
||||
}
|
||||
.certificateList thead>tr>th {
|
||||
font-family: Helvetica Neue;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
padding: 2px;
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.certificateList tbody>tr>td {
|
||||
width: 227.17px;
|
||||
height: 47px;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
table.certificateList {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CCCCCC;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0px 1px 1px rgba(0, 0, 0, 0.0008);
|
||||
border-radius: 4px;
|
||||
border-collapse: unset;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.certificateModal button {
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #CCCCCC;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
padding: 9px 12px;
|
||||
}
|
||||
|
||||
.certificateModal button:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.certificateModal button.primary {
|
||||
background: #457AB2;
|
||||
color: #FFFFFF;
|
||||
border: 1px solid #2E6DA4;
|
||||
}
|
||||
.certificateModal button.info {
|
||||
color: #457AB2;
|
||||
}
|
||||
|
||||
.certificateModal button.primary:hover {
|
||||
background: #659AD2;
|
||||
}
|
||||
|
||||
.certificateModal .subtitle {
|
||||
color: #737373;
|
||||
margin: 0px 15px 10px;
|
||||
}
|
||||
|
||||
.certificateModal .bottomBorder {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
}
|
||||
|
||||
.certificateModal .topBorder {
|
||||
border-top: 1px solid #CCCCCC;
|
||||
margin: 0 1px 0 1px;
|
||||
padding-top: 15px;
|
||||
}
|
||||
.certificateModal .noBorder {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-right: 1px solid #CCCCCC;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.certificateModal dt, dd {
|
||||
float: left;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.certificateModal dt { clear:both }
|
||||
|
||||
.certificateModal dl {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.certificateKey {
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
|
||||
color: #737373;
|
||||
}
|
||||
|
||||
.certInfo {
|
||||
font-family: Helvetica Neue;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.emtpyDescriptor {
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
}
|
|
@ -8,3 +8,4 @@
|
|||
@import url("TeamListItem.css");
|
||||
@import url("Finder.css");
|
||||
@import url("UpdaterPage.css");
|
||||
@import url("CertificateModal.css");
|
31
src/main.js
31
src/main.js
|
@ -50,6 +50,7 @@ const {
|
|||
const criticalErrorHandler = new CriticalErrorHandler();
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
const loginCallbackMap = new Map();
|
||||
const certificateRequests = new Map();
|
||||
const userActivityMonitor = new UserActivityMonitor();
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
|
@ -158,6 +159,7 @@ function initializeAppEventListeners() {
|
|||
app.on('activate', handleAppActivate);
|
||||
app.on('before-quit', handleAppBeforeQuit);
|
||||
app.on('certificate-error', handleAppCertificateError);
|
||||
app.on('select-client-certificate', handleSelectCertificate);
|
||||
app.on('gpu-process-crashed', handleAppGPUProcessCrashed);
|
||||
app.on('login', handleAppLogin);
|
||||
app.on('will-finish-launching', handleAppWillFinishLaunching);
|
||||
|
@ -214,6 +216,8 @@ function initializeInterCommunicationEventListeners() {
|
|||
ipcMain.on('get-spelling-suggestions', handleGetSpellingSuggestionsEvent);
|
||||
ipcMain.on('get-spellchecker-locale', handleGetSpellcheckerLocaleEvent);
|
||||
ipcMain.on('reply-on-spellchecker-is-ready', handleReplyOnSpellcheckerIsReadyEvent);
|
||||
ipcMain.on('selected-client-certificate', handleSelectedCertificate);
|
||||
|
||||
if (shouldShowTrayIcon()) {
|
||||
ipcMain.on('update-unread', handleUpdateUnreadEvent);
|
||||
}
|
||||
|
@ -307,6 +311,32 @@ function handleAppBeforeQuit() {
|
|||
global.willAppQuit = true;
|
||||
}
|
||||
|
||||
function handleSelectCertificate(event, webContents, url, list, callback) {
|
||||
event.preventDefault(); // prevent the app from getting the first certificate available
|
||||
// store callback so it can be called with selected certificate
|
||||
certificateRequests.set(url, callback);
|
||||
|
||||
// open modal for selecting certificate
|
||||
mainWindow.webContents.send('select-user-certificate', url, list);
|
||||
}
|
||||
|
||||
function handleSelectedCertificate(event, server, cert) {
|
||||
const callback = certificateRequests.get(server);
|
||||
if (!callback) {
|
||||
console.error(`there was no callback associated with: ${server}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (typeof cert === 'undefined') {
|
||||
callback(); //user cancelled, so we use the callback without certificate.
|
||||
} else {
|
||||
callback(cert);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`There was a problem using the selected certificate: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppCertificateError(event, webContents, url, error, certificate, callback) {
|
||||
if (certificateStore.isTrusted(url, certificate)) {
|
||||
event.preventDefault();
|
||||
|
@ -912,6 +942,7 @@ function isTrustedURL(url) {
|
|||
if (!parsedURL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const teamURLs = config.teams.reduce((urls, team) => {
|
||||
const parsedTeamURL = parseURL(team.url);
|
||||
if (parsedTeamURL) {
|
||||
|
|
Loading…
Reference in a new issue