Commit 788e009d authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch '230581-license-compliance-disable-inline-editing' into 'master'

Disable inline editing of license compliance approval

See merge request gitlab-org/gitlab!43470
parents e7650bc4 7dfae341
......@@ -37,10 +37,7 @@ compliance report will be shown properly.
![License Compliance Widget](img/license_compliance_v13_0.png)
If you are a project or group Maintainer, you can click on a license to be given
the choice to allow it or deny it.
![License approval decision](img/license_compliance_decision_v13_0.png)
You can click on a license to see more information.
When GitLab detects a **Denied** license, you can view it in the [license list](#license-list).
......
<script>
import { mapActions } from 'vuex';
import { GlLink } from '@gitlab/ui';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import LicensePackages from './license_packages.vue';
export default {
name: 'LicenseIssueBody',
components: { LicensePackages },
components: { LicensePackages, GlLink },
props: {
issue: {
type: Object,
......@@ -19,15 +20,7 @@ export default {
<template>
<div class="report-block-info license-item">
<button
class="btn-blank btn-link gl-mr-2"
type="button"
data-toggle="modal"
data-target="#modal-set-license-approval"
@click="setLicenseInModal(issue)"
>
{{ issue.name }}
</button>
<gl-link :href="issue.url" target="_blank">{{ issue.name }}</gl-link>
<license-packages :packages="issue.packages" class="text-secondary" />
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import { GlLink } from '@gitlab/ui';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import { s__ } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import LicensePackages from './license_packages.vue';
import { LICENSE_APPROVAL_STATUS } from '../constants';
export default {
name: 'LicenseSetApprovalStatusModal',
components: { GlLink, LicensePackages, GlModal: DeprecatedModal2 },
computed: {
...mapState(LICENSE_MANAGEMENT, ['currentLicenseInModal', 'canManageLicenses']),
headerTitleText() {
if (!this.canManageLicenses) {
return s__('LicenseCompliance|License details');
}
return s__('LicenseCompliance|License review');
},
canApprove() {
return (
this.canManageLicenses &&
this.currentLicenseInModal &&
this.currentLicenseInModal.approvalStatus !== LICENSE_APPROVAL_STATUS.ALLOWED
);
},
canBlacklist() {
return (
this.canManageLicenses &&
this.currentLicenseInModal &&
this.currentLicenseInModal.approvalStatus !== LICENSE_APPROVAL_STATUS.DENIED
);
},
},
methods: {
...mapActions(LICENSE_MANAGEMENT, ['resetLicenseInModal', 'allowLicense', 'denyLicense']),
},
};
</script>
<template>
<gl-modal
id="modal-set-license-approval"
:header-title-text="headerTitleText"
modal-size="lg"
data-qa-selector="license_management_modal"
@cancel="resetLicenseInModal"
>
<slot v-if="currentLicenseInModal">
<div class="row gl-mt-3 gl-mb-3 js-license-name">
<label class="col-sm-3 text-right font-weight-bold">
{{ s__('LicenseCompliance|License') }}:
</label>
<div class="col-sm-9 text-secondary">{{ currentLicenseInModal.name }}</div>
</div>
<div v-if="currentLicenseInModal.url" class="row gl-mt-3 gl-mb-3 js-license-url">
<label class="col-sm-3 text-right font-weight-bold">
{{ s__('LicenseCompliance|URL') }}:
</label>
<div class="col-sm-9 text-secondary">
<gl-link :href="currentLicenseInModal.url" target="_blank" rel="nofollow">{{
currentLicenseInModal.url
}}</gl-link>
</div>
</div>
<div class="row gl-mt-3 gl-mb-3 js-license-packages">
<label class="col-sm-3 text-right font-weight-bold">
{{ s__('LicenseCompliance|Packages') }}:
</label>
<license-packages
:packages="currentLicenseInModal.packages"
class="col-sm-9 text-secondary"
/>
</div>
</slot>
<template slot="footer">
<button
type="button"
class="btn js-modal-cancel-action"
data-dismiss="modal"
@click="resetLicenseInModal"
>
{{ s__('Modal|Cancel') }}
</button>
<button
v-if="canBlacklist"
class="btn btn-remove btn-inverted js-modal-secondary-action"
data-dismiss="modal"
data-qa-selector="deny_license_button"
@click="denyLicense(currentLicenseInModal)"
>
{{ s__('LicenseCompliance|Deny') }}
</button>
<button
v-if="canApprove"
type="button"
class="btn btn-success js-modal-primary-action"
data-dismiss="modal"
data-qa-selector="approve_license_button"
@click="allowLicense(currentLicenseInModal)"
>
{{ s__('LicenseCompliance|Allow') }}
</button>
</template>
</gl-modal>
</template>
......@@ -2,7 +2,6 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLink, GlIcon } from '@gitlab/ui';
import reportsMixin from 'ee/vue_shared/security_reports/mixins/reports_mixin';
import SetLicenseApprovalModal from 'ee/vue_shared/license_compliance/components/set_approval_status_modal.vue';
import { componentNames } from 'ee/reports/components/issue_body';
import { LICENSE_MANAGEMENT } from 'ee/vue_shared/license_compliance/store/constants';
import ReportItem from '~/reports/components/report_item.vue';
......@@ -20,7 +19,6 @@ export default {
GlLink,
ReportItem,
ReportSection,
SetLicenseApprovalModal,
SmartVirtualList,
GlIcon,
},
......@@ -121,7 +119,6 @@ export default {
</script>
<template>
<div>
<set-license-approval-modal />
<report-section
:status="licenseReportStatus"
:loading-text="licenseSummaryText"
......@@ -185,6 +182,7 @@ export default {
:class="{ 'gl-mr-3': fullReportPath }"
:href="licenseManagementSettingsPath"
class="btn btn-default btn-sm js-manage-licenses"
data-qa-selector="manage_licenses_button"
>
{{ s__('ciReport|Manage licenses') }}
</a>
......
---
title: Removes ability to change license status through MR and Pipeline pages
merge_request: 43470
author:
type: changed
......@@ -51,6 +51,7 @@ exports[`License Report MR Widget report section should render correctly 1`] = `
>
<a
class="btn btn-default btn-sm js-manage-licenses gl-mr-3"
data-qa-selector="manage_licenses_button"
href="http://test.host/lm_settings"
>
......
......@@ -21,25 +21,13 @@ describe('LicenseIssueBody', () => {
vm.$destroy();
});
describe('interaction', () => {
it('clicking the button triggers openModal with the current license', () => {
const linkEl = vm.$el.querySelector('.license-item > .btn-link');
expect(store.state.licenseManagement.currentLicenseInModal).toBe(null);
linkEl.click();
expect(store.state.licenseManagement.currentLicenseInModal).toBe(issue);
});
});
describe('template', () => {
it('renders component container element with class `license-item`', () => {
expect(vm.$el.classList.contains('license-item')).toBe(true);
});
it('renders button to open modal', () => {
const linkEl = vm.$el.querySelector('.license-item > .btn-link');
it('renders link to view license', () => {
const linkEl = vm.$el.querySelector('.license-item > a');
expect(linkEl).not.toBeNull();
expect(linkEl.innerText.trim()).toBe(issue.name);
......
import Vue from 'vue';
import Vuex from 'vuex';
import SetApprovalModal from 'ee/vue_shared/license_compliance/components/set_approval_status_modal.vue';
import { LICENSE_APPROVAL_STATUS } from 'ee/vue_shared/license_compliance/constants';
import { trimText } from 'helpers/text_helper';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { licenseReport } from '../mock_data';
Vue.use(Vuex);
describe('SetApprovalModal', () => {
const Component = Vue.extend(SetApprovalModal);
let vm;
let store;
let actions;
beforeEach(() => {
actions = {
resetLicenseInModal: jest.fn(),
allowLicense: jest.fn(),
denyLicense: jest.fn(),
};
store = new Vuex.Store({
modules: {
licenseManagement: {
namespaced: true,
state: {
currentLicenseInModal: licenseReport[0],
canManageLicenses: true,
},
actions,
},
},
});
vm = mountComponentWithStore(Component, { store });
});
afterEach(() => {
vm.$destroy();
});
describe('for approved license', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.ALLOWED,
},
canManageLicenses: true,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License review', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is false', () => {
expect(vm.canApprove).toBe(false);
});
it('canBlacklist is true', () => {
expect(vm.canBlacklist).toBe(true);
});
});
describe('template correctly', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders no Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).toBeNull();
});
it('renders Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Deny');
});
});
});
describe('for unapproved license', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: true,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License review`', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is true', () => {
expect(vm.canApprove).toBe(true);
});
it('canBlacklist is true', () => {
expect(vm.canBlacklist).toBe(true);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Allow');
});
it('renders Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Deny');
});
});
});
describe('for blacklisted license', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: LICENSE_APPROVAL_STATUS.DENIED,
},
canManageLicenses: true,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License review`', () => {
expect(vm.headerTitleText).toBe('License review');
});
it('canApprove is true', () => {
expect(vm.canApprove).toBe(true);
});
it('canBlacklist is false', () => {
expect(vm.canBlacklist).toBe(false);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License review');
});
it('renders Allow button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).not.toBeNull();
expect(footerButton.innerText.trim()).toBe('Allow');
});
it('renders no Deny button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).toBeNull();
});
});
});
describe('for user without the rights to manage licenses', () => {
beforeEach(done => {
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
approvalStatus: undefined,
},
canManageLicenses: false,
},
});
Vue.nextTick(done);
});
describe('computed', () => {
it('headerTitleText returns `License details`', () => {
expect(vm.headerTitleText).toBe('License details');
});
it('canApprove is false', () => {
expect(vm.canApprove).toBe(false);
});
it('canBlacklist is false', () => {
expect(vm.canBlacklist).toBe(false);
});
});
describe('template', () => {
it('renders modal title', () => {
const headerEl = vm.$el.querySelector('.modal-title');
expect(headerEl).not.toBeNull();
expect(headerEl.innerText.trim()).toBe('License details');
});
it('renders no Approve button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-primary-action');
expect(footerButton).toBeNull();
});
it('renders no Blacklist button in modal footer', () => {
const footerButton = vm.$el.querySelector('.js-modal-secondary-action');
expect(footerButton).toBeNull();
});
});
});
describe('Modal Body', () => {
it('renders the license name', () => {
const licenseName = vm.$el.querySelector('.js-license-name');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`License: ${licenseReport[0].name}`);
});
it('renders the license url with link', () => {
const licenseName = vm.$el.querySelector('.js-license-url');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`URL: ${licenseReport[0].url}`);
const licenseLink = licenseName.querySelector('a');
expect(licenseLink.getAttribute('href')).toBe(licenseReport[0].url);
expect(trimText(licenseLink.innerText)).toBe(licenseReport[0].url);
});
it('renders the license url', () => {
const licenseName = vm.$el.querySelector('.js-license-packages');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe('Packages: Used by pg, puma, foo, and 2 more');
});
});
describe('interaction', () => {
describe('triggering resetLicenseInModal on canceling', () => {
it('by clicking the cancel button', () => {
const linkEl = vm.$el.querySelector('.js-modal-cancel-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
it('triggering resetLicenseInModal by clicking the X button', () => {
const linkEl = vm.$el.querySelector('.js-modal-close-action');
linkEl.click();
expect(actions.resetLicenseInModal).toHaveBeenCalled();
});
});
describe('triggering allowLicense on approving', () => {
it('by clicking the confirmation button', () => {
const linkEl = vm.$el.querySelector('.js-modal-primary-action');
linkEl.click();
expect(actions.allowLicense).toHaveBeenCalledWith(
expect.any(Object),
store.state.licenseManagement.currentLicenseInModal,
);
});
});
describe('triggering denyLicense on blacklisting', () => {
it('by clicking the confirmation button', () => {
const linkEl = vm.$el.querySelector('.js-modal-secondary-action');
linkEl.click();
expect(actions.denyLicense).toHaveBeenCalledWith(
expect.any(Object),
store.state.licenseManagement.currentLicenseInModal,
);
});
});
});
it('does not render a XSS link', done => {
// eslint-disable-next-line no-script-url
const badURL = 'javascript:alert("")';
store.replaceState({
licenseManagement: {
currentLicenseInModal: {
...licenseReport[0],
url: badURL,
approvalStatus: LICENSE_APPROVAL_STATUS.ALLOWED,
},
},
});
Vue.nextTick()
.then(() => {
const licenseName = vm.$el.querySelector('.js-license-url');
expect(licenseName).not.toBeNull();
expect(trimText(licenseName.innerText)).toBe(`URL: ${badURL}`);
expect(licenseName.querySelector('a').getAttribute('href')).toBe('about:blank');
expect(licenseName.querySelector('a').innerText).toBe(badURL);
})
.then(done)
.catch(done.fail);
});
});
......@@ -333,12 +333,6 @@ describe('License Report MR Widget', () => {
});
});
it('should render set approval modal', () => {
mountComponent();
expect(wrapper.find('#modal-set-license-approval')).not.toBeNull();
});
it('should init store after mount', () => {
const actions = {
setAPISettings: jest.fn(),
......
......@@ -15964,9 +15964,6 @@ msgstr ""
msgid "LicenseCompliance|Learn more about %{linkStart}License Approvals%{linkEnd}"
msgstr ""
msgid "LicenseCompliance|License"
msgstr ""
msgid "LicenseCompliance|License Approvals"
msgstr ""
......@@ -16006,18 +16003,9 @@ msgstr ""
msgid "LicenseCompliance|License Compliance detected no new licenses"
msgstr ""
msgid "LicenseCompliance|License details"
msgstr ""
msgid "LicenseCompliance|License name"
msgstr ""
msgid "LicenseCompliance|License review"
msgstr ""
msgid "LicenseCompliance|Packages"
msgstr ""
msgid "LicenseCompliance|Remove license"
msgstr ""
......@@ -16033,9 +16021,6 @@ msgstr ""
msgid "LicenseCompliance|This license already exists in this project."
msgstr ""
msgid "LicenseCompliance|URL"
msgstr ""
msgid "LicenseCompliance|You are about to remove the license, %{name}, from this project."
msgstr ""
......
......@@ -9,23 +9,25 @@
{
"id": "MIT",
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT"
"url": "http://opensource.org/licenses/mit-license"
}
],
"dependencies": [
{
"name": "actioncable",
"version": "6.0.3.3",
"name": "test_dependency",
"version": "0.1.0",
"package_manager": "bundler",
"path": "Gemfile.lock",
"licenses": ["MIT"]
"licenses": ["Apache-2.0"]
},
{
"name": "test_package",
"version": "0.1.0",
"name": "actioncable",
"version": "1.2",
"url": "http://rubyonrails.org",
"package_manager": "bundler",
"path": "Gemfile.lock",
"licenses": ["Apache-2.0"]
"description": "WebSocket framework for Rails.",
"path": ".",
"licenses": ["MIT"]
}
]
}
......@@ -19,14 +19,9 @@ module QA
element :icon_status, ':data-qa-selector="`status_${status}_icon`" ' # rubocop:disable QA/ElementWithPattern
end
view 'ee/app/assets/javascripts/vue_shared/license_compliance/components/set_approval_status_modal.vue' do
element :license_management_modal
element :approve_license_button
element :deny_license_button
end
view 'ee/app/assets/javascripts/vue_shared/license_compliance/mr_widget_license_report.vue' do
element :license_report_widget
element :manage_licenses_button
end
end
end
......@@ -50,20 +45,14 @@ module QA
wait_for_animated_element(:license_management_modal)
end
def approve_license(name)
wait_until(reload: true) do
click_license(name)
has_element?(:approve_license_button, wait: 1)
def click_manage_licenses_button
previous_page = page.current_url
within_element(:license_report_widget) do
click_element :manage_licenses_button
end
click_element(:approve_license_button)
end
def deny_license(name)
wait_until(reload: true) do
click_license(name)
has_element?(:deny_license_button, wait: 1)
wait_until(max_duration: 15, reload: false) do
page.current_url != previous_page
end
click_element(:deny_license_button)
end
end
end
......
......@@ -114,16 +114,6 @@ module QA
end
end
def approve_license_with_mr(name)
expand_license_report unless license_report_expanded?
approve_license(name)
end
def deny_license_with_mr(name)
expand_license_report unless license_report_expanded?
deny_license(name)
end
def expand_vulnerability_report
within_element :vulnerability_report_grouped do
click_element :expand_report_button unless has_content? 'Collapse'
......
......@@ -28,7 +28,7 @@ module QA
def approve_license(license)
click_element :license_add_button
expand_select_list
search_and_select license
search_and_select_exact license
click_element :approved_license_radio
click_element :add_license_submit_button
......@@ -36,6 +36,7 @@ module QA
end
def has_approved_license?(name)
has_element?(:admin_license_compliance_row, text: name)
within_element(:admin_license_compliance_row, text: name) do
has_element?(:status_success_icon)
end
......@@ -44,7 +45,7 @@ module QA
def deny_license(license)
click_element :license_add_button
expand_select_list
search_and_select license
search_and_select_exact license
click_element :blacklisted_license_radio
click_element :add_license_submit_button
......@@ -52,6 +53,7 @@ module QA
end
def has_denied_license?(name)
has_element?(:admin_license_compliance_row, text: name)
within_element(:admin_license_compliance_row, text: name) do
has_element?(:status_failed_icon)
end
......
......@@ -38,6 +38,16 @@ module QA
select_item(item_text)
end
def search_and_select_exact(item_text)
QA::Runtime::Logger.info "Searching and selecting: #{item_text}"
search_item(item_text)
raise QA::Page::Base::ElementNotFound, %Q(Couldn't find option named "#{item_text}") unless has_item?(item_text)
find('.select2-result-label', text: item_text, exact_text: true).click
end
def expand_select_list
find('span.select2-arrow').click
end
......
......@@ -26,6 +26,10 @@ module QA
element :child_pipeline
end
view 'app/assets/javascripts/reports/components/report_section.vue' do
element :expand_report_button
end
view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern
end
......@@ -78,6 +82,12 @@ module QA
end
end
def expand_license_report
within_element(:license_report_widget) do
click_element(:expand_report_button)
end
end
def click_on_first_job
first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click
end
......
......@@ -71,7 +71,6 @@ module QA
end
describe 'License Compliance pipeline reports' do
let(:number_of_licenses_in_fixture) { 2 }
let(:executor) {"qa-runner-#{Time.now.to_i}"}
after do
......@@ -101,29 +100,29 @@ module QA
.new(__dir__)
.join('../../../../../ee/fixtures/secure_premade_reports')
project_push.commit_message = 'Create Secure compatible application to serve premade reports'
end.project.visit!
end
@project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success)
Page::Project::Menu.perform(&:click_on_license_compliance)
end
it 'can approve and deny licenses in the pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/965' do
Flow::Pipeline.visit_latest_pipeline
EE::Page::Project::Secure::LicenseCompliance.perform do |license_compliance|
license_compliance.open_tab
license_compliance.approve_license approved_license_name
license_compliance.deny_license denied_license_name
end
@project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_on_licenses
expect(pipeline).to have_license_count_of number_of_licenses_in_fixture
pipeline.approve_license(approved_license_name)
pipeline.deny_license(denied_license_name)
end
Page::Project::Menu.perform(&:click_on_license_compliance)
EE::Page::Project::Secure::LicenseCompliance.perform do |license_compliance|
license_compliance.open_tab
expect(license_compliance).to have_approved_license approved_license_name
expect(license_compliance).to have_denied_license denied_license_name
expect(pipeline).to have_approved_license approved_license_name
expect(pipeline).to have_denied_license denied_license_name
end
end
end
......
......@@ -103,11 +103,21 @@ module QA
@merge_request.visit!
Page::MergeRequest::Show.perform do |show|
show.approve_license_with_mr(approved_license_name)
show.deny_license_with_mr(denied_license_name)
show.wait_for_license_compliance_report
show.click_manage_licenses_button
end
EE::Page::Project::Secure::LicenseCompliance.perform do |license_compliance|
license_compliance.open_tab
license_compliance.approve_license approved_license_name
license_compliance.deny_license denied_license_name
end
@merge_request.visit!
Page::MergeRequest::Show.perform do |show|
show.wait_for_license_compliance_report
show.expand_license_report
expect(show).to have_approved_license approved_license_name
expect(show).to have_denied_license denied_license_name
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