Commit fcba9ed7 authored by Stan Hu's avatar Stan Hu

Merge branch '215682-suggested-solution-setting-fe' into 'master'

Add a setting for toggling auto-fix feature

See merge request gitlab-org/gitlab!30530
parents da707fde d3e197e6
...@@ -2,12 +2,16 @@ ...@@ -2,12 +2,16 @@
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import AutoFixSettings from './auto_fix_settings.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
GlLink, GlLink,
Icon, Icon,
AutoFixSettings,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
autoDevopsEnabled: { autoDevopsEnabled: {
type: Boolean, type: Boolean,
...@@ -31,6 +35,10 @@ export default { ...@@ -31,6 +35,10 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
autoFixSettingsProps: {
type: Object,
required: true,
},
}, },
computed: { computed: {
headerContent() { headerContent() {
...@@ -141,5 +149,6 @@ export default { ...@@ -141,5 +149,6 @@ export default {
</div> </div>
</div> </div>
</section> </section>
<auto-fix-settings v-if="glFeatures.securityAutoFix" v-bind="autoFixSettingsProps" />
</article> </article>
</template> </template>
<script>
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import { GlIcon, GlLink, GlCard, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import createFlash from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
GlIcon,
GlLink,
GlCard,
GlFormCheckbox,
GlSprintf,
},
mixins: [glFeatureFlagsMixin()],
props: {
autoFixEnabled: {
type: Object,
required: true,
},
autoFixHelpPath: {
type: String,
required: true,
},
autoFixUserPath: {
type: String,
required: true,
},
containerScanningHelpPath: {
type: String,
required: true,
},
dependencyScanningHelpPath: {
type: String,
required: true,
},
toggleAutofixSettingEndpoint: {
type: String,
required: true,
},
},
data() {
// In this first iteration, the auto-fix settings is toggled for all features at once via a
// single checkbox. The line below is a temporary workaround to initialize the setting's state
// until we have distinct checkboxes for each auto-fixable feature.
const autoFixEnabled = Object.values(this.autoFixEnabled).some(enabled => enabled);
return {
autoFixEnabledLocal: autoFixEnabled,
isChecked: autoFixEnabled,
autoFixStatusLoading: false,
};
},
methods: {
toggleAutoFix(enabled) {
this.autoFixStatusLoading = true;
return axios
.post(this.toggleAutofixSettingEndpoint, {
// When we have distinct checkboxes for each feature, we'll need to pass the feature being
// toggled to the API. It's not required for now as all features are being toggled at once.
feature: '',
enabled,
})
.then(() => {
this.autoFixEnabledLocal = enabled;
this.isChecked = enabled;
})
.catch(e => {
Sentry.captureException(e);
createFlash(
__('Something went wrong while toggling auto-fix settings, please try again later.'),
);
this.isChecked = !enabled;
})
.finally(() => {
this.autoFixStatusLoading = false;
});
},
},
};
</script>
<template>
<section>
<h4 class="gl-h4 gl-my-5">
{{ __('Suggested Solutions') }}
<gl-link
target="_blank"
:href="autoFixHelpPath"
:aria-label="__('Suggested solutions help link')"
>
<gl-icon name="question" />
</gl-link>
</h4>
<gl-card>
<gl-form-checkbox
v-model="isChecked"
:disabled="autoFixStatusLoading"
@change="toggleAutoFix"
>
{{
__('Automatically create merge requests for vulnerabilities that have fixes available.')
}}
<template #help>{{ __('Available for dependency and container scanning') }}</template>
</gl-form-checkbox>
<footer class="gl-bg-blue-100 gl-px-5 gl-py-3">
<gl-sprintf
:message="
__(
'%{containerScanningLinkStart}Container Scanning%{containerScanningLinkEnd} and/or %{dependencyScanningLinkStart}Dependency Scanning%{dependencyScanningLinkEnd} must be enabled. %{securityBotLinkStart}GitLab-Security-Bot%{securityBotLinkEnd} will be the author of the auto-created merge request. %{moreInfoLinkStart}More information%{moreInfoLinkEnd}.',
)
"
>
<template #containerScanningLink="{ content }">
<gl-link :href="containerScanningHelpPath">{{ content }}</gl-link>
</template>
<template #dependencyScanningLink="{ content }">
<gl-link :href="dependencyScanningHelpPath">{{ content }}</gl-link>
</template>
<template #securityBotLink="{ content }">
<gl-link :href="autoFixUserPath">{{ content }}</gl-link>
</template>
<template #moreInfoLink="{ content }">
<gl-link :href="autoFixHelpPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</footer>
</gl-card>
</section>
</template>
...@@ -9,6 +9,12 @@ export default function init() { ...@@ -9,6 +9,12 @@ export default function init() {
features, features,
helpPagePath, helpPagePath,
latestPipelinePath, latestPipelinePath,
autoFixEnabled,
autoFixHelpPath,
autoFixUserPath,
containerScanningHelpPath,
dependencyScanningHelpPath,
toggleAutofixSettingEndpoint,
} = el.dataset; } = el.dataset;
return new Vue({ return new Vue({
...@@ -24,6 +30,14 @@ export default function init() { ...@@ -24,6 +30,14 @@ export default function init() {
features: JSON.parse(features), features: JSON.parse(features),
helpPagePath, helpPagePath,
latestPipelinePath, latestPipelinePath,
autoFixSettingsProps: {
autoFixEnabled: JSON.parse(autoFixEnabled),
autoFixHelpPath,
autoFixUserPath,
containerScanningHelpPath,
dependencyScanningHelpPath,
toggleAutofixSettingEndpoint,
},
}, },
}); });
}, },
......
...@@ -7,6 +7,10 @@ module Projects ...@@ -7,6 +7,10 @@ module Projects
alias_method :vulnerable, :project alias_method :vulnerable, :project
before_action only: [:show] do
push_frontend_feature_flag(:security_auto_fix, project, default_enabled: false)
end
def show def show
@configuration = ConfigurationPresenter.new(project) @configuration = ConfigurationPresenter.new(project)
end end
......
...@@ -44,7 +44,12 @@ module Projects ...@@ -44,7 +44,12 @@ module Projects
auto_devops_help_page_path: help_page_path('topics/autodevops/index'), auto_devops_help_page_path: help_page_path('topics/autodevops/index'),
features: features.to_json, features: features.to_json,
help_page_path: help_page_path('user/application_security/index'), help_page_path: help_page_path('user/application_security/index'),
latest_pipeline_path: latest_pipeline_path latest_pipeline_path: latest_pipeline_path,
auto_fix_enabled: {
dependency_scanning: true,
container_scanning: true
}.to_json,
auto_fix_user_path: '/' # TODO: real link will be updated with https://gitlab.com/gitlab-org/gitlab/-/issues/215669
} }
end end
......
- breadcrumb_title _("Security Configuration") - breadcrumb_title _("Security Configuration")
- page_title _("Security Configuration") - page_title _("Security Configuration")
#js-security-configuration{ data: @configuration.to_h } #js-security-configuration{ data: { **@configuration.to_h,
auto_fix_help_path: '/',
toggle_autofix_setting_endpoint: 'auto_fix',
container_scanning_help_path: help_page_path('user/application_security/container_scanning/index'),
dependency_scanning_help_path: help_page_path('user/application_security/dependency_scanning/index') } }
...@@ -13,6 +13,7 @@ describe('Security Configuration App', () => { ...@@ -13,6 +13,7 @@ describe('Security Configuration App', () => {
latestPipelinePath: 'http://latestPipelinePath', latestPipelinePath: 'http://latestPipelinePath',
autoDevopsHelpPagePath: 'http://autoDevopsHelpPagePath', autoDevopsHelpPagePath: 'http://autoDevopsHelpPagePath',
helpPagePath: 'http://helpPagePath', helpPagePath: 'http://helpPagePath',
autoFixSettingsProps: {},
...props, ...props,
}, },
}); });
......
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import AutoFixSettings from 'ee/security_configuration/components/auto_fix_settings.vue';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/flash.js');
const AUTO_FIX_USER_PATH = `${TEST_HOST}/security_bot`;
const AUTO_FIX_HELP_PATH = `${TEST_HOST}/help/auto_fix`;
const CONTAINER_SCANNING_HELP_PATH = `${TEST_HOST}/help/container_scanning`;
const DEPENDENCY_SCANNING_HELP_PATH = `${TEST_HOST}/help/dependency_scanning`;
const TOGGLE_AUTO_FIX_ENDPOINT = 'auto_fix';
const AUTO_FIX_ENABLED_PROPS = {
container_scanning: true,
dependency_scanning: true,
};
const AUTO_FIX_DISABLED_PROPS = {
container_scanning: false,
dependency_scanning: false,
};
const FOOTER_TEXT =
'Container Scanning and/or Dependency Scanning must be enabled. ' +
'GitLab-Security-Bot will be the author of the auto-created merge request. More information.';
describe('Auto-fix Settings', () => {
let axiosMock;
let wrapper;
const createComponent = (props = {}) => {
axiosMock = new AxiosMockAdapter(axios);
wrapper = mount(AutoFixSettings, {
propsData: {
autoFixHelpPath: AUTO_FIX_HELP_PATH,
autoFixUserPath: AUTO_FIX_USER_PATH,
containerScanningHelpPath: CONTAINER_SCANNING_HELP_PATH,
dependencyScanningHelpPath: DEPENDENCY_SCANNING_HELP_PATH,
toggleAutofixSettingEndpoint: TOGGLE_AUTO_FIX_ENDPOINT,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
axiosMock.restore();
});
const findCheckbox = () => wrapper.find('input[type="checkbox"]');
const findFooter = () => wrapper.find('footer');
const findFooterLinks = () => findFooter().findAll('a');
const getFooterTextContent = () =>
findFooter()
.text()
.trim();
const toggleCheckbox = () => findCheckbox().setChecked(!wrapper.vm.autoFixEnabledLocal);
const expectCorrectLinks = () => {
const links = findFooterLinks();
expect(links.length).toBe(4);
expect(links.at(0).attributes('href')).toBe(CONTAINER_SCANNING_HELP_PATH);
expect(links.at(1).attributes('href')).toBe(DEPENDENCY_SCANNING_HELP_PATH);
expect(links.at(2).attributes('href')).toBe(AUTO_FIX_USER_PATH);
expect(links.at(3).attributes('href')).toBe(AUTO_FIX_HELP_PATH);
};
const itShowsEnabledInformation = () => {
it('checkbox is checked', () => {
expect(findCheckbox().element.checked).toBeTruthy();
});
it('explains how GitLab Security Bot will author auto-fix MRs', () => {
expect(getFooterTextContent()).toBe(FOOTER_TEXT);
});
it('shows links to GitLab Security Bot profile and auto-fix documentation', () => {
expectCorrectLinks();
});
};
const itShowsDisabledInformation = () => {
it('checkbox is unchecked', () => {
expect(findCheckbox().element.checked).toBeFalsy();
});
it('explains how auto-fix will behave when enabled', () => {
expect(getFooterTextContent()).toBe(FOOTER_TEXT);
});
it('shows links ', () => {
expectCorrectLinks();
});
};
const itSendsPostRequest = () => {
it('sends a post request and sets loading state', () => {
expect(axiosMock.history.post).toHaveLength(1);
expect(findCheckbox().element.disabled).toBe(true);
});
};
describe.each`
description | autoFixEnabled | itShowsInitialState | itShowsToggleSuccessState
${'enabled'} | ${AUTO_FIX_ENABLED_PROPS} | ${itShowsEnabledInformation} | ${itShowsDisabledInformation}
${'disabled'} | ${AUTO_FIX_DISABLED_PROPS} | ${itShowsDisabledInformation} | ${itShowsEnabledInformation}
`(
'with auto-fix $description',
({ autoFixEnabled, itShowsInitialState, itShowsToggleSuccessState }) => {
beforeEach(() => {
createComponent({ autoFixEnabled });
});
itShowsInitialState();
describe('when toggling the checkbox', () => {
describe('on success', () => {
beforeEach(() => {
axiosMock.onPost(TOGGLE_AUTO_FIX_ENDPOINT).reply(200);
toggleCheckbox();
});
itSendsPostRequest();
describe('when request resolves', () => {
beforeEach(waitForPromises);
itShowsToggleSuccessState();
it('resets loading state', () => {
expect(findCheckbox().element.disabled).toBe(false);
});
});
});
describe('on error', () => {
beforeEach(() => {
axiosMock.onPost(TOGGLE_AUTO_FIX_ENDPOINT).reply(500);
toggleCheckbox();
});
itSendsPostRequest();
describe('when request resolves', () => {
beforeEach(waitForPromises);
itShowsInitialState();
it('shows error flash', () => {
expect(createFlash).toHaveBeenCalledWith(
'Something went wrong while toggling auto-fix settings, please try again later.',
);
});
});
});
});
},
);
});
...@@ -241,6 +241,9 @@ msgstr "" ...@@ -241,6 +241,9 @@ msgstr ""
msgid "%{completedWeight} of %{totalWeight} weight completed" msgid "%{completedWeight} of %{totalWeight} weight completed"
msgstr "" msgstr ""
msgid "%{containerScanningLinkStart}Container Scanning%{containerScanningLinkEnd} and/or %{dependencyScanningLinkStart}Dependency Scanning%{dependencyScanningLinkEnd} must be enabled. %{securityBotLinkStart}GitLab-Security-Bot%{securityBotLinkEnd} will be the author of the auto-created merge request. %{moreInfoLinkStart}More information%{moreInfoLinkEnd}."
msgstr ""
msgid "%{cores} cores" msgid "%{cores} cores"
msgstr "" msgstr ""
...@@ -2912,6 +2915,9 @@ msgstr "" ...@@ -2912,6 +2915,9 @@ msgstr ""
msgid "Automatic certificate management using Let's Encrypt" msgid "Automatic certificate management using Let's Encrypt"
msgstr "" msgstr ""
msgid "Automatically create merge requests for vulnerabilities that have fixes available."
msgstr ""
msgid "Automatically marked as default internal user" msgid "Automatically marked as default internal user"
msgstr "" msgstr ""
...@@ -2927,6 +2933,9 @@ msgstr "" ...@@ -2927,6 +2933,9 @@ msgstr ""
msgid "Available" msgid "Available"
msgstr "" msgstr ""
msgid "Available for dependency and container scanning"
msgstr ""
msgid "Available group Runners: %{runners}" msgid "Available group Runners: %{runners}"
msgstr "" msgstr ""
...@@ -19492,6 +19501,9 @@ msgstr "" ...@@ -19492,6 +19501,9 @@ msgstr ""
msgid "Something went wrong while stopping this environment. Please try again." msgid "Something went wrong while stopping this environment. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while toggling auto-fix settings, please try again later."
msgstr ""
msgid "Something went wrong while updating a requirement." msgid "Something went wrong while updating a requirement."
msgstr "" msgstr ""
...@@ -20254,9 +20266,15 @@ msgstr "" ...@@ -20254,9 +20266,15 @@ msgstr ""
msgid "Suggest code changes which can be immediately applied in one click. Try it out!" msgid "Suggest code changes which can be immediately applied in one click. Try it out!"
msgstr "" msgstr ""
msgid "Suggested Solutions"
msgstr ""
msgid "Suggested change" msgid "Suggested change"
msgstr "" msgstr ""
msgid "Suggested solutions help link"
msgstr ""
msgid "SuggestedColors|Bright green" msgid "SuggestedColors|Bright green"
msgstr "" 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