mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 01:47:35 -02:30
Add support for password prompting on job launch
This commit is contained in:
@@ -8,7 +8,7 @@ import PasswordInput from './PasswordInput';
|
|||||||
function PasswordField(props) {
|
function PasswordField(props) {
|
||||||
const { id, name, label, validate, isRequired, helperText } = props;
|
const { id, name, label, validate, isRequired, helperText } = props;
|
||||||
const [, meta] = useField({ name, validate });
|
const [, meta] = useField({ name, validate });
|
||||||
const isValid = !(meta.touched && meta.error);
|
const isValid = !meta.touched || (meta.value && meta.value !== '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ import {
|
|||||||
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
|
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
function PasswordInput(props) {
|
function PasswordInput(props) {
|
||||||
const { id, name, validate, isRequired, isDisabled, i18n } = props;
|
const {
|
||||||
|
autocomplete,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
validate,
|
||||||
|
isRequired,
|
||||||
|
isDisabled,
|
||||||
|
i18n,
|
||||||
|
} = props;
|
||||||
const [inputType, setInputType] = useState('password');
|
const [inputType, setInputType] = useState('password');
|
||||||
const [field, meta] = useField({ name, validate });
|
const [field, meta] = useField({ name, validate });
|
||||||
|
|
||||||
@@ -38,6 +46,7 @@ function PasswordInput(props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<TextInput
|
<TextInput
|
||||||
|
autoComplete={autocomplete}
|
||||||
id={id}
|
id={id}
|
||||||
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
|
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
|
||||||
{...field}
|
{...field}
|
||||||
@@ -55,6 +64,7 @@ function PasswordInput(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
|
autocomplete: PropTypes.string,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
validate: PropTypes.func,
|
validate: PropTypes.func,
|
||||||
@@ -63,6 +73,7 @@ PasswordInput.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
PasswordInput.defaultProps = {
|
||||||
|
autocomplete: 'new-password',
|
||||||
validate: () => {},
|
validate: () => {},
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ function canLaunchWithoutPrompt(launchData) {
|
|||||||
!launchData.ask_limit_on_launch &&
|
!launchData.ask_limit_on_launch &&
|
||||||
!launchData.ask_scm_branch_on_launch &&
|
!launchData.ask_scm_branch_on_launch &&
|
||||||
!launchData.survey_enabled &&
|
!launchData.survey_enabled &&
|
||||||
|
(!launchData.passwords_needed_to_start ||
|
||||||
|
launchData.passwords_needed_to_start.length === 0) &&
|
||||||
(!launchData.variables_needed_to_start ||
|
(!launchData.variables_needed_to_start ||
|
||||||
launchData.variables_needed_to_start.length === 0)
|
launchData.variables_needed_to_start.length === 0)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ContentError from '../ContentError';
|
|||||||
import ContentLoading from '../ContentLoading';
|
import ContentLoading from '../ContentLoading';
|
||||||
import { useDismissableError } from '../../util/useRequest';
|
import { useDismissableError } from '../../util/useRequest';
|
||||||
import mergeExtraVars from '../../util/prompt/mergeExtraVars';
|
import mergeExtraVars from '../../util/prompt/mergeExtraVars';
|
||||||
|
import getCredentialPasswords from '../../util/prompt/getCredentialPasswords';
|
||||||
import getSurveyValues from '../../util/prompt/getSurveyValues';
|
import getSurveyValues from '../../util/prompt/getSurveyValues';
|
||||||
import useLaunchSteps from './useLaunchSteps';
|
import useLaunchSteps from './useLaunchSteps';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
@@ -19,17 +20,18 @@ function PromptModalForm({
|
|||||||
resource,
|
resource,
|
||||||
surveyConfig,
|
surveyConfig,
|
||||||
}) {
|
}) {
|
||||||
const { values, setTouched, validateForm } = useFormikContext();
|
const { setFieldTouched, values } = useFormikContext();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
steps,
|
steps,
|
||||||
isReady,
|
isReady,
|
||||||
|
validateStep,
|
||||||
visitStep,
|
visitStep,
|
||||||
visitAllSteps,
|
visitAllSteps,
|
||||||
contentError,
|
contentError,
|
||||||
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSubmit = () => {
|
||||||
const postValues = {};
|
const postValues = {};
|
||||||
const setValue = (key, value) => {
|
const setValue = (key, value) => {
|
||||||
if (typeof value !== 'undefined' && value !== null) {
|
if (typeof value !== 'undefined' && value !== null) {
|
||||||
@@ -37,6 +39,8 @@ function PromptModalForm({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const surveyValues = getSurveyValues(values);
|
const surveyValues = getSurveyValues(values);
|
||||||
|
const credentialPasswords = getCredentialPasswords(values);
|
||||||
|
setValue('credential_passwords', credentialPasswords);
|
||||||
setValue('inventory_id', values.inventory?.id);
|
setValue('inventory_id', values.inventory?.id);
|
||||||
setValue(
|
setValue(
|
||||||
'credentials',
|
'credentials',
|
||||||
@@ -75,22 +79,25 @@ function PromptModalForm({
|
|||||||
<Wizard
|
<Wizard
|
||||||
isOpen
|
isOpen
|
||||||
onClose={onCancel}
|
onClose={onCancel}
|
||||||
onSave={handleSave}
|
onSave={handleSubmit}
|
||||||
|
onBack={async nextStep => {
|
||||||
|
validateStep(nextStep.id);
|
||||||
|
}}
|
||||||
onNext={async (nextStep, prevStep) => {
|
onNext={async (nextStep, prevStep) => {
|
||||||
if (nextStep.id === 'preview') {
|
if (nextStep.id === 'preview') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
visitStep(prevStep.prevId, setFieldTouched);
|
||||||
|
validateStep(nextStep.id);
|
||||||
}
|
}
|
||||||
await validateForm();
|
|
||||||
}}
|
}}
|
||||||
onGoToStep={async (nextStep, prevStep) => {
|
onGoToStep={async (nextStep, prevStep) => {
|
||||||
if (nextStep.id === 'preview') {
|
if (nextStep.id === 'preview') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
visitStep(prevStep.prevId, setFieldTouched);
|
||||||
|
validateStep(nextStep.id);
|
||||||
}
|
}
|
||||||
await validateForm();
|
|
||||||
}}
|
}}
|
||||||
title={i18n._(t`Prompts`)}
|
title={i18n._(t`Prompts`)}
|
||||||
steps={
|
steps={
|
||||||
|
|||||||
@@ -82,8 +82,26 @@ describe('LaunchPrompt', () => {
|
|||||||
ask_credential_on_launch: true,
|
ask_credential_on_launch: true,
|
||||||
ask_scm_branch_on_launch: true,
|
ask_scm_branch_on_launch: true,
|
||||||
survey_enabled: true,
|
survey_enabled: true,
|
||||||
|
passwords_needed_to_start: ['ssh_password'],
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['ssh_password'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
resource={{
|
||||||
|
...resource,
|
||||||
|
summary_fields: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
resource={resource}
|
|
||||||
onLaunch={noop}
|
onLaunch={noop}
|
||||||
onCancel={noop}
|
onCancel={noop}
|
||||||
surveyConfig={{
|
surveyConfig={{
|
||||||
@@ -110,12 +128,13 @@ describe('LaunchPrompt', () => {
|
|||||||
const wizard = await waitForElement(wrapper, 'Wizard');
|
const wizard = await waitForElement(wrapper, 'Wizard');
|
||||||
const steps = wizard.prop('steps');
|
const steps = wizard.prop('steps');
|
||||||
|
|
||||||
expect(steps).toHaveLength(5);
|
expect(steps).toHaveLength(6);
|
||||||
expect(steps[0].name.props.children).toEqual('Inventory');
|
expect(steps[0].name.props.children).toEqual('Inventory');
|
||||||
expect(steps[1].name.props.children).toEqual('Credentials');
|
expect(steps[1].name.props.children).toEqual('Credentials');
|
||||||
expect(steps[2].name.props.children).toEqual('Other prompts');
|
expect(steps[2].name.props.children).toEqual('Credential passwords');
|
||||||
expect(steps[3].name.props.children).toEqual('Survey');
|
expect(steps[3].name.props.children).toEqual('Other prompts');
|
||||||
expect(steps[4].name.props.children).toEqual('Preview');
|
expect(steps[4].name.props.children).toEqual('Survey');
|
||||||
|
expect(steps[5].name.props.children).toEqual('Preview');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add inventory step', async () => {
|
test('should add inventory step', async () => {
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Form } from '@patternfly/react-core';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import { PasswordField } from '../../FormField';
|
||||||
|
|
||||||
|
function CredentialPasswordsStep({ launchConfig, i18n }) {
|
||||||
|
const {
|
||||||
|
values: { credentials },
|
||||||
|
} = useFormikContext();
|
||||||
|
|
||||||
|
const vaultsThatPrompt = [];
|
||||||
|
let showcredentialPasswordSsh = false;
|
||||||
|
let showcredentialPasswordPrivilegeEscalation = false;
|
||||||
|
let showcredentialPasswordPrivateKeyPassphrase = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
if (password === 'ssh_password') {
|
||||||
|
showcredentialPasswordSsh = true;
|
||||||
|
} else if (password === 'become_password') {
|
||||||
|
showcredentialPasswordPrivilegeEscalation = true;
|
||||||
|
} else if (password === 'ssh_key_unlock') {
|
||||||
|
showcredentialPasswordPrivateKeyPassphrase = true;
|
||||||
|
} else if (password.startsWith('vault_password')) {
|
||||||
|
const vaultId = password.split(/\.(.+)/)[1] || '';
|
||||||
|
vaultsThatPrompt.push(vaultId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (credentials) {
|
||||||
|
credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('ssh_password')
|
||||||
|
) {
|
||||||
|
showcredentialPasswordSsh = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('become_password')
|
||||||
|
) {
|
||||||
|
showcredentialPasswordPrivilegeEscalation = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('ssh_key_unlock')
|
||||||
|
) {
|
||||||
|
showcredentialPasswordPrivateKeyPassphrase = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultPasswordIds = launchConfigCredential.passwords_needed
|
||||||
|
.filter(passwordNeeded =>
|
||||||
|
passwordNeeded.startsWith('vault_password')
|
||||||
|
)
|
||||||
|
.map(vaultPassword => vaultPassword.split(/\.(.+)/)[1] || '');
|
||||||
|
|
||||||
|
vaultsThatPrompt.push(...vaultPasswordIds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (credential?.inputs?.password === 'ASK') {
|
||||||
|
showcredentialPasswordSsh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.become_password === 'ASK') {
|
||||||
|
showcredentialPasswordPrivilegeEscalation = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.ssh_key_unlock === 'ASK') {
|
||||||
|
showcredentialPasswordPrivateKeyPassphrase = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.vault_password === 'ASK') {
|
||||||
|
vaultsThatPrompt.push(credential.inputs.vault_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form>
|
||||||
|
{showcredentialPasswordSsh && (
|
||||||
|
<PasswordField
|
||||||
|
id="launch-ssh-password"
|
||||||
|
label={i18n._(t`SSH password`)}
|
||||||
|
name="credentialPasswordSsh"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showcredentialPasswordPrivateKeyPassphrase && (
|
||||||
|
<PasswordField
|
||||||
|
id="launch-private-key-passphrase"
|
||||||
|
label={i18n._(t`Private key passphrase`)}
|
||||||
|
name="credentialPasswordPrivateKeyPassphrase"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showcredentialPasswordPrivilegeEscalation && (
|
||||||
|
<PasswordField
|
||||||
|
id="launch-privilege-escalation-password"
|
||||||
|
label={i18n._(t`Privilege escalation password`)}
|
||||||
|
name="credentialPasswordPrivilegeEscalation"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{vaultsThatPrompt.map(credId => (
|
||||||
|
<PasswordField
|
||||||
|
id={`launch-vault-password-${credId}`}
|
||||||
|
key={credId}
|
||||||
|
label={
|
||||||
|
credId === ''
|
||||||
|
? i18n._(t`Vault password`)
|
||||||
|
: i18n._(t`Vault password - ${credId}`)
|
||||||
|
}
|
||||||
|
name={`credentialPasswordVault_${credId}`}
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n()(CredentialPasswordsStep);
|
||||||
@@ -0,0 +1,603 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import CredentialPasswordsStep from './CredentialPasswordsStep';
|
||||||
|
|
||||||
|
describe('CredentialPasswordsStep', () => {
|
||||||
|
describe('JT default credentials (no credential replacement) and creds are promptable', () => {
|
||||||
|
test('should render ssh password field when JT has default machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['ssh_password'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render become password field when JT has default machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['become_password'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render private key passphrase field when JT has default machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
defaults: {
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['ssh_key_unlock'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render vault password field when JT has default vault cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: ['vault_password.1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-1')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
test('should render all password field when JT has default vault cred and machine cred', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
passwords_needed: [
|
||||||
|
'ssh_password',
|
||||||
|
'become_password',
|
||||||
|
'ssh_key_unlock',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
passwords_needed: ['vault_password.1'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-1')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Credentials have been replaced and creds are promptable', () => {
|
||||||
|
test('should render ssh password field when replacement machine cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: 'ASK',
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render become password field when replacement machine cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: 'ASK',
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render private key passphrase field when replacement machine cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: 'ASK',
|
||||||
|
vault_password: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render vault password field when replacement vault cred prompts for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: 'ASK',
|
||||||
|
vault_id: 'foobar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
test('should render all password fields when replacement vault and machine creds prompt for it', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
inputs: {
|
||||||
|
password: 'ASK',
|
||||||
|
become_password: 'ASK',
|
||||||
|
ssh_key_unlock: 'ASK',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
inputs: {
|
||||||
|
password: null,
|
||||||
|
become_password: null,
|
||||||
|
ssh_key_unlock: null,
|
||||||
|
vault_password: 'ASK',
|
||||||
|
vault_id: 'foobar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: true,
|
||||||
|
defaults: {
|
||||||
|
credentials: [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Credentials have been replaced and creds are not promptable', () => {
|
||||||
|
test('should render ssh password field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['ssh_password'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render become password field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['become_password'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render private key passphrase field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['ssh_key_unlock'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(0);
|
||||||
|
});
|
||||||
|
test('should render vault password field when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: ['vault_password.foobar'],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(0);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
test('should render all password fields when required', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Formik initialValues={{}}>
|
||||||
|
<CredentialPasswordsStep
|
||||||
|
launchConfig={{
|
||||||
|
ask_credential_on_launch: false,
|
||||||
|
passwords_needed_to_start: [
|
||||||
|
'ssh_password',
|
||||||
|
'become_password',
|
||||||
|
'ssh_key_unlock',
|
||||||
|
'vault_password.foobar',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('PasswordField#launch-ssh-password')).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-private-key-passphrase')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-privilege-escalation-password')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField[id^="launch-vault-password-"]')
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
wrapper.find('PasswordField#launch-vault-password-foobar')
|
||||||
|
).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@ import { useHistory } from 'react-router-dom';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
|
import { Alert } from '@patternfly/react-core';
|
||||||
import { InventoriesAPI } from '../../../api';
|
import { InventoriesAPI } from '../../../api';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
@@ -17,9 +18,10 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventoryStep({ i18n }) {
|
function InventoryStep({ i18n }) {
|
||||||
const [field, , helpers] = useField({
|
const [field, meta, helpers] = useField({
|
||||||
name: 'inventory',
|
name: 'inventory',
|
||||||
});
|
});
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -65,40 +67,45 @@ function InventoryStep({ i18n }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsList
|
<>
|
||||||
value={field.value ? [field.value] : []}
|
<OptionsList
|
||||||
options={inventories}
|
value={field.value ? [field.value] : []}
|
||||||
optionCount={count}
|
options={inventories}
|
||||||
searchColumns={[
|
optionCount={count}
|
||||||
{
|
searchColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'name__icontains',
|
name: i18n._(t`Name`),
|
||||||
isDefault: true,
|
key: 'name__icontains',
|
||||||
},
|
isDefault: true,
|
||||||
{
|
},
|
||||||
name: i18n._(t`Created By (Username)`),
|
{
|
||||||
key: 'created_by__username__icontains',
|
name: i18n._(t`Created By (Username)`),
|
||||||
},
|
key: 'created_by__username__icontains',
|
||||||
{
|
},
|
||||||
name: i18n._(t`Modified By (Username)`),
|
{
|
||||||
key: 'modified_by__username__icontains',
|
name: i18n._(t`Modified By (Username)`),
|
||||||
},
|
key: 'modified_by__username__icontains',
|
||||||
]}
|
},
|
||||||
sortColumns={[
|
]}
|
||||||
{
|
sortColumns={[
|
||||||
name: i18n._(t`Name`),
|
{
|
||||||
key: 'name',
|
name: i18n._(t`Name`),
|
||||||
},
|
key: 'name',
|
||||||
]}
|
},
|
||||||
searchableKeys={searchableKeys}
|
]}
|
||||||
relatedSearchableKeys={relatedSearchableKeys}
|
searchableKeys={searchableKeys}
|
||||||
header={i18n._(t`Inventory`)}
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
name="inventory"
|
header={i18n._(t`Inventory`)}
|
||||||
qsConfig={QS_CONFIG}
|
name="inventory"
|
||||||
readOnly
|
qsConfig={QS_CONFIG}
|
||||||
selectItem={helpers.setValue}
|
readOnly
|
||||||
deselectItem={() => field.onChange(null)}
|
selectItem={helpers.setValue}
|
||||||
/>
|
deselectItem={() => field.onChange(null)}
|
||||||
|
/>
|
||||||
|
{meta.touched && meta.error && (
|
||||||
|
<Alert variant="danger" isInline title={meta.error} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,342 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useFormikContext } from 'formik';
|
||||||
|
import CredentialPasswordsStep from './CredentialPasswordsStep';
|
||||||
|
import StepName from './StepName';
|
||||||
|
|
||||||
|
const STEP_ID = 'credentialPasswords';
|
||||||
|
|
||||||
|
const isValueMissing = val => {
|
||||||
|
return !val || val === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useCredentialPasswordsStep(
|
||||||
|
launchConfig,
|
||||||
|
i18n,
|
||||||
|
showStep,
|
||||||
|
visitedSteps
|
||||||
|
) {
|
||||||
|
const { values, setFieldError } = useFormikContext();
|
||||||
|
const hasError =
|
||||||
|
Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
|
checkForError(launchConfig, values);
|
||||||
|
|
||||||
|
return {
|
||||||
|
step: showStep
|
||||||
|
? {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: (
|
||||||
|
<StepName hasErrors={hasError} id="credential-passwords-step">
|
||||||
|
{i18n._(t`Credential passwords`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: (
|
||||||
|
<CredentialPasswordsStep launchConfig={launchConfig} i18n={i18n} />
|
||||||
|
),
|
||||||
|
enableNext: true,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
initialValues: getInitialValues(launchConfig, values.credentials),
|
||||||
|
isReady: true,
|
||||||
|
contentError: null,
|
||||||
|
hasError,
|
||||||
|
setTouched: setFieldTouched => {
|
||||||
|
Object.keys(values)
|
||||||
|
.filter(valueKey => valueKey.startsWith('credentialPassword'))
|
||||||
|
.forEach(credentialValueKey =>
|
||||||
|
setFieldTouched(credentialValueKey, true, false)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validate: () => {
|
||||||
|
const setPasswordFieldError = fieldName => {
|
||||||
|
setFieldError(fieldName, i18n._(t`This field may not be blank`));
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
credentialPasswordSsh,
|
||||||
|
credentialPasswordPrivilegeEscalation,
|
||||||
|
credentialPasswordPrivateKeyPassphrase,
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
if (
|
||||||
|
password === 'ssh_password' &&
|
||||||
|
isValueMissing(credentialPasswordSsh)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordSsh');
|
||||||
|
} else if (
|
||||||
|
password === 'become_password' &&
|
||||||
|
isValueMissing(credentialPasswordPrivilegeEscalation)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordPrivilegeEscalation');
|
||||||
|
} else if (
|
||||||
|
password === 'ssh_key_unlock' &&
|
||||||
|
isValueMissing(credentialPasswordPrivateKeyPassphrase)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordPrivateKeyPassphrase');
|
||||||
|
} else if (password.startsWith('vault_password')) {
|
||||||
|
const vaultId = password.split(/\.(.+)/)[1] || '';
|
||||||
|
if (isValueMissing(values[`credentialPasswordVault_${vaultId}`])) {
|
||||||
|
setPasswordFieldError(`credentialPasswordVault_${vaultId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (values.credentials) {
|
||||||
|
values.credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes(
|
||||||
|
'ssh_password'
|
||||||
|
) &&
|
||||||
|
isValueMissing(credentialPasswordSsh)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordSsh');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes(
|
||||||
|
'become_password'
|
||||||
|
) &&
|
||||||
|
isValueMissing(credentialPasswordPrivilegeEscalation)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordPrivilegeEscalation');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes(
|
||||||
|
'ssh_key_unlock'
|
||||||
|
) &&
|
||||||
|
isValueMissing(credentialPasswordPrivateKeyPassphrase)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordPrivateKeyPassphrase');
|
||||||
|
}
|
||||||
|
|
||||||
|
launchConfigCredential.passwords_needed
|
||||||
|
.filter(passwordNeeded =>
|
||||||
|
passwordNeeded.startsWith('vault_password')
|
||||||
|
)
|
||||||
|
.map(vaultPassword => vaultPassword.split(/\.(.+)/)[1] || '')
|
||||||
|
.forEach(vaultId => {
|
||||||
|
if (
|
||||||
|
isValueMissing(values[`credentialPasswordVault_${vaultId}`])
|
||||||
|
) {
|
||||||
|
setPasswordFieldError(`credentialPasswordVault_${vaultId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
credential?.inputs?.password === 'ASK' &&
|
||||||
|
isValueMissing(credentialPasswordSsh)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordSsh');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.become_password === 'ASK' &&
|
||||||
|
isValueMissing(credentialPasswordPrivilegeEscalation)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordPrivilegeEscalation');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.ssh_key_unlock === 'ASK' &&
|
||||||
|
isValueMissing(credentialPasswordPrivateKeyPassphrase)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError('credentialPasswordPrivateKeyPassphrase');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.vault_password === 'ASK' &&
|
||||||
|
isValueMissing(
|
||||||
|
values[`credentialPasswordVault_${credential.inputs.vault_id}`]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setPasswordFieldError(
|
||||||
|
`credentialPasswordVault_${credential.inputs.vault_id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialValues(launchConfig, selectedCredentials = []) {
|
||||||
|
const initialValues = {};
|
||||||
|
|
||||||
|
if (!launchConfig) {
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
if (password === 'ssh_password') {
|
||||||
|
initialValues.credentialPasswordSsh = '';
|
||||||
|
} else if (password === 'become_password') {
|
||||||
|
initialValues.credentialPasswordPrivilegeEscalation = '';
|
||||||
|
} else if (password === 'ssh_key_unlock') {
|
||||||
|
initialValues.credentialPasswordPrivateKeyPassphrase = '';
|
||||||
|
} else if (password.startsWith('vault_password')) {
|
||||||
|
const vaultId = password.split(/\.(.+)/)[1] || '';
|
||||||
|
initialValues[`credentialPasswordVault_${vaultId}`] = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCredentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
if (launchConfigCredential.passwords_needed.includes('ssh_password')) {
|
||||||
|
initialValues.credentialPasswordSsh = '';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('become_password')
|
||||||
|
) {
|
||||||
|
initialValues.credentialPasswordPrivilegeEscalation = '';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
launchConfigCredential.passwords_needed.includes('ssh_key_unlock')
|
||||||
|
) {
|
||||||
|
initialValues.credentialPasswordPrivateKeyPassphrase = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultPasswordIds = launchConfigCredential.passwords_needed
|
||||||
|
.filter(passwordNeeded => passwordNeeded.startsWith('vault_password'))
|
||||||
|
.map(vaultPassword => vaultPassword.split(/\.(.+)/)[1] || '');
|
||||||
|
|
||||||
|
vaultPasswordIds.forEach(vaultPasswordId => {
|
||||||
|
initialValues[`credentialPasswordVault_${vaultPasswordId}`] = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (credential?.inputs?.password === 'ASK') {
|
||||||
|
initialValues.credentialPasswordSsh = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.become_password === 'ASK') {
|
||||||
|
initialValues.credentialPasswordPrivilegeEscalation = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.ssh_key_unlock === 'ASK') {
|
||||||
|
initialValues.credentialPasswordPrivateKeyPassphrase = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credential?.inputs?.vault_password === 'ASK') {
|
||||||
|
initialValues[`credentialPasswordVault_${credential.inputs.vault_id}`] =
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return initialValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForError(launchConfig, values) {
|
||||||
|
const {
|
||||||
|
credentialPasswordSsh,
|
||||||
|
credentialPasswordPrivilegeEscalation,
|
||||||
|
credentialPasswordPrivateKeyPassphrase,
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!launchConfig.ask_credential_on_launch &&
|
||||||
|
launchConfig.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
launchConfig.passwords_needed_to_start.forEach(password => {
|
||||||
|
if (
|
||||||
|
(password === 'ssh_password' &&
|
||||||
|
isValueMissing(credentialPasswordSsh)) ||
|
||||||
|
(password === 'become_password' &&
|
||||||
|
isValueMissing(credentialPasswordPrivilegeEscalation)) ||
|
||||||
|
(password === 'ssh_key_unlock' &&
|
||||||
|
isValueMissing(credentialPasswordPrivateKeyPassphrase))
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
} else if (password.startsWith('vault_password')) {
|
||||||
|
const vaultId = password.split(/\.(.+)/)[1] || '';
|
||||||
|
if (isValueMissing(values[`credentialPasswordVault_${vaultId}`])) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (values.credentials) {
|
||||||
|
values.credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
if (
|
||||||
|
(launchConfigCredential.passwords_needed.includes('ssh_password') &&
|
||||||
|
isValueMissing(credentialPasswordSsh)) ||
|
||||||
|
(launchConfigCredential.passwords_needed.includes(
|
||||||
|
'become_password'
|
||||||
|
) &&
|
||||||
|
isValueMissing(credentialPasswordPrivilegeEscalation)) ||
|
||||||
|
(launchConfigCredential.passwords_needed.includes(
|
||||||
|
'ssh_key_unlock'
|
||||||
|
) &&
|
||||||
|
isValueMissing(credentialPasswordPrivateKeyPassphrase))
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
launchConfigCredential.passwords_needed
|
||||||
|
.filter(passwordNeeded =>
|
||||||
|
passwordNeeded.startsWith('vault_password')
|
||||||
|
)
|
||||||
|
.map(vaultPassword => vaultPassword.split(/\.(.+)/)[1] || '')
|
||||||
|
.forEach(vaultId => {
|
||||||
|
if (
|
||||||
|
isValueMissing(values[`credentialPasswordVault_${vaultId}`])
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
(credential?.inputs?.password === 'ASK' &&
|
||||||
|
isValueMissing(credentialPasswordSsh)) ||
|
||||||
|
(credential?.inputs?.become_password === 'ASK' &&
|
||||||
|
isValueMissing(credentialPasswordPrivilegeEscalation)) ||
|
||||||
|
(credential?.inputs?.ssh_key_unlock === 'ASK' &&
|
||||||
|
isValueMissing(credentialPasswordPrivateKeyPassphrase))
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
credential?.inputs?.vault_password === 'ASK' &&
|
||||||
|
isValueMissing(
|
||||||
|
values[`credentialPasswordVault_${credential.inputs.vault_id}`]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
@@ -9,15 +9,13 @@ export default function useCredentialsStep(launchConfig, resource, i18n) {
|
|||||||
return {
|
return {
|
||||||
step: getStep(launchConfig, i18n),
|
step: getStep(launchConfig, i18n),
|
||||||
initialValues: getInitialValues(launchConfig, resource),
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
validate: () => ({}),
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: null,
|
hasError: false,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('credentials', true, false);
|
||||||
credentials: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,20 +12,27 @@ export default function useInventoryStep(
|
|||||||
i18n,
|
i18n,
|
||||||
visitedSteps
|
visitedSteps
|
||||||
) {
|
) {
|
||||||
const [, meta] = useField('inventory');
|
const [, meta, helpers] = useField('inventory');
|
||||||
const formError =
|
const formError =
|
||||||
Object.keys(visitedSteps).includes(STEP_ID) && (!meta.value || meta.error);
|
!resource || resource?.type === 'workflow_job_template'
|
||||||
|
? false
|
||||||
|
: Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
|
meta.touched &&
|
||||||
|
!meta.value;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
step: getStep(launchConfig, i18n, formError),
|
step: getStep(launchConfig, i18n, formError),
|
||||||
initialValues: getInitialValues(launchConfig, resource),
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: launchConfig.ask_inventory_on_launch && formError,
|
hasError: launchConfig.ask_inventory_on_launch && formError,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('inventory', true, false);
|
||||||
inventory: true,
|
},
|
||||||
});
|
validate: () => {
|
||||||
|
if (meta.touched && !meta.value && resource.type === 'job_template') {
|
||||||
|
helpers.setError(i18n._(t`An inventory must be selected`));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,19 @@ export default function useOtherPromptsStep(launchConfig, resource, i18n) {
|
|||||||
initialValues: getInitialValues(launchConfig, resource),
|
initialValues: getInitialValues(launchConfig, resource),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: null,
|
hasError: false,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
[
|
||||||
job_type: true,
|
'job_type',
|
||||||
limit: true,
|
'limit',
|
||||||
verbosity: true,
|
'verbosity',
|
||||||
diff_mode: true,
|
'diff_mode',
|
||||||
job_tags: true,
|
'job_tags',
|
||||||
skip_tags: true,
|
'skip_tags',
|
||||||
extra_vars: true,
|
'extra_vars',
|
||||||
});
|
].forEach(field => setFieldTouched(field, true, false));
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ export default function usePreviewStep(
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
initialValues: {},
|
initialValues: {},
|
||||||
validate: () => ({}),
|
|
||||||
isReady: true,
|
isReady: true,
|
||||||
error: null,
|
error: null,
|
||||||
setTouched: () => {},
|
setTouched: () => {},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,89 +13,51 @@ export default function useSurveyStep(
|
|||||||
i18n,
|
i18n,
|
||||||
visitedSteps
|
visitedSteps
|
||||||
) {
|
) {
|
||||||
const { values } = useFormikContext();
|
const { setFieldError, values } = useFormikContext();
|
||||||
const errors = {};
|
const hasError =
|
||||||
const validate = () => {
|
Object.keys(visitedSteps).includes(STEP_ID) &&
|
||||||
if (!launchConfig.survey_enabled || !surveyConfig?.spec) {
|
checkForError(launchConfig, surveyConfig, values);
|
||||||
return {};
|
|
||||||
}
|
|
||||||
surveyConfig.spec.forEach(question => {
|
|
||||||
const errMessage = validateField(
|
|
||||||
question,
|
|
||||||
values[`survey_${question.variable}`],
|
|
||||||
i18n
|
|
||||||
);
|
|
||||||
if (errMessage) {
|
|
||||||
errors[`survey_${question.variable}`] = errMessage;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
const formError = Object.keys(validate()).length > 0;
|
|
||||||
return {
|
return {
|
||||||
step: getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps),
|
step: launchConfig.survey_enabled
|
||||||
|
? {
|
||||||
|
id: STEP_ID,
|
||||||
|
name: (
|
||||||
|
<StepName hasErrors={hasError} id="survey-step">
|
||||||
|
{i18n._(t`Survey`)}
|
||||||
|
</StepName>
|
||||||
|
),
|
||||||
|
component: <SurveyStep surveyConfig={surveyConfig} i18n={i18n} />,
|
||||||
|
enableNext: true,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
initialValues: getInitialValues(launchConfig, surveyConfig, resource),
|
initialValues: getInitialValues(launchConfig, surveyConfig, resource),
|
||||||
validate,
|
|
||||||
surveyConfig,
|
surveyConfig,
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError,
|
hasError,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
if (!surveyConfig?.spec) {
|
if (!surveyConfig?.spec) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fields = {};
|
|
||||||
surveyConfig.spec.forEach(question => {
|
surveyConfig.spec.forEach(question => {
|
||||||
fields[`survey_${question.variable}`] = true;
|
setFieldTouched(`survey_${question.variable}`, true, false);
|
||||||
});
|
});
|
||||||
setFieldsTouched(fields);
|
|
||||||
},
|
},
|
||||||
};
|
validate: () => {
|
||||||
}
|
if (launchConfig.survey_enabled && surveyConfig.spec) {
|
||||||
|
surveyConfig.spec.forEach(question => {
|
||||||
function validateField(question, value, i18n) {
|
const errMessage = validateSurveyField(
|
||||||
const isTextField = ['text', 'textarea'].includes(question.type);
|
question,
|
||||||
const isNumeric = ['integer', 'float'].includes(question.type);
|
values[`survey_${question.variable}`],
|
||||||
if (isTextField && (value || value === 0)) {
|
i18n
|
||||||
if (question.min && value.length < question.min) {
|
);
|
||||||
return i18n._(t`This field must be at least ${question.min} characters`);
|
if (errMessage) {
|
||||||
}
|
setFieldError(`survey_${question.variable}`, errMessage);
|
||||||
if (question.max && value.length > question.max) {
|
}
|
||||||
return i18n._(t`This field must not exceed ${question.max} characters`);
|
});
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
if (isNumeric && (value || value === 0)) {
|
|
||||||
if (value < question.min || value > question.max) {
|
|
||||||
return i18n._(
|
|
||||||
t`This field must be a number and have a value between ${question.min} and ${question.max}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (question.required && !value && value !== 0) {
|
|
||||||
return i18n._(t`This field must not be blank`);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
function getStep(launchConfig, surveyConfig, validate, i18n, visitedSteps) {
|
|
||||||
if (!launchConfig.survey_enabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: STEP_ID,
|
|
||||||
name: (
|
|
||||||
<StepName
|
|
||||||
hasErrors={
|
|
||||||
Object.keys(visitedSteps).includes(STEP_ID) &&
|
|
||||||
Object.keys(validate()).length
|
|
||||||
}
|
|
||||||
id="survey-step"
|
|
||||||
>
|
|
||||||
{i18n._(t`Survey`)}
|
|
||||||
</StepName>
|
|
||||||
),
|
|
||||||
component: <SurveyStep surveyConfig={surveyConfig} i18n={i18n} />,
|
|
||||||
enableNext: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,3 +95,56 @@ function getInitialValues(launchConfig, surveyConfig, resource) {
|
|||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateSurveyField(question, value, i18n) {
|
||||||
|
const isTextField = ['text', 'textarea'].includes(question.type);
|
||||||
|
const isNumeric = ['integer', 'float'].includes(question.type);
|
||||||
|
if (isTextField && (value || value === 0)) {
|
||||||
|
if (question.min && value.length < question.min) {
|
||||||
|
return i18n._(t`This field must be at least ${question.min} characters`);
|
||||||
|
}
|
||||||
|
if (question.max && value.length > question.max) {
|
||||||
|
return i18n._(t`This field must not exceed ${question.max} characters`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNumeric && (value || value === 0)) {
|
||||||
|
if (value < question.min || value > question.max) {
|
||||||
|
return i18n._(
|
||||||
|
t`This field must be a number and have a value between ${question.min} and ${question.max}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (question.required && !value && value !== 0) {
|
||||||
|
return i18n._(t`This field must not be blank`);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForError(launchConfig, surveyConfig, values) {
|
||||||
|
let hasError = false;
|
||||||
|
if (launchConfig.survey_enabled && surveyConfig.spec) {
|
||||||
|
surveyConfig.spec.forEach(question => {
|
||||||
|
const value = values[`survey_${question.variable}`];
|
||||||
|
const isTextField = ['text', 'textarea'].includes(question.type);
|
||||||
|
const isNumeric = ['integer', 'float'].includes(question.type);
|
||||||
|
if (isTextField && (value || value === 0)) {
|
||||||
|
if (
|
||||||
|
(question.min && value.length < question.min) ||
|
||||||
|
(question.max && value.length > question.max)
|
||||||
|
) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNumeric && (value || value === 0)) {
|
||||||
|
if (value < question.min || value > question.max) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (question.required && !value && value !== 0) {
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,43 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useFormikContext } from 'formik';
|
import { useFormikContext } from 'formik';
|
||||||
import useInventoryStep from './steps/useInventoryStep';
|
import useInventoryStep from './steps/useInventoryStep';
|
||||||
import useCredentialsStep from './steps/useCredentialsStep';
|
import useCredentialsStep from './steps/useCredentialsStep';
|
||||||
|
import useCredentialPasswordsStep from './steps/useCredentialPasswordsStep';
|
||||||
import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
import useOtherPromptsStep from './steps/useOtherPromptsStep';
|
||||||
import useSurveyStep from './steps/useSurveyStep';
|
import useSurveyStep from './steps/useSurveyStep';
|
||||||
import usePreviewStep from './steps/usePreviewStep';
|
import usePreviewStep from './steps/usePreviewStep';
|
||||||
|
|
||||||
|
function showCredentialPasswordsStep(credentials = [], launchConfig) {
|
||||||
|
if (
|
||||||
|
!launchConfig?.ask_credential_on_launch &&
|
||||||
|
launchConfig?.passwords_needed_to_start
|
||||||
|
) {
|
||||||
|
return launchConfig.passwords_needed_to_start.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let credentialPasswordStepRequired = false;
|
||||||
|
|
||||||
|
credentials.forEach(credential => {
|
||||||
|
if (!credential.inputs) {
|
||||||
|
const launchConfigCredential = launchConfig.defaults.credentials.find(
|
||||||
|
defaultCred => defaultCred.id === credential.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (launchConfigCredential?.passwords_needed.length > 0) {
|
||||||
|
credentialPasswordStepRequired = true;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
credential?.inputs?.password === 'ASK' ||
|
||||||
|
credential?.inputs?.become_password === 'ASK' ||
|
||||||
|
credential?.inputs?.ssh_key_unlock === 'ASK' ||
|
||||||
|
credential?.inputs?.vault_password === 'ASK'
|
||||||
|
) {
|
||||||
|
credentialPasswordStepRequired = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return credentialPasswordStepRequired;
|
||||||
|
}
|
||||||
|
|
||||||
export default function useLaunchSteps(
|
export default function useLaunchSteps(
|
||||||
launchConfig,
|
launchConfig,
|
||||||
surveyConfig,
|
surveyConfig,
|
||||||
@@ -14,14 +47,21 @@ export default function useLaunchSteps(
|
|||||||
) {
|
) {
|
||||||
const [visited, setVisited] = useState({});
|
const [visited, setVisited] = useState({});
|
||||||
const [isReady, setIsReady] = useState(false);
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
const { touched, values: formikValues } = useFormikContext();
|
||||||
const steps = [
|
const steps = [
|
||||||
useInventoryStep(launchConfig, resource, i18n, visited),
|
useInventoryStep(launchConfig, resource, i18n, visited),
|
||||||
useCredentialsStep(launchConfig, resource, i18n),
|
useCredentialsStep(launchConfig, resource, i18n),
|
||||||
|
useCredentialPasswordsStep(
|
||||||
|
launchConfig,
|
||||||
|
i18n,
|
||||||
|
showCredentialPasswordsStep(formikValues.credentials, launchConfig),
|
||||||
|
visited
|
||||||
|
),
|
||||||
useOtherPromptsStep(launchConfig, resource, i18n),
|
useOtherPromptsStep(launchConfig, resource, i18n),
|
||||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||||
];
|
];
|
||||||
const { resetForm } = useFormikContext();
|
const { resetForm } = useFormikContext();
|
||||||
const hasErrors = steps.some(step => step.formError);
|
const hasErrors = steps.some(step => step.hasError);
|
||||||
|
|
||||||
steps.push(
|
steps.push(
|
||||||
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
|
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
|
||||||
@@ -38,16 +78,26 @@ export default function useLaunchSteps(
|
|||||||
...cur.initialValues,
|
...cur.initialValues,
|
||||||
};
|
};
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
|
const newFormValues = { ...initialValues };
|
||||||
|
|
||||||
|
Object.keys(formikValues).forEach(formikValueKey => {
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(newFormValues, formikValueKey)
|
||||||
|
) {
|
||||||
|
newFormValues[formikValueKey] = formikValues[formikValueKey];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
resetForm({
|
resetForm({
|
||||||
values: {
|
values: newFormValues,
|
||||||
...initialValues,
|
touched,
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsReady(true);
|
setIsReady(true);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [stepsAreReady]);
|
}, [formikValues.credentials, stepsAreReady]);
|
||||||
|
|
||||||
const stepWithError = steps.find(s => s.contentError);
|
const stepWithError = steps.find(s => s.contentError);
|
||||||
const contentError = stepWithError ? stepWithError.contentError : null;
|
const contentError = stepWithError ? stepWithError.contentError : null;
|
||||||
@@ -55,20 +105,26 @@ export default function useLaunchSteps(
|
|||||||
return {
|
return {
|
||||||
steps: pfSteps,
|
steps: pfSteps,
|
||||||
isReady,
|
isReady,
|
||||||
visitStep: stepId =>
|
validateStep: stepId => {
|
||||||
|
steps.find(s => s?.step?.id === stepId).validate();
|
||||||
|
},
|
||||||
|
visitStep: (prevStepId, setFieldTouched) => {
|
||||||
setVisited({
|
setVisited({
|
||||||
...visited,
|
...visited,
|
||||||
[stepId]: true,
|
[prevStepId]: true,
|
||||||
}),
|
});
|
||||||
visitAllSteps: setFieldsTouched => {
|
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||||
|
},
|
||||||
|
visitAllSteps: setFieldTouched => {
|
||||||
setVisited({
|
setVisited({
|
||||||
inventory: true,
|
inventory: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
|
credentialPasswords: true,
|
||||||
other: true,
|
other: true,
|
||||||
survey: true,
|
survey: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
});
|
});
|
||||||
steps.forEach(s => s.setTouched(setFieldsTouched));
|
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||||
},
|
},
|
||||||
contentError,
|
contentError,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function NodeModalForm({
|
|||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useContext(WorkflowDispatchContext);
|
const dispatch = useContext(WorkflowDispatchContext);
|
||||||
const { values, setTouched, validateForm } = useFormikContext();
|
const { values, setFieldTouched } = useFormikContext();
|
||||||
|
|
||||||
const [triggerNext, setTriggerNext] = useState(0);
|
const [triggerNext, setTriggerNext] = useState(0);
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ function NodeModalForm({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
steps: promptSteps,
|
steps: promptSteps,
|
||||||
|
validateStep,
|
||||||
visitStep,
|
visitStep,
|
||||||
visitAllSteps,
|
visitAllSteps,
|
||||||
contentError,
|
contentError,
|
||||||
@@ -192,24 +193,27 @@ function NodeModalForm({
|
|||||||
onSave={() => {
|
onSave={() => {
|
||||||
handleSaveNode();
|
handleSaveNode();
|
||||||
}}
|
}}
|
||||||
|
onBack={async nextStep => {
|
||||||
|
validateStep(nextStep.id);
|
||||||
|
}}
|
||||||
onGoToStep={async (nextStep, prevStep) => {
|
onGoToStep={async (nextStep, prevStep) => {
|
||||||
if (nextStep.id === 'preview') {
|
if (nextStep.id === 'preview') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
visitStep(prevStep.prevId, setFieldTouched);
|
||||||
|
validateStep(nextStep.id);
|
||||||
}
|
}
|
||||||
await validateForm();
|
|
||||||
}}
|
}}
|
||||||
steps={promptSteps}
|
steps={promptSteps}
|
||||||
css="overflow: scroll"
|
css="overflow: scroll"
|
||||||
title={title}
|
title={title}
|
||||||
onNext={async (nextStep, prevStep) => {
|
onNext={async (nextStep, prevStep) => {
|
||||||
if (nextStep.id === 'preview') {
|
if (nextStep.id === 'preview') {
|
||||||
visitAllSteps(setTouched);
|
visitAllSteps(setFieldTouched);
|
||||||
} else {
|
} else {
|
||||||
visitStep(prevStep.prevId);
|
visitStep(prevStep.prevId, setFieldTouched);
|
||||||
|
validateStep(nextStep.id);
|
||||||
}
|
}
|
||||||
await validateForm();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,12 +17,11 @@ export default function useNodeTypeStep(i18n) {
|
|||||||
initialValues: getInitialValues(),
|
initialValues: getInitialValues(),
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: meta.error,
|
hasError: !!meta.error,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('nodeType', true, false);
|
||||||
inventory: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ export default function useRunTypeStep(i18n, askLinkType) {
|
|||||||
initialValues: askLinkType ? { linkType: 'success' } : {},
|
initialValues: askLinkType ? { linkType: 'success' } : {},
|
||||||
isReady: true,
|
isReady: true,
|
||||||
contentError: null,
|
contentError: null,
|
||||||
formError: meta.error,
|
hasError: !!meta.error,
|
||||||
setTouched: setFieldsTouched => {
|
setTouched: setFieldTouched => {
|
||||||
setFieldsTouched({
|
setFieldTouched('linkType', true, false);
|
||||||
inventory: true,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
validate: () => {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function getStep(askLinkType, meta, i18n) {
|
function getStep(askLinkType, meta, i18n) {
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ export default function useWorkflowNodeSteps(
|
|||||||
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasErrors = steps.some(step => step.formError);
|
const hasErrors = steps.some(step => step.hasError);
|
||||||
|
|
||||||
steps.push(
|
steps.push(
|
||||||
usePreviewStep(
|
usePreviewStep(
|
||||||
launchConfig,
|
launchConfig,
|
||||||
@@ -250,12 +251,17 @@ export default function useWorkflowNodeSteps(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
steps: pfSteps,
|
steps: pfSteps,
|
||||||
visitStep: stepId =>
|
validateStep: stepId => {
|
||||||
|
steps.find(s => s?.step?.id === stepId).validate();
|
||||||
|
},
|
||||||
|
visitStep: (prevStepId, setFieldTouched) => {
|
||||||
setVisited({
|
setVisited({
|
||||||
...visited,
|
...visited,
|
||||||
[stepId]: true,
|
[prevStepId]: true,
|
||||||
}),
|
});
|
||||||
visitAllSteps: setFieldsTouched => {
|
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
|
||||||
|
},
|
||||||
|
visitAllSteps: setFieldTouched => {
|
||||||
setVisited({
|
setVisited({
|
||||||
inventory: true,
|
inventory: true,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
@@ -263,7 +269,7 @@ export default function useWorkflowNodeSteps(
|
|||||||
survey: true,
|
survey: true,
|
||||||
preview: true,
|
preview: true,
|
||||||
});
|
});
|
||||||
steps.forEach(s => s.setTouched(setFieldsTouched));
|
steps.forEach(s => s.setTouched(setFieldTouched));
|
||||||
},
|
},
|
||||||
contentError,
|
contentError,
|
||||||
};
|
};
|
||||||
|
|||||||
29
awx/ui_next/src/util/prompt/getCredentialPasswords.js
Normal file
29
awx/ui_next/src/util/prompt/getCredentialPasswords.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export default function getCredentialPasswords(values) {
|
||||||
|
const credentialPasswords = {};
|
||||||
|
Object.keys(values)
|
||||||
|
.filter(valueKey => valueKey.startsWith('credentialPassword'))
|
||||||
|
.forEach(credentialValueKey => {
|
||||||
|
if (credentialValueKey === 'credentialPasswordSsh') {
|
||||||
|
credentialPasswords.ssh_password = values[credentialValueKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialValueKey === 'credentialPasswordPrivilegeEscalation') {
|
||||||
|
credentialPasswords.become_password = values[credentialValueKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialValueKey === 'credentialPasswordPrivateKeyPassphrase') {
|
||||||
|
credentialPasswords.ssh_key_unlock = values[credentialValueKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (credentialValueKey.startsWith('credentialPasswordVault_')) {
|
||||||
|
const vaultId = credentialValueKey.split('credentialPasswordVault_')[1];
|
||||||
|
if (vaultId.length > 0) {
|
||||||
|
credentialPasswords[`vault_password.${vaultId}`] =
|
||||||
|
values[credentialValueKey];
|
||||||
|
} else {
|
||||||
|
credentialPasswords.vault_password = values[credentialValueKey];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return credentialPasswords;
|
||||||
|
}
|
||||||
66
awx/ui_next/src/util/prompt/getCredentialPasswords.test.js
Normal file
66
awx/ui_next/src/util/prompt/getCredentialPasswords.test.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import getCredentialPasswords from './getCredentialPasswords';
|
||||||
|
|
||||||
|
describe('getCredentialPasswords', () => {
|
||||||
|
test('should handle ssh password', () => {
|
||||||
|
expect(
|
||||||
|
getCredentialPasswords({
|
||||||
|
credentialPasswordSsh: 'foobar',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
ssh_password: 'foobar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('should handle become password', () => {
|
||||||
|
expect(
|
||||||
|
getCredentialPasswords({
|
||||||
|
credentialPasswordPrivilegeEscalation: 'foobar',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
become_password: 'foobar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('should handle ssh key unlock', () => {
|
||||||
|
expect(
|
||||||
|
getCredentialPasswords({
|
||||||
|
credentialPasswordPrivateKeyPassphrase: 'foobar',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
ssh_key_unlock: 'foobar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('should handle vault password with identifier', () => {
|
||||||
|
expect(
|
||||||
|
getCredentialPasswords({
|
||||||
|
credentialPasswordVault_1: 'foobar',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
'vault_password.1': 'foobar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('should handle vault password without identifier', () => {
|
||||||
|
expect(
|
||||||
|
getCredentialPasswords({
|
||||||
|
credentialPasswordVault_: 'foobar',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
vault_password: 'foobar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
test('should handle all password types', () => {
|
||||||
|
expect(
|
||||||
|
getCredentialPasswords({
|
||||||
|
credentialPasswordSsh: '1',
|
||||||
|
credentialPasswordPrivilegeEscalation: '2',
|
||||||
|
credentialPasswordPrivateKeyPassphrase: '3',
|
||||||
|
credentialPasswordVault_: '4',
|
||||||
|
credentialPasswordVault_1: '5',
|
||||||
|
})
|
||||||
|
).toEqual({
|
||||||
|
ssh_password: '1',
|
||||||
|
become_password: '2',
|
||||||
|
ssh_key_unlock: '3',
|
||||||
|
vault_password: '4',
|
||||||
|
'vault_password.1': '5',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user