Skip to content

Commit

Permalink
Merge pull request #1475 from jpuzz0/MTV-1794-warning-for-secure-prov…
Browse files Browse the repository at this point in the history
…ider

[MTV-1794] Add warning when specifying IP for a secure provider
  • Loading branch information
metalice authored Feb 24, 2025
2 parents ce30c1a + 0bba5b7 commit de37f56
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 12 deletions.
1 change: 1 addition & 0 deletions packages/forklift-console-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"jsonpath": "^1.1.1",
"jsrsasign": "11.1.0",
"luxon": "^3.5.0",
"node-forge": "^1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-i18next": "^11.14.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const QUERY_PARAMS = '(\\?[a-zA-Z0-9=&_]*)?';
const URL_REGEX = new RegExp(
`^${PROTOCOL}((${IPV4})|(${HOSTNAME}))((${PORT})(${PATH})(${QUERY_PARAMS})?)?$`,
);
const IPV4_REGEX = new RegExp(IPV4);

// validate NFS mount NFS_SERVER:EXPORTED_DIRECTORY
// example: 10.10.0.10:/backups
Expand Down Expand Up @@ -78,6 +79,10 @@ export function validateURL(url: string) {
return URL_REGEX.test(url);
}

export function validateIpv4(value: string) {
return IPV4_REGEX.test(value);
}

export function validateNFSMount(nfsPath: string) {
return NFS_REGEX.test(nfsPath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function providerValidator(
validationError = ovirtProviderValidator(provider);
break;
case 'vsphere':
validationError = vsphereProviderValidator(provider);
validationError = vsphereProviderValidator(provider, secret?.data?.cacert);
break;
case 'ova':
validationError = ovaProviderValidator(provider);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
import { validateURL, ValidationMsg } from '../../common';
import { pki } from 'node-forge';

export const validateVCenterURL = (url: string | number): ValidationMsg => {
import { safeBase64Decode } from '../../../helpers';
import { validateIpv4, validateURL, ValidationMsg } from '../../common';

export const urlMatchesCertFqdn = (urlHostname: string, caCert: string): boolean => {
try {
const decodedCaCert = safeBase64Decode(caCert);
const cert = pki.certificateFromPem(decodedCaCert);
const dnsAltName = cert.extensions
.find((ext) => ext.name === 'subjectAltName')
?.altNames.find((altName) => altName.type === 2)?.value;
const commonName = cert.subject.attributes.find((attr) => attr.name === 'commonName')?.value;

return urlHostname === (dnsAltName || commonName);
} catch (e) {
console.error('Unable to parse certificate object from PEM.');
}

return false;
};

export const validateVCenterURL = (url: string, caCert?: string): ValidationMsg => {
// For a newly opened form where the field is not set yet, set the validation type to default.
if (url === undefined) {
return {
Expand All @@ -16,6 +36,7 @@ export const validateVCenterURL = (url: string | number): ValidationMsg => {

const trimmedUrl: string = url.trim();
const isValidURL = validateURL(trimmedUrl);
const urlHostname = getUrlHostname(url);

if (trimmedUrl === '') {
return {
Expand All @@ -37,8 +58,32 @@ export const validateVCenterURL = (url: string | number): ValidationMsg => {
type: 'warning',
};

if (validateIpv4(urlHostname)) {
return {
type: 'error',
msg: 'Invalid URL. The URL must be a fully qualified domain name (FQDN).',
};
}

if (caCert && !urlMatchesCertFqdn(urlHostname, caCert)) {
return {
type: 'error',
msg: 'Invalid URL. The URL must be a fully qualified domain name (FQDN) and match the FQDN in the certificate you uploaded.',
};
}

return {
type: 'success',
msg: 'The URL of the vCenter API endpoint for example: https://host-example.com/sdk .',
};
};

const getUrlHostname = (url: string) => {
try {
return new URL(url)?.hostname;
} catch {
console.error('Unable to parse URL.');
}

return '';
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import { V1beta1Provider } from '@kubev2v/types';

import { validateK8sName, validateURL, ValidationMsg } from '../../common';

import { validateVCenterURL } from './validateVCenterURL';
import { validateVDDKImage } from './validateVDDKImage';

export function vsphereProviderValidator(provider: V1beta1Provider): ValidationMsg {
export function vsphereProviderValidator(
provider: V1beta1Provider,
caCert?: string,
): ValidationMsg {
const name = provider?.metadata?.name;
const url = provider?.spec?.url || '';
const vddkInitImage = provider?.spec?.settings?.['vddkInitImage'] || '';
Expand All @@ -16,7 +20,7 @@ export function vsphereProviderValidator(provider: V1beta1Provider): ValidationM
return { type: 'error', msg: 'invalid kubernetes resource name' };
}

if (!validateURL(url)) {
if (caCert ? validateVCenterURL(url, caCert).type === 'error' : !validateURL(url)) {
return { type: 'error', msg: 'invalid URL' };
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ export const EditProvider: React.FC<ProvidersCreateFormProps> = ({
default:
return (
<>
<VCenterProviderCreateForm provider={newProvider} onChange={onNewProviderChange} />
<VCenterProviderCreateForm
provider={newProvider}
caCert={newSecret.data.cacert}
onChange={onNewProviderChange}
/>

<EditProviderSectionHeading text={t('Provider credentials')} />
<VCenterCredentialsEdit secret={newSecret} onChange={onNewSecretChange} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useReducer } from 'react';
import React, { useCallback, useEffect, useReducer } from 'react';
import {
validateVCenterURL,
validateVDDKImage,
Expand All @@ -14,11 +14,13 @@ import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';

export interface VCenterProviderCreateFormProps {
provider: V1beta1Provider;
caCert: string;
onChange: (newValue: V1beta1Provider) => void;
}

export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps> = ({
provider,
caCert,
onChange,
}) => {
const { t } = useForkliftTranslation();
Expand All @@ -31,11 +33,19 @@ export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps>

const initialState = {
validation: {
url: validateVCenterURL(url),
url: validateVCenterURL(url, caCert),
vddkInitImage: validateVDDKImage(vddkInitImage),
},
};

// When certificate changes, re-validate the URL
useEffect(() => {
dispatch({
type: 'SET_FIELD_VALIDATED',
payload: { field: 'url', validationState: validateVCenterURL(url, caCert) },
});
}, [caCert]);

const reducer = (state, action) => {
switch (action.type) {
case 'SET_FIELD_VALIDATED':
Expand Down Expand Up @@ -93,7 +103,7 @@ export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps>
spec: {
...provider?.spec,
settings: {
...(provider?.spec?.settings as object),
...provider?.spec?.settings,
vddkInitImage: trimmedValue,
},
},
Expand All @@ -108,7 +118,7 @@ export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps>
spec: {
...provider?.spec,
settings: {
...(provider?.spec?.settings as object),
...provider?.spec?.settings,
sdkEndpoint: sdkEndpoint,
},
},
Expand All @@ -117,14 +127,14 @@ export const VCenterProviderCreateForm: React.FC<VCenterProviderCreateFormProps>

if (id === 'url') {
// Validate URL - VCenter of ESXi
const validationState = validateVCenterURL(trimmedValue);
const validationState = validateVCenterURL(trimmedValue, caCert);

dispatch({ type: 'SET_FIELD_VALIDATED', payload: { field: 'url', validationState } });

onChange({ ...provider, spec: { ...provider.spec, url: trimmedValue } });
}
},
[provider],
[provider, caCert],
);

const onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void = (event) => {
Expand Down

0 comments on commit de37f56

Please sign in to comment.