Add Content Signature Validation Credential field to Projects Form page and Projects Detail page

This commit is contained in:
Veda Periwal
2022-07-19 22:47:48 -07:00
committed by Rick Elrod
parent f5a2246817
commit e896dc1aa7
13 changed files with 285 additions and 20 deletions

View File

@@ -7,7 +7,15 @@ class CredentialTypes extends Base {
} }
async loadAllTypes( async loadAllTypes(
acceptableKinds = ['machine', 'cloud', 'net', 'ssh', 'vault', 'kubernetes'] acceptableKinds = [
'machine',
'cloud',
'net',
'ssh',
'vault',
'kubernetes',
'cryptography',
]
) { ) {
const pageSize = 200; const pageSize = 200;
// The number of credential types a user can have is unlimited. In practice, it is unlikely for // The number of credential types a user can have is unlimited. In practice, it is unlikely for

View File

@@ -125,6 +125,21 @@ function PromptProjectDetail({ resource }) {
} }
/> />
)} )}
{summary_fields?.signature_validation_credential?.id && (
<Detail
label={t`Content Signature Validation Credential`}
dataCy={`${prefixCy}-content-signature-validation-credential`}
value={
<CredentialChip
key={resource.summary_fields.signature_validation_credential.id}
credential={
resource.summary_fields.signature_validation_credential
}
isReadOnly
/>
}
/>
)}
{optionsList && ( {optionsList && (
<Detail <Detail
label={t`Enabled Options`} label={t`Enabled Options`}

View File

@@ -18,10 +18,20 @@ function ProjectAdd() {
// the API might throw an unexpected error if our creation request // the API might throw an unexpected error if our creation request
// has a zero-length string as its credential field. As a work-around, // has a zero-length string as its credential field. As a work-around,
// normalize falsey credential fields by deleting them. // normalize falsey credential fields by deleting them.
delete values.credential; values.credential = null;
} else { } else if (typeof values.credential.id === 'number') {
values.credential = values.credential.id; values.credential = values.credential.id;
} }
if (values.scm_type === 'git') {
if (!values.signature_validation_credential) {
values.signature_validation_credential = null;
} else if (
typeof values.signature_validation_credential.id === 'number'
) {
values.signature_validation_credential =
values.signature_validation_credential.id;
}
}
setFormSubmitError(null); setFormSubmitError(null);
try { try {
const { const {

View File

@@ -20,6 +20,7 @@ describe('<ProjectAdd />', () => {
scm_clean: true, scm_clean: true,
scm_track_submodules: false, scm_track_submodules: false,
credential: 100, credential: 100,
signature_validation_credential: 200,
local_path: '', local_path: '',
organization: { id: 2, name: 'Bar' }, organization: { id: 2, name: 'Bar' },
scm_update_on_launch: true, scm_update_on_launch: true,
@@ -73,16 +74,32 @@ describe('<ProjectAdd />', () => {
}, },
}; };
const cryptographyCredentialResolve = {
data: {
results: [
{
id: 6,
name: 'GPG Public Key',
kind: 'cryptography',
},
],
count: 1,
},
};
beforeEach(async () => { beforeEach(async () => {
await ProjectsAPI.readOptions.mockImplementation( await ProjectsAPI.readOptions.mockImplementation(
() => projectOptionsResolve () => projectOptionsResolve
); );
await CredentialTypesAPI.read.mockImplementationOnce( await CredentialTypesAPI.read.mockImplementation(
() => scmCredentialResolve () => scmCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementationOnce( await CredentialTypesAPI.read.mockImplementation(
() => insightsCredentialResolve () => insightsCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation(
() => cryptographyCredentialResolve
);
}); });
afterEach(() => { afterEach(() => {
@@ -110,6 +127,7 @@ describe('<ProjectAdd />', () => {
...projectData, ...projectData,
organization: 2, organization: 2,
default_environment: 1, default_environment: 1,
signature_validation_credential: 200,
}); });
}); });

View File

@@ -124,7 +124,6 @@ function ProjectDetail({ project }) {
</TextList> </TextList>
); );
} }
const generateLastJobTooltip = (job) => ( const generateLastJobTooltip = (job) => (
<> <>
<div>{t`MOST RECENT SYNC`}</div> <div>{t`MOST RECENT SYNC`}</div>
@@ -149,6 +148,7 @@ function ProjectDetail({ project }) {
} else if (summary_fields?.last_job) { } else if (summary_fields?.last_job) {
job = summary_fields.last_job; job = summary_fields.last_job;
} }
const getSourceControlUrlHelpText = () => const getSourceControlUrlHelpText = () =>
scm_type === 'git' scm_type === 'git'
? projectHelpText.githubSourceControlUrl ? projectHelpText.githubSourceControlUrl
@@ -234,6 +234,22 @@ function ProjectDetail({ project }) {
label={t`Source Control Refspec`} label={t`Source Control Refspec`}
value={scm_refspec} value={scm_refspec}
/> />
{summary_fields.signature_validation_credential && (
<Detail
label={t`Content Signature Validation Credential`}
helpText={projectHelpText.signatureValidation}
value={
<CredentialChip
key={summary_fields.signature_validation_credential.id}
credential={summary_fields.signature_validation_credential}
isReadOnly
/>
}
isEmpty={
summary_fields.signature_validation_credential.length === 0
}
/>
)}
{summary_fields.credential && ( {summary_fields.credential && (
<Detail <Detail
label={t`Source Control Credential`} label={t`Source Control Credential`}
@@ -244,6 +260,7 @@ function ProjectDetail({ project }) {
isReadOnly isReadOnly
/> />
} }
isEmpty={summary_fields.credential.length === 0}
/> />
)} )}
<Detail <Detail

View File

@@ -46,6 +46,11 @@ describe('<ProjectDetail />', () => {
name: 'qux', name: 'qux',
kind: 'scm', kind: 'scm',
}, },
signature_validation_credential: {
id: 2000,
name: 'svc',
kind: 'cryptography',
},
last_job: { last_job: {
id: 9000, id: 9000,
status: 'successful', status: 'successful',
@@ -78,6 +83,7 @@ describe('<ProjectDetail />', () => {
scm_delete_on_update: true, scm_delete_on_update: true,
scm_track_submodules: true, scm_track_submodules: true,
credential: 100, credential: 100,
signature_validation_credential: 200,
status: 'successful', status: 'successful',
organization: 10, organization: 10,
scm_update_on_launch: true, scm_update_on_launch: true,
@@ -108,6 +114,10 @@ describe('<ProjectDetail />', () => {
'Source Control Credential', 'Source Control Credential',
`Scm: ${mockProject.summary_fields.credential.name}` `Scm: ${mockProject.summary_fields.credential.name}`
); );
assertDetail(
'Content Signature Validation Credential',
`Cryptography: ${mockProject.summary_fields.signature_validation_credential.name}`
);
assertDetail( assertDetail(
'Cache Timeout', 'Cache Timeout',
`${mockProject.scm_update_cache_timeout} Seconds` `${mockProject.scm_update_cache_timeout} Seconds`

View File

@@ -18,10 +18,21 @@ function ProjectEdit({ project }) {
// the API might throw an unexpected error if our creation request // the API might throw an unexpected error if our creation request
// has a zero-length string as its credential field. As a work-around, // has a zero-length string as its credential field. As a work-around,
// normalize falsey credential fields by deleting them. // normalize falsey credential fields by deleting them.
delete values.credential; values.credential = null;
} else { } else if (typeof values.credential.id === 'number') {
values.credential = values.credential.id; values.credential = values.credential.id;
} }
if (values.scm_type === 'git') {
if (!values.signature_validation_credential) {
values.signature_validation_credential = null;
} else if (
typeof values.signature_validation_credential.id === 'number'
) {
values.signature_validation_credential =
values.signature_validation_credential.id;
}
}
try { try {
const { const {
data: { id }, data: { id },

View File

@@ -21,6 +21,7 @@ describe('<ProjectEdit />', () => {
scm_clean: true, scm_clean: true,
scm_track_submodules: false, scm_track_submodules: false,
credential: 100, credential: 100,
signature_validation_credential: 200,
local_path: 'bar', local_path: 'bar',
organization: 2, organization: 2,
scm_update_on_launch: true, scm_update_on_launch: true,
@@ -33,6 +34,12 @@ describe('<ProjectEdit />', () => {
credential_type_id: 5, credential_type_id: 5,
kind: 'insights', kind: 'insights',
}, },
signature_validation_credential: {
id: 200,
credential_type_id: 6,
kind: 'cryptography',
name: 'foo',
},
organization: { organization: {
id: 2, id: 2,
name: 'Default', name: 'Default',
@@ -60,6 +67,7 @@ describe('<ProjectEdit />', () => {
const scmCredentialResolve = { const scmCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 4, id: 4,
@@ -72,6 +80,7 @@ describe('<ProjectEdit />', () => {
const insightsCredentialResolve = { const insightsCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 5, id: 5,
@@ -82,6 +91,19 @@ describe('<ProjectEdit />', () => {
}, },
}; };
const cryptographyCredentialResolve = {
data: {
count: 1,
results: [
{
id: 6,
name: 'GPG Public Key',
kind: 'cryptography',
},
],
},
};
beforeEach(async () => { beforeEach(async () => {
RootAPI.readAssetVariables.mockResolvedValue({ RootAPI.readAssetVariables.mockResolvedValue({
data: { data: {
@@ -91,12 +113,15 @@ describe('<ProjectEdit />', () => {
await ProjectsAPI.readOptions.mockImplementation( await ProjectsAPI.readOptions.mockImplementation(
() => projectOptionsResolve () => projectOptionsResolve
); );
await CredentialTypesAPI.read.mockImplementationOnce( await CredentialTypesAPI.read.mockImplementation(
() => scmCredentialResolve () => scmCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementationOnce( await CredentialTypesAPI.read.mockImplementation(
() => insightsCredentialResolve () => insightsCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation(
() => cryptographyCredentialResolve
);
}); });
afterEach(() => { afterEach(() => {

View File

@@ -105,6 +105,10 @@ const projectHelpTextStrings = {
you can input tags, commit hashes, and arbitrary refs. Some you can input tags, commit hashes, and arbitrary refs. Some
commit hashes and refs may not be available unless you also commit hashes and refs may not be available unless you also
provide a custom refspec.`, provide a custom refspec.`,
signatureValidation: t`Enable content signing to verify that the content
has remained secure when a project is synced.
If the content has been tampered with, the
job will not run.`,
options: { options: {
clean: t`Remove any local modifications prior to performing an update.`, clean: t`Remove any local modifications prior to performing an update.`,
delete: t`Delete the local repository in its entirety prior to delete: t`Delete the local repository in its entirety prior to

View File

@@ -37,15 +37,22 @@ const fetchCredentials = async (credential) => {
results: [insightsCredentialType], results: [insightsCredentialType],
}, },
}, },
{
data: {
results: [cryptographyCredentialType],
},
},
] = await Promise.all([ ] = await Promise.all([
CredentialTypesAPI.read({ kind: 'scm' }), CredentialTypesAPI.read({ kind: 'scm' }),
CredentialTypesAPI.read({ name: 'Insights' }), CredentialTypesAPI.read({ name: 'Insights' }),
CredentialTypesAPI.read({ kind: 'cryptography' }),
]); ]);
if (!credential) { if (!credential) {
return { return {
scm: { typeId: scmCredentialType.id }, scm: { typeId: scmCredentialType.id },
insights: { typeId: insightsCredentialType.id }, insights: { typeId: insightsCredentialType.id },
cryptography: { typeId: cryptographyCredentialType.id },
}; };
} }
@@ -60,6 +67,13 @@ const fetchCredentials = async (credential) => {
value: value:
credential_type_id === insightsCredentialType.id ? credential : null, credential_type_id === insightsCredentialType.id ? credential : null,
}, },
cryptography: {
typeId: cryptographyCredentialType.id,
value:
credential_type_id === cryptographyCredentialType.id
? credential
: null,
},
}; };
}; };
@@ -69,7 +83,9 @@ function ProjectFormFields({
project_local_paths, project_local_paths,
formik, formik,
setCredentials, setCredentials,
setSignatureValidationCredentials,
credentials, credentials,
signatureValidationCredentials,
scmTypeOptions, scmTypeOptions,
setScmSubFormState, setScmSubFormState,
scmSubFormState, scmSubFormState,
@@ -79,6 +95,7 @@ function ProjectFormFields({
scm_branch: '', scm_branch: '',
scm_refspec: '', scm_refspec: '',
credential: '', credential: '',
signature_validation_credential: '',
scm_clean: false, scm_clean: false,
scm_delete_on_update: false, scm_delete_on_update: false,
scm_track_submodules: false, scm_track_submodules: false,
@@ -86,7 +103,6 @@ function ProjectFormFields({
allow_override: false, allow_override: false,
scm_update_cache_timeout: 0, scm_update_cache_timeout: 0,
}; };
const { setFieldValue, setFieldTouched } = useFormikContext(); const { setFieldValue, setFieldTouched } = useFormikContext();
const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({ const [scmTypeField, scmTypeMeta, scmTypeHelpers] = useField({
@@ -147,6 +163,19 @@ function ProjectFormFields({
[credentials, setCredentials] [credentials, setCredentials]
); );
const handleSignatureValidationCredentialSelection = useCallback(
(type, value) => {
setSignatureValidationCredentials({
...signatureValidationCredentials,
[type]: {
...signatureValidationCredentials[type],
value,
},
});
},
[signatureValidationCredentials, setSignatureValidationCredentials]
);
const handleOrganizationUpdate = useCallback( const handleOrganizationUpdate = useCallback(
(value) => { (value) => {
setFieldValue('organization', value); setFieldValue('organization', value);
@@ -259,7 +288,13 @@ function ProjectFormFields({
git: ( git: (
<GitSubForm <GitSubForm
credential={credentials.scm} credential={credentials.scm}
signature_validation_credential={
signatureValidationCredentials.cryptography
}
onCredentialSelection={handleCredentialSelection} onCredentialSelection={handleCredentialSelection}
onSignatureValidationCredentialSelection={
handleSignatureValidationCredentialSelection
}
scmUpdateOnLaunch={formik.values.scm_update_on_launch} scmUpdateOnLaunch={formik.values.scm_update_on_launch}
/> />
), ),
@@ -295,7 +330,6 @@ function ProjectFormFields({
</> </>
); );
} }
function ProjectForm({ project, submitError, ...props }) { function ProjectForm({ project, submitError, ...props }) {
const { handleCancel, handleSubmit } = props; const { handleCancel, handleSubmit } = props;
const { summary_fields = {} } = project; const { summary_fields = {} } = project;
@@ -307,6 +341,7 @@ function ProjectForm({ project, submitError, ...props }) {
scm_branch: '', scm_branch: '',
scm_refspec: '', scm_refspec: '',
credential: '', credential: '',
signature_validation_credential: '',
scm_clean: false, scm_clean: false,
scm_delete_on_update: false, scm_delete_on_update: false,
scm_track_submodules: false, scm_track_submodules: false,
@@ -318,12 +353,22 @@ function ProjectForm({ project, submitError, ...props }) {
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
scm: { typeId: null, value: null }, scm: { typeId: null, value: null },
insights: { typeId: null, value: null }, insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null },
}); });
const [signatureValidationCredentials, setSignatureValidationCredentials] =
useState({
scm: { typeId: null, value: null },
insights: { typeId: null, value: null },
cryptography: { typeId: null, value: null },
});
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
try { try {
const credentialResponse = fetchCredentials(summary_fields.credential); const credentialResponse = fetchCredentials(summary_fields.credential);
const signatureValidationCredentialResponse = fetchCredentials(
summary_fields.signature_validation_credential
);
const { const {
data: { data: {
actions: { actions: {
@@ -335,6 +380,9 @@ function ProjectForm({ project, submitError, ...props }) {
} = await ProjectsAPI.readOptions(); } = await ProjectsAPI.readOptions();
setCredentials(await credentialResponse); setCredentials(await credentialResponse);
setSignatureValidationCredentials(
await signatureValidationCredentialResponse
);
setScmTypeOptions(choices); setScmTypeOptions(choices);
} catch (error) { } catch (error) {
setContentError(error); setContentError(error);
@@ -344,7 +392,10 @@ function ProjectForm({ project, submitError, ...props }) {
} }
fetchData(); fetchData();
}, [summary_fields.credential]); }, [
summary_fields.credential,
summary_fields.signature_validation_credential,
]);
if (isLoading) { if (isLoading) {
return <ContentLoading />; return <ContentLoading />;
@@ -378,6 +429,8 @@ function ProjectForm({ project, submitError, ...props }) {
scm_update_cache_timeout: project.scm_update_cache_timeout || 0, scm_update_cache_timeout: project.scm_update_cache_timeout || 0,
scm_update_on_launch: project.scm_update_on_launch || false, scm_update_on_launch: project.scm_update_on_launch || false,
scm_url: project.scm_url || '', scm_url: project.scm_url || '',
signature_validation_credential:
project.signature_validation_credential || '',
default_environment: default_environment:
project.summary_fields?.default_environment || null, project.summary_fields?.default_environment || null,
}} }}
@@ -392,7 +445,11 @@ function ProjectForm({ project, submitError, ...props }) {
project_local_paths={project_local_paths} project_local_paths={project_local_paths}
formik={formik} formik={formik}
setCredentials={setCredentials} setCredentials={setCredentials}
setSignatureValidationCredentials={
setSignatureValidationCredentials
}
credentials={credentials} credentials={credentials}
signatureValidationCredentials={signatureValidationCredentials}
scmTypeOptions={scmTypeOptions} scmTypeOptions={scmTypeOptions}
setScmSubFormState={setScmSubFormState} setScmSubFormState={setScmSubFormState}
scmSubFormState={scmSubFormState} scmSubFormState={scmSubFormState}

View File

@@ -19,6 +19,7 @@ describe('<ProjectForm />', () => {
scm_clean: true, scm_clean: true,
scm_track_submodules: false, scm_track_submodules: false,
credential: 100, credential: 100,
signature_validation_credential: 200,
organization: 2, organization: 2,
scm_update_on_launch: true, scm_update_on_launch: true,
scm_update_cache_timeout: 3, scm_update_cache_timeout: 3,
@@ -35,6 +36,12 @@ describe('<ProjectForm />', () => {
id: 2, id: 2,
name: 'Default', name: 'Default',
}, },
signature_validation_credential: {
id: 200,
credential_type_id: 6,
kind: 'cryptography',
name: 'Svc',
},
}, },
}; };
@@ -58,6 +65,7 @@ describe('<ProjectForm />', () => {
const scmCredentialResolve = { const scmCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 4, id: 4,
@@ -70,6 +78,7 @@ describe('<ProjectForm />', () => {
const insightsCredentialResolve = { const insightsCredentialResolve = {
data: { data: {
count: 1,
results: [ results: [
{ {
id: 5, id: 5,
@@ -80,6 +89,19 @@ describe('<ProjectForm />', () => {
}, },
}; };
const cryptographyCredentialResolve = {
data: {
count: 1,
results: [
{
id: 6,
name: 'GPG Public Key',
kind: 'cryptography',
},
],
},
};
beforeEach(async () => { beforeEach(async () => {
RootAPI.readAssetVariables.mockResolvedValue({ RootAPI.readAssetVariables.mockResolvedValue({
data: { data: {
@@ -89,12 +111,15 @@ describe('<ProjectForm />', () => {
await ProjectsAPI.readOptions.mockImplementation( await ProjectsAPI.readOptions.mockImplementation(
() => projectOptionsResolve () => projectOptionsResolve
); );
await CredentialTypesAPI.read.mockImplementationOnce( await CredentialTypesAPI.read.mockImplementation(
() => scmCredentialResolve () => scmCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementationOnce( await CredentialTypesAPI.read.mockImplementation(
() => insightsCredentialResolve () => insightsCredentialResolve
); );
await CredentialTypesAPI.read.mockImplementation(
() => cryptographyCredentialResolve
);
}); });
afterEach(() => { afterEach(() => {
@@ -153,9 +178,17 @@ describe('<ProjectForm />', () => {
expect( expect(
wrapper.find('FormGroup[label="Source Control Refspec"]').length wrapper.find('FormGroup[label="Source Control Refspec"]').length
).toBe(1); ).toBe(1);
expect(
wrapper.find('FormGroup[label="Content Signature Validation Credential"]')
.length
).toBe(1);
expect( expect(
wrapper.find('FormGroup[label="Source Control Credential"]').length wrapper.find('FormGroup[label="Source Control Credential"]').length
).toBe(1); ).toBe(1);
expect(
wrapper.find('FormGroup[label="Content Signature Validation Credential"]')
.length
).toBe(1);
expect(wrapper.find('FormGroup[label="Options"]').length).toBe(1); expect(wrapper.find('FormGroup[label="Options"]').length).toBe(1);
}); });
@@ -177,21 +210,52 @@ describe('<ProjectForm />', () => {
id: 1, id: 1,
name: 'organization', name: 'organization',
}); });
wrapper.find('CredentialLookup').invoke('onBlur')(); wrapper
wrapper.find('CredentialLookup').invoke('onChange')({ .find('CredentialLookup[label="Source Control Credential"]')
.invoke('onBlur')();
wrapper
.find('CredentialLookup[label="Source Control Credential"]')
.invoke('onChange')({
id: 10, id: 10,
name: 'credential', name: 'credential',
}); });
wrapper
.find(
'CredentialLookup[label="Content Signature Validation Credential"]'
)
.invoke('onBlur')();
wrapper
.find(
'CredentialLookup[label="Content Signature Validation Credential"]'
)
.invoke('onChange')({
id: 20,
name: 'signature_validation_credential',
});
}); });
wrapper.update(); wrapper.update();
expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({ expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({
id: 1, id: 1,
name: 'organization', name: 'organization',
}); });
expect(wrapper.find('CredentialLookup').prop('value')).toEqual({ expect(
wrapper
.find('CredentialLookup[label="Source Control Credential"]')
.prop('value')
).toEqual({
id: 10, id: 10,
name: 'credential', name: 'credential',
}); });
expect(
wrapper
.find(
'CredentialLookup[label="Content Signature Validation Credential"]'
)
.prop('value')
).toEqual({
id: 20,
name: 'signature_validation_credential',
});
}); });
test('should display insights credential lookup when source control type is "insights"', async () => { test('should display insights credential lookup when source control type is "insights"', async () => {
@@ -358,7 +422,9 @@ describe('<ProjectForm />', () => {
}); });
test('should display ContentError on throw', async () => { test('should display ContentError on throw', async () => {
CredentialTypesAPI.read = () => Promise.reject(new Error()); CredentialTypesAPI.read.mockImplementationOnce(() =>
Promise.reject(new Error())
);
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} /> <ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />

View File

@@ -1,6 +1,8 @@
import 'styled-components/macro'; import 'styled-components/macro';
import React from 'react'; import React, { useCallback } from 'react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useFormikContext } from 'formik';
import CredentialLookup from 'components/Lookup/CredentialLookup';
import FormField from 'components/FormField'; import FormField from 'components/FormField';
import getDocsBaseUrl from 'util/getDocsBaseUrl'; import getDocsBaseUrl from 'util/getDocsBaseUrl';
import { useConfig } from 'contexts/Config'; import { useConfig } from 'contexts/Config';
@@ -16,9 +18,22 @@ import projectHelpStrings from '../Project.helptext';
const GitSubForm = ({ const GitSubForm = ({
credential, credential,
signature_validation_credential,
onCredentialSelection, onCredentialSelection,
onSignatureValidationCredentialSelection,
scmUpdateOnLaunch, scmUpdateOnLaunch,
}) => { }) => {
const { setFieldValue, setFieldTouched } = useFormikContext();
const onCredentialChange = useCallback(
(value) => {
onSignatureValidationCredentialSelection('cryptography', value);
setFieldValue('signature_validation_credential', value);
setFieldTouched('signature_validation_credential', true, false);
},
[onSignatureValidationCredentialSelection, setFieldValue, setFieldTouched]
);
const docsURL = `${getDocsBaseUrl( const docsURL = `${getDocsBaseUrl(
useConfig() useConfig()
)}/html/userguide/projects.html#manage-playbooks-using-source-control`; )}/html/userguide/projects.html#manage-playbooks-using-source-control`;
@@ -35,6 +50,13 @@ const GitSubForm = ({
tooltipMaxWidth="400px" tooltipMaxWidth="400px"
tooltip={projectHelpStrings.sourceControlRefspec(docsURL)} tooltip={projectHelpStrings.sourceControlRefspec(docsURL)}
/> />
<CredentialLookup
credentialTypeId={signature_validation_credential.typeId}
label={t`Content Signature Validation Credential`}
onChange={onCredentialChange}
value={signature_validation_credential.value}
tooltip={projectHelpStrings.signatureValidation}
/>
<ScmCredentialFormField <ScmCredentialFormField
credential={credential} credential={credential}
onCredentialSelection={onCredentialSelection} onCredentialSelection={onCredentialSelection}

View File

@@ -145,6 +145,7 @@ export const Project = shape({
summary_fields: shape({ summary_fields: shape({
organization: Organization, organization: Organization,
credential: Credential, credential: Credential,
signature_validation_credential: Credential,
last_job: shape({}), last_job: shape({}),
last_update: shape({}), last_update: shape({}),
created_by: shape({}), created_by: shape({}),
@@ -163,6 +164,7 @@ export const Project = shape({
scm_delete_on_update: bool, scm_delete_on_update: bool,
scm_track_submodules: bool, scm_track_submodules: bool,
credential: number, credential: number,
signature_validation_credential: number,
status: oneOf([ status: oneOf([
'new', 'new',
'pending', 'pending',