Commit 0f508dac authored by Phil Hughes's avatar Phil Hughes

Merge branch '6078-add-permissions-checks-dismiss-issue' into 'master'

Resolve "Hide or disable security reports feedback actions when not authorized"

Closes #6078

See merge request gitlab-org/gitlab-ee!6181
parents 1156689f 600a6b52
......@@ -122,6 +122,8 @@ export default () => {
sastContainerHelpPath,
} = datasetOptions;
const pipelineId = parseInt(datasetOptions.pipelineId, 10);
const canCreateIssue = datasetOptions.canCreateIssue;
const canCreateFeedback = datasetOptions.canCreateFeedback;
const store = createStore();
// Widget summary
......@@ -174,6 +176,8 @@ export default () => {
sastContainerHeadPath: sastContainerEndpoint,
dastHelpPath,
sastContainerHelpPath,
canCreateFeedback,
canCreateIssue,
},
on: {
updateBadgeCount: this.updateBadge,
......
......@@ -100,4 +100,6 @@
sast_help_path: help_page_path('user/project/merge_requests/sast'),
dependency_scanning_help_path: help_page_path('user/project/merge_requests/dependency_scanning'),
dast_help_path: help_page_path('user/project/merge_requests/dast'),
sast_container_help_path: help_page_path('user/project/merge_requests/sast_container')} }
sast_container_help_path: help_page_path('user/project/merge_requests/sast_container'),
can_create_feedback: can?(current_user, :admin_vulnerability_feedback, @project),
can_create_issue: show_new_issue_link?(@project)} }
......@@ -288,6 +288,8 @@ export default {
:vulnerability-feedback-path="mr.vulnerabilityFeedbackPath"
:vulnerability-feedback-help-path="mr.vulnerabilityFeedbackHelpPath"
:pipeline-id="mr.securityReportsPipelineId"
:can-create-issue="mr.canCreateIssue"
:can-create-feedback="mr.canCreateFeedback"
/>
<report-section
v-if="shouldRenderLicenseReport"
......
......@@ -20,6 +20,7 @@ export default class MergeRequestStore extends CEMergeRequestStore {
this.vulnerabilityFeedbackHelpPath = data.vulnerability_feedback_help_path;
this.approvalsHelpPath = data.approvals_help_path;
this.securityReportsPipelineId = data.pipeline_id;
this.canCreateFeedback = data.can_create_feedback || false;
this.initCodeclimate(data);
this.initPerformanceReport(data);
......
......@@ -2,11 +2,7 @@
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import { inserted } from '~/feature_highlight/feature_highlight_helper';
import {
mouseenter,
debouncedMouseleave,
togglePopover,
} from '~/shared/popover';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
export default {
name: 'SecurityReportsHelpPopover',
......@@ -22,7 +18,8 @@ export default {
mounted() {
const $el = $(this.$el);
$el.popover({
$el
.popover({
html: true,
trigger: 'focus',
container: 'body',
......
<script>
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Modal from '~/vue_shared/components/gl_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Modal from '~/vue_shared/components/gl_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue';
export default {
export default {
components: {
Modal,
LoadingButton,
......@@ -14,16 +14,32 @@ export default {
Icon,
},
computed: {
...mapState(['modal', 'vulnerabilityFeedbackHelpPath']),
...mapState([
'modal',
'vulnerabilityFeedbackHelpPath',
'canCreateIssuePermission',
'canCreateFeedbackPermission',
]),
revertTitle() {
return this.modal.vulnerability.isDismissed
? s__('ciReport|Revert dismissal')
: s__('ciReport|Dismiss vulnerability');
},
hasDismissedBy() {
return this.modal.vulnerability.dismissalFeedback &&
return (
this.modal.vulnerability.dismissalFeedback &&
this.modal.vulnerability.dismissalFeedback.pipeline &&
this.modal.vulnerability.dismissalFeedback.author;
this.modal.vulnerability.dismissalFeedback.author
);
},
/**
* The slot for the footer should be rendered if any of the conditions is true.
*/
shouldRenderFooterSection() {
return (
!this.modal.isResolved &&
(this.canCreateFeedbackPermission || this.canCreateIssuePermission)
);
},
},
methods: {
......@@ -51,13 +67,13 @@ export default {
return key === 'links' && this.hasValue(field);
},
},
};
};
</script>
<template>
<modal
id="modal-mrwidget-security-issue"
:header-title-text="modal.title"
:class="{'modal-hide-footer': modal.isResolved}"
:class="{ 'modal-hide-footer': !shouldRenderFooterSection }"
class="modal-security-report-dast"
>
<slot>
......@@ -183,7 +199,7 @@ export default {
:href="vulnerabilityFeedbackHelpPath"
class="js-link-vulnerabilityFeedbackHelpPath"
>
Learn more about interacting with security reports (Alpha).
{{ s__('ciReport|Learn more about interacting with security reports (Alpha).') }}
</a>
</div>
</div>
......@@ -196,7 +212,7 @@ export default {
</div>
</slot>
<div slot="footer">
<template v-if="!modal.isResolved">
<template v-if="shouldRenderFooterSection">
<button
type="button"
class="btn btn-default"
......@@ -206,6 +222,7 @@ export default {
</button>
<loading-button
v-if="canCreateFeedbackPermission"
:loading="modal.isDismissingIssue"
:disabled="modal.isDismissingIssue"
:label="revertTitle"
......@@ -221,12 +238,13 @@ export default {
>
{{ __('View issue' ) }}
</a>
<loading-button
v-else
v-else-if="!modal.vulnerability.hasIssue && canCreateIssuePermission"
:loading="modal.isCreatingNewIssue"
:disabled="modal.isCreatingNewIssue"
:label="__('Create issue')"
container-class="btn btn-success btn-inverted"
container-class="js-create-issue-btn btn btn-success btn-inverted"
@click="createNewIssue"
/>
</template>
......
......@@ -102,6 +102,14 @@ export default {
required: false,
default: null,
},
canCreateFeedback: {
type: Boolean,
required: true,
},
canCreateIssue: {
type: Boolean,
required: true,
},
},
sast: SAST,
dast: DAST,
......@@ -130,6 +138,9 @@ export default {
this.setVulnerabilityFeedbackHelpPath(this.vulnerabilityFeedbackHelpPath);
this.setPipelineId(this.pipelineId);
this.setCanCreateIssuePermission(this.canCreateIssue);
this.setCanCreateFeedbackPermission(this.canCreateFeedback);
if (this.sastHeadPath) {
this.setSastHeadPath(this.sastHeadPath);
......@@ -186,6 +197,8 @@ export default {
'setVulnerabilityFeedbackPath',
'setVulnerabilityFeedbackHelpPath',
'setPipelineId',
'setCanCreateIssuePermission',
'setCanCreateFeedbackPermission',
]),
},
};
......
......@@ -74,6 +74,14 @@ export default {
required: false,
default: null,
},
canCreateFeedback: {
type: Boolean,
required: true,
},
canCreateIssue: {
type: Boolean,
required: true,
},
},
sast: SAST,
dast: DAST,
......@@ -106,6 +114,8 @@ export default {
this.setVulnerabilityFeedbackPath(this.vulnerabilityFeedbackPath);
this.setVulnerabilityFeedbackHelpPath(this.vulnerabilityFeedbackHelpPath);
this.setPipelineId(this.pipelineId);
this.setCanCreateIssuePermission(this.canCreateIssue);
this.setCanCreateFeedbackPermission(this.canCreateFeedback);
if (this.sastHeadPath) {
this.setSastHeadPath(this.sastHeadPath);
......@@ -168,6 +178,8 @@ export default {
'setVulnerabilityFeedbackPath',
'setVulnerabilityFeedbackHelpPath',
'setPipelineId',
'setCanCreateIssuePermission',
'setCanCreateFeedbackPermission',
]),
summaryTextBuilder(type, issuesCount = 0) {
......
......@@ -16,6 +16,11 @@ export const setVulnerabilityFeedbackHelpPath = ({ commit }, path) =>
export const setPipelineId = ({ commit }, id) => commit(types.SET_PIPELINE_ID, id);
export const setCanCreateIssuePermission = ({ commit }, permission) =>
commit(types.SET_CAN_CREATE_ISSUE_PERMISSION, permission);
export const setCanCreateFeedbackPermission = ({ commit }, permission) =>
commit(types.SET_CAN_CREATE_FEEDBACK_PERMISSION, permission);
/**
* SAST
*/
......@@ -211,13 +216,15 @@ export const dismissIssue = ({ state, dispatch }) => {
dispatch('requestDismissIssue');
return axios
.post(state.vulnerabilityFeedbackPath, { vulnerability_feedback: {
.post(state.vulnerabilityFeedbackPath, {
vulnerability_feedback: {
feedback_type: 'dismissal',
category: state.modal.vulnerability.category,
project_fingerprint: state.modal.vulnerability.project_fingerprint,
pipeline_id: state.pipelineId,
vulnerability_data: state.modal.vulnerability,
} })
},
})
.then(({ data }) => {
dispatch('receiveDismissIssue');
......@@ -302,13 +309,15 @@ export const createNewIssue = ({ state, dispatch }) => {
dispatch('requestCreateIssue');
return axios
.post(state.vulnerabilityFeedbackPath, { vulnerability_feedback: {
.post(state.vulnerabilityFeedbackPath, {
vulnerability_feedback: {
feedback_type: 'issue',
category: state.modal.vulnerability.category,
project_fingerprint: state.modal.vulnerability.project_fingerprint,
pipeline_id: state.pipelineId,
vulnerability_data: state.modal.vulnerability,
} })
},
})
.then(response => {
dispatch('receiveCreateIssue');
// redirect the user to the created issue
......
......@@ -3,6 +3,8 @@ export const SET_BASE_BLOB_PATH = 'SET_BASE_BLOB_PATH';
export const SET_VULNERABILITY_FEEDBACK_PATH = 'SET_VULNERABILITY_FEEDBACK_PATH';
export const SET_VULNERABILITY_FEEDBACK_HELP_PATH = 'SET_VULNERABILITY_FEEDBACK_HELP_PATH';
export const SET_PIPELINE_ID = 'SET_PIPELINE_ID';
export const SET_CAN_CREATE_ISSUE_PERMISSION = 'SET_CAN_CREATE_ISSUE_PERMISSION';
export const SET_CAN_CREATE_FEEDBACK_PERMISSION = 'SET_CAN_CREATE_FEEDBACK_PERMISSION';
// SAST
export const SET_SAST_HEAD_PATH = 'SET_SAST_HEAD_PATH';
......
......@@ -32,6 +32,14 @@ export default {
state.pipelineId = id;
},
[types.SET_CAN_CREATE_ISSUE_PERMISSION](state, permission) {
state.canCreateIssuePermission = permission;
},
[types.SET_CAN_CREATE_FEEDBACK_PERMISSION](state, permission) {
state.canCreateFeedbackPermission = permission;
},
// SAST
[types.SET_SAST_HEAD_PATH](state, path) {
state.sast.paths.head = path;
......
......@@ -14,6 +14,8 @@ export default () => ({
vulnerabilityFeedbackPath: null,
vulnerabilityFeedbackHelpPath: null,
pipelineId: null,
canCreateIssuePermission: false,
canCreateFeedbackPermission: false,
sast: {
paths: {
......
......@@ -153,6 +153,10 @@ module EE
project_vulnerability_feedback_index_path(merge_request.project)
end
expose :can_create_feedback do |merge_request|
can?(current_user, :admin_vulnerability_feedback, merge_request)
end
expose :rebase_commit_sha
expose :rebase_in_progress?, as: :rebase_in_progress
......
---
title: Adds permission checks to dismiss issue in security reports
merge_request:
author:
type: fixed
......@@ -129,7 +129,8 @@
"head_path": { "type": "string" },
"base_path": { "type": "string" }
},
"vulnerability_feedback_path": { "type": "string" }
"vulnerability_feedback_path": { "type": "string" },
"can_create_feedback": { "type": "boolean" }
},
"additionalProperties": false
}
import Vue from 'vue';
import component from 'ee/vue_shared/security_reports/components/modal.vue';
import state from 'ee/vue_shared/security_reports/store/state';
import createStore from 'ee/vue_shared/security_reports/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
......@@ -8,14 +9,21 @@ describe('Security Reports modal', () => {
let vm;
const store = createStore();
afterEach(() => {
vm.$destroy();
vm.$store.replaceState(state());
});
beforeEach(() => {
store.dispatch('setVulnerabilityFeedbackPath', 'path');
store.dispatch('setVulnerabilityFeedbackHelpPath', 'feedbacksHelpPath');
store.dispatch('setPipelineId', 123);
});
afterEach(() => {
vm.$destroy();
describe('with permissions', () => {
beforeEach(() => {
store.dispatch('setCanCreateIssuePermission', true);
store.dispatch('setCanCreateFeedbackPermission', true);
});
describe('with dismissed issue', () => {
......@@ -202,6 +210,159 @@ describe('Security Reports modal', () => {
expect(vm.$el.querySelector('.js-link-vulnerabilityFeedbackHelpPath').getAttribute('href')).toEqual('feedbacksHelpPath');
});
});
});
describe('without permissions', () => {
beforeEach(() => {
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
dismissalFeedback: {
id: 1,
category: 'sast',
feedback_type: 'dismissal',
issue_id: null,
author: {
name: 'John Smith',
username: 'jsmith',
web_url: 'https;//gitlab.com/user1',
},
pipeline: {
id: 123,
path: '/jsmith/awesome-project/pipelines/123',
},
},
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
});
it('does not render action buttons', () => {
expect(vm.$el.querySelector('.js-dismiss-btn')).toBe(null);
expect(vm.$el.querySelector('.js-create-issue-btn')).toBe(null);
});
it('does not display the footer', () => {
expect(vm.$el.classList.contains('modal-hide-footer')).toEqual(true);
});
});
describe('with permission to create issue', () => {
beforeEach(() => {
store.dispatch('setCanCreateIssuePermission', true);
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
dismissalFeedback: {
id: 1,
category: 'sast',
feedback_type: 'dismissal',
issue_id: null,
author: {
name: 'John Smith',
username: 'jsmith',
web_url: 'https;//gitlab.com/user1',
},
pipeline: {
id: 123,
path: '/jsmith/awesome-project/pipelines/123',
},
},
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
});
it('does not render dismiss button', () => {
expect(vm.$el.querySelector('.js-dismiss-btn')).toBe(null);
});
it('renders create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue-btn')).not.toBe(null);
});
it('renders the footer', () => {
expect(vm.$el.classList.contains('modal-hide-footer')).toEqual(false);
});
});
describe('with permission to dismiss issue', () => {
beforeEach(() => {
store.dispatch('setCanCreateFeedbackPermission', true);
store.dispatch('setModalData', {
issue: {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
dismissalFeedback: {
id: 1,
category: 'sast',
feedback_type: 'dismissal',
issue_id: null,
author: {
name: 'John Smith',
username: 'jsmith',
web_url: 'https;//gitlab.com/user1',
},
pipeline: {
id: 123,
path: '/jsmith/awesome-project/pipelines/123',
},
},
},
status: 'failed',
});
vm = mountComponentWithStore(Component, {
store,
});
});
it('does not render create issue button', () => {
expect(vm.$el.querySelector('.js-create-issue-btn')).toBe(null);
});
it('renders create issue button and footer', () => {
expect(vm.$el.querySelector('.js-dismiss-btn')).not.toBe(null);
});
it('renders the footer', () => {
expect(vm.$el.classList.contains('modal-hide-footer')).toEqual(false);
});
});
describe('with a resolved issue', () => {
beforeEach(() => {
......
......@@ -67,6 +67,8 @@ describe('Grouped security reports app', () => {
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateFeedback: true,
});
});
......@@ -123,6 +125,8 @@ describe('Grouped security reports app', () => {
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateFeedback: true,
});
});
......@@ -174,6 +178,8 @@ describe('Grouped security reports app', () => {
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateFeedback: true,
});
});
......@@ -252,6 +258,8 @@ describe('Grouped security reports app', () => {
vulnerabilityFeedbackPath: 'vulnerability_feedback_path.json',
vulnerabilityFeedbackHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateFeedback: true,
});
});
......
......@@ -54,6 +54,8 @@ describe('Slipt security reports app', () => {
dastHelpPath: 'path',
sastContainerHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateFeedback: true,
},
});
});
......@@ -96,6 +98,8 @@ describe('Slipt security reports app', () => {
dastHelpPath: 'path',
sastContainerHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateFeedback: true,
},
});
});
......@@ -144,6 +148,8 @@ describe('Slipt security reports app', () => {
dastHelpPath: 'path',
sastContainerHelpPath: 'path',
pipelineId: 123,
canCreateIssue: true,
canCreateFeedback: true,
},
});
});
......
......@@ -6,6 +6,8 @@ import actions, {
setVulnerabilityFeedbackPath,
setVulnerabilityFeedbackHelpPath,
setPipelineId,
setCanCreateIssuePermission,
setCanCreateFeedbackPermission,
setSastHeadPath,
setSastBasePath,
requestSastReports,
......@@ -164,6 +166,42 @@ describe('security reports actions', () => {
});
});
describe('setCanCreateIssuePermission', () => {
it('should commit set can create issue permission', done => {
testAction(
setCanCreateIssuePermission,
true,
mockedState,
[
{
type: types.SET_CAN_CREATE_ISSUE_PERMISSION,
payload: true,
},
],
[],
done,
);
});
});
describe('setCanCreateFeedbackPermission', () => {
it('should commit set can create feedback permission', done => {
testAction(
setCanCreateFeedbackPermission,
true,
mockedState,
[
{
type: types.SET_CAN_CREATE_FEEDBACK_PERMISSION,
payload: true,
},
],
[],
done,
);
});
});
describe('setSastHeadPath', () => {
it('should commit set head blob path', done => {
testAction(
......
......@@ -66,6 +66,20 @@ describe('security reports mutations', () => {
});
});
describe('SET_CAN_CREATE_ISSUE_PERMISSION', () => {
it('should set permission for create issue', () => {
mutations[types.SET_CAN_CREATE_ISSUE_PERMISSION](stateCopy, true);
expect(stateCopy.canCreateIssuePermission).toEqual(true);
});
});
describe('SET_CAN_CREATE_FEEDBACK_PERMISSION', () => {
it('should set permission for create feedback', () => {
mutations[types.SET_CAN_CREATE_FEEDBACK_PERMISSION](stateCopy, true);
expect(stateCopy.canCreateFeedbackPermission).toEqual(true);
});
});
describe('SET_SAST_HEAD_PATH', () => {
it('should set sast head path', () => {
mutations[types.SET_SAST_HEAD_PATH](stateCopy, 'sast_head_path');
......
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