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

[MTV-1794] Add warning when specifying IP for a secure provider #1475

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
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 @@ -12,9 +12,10 @@ export function providerAndSecretValidator(
const type = provider?.spec?.type || '';
const subTypeString = provider?.spec?.settings?.['sdkEndpoint'] || '';
const subType = subTypeString === 'esxi' ? 'esxi' : 'vcenter';
const caCert = subType ? secret?.data?.cacert : undefined;

const secretValidation = secretValidator(type, subType, secret);
const providerValidation = providerValidator(provider);
const providerValidation = providerValidator(provider, caCert);

// Test for validation errors
if (providerValidation?.type === 'error') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ovaProviderValidator } from './ova/ovaProviderValidator';
import { ovirtProviderValidator } from './ovirt/ovirtProviderValidator';
import { vsphereProviderValidator } from './vsphere/vsphereProviderValidator';

export function providerValidator(provider: V1beta1Provider): ValidationMsg {
export function providerValidator(provider: V1beta1Provider, caCert?: string): ValidationMsg {
let validationError: ValidationMsg;

switch (provider.spec.type) {
Expand All @@ -22,7 +22,7 @@ export function providerValidator(provider: V1beta1Provider): ValidationMsg {
validationError = ovirtProviderValidator(provider);
break;
case 'vsphere':
validationError = vsphereProviderValidator(provider);
validationError = vsphereProviderValidator(provider, 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,34 @@ 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) => {
let hostname = '';

try {
hostname = new URL(url)?.hostname;
} catch {
console.error('Unable to parse URL.');
}

return hostname;
};
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
Loading