Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show Client Certificate paths #2714

Merged
merged 2 commits into from Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,6 +1,7 @@
import { autoBindMethodsForReact } from 'class-autobind-decorator';
import React, { PureComponent } from 'react';
import React, { FC, PureComponent, ReactNode } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import styled from 'styled-components';

import { AUTOBIND_CFG } from '../../../common/constants';
import { getWorkspaceLabel } from '../../../common/get-workspace-label';
Expand All @@ -19,8 +20,44 @@ import ModalHeader from '../base/modal-header';
import PromptButton from '../base/prompt-button';
import HelpTooltip from '../help-tooltip';
import MarkdownEditor from '../markdown-editor';
import { PasswordViewer } from '../viewers/password-viewer';
import { showWorkspaceDuplicateModal } from './workspace-duplicate-modal';

const CertificateFields = styled.div({
display: 'flex',
flexDirection: 'column',
margin: 'var(--padding-sm) 0',
});

const CertificateField: FC<{
title: string;
value: string | null;
privateText?: boolean;
optional?: boolean;
}> = ({
title,
value,
privateText,
optional,
}) => {
if (optional && value === null) {
return null;
}

let display: ReactNode = value;
if (privateText) {
display = <PasswordViewer text={value} />;
} else {
display = <span className="monospace selectable">{value}</span>;
}

return (
<span className="pad-right no-wrap">
<strong>{title}:</strong>{' '}{display}
</span>
);
};

interface Props {
clientCertificates: ClientCertificate[];
workspace: Workspace;
Expand Down Expand Up @@ -214,55 +251,38 @@ class WorkspaceSettingsModal extends PureComponent<Props, State> {

renderCertificate(certificate: ClientCertificate) {
return (
<div key={certificate._id}>
<div className="row-spaced">
<div>
<span className="pad-right no-wrap">
<strong>PFX:</strong>{' '}
{certificate.pfx ? <i className="fa fa-check" /> : <i className="fa fa-remove" />}
</span>
<span className="pad-right no-wrap">
<strong>CRT:</strong>{' '}
{certificate.cert ? <i className="fa fa-check" /> : <i className="fa fa-remove" />}
</span>
<span className="pad-right no-wrap">
<strong>Key:</strong>{' '}
{certificate.key ? <i className="fa fa-check" /> : <i className="fa fa-remove" />}
</span>
<span className="pad-right no-wrap" title={certificate.passphrase || undefined}>
<strong>Passphrase:</strong>{' '}
{certificate.passphrase ? (
<i className="fa fa-check" />
) : (
<i className="fa fa-remove" />
)}
</span>
<span className="pad-right">
<strong>Host:</strong>{' '}
<span className="monospace selectable">{certificate.host}</span>
</span>
</div>
<div className="no-wrap">
<button
className="btn btn--super-compact width-auto"
title="Enable or disable certificate"
onClick={() => WorkspaceSettingsModal._handleToggleCertificate(certificate)}
>
{certificate.disabled ? (
<i className="fa fa-square-o" />
) : (
<i className="fa fa-check-square-o" />
)}
</button>
<PromptButton
className="btn btn--super-compact width-auto"
confirmMessage=""
addIcon
onClick={() => WorkspaceSettingsModal._handleDeleteCertificate(certificate)}
>
<i className="fa fa-trash-o" />
</PromptButton>
</div>
<div className="row-spaced" key={certificate._id}>
<CertificateFields>
<CertificateField title="Host" value={certificate.host} />
{certificate.pfx ? (
<CertificateField title="PFX" value={certificate.pfx} />
) : (
<CertificateField title="CRT" value={certificate.cert} />
)}
<CertificateField title="Key" value={certificate.key} optional />
<CertificateField title="Passphrase" value={certificate.passphrase} privateText optional />
</CertificateFields>

<div className="no-wrap">
<button
className="btn btn--super-compact width-auto"
title="Enable or disable certificate"
onClick={() => WorkspaceSettingsModal._handleToggleCertificate(certificate)}
>
{certificate.disabled ? (
<i className="fa fa-square-o" />
) : (
<i className="fa fa-check-square-o" />
)}
</button>
<PromptButton
className="btn btn--super-compact width-auto"
confirmMessage=""
addIcon
onClick={() => WorkspaceSettingsModal._handleDeleteCertificate(certificate)}
>
<i className="fa fa-trash-o" />
</PromptButton>
</div>
</div>
);
Expand Down
@@ -0,0 +1,58 @@
import React, { FC, useCallback, useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';

import { selectSettings } from '../../redux/selectors';

const EyeIcon = styled.i({
cursor: 'pointer',
paddingRight: 'var(--padding-xs)',
});

const MASK_CHARACTER = '•';
/** randomly get anywhere between 4 and 11 mask characters on each invocation */
const getMask = () => MASK_CHARACTER.repeat(4 + (Math.random() * 7));

export const PasswordViewer: FC<{
onShow?: () => void;
initiallyHidden?: boolean;
text: string | null;

/**
* If true, between 4 and 11 mask characters (e.g. '••••••••') will be shown in place of the password (immediately after the show/hide button).
* If false, nothing is rendered after the show/hide button.
*
* @default true
* @note the number is random to avoid exposing the length of the password.
*/
maskText?: boolean;
}> = ({
onShow,
initiallyHidden = true,
text,
maskText = true,
}) => {
const { showPasswords } = useSelector(selectSettings);
const [mask, setMask] = useState<string | null>(null);
useEffect(() => {
if (maskText) {
setMask(getMask());
}
}, [maskText]);

const [textVisible, setTextVisible] = useState(showPasswords ?? initiallyHidden);
const toggleVisible = useCallback(() => {
onShow?.();
setTextVisible(!textVisible);
}, [textVisible, onShow]);

return (
<span className="monospace">
<EyeIcon
className={`fa ${textVisible ? 'fa-eye' : 'fa-eye-slash'}`}
onClick={toggleVisible}
/>
{textVisible ? <span className="selectable">{text}</span> : mask}
</span>
);
};