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

wizrad: add validation to ssh key field (HMS-5349) #2794

Merged
merged 1 commit into from
Jan 23, 2025
Merged
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
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
194 changes: 88 additions & 106 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 @@ -54,41 +56,27 @@ 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 @@ -129,100 +117,94 @@ 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 invalid name', async () => {
await renderCreateMode();
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
await clickAddUser();
await addUserName('ss.');
await addSshKey('ssh');
const invalidUserMessage = screen.getByText(/invalid user name/i);
await waitFor(() => expect(invalidUserMessage));
});

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();
const isAdmin = screen.getByRole('checkbox', {
name: /administrator/i,
});
user.click(isAdmin);
await goToReviewStep();
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);

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

await waitFor(() => {
expect(receivedRequest).toEqual(expectedRequest);
});
});
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));
});
});

test('with invalid name', async () => {
await renderCreateMode();
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
await addInvalidUser();
const invalidUserMessage = screen.getByText(/invalid user name/i);
await waitFor(() => expect(invalidUserMessage));
describe('User request generated correctly', () => {
test('with valid name, ssh key and checked Administrator checkbox', async () => {
const user = userEvent.setup();
await renderCreateMode();
await goToRegistrationStep();
await clickRegisterLater();
await goToUsersStep();
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 = {
...blueprintRequest,
customizations: {
users: [
{
name: 'best',
ssh_key: 'ssh-rsa d',
groups: ['wheel'],
},
],
},
};
await waitFor(() => {
expect(receivedRequest).toEqual(expectedRequest);
});
});
});

describe('Users edit mode', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('Users edit mode', () => {
beforeEach(() => {
vi.clearAllMocks();
});

test('edit mode works', async () => {
const id = mockBlueprintIds['users'];
await renderEditMode(id);
test('edit mode works', async () => {
const id = mockBlueprintIds['users'];
await renderEditMode(id);

// starts on review step
const receivedRequest = await interceptEditBlueprintRequest(
`${EDIT_BLUEPRINT}/${id}`
);
const expectedRequest = usersCreateBlueprintRequest;
expect(receivedRequest).toEqual(expectedRequest);
});
// starts on review step
const receivedRequest = await interceptEditBlueprintRequest(
`${EDIT_BLUEPRINT}/${id}`
);
const expectedRequest = usersCreateBlueprintRequest;
expect(receivedRequest).toEqual(expectedRequest);
});
});
Loading