Commit 4d6f2ad1 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'allow_toggle_modsecurity_settings' into 'master'

Allow enabling/disabling modsecurity from UI

See merge request gitlab-org/gitlab!24747
parents 5f7da728 a8231e87
......@@ -255,6 +255,7 @@ export default class Clusters {
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
......@@ -268,6 +269,7 @@ export default class Clusters {
eventHub.$off('setKnativeHostname');
eventHub.$off('setCrossplaneProviderStack');
eventHub.$off('uninstallApplication');
eventHub.$off('setIngressModSecurityEnabled');
}
initPolling(method, successCallback, errorCallback) {
......@@ -513,6 +515,11 @@ export default class Clusters {
this.store.updateAppProperty(appId, 'validationError', null);
}
setIngressModSecurityEnabled({ id, modSecurityEnabled }) {
this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', true);
this.store.updateAppProperty(id, 'modsecurity_enabled', modSecurityEnabled);
}
destroy() {
this.destroyed = true;
......
......@@ -21,6 +21,7 @@ import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import eventHub from '~/clusters/event_hub';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
import IngressModsecuritySettings from './ingress_modsecurity_settings.vue';
export default {
components: {
......@@ -29,6 +30,7 @@ export default {
GlLoadingIcon,
KnativeDomainEditor,
CrossplaneProviderStack,
IngressModsecuritySettings,
},
props: {
type: {
......@@ -129,18 +131,6 @@ export default {
crossplaneInstalled() {
return this.applications.crossplane.status === APPLICATION_STATUS.INSTALLED;
},
ingressModSecurityDescription() {
const escapedUrl = _.escape(this.ingressModSecurityHelpPath);
return sprintf(
s__('ClusterIntegration|Learn more about %{startLink}ModSecurity%{endLink}'),
{
startLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
endLink: '</a>',
},
false,
);
},
ingressDescription() {
return sprintf(
_.escape(
......@@ -241,6 +231,9 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
}
return null;
},
ingress() {
return this.applications.ingress;
},
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
......@@ -329,6 +322,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
:uninstall-successful="applications.ingress.uninstallSuccessful"
:uninstall-failed="applications.ingress.uninstallFailed"
:disabled="!helmInstalled"
:updateable="false"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
<div slot="description">
......@@ -340,25 +334,10 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
}}
</p>
<template>
<div class="form-group">
<div class="form-check form-check-inline">
<input
v-model="applications.ingress.modsecurity_enabled"
:disabled="ingressInstalled"
type="checkbox"
autocomplete="off"
class="form-check-input"
/>
<label class="form-check-label label-bold" for="ingress-enable-modsecurity">
{{ s__('ClusterIntegration|Enable Web Application Firewall') }}
</label>
</div>
<p class="form-text text-muted">
<strong v-html="ingressModSecurityDescription"></strong>
</p>
</div>
</template>
<ingress-modsecurity-settings
:ingress="ingress"
:ingress-mod-security-help-path="ingressModSecurityHelpPath"
/>
<template v-if="ingressInstalled">
<div class="form-group">
......
<script>
import _ from 'lodash';
import { __ } from '../../locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
LoadingButton,
GlAlert,
GlSprintf,
GlLink,
},
props: {
ingress: {
type: Object,
required: true,
},
ingressModSecurityHelpPath: {
type: String,
required: false,
default: '',
},
},
computed: {
modSecurityEnabled: {
get() {
return this.ingress.modsecurity_enabled;
},
set(isEnabled) {
eventHub.$emit('setIngressModSecurityEnabled', {
id: INGRESS,
modSecurityEnabled: isEnabled,
});
},
},
ingressModSecurityDescription() {
return _.escape(this.ingressModSecurityHelpPath);
},
saving() {
return [UPDATING].includes(this.ingress.status);
},
saveButtonDisabled() {
return [UNINSTALLING, UPDATING].includes(this.ingress.status);
},
saveButtonLabel() {
return this.saving ? __('Saving') : __('Save changes');
},
ingressInstalled() {
return this.ingress.installed;
},
},
methods: {
updateApplication() {
eventHub.$emit('updateApplication', {
id: INGRESS,
params: { modsecurity_enabled: this.ingress.modsecurity_enabled },
});
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="ingress.updateFailed"
class="mb-3"
variant="danger"
:dismissible="false"
@dismiss="alert = null"
>
{{
s__('ClusterIntegration|Something went wrong while updating the Web Application Firewall.')
}}
</gl-alert>
<div class="form-group">
<div class="form-check form-check-inline">
<input
v-model="modSecurityEnabled"
type="checkbox"
autocomplete="off"
class="form-check-input"
/>
<label class="form-check-label label-bold" for="ingress-enable-modsecurity">
{{ s__('ClusterIntegration|Enable Web Application Firewall') }}
</label>
</div>
<p class="form-text text-muted">
<strong>
<gl-sprintf
:message="s__('ClusterIntegration|Learn more about %{linkStart}ModSecurity%{linkEnd}')"
>
<template #link="{ content }">
<gl-link :href="ingressModSecurityDescription" target="_blank"
>{{ content }}
</gl-link>
</template>
</gl-sprintf>
</strong>
</p>
<loading-button
v-if="ingressInstalled"
class="btn-success mt-1"
:loading="saving"
:disabled="saveButtonDisabled"
:label="saveButtonLabel"
@click="updateApplication"
/>
</div>
</div>
</template>
......@@ -54,6 +54,8 @@ export default class ClusterStore {
modsecurity_enabled: false,
externalIp: null,
externalHostname: null,
isEditingModSecurityEnabled: false,
updateFailed: false,
},
cert_manager: {
...applicationInitialState,
......@@ -208,8 +210,11 @@ export default class ClusterStore {
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname;
this.state.applications.ingress.modsecurity_enabled =
serverAppEntry.modsecurity_enabled || this.state.applications.ingress.modsecurity_enabled;
if (!this.state.applications.ingress.isEditingModSecurityEnabled) {
this.state.applications.ingress.modsecurity_enabled =
serverAppEntry.modsecurity_enabled ||
this.state.applications.ingress.modsecurity_enabled;
}
} else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
......
---
title: Allow enabling/disabling modsecurity from UI
merge_request: 24747
author:
type: added
......@@ -4310,7 +4310,7 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}."
msgstr ""
msgid "ClusterIntegration|Learn more about %{startLink}ModSecurity%{endLink}"
msgid "ClusterIntegration|Learn more about %{linkStart}ModSecurity%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|Learn more about %{startLink}Regions %{externalLinkIcon}%{endLink}."
......@@ -4595,6 +4595,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while updating Knative domain name."
msgstr ""
msgid "ClusterIntegration|Something went wrong while updating the Web Application Firewall."
msgstr ""
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
msgstr ""
......
......@@ -7,6 +7,7 @@ import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
import eventHub from '~/clusters/event_hub';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
describe('Applications', () => {
let vm;
......@@ -156,6 +157,30 @@ describe('Applications', () => {
});
describe('Ingress application', () => {
describe('with nested component', () => {
const propsData = {
applications: {
...APPLICATIONS_MOCK_STATE,
ingress: {
title: 'Ingress',
status: 'installed',
},
},
};
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Applications, { propsData });
});
afterEach(() => {
wrapper.destroy();
});
it('renders IngressModsecuritySettings', () => {
const modsecuritySettings = wrapper.find(IngressModsecuritySettings);
expect(modsecuritySettings.exists()).toBe(true);
});
});
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
......
import { shallowMount } from '@vue/test-utils';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants';
import { GlAlert } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
const { UPDATING } = APPLICATION_STATUS;
describe('IngressModsecuritySettings', () => {
let wrapper;
const defaultProps = {
modsecurity_enabled: false,
status: 'installable',
installed: false,
};
const createComponent = (props = defaultProps) => {
wrapper = shallowMount(IngressModsecuritySettings, {
propsData: {
ingress: {
...defaultProps,
...props,
},
},
});
};
const findSaveButton = () => wrapper.find(LoadingButton);
const findModSecurityCheckbox = () => wrapper.find('input').element;
describe('when ingress is installed', () => {
beforeEach(() => {
createComponent({ installed: true });
jest.spyOn(eventHub, '$emit');
});
it('renders save button', () => {
expect(findSaveButton().exists()).toBe(true);
expect(findModSecurityCheckbox().checked).toBe(false);
});
describe('and the save changes button is clicked', () => {
beforeEach(() => {
findSaveButton().vm.$emit('click');
});
it('triggers save event and pass current modsecurity value', () =>
wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: INGRESS,
params: { modsecurity_enabled: false },
});
}));
});
it('triggers set event to be propagated with the current modsecurity value', () => {
wrapper.setData({ modSecurityEnabled: true });
return wrapper.vm.$nextTick().then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('setIngressModSecurityEnabled', {
id: INGRESS,
modSecurityEnabled: true,
});
});
});
describe(`when ingress status is ${UPDATING}`, () => {
beforeEach(() => {
createComponent({ installed: true, status: UPDATING });
});
it('renders loading spinner in save button', () => {
expect(findSaveButton().props('loading')).toBe(true);
});
it('renders disabled save button', () => {
expect(findSaveButton().props('disabled')).toBe(true);
});
it('renders save button with "Saving" label', () => {
expect(findSaveButton().props('label')).toBe('Saving');
});
});
describe('when ingress fails to update', () => {
beforeEach(() => {
createComponent({ updateFailed: true });
});
it('displays a error message', () => {
expect(wrapper.find(GlAlert).exists()).toBe(true);
});
});
});
describe('when ingress is not installed', () => {
beforeEach(() => {
createComponent();
});
it('does not render the save button', () => {
expect(findSaveButton().exists()).toBe(false);
expect(findModSecurityCheckbox().checked).toBe(false);
});
});
});
......@@ -81,8 +81,10 @@ describe('Clusters Store', () => {
externalIp: null,
externalHostname: null,
installed: false,
isEditingModSecurityEnabled: false,
installFailed: true,
uninstallable: false,
updateFailed: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
......
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