Commit e98aebb3 authored by Dheeraj Joshi's avatar Dheeraj Joshi Committed by Natalia Tepluhina

Integrate site validation in profile library

 * Add site validation status column
 * Add validation buttons
 * Integrate validation modal
parent ac4588fe
......@@ -261,6 +261,7 @@ export default {
:profiles-per-page="$options.profilesPerPage"
:profiles="profileTypes[profileType].profiles"
:fields="settings.tableFields"
:full-path="projectFullPath"
@load-more-profiles="fetchMoreProfiles(profileType)"
@delete-profile="deleteProfile(profileType, $event)"
/>
......
......@@ -9,6 +9,14 @@ import {
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
import DastSiteValidationModal from 'ee/security_configuration/dast_site_validation/components/dast_site_validation_modal.vue';
import {
DAST_SITE_VALIDATION_STATUS,
DAST_SITE_VALIDATION_STATUS_PROPS,
} from 'ee/security_configuration/dast_site_validation/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const { PENDING, FAILED } = DAST_SITE_VALIDATION_STATUS;
export default {
components: {
......@@ -18,10 +26,12 @@ export default {
GlModal,
GlSkeletonLoader,
GlTable,
DastSiteValidationModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
profiles: {
type: Array,
......@@ -55,12 +65,18 @@ export default {
required: false,
default: false,
},
fullPath: {
type: String,
required: true,
},
},
data() {
return {
toBeDeletedProfileId: null,
validatingProfile: null,
};
},
statuses: DAST_SITE_VALIDATION_STATUS_PROPS,
computed: {
hasError() {
return this.errorMessage !== '';
......@@ -99,6 +115,24 @@ export default {
handleCancel() {
this.toBeDeletedProfileId = null;
},
shouldShowValidationBtn(status) {
return (
this.glFeatures.securityOnDemandScansSiteValidation &&
(status === PENDING || status === FAILED)
);
},
shouldShowValidationStatus(status) {
return this.glFeatures.securityOnDemandScansSiteValidation && status !== PENDING;
},
showValidationModal() {
this.$refs['dast-site-validation-modal'].show();
},
setValidatingProfile(profile) {
this.validatingProfile = profile;
this.$nextTick(() => {
this.showValidationModal();
});
},
},
};
</script>
......@@ -133,28 +167,44 @@ export default {
</template>
<template #cell(validationStatus)="{ value }">
<span>
<template v-if="shouldShowValidationStatus(value)">
<span :class="$options.statuses[value].cssClass">
{{ $options.statuses[value].label }}
</span>
<gl-icon
:size="16"
class="gl-vertical-align-text-bottom gl-text-gray-600"
name="information-o"
v-gl-tooltip
name="question-o"
class="gl-vertical-align-text-bottom gl-text-gray-300 gl-ml-2"
:title="$options.statuses[value].tooltipText"
/>
{{ value }}
</span>
</template>
</template>
<template #cell(actions)="{ item }">
<div class="gl-text-right">
<gl-button
v-if="shouldShowValidationBtn(item.validationStatus)"
variant="info"
category="secondary"
size="small"
@click="setValidatingProfile(item)"
>{{ s__('DastSiteValidation|Validate target site') }}</gl-button
>
<gl-button v-if="item.editPath" :href="item.editPath" class="gl-mx-5" size="small">{{
__('Edit')
}}</gl-button>
<gl-button
v-gl-tooltip.hover.focus
icon="remove"
variant="danger"
category="secondary"
size="small"
class="gl-mr-3"
:title="s__('DastProfiles|Delete profile')"
@click="prepareProfileDeletion(item.id)"
/>
<gl-button v-if="item.editPath" :href="item.editPath">{{ __('Edit') }}</gl-button>
</div>
</template>
......@@ -197,5 +247,12 @@ export default {
@ok="handleDelete"
@cancel="handleCancel"
/>
<dast-site-validation-modal
v-if="validatingProfile"
ref="dast-site-validation-modal"
:full-path="fullPath"
:target-url="validatingProfile.targetUrl"
/>
</section>
</template>
......@@ -19,7 +19,7 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({
}),
},
},
tableFields: ['profileName', 'targetUrl'],
tableFields: ['profileName', 'targetUrl', 'validationStatus'],
i18n: {
createNewLinkText: s__('DastProfiles|Site Profile'),
tabName: s__('DastProfiles|Site Profiles'),
......
......@@ -139,6 +139,9 @@ export default {
);
},
methods: {
show() {
this.$refs.modal.show();
},
updateValidationPath() {
this.validationPath = this.isTextFileValidation
? this.getTextFileValidationPath()
......
......@@ -27,6 +27,26 @@ export const DAST_SITE_VALIDATION_STATUS = {
FAILED: 'FAILED_VALIDATION',
};
export const DAST_SITE_VALIDATION_STATUS_PROPS = {
[DAST_SITE_VALIDATION_STATUS.INPROGRESS]: {
label: s__('DastSiteValidation|Validating...'),
cssClass: 'gl-text-blue-300',
tooltipText: s__('DastSiteValidation|The validation is in progress. Please wait...'),
},
[DAST_SITE_VALIDATION_STATUS.PASSED]: {
label: s__('DastSiteValidation|Validated'),
cssClass: 'gl-text-green-500',
tooltipText: s__(
'DastSiteValidation|Validation succeeded. Both active and passive scans can be run against the target site.',
),
},
[DAST_SITE_VALIDATION_STATUS.FAILED]: {
label: s__('DastSiteValidation|Validation failed'),
cssClass: 'gl-text-red-500',
tooltipText: s__('DastSiteValidation|The validation has failed. Please try again.'),
},
};
export const DAST_SITE_VALIDATION_HTTP_HEADER_KEY = 'Gitlab-On-Demand-DAST';
export const DAST_SITE_VALIDATION_MODAL_ID = 'dast-site-validation-modal';
......@@ -3,7 +3,11 @@
module Projects
module Security
class DastProfilesController < Projects::ApplicationController
before_action :authorize_read_on_demand_scans!
before_action do
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_site_validation, @project)
push_frontend_feature_flag(:security_on_demand_scans_http_header_validation, @project)
end
feature_category :dynamic_application_security_testing
......
......@@ -3,11 +3,7 @@
module Projects
module Security
class DastSiteProfilesController < Projects::ApplicationController
before_action do
authorize_read_on_demand_scans!
push_frontend_feature_flag(:security_on_demand_scans_site_validation, @project)
push_frontend_feature_flag(:security_on_demand_scans_http_header_validation, @project)
end
before_action :authorize_read_on_demand_scans!
feature_category :dynamic_application_security_testing
......
---
title: Swap edit and delete button for DAST Profile library
merge_request: 48124
author:
type: changed
......@@ -4,6 +4,7 @@ import { mount, shallowMount, createWrapper } from '@vue/test-utils';
import { merge } from 'lodash';
import DastProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { siteProfiles as profiles } from './mock_data';
const TEST_ERROR_MESSAGE = 'something went wrong';
......@@ -18,6 +19,7 @@ describe('EE - DastProfilesList', () => {
profilesPerPage: 10,
errorMessage: '',
errorDetails: [],
fullPath: '/namespace/project',
};
wrapper = mountFn(
......@@ -25,6 +27,9 @@ describe('EE - DastProfilesList', () => {
merge(
{},
{
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
propsData: defaultProps,
},
options,
......@@ -108,30 +113,6 @@ describe('EE - DastProfilesList', () => {
});
describe('with existing profiles', () => {
const profiles = [
{
id: 1,
profileName: 'Profile 1',
targetUrl: 'http://example-1.com',
editPath: '/1/edit',
validationStatus: 'Pending',
},
{
id: 2,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '/2/edit',
validationStatus: 'Pending',
},
{
id: 3,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '/3/edit',
validationStatus: 'Pending',
},
];
const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)];
describe('profiles list', () => {
......@@ -149,22 +130,64 @@ describe('EE - DastProfilesList', () => {
});
it.each(profiles)('renders list item %# correctly', profile => {
const [
profileCell,
targetUrlCell,
validationStatusCell,
actionsCell,
] = getTableRowForProfile(profile).cells;
const [profileCell, targetUrlCell, , actionsCell] = getTableRowForProfile(profile).cells;
expect(profileCell.innerText).toContain(profile.profileName);
expect(targetUrlCell.innerText).toContain(profile.targetUrl);
expect(validationStatusCell.innerText).toContain(profile.validationStatus);
expect(within(actionsCell).getByRole('button', { name: /delete/i })).not.toBe(null);
const editLink = within(actionsCell).getByRole('link', { name: /edit/i });
expect(editLink).not.toBe(null);
expect(editLink.getAttribute('href')).toBe(profile.editPath);
});
describe('with site validation enabled', () => {
describe.each`
status | statusEnum | label | hasValidateButton
${'pending'} | ${'PENDING_VALIDATION'} | ${''} | ${true}
${'in-progress'} | ${'INPROGRESS_VALIDATION'} | ${'Validating...'} | ${false}
${'passed'} | ${'PASSED_VALIDATION'} | ${'Validated'} | ${false}
${'failed'} | ${'FAILED_VALIDATION'} | ${'Validation failed'} | ${true}
`('profile with validation $status', ({ statusEnum, label, hasValidateButton }) => {
const profile = profiles.find(({ validationStatus }) => validationStatus === statusEnum);
it(`should show correct label`, () => {
const validationStatusCell = getTableRowForProfile(profile).cells[2];
expect(validationStatusCell.innerText).toContain(label);
});
it(`should ${hasValidateButton ? '' : 'not '}render validate button`, () => {
const actionsCell = getTableRowForProfile(profile).cells[3];
const validateButton = within(actionsCell).queryByRole('button', {
name: /validate/i,
});
if (hasValidateButton) {
expect(validateButton).not.toBeNull();
} else {
expect(validateButton).toBeNull();
}
});
});
});
describe('without site validation enabled', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: false },
},
propsData: { profiles },
});
});
it.each(profiles)('profile %# should not have validate button and status', profile => {
const [, , validationStatusCell, actionsCell] = getTableRowForProfile(profile).cells;
expect(within(actionsCell).queryByRole('button', { name: /validate/i })).toBe(null);
expect(validationStatusCell.innerText).toBe('');
});
});
});
describe('load more profiles', () => {
......
......@@ -180,6 +180,7 @@ describe('EE - DastProfiles', () => {
profilesPerPage: expect.any(Number),
profiles: [],
fields: expect.any(Array),
fullPath: '/namespace/project',
});
});
......
export const siteProfiles = [
{
id: 1,
profileName: 'Profile 1',
targetUrl: 'http://example-1.com',
editPath: '/1/edit',
validationStatus: 'PENDING_VALIDATION',
},
{
id: 2,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '/2/edit',
validationStatus: 'INPROGRESS_VALIDATION',
},
{
id: 3,
profileName: 'Profile 3',
targetUrl: 'http://example-2.com',
editPath: '/3/edit',
validationStatus: 'PASSED_VALIDATION',
},
{
id: 4,
profileName: 'Profile 4',
targetUrl: 'http://example-3.com',
editPath: '/3/edit',
validationStatus: 'FAILED_VALIDATION',
},
];
......@@ -90,6 +90,25 @@ describe('DastSiteValidationModal', () => {
wrapper.destroy();
});
it("calls GlModal's show method when own show method is called", () => {
const showMock = jest.fn();
createComponent({
mountOptions: {
stubs: {
GlModal: {
render: () => {},
methods: {
show: showMock,
},
},
},
},
});
wrapper.vm.show();
expect(showMock).toHaveBeenCalled();
});
describe('rendering', () => {
describe('loading', () => {
beforeEach(() => {
......
......@@ -8674,12 +8674,30 @@ msgstr ""
msgid "DastSiteValidation|Text file validation"
msgstr ""
msgid "DastSiteValidation|The validation has failed. Please try again."
msgstr ""
msgid "DastSiteValidation|The validation is in progress. Please wait..."
msgstr ""
msgid "DastSiteValidation|Validate"
msgstr ""
msgid "DastSiteValidation|Validate target site"
msgstr ""
msgid "DastSiteValidation|Validated"
msgstr ""
msgid "DastSiteValidation|Validating..."
msgstr ""
msgid "DastSiteValidation|Validation failed"
msgstr ""
msgid "DastSiteValidation|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