Skip to content
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

import { Alert, Content } from '@patternfly/react-core';

import { useAppSelector } from '@/store/hooks';
import {
selectImageTypes,
selectIsImageMode,
selectUsers,
type SupportedImageTypes,
} from '@/store/slices/wizard';

const DISK_IMAGE_TYPES: SupportedImageTypes[] = ['guest-image', 'aws', 'ami'];

const NoUsersAlert = () => {
const isImageMode = useAppSelector(selectIsImageMode);
const imageTypes = useAppSelector(selectImageTypes);
const users = useAppSelector(selectUsers);

const hasUser = users.some((user) => (user.name || '').trim() !== '');
const isDiskImage = imageTypes.some((imageType) =>
DISK_IMAGE_TYPES.includes(imageType),
);

if (!isImageMode || !isDiskImage || hasUser) {
return null;
}

return (
<Alert variant='warning' isInline title='No users added' ouiaId='NoUsers'>
<Content component='p'>
This image has no user accounts, so you won&apos;t be able to log in to
it directly. To log in, use cloud-init to create a user when you launch
the image, or go back to the Users step and add one now.
</Content>
</Alert>
);
};

export default NoUsersAlert;
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export { default as ContentOverview } from './Content';
export { default as ImageOverview } from './ImageOverview';
export { default as RepeatableBuild } from './RepeatableBuild';
export { default as Registration } from './Registration';
export { default as NoUsersAlert } from './NoUsersAlert';
export { default as ReadyToBuildAlert } from './ReadyToBuildAlert';
export { default as Security } from './Security';
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';

import { screen } from '@testing-library/react';

import { initialState } from '@/store/slices/wizard';
import { renderWithRedux, type WizardStateOverrides } from '@/test/testUtils';

import NoUsersAlert from '../NoUsersAlert';

const testUser = {
name: 'testuser',
password: '',
ssh_key: '',
isAdministrator: false,
groups: [],
hasPassword: false,
};

const imageModeOverrides = (
overrides: WizardStateOverrides = {},
): WizardStateOverrides => ({
details: {
...initialState.details,
blueprint: { ...initialState.details.blueprint, mode: 'image' },
},
output: {
...initialState.output,
imageTypes: ['guest-image'],
},
...overrides,
});

describe('NoUsersAlert', () => {
test('warns when a disk image has no users', async () => {
renderWithRedux(<NoUsersAlert />, imageModeOverrides());

expect(await screen.findByText(/no users added/i)).toBeInTheDocument();
expect(screen.getByText(/cloud-init/i)).toBeInTheDocument();
});

Comment on lines +33 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider covering all supported disk image types and mixed-type outputs

The current test only exercises the guest-image type, but NoUsersAlert treats guest-image, aws, and ami as disk images. To better align tests with the implementation, please:

  1. Add coverage for aws and ami (e.g., via a parameterized test) to confirm they also show the warning when no users are configured.
  2. Add a case where imageTypes mixes disk and non-disk types (e.g. ['guest-image', 'bootable-container-iso']) to confirm that any disk image still triggers the alert.

This will keep the tests resilient if DISK_IMAGE_TYPES changes in the future.

Suggested change
describe('NoUsersAlert', () => {
test('warns when a disk image has no users', async () => {
renderWithRedux(<NoUsersAlert />, imageModeOverrides());
expect(await screen.findByText(/no users added/i)).toBeInTheDocument();
expect(screen.getByText(/cloud-init/i)).toBeInTheDocument();
});
describe('NoUsersAlert', () => {
test.each([
['guest-image'],
['aws'],
['ami'],
])('warns when a %s disk image has no users', async (imageType) => {
renderWithRedux(
<NoUsersAlert />,
imageModeOverrides({
output: {
...initialState.output,
imageTypes: [imageType],
},
})
);
expect(await screen.findByText(/no users added/i)).toBeInTheDocument();
expect(screen.getByText(/cloud-init/i)).toBeInTheDocument();
});
test('warns when any disk image is present alongside non-disk images', async () => {
renderWithRedux(
<NoUsersAlert />,
imageModeOverrides({
output: {
...initialState.output,
imageTypes: ['guest-image', 'bootable-container-iso'],
},
})
);
expect(await screen.findByText(/no users added/i)).toBeInTheDocument();
expect(screen.getByText(/cloud-init/i)).toBeInTheDocument();
});

test('does not warn for the container installer', () => {
renderWithRedux(
<NoUsersAlert />,
imageModeOverrides({
output: {
...initialState.output,
imageTypes: ['bootable-container-iso'],
},
}),
);

expect(screen.queryByText(/no users added/i)).not.toBeInTheDocument();
});

test('does not warn when a user is configured', () => {
renderWithRedux(
<NoUsersAlert />,
imageModeOverrides({
system: {
...initialState.system,
users: [testUser],
},
}),
);

expect(screen.queryByText(/no users added/i)).not.toBeInTheDocument();
});

test('does not warn in package mode', () => {
renderWithRedux(<NoUsersAlert />, {
output: {
...initialState.output,
imageTypes: ['guest-image'],
},
});

expect(screen.queryByText(/no users added/i)).not.toBeInTheDocument();
});
});
2 changes: 2 additions & 0 deletions src/Components/CreateImageWizard/steps/Review/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
AdvancedSettingsOverview,
ContentOverview,
ImageOverview,
NoUsersAlert,
ReadyToBuildAlert,
Registration,
RepeatableBuild,
Expand All @@ -33,6 +34,7 @@ const ReviewStep = () => {
return (
<>
<FormHeader />
<NoUsersAlert />
<ImageOverview restrictions={restrictions} />
<Registration restrictions={restrictions} />
<RepeatableBuild restrictions={restrictions} />
Expand Down
15 changes: 10 additions & 5 deletions src/Components/CreateImageWizard/steps/Users/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Content, Title } from '@patternfly/react-core';
import { CustomizationLabels } from '@/Components/sharedComponents/CustomizationLabels';
import { useAppSelector } from '@/store/hooks';
import { selectIsOnPremise } from '@/store/slices/env';
import { selectBlueprintMode } from '@/store/slices/wizard';

import UserInfo from './components/UserInfo';

Expand All @@ -14,7 +13,6 @@ type UsersStepProps = {
};

const UsersStep = ({ attemptedNext }: UsersStepProps) => {
const blueprintMode = useAppSelector(selectBlueprintMode);
const isOnPremise = useAppSelector(selectIsOnPremise);
return (
<>
Expand All @@ -27,9 +25,16 @@ const UsersStep = ({ attemptedNext }: UsersStepProps) => {
Create user accounts to manage access to your image. All usernames
must be unique.
{/* TO DO: learn more about accessing your SSH keys link */}
{isOnPremise &&
blueprintMode === 'image' &&
' You must create a user during the image build process to be able to log in.'}
{isOnPremise && (
<>
{' '}
Passwords are stored in plain text on this host. To store a hashed
password instead, generate one with <code>
openssl passwd -6
</code>{' '}
and enter the result.
</>
)}
</Content>
</Content>
<UserInfo attemptedNext={attemptedNext} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,33 @@ describe('Users Component', () => {
);
});
});

describe('Password guidance', () => {
test('shows plain text warning and openssl hint on-prem', async () => {
renderWithRedux(
<UsersStep />,
{},
{
preloadedState: {
env: { isOnPremise: true },
},
},
);

expect(
await screen.findByText(/stored in plain text on this host/i),
).toBeInTheDocument();
expect(screen.getByText('openssl passwd -6')).toBeInTheDocument();
});

test('does not show the plain text warning in the hosted service', async () => {
renderWithRedux(<UsersStep />, {});

await screen.findByText(/create user accounts/i);

expect(
screen.queryByText(/stored in plain text/i),
).not.toBeInTheDocument();
});
});
});
10 changes: 0 additions & 10 deletions src/Components/CreateImageWizard/utilities/useValidation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -881,8 +881,6 @@ const validateSshKey = (userSshKey: string): string => {

export function useUsersValidation(): UsersStepValidation {
const environments = useAppSelector(selectImageTypes);
const blueprintMode = useAppSelector(selectBlueprintMode);
const isOnPremise = useAppSelector(selectIsOnPremise);
const users = useAppSelector(selectUsers);
const userGroups = useAppSelector(selectUserGroups);
const errors: { [key: string]: { [key: string]: string } } = {};
Expand All @@ -891,14 +889,6 @@ export function useUsersValidation(): UsersStepValidation {
users.length === 0 ||
(users.length === 1 && (users[0].name || '').trim() === '')
) {
if (isOnPremise && blueprintMode === 'image') {
return {
errors: {},
warnings: {},
disabledNext: true,
};
}

return {
errors: {},
warnings: {},
Expand Down
Loading