Skip to content
This repository has been archived by the owner on Jun 6, 2024. It is now read-only.

Add ssh public keys on user-profile page #5223

Merged
merged 8 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 26 additions & 41 deletions src/webportal/src/app/job-submission/components/tools/job-ssh.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import { cloneDeep, isEmpty, isNil } from 'lodash';
import { TooltipIcon } from '../controls/tooltip-icon';
import {
PAI_PLUGIN,
USERSSH_TYPE_OPTIONS,
SSH_KEY_BITS,
PROTOCOL_TOOLTIPS,
} from '../../utils/constants';
import { Hint } from '../sidebar/hint';
import { SSHPlugin } from '../../models/plugin/ssh-plugin';
import SSHGenerator from './ssh-generator';

import {
DefaultButton,
Dropdown,
FontWeights,
Toggle,
Stack,
Expand Down Expand Up @@ -63,16 +62,6 @@ export const JobSSH = ({ extras, onExtrasChange }) => {
[extras],
);

const _onUsersshTypeChange = useCallback(
(_, item) => {
_onChangeExtras('userssh', {
type: item.key,
value: '',
});
},
[extras, _onChangeExtras],
);

const _onUsersshValueChange = useCallback(
e => {
_onChangeExtras('userssh', {
Expand Down Expand Up @@ -128,36 +117,32 @@ export const JobSSH = ({ extras, onExtrasChange }) => {
onChange={_onUsersshEnable}
/>
{!isEmpty(sshPlugin.userssh) && (
<Stack horizontal gap='l1'>
<Dropdown
placeholder='Select user ssh key type...'
options={USERSSH_TYPE_OPTIONS}
onChange={_onUsersshTypeChange}
selectedKey={sshPlugin.userssh.type}
disabled={Object.keys(USERSSH_TYPE_OPTIONS).length <= 1}
/>
<TextField
placeholder='Enter ssh public key'
disabled={sshPlugin.userssh.type === 'none'}
errorMessage={
isEmpty(sshPlugin.getUserSshValue())
? 'Please Enter Valid SSH public key'
: null
}
onChange={_onUsersshValueChange}
value={sshPlugin.getUserSshValue()}
/>
<DefaultButton onClick={ev => openSshGenerator(ev)}>
SSH Key Generator
</DefaultButton>
{sshGenerator.isOpen && (
<SSHGenerator
isOpen={sshGenerator.isOpen}
bits={sshGenerator.bits}
hide={hideSshGenerator}
onSshKeysChange={_onSshKeysGenerated}
<Stack gap='l1'>
<Hint>
Your pre-defined SSH public keys on the{' '}
<a href='/user-profile.html'>User Profile</a> page will be set
automatically.
</Hint>
<Stack horizontal gap='l1'>
<TextField
Lable='Add additional ssh public key'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change ssh to SSH.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

placeholder='Additional ssh public key'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change ssh to SSH.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

disabled={sshPlugin.userssh.type === 'none'}
onChange={_onUsersshValueChange}
value={sshPlugin.getUserSshValue()}
/>
)}
<DefaultButton onClick={ev => openSshGenerator(ev)}>
Generator
</DefaultButton>
{sshGenerator.isOpen && (
<SSHGenerator
isOpen={sshGenerator.isOpen}
bits={sshGenerator.bits}
hide={hideSshGenerator}
onSshKeysChange={_onSshKeysGenerated}
/>
)}
</Stack>
</Stack>
)}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,10 @@ const runtimePluginSchema = Joi.object().keys({
sshbarrierTimeout: Joi.number(),
userssh: Joi.object().keys({
type: Joi.string(),
value: Joi.string(),
value: Joi.string()
.allow(null)
.allow('')
.optional(),
}),
})
.required(),
Expand Down
20 changes: 20 additions & 0 deletions src/webportal/src/app/user/fabric/conn.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,26 @@ export const getUserRequest = async username => {
});
};

export const updateUserRequest = async (username, sskMessage) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sskMessage -> sshMessage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

const url = `${config.restServerUri}/api/v2/users/me`;
const token = checkToken();
return fetchWrapper(url, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
data: {
username: username,
extension: {
sshKeys: sskMessage,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sskMessage -> sshMessage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

},
},
patch: true,
}),
});
};

export const getTokenRequest = async () => {
return wrapper(() => client.token.getTokens());
};
Expand Down
63 changes: 63 additions & 0 deletions src/webportal/src/app/user/fabric/user-profile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ import {
listStorageDetailRequest,
getGroupsRequest,
updateBoundedClustersRequest,
updateUserRequest,
} from './conn';

import t from '../../components/tachyons.scss';
import { VirtualClusterDetailsList } from '../../home/home/virtual-cluster-statistics';
import TokenList from './user-profile/token-list';
import SSHlist from './user-profile/ssh-list';
import UserProfileHeader from './user-profile/header';
import StorageList from './user-profile/storage-list';
import BoundedClusterDialog from './user-profile/bounded-cluster-dialog';
import SSHListDialog from './user-profile/ssh-list-dialog';
import BoundedClusterList from './user-profile/bounded-cluster-list';

const UserProfileCard = ({ title, children, headerButton }) => {
Expand Down Expand Up @@ -70,7 +73,11 @@ const UserProfile = () => {
const [showBoundedClusterDialog, setShowBoundedClusterDialog] = useState(
false,
);
const [showAddSSHpublicKeysDialog, setShowAddSSHpublicKeysDialog] = useState(
false,
);
const [processing, setProcessing] = useState(false);
const [sshProcessing, setSSHProcessing] = useState(false);

useEffect(() => {
const fetchData = async () => {
Expand Down Expand Up @@ -139,6 +146,39 @@ const UserProfile = () => {
});
});

// click `add public ssh keys button` -> open dialog
const onAddPublicKeys = useCallback(async sshPublicKeys => {
setSSHProcessing(true);
let updatedSSHPublickeys = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate here: the user cannot add ssh keys with duplicate titles. (Title should be unique for one user)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

if (userInfo.extension.sshKeys) {
updatedSSHPublickeys = cloneDeep(userInfo.extension.sshKeys);
}
updatedSSHPublickeys.push({
title: sshPublicKeys.title,
value: sshPublicKeys.value,
time: sshPublicKeys.time,
});
await updateUserRequest(userInfo.username, updatedSSHPublickeys);
const updatedUserInfo = await getUserRequest(userInfo.username);
setUserInfo(updatedUserInfo);
setSSHProcessing(false);
});

const onDeleteSSHkeys = useCallback(async sshPublicKeys => {
let updatedSSHPublickeys = [];
if (userInfo.extension.sshKeys) {
updatedSSHPublickeys = cloneDeep(userInfo.extension.sshKeys);
}
updatedSSHPublickeys = updatedSSHPublickeys.filter(
item =>
item.title !== sshPublicKeys.title &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please only filter with item.title !== sshPublicKeys.title.

Title should be unique and delete means delete an ssh key with a certain title.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

item.value !== sshPublicKeys.value,
);
await updateUserRequest(userInfo.username, updatedSSHPublickeys);
const updatedUserInfo = await getUserRequest(userInfo.username);
setUserInfo(updatedUserInfo);
});

const onRevokeToken = useCallback(async token => {
await revokeTokenRequest(token);
await getTokenRequest().then(res => setTokens(res.tokens));
Expand Down Expand Up @@ -210,6 +250,29 @@ const UserProfile = () => {
>
<TokenList tokens={tokens} onRevoke={onRevokeToken} />
</UserProfileCard>
<UserProfileCard
Copy link
Contributor

@hzy46 hzy46 Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this UserProfileCard of SSH Public Keys before the UserProfileCard of Tokens.

SSH is supposed to be used more frequently than tokens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

title='SSH Public Keys'
headerButton={
<DefaultButton
onClick={() => setShowAddSSHpublicKeysDialog(true)}
disabled={sshProcessing}
>
Add SSH Public Keys
</DefaultButton>
}
>
<SSHlist
sshKeys={userInfo.extension.sshKeys}
onDeleteSSHkeys={onDeleteSSHkeys}
/>
{/* dialog for add public ssh keys */}
{showAddSSHpublicKeysDialog && (
<SSHListDialog
onDismiss={() => setShowAddSSHpublicKeysDialog(false)}
onAddPublickeys={onAddPublicKeys}
/>
)}
</UserProfileCard>
<UserProfileCard title='Storage'>
<StorageList storageDetails={storageDetails} />
</UserProfileCard>
Expand Down
128 changes: 128 additions & 0 deletions src/webportal/src/app/user/fabric/user-profile/ssh-list-dialog.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { isEmpty } from 'lodash';
import {
DefaultButton,
PrimaryButton,
DialogType,
Dialog,
DialogFooter,
TextField,
} from 'office-ui-fabric-react';

import t from '../../../components/tachyons.scss';

const SSHListDialog = ({ onDismiss, onAddPublickeys }) => {
const [error, setError] = useState('');
const [inputTitleError, setInputTitleError] = useState('');
const [inputValueError, setInputValueError] = useState('');
const [processing, setProcessing] = useState(false);
const [title, setTitle] = useState('');
const [value, setValue] = useState('');

const onAddAsync = async () => {
if (title.trim() === '') {
setInputTitleError('Please input title');
} else if (value.trim() === '') {
setInputValueError('Please input ssk value');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please change ssk to SSH.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

} else {
Copy link
Contributor

@hzy46 hzy46 Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate the value matches SSH key's format.

You can use this regex: /^ssh-rsa AAAA[0-9A-Za-z+/]+[=]{0,3}.*$/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

setProcessing(true);
try {
await onAddPublickeys({
title: title.trim(),
value: value.trim(),
time: new Date().getTime(),
});
} catch (error) {
setError(error.message);
} finally {
setProcessing(false);
onDismiss();
}
}
};

return (
<div>
<Dialog
hidden={false}
onDismiss={onDismiss}
dialogContentProps={{
type: DialogType.normal,
title: 'Add SSH Public Keys',
}}
modalProps={{
isBlocking: true,
}}
minWidth={600}
>
<div>
<div className={t.mt1}>
<TextField
label='title'
Copy link
Contributor

@hzy46 hzy46 Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to give the user some hints.

title -> Title (Please give the SSH key a name):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

required={true}
errorMessage={inputTitleError}
onChange={e => {
setTitle(e.target.value);
setInputTitleError(null);
}}
validateOnFocusOut={true}
/>
</div>
<div className={t.mt1}>
<TextField
label='value'
Copy link
Contributor

@hzy46 hzy46 Feb 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to give the user some hints.

value -> Value (SSH Public key, starts with ssh-rsa):

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

required={true}
errorMessage={inputValueError}
onChange={e => {
setValue(e.target.value);
setInputValueError(null);
}}
multiline
rows={5}
validateOnFocusOut={true}
/>
</div>
</div>
<DialogFooter>
<PrimaryButton
onClick={onAddAsync}
disabled={processing}
text='Add'
/>
<DefaultButton
onClick={onDismiss}
disabled={processing}
text='Cancel'
/>
</DialogFooter>
</Dialog>
<Dialog
hidden={isEmpty(error)}
onDismiss={() => setError('')}
dialogContentProps={{
type: DialogType.normal,
title: 'Error',
subText: error,
}}
modalProps={{
isBlocking: true,
}}
>
<DialogFooter>
<DefaultButton onClick={() => setError('')}>OK</DefaultButton>
</DialogFooter>
</Dialog>
</div>
);
};

SSHListDialog.propTypes = {
onDismiss: PropTypes.func.isRequired,
onAddPublickeys: PropTypes.func.isRequired,
};

export default SSHListDialog;
Loading