Skip to content

Commit

Permalink
Add passphrase input to the add SSH key dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
vinokurig authored and ibuziuk committed Sep 16, 2024
1 parent 9c41f1b commit aff295c
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/common/src/dto/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export type SshKey = {
* base64 encoded public key.
*/
keyPub: string;
passphrase?: string;
};
export type NewSshKey = Omit<SshKey, 'creationTimestamp'> & {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('Helpers for SSH Keys API', () => {
'dwo_ssh_key.pub': btoa('ssh-key-pub-data'),
dwo_ssh_key: btoa('ssh-key-data'),
ssh_config: btoa('git-config'),
passphrase: '',
},
};

Expand Down Expand Up @@ -125,4 +126,31 @@ describe('Helpers for SSH Keys API', () => {
} as SshKeySecret);
});
});
test('SSH key with passphrase', () => {
const namespace = 'user-che';
const token: api.NewSshKey = {
name: 'git-ssh-key',
key: 'ssh-key-data',
keyPub: 'ssh-key-pub-data',
passphrase: 'passphrase',
};

const secret = toSecret(namespace, token);
expect(secret).toStrictEqual({
apiVersion: 'v1',
data: {
'dwo_ssh_key.pub': token.keyPub,
dwo_ssh_key: token.key,
ssh_config: btoa(SSH_CONFIG),
passphrase: 'passphrase',
},
kind: 'Secret',
metadata: {
annotations: SSH_SECRET_ANNOTATIONS,
labels: SSH_SECRET_LABELS,
name: 'git-ssh-key',
namespace: 'user-che',
},
} as SshKeySecret);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface SshKeySecret extends k8s.V1Secret {
data: {
dwo_ssh_key: string;
'dwo_ssh_key.pub': string;
passphrase?: string;
ssh_config: string;
};
}
Expand Down Expand Up @@ -88,6 +89,7 @@ export function toSecret(namespace: string, sshKey: api.NewSshKey): SshKeySecret
'dwo_ssh_key.pub': sshKey.keyPub,
dwo_ssh_key: sshKey.key,
ssh_config: btoa(SSH_CONFIG),
...(sshKey.passphrase && { passphrase: sshKey.passphrase }),
},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SshPassphrase snapshot 1`] = `
<form
className="pf-c-form"
noValidate={true}
>
<div
className="pf-c-form__group"
>
<div
className="pf-c-form__group-label"
>
<label
className="pf-c-form__label"
htmlFor="ssh-passphrase"
>
<span
className="pf-c-form__label-text"
>
Passphrase
</span>
</label>
</div>
<div
className="pf-c-form__group-control"
>
<input
aria-invalid={false}
aria-label="Passphrase"
className="pf-c-form-control"
data-ouia-component-id="OUIA-Generated-TextInputBase-1"
data-ouia-component-type="PF4/TextInput"
data-ouia-safe={true}
disabled={false}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder="Enter passphrase (optional)"
required={false}
type="text"
/>
</div>
</div>
</form>
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (c) 2018-2024 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { Form } from '@patternfly/react-core';
import userEvent from '@testing-library/user-event';
import React from 'react';

import { SshPassphrase } from '@/pages/UserPreferences/SshKeys/AddModal/Form/SshPassphrase';
import getComponentRenderer, { screen } from '@/services/__mocks__/getComponentRenderer';

const { createSnapshot, renderComponent } = getComponentRenderer(getComponent);

const mockOnChange = jest.fn();

jest.mock('@/components/TextFileUpload');

describe('SshPassphrase', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('snapshot', () => {
const snapshot = createSnapshot();
expect(snapshot.toJSON()).toMatchSnapshot();
});

describe('text area', () => {
it('should handle SSH passphrase', () => {
renderComponent();

expect(mockOnChange).not.toHaveBeenCalled();

const input = screen.getByPlaceholderText('Enter passphrase (optional)');

const passphrase = 'passphrase';
userEvent.paste(input, passphrase);

expect(mockOnChange).toHaveBeenCalledWith(passphrase);
});

it('should handle the empty value', () => {
renderComponent();

expect(mockOnChange).not.toHaveBeenCalled();

const input = screen.getByPlaceholderText('Enter passphrase (optional)');

// fill the SSH key data field
userEvent.paste(input, 'ssh-key-data');

mockOnChange.mockClear();

// clear the SSH key data field
userEvent.clear(input);

expect(mockOnChange).toHaveBeenCalledWith('');
});
});
});

function getComponent(): React.ReactElement {
return (
<Form>
<SshPassphrase onChange={mockOnChange} />
</Form>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2018-2024 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/

import { FormGroup, TextInput } from '@patternfly/react-core';
import React from 'react';

export type Props = {
onChange: (passphrase: string) => void;
};

export type State = {
passphrase: string | undefined;
};

export class SshPassphrase extends React.Component<Props, State> {
constructor(props: Props) {
super(props);

this.state = {
passphrase: undefined,
};
}

private onChange(passphrase: string): void {
const { onChange } = this.props;

this.setState({ passphrase });
onChange(passphrase);
}

public render(): React.ReactElement {
return (
<FormGroup fieldId="ssh-passphrase" label="Passphrase" isRequired={false}>
<TextInput
aria-label="Passphrase"
placeholder="Enter passphrase (optional)"
onChange={passphrase => this.onChange(passphrase)}
/>
</FormGroup>
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { api } from '@eclipse-che/common';
import { Form } from '@patternfly/react-core';
import React from 'react';

import { SshPassphrase } from '@/pages/UserPreferences/SshKeys/AddModal/Form/SshPassphrase';
import { SshPrivateKey } from '@/pages/UserPreferences/SshKeys/AddModal/Form/SshPrivateKey';
import { SshPublicKey } from '@/pages/UserPreferences/SshKeys/AddModal/Form/SshPublicKey';

Expand All @@ -27,6 +28,7 @@ export type State = {
privateKeyIsValid: boolean;
publicKey: string | undefined;
publicKeyIsValid: boolean;
passphrase: string | undefined;
};

export class AddModalForm extends React.PureComponent<Props, State> {
Expand All @@ -38,19 +40,27 @@ export class AddModalForm extends React.PureComponent<Props, State> {
privateKeyIsValid: false,
publicKey: undefined,
publicKeyIsValid: false,
passphrase: undefined,
};
}

private updateChangeSshKey(partialState: Partial<State>): void {
const nextState = { ...this.state, ...partialState };
this.setState(nextState);

const { privateKey = '', privateKeyIsValid, publicKey = '', publicKeyIsValid } = nextState;
const {
privateKey = '',
privateKeyIsValid,
publicKey = '',
publicKeyIsValid,
passphrase = '',
} = nextState;

const newSshKey: api.NewSshKey = {
name: SSH_KEY_NAME,
key: privateKey,
keyPub: publicKey,
passphrase: passphrase,
};
const isValid = privateKeyIsValid && publicKeyIsValid;
this.props.onChange(newSshKey, isValid);
Expand All @@ -70,11 +80,18 @@ export class AddModalForm extends React.PureComponent<Props, State> {
});
}

private handleChangeSshPassphrase(passphrase: string): void {
this.updateChangeSshKey({
passphrase,
});
}

public render(): React.ReactElement {
return (
<Form onSubmit={e => e.preventDefault()}>
<SshPrivateKey onChange={(...args) => this.handleChangeSshPrivateKey(...args)} />
<SshPublicKey onChange={(...args) => this.handleChangeSshPublicKey(...args)} />
<SshPassphrase onChange={(...args) => this.handleChangeSshPassphrase(...args)} />
</Form>
);
}
Expand Down

0 comments on commit aff295c

Please sign in to comment.