diff --git a/src/Components/CreateImageWizard/steps/Review/components/NoUsersAlert.tsx b/src/Components/CreateImageWizard/steps/Review/components/NoUsersAlert.tsx new file mode 100644 index 0000000000..8d99cd854b --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Review/components/NoUsersAlert.tsx @@ -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 ( + + + This image has no user accounts, so you won'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. + + + ); +}; + +export default NoUsersAlert; diff --git a/src/Components/CreateImageWizard/steps/Review/components/index.ts b/src/Components/CreateImageWizard/steps/Review/components/index.ts index 636233723a..630bf434ff 100644 --- a/src/Components/CreateImageWizard/steps/Review/components/index.ts +++ b/src/Components/CreateImageWizard/steps/Review/components/index.ts @@ -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'; diff --git a/src/Components/CreateImageWizard/steps/Review/components/tests/NoUsersAlert.test.tsx b/src/Components/CreateImageWizard/steps/Review/components/tests/NoUsersAlert.test.tsx new file mode 100644 index 0000000000..06ca554f9a --- /dev/null +++ b/src/Components/CreateImageWizard/steps/Review/components/tests/NoUsersAlert.test.tsx @@ -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(, imageModeOverrides()); + + 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( + , + 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( + , + imageModeOverrides({ + system: { + ...initialState.system, + users: [testUser], + }, + }), + ); + + expect(screen.queryByText(/no users added/i)).not.toBeInTheDocument(); + }); + + test('does not warn in package mode', () => { + renderWithRedux(, { + output: { + ...initialState.output, + imageTypes: ['guest-image'], + }, + }); + + expect(screen.queryByText(/no users added/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/Components/CreateImageWizard/steps/Review/index.tsx b/src/Components/CreateImageWizard/steps/Review/index.tsx index c437367b05..2e1b82c8e1 100644 --- a/src/Components/CreateImageWizard/steps/Review/index.tsx +++ b/src/Components/CreateImageWizard/steps/Review/index.tsx @@ -9,6 +9,7 @@ import { AdvancedSettingsOverview, ContentOverview, ImageOverview, + NoUsersAlert, ReadyToBuildAlert, Registration, RepeatableBuild, @@ -33,6 +34,7 @@ const ReviewStep = () => { return ( <> + diff --git a/src/Components/CreateImageWizard/steps/Users/index.tsx b/src/Components/CreateImageWizard/steps/Users/index.tsx index b45498ae30..8f70d01201 100644 --- a/src/Components/CreateImageWizard/steps/Users/index.tsx +++ b/src/Components/CreateImageWizard/steps/Users/index.tsx @@ -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'; @@ -14,7 +13,6 @@ type UsersStepProps = { }; const UsersStep = ({ attemptedNext }: UsersStepProps) => { - const blueprintMode = useAppSelector(selectBlueprintMode); const isOnPremise = useAppSelector(selectIsOnPremise); return ( <> @@ -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 + openssl passwd -6 + {' '} + and enter the result. + + )} diff --git a/src/Components/CreateImageWizard/steps/Users/tests/Users.test.tsx b/src/Components/CreateImageWizard/steps/Users/tests/Users.test.tsx index 9fb6552f2a..075a89e257 100644 --- a/src/Components/CreateImageWizard/steps/Users/tests/Users.test.tsx +++ b/src/Components/CreateImageWizard/steps/Users/tests/Users.test.tsx @@ -96,4 +96,33 @@ describe('Users Component', () => { ); }); }); + + describe('Password guidance', () => { + test('shows plain text warning and openssl hint on-prem', async () => { + renderWithRedux( + , + {}, + { + 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(, {}); + + await screen.findByText(/create user accounts/i); + + expect( + screen.queryByText(/stored in plain text/i), + ).not.toBeInTheDocument(); + }); + }); }); diff --git a/src/Components/CreateImageWizard/utilities/useValidation.tsx b/src/Components/CreateImageWizard/utilities/useValidation.tsx index 1df52e35ea..ec7826edb5 100644 --- a/src/Components/CreateImageWizard/utilities/useValidation.tsx +++ b/src/Components/CreateImageWizard/utilities/useValidation.tsx @@ -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 } } = {}; @@ -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: {},