Add support for password prompting on job launch

This commit is contained in:
mabashian 2021-01-06 13:17:49 -05:00
parent 3a467067f3
commit 448e49ae43
21 changed files with 1481 additions and 181 deletions

View File

@ -8,7 +8,7 @@ import PasswordInput from './PasswordInput';
function PasswordField(props) {
const { id, name, label, validate, isRequired, helperText } = props;
const [, meta] = useField({ name, validate });
const isValid = !(meta.touched && meta.error);
const isValid = !meta.touched || (meta.value && meta.value !== '');
return (
<FormGroup

View File

@ -12,7 +12,15 @@ import {
import { EyeIcon, EyeSlashIcon } from '@patternfly/react-icons';
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 [field, meta] = useField({ name, validate });
@ -38,6 +46,7 @@ function PasswordInput(props) {
</Button>
</Tooltip>
<TextInput
autoComplete={autocomplete}
id={id}
placeholder={field.value === '$encrypted$' ? 'ENCRYPTED' : undefined}
{...field}
@ -55,6 +64,7 @@ function PasswordInput(props) {
}
PasswordInput.propTypes = {
autocomplete: PropTypes.string,
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
validate: PropTypes.func,
@ -63,6 +73,7 @@ PasswordInput.propTypes = {
};
PasswordInput.defaultProps = {
autocomplete: 'new-password',
validate: () => {},
isRequired: false,
isDisabled: false,

View File

@ -25,6 +25,8 @@ function canLaunchWithoutPrompt(launchData) {
!launchData.ask_limit_on_launch &&
!launchData.ask_scm_branch_on_launch &&
!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.length === 0)
);

View File

@ -7,6 +7,7 @@ import ContentError from '../ContentError';
import ContentLoading from '../ContentLoading';
import { useDismissableError } from '../../util/useRequest';
import mergeExtraVars from '../../util/prompt/mergeExtraVars';
import getCredentialPasswords from '../../util/prompt/getCredentialPasswords';
import getSurveyValues from '../../util/prompt/getSurveyValues';
import useLaunchSteps from './useLaunchSteps';
import AlertModal from '../AlertModal';
@ -19,17 +20,18 @@ function PromptModalForm({
resource,
surveyConfig,
}) {
const { values, setTouched, validateForm } = useFormikContext();
const { setFieldTouched, values } = useFormikContext();
const {
steps,
isReady,
validateStep,
visitStep,
visitAllSteps,
contentError,
} = useLaunchSteps(launchConfig, surveyConfig, resource, i18n);
const handleSave = () => {
const handleSubmit = () => {
const postValues = {};
const setValue = (key, value) => {
if (typeof value !== 'undefined' && value !== null) {
@ -37,6 +39,8 @@ function PromptModalForm({
}
};
const surveyValues = getSurveyValues(values);
const credentialPasswords = getCredentialPasswords(values);
setValue('credential_passwords', credentialPasswords);
setValue('inventory_id', values.inventory?.id);
setValue(
'credentials',
@ -75,22 +79,25 @@ function PromptModalForm({
<Wizard
isOpen
onClose={onCancel}
onSave={handleSave}
onSave={handleSubmit}
onBack={async nextStep => {
validateStep(nextStep.id);
}}
onNext={async (nextStep, prevStep) => {
if (nextStep.id === 'preview') {
visitAllSteps(setTouched);
visitAllSteps(setFieldTouched);
} else {
visitStep(prevStep.prevId);
visitStep(prevStep.prevId, setFieldTouched);
validateStep(nextStep.id);
}
await validateForm();
}}
onGoToStep={async (nextStep, prevStep) => {
if (nextStep.id === 'preview') {
visitAllSteps(setTouched);
visitAllSteps(setFieldTouched);
} else {
visitStep(prevStep.prevId);
visitStep(prevStep.prevId, setFieldTouched);
validateStep(nextStep.id);
}
await validateForm();
}}
title={i18n._(t`Prompts`)}
steps={

View File

@ -82,8 +82,26 @@ describe('LaunchPrompt', () => {
ask_credential_on_launch: true,
ask_scm_branch_on_launch: 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}
onCancel={noop}
surveyConfig={{
@ -110,12 +128,13 @@ describe('LaunchPrompt', () => {
const wizard = await waitForElement(wrapper, 'Wizard');
const steps = wizard.prop('steps');
expect(steps).toHaveLength(5);
expect(steps).toHaveLength(6);
expect(steps[0].name.props.children).toEqual('Inventory');
expect(steps[1].name.props.children).toEqual('Credentials');
expect(steps[2].name.props.children).toEqual('Other prompts');
expect(steps[3].name.props.children).toEqual('Survey');
expect(steps[4].name.props.children).toEqual('Preview');
expect(steps[2].name.props.children).toEqual('Credential passwords');
expect(steps[3].name.props.children).toEqual('Other prompts');
expect(steps[4].name.props.children).toEqual('Survey');
expect(steps[5].name.props.children).toEqual('Preview');
});
test('should add inventory step', async () => {

View File

@ -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);

View File

@ -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);
});
});
});

View File

@ -3,6 +3,7 @@ import { useHistory } from 'react-router-dom';
import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import { Alert } from '@patternfly/react-core';
import { InventoriesAPI } from '../../../api';
import { getQSConfig, parseQueryString } from '../../../util/qs';
import useRequest from '../../../util/useRequest';
@ -17,9 +18,10 @@ const QS_CONFIG = getQSConfig('inventory', {
});
function InventoryStep({ i18n }) {
const [field, , helpers] = useField({
const [field, meta, helpers] = useField({
name: 'inventory',
});
const history = useHistory();
const {
@ -65,40 +67,45 @@ function InventoryStep({ i18n }) {
}
return (
<OptionsList
value={field.value ? [field.value] : []}
options={inventories}
optionCount={count}
searchColumns={[
{
name: i18n._(t`Name`),
key: 'name__icontains',
isDefault: true,
},
{
name: i18n._(t`Created By (Username)`),
key: 'created_by__username__icontains',
},
{
name: i18n._(t`Modified By (Username)`),
key: 'modified_by__username__icontains',
},
]}
sortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
header={i18n._(t`Inventory`)}
name="inventory"
qsConfig={QS_CONFIG}
readOnly
selectItem={helpers.setValue}
deselectItem={() => field.onChange(null)}
/>
<>
<OptionsList
value={field.value ? [field.value] : []}
options={inventories}
optionCount={count}
searchColumns={[
{
name: i18n._(t`Name`),
key: 'name__icontains',
isDefault: true,
},
{
name: i18n._(t`Created By (Username)`),
key: 'created_by__username__icontains',
},
{
name: i18n._(t`Modified By (Username)`),
key: 'modified_by__username__icontains',
},
]}
sortColumns={[
{
name: i18n._(t`Name`),
key: 'name',
},
]}
searchableKeys={searchableKeys}
relatedSearchableKeys={relatedSearchableKeys}
header={i18n._(t`Inventory`)}
name="inventory"
qsConfig={QS_CONFIG}
readOnly
selectItem={helpers.setValue}
deselectItem={() => field.onChange(null)}
/>
{meta.touched && meta.error && (
<Alert variant="danger" isInline title={meta.error} />
)}
</>
);
}

View File

@ -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;
}

View File

@ -9,15 +9,13 @@ export default function useCredentialsStep(launchConfig, resource, i18n) {
return {
step: getStep(launchConfig, i18n),
initialValues: getInitialValues(launchConfig, resource),
validate: () => ({}),
isReady: true,
contentError: null,
formError: null,
setTouched: setFieldsTouched => {
setFieldsTouched({
credentials: true,
});
hasError: false,
setTouched: setFieldTouched => {
setFieldTouched('credentials', true, false);
},
validate: () => {},
};
}

View File

@ -12,20 +12,27 @@ export default function useInventoryStep(
i18n,
visitedSteps
) {
const [, meta] = useField('inventory');
const [, meta, helpers] = useField('inventory');
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 {
step: getStep(launchConfig, i18n, formError),
initialValues: getInitialValues(launchConfig, resource),
isReady: true,
contentError: null,
formError: launchConfig.ask_inventory_on_launch && formError,
setTouched: setFieldsTouched => {
setFieldsTouched({
inventory: true,
});
hasError: launchConfig.ask_inventory_on_launch && formError,
setTouched: setFieldTouched => {
setFieldTouched('inventory', true, false);
},
validate: () => {
if (meta.touched && !meta.value && resource.type === 'job_template') {
helpers.setError(i18n._(t`An inventory must be selected`));
}
},
};
}

View File

@ -22,18 +22,19 @@ export default function useOtherPromptsStep(launchConfig, resource, i18n) {
initialValues: getInitialValues(launchConfig, resource),
isReady: true,
contentError: null,
formError: null,
setTouched: setFieldsTouched => {
setFieldsTouched({
job_type: true,
limit: true,
verbosity: true,
diff_mode: true,
job_tags: true,
skip_tags: true,
extra_vars: true,
});
hasError: false,
setTouched: setFieldTouched => {
[
'job_type',
'limit',
'verbosity',
'diff_mode',
'job_tags',
'skip_tags',
'extra_vars',
].forEach(field => setFieldTouched(field, true, false));
},
validate: () => {},
};
}

View File

@ -35,9 +35,9 @@ export default function usePreviewStep(
}
: null,
initialValues: {},
validate: () => ({}),
isReady: true,
error: null,
setTouched: () => {},
validate: () => {},
};
}

View File

@ -13,89 +13,51 @@ export default function useSurveyStep(
i18n,
visitedSteps
) {
const { values } = useFormikContext();
const errors = {};
const validate = () => {
if (!launchConfig.survey_enabled || !surveyConfig?.spec) {
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;
const { setFieldError, values } = useFormikContext();
const hasError =
Object.keys(visitedSteps).includes(STEP_ID) &&
checkForError(launchConfig, surveyConfig, values);
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),
validate,
surveyConfig,
isReady: true,
contentError: null,
formError,
setTouched: setFieldsTouched => {
hasError,
setTouched: setFieldTouched => {
if (!surveyConfig?.spec) {
return;
}
const fields = {};
surveyConfig.spec.forEach(question => {
fields[`survey_${question.variable}`] = true;
setFieldTouched(`survey_${question.variable}`, true, false);
});
setFieldsTouched(fields);
},
};
}
function validateField(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 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,
validate: () => {
if (launchConfig.survey_enabled && surveyConfig.spec) {
surveyConfig.spec.forEach(question => {
const errMessage = validateSurveyField(
question,
values[`survey_${question.variable}`],
i18n
);
if (errMessage) {
setFieldError(`survey_${question.variable}`, errMessage);
}
});
}
},
};
}
@ -133,3 +95,56 @@ function getInitialValues(launchConfig, surveyConfig, resource) {
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;
}

View File

@ -2,10 +2,43 @@ import { useState, useEffect } from 'react';
import { useFormikContext } from 'formik';
import useInventoryStep from './steps/useInventoryStep';
import useCredentialsStep from './steps/useCredentialsStep';
import useCredentialPasswordsStep from './steps/useCredentialPasswordsStep';
import useOtherPromptsStep from './steps/useOtherPromptsStep';
import useSurveyStep from './steps/useSurveyStep';
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(
launchConfig,
surveyConfig,
@ -14,14 +47,21 @@ export default function useLaunchSteps(
) {
const [visited, setVisited] = useState({});
const [isReady, setIsReady] = useState(false);
const { touched, values: formikValues } = useFormikContext();
const steps = [
useInventoryStep(launchConfig, resource, i18n, visited),
useCredentialsStep(launchConfig, resource, i18n),
useCredentialPasswordsStep(
launchConfig,
i18n,
showCredentialPasswordsStep(formikValues.credentials, launchConfig),
visited
),
useOtherPromptsStep(launchConfig, resource, i18n),
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
];
const { resetForm } = useFormikContext();
const hasErrors = steps.some(step => step.formError);
const hasErrors = steps.some(step => step.hasError);
steps.push(
usePreviewStep(launchConfig, i18n, resource, surveyConfig, hasErrors, true)
@ -38,16 +78,26 @@ export default function useLaunchSteps(
...cur.initialValues,
};
}, {});
const newFormValues = { ...initialValues };
Object.keys(formikValues).forEach(formikValueKey => {
if (
Object.prototype.hasOwnProperty.call(newFormValues, formikValueKey)
) {
newFormValues[formikValueKey] = formikValues[formikValueKey];
}
});
resetForm({
values: {
...initialValues,
},
values: newFormValues,
touched,
});
setIsReady(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stepsAreReady]);
}, [formikValues.credentials, stepsAreReady]);
const stepWithError = steps.find(s => s.contentError);
const contentError = stepWithError ? stepWithError.contentError : null;
@ -55,20 +105,26 @@ export default function useLaunchSteps(
return {
steps: pfSteps,
isReady,
visitStep: stepId =>
validateStep: stepId => {
steps.find(s => s?.step?.id === stepId).validate();
},
visitStep: (prevStepId, setFieldTouched) => {
setVisited({
...visited,
[stepId]: true,
}),
visitAllSteps: setFieldsTouched => {
[prevStepId]: true,
});
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
},
visitAllSteps: setFieldTouched => {
setVisited({
inventory: true,
credentials: true,
credentialPasswords: true,
other: true,
survey: true,
preview: true,
});
steps.forEach(s => s.setTouched(setFieldsTouched));
steps.forEach(s => s.setTouched(setFieldTouched));
},
contentError,
};

View File

@ -44,7 +44,7 @@ function NodeModalForm({
}) {
const history = useHistory();
const dispatch = useContext(WorkflowDispatchContext);
const { values, setTouched, validateForm } = useFormikContext();
const { values, setFieldTouched } = useFormikContext();
const [triggerNext, setTriggerNext] = useState(0);
@ -60,6 +60,7 @@ function NodeModalForm({
const {
steps: promptSteps,
validateStep,
visitStep,
visitAllSteps,
contentError,
@ -192,24 +193,27 @@ function NodeModalForm({
onSave={() => {
handleSaveNode();
}}
onBack={async nextStep => {
validateStep(nextStep.id);
}}
onGoToStep={async (nextStep, prevStep) => {
if (nextStep.id === 'preview') {
visitAllSteps(setTouched);
visitAllSteps(setFieldTouched);
} else {
visitStep(prevStep.prevId);
visitStep(prevStep.prevId, setFieldTouched);
validateStep(nextStep.id);
}
await validateForm();
}}
steps={promptSteps}
css="overflow: scroll"
title={title}
onNext={async (nextStep, prevStep) => {
if (nextStep.id === 'preview') {
visitAllSteps(setTouched);
visitAllSteps(setFieldTouched);
} else {
visitStep(prevStep.prevId);
visitStep(prevStep.prevId, setFieldTouched);
validateStep(nextStep.id);
}
await validateForm();
}}
/>
);

View File

@ -17,12 +17,11 @@ export default function useNodeTypeStep(i18n) {
initialValues: getInitialValues(),
isReady: true,
contentError: null,
formError: meta.error,
setTouched: setFieldsTouched => {
setFieldsTouched({
inventory: true,
});
hasError: !!meta.error,
setTouched: setFieldTouched => {
setFieldTouched('nodeType', true, false);
},
validate: () => {},
};
}
function getStep(i18n, nodeTypeField, approvalNameField, nodeResourceField) {

View File

@ -14,12 +14,11 @@ export default function useRunTypeStep(i18n, askLinkType) {
initialValues: askLinkType ? { linkType: 'success' } : {},
isReady: true,
contentError: null,
formError: meta.error,
setTouched: setFieldsTouched => {
setFieldsTouched({
inventory: true,
});
hasError: !!meta.error,
setTouched: setFieldTouched => {
setFieldTouched('linkType', true, false);
},
validate: () => {},
};
}
function getStep(askLinkType, meta, i18n) {

View File

@ -194,7 +194,8 @@ export default function useWorkflowNodeSteps(
useSurveyStep(launchConfig, surveyConfig, resource, i18n, visited),
];
const hasErrors = steps.some(step => step.formError);
const hasErrors = steps.some(step => step.hasError);
steps.push(
usePreviewStep(
launchConfig,
@ -250,12 +251,17 @@ export default function useWorkflowNodeSteps(
return {
steps: pfSteps,
visitStep: stepId =>
validateStep: stepId => {
steps.find(s => s?.step?.id === stepId).validate();
},
visitStep: (prevStepId, setFieldTouched) => {
setVisited({
...visited,
[stepId]: true,
}),
visitAllSteps: setFieldsTouched => {
[prevStepId]: true,
});
steps.find(s => s?.step?.id === prevStepId).setTouched(setFieldTouched);
},
visitAllSteps: setFieldTouched => {
setVisited({
inventory: true,
credentials: true,
@ -263,7 +269,7 @@ export default function useWorkflowNodeSteps(
survey: true,
preview: true,
});
steps.forEach(s => s.setTouched(setFieldsTouched));
steps.forEach(s => s.setTouched(setFieldTouched));
},
contentError,
};

View 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;
}

View 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',
});
});
});