Skip to content

Commit

Permalink
wizard: add ssh key validation
Browse files Browse the repository at this point in the history
  • Loading branch information
mgold1234 committed Jan 23, 2025
1 parent c6bee55 commit 9063e9f
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 57 deletions.
13 changes: 11 additions & 2 deletions src/Components/CreateImageWizard/utilities/useValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
isHostnameValid,
isKernelNameValid,
isUserNameValid,
isSshKeyValid,
} from '../validators';

export type StepValidation = {
Expand Down Expand Up @@ -192,12 +193,20 @@ export function useUsersValidation(): StepValidation {
users.length === 0 ||
// Case 2: All fields are empty
(userName === '' && userPassword === '' && userSshKey === '') ||
// Case 3: userName is valid
(userName && isUserNameValid(userName));
// Case 3: userName is valid and SshKey is valid
(userName &&
isUserNameValid(userName) &&
userSshKey &&
isSshKeyValid(userSshKey));

return {
errors: {
userName: !isUserNameValid(userName) ? 'Invalid user name' : '',
userSshKey: !userSshKey
? ''
: !isSshKeyValid(userSshKey)
? 'Invalid SSH key'
: '',
},
disabledNext: !canProceed,
};
Expand Down
11 changes: 11 additions & 0 deletions src/Components/CreateImageWizard/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ export const isUserNameValid = (userName: string) => {
return isLengthValid && isNotNumericOnly && isPatternValid;
};

export const isSshKeyValid = (sshKey: string) => {
// 1. Key types: ssh-rsa, ssh-dss, ssh-ed25519, or ecdsa-sha2-nistp(256|384|521).
// 2. Base64-encoded key material.
// 3. Optional comment at the end.
const isPatternValid =
/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp(256|384|521))\s+[A-Za-z0-9+/=]+(\s+\S+)?$/.test(
sshKey
);
return isPatternValid;
};

export const getDuplicateMountPoints = (partitions: Partition[]): string[] => {
const mountPointSet: Set<string> = new Set();
const duplicates: string[] = [];
Expand Down
97 changes: 42 additions & 55 deletions src/test/Components/CreateImageWizard/steps/Users/Users.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
} from '../../wizardTestUtils';

let router: RemixRouter | undefined = undefined;
const validUserName = 'best';
const validSshKey = 'ssh-rsa d';

const goToUsersStep = async () => {
await clickNext();
Expand Down Expand Up @@ -53,41 +55,29 @@ const clickRevisitButton = async () => {
await waitFor(() => user.click(revisitButton));
};

const addValidUser = async () => {
const clickAddUser = async () => {
const user = userEvent.setup();
const addUser = await screen.findByRole('button', { name: /add a user/i });
expect(addUser).toBeEnabled();
await waitFor(() => user.click(addUser));
const enterUserName = screen.getByRole('textbox', {
name: /blueprint user name/i,
});
const nextButton = await getNextButton();
await waitFor(() => user.type(enterUserName, 'best'));
await waitFor(() => expect(enterUserName).toHaveValue('best'));
};

const addSshKey = async (sshKey: string) => {
const user = userEvent.setup();
const enterSshKey = await screen.findByRole('textbox', {
name: /public SSH key/i,
});
await waitFor(() => user.type(enterSshKey, 'ssh-rsa d'));
await waitFor(() => expect(enterSshKey).toHaveValue('ssh-rsa d'));
await waitFor(() => expect(nextButton).toBeEnabled());
await waitFor(() => user.type(enterSshKey, sshKey));
await waitFor(() => expect(enterSshKey).toHaveValue(sshKey));
};

const addInvalidUser = async () => {
const addUserName = async (userName: string) => {
const user = userEvent.setup();
const addUser = await screen.findByRole('button', { name: /add a user/i });
expect(addUser).toBeEnabled();
await waitFor(() => user.click(addUser));
const enterUserName = screen.getByRole('textbox', {
name: /blueprint user name/i,
});
const nextButton = await getNextButton();
await waitFor(() => user.type(enterUserName, '..'));
await waitFor(() => expect(enterUserName).toHaveValue('..'));
const enterSshKey = await screen.findByRole('textbox', {
name: /public SSH key/i,
});
await waitFor(() => user.type(enterSshKey, 'ssh-rsa d'));
await waitFor(() => expect(nextButton).toBeDisabled());
await waitFor(() => user.type(enterUserName, userName));
await waitFor(() => expect(enterUserName).toHaveValue(userName));
};

describe('Step Users', () => {
Expand Down Expand Up @@ -128,54 +118,38 @@ describe('Step Users', () => {
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
await addValidUser();
await clickAddUser();
await addUserName(validUserName);
await addSshKey(validSshKey);
const nextButton = await getNextButton();
await waitFor(() => expect(nextButton).toBeEnabled());
await goToReviewStep();
await clickRevisitButton();
await screen.findByRole('heading', { name: /Users/ });
});

describe('User request generated correctly', () => {
test('with valid name and password', async () => {
await renderCreateMode();
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
await addValidUser();
await goToReviewStep();
// informational modal pops up in the first test only as it's tied
// to a 'imageBuilder.saveAndBuildModalSeen' variable in localStorage
await openAndDismissSaveAndBuildModal();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

const expectedRequest = {
...blueprintRequest,
customizations: {
users: [
{
name: 'best',
ssh_key: 'ssh-rsa d',
},
],
},
};

await waitFor(() => {
expect(receivedRequest).toEqual(expectedRequest);
});
});

test('with valid name, ssh key and checked Administrator checkbox', async () => {
const user = userEvent.setup();
await renderCreateMode();
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
await addValidUser();
await clickAddUser();
await addUserName(validUserName);
await addSshKey(validSshKey);
const nextButton = await getNextButton();
await waitFor(() => expect(nextButton).toBeEnabled());

const isAdmin = screen.getByRole('checkbox', {
name: /administrator/i,
});
user.click(isAdmin);

await goToReviewStep();
// informational modal pops up in the first test only as it's tied
// to a 'imageBuilder.saveAndBuildModalSeen' variable in localStorage
await openAndDismissSaveAndBuildModal();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

const expectedRequest = {
Expand All @@ -190,7 +164,6 @@ describe('Step Users', () => {
],
},
};

await waitFor(() => {
expect(receivedRequest).toEqual(expectedRequest);
});
Expand All @@ -201,10 +174,24 @@ describe('Step Users', () => {
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
await addInvalidUser();
await clickAddUser();
await addUserName('ss.');
await addSshKey('ssh');
const invalidUserMessage = screen.getByText(/invalid user name/i);
await waitFor(() => expect(invalidUserMessage));
});

test('with invalid ssh key', async () => {
await renderCreateMode();
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
await clickAddUser();
await addSshKey('ssh');
await addUserName('bestUser');
const invalidUserMessage = screen.getByText(/invalid ssh key/i);
await waitFor(() => expect(invalidUserMessage));
});
});

describe('Users edit mode', () => {
Expand Down

0 comments on commit 9063e9f

Please sign in to comment.