diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js index daf49781038630b2c015640dbb092810fd74dc5c..b675cbf48ff99a393421c0cfe8883b0d3cea99b1 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/constants.js @@ -3,6 +3,8 @@ export const EditorModeYAML = 'yaml'; export const RuleTypeNetwork = 'network'; +export const RuleActionTypeAllow = 'allow'; + export const RuleDirectionInbound = 'ingress'; export const RuleDirectionOutbound = 'egress'; diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_action_picker.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_action_picker.vue index 594f21b5f6e405f55c48d149140d8e5539e1951c..06c8ea0310b3fcba6edf7b8524a0338f1d61d417 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_action_picker.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_action_picker.vue @@ -1,7 +1,52 @@ <script> -export default {}; +import { GlForm, GlFormSelect, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { RuleActionTypeAllow } from './constants'; + +export default { + components: { + GlForm, + GlFormSelect, + GlSprintf, + }, + data() { + return { actionType: RuleActionTypeAllow }; + }, + actionTypes: [{ value: RuleActionTypeAllow, text: s__('NetworkPolicies|Allow') }], +}; </script> <template> - <div class="gl-bg-gray-100 p-2"></div> + <div + class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-px-5 gl-pt-5" + > + <gl-form inline> + <gl-sprintf + :message=" + s__( + 'NetworkPolicies|%{labelStart}Then%{labelEnd} %{action} %{spanStart}the network traffic.%{spanEnd}', + ) + " + > + <template #label="{ content }"> + <label for="actionType" class="text-uppercase gl-font-lg gl-mr-4 gl-mb-5!">{{ + content + }}</label> + </template> + + <template #action> + <gl-form-select + id="actionType" + class="gl-mr-4 gl-mb-5!" + :value="actionType" + :options="$options.actionTypes" + /> + </template> + + <template #span="{ content }"> + <span class="gl-mb-5">{{ content }}</span> + </template> + </gl-sprintf> + </gl-form> + </div> </template> diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_editor.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_editor.vue index 4d2a8843b00a889f4b84195b2b05b6b134b426ee..fa6be64f0c3e3757b597db03554f0f50a6c8f9df 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_editor.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_editor.vue @@ -1,5 +1,5 @@ <script> -import { mapActions } from 'vuex'; +import { mapState, mapActions } from 'vuex'; import { GlFormGroup, GlFormSelect, @@ -11,6 +11,7 @@ import { GlAlert, } from '@gitlab/ui'; import { s__ } from '~/locale'; +import { redirectTo } from '~/lib/utils/url_utility'; import EnvironmentPicker from '../environment_picker.vue'; import NetworkPolicyEditor from '../network_policy_editor.vue'; import PolicyRuleBuilder from './policy_rule_builder.vue'; @@ -43,6 +44,12 @@ export default { PolicyPreview, PolicyActionPicker, }, + props: { + threatMonitoringPath: { + type: String, + required: true, + }, + }, data() { return { editorMode: EditorModeRule, @@ -65,6 +72,8 @@ export default { policyYaml() { return toYaml(this.policy); }, + ...mapState('threatMonitoring', ['currentEnvironmentId']), + ...mapState('networkPolicies', ['errorUpdatingPolicy']), shouldShowRuleEditor() { return this.editorMode === EditorModeRule; }, @@ -80,6 +89,7 @@ export default { }, methods: { ...mapActions('threatMonitoring', ['fetchEnvironments']), + ...mapActions('networkPolicies', ['createPolicy']), addRule() { this.policy.rules.push(buildRule(RuleTypeEndpoint)); }, @@ -110,6 +120,15 @@ export default { this.editorMode = mode; }, + savePolicy() { + const policy = { manifest: toYaml(this.policy) }; + return this.createPolicy({ + environmentId: this.currentEnvironmentId, + policy, + }).then(() => { + if (!this.errorUpdatingPolicy) redirectTo(this.threatMonitoringPath); + }); + }, }, policyTypes: [{ value: 'networkPolicy', text: s__('NetworkPolicies|Network Policy') }], editorModes: [ @@ -197,7 +216,7 @@ export default { @endpoint-labels-change="updateEndpointLabels" /> - <div class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"> + <div class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5"> <gl-button variant="link" category="primary" @@ -209,6 +228,7 @@ export default { </div> <h4>{{ s__('NetworkPolicies|Actions') }}</h4> + <p>{{ s__('NetworkPolicies|Traffic that does not match any rule will be blocked.') }}</p> <policy-action-picker /> </div> <div class="col-sm-12 col-md-6 col-lg-5 col-xl-4"> @@ -238,10 +258,17 @@ export default { <hr /> <div class="row"> <div class="col-md-auto"> - <gl-button type="submit" category="primary" variant="success">{{ - s__('NetworkPolicies|Create policy') + <gl-button + type="submit" + category="primary" + variant="success" + data-testid="create-policy" + @click="savePolicy" + >{{ s__('NetworkPolicies|Create policy') }}</gl-button + > + <gl-button category="secondary" variant="default" :href="threatMonitoringPath">{{ + __('Cancel') }}</gl-button> - <gl-button category="secondary" variant="default">{{ __('Cancel') }}</gl-button> </div> </div> </section> diff --git a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_rule_builder.vue b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_rule_builder.vue index d849a85a27c52a2839e281a9bada25a98ac55d20..d8a8677c3ccd4ba426f0a44f6168905f77f33ff9 100644 --- a/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_rule_builder.vue +++ b/ee/app/assets/javascripts/threat_monitoring/components/policy_editor/policy_rule_builder.vue @@ -133,7 +133,7 @@ export default { <div class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base px-3 pt-3" > - <gl-form inline> + <gl-form inline @submit.prevent> <gl-sprintf :message="sprintfTemplate"> <template #ifLabel="{ content }"> <label for="ruleType" class="text-uppercase gl-font-lg gl-mr-4 gl-mb-5!">{{ diff --git a/ee/app/assets/javascripts/threat_monitoring/policy_editor.js b/ee/app/assets/javascripts/threat_monitoring/policy_editor.js index cc89bf739894dddcb5acfb88d7b0aa3f6d2de1ef..e7972d00fad10b5e518a8cf14ddb86072c0822d3 100644 --- a/ee/app/assets/javascripts/threat_monitoring/policy_editor.js +++ b/ee/app/assets/javascripts/threat_monitoring/policy_editor.js @@ -4,7 +4,7 @@ import createStore from './store'; export default () => { const el = document.querySelector('#js-policy-builder-app'); - const { environmentsEndpoint, networkPoliciesEndpoint } = el.dataset; + const { environmentsEndpoint, networkPoliciesEndpoint, threatMonitoringPath } = el.dataset; const store = createStore(); store.dispatch('threatMonitoring/setEndpoints', { @@ -18,7 +18,9 @@ export default () => { el, store, render(createElement) { - return createElement(PolicyEditorApp, {}); + return createElement(PolicyEditorApp, { + props: { threatMonitoringPath }, + }); }, }); }; diff --git a/ee/app/assets/javascripts/threat_monitoring/store/modules/threat_monitoring/mutations.js b/ee/app/assets/javascripts/threat_monitoring/store/modules/threat_monitoring/mutations.js index 33627be4f621d568ff795e1d5a25f4d2c46fc3e8..1a61b948c9bdcb4b21279d99e243f2bf43037d5a 100644 --- a/ee/app/assets/javascripts/threat_monitoring/store/modules/threat_monitoring/mutations.js +++ b/ee/app/assets/javascripts/threat_monitoring/store/modules/threat_monitoring/mutations.js @@ -12,6 +12,7 @@ export default { state.environments = payload; state.isLoadingEnvironments = false; state.errorLoadingEnvironments = false; + if (payload.length > 0) state.currentEnvironmentId = payload[0].id; }, [types.RECEIVE_ENVIRONMENTS_ERROR](state) { state.isLoadingEnvironments = false; diff --git a/ee/app/views/projects/threat_monitoring/new.html.haml b/ee/app/views/projects/threat_monitoring/new.html.haml index 15cf7234ba1e2325f8b2850dba0069badcdc47bc..bd20cfb2e8f47864d620cdcbf172bfffca94e2b3 100644 --- a/ee/app/views/projects/threat_monitoring/new.html.haml +++ b/ee/app/views/projects/threat_monitoring/new.html.haml @@ -4,4 +4,5 @@ #js-policy-builder-app{ data: { network_policies_endpoint: project_security_network_policies_path(@project), environments_endpoint: project_environments_path(@project), + threat_monitoring_path: project_threat_monitoring_path(@project), } } diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_action_picker_spec.js.snap b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_action_picker_spec.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..3d998885233e225338bc2a19e50d35f7b3f241ce --- /dev/null +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_action_picker_spec.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PolicyActionPicker component renders policy action picker 1`] = ` +<div + class="gl-bg-gray-10 gl-border-solid gl-border-1 gl-border-gray-100 gl-rounded-base gl-px-5 gl-pt-5" +> + <gl-form-stub + inline="" + > + <gl-sprintf-stub + message="%{labelStart}Then%{labelEnd} %{action} %{spanStart}the network traffic.%{spanEnd}" + /> + </gl-form-stub> +</div> +`; diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap index 29ede8160db38129bd35917d54856b31586ff1f0..40878e84d36e8beeab648a2e1d58e4abe572b17d 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/__snapshots__/policy_editor_spec.js.snap @@ -158,7 +158,7 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = ` </h4> <div - class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100" + class="gl-p-3 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-mb-5" > <gl-button-stub category="primary" @@ -175,6 +175,10 @@ exports[`PolicyEditorApp component renders the policy editor layout 1`] = ` Actions </h4> + <p> + Traffic that does not match any rule will be blocked. + </p> + <policy-action-picker-stub /> </div> @@ -213,6 +217,7 @@ spec: > <gl-button-stub category="primary" + data-testid="create-policy" icon="" size="medium" type="submit" @@ -223,6 +228,7 @@ spec: <gl-button-stub category="secondary" + href="/threat-monitoring" icon="" size="medium" variant="default" diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_action_picker_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_action_picker_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c76d0299aee43815809780616d3fba00329631c0 --- /dev/null +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_action_picker_spec.js @@ -0,0 +1,26 @@ +import { shallowMount } from '@vue/test-utils'; +import PolicyActionPicker from 'ee/threat_monitoring/components/policy_editor/policy_action_picker.vue'; + +describe('PolicyActionPicker component', () => { + let wrapper; + + const factory = ({ propsData } = {}) => { + wrapper = shallowMount(PolicyActionPicker, { + propsData: { + ...propsData, + }, + }); + }; + + beforeEach(() => { + factory(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders policy action picker', () => { + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js index 982a5d1250ea245c3dddcc3985da8ab9850a8fa4..e81562b14f949568695408615b30e878b526a922 100644 --- a/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js +++ b/ee/spec/frontend/threat_monitoring/components/policy_editor/policy_editor_spec.js @@ -12,6 +12,10 @@ import { EndpointMatchModeLabel, } from 'ee/threat_monitoring/components/policy_editor/constants'; import fromYaml from 'ee/threat_monitoring/components/policy_editor/lib/from_yaml'; +import toYaml from 'ee/threat_monitoring/components/policy_editor/lib/to_yaml'; +import { redirectTo } from '~/lib/utils/url_utility'; + +jest.mock('~/lib/utils/url_utility'); describe('PolicyEditorApp component', () => { let store; @@ -22,9 +26,15 @@ describe('PolicyEditorApp component', () => { Object.assign(store.state.threatMonitoring, { ...state, }); + Object.assign(store.state.networkPolicies, { + ...state, + }); + + jest.spyOn(store, 'dispatch').mockImplementation(() => Promise.resolve()); wrapper = shallowMount(PolicyEditorApp, { propsData: { + threatMonitoringPath: '/threat-monitoring', ...propsData, }, store, @@ -45,7 +55,6 @@ describe('PolicyEditorApp component', () => { afterEach(() => { wrapper.destroy(); - wrapper = null; }); it('renders the policy editor layout', () => { @@ -188,4 +197,32 @@ spec: expect(findAddRuleButton().props('disabled')).toBe(true); }); }); + + it('creates policy and redirects to a threat monitoring path', async () => { + wrapper.find("[data-testid='create-policy']").vm.$emit('click'); + + await wrapper.vm.$nextTick(); + expect(store.dispatch).toHaveBeenCalledWith('networkPolicies/createPolicy', { + environmentId: -1, + policy: { manifest: toYaml(wrapper.vm.policy) }, + }); + expect(redirectTo).toHaveBeenCalledWith('/threat-monitoring'); + }); + + describe('given there is a createPolicy error', () => { + beforeEach(() => { + factory({ + state: { + errorUpdatingPolicy: true, + }, + }); + }); + + it('it does not redirect', async () => { + wrapper.find("[data-testid='create-policy']").vm.$emit('click'); + + await wrapper.vm.$nextTick(); + expect(redirectTo).not.toHaveBeenCalledWith('/threat-monitoring'); + }); + }); }); diff --git a/ee/spec/frontend/threat_monitoring/store/modules/threat_monitoring/mutations_spec.js b/ee/spec/frontend/threat_monitoring/store/modules/threat_monitoring/mutations_spec.js index 8ed5850958d8caf2894cc99631a3adfab20a8201..b7e6ffade1ffb956fc27e5616fee69d4e59b8856 100644 --- a/ee/spec/frontend/threat_monitoring/store/modules/threat_monitoring/mutations_spec.js +++ b/ee/spec/frontend/threat_monitoring/store/modules/threat_monitoring/mutations_spec.js @@ -48,6 +48,10 @@ describe('Threat Monitoring mutations', () => { it('sets errorLoadingEnvironments to false', () => { expect(state.errorLoadingEnvironments).toBe(false); }); + + it('sets currentEnvironmentId to 1', () => { + expect(state.currentEnvironmentId).toEqual(1); + }); }); describe(types.RECEIVE_ENVIRONMENTS_ERROR, () => { diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3eaaf4005806c08182f10f7b332a94c872c87bca..df6345cba625fd4887bb6a488692bb4e297f22bc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16170,6 +16170,9 @@ msgstr "" msgid "NetworkPolicies|%{ifLabelStart}if%{ifLabelEnd} %{ruleType} %{isLabelStart}is%{isLabelEnd} %{ruleDirection} %{ruleSelector} %{directionLabelStart}and is outbound to a%{directionLabelEnd} %{rule} %{portsLabelStart}on%{portsLabelEnd} %{ports}" msgstr "" +msgid "NetworkPolicies|%{labelStart}Then%{labelEnd} %{action} %{spanStart}the network traffic.%{spanEnd}" +msgstr "" + msgid "NetworkPolicies|%{number} selected" msgstr "" @@ -16191,6 +16194,9 @@ msgstr "" msgid "NetworkPolicies|All selected" msgstr "" +msgid "NetworkPolicies|Allow" +msgstr "" + msgid "NetworkPolicies|Allow all inbound traffic to %{selector} from %{ruleSelector} on %{ports}" msgstr "" @@ -16299,6 +16305,9 @@ msgstr "" msgid "NetworkPolicies|Status" msgstr "" +msgid "NetworkPolicies|Traffic that does not match any rule will be blocked." +msgstr "" + msgid "NetworkPolicies|YAML editor" msgstr ""