Commit ec218553 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '280559-remove-dast-site-profile-validation' into 'master'

Remove validation from DAST site profile form

See merge request gitlab-org/gitlab!47701
parents f0b673bc d9cfd07d
<script>
import { isEqual } from 'lodash';
import {
GlAlert,
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormInput,
GlModal,
GlToggle,
} from '@gitlab/ui';
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput, GlModal } from '@gitlab/ui';
import { initFormField } from 'ee/security_configuration/utils';
import * as Sentry from '~/sentry/wrapper';
import { __, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import { serializeFormObject } from '~/lib/utils/forms';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import validation from '~/vue_shared/directives/validation';
import DastSiteValidation from './dast_site_validation.vue';
import dastSiteProfileCreateMutation from '../graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from '../graphql/dast_site_profile_update.mutation.graphql';
import dastSiteTokenCreateMutation from '../graphql/dast_site_token_create.mutation.graphql';
import dastSiteValidationQuery from '../graphql/dast_site_validation.query.graphql';
import { DAST_SITE_VALIDATION_STATUS, DAST_SITE_VALIDATION_POLL_INTERVAL } from '../constants';
const { PENDING, INPROGRESS, PASSED, FAILED } = DAST_SITE_VALIDATION_STATUS;
export default {
name: 'DastSiteProfileForm',
components: {
GlAlert,
GlButton,
GlCollapse,
GlForm,
GlFormGroup,
GlFormInput,
GlModal,
GlToggle,
DastSiteValidation,
},
directives: {
validation: validation(),
},
mixins: [glFeatureFlagsMixin()],
props: {
fullPath: {
type: String,
......@@ -72,18 +51,12 @@ export default {
};
return {
fetchValidationTimeout: null,
form,
initialFormValues: serializeFormObject(form.fields),
isFetchingValidationStatus: false,
isValidatingSite: false,
isLoading: false,
hasAlert: false,
tokenId: null,
token: null,
isSiteValidationActive: false,
isSiteValidationTouched: false,
validationStatus: null,
errorMessage: '',
errors: [],
};
......@@ -92,9 +65,6 @@ export default {
isEdit() {
return Boolean(this.siteProfile?.id);
},
isSiteValidationDisabled() {
return !this.form.fields.targetUrl.state || this.validationStatusMatches(INPROGRESS);
},
i18n() {
const { isEdit } = this;
return {
......@@ -111,163 +81,18 @@ export default {
okTitle: __('Discard'),
cancelTitle: __('Cancel'),
},
siteValidation: {
validationStatusFetchError: s__(
'DastProfiles|Could not retrieve site validation status. Please refresh the page, or try again later.',
),
createTokenError: s__(
'DastProfiles|Could not create site validation token. Please refresh the page, or try again later.',
),
},
};
},
formTouched() {
return !isEqual(serializeFormObject(this.form.fields), this.initialFormValues);
},
isSubmitDisabled() {
return (
this.validationStatusMatches(INPROGRESS) ||
(this.isSiteValidationActive && !this.validationStatusMatches(PASSED))
);
},
showValidationSection() {
return (
this.isSiteValidationActive &&
!this.isValidatingSite &&
![INPROGRESS, PASSED].some(this.validationStatusMatches)
);
},
siteValidationStatusDescription() {
const descriptions = {
[PENDING]: { text: s__('DastProfiles|Site must be validated to run an active scan.') },
[INPROGRESS]: {
text: s__('DastProfiles|Validation is in progress...'),
},
[PASSED]: {
text: s__(
'DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site.',
),
cssClass: 'gl-text-green-500',
},
[FAILED]: {
text: s__('DastProfiles|Validation failed. Please try again.'),
cssClass: 'gl-text-red-500',
dismissed: this.isSiteValidationTouched,
},
};
const defaultDescription = descriptions[PENDING];
const currentStatusDescription = descriptions[this.validationStatus];
return currentStatusDescription && !currentStatusDescription.dismissed
? currentStatusDescription
: defaultDescription;
},
},
async mounted() {
if (this.isEdit) {
this.form.showValidation = true;
if (this.glFeatures.securityOnDemandScansSiteValidation) {
await this.fetchValidationStatus();
this.isSiteValidationActive = this.validationStatusMatches(PASSED);
}
}
},
destroyed() {
clearTimeout(this.fetchValidationTimeout);
this.fetchValidationTimeout = null;
},
methods: {
async validateSite(validate) {
this.isSiteValidationActive = validate;
this.isSiteValidationTouched = true;
this.tokenId = null;
this.token = null;
if (!validate) {
this.validationStatus = null;
} else {
try {
this.isValidatingSite = true;
await this.fetchValidationStatus();
if (![PASSED, INPROGRESS].some(this.validationStatusMatches)) {
await this.createValidationToken();
}
} catch (exception) {
this.captureException(exception);
this.isSiteValidationActive = false;
} finally {
this.isValidatingSite = false;
}
}
},
validationStatusMatches(status) {
return this.validationStatus === status;
},
async fetchValidationStatus() {
this.isFetchingValidationStatus = true;
try {
const {
data: {
project: { dastSiteValidation },
},
} = await this.$apollo.query({
query: dastSiteValidationQuery,
variables: {
fullPath: this.fullPath,
targetUrl: this.form.fields.targetUrl.value,
},
fetchPolicy: fetchPolicies.NETWORK_ONLY,
});
this.validationStatus = dastSiteValidation?.status || null;
if (this.validationStatusMatches(INPROGRESS)) {
await new Promise(resolve => {
this.fetchValidationTimeout = setTimeout(resolve, DAST_SITE_VALIDATION_POLL_INTERVAL);
});
await this.fetchValidationStatus();
}
} catch (exception) {
this.showErrors({
message: this.i18n.siteValidation.validationStatusFetchError,
});
throw new Error(exception);
} finally {
this.isFetchingValidationStatus = false;
}
},
async createValidationToken() {
const errorMessage = this.i18n.siteValidation.createTokenError;
try {
const {
data: {
dastSiteTokenCreate: { id, token, errors = [] },
},
} = await this.$apollo.mutate({
mutation: dastSiteTokenCreateMutation,
variables: {
fullPath: this.fullPath,
targetUrl: this.form.fields.targetUrl.value,
},
});
if (errors.length) {
this.showErrors({ message: errorMessage, errors });
} else {
this.tokenId = id;
this.token = token;
}
} catch (exception) {
this.showErrors({ message: errorMessage });
throw new Error(exception);
}
},
onSubmit() {
this.form.showValidation = true;
......@@ -317,9 +142,6 @@ export default {
this.$refs[this.$options.modalId].show();
}
},
onValidationSuccess() {
this.validationStatus = PASSED;
},
discard() {
redirectTo(this.profilesLibraryPath);
},
......@@ -381,11 +203,6 @@ export default {
<gl-form-group
data-testid="target-url-input-group"
:invalid-feedback="form.fields.targetUrl.feedback"
:description="
isSiteValidationActive && !isValidatingSite
? s__('DastProfiles|Validation must be turned off to change the target URL')
: null
"
:label="s__('DastProfiles|Target URL')"
>
<gl-form-input
......@@ -397,44 +214,9 @@ export default {
required
type="url"
:state="form.fields.targetUrl.state"
:disabled="isSiteValidationActive"
/>
</gl-form-group>
<template v-if="glFeatures.securityOnDemandScansSiteValidation">
<gl-form-group :label="s__('DastProfiles|Validate target site')">
<template #description>
<p
v-if="siteValidationStatusDescription.text"
class="gl-mt-3"
:class="siteValidationStatusDescription.cssClass"
data-testid="siteValidationStatusDescription"
>
{{ siteValidationStatusDescription.text }}
</p>
</template>
<gl-toggle
data-testid="dast-site-validation-toggle"
:value="isSiteValidationActive"
:disabled="isSiteValidationDisabled"
:is-loading="
!isSiteValidationDisabled && (isFetchingValidationStatus || isValidatingSite)
"
@change="validateSite"
/>
</gl-form-group>
<gl-collapse :visible="showValidationSection">
<dast-site-validation
:full-path="fullPath"
:token-id="tokenId"
:token="token"
:target-url="form.fields.targetUrl.value"
@success="onValidationSuccess"
/>
</gl-collapse>
</template>
<hr />
<div class="gl-mt-6 gl-pt-6">
......@@ -443,7 +225,6 @@ export default {
variant="success"
class="js-no-auto-disable"
data-testid="dast-site-profile-form-submit-button"
:disabled="isSubmitDisabled"
:loading="isLoading"
>
{{ s__('DastProfiles|Save profile') }}
......
......@@ -7,14 +7,10 @@ import { GlForm, GlModal } from '@gitlab/ui';
import waitForPromises from 'jest/helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import DastSiteProfileForm from 'ee/security_configuration/dast_site_profiles_form/components/dast_site_profile_form.vue';
import DastSiteValidation from 'ee/security_configuration/dast_site_profiles_form/components/dast_site_validation.vue';
import dastSiteValidationQuery from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_validation.query.graphql';
import dastSiteProfileCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_create.mutation.graphql';
import dastSiteProfileUpdateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_profile_update.mutation.graphql';
import dastSiteTokenCreateMutation from 'ee/security_configuration/dast_site_profiles_form/graphql/dast_site_token_create.mutation.graphql';
import { siteProfiles } from 'ee_jest/on_demand_scans/mock_data';
import * as responses from 'ee_jest/security_configuration/dast_site_profiles_form/mock_data/apollo_mock';
import { DAST_SITE_VALIDATION_STATUS } from 'ee/security_configuration/dast_site_profiles_form/constants';
import * as urlUtility from '~/lib/utils/url_utility';
const localVue = createLocalVue();
......@@ -25,8 +21,6 @@ const fullPath = 'group/project';
const profilesLibraryPath = `${TEST_HOST}/${fullPath}/-/security/configuration/dast_profiles`;
const profileName = 'My DAST site profile';
const targetUrl = 'http://example.com';
const tokenId = '3455';
const token = '33988';
const defaultProps = {
profilesLibraryPath,
......@@ -36,10 +30,6 @@ const defaultProps = {
const defaultRequestHandlers = {
dastSiteProfileCreate: jest.fn().mockResolvedValue(responses.dastSiteProfileCreate()),
dastSiteProfileUpdate: jest.fn().mockResolvedValue(responses.dastSiteProfileUpdate()),
dastSiteTokenCreate: jest
.fn()
.mockResolvedValue(responses.dastSiteTokenCreate({ id: tokenId, token })),
dastSiteValidation: jest.fn().mockResolvedValue(responses.dastSiteValidation()),
};
describe('DastSiteProfileForm', () => {
......@@ -52,15 +42,12 @@ describe('DastSiteProfileForm', () => {
const findForm = () => wrapper.find(GlForm);
const findByTestId = testId => wrapper.find(`[data-testid="${testId}"]`);
const findProfileNameInput = () => findByTestId('profile-name-input');
const findTargetUrlInputGroup = () => findByTestId('target-url-input-group');
const findTargetUrlInput = () => findByTestId('target-url-input');
const findSubmitButton = () => findByTestId('dast-site-profile-form-submit-button');
const findCancelButton = () => findByTestId('dast-site-profile-form-cancel-button');
const findCancelModal = () => wrapper.find(GlModal);
const submitForm = () => findForm().vm.$emit('submit', { preventDefault: () => {} });
const findAlert = () => findByTestId('dast-site-profile-form-alert');
const findSiteValidationToggle = () => findByTestId('dast-site-validation-toggle');
const findDastSiteValidation = () => wrapper.find(DastSiteValidation);
const setFieldValue = async (field, value) => {
await field.setValue(value);
......@@ -85,10 +72,6 @@ describe('DastSiteProfileForm', () => {
requestHandlers.dastSiteProfileUpdate,
);
mockClient.setRequestHandler(dastSiteTokenCreateMutation, requestHandlers.dastSiteTokenCreate);
mockClient.setRequestHandler(dastSiteValidationQuery, requestHandlers.dastSiteValidation);
return mockClient;
};
......@@ -149,139 +132,6 @@ describe('DastSiteProfileForm', () => {
});
});
describe('validation', () => {
const enableValidationToggle = async () => {
await setFieldValue(findTargetUrlInput(), targetUrl);
await findSiteValidationToggle().vm.$emit('change', true);
};
describe.each`
title | siteProfile
${'New site profile'} | ${null}
${'Edit site profile'} | ${siteProfileOne}
`('$title with feature flag disabled', ({ siteProfile }) => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: false },
},
propsData: {
siteProfile,
},
});
});
it('does not render validation components', () => {
expect(findSiteValidationToggle().exists()).toBe(false);
expect(findDastSiteValidation().exists()).toBe(false);
});
it('does not check the target URLs validation status', () => {
expect(requestHandlers.dastSiteValidation).not.toHaveBeenCalled();
});
});
describe('with feature flag enabled', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
});
});
it('renders validation components', () => {
expect(findSiteValidationToggle().exists()).toBe(true);
expect(findDastSiteValidation().exists()).toBe(true);
});
it('toggle is disabled until target URL is valid', async () => {
expect(findSiteValidationToggle().props('disabled')).toBe(true);
await setFieldValue(findTargetUrlInput(), targetUrl);
expect(findSiteValidationToggle().props('disabled')).toBe(false);
});
it('disables target URL input when validation is enabled', async () => {
const targetUrlInputGroup = findTargetUrlInputGroup();
const targetUrlInput = findTargetUrlInput();
expect(targetUrlInputGroup.attributes('description')).toBeUndefined();
expect(targetUrlInput.attributes('disabled')).toBeUndefined();
await enableValidationToggle();
await waitForPromises();
expect(targetUrlInputGroup.text()).toContain(
'Validation must be turned off to change the target URL',
);
expect(targetUrlInput.attributes('disabled')).toBe('disabled');
});
it('checks the target URLs validation status when validation is enabled', async () => {
expect(requestHandlers.dastSiteValidation).not.toHaveBeenCalled();
await enableValidationToggle();
expect(requestHandlers.dastSiteValidation).toHaveBeenCalledWith({
fullPath,
targetUrl,
});
});
it('creates a site token if the target URL has not been validated', async () => {
expect(requestHandlers.dastSiteTokenCreate).not.toHaveBeenCalled();
await enableValidationToggle();
await waitForPromises();
expect(requestHandlers.dastSiteTokenCreate).toHaveBeenCalledWith({
fullPath,
targetUrl,
});
expect(findDastSiteValidation().props()).toMatchObject({
tokenId,
token,
});
});
it.each`
description | failingResponse | errorMessageStart
${'target URLs validation status can not be retrieved'} | ${'dastSiteValidation'} | ${'Could not retrieve site validation status'}
${'validation token can not be created'} | ${'dastSiteTokenCreate'} | ${'Could not create site validation token'}
`('shows an error if $description', async ({ failingResponse, errorMessageStart }) => {
respondWith({
[failingResponse]: jest.fn().mockRejectedValue(),
});
expect(findAlert().exists()).toBe(false);
await enableValidationToggle();
await waitForPromises();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(
`${errorMessageStart}. Please refresh the page, or try again later.`,
);
});
it('when validation section is opened and validation succeeds, section is collapsed', async () => {
expect(wrapper.vm.showValidationSection).toBe(false);
await enableValidationToggle();
await waitForPromises();
expect(wrapper.vm.showValidationSection).toBe(true);
await findDastSiteValidation().vm.$emit('success');
expect(wrapper.vm.showValidationSection).toBe(false);
});
});
});
describe.each`
title | siteProfile | mutationVars | mutationKind
${'New site profile'} | ${null} | ${{}} | ${'dastSiteProfileCreate'}
......@@ -414,84 +264,4 @@ describe('DastSiteProfileForm', () => {
});
});
});
describe.each`
givenValidationStatus | expectedDescription | shouldShowDefaultDescriptionAfterToggle | shouldHaveSiteValidationActivated | shouldHaveSiteValidationDisabled | shouldPoll
${DAST_SITE_VALIDATION_STATUS.PENDING} | ${'Site must be validated to run an active scan.'} | ${false} | ${false} | ${false} | ${false}
${DAST_SITE_VALIDATION_STATUS.INPROGRESS} | ${'Validation is in progress...'} | ${false} | ${false} | ${true} | ${true}
${DAST_SITE_VALIDATION_STATUS.PASSED} | ${'Validation succeeded. Both active and passive scans can be run against the target site.'} | ${false} | ${true} | ${false} | ${false}
${DAST_SITE_VALIDATION_STATUS.FAILED} | ${'Validation failed. Please try again.'} | ${true} | ${false} | ${false} | ${false}
`(
'when editing an existing profile and the validation status is "$givenValidationStatus"',
({
givenValidationStatus,
expectedDescription,
shouldHaveSiteValidationActivated,
shouldShowDefaultDescriptionAfterToggle,
shouldHaveSiteValidationDisabled,
shouldPoll,
}) => {
let dastSiteValidationHandler;
beforeEach(() => {
dastSiteValidationHandler = jest
.fn()
.mockResolvedValue(responses.dastSiteValidation(givenValidationStatus));
createFullComponent(
{
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
propsData: {
siteProfile: siteProfileOne,
},
},
{
dastSiteValidation: dastSiteValidationHandler,
},
);
return wrapper.vm.$nextTick();
});
it('shows the correct status text', () => {
expect(findByTestId('siteValidationStatusDescription').text()).toBe(expectedDescription);
});
it(`shows the correct status text after the validation toggle has been changed`, async () => {
const defaultDescription = 'Site must be validated to run an active scan.';
findSiteValidationToggle().vm.$emit('change', true);
await wrapper.vm.$nextTick();
expect(findByTestId('siteValidationStatusDescription').text()).toBe(
shouldShowDefaultDescriptionAfterToggle ? defaultDescription : expectedDescription,
);
});
it('sets the validation toggle to the correct state', () => {
expect(findSiteValidationToggle().props()).toMatchObject({
value: shouldHaveSiteValidationActivated,
disabled: shouldHaveSiteValidationDisabled,
});
});
it(`should ${shouldPoll ? '' : 'not '}poll the validation status`, async () => {
jest.useFakeTimers();
expect(dastSiteValidationHandler).toHaveBeenCalledTimes(1);
jest.runOnlyPendingTimers();
await waitForPromises();
if (shouldPoll) {
expect(dastSiteValidationHandler).toHaveBeenCalledTimes(2);
} else {
expect(dastSiteValidationHandler).toHaveBeenCalledTimes(1);
}
});
},
);
});
......@@ -8413,9 +8413,6 @@ msgstr ""
msgid "DastProfiles|Copy HTTP header to clipboard"
msgstr ""
msgid "DastProfiles|Could not create site validation token. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not create the scanner profile. Please try again."
msgstr ""
......@@ -8440,9 +8437,6 @@ msgstr ""
msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not retrieve site validation status. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not update the scanner profile. Please try again."
msgstr ""
......@@ -8557,9 +8551,6 @@ msgstr ""
msgid "DastProfiles|Site is not validated yet, please follow the steps."
msgstr ""
msgid "DastProfiles|Site must be validated to run an active scan."
msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
......@@ -8605,27 +8596,12 @@ msgstr ""
msgid "DastProfiles|Validate"
msgstr ""
msgid "DastProfiles|Validate target site"
msgstr ""
msgid "DastProfiles|Validating..."
msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the chosen method."
msgstr ""
msgid "DastProfiles|Validation failed. Please try again."
msgstr ""
msgid "DastProfiles|Validation is in progress..."
msgstr ""
msgid "DastProfiles|Validation must be turned off to change the target URL"
msgstr ""
msgid "DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site."
msgstr ""
msgid "Data is still calculating..."
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment