diff --git a/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx b/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx index 7527f4a4..0ffafe20 100644 --- a/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx +++ b/src/Components/CreateImageWizard/steps/Firewall/components/PortsInput.tsx @@ -6,13 +6,13 @@ import { useAppSelector } from '../../../../../store/hooks'; import { addPort, removePort, - selectPorts, + selectFirewall, } from '../../../../../store/wizardSlice'; import ChippingInput from '../../../ChippingInput'; import { isPortValid } from '../../../validators'; const PortsInput = () => { - const ports = useAppSelector(selectPorts); + const ports = useAppSelector(selectFirewall).ports; return ( diff --git a/src/Components/CreateImageWizard/steps/Firewall/components/Services.tsx b/src/Components/CreateImageWizard/steps/Firewall/components/Services.tsx new file mode 100644 index 00000000..76327060 --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Firewall/components/Services.tsx @@ -0,0 +1,48 @@ +import React from 'react'; + +import { FormGroup } from '@patternfly/react-core'; + +import { useAppSelector } from '../../../../../store/hooks'; +import { + addDisabledFirewallService, + addEnabledFirewallService, + removeDisabledFirewallService, + removeEnabledFirewallService, + selectFirewall, +} from '../../../../../store/wizardSlice'; +import ChippingInput from '../../../ChippingInput'; +import { isServiceValid } from '../../../validators'; + +const Services = () => { + const disabledServices = useAppSelector(selectFirewall).services.disabled; + const enabledServices = useAppSelector(selectFirewall).services.enabled; + + return ( + <> + + + + + + + + ); +}; + +export default Services; diff --git a/src/Components/CreateImageWizard/steps/Firewall/index.tsx b/src/Components/CreateImageWizard/steps/Firewall/index.tsx index 1795bc86..5707862c 100644 --- a/src/Components/CreateImageWizard/steps/Firewall/index.tsx +++ b/src/Components/CreateImageWizard/steps/Firewall/index.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { Text, Form, Title } from '@patternfly/react-core'; import PortsInput from './components/PortsInput'; +import Services from './components/Services'; const FirewallStep = () => { return ( @@ -12,6 +13,7 @@ const FirewallStep = () => { Customize firewall settings for your image. + ); }; diff --git a/src/Components/CreateImageWizard/utilities/requestMapper.ts b/src/Components/CreateImageWizard/utilities/requestMapper.ts index dc203e4c..563b1f9e 100644 --- a/src/Components/CreateImageWizard/utilities/requestMapper.ts +++ b/src/Components/CreateImageWizard/utilities/requestMapper.ts @@ -83,7 +83,7 @@ import { selectHostname, selectUsers, selectMetadata, - selectPorts, + selectFirewall, } from '../../../store/wizardSlice'; import { FileSystemConfigurationType } from '../steps/FileSystem'; import { @@ -345,6 +345,10 @@ function commonRequestToState( hostname: request.customizations.hostname || '', firewall: { ports: request.customizations.firewall?.ports || [], + services: { + enabled: request.customizations.firewall?.services?.enabled || [], + disabled: request.customizations.firewall?.services?.disabled || [], + }, }, }; } @@ -714,15 +718,25 @@ const getLocale = (state: RootState) => { }; const getFirewall = (state: RootState) => { - const ports = selectPorts(state); + const ports = selectFirewall(state).ports; + const services = selectFirewall(state).services; - if (ports.length === 0) { - return undefined; - } else { - return { - ports: ports, - }; + const firewall = {}; + + if (ports.length > 0) { + Object.assign(firewall, { ports: ports }); } + + if (services.enabled.length > 0 || services.disabled.length > 0) { + Object.assign(firewall, { + services: { + enabled: services.enabled.length > 0 ? services.enabled : undefined, + disabled: services.disabled.length > 0 ? services.disabled : undefined, + }, + }); + } + + return Object.keys(firewall).length > 0 ? firewall : undefined; }; const getCustomRepositories = (state: RootState) => { diff --git a/src/Components/CreateImageWizard/validators.ts b/src/Components/CreateImageWizard/validators.ts index 037d070f..f667f858 100644 --- a/src/Components/CreateImageWizard/validators.ts +++ b/src/Components/CreateImageWizard/validators.ts @@ -135,3 +135,14 @@ export const isKernelArgumentValid = (arg: string) => { export const isPortValid = (port: string) => { return /^(\d{1,5}|[a-z]{1,6})(-\d{1,5})?:[a-z]{1,6}$/.test(port); }; + +export const isServiceValid = (service: string) => { + // Restraints taken from service name syntax reference + // https://www.rfc-editor.org/rfc/rfc6335#section-5.1 + return ( + service.length <= 15 && + /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(service) && + !/--/.test(service) && // does not contain more hyphens in a row + /[a-zA-Z]+/.test(service) // contains at least one letter + ); +}; diff --git a/src/store/wizardSlice.ts b/src/store/wizardSlice.ts index fee3ecab..5bac6611 100644 --- a/src/store/wizardSlice.ts +++ b/src/store/wizardSlice.ts @@ -146,6 +146,10 @@ export type wizardState = { hostname: string; firewall: { ports: string[]; + services: { + enabled: string[]; + disabled: string[]; + }; }; metadata?: { parent_id: string | null; @@ -230,6 +234,10 @@ export const initialState: wizardState = { hostname: '', firewall: { ports: [], + services: { + enabled: [], + disabled: [], + }, }, firstBoot: { script: '' }, users: [], @@ -438,8 +446,8 @@ export const selectHostname = (state: RootState) => { return state.wizard.hostname; }; -export const selectPorts = (state: RootState) => { - return state.wizard.firewall.ports; +export const selectFirewall = (state: RootState) => { + return state.wizard.firewall; }; export const wizardSlice = createSlice({ @@ -839,6 +847,40 @@ export const wizardSlice = createSlice({ clearKernelAppend: (state) => { state.kernel.append = []; }, + addEnabledFirewallService: (state, action: PayloadAction) => { + if ( + !state.firewall.services.enabled.some( + (service) => service === action.payload + ) + ) { + state.firewall.services.enabled.push(action.payload); + } + }, + removeEnabledFirewallService: (state, action: PayloadAction) => { + state.firewall.services.enabled.splice( + state.firewall.services.enabled.findIndex( + (service) => service === action.payload + ), + 1 + ); + }, + addDisabledFirewallService: (state, action: PayloadAction) => { + if ( + !state.firewall.services.disabled.some( + (service) => service === action.payload + ) + ) { + state.firewall.services.disabled.push(action.payload); + } + }, + removeDisabledFirewallService: (state, action: PayloadAction) => { + state.firewall.services.disabled.splice( + state.firewall.services.disabled.findIndex( + (service) => service === action.payload + ), + 1 + ); + }, changeTimezone: (state, action: PayloadAction) => { state.timezone.timezone = action.payload; }, @@ -979,6 +1021,10 @@ export const { addKernelArg, removeKernelArg, clearKernelAppend, + addDisabledFirewallService, + removeDisabledFirewallService, + addEnabledFirewallService, + removeEnabledFirewallService, changeTimezone, addNtpServer, removeNtpServer, diff --git a/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx b/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx index e932e07b..2b8731e7 100644 --- a/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx +++ b/src/test/Components/CreateImageWizard/steps/Firewall/Firewall.test.tsx @@ -51,6 +51,22 @@ const addPort = async (port: string) => { await waitFor(() => user.type(portsInput, port.concat(' '))); }; +const addEnabledFirewallService = async (service: string) => { + const user = userEvent.setup(); + const enabledServicesInput = await screen.findByPlaceholderText( + /add enabled service/i + ); + await waitFor(() => user.type(enabledServicesInput, service.concat(' '))); +}; + +const addDisabledFirewallService = async (service: string) => { + const user = userEvent.setup(); + const disabledServiceInput = await screen.findByPlaceholderText( + /add disabled service/i + ); + await waitFor(() => user.type(disabledServiceInput, service.concat(' '))); +}; + describe('Step Firewall', () => { beforeEach(() => { vi.clearAllMocks(); @@ -95,6 +111,14 @@ describe('Step Firewall', () => { await addPort('00:wrongFormat'); await screen.findByText('Invalid format.'); }); + + test('service in an invalid format cannot be added', async () => { + await renderCreateMode(); + await goToFirewallStep(); + expect(screen.queryByText('Invalid format.')).not.toBeInTheDocument(); + await addPort('wrong--service'); + await screen.findByText('Invalid format.'); + }); }); describe('Firewall request generated correctly', () => { @@ -122,8 +146,64 @@ describe('Firewall request generated correctly', () => { expect(receivedRequest).toEqual(expectedRequest); }); }); + + test('with services added', async () => { + await renderCreateMode(); + await goToFirewallStep(); + await addEnabledFirewallService('ftp'); + await addEnabledFirewallService('ntp'); + await addEnabledFirewallService('dhcp'); + await addDisabledFirewallService('telnet'); + await goToReviewStep(); + const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT); + + const expectedRequest = { + ...blueprintRequest, + customizations: { + firewall: { + services: { + enabled: ['ftp', 'ntp', 'dhcp'], + disabled: ['telnet'], + }, + }, + }, + }; + + await waitFor(() => { + expect(receivedRequest).toEqual(expectedRequest); + }); + }); + + test('with ports and services added', async () => { + await renderCreateMode(); + await goToFirewallStep(); + await addPort('22:tcp'); + await addPort('imap:tcp'); + await addEnabledFirewallService('ftp'); + await addEnabledFirewallService('ntp'); + await addEnabledFirewallService('dhcp'); + await addDisabledFirewallService('telnet'); + await goToReviewStep(); + const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT); + + const expectedRequest = { + ...blueprintRequest, + customizations: { + firewall: { + ports: ['22:tcp', 'imap:tcp'], + services: { + enabled: ['ftp', 'ntp', 'dhcp'], + disabled: ['telnet'], + }, + }, + }, + }; + + await waitFor(() => { + expect(receivedRequest).toEqual(expectedRequest); + }); + }); }); // TO DO Step Firewall -> revisit step button on Review works -// TO DO Firewall request generated correctly // TO DO Firewall edit mode