Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
6.9 KiB

import { FC, useMemo, useRef, useState } from 'react';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import api from '../../lib/api';
import ConfirmDialog from '../ConfirmDialog';
import Divider from '../Divider';
import FlexBox from '../FlexBox';
import handleAPIError from '../../lib/handleAPIError';
import Link from '../Link';
import List from '../List';
import MessageBox, { Message } from '../MessageBox';
import { ExpandablePanel } from '../Panels';
import periodicFetch from '../../lib/fetchers/periodicFetch';
import { BodyText } from '../Text';
import useChecklist from '../../hooks/useChecklist';
const ManageChangedSSHKeysForm: FC<ManageChangedSSHKeysFormProps> = ({
mitmExternalHref = 'https://en.wikipedia.org/wiki/Man-in-the-middle_attack',
refreshInterval = 60000,
}) => {
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const [apiMessage, setApiMessage] = useState<Message | undefined>();
const [changedSSHKeys, setChangedSSHKeys] = useState<ChangedSSHKeys>({});
const [confirmDialogProps, setConfirmDialogProps] =
useState<ConfirmDialogProps>({
actionProceedText: '',
content: '',
titleText: '',
});
const { checks, getCheck, hasAllChecks, hasChecks, setAllChecks, setCheck } =
useChecklist({ list: changedSSHKeys });
const apiMessageElement = useMemo(
() => apiMessage && <MessageBox {...apiMessage} />,
[apiMessage],
);
const isAllowCheckAll = useMemo(
() => Object.keys(changedSSHKeys).length > 1,
[changedSSHKeys],
);
const { isLoading } = periodicFetch<APISSHKeyConflictOverviewList>(
`${API_BASE_URL}/ssh-key/conflict`,
{
onError: (error) => {
setApiMessage({
children: `Failed to fetch SSH key conflicts. Error: ${error}`,
type: 'error',
});
},
onSuccess: (data) => {
setChangedSSHKeys((previous) =>
Object.values(data).reduce<ChangedSSHKeys>((nyu, stateList) => {
Object.values(stateList).forEach(
({ hostName, hostUUID, ipAddress, stateUUID }) => {
nyu[stateUUID] = {
...previous[stateUUID],
hostName,
hostUUID,
ipAddress,
};
},
);
return nyu;
}, {}),
);
},
refreshInterval,
},
);
return (
<>
<ExpandablePanel header="Manage changed SSH keys" loading={isLoading}>
<FlexBox spacing=".2em">
<BodyText>
The identity of the following targets have unexpectedly changed.
</BodyText>
<MessageBox type="warning" isAllowClose>
If you haven&apos;t rebuilt the listed targets, then you could be
experiencing a{' '}
<Link
href={mitmExternalHref}
sx={{ display: 'inline-flex' }}
target="_blank"
>
&quot;Man In The Middle&quot;
</Link>{' '}
attack. Please verify the targets have changed for a known reason
before proceeding to remove the broken keys.
</MessageBox>
<List
header={
<FlexBox
row
spacing=".3em"
sx={{
width: '100%',
'& > :not(:last-child)': {
display: { xs: 'none', sm: 'flex' },
},
'& > :last-child': {
display: { xs: 'initial', sm: 'none' },
marginLeft: 0,
},
}}
>
<FlexBox
row
spacing=".3em"
sx={{ flexBasis: 'calc(50% + 1em)' }}
>
<BodyText>Host name</BodyText>
<Divider sx={{ flexGrow: 1 }} />
</FlexBox>
<FlexBox row spacing=".3em" sx={{ flexGrow: 1 }}>
<BodyText>IP address</BodyText>
<Divider sx={{ flexGrow: 1 }} />
</FlexBox>
<Divider sx={{ flexGrow: 1 }} />
</FlexBox>
}
allowCheckAll={isAllowCheckAll}
allowCheckItem
allowDelete
allowEdit={false}
disableDelete={!hasChecks}
edit
getListCheckboxProps={() => ({
checked: hasAllChecks,
})}
listEmpty={
<BodyText align="center">No conflicting keys found.</BodyText>
}
listItems={changedSSHKeys}
onAllCheckboxChange={(event, checked) => {
setAllChecks(checked);
}}
onDelete={() => {
const deleteRequestBody = checks.reduce<{
[hostUUID: string]: string[];
}>((previous, stateUUID) => {
const checked = getCheck(stateUUID);
if (!checked) return previous;
const { hostUUID } = changedSSHKeys[stateUUID];
if (!previous[hostUUID]) {
previous[hostUUID] = [];
}
previous[hostUUID].push(stateUUID);
return previous;
}, {});
setConfirmDialogProps({
actionProceedText: 'Delete',
content: `Resolve ${checks.length} SSH key conflicts. Please make sure the identity change(s) are expected to avoid MITM attacks.`,
onProceedAppend: () => {
api
.delete('/ssh-key/conflict', { data: deleteRequestBody })
.catch((error) => {
const emsg = handleAPIError(error);
emsg.children = `Failed to delete selected SSH key conflicts. ${emsg.children}`;
setApiMessage(emsg);
});
},
proceedColour: 'red',
titleText: `Delete ${checks.length} conflicting SSH keys?`,
});
confirmDialogRef.current.setOpen?.call(null, true);
}}
onItemCheckboxChange={(key, event, checked) => {
setCheck(key, checked);
}}
renderListItem={(hostUUID, { hostName, ipAddress }) => (
<FlexBox
spacing={0}
sm="row"
sx={{ width: '100%', '& > *': { flexBasis: '50%' } }}
xs="column"
>
<BodyText>{hostName}</BodyText>
<BodyText>{ipAddress}</BodyText>
</FlexBox>
)}
renderListItemCheckboxState={(key) => getCheck(key)}
/>
</FlexBox>
{apiMessageElement}
</ExpandablePanel>
<ConfirmDialog
closeOnProceed
{...confirmDialogProps}
ref={confirmDialogRef}
/>
</>
);
};
export default ManageChangedSSHKeysForm;