Commit ef0e0286 authored by Zamir Martins Filho's avatar Zamir Martins Filho Committed by Tim Zallmann

Add option for switching WAF modes

Modes can be either logging
and blocking in addition to
enabling or disabling Modsecurity
parent 38f46ea8
......@@ -256,7 +256,8 @@ export default class Clusters {
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
eventHub.$on('resetIngressModSecurityEnabled', id => this.resetIngressModSecurityEnabled(id));
eventHub.$on('setIngressModSecurityMode', data => this.setIngressModSecurityMode(data));
eventHub.$on('resetIngressModSecurityChanges', id => this.resetIngressModSecurityChanges(id));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
......@@ -271,7 +272,8 @@ export default class Clusters {
eventHub.$off('setCrossplaneProviderStack');
eventHub.$off('uninstallApplication');
eventHub.$off('setIngressModSecurityEnabled');
eventHub.$off('resetIngressModSecurityEnabled');
eventHub.$off('setIngressModSecurityMode');
eventHub.$off('resetIngressModSecurityChanges');
}
initPolling(method, successCallback, errorCallback) {
......@@ -525,8 +527,14 @@ export default class Clusters {
this.store.updateAppProperty(id, 'modsecurity_enabled', modSecurityEnabled);
}
resetIngressModSecurityEnabled(id) {
setIngressModSecurityMode({ id, modSecurityMode }) {
this.store.updateAppProperty(id, 'isEditingModSecurityMode', true);
this.store.updateAppProperty(id, 'modsecurity_mode', modSecurityMode);
}
resetIngressModSecurityChanges(id) {
this.store.updateAppProperty(id, 'isEditingModSecurityEnabled', false);
this.store.updateAppProperty(id, 'isEditingModSecurityMode', false);
}
destroy() {
......
......@@ -313,6 +313,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
:install-failed="applications.ingress.installFailed"
:install-application-request-params="{
modsecurity_enabled: applications.ingress.modsecurity_enabled,
modsecurity_mode: applications.ingress.modsecurity_mode,
}"
:uninstallable="applications.ingress.uninstallable"
:uninstall-successful="applications.ingress.uninstallSuccessful"
......
<script>
import _ from 'lodash';
import { __ } from '../../locale';
import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants';
import { GlAlert, GlSprintf, GlLink, GlToggle, GlButton } from '@gitlab/ui';
import { escape as esc } from 'lodash';
import { s__, __ } from '../../locale';
import { APPLICATION_STATUS, INGRESS, LOGGING_MODE, BLOCKING_MODE } from '~/clusters/constants';
import {
GlAlert,
GlSprintf,
GlLink,
GlToggle,
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
} from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
import modSecurityLogo from 'images/cluster_app_logos/modsecurity.png';
......@@ -17,6 +26,9 @@ export default {
GlLink,
GlToggle,
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
},
props: {
ingress: {
......@@ -28,10 +40,23 @@ export default {
required: false,
default: '',
},
modes: {
type: Object,
required: false,
default: () => ({
[LOGGING_MODE]: {
name: s__('ClusterIntegration|Logging mode'),
},
[BLOCKING_MODE]: {
name: s__('ClusterIntegration|Blocking mode'),
},
}),
},
},
data: () => ({
modSecurityLogo,
hasValueChanged: false,
initialValue: null,
initialMode: null,
}),
computed: {
modSecurityEnabled: {
......@@ -39,19 +64,30 @@ export default {
return this.ingress.modsecurity_enabled;
},
set(isEnabled) {
if (this.initialValue === null) {
this.initialValue = this.ingress.modsecurity_enabled;
}
eventHub.$emit('setIngressModSecurityEnabled', {
id: INGRESS,
modSecurityEnabled: isEnabled,
});
if (this.hasValueChanged) {
this.resetStatus();
} else {
this.hasValueChanged = true;
}
},
},
hasValueChanged() {
return this.modSecurityEnabledChanged || this.modSecurityModeChanged;
},
modSecurityEnabledChanged() {
return this.initialValue !== null && this.initialValue !== this.ingress.modsecurity_enabled;
},
modSecurityModeChanged() {
return (
this.ingress.modsecurity_enabled &&
this.initialMode !== null &&
this.initialMode !== this.ingress.modsecurity_mode
);
},
ingressModSecurityDescription() {
return _.escape(this.ingressModSecurityHelpPath);
return esc(this.ingressModSecurityHelpPath);
},
saving() {
return [UPDATING].includes(this.ingress.status);
......@@ -73,18 +109,40 @@ export default {
this.saving || (this.hasValueChanged && [INSTALLED, UPDATED].includes(this.ingress.status))
);
},
modSecurityModeName() {
return this.modes[this.ingress.modsecurity_mode].name;
},
},
methods: {
updateApplication() {
eventHub.$emit('updateApplication', {
id: INGRESS,
params: { modsecurity_enabled: this.ingress.modsecurity_enabled },
params: {
modsecurity_enabled: this.ingress.modsecurity_enabled,
modsecurity_mode: this.ingress.modsecurity_mode,
},
});
this.resetStatus();
},
resetStatus() {
eventHub.$emit('resetIngressModSecurityEnabled', INGRESS);
this.hasValueChanged = false;
if (this.initialMode !== null) {
this.ingress.modsecurity_mode = this.initialMode;
}
if (this.initialValue !== null) {
this.ingress.modsecurity_enabled = this.initialValue;
}
this.initialValue = null;
this.initialMode = null;
eventHub.$emit('resetIngressModSecurityChanges', INGRESS);
},
selectMode(modeKey) {
if (this.initialMode === null) {
this.initialMode = this.ingress.modsecurity_mode;
}
eventHub.$emit('setIngressModSecurityMode', {
id: INGRESS,
modSecurityMode: modeKey,
});
},
},
};
......@@ -144,7 +202,35 @@ export default {
label-position="right"
/>
</div>
<div v-if="showButtons">
<div
v-if="ingress.modsecurity_enabled"
class="gl-responsive-table-row-layout mt-3"
role="row"
>
<div class="table-section section-wrap" role="gridcell">
<strong>
{{ s__('ClusterIntegration|Global default') }}
<gl-icon name="earth" class="align-text-bottom" />
</strong>
<div class="form-group">
<p class="form-text text-muted">
<strong>
{{
s__(
'ClusterIntegration|Set the global mode for the WAF in this cluster. This can be overridden at the environmental level.',
)
}}
</strong>
</p>
</div>
<gl-dropdown :text="modSecurityModeName" :disabled="saveButtonDisabled">
<gl-dropdown-item v-for="(mode, key) in modes" :key="key" @click="selectMode(key)">
{{ mode.name }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</div>
<div v-if="showButtons" class="mt-3">
<gl-button
class="btn-success inline mr-1"
:loading="saving"
......
......@@ -66,3 +66,6 @@ export const APPLICATIONS = [
];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
export const LOGGING_MODE = 'logging';
export const BLOCKING_MODE = 'blocking';
......@@ -53,9 +53,11 @@ export default class ClusterStore {
...applicationInitialState,
title: s__('ClusterIntegration|Ingress'),
modsecurity_enabled: false,
modsecurity_mode: null,
externalIp: null,
externalHostname: null,
isEditingModSecurityEnabled: false,
isEditingModSecurityMode: false,
updateFailed: false,
},
cert_manager: {
......@@ -214,6 +216,9 @@ export default class ClusterStore {
if (!this.state.applications.ingress.isEditingModSecurityEnabled) {
this.state.applications.ingress.modsecurity_enabled = serverAppEntry.modsecurity_enabled;
}
if (!this.state.applications.ingress.isEditingModSecurityMode) {
this.state.applications.ingress.modsecurity_mode = serverAppEntry.modsecurity_mode;
}
} else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
......
......@@ -47,7 +47,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
def cluster_application_params
params.permit(:application, :hostname, :email, :stack, :modsecurity_enabled)
params.permit(:application, :hostname, :email, :stack, :modsecurity_enabled, :modsecurity_mode)
end
def cluster_application_destroy_params
......
......@@ -6,6 +6,9 @@ module Clusters
VERSION = '1.29.7'
INGRESS_CONTAINER_NAME = 'nginx-ingress-controller'
MODSECURITY_LOG_CONTAINER_NAME = 'modsecurity-log'
MODSECURITY_MODE_LOGGING = "DetectionOnly"
MODSECURITY_MODE_BLOCKING = "On"
MODSECURITY_OWASP_RULES_FILE = "/etc/nginx/owasp-modsecurity-crs/nginx-modsecurity.conf"
self.table_name = 'clusters_applications_ingress'
......@@ -18,11 +21,14 @@ module Clusters
default_value_for :ingress_type, :nginx
default_value_for :modsecurity_enabled, true
default_value_for :version, VERSION
default_value_for :modsecurity_mode, :logging
enum ingress_type: {
nginx: 1
}
enum modsecurity_mode: { logging: 0, blocking: 1 }
FETCH_IP_ADDRESS_DELAY = 30.seconds
MODSEC_SIDECAR_INITIAL_DELAY_SECONDS = 10
......@@ -82,7 +88,8 @@ module Clusters
"controller" => {
"config" => {
"enable-modsecurity" => "true",
"enable-owasp-modsecurity-crs" => "true",
"enable-owasp-modsecurity-crs" => "false",
"modsecurity-snippet" => modsecurity_snippet_content,
"modsecurity.conf" => modsecurity_config_content
},
"extraContainers" => [
......@@ -157,6 +164,11 @@ module Clusters
def application_jupyter_nil_or_installable?
cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
end
def modsecurity_snippet_content
sec_rule_engine = logging? ? MODSECURITY_MODE_LOGGING : MODSECURITY_MODE_BLOCKING
"SecRuleEngine #{sec_rule_engine}\nInclude #{MODSECURITY_OWASP_RULES_FILE}"
end
end
end
end
......@@ -15,4 +15,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :can_uninstall?, as: :can_uninstall
expose :available_domains, using: Serverless::DomainEntity, if: -> (e, _) { e.respond_to?(:available_domains) }
expose :pages_domain, using: Serverless::DomainEntity, if: -> (e, _) { e.respond_to?(:pages_domain) }
expose :modsecurity_mode, if: -> (e, _) { e.respond_to?(:modsecurity_mode) }
end
......@@ -31,6 +31,10 @@ module Clusters
application.modsecurity_enabled = params[:modsecurity_enabled] || false
end
if application.has_attribute?(:modsecurity_mode)
application.modsecurity_mode = params[:modsecurity_mode] || 0
end
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application, request)
end
......
---
title: Add option for switching between blocking and logging for WAF
merge_request: 27133
author:
type: added
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddModsecurityModeToIngressApplication < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:clusters_applications_ingress, :modsecurity_mode, :smallint, default: 0, allow_null: false)
end
def down
remove_column :clusters_applications_ingress, :modsecurity_mode
end
end
......@@ -1199,6 +1199,7 @@ ActiveRecord::Schema.define(version: 2020_03_13_123934) do
t.string "external_ip"
t.string "external_hostname"
t.boolean "modsecurity_enabled"
t.integer "modsecurity_mode", limit: 2, default: 0, null: false
t.index ["cluster_id"], name: "index_clusters_applications_ingress_on_cluster_id", unique: true
end
......
......@@ -4126,6 +4126,9 @@ msgstr ""
msgid "ClusterIntegration|Base domain"
msgstr ""
msgid "ClusterIntegration|Blocking mode"
msgstr ""
msgid "ClusterIntegration|CA Certificate"
msgstr ""
......@@ -4324,6 +4327,9 @@ msgstr ""
msgid "ClusterIntegration|Gitlab Integration"
msgstr ""
msgid "ClusterIntegration|Global default"
msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
......@@ -4483,6 +4489,9 @@ msgstr ""
msgid "ClusterIntegration|Loading subnetworks"
msgstr ""
msgid "ClusterIntegration|Logging mode"
msgstr ""
msgid "ClusterIntegration|Machine type"
msgstr ""
......@@ -4711,6 +4720,9 @@ msgstr ""
msgid "ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared."
msgstr ""
msgid "ClusterIntegration|Set the global mode for the WAF in this cluster. This can be overridden at the environmental level."
msgstr ""
msgid "ClusterIntegration|Show"
msgstr ""
......
......@@ -38,6 +38,7 @@
"email": { "type": ["string", "null"] },
"stack": { "type": ["string", "null"] },
"modsecurity_enabled": { "type": ["boolean", "null"] },
"modsecurity_mode": {"type": ["integer", "0"]},
"update_available": { "type": ["boolean", "null"] },
"can_uninstall": { "type": "boolean" },
"available_domains": {
......
import { shallowMount } from '@vue/test-utils';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants';
import { GlAlert, GlToggle } from '@gitlab/ui';
import { GlAlert, GlToggle, GlDropdown } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
const { UPDATING } = APPLICATION_STATUS;
......@@ -13,6 +13,7 @@ describe('IngressModsecuritySettings', () => {
modsecurity_enabled: false,
status: 'installable',
installed: false,
modsecurity_mode: 'logging',
};
const createComponent = (props = defaultProps) => {
......@@ -29,6 +30,7 @@ describe('IngressModsecuritySettings', () => {
const findSaveButton = () => wrapper.find('.btn-success');
const findCancelButton = () => wrapper.find('[variant="secondary"]');
const findModSecurityToggle = () => wrapper.find(GlToggle);
const findModSecurityDropdown = () => wrapper.find(GlDropdown);
describe('when ingress is installed', () => {
beforeEach(() => {
......@@ -44,22 +46,50 @@ describe('IngressModsecuritySettings', () => {
describe('with toggle changed by the user', () => {
beforeEach(() => {
findModSecurityToggle().vm.$emit('change');
wrapper.setProps({
ingress: {
...defaultProps,
installed: true,
status: 'installed',
modsecurity_enabled: true,
},
});
});
it('renders both save and cancel buttons', () => {
it('renders save and cancel buttons', () => {
expect(findSaveButton().exists()).toBe(true);
expect(findCancelButton().exists()).toBe(true);
});
describe('and the save changes button is clicked', () => {
describe('with dropdown changed by the user', () => {
beforeEach(() => {
findSaveButton().vm.$emit('click');
findModSecurityDropdown().vm.$children[1].$emit('click');
wrapper.setProps({
ingress: {
...defaultProps,
installed: true,
status: 'installed',
modsecurity_enabled: true,
modsecurity_mode: 'blocking',
},
});
});
it('renders both save and cancel buttons', () => {
expect(findSaveButton().exists()).toBe(true);
expect(findCancelButton().exists()).toBe(true);
});
it('triggers save event and pass current modsecurity value', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: INGRESS,
params: { modsecurity_enabled: false },
describe('and the save changes button is clicked', () => {
beforeEach(() => {
findSaveButton().vm.$emit('click');
});
it('triggers save event and pass current modsecurity value', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: INGRESS,
params: { modsecurity_enabled: true, modsecurity_mode: 'blocking' },
});
});
});
});
......@@ -70,7 +100,7 @@ describe('IngressModsecuritySettings', () => {
});
it('triggers reset event and hides both cancel and save changes button', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('resetIngressModSecurityEnabled', INGRESS);
expect(eventHub.$emit).toHaveBeenCalledWith('resetIngressModSecurityChanges', INGRESS);
expect(findSaveButton().exists()).toBe(false);
expect(findCancelButton().exists()).toBe(false);
});
......
......@@ -82,6 +82,7 @@ describe('Clusters Store', () => {
externalHostname: null,
installed: false,
isEditingModSecurityEnabled: false,
isEditingModSecurityMode: false,
installFailed: true,
uninstallable: false,
updateFailed: false,
......@@ -89,6 +90,7 @@ describe('Clusters Store', () => {
uninstallFailed: false,
validationError: null,
modsecurity_enabled: false,
modsecurity_mode: undefined,
},
runner: {
title: 'GitLab Runner',
......
......@@ -140,13 +140,10 @@ describe Clusters::Applications::Ingress do
end
describe '#values' do
let(:project) { build(:project) }
let(:cluster) { build(:cluster, projects: [project]) }
subject { ingress }
context 'when modsecurity_enabled is enabled' do
before do
allow(subject).to receive(:cluster).and_return(cluster)
allow(subject).to receive(:modsecurity_enabled).and_return(true)
end
......@@ -154,8 +151,24 @@ describe Clusters::Applications::Ingress do
expect(subject.values).to include("enable-modsecurity: 'true'")
end
it 'includes modsecurity core ruleset enablement' do
expect(subject.values).to include("enable-owasp-modsecurity-crs: 'true'")
it 'includes modsecurity core ruleset enablement set to false' do
expect(subject.values).to include("enable-owasp-modsecurity-crs: 'false'")
end
it 'includes modsecurity snippet with information related to security rules' do
expect(subject.values).to include("SecRuleEngine DetectionOnly")
expect(subject.values).to include("Include #{described_class::MODSECURITY_OWASP_RULES_FILE}")
end
context 'when modsecurity_mode is set to :blocking' do
before do
subject.blocking!
end
it 'includes modsecurity snippet with information related to security rules' do
expect(subject.values).to include("SecRuleEngine On")
expect(subject.values).to include("Include #{described_class::MODSECURITY_OWASP_RULES_FILE}")
end
end
it 'includes modsecurity.conf content' do
......@@ -176,7 +189,6 @@ describe Clusters::Applications::Ingress do
context 'when modsecurity_enabled is disabled' do
before do
allow(subject).to receive(:cluster).and_return(cluster)
allow(subject).to receive(:modsecurity_enabled).and_return(false)
end
......
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