Skip to content

Commit

Permalink
Wizard: Add firewall services
Browse files Browse the repository at this point in the history
This adds firewall services, using the `<ChippingInput>` components. New tests were also added.
  • Loading branch information
regexowl committed Jan 23, 2025
1 parent c6bee55 commit e1f7123
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<FormGroup label="Ports">
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<>
<FormGroup label="Disabled services">
<ChippingInput
ariaLabel="Add disabled service"
placeholder="Add disabled service"
validator={isServiceValid}
list={disabledServices}
item="Disabled service"
addAction={addDisabledFirewallService}
removeAction={removeDisabledFirewallService}
/>
</FormGroup>
<FormGroup label="Enabled services">
<ChippingInput
ariaLabel="Add enabled service"
placeholder="Add enabled service"
validator={isServiceValid}
list={enabledServices}
item="Enabled service"
addAction={addEnabledFirewallService}
removeAction={removeEnabledFirewallService}
/>
</FormGroup>
</>
);
};

export default Services;
2 changes: 2 additions & 0 deletions src/Components/CreateImageWizard/steps/Firewall/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -12,6 +13,7 @@ const FirewallStep = () => {
</Title>
<Text>Customize firewall settings for your image.</Text>
<PortsInput />
<Services />
</Form>
);
};
Expand Down
30 changes: 22 additions & 8 deletions src/Components/CreateImageWizard/utilities/requestMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import {
selectHostname,
selectUsers,
selectMetadata,
selectPorts,
selectFirewall,
} from '../../../store/wizardSlice';
import { FileSystemConfigurationType } from '../steps/FileSystem';
import {
Expand Down Expand Up @@ -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 || [],
},
},
};
}
Expand Down Expand Up @@ -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) => {
Expand Down
10 changes: 10 additions & 0 deletions src/Components/CreateImageWizard/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,13 @@ export const isKernelNameValid = (kernelName: 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-Z][a-zA-Z0-9-]*[a-zA-Z]$/.test(service) &&
!/--/.test(service)
);
};
50 changes: 48 additions & 2 deletions src/store/wizardSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ export type wizardState = {
hostname: string;
firewall: {
ports: string[];
services: {
enabled: string[];
disabled: string[];
};
};
metadata?: {
parent_id: string | null;
Expand Down Expand Up @@ -230,6 +234,10 @@ export const initialState: wizardState = {
hostname: '',
firewall: {
ports: [],
services: {
enabled: [],
disabled: [],
},
},
firstBoot: { script: '' },
users: [],
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -817,6 +825,40 @@ export const wizardSlice = createSlice({
changeKernelName: (state, action: PayloadAction<string>) => {
state.kernel.name = action.payload;
},
addEnabledFirewallService: (state, action: PayloadAction<string>) => {
if (
!state.firewall.services.enabled.some(
(service) => service === action.payload
)
) {
state.firewall.services.enabled.push(action.payload);
}
},
removeEnabledFirewallService: (state, action: PayloadAction<string>) => {
state.firewall.services.enabled.splice(
state.firewall.services.enabled.findIndex(
(service) => service === action.payload
),
1
);
},
addDisabledFirewallService: (state, action: PayloadAction<string>) => {
if (
!state.firewall.services.disabled.some(
(service) => service === action.payload
)
) {
state.firewall.services.disabled.push(action.payload);
}
},
removeDisabledFirewallService: (state, action: PayloadAction<string>) => {
state.firewall.services.disabled.splice(
state.firewall.services.disabled.findIndex(
(service) => service === action.payload
),
1
);
},
changeKernelAppend: (state, action: PayloadAction<string>) => {
state.kernel.append = action.payload;
},
Expand Down Expand Up @@ -954,6 +996,10 @@ export const {
changeMaskedServices,
changeDisabledServices,
changeKernelName,
addDisabledFirewallService,
removeDisabledFirewallService,
addEnabledFirewallService,
removeEnabledFirewallService,
changeKernelAppend,
changeTimezone,
addNtpServer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,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();
Expand Down Expand Up @@ -94,6 +110,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', () => {
Expand Down Expand Up @@ -121,8 +145,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

0 comments on commit e1f7123

Please sign in to comment.