diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_labeled_toggle.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_labeled_toggle.vue new file mode 100644 index 0000000000000000000000000000000000000000..9ffaa3ed7654156919573612d21a15ddc043ab76 --- /dev/null +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_labeled_toggle.vue @@ -0,0 +1,72 @@ + + + diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index f24372b80df006ce191d7d204e2936cad77d080e..4ac79bfb817901fca1452ff9fc8204951b981311 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -4,6 +4,7 @@ import { GlIcon, GlSprintf, GlLink, GlFormCheckbox } from '@gitlab/ui'; import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin'; import { s__ } from '~/locale'; import projectFeatureSetting from './project_feature_setting.vue'; +import projectFeatureLabeledToggle from './project_feature_labeled_toggle.vue'; import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue'; import projectSettingRow from './project_setting_row.vue'; import { @@ -22,6 +23,7 @@ export default { components: { projectFeatureSetting, projectFeatureToggle, + projectFeatureLabeledToggle, projectSettingRow, GlIcon, GlSprintf, @@ -31,6 +33,11 @@ export default { mixins: [settingsMixin, glFeatureFlagsMixin()], props: { + requestCveAvailable: { + type: Boolean, + required: false, + default: false, + }, currentSettings: { type: Object, required: true, @@ -94,6 +101,11 @@ export default { required: false, default: '', }, + cveIdRequestHelpPath: { + type: String, + required: false, + default: 'https://about.gitlab.com/security/cve', + }, registryHelpPath: { type: String, required: false, @@ -144,6 +156,7 @@ export default { requestAccessEnabled: true, highlightChangesClass: false, emailsDisabled: false, + cveIdRequestEnabled: true, featureAccessLevelEveryone, featureAccessLevelMembers, }; @@ -373,6 +386,15 @@ export default { :options="featureAccessLevelOptions" name="project[project_feature_attributes][issues_access_level]" /> + diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 15842dec3dd77157466a158d531b52f2f5fe7b33..65f89e991a5ad2d38a243d08d373e66447742b96 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -441,6 +441,7 @@ def issuable_sidebar_options(issuable) rootPath: root_path, fullPath: issuable[:project_full_path], iid: issuable[:iid], + title: issuable[:title], severity: issuable[:severity], timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours } diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb index 53c123c06fd21d694efb7c075c3bed2224a842f2..44fff80ae0fa87b4c514285a9d59e96d74ebb911 100644 --- a/app/serializers/issuable_sidebar_basic_entity.rb +++ b/app/serializers/issuable_sidebar_basic_entity.rb @@ -8,6 +8,7 @@ class IssuableSidebarBasicEntity < Grape::Entity expose :type do |issuable| issuable.to_ability_name end + expose :title expose :author_id expose :project_id do |issuable| issuable.project.id diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index fe507697321731161774a54ed8bb8fade88d80d6..37c720e866ed06313f645851ac6cbb5dcff24407 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -117,6 +117,8 @@ %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe #js-confidential-entry-point + = render_if_exists 'shared/issuable/sidebar_cve_id_request', issuable_sidebar: issuable_sidebar + %script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe #js-lock-entry-point diff --git a/changelogs/unreleased/add_request_cve_issue.yml b/changelogs/unreleased/add_request_cve_issue.yml new file mode 100644 index 0000000000000000000000000000000000000000..45b2c1bf384b329c063ed02a3ef2b58f3ee376db --- /dev/null +++ b/changelogs/unreleased/add_request_cve_issue.yml @@ -0,0 +1,5 @@ +--- +title: Adds Request CVE ID button to issue sidebar +merge_request: 41203 +author: +type: added diff --git a/db/migrate/20200816133024_add_cve_id_request_project_setting.rb b/db/migrate/20200816133024_add_cve_id_request_project_setting.rb new file mode 100644 index 0000000000000000000000000000000000000000..ce0edeb43d6213be2efe6e389a2bbcc6f40a7d54 --- /dev/null +++ b/db/migrate/20200816133024_add_cve_id_request_project_setting.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddCveIdRequestProjectSetting < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def up + add_column :project_security_settings, :cve_id_request_enabled, :boolean, default: true, null: false + end + + def down + remove_column :project_security_settings, :cve_id_request_enabled + end +end diff --git a/db/schema_migrations/20200816133024 b/db/schema_migrations/20200816133024 new file mode 100644 index 0000000000000000000000000000000000000000..1637f1668571f74877974d0c98c8a63dca3e4b39 --- /dev/null +++ b/db/schema_migrations/20200816133024 @@ -0,0 +1 @@ +37196d54d03791f7509e411d5c545f22aa70f7c07d1f13d76f70008a06e72b57 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fff314a9384599220511e051b63198356dbc2d85..8491523f978828f8baa1fb89a4499ac3a4e5b02a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -15610,7 +15610,8 @@ CREATE TABLE project_security_settings ( auto_fix_container_scanning boolean DEFAULT true NOT NULL, auto_fix_dast boolean DEFAULT true NOT NULL, auto_fix_dependency_scanning boolean DEFAULT true NOT NULL, - auto_fix_sast boolean DEFAULT true NOT NULL + auto_fix_sast boolean DEFAULT true NOT NULL, + cve_id_request_enabled boolean DEFAULT true NOT NULL ); CREATE SEQUENCE project_security_settings_project_id_seq diff --git a/ee/app/assets/javascripts/sidebar/components/cve_id_request/cve_id_request_sidebar.vue b/ee/app/assets/javascripts/sidebar/components/cve_id_request/cve_id_request_sidebar.vue new file mode 100644 index 0000000000000000000000000000000000000000..79984dd1e225caf268dfa999e9a255c17b6cb727 --- /dev/null +++ b/ee/app/assets/javascripts/sidebar/components/cve_id_request/cve_id_request_sidebar.vue @@ -0,0 +1,205 @@ + + + diff --git a/ee/app/assets/javascripts/sidebar/mount_sidebar.js b/ee/app/assets/javascripts/sidebar/mount_sidebar.js index 3dc692d3d010a678ebd8dbb4d01d358c38e19e21..4910b72880d5267d56d5b3704d91eeba26528964 100644 --- a/ee/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/ee/app/assets/javascripts/sidebar/mount_sidebar.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { parseBoolean } from '~/lib/utils/common_utils'; +import CveIdRequest from './components/cve_id_request/cve_id_request_sidebar.vue'; import * as CEMountSidebar from '~/sidebar/mount_sidebar'; import SidebarItemEpicsSelect from './components/sidebar_item_epics_select.vue'; import SidebarStatus from './components/status/sidebar_status.vue'; @@ -12,7 +13,7 @@ import { store } from '~/notes/stores'; Vue.use(VueApollo); -const mountWeightComponent = mediator => { +const mountWeightComponent = (mediator) => { const el = document.querySelector('.js-sidebar-weight-entry-point'); if (!el) return false; @@ -22,7 +23,7 @@ const mountWeightComponent = mediator => { components: { SidebarWeight, }, - render: createElement => + render: (createElement) => createElement('sidebar-weight', { props: { mediator, @@ -31,7 +32,7 @@ const mountWeightComponent = mediator => { }); }; -const mountStatusComponent = mediator => { +const mountStatusComponent = (mediator) => { const el = document.querySelector('.js-sidebar-status-entry-point'); if (!el) { @@ -44,7 +45,7 @@ const mountStatusComponent = mediator => { components: { SidebarStatus, }, - render: createElement => + render: (createElement) => createElement('sidebar-status', { props: { mediator, @@ -53,6 +54,31 @@ const mountStatusComponent = mediator => { }); }; +function mountCveIdRequestComponent() { + const el = document.getElementById('js-sidebar-cve-id-request-entry-point'); + + if (!el) return; + const { iid, fullPath, title } = CEMountSidebar.getSidebarOptions(); + + const dataNode = document.getElementById('js-sidebar-cve-id-request-issue-data'); + const { is_confidential } = JSON.parse(dataNode.innerHTML); + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + CveIdRequest, + }, + provide: { + iid: String(iid), + fullPath, + issueTitle: title, + initialConfidential: is_confidential, + }, + render: (createElement) => createElement('cve-id-request'), + }); +} + const mountEpicsSelect = () => { const el = document.querySelector('#js-vue-sidebar-item-epics-select'); @@ -66,7 +92,7 @@ const mountEpicsSelect = () => { components: { SidebarItemEpicsSelect, }, - render: createElement => + render: (createElement) => createElement('sidebar-item-epics-select', { props: { sidebarStore, @@ -97,7 +123,7 @@ function mountIterationSelect() { components: { IterationSelect, }, - render: createElement => + render: (createElement) => createElement('iteration-select', { props: { groupPath, @@ -115,4 +141,5 @@ export default function mountSidebar(mediator) { mountStatusComponent(mediator); mountEpicsSelect(); mountIterationSelect(); + mountCveIdRequestComponent(); } diff --git a/ee/app/assets/stylesheets/pages/issuable.scss b/ee/app/assets/stylesheets/pages/issuable.scss index 0f3df33677a451937b337af3322a5f918226fe34..cde1881b4d5fd9f196813b0793aa1038c4f1e8e0 100644 --- a/ee/app/assets/stylesheets/pages/issuable.scss +++ b/ee/app/assets/stylesheets/pages/issuable.scss @@ -87,6 +87,45 @@ } } +.cve-id-request { + padding-bottom: 0; + border-bottom: 0; + + .help-button, + .close-help-button { + cursor: pointer; + } + + .help-state-toggle-enter-active { + transition: all 0.8s ease; + } + + .help-state-toggle-leave-active { + transition: all 0.5s ease; + } + + .help-state-toggle-enter, + .help-state-toggle-leave-active { + opacity: 0; + } + + .cve-id-request-content { + margin-top: 16px; + } + + .cve-id-request-help-state { + background: $white; + margin: 16px -20px -20px; + padding: 16px 20px; + border-top: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-light; + + a:hover { + color: $btn-white-active; + } + } +} + // This override is needed because `display: flex` // on `.epic` above causes hiding logic in global // stylesheet is of lower specificity. diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb index 3ecdfdd914ba5028aa85e998f6a8bb19d2460290..506040bf35f8e174b5ebec12dae547b1b99d6bcf 100644 --- a/ee/app/controllers/ee/projects_controller.rb +++ b/ee/app/controllers/ee/projects_controller.rb @@ -74,18 +74,22 @@ def active_new_project_tab private def project_params_ee - attrs = %i[ - approvals_before_merge - approver_group_ids - approver_ids - issues_template - merge_requests_template - repository_size_limit - reset_approvals_on_push - ci_cd_only - use_custom_template - require_password_to_approve - group_with_project_templates_id + attrs = [ + :approvals_before_merge, + :approver_group_ids, + :approver_ids, + :issues_template, + :merge_requests_template, + :repository_size_limit, + :reset_approvals_on_push, + :ci_cd_only, + :use_custom_template, + :require_password_to_approve, + :group_with_project_templates_id, + + security_setting_attributes: %i[ + cve_id_request_enabled + ] ] attrs << %i[merge_pipelines_enabled] if allow_merge_pipelines_params? diff --git a/ee/app/helpers/ee/cve_request_helper.rb b/ee/app/helpers/ee/cve_request_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..af7e616512214fc7bcbbc2b99e2a01f4742aa959 --- /dev/null +++ b/ee/app/helpers/ee/cve_request_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module EE + module CveRequestHelper + def request_cve_enabled_for_user?(issue, user) + can?(user, :admin_project, issue.project) && + request_cve_enabled?(issue.project) + end + + def request_cve_enabled?(project) + request_cve_available?(project) && + project.visibility_level == ::Gitlab::VisibilityLevel::PUBLIC && + ProjectSecuritySetting.safe_find_or_create_for(project).cve_id_request_enabled? + end + + def request_cve_available?(project) + ::Gitlab.dev_env_org_or_com? + end + end +end diff --git a/ee/app/helpers/ee/projects_helper.rb b/ee/app/helpers/ee/projects_helper.rb index 3af014b9ef528addee552c73899f44e8b03b9486..f965a3fd6d3e914978bbc6c1b45ba18e8995987f 100644 --- a/ee/app/helpers/ee/projects_helper.rb +++ b/ee/app/helpers/ee/projects_helper.rb @@ -4,6 +4,8 @@ module EE module ProjectsHelper extend ::Gitlab::Utils::Override + include CveRequestHelper + override :sidebar_settings_paths def sidebar_settings_paths super + %w[ @@ -60,14 +62,16 @@ def get_project_nav_tabs(project, current_user) override :project_permissions_settings def project_permissions_settings(project) super.merge( - requirementsAccessLevel: project.requirements_access_level + requirementsAccessLevel: project.requirements_access_level, + cveIdRequestEnabled: !!project.ensure_security_setting.cve_id_request_enabled ) end override :project_permissions_panel_data def project_permissions_panel_data(project) super.merge( - requirementsAvailable: project.feature_available?(:requirements) + requirementsAvailable: project.feature_available?(:requirements), + requestCveAvailable: request_cve_available?(project) ) end diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index 11af98dbc07cc2c777ed294f0f58aab072ccdcc0..9900cad3ec9831b9eed5e6cbfefc40370af53035 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -227,8 +227,13 @@ def requirements_enabled=(value) accepts_nested_attributes_for :status_page_setting, update_only: true, allow_destroy: true accepts_nested_attributes_for :compliance_framework_setting, update_only: true, allow_destroy: true + accepts_nested_attributes_for :security_setting, update_only: true alias_attribute :fallback_approvals_required, :approvals_before_merge + + def ensure_security_setting + self.security_setting ||= ProjectSecuritySetting.safe_find_or_create_for(self) + end end class_methods do diff --git a/ee/app/serializers/ee/issue_sidebar_basic_entity.rb b/ee/app/serializers/ee/issue_sidebar_basic_entity.rb index adfb33b4de61dbce497d4cf3610cccdb4e8984df..3414df2061fc3b492375d7988547480a9b38b843 100644 --- a/ee/app/serializers/ee/issue_sidebar_basic_entity.rb +++ b/ee/app/serializers/ee/issue_sidebar_basic_entity.rb @@ -18,6 +18,10 @@ module IssueSidebarBasicEntity issuable.project&.group&.feature_available?(:epics) end end + + expose :request_cve_enabled_for_user do |issue| + request_cve_enabled_for_user?(issue, current_user) + end end end end diff --git a/ee/app/views/shared/issuable/_sidebar_cve_id_request.haml b/ee/app/views/shared/issuable/_sidebar_cve_id_request.haml new file mode 100644 index 0000000000000000000000000000000000000000..fc015bd0ef538f7fbe8d796bdaac534701f0ed70 --- /dev/null +++ b/ee/app/views/shared/issuable/_sidebar_cve_id_request.haml @@ -0,0 +1,4 @@ +- if issuable_sidebar[:request_cve_enabled_for_user] + %script#js-sidebar-cve-id-request-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential] }.to_json.html_safe + #js-sidebar-cve-id-request-entry-point + diff --git a/ee/spec/factories/project_security_settings.rb b/ee/spec/factories/project_security_settings.rb index 765c60ab469a66b86aaa84aceaab0a692fef243e..4bfab17f808177ad5eedfa21721417ea9a231061 100644 --- a/ee/spec/factories/project_security_settings.rb +++ b/ee/spec/factories/project_security_settings.rb @@ -7,6 +7,7 @@ auto_fix_dast { true } auto_fix_dependency_scanning { true } auto_fix_sast { true } + cve_id_request_enabled { true } trait :disabled_auto_fix do auto_fix_container_scanning { false } diff --git a/ee/spec/features/projects/settings/issues_settings_spec.rb b/ee/spec/features/projects/settings/issues_settings_spec.rb index d4ddff77f325e69e4831a6ea5c5557ceb0dcdd31..329836107980f87fbaae499a974f513bed34d155 100644 --- a/ee/spec/features/projects/settings/issues_settings_spec.rb +++ b/ee/spec/features/projects/settings/issues_settings_spec.rb @@ -72,4 +72,56 @@ expect(page).to have_selector('#project_issues_template') end end + + context 'when viewing CVE request settings' do + using RSpec::Parameterized::TableSyntax + + where(:gitlab_com, :project_visibility, :cve_enabled, :toggle_checked, :toggle_disabled, :has_toggle) do + true | :PUBLIC | true | true | false | true + true | :INTERNAL | true | nil | true | true + true | :PRIVATE | true | nil | true | true + false | :PUBLIC | true | nil | nil | false + end + + with_them do + before do + allow(::Gitlab).to receive(:com?).and_return(gitlab_com) + + vis_val = Gitlab::VisibilityLevel.const_get(project_visibility, false) + project.visibility_level = vis_val + project.save! + + security_setting = ProjectSecuritySetting.safe_find_or_create_for(project) + security_setting.cve_id_request_enabled = cve_enabled + security_setting.save! + + visit edit_project_path(project) + end + + it "CVE ID Request toggle should be correctly visible" do + if has_toggle + expect(page).to have_selector('#cve_id_request_toggle') + else + expect(page).not_to have_selector('#cve_id_request_toggle') + next + end + + toggle_btn = find('#cve_id_request_toggle button.project-feature-toggle') + + if toggle_disabled + expect(toggle_btn).to match_css('.is-disabled', wait: 0) + else + expect(toggle_btn).not_to match_css('.is-disabled', wait: 0) + end + + next if toggle_checked.nil? + + if toggle_checked + expect(toggle_btn).to match_css('.is-checked', wait: 0) + else + expect(toggle_btn).not_to match_css('.is-checked', wait: 0) + end + end + end + end end diff --git a/ee/spec/frontend/sidebar/components/cve_id_request_spec.js b/ee/spec/frontend/sidebar/components/cve_id_request_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..96339b238c80d8b56b1e69f9e41440248c32d4a0 --- /dev/null +++ b/ee/spec/frontend/sidebar/components/cve_id_request_spec.js @@ -0,0 +1,97 @@ +import Vue from 'vue'; +import CveIdRequest from 'ee/sidebar/components/cve_id_request/cve_id_request_sidebar.vue'; +import { shallowMount } from '@vue/test-utils'; + +describe('CveIdRequest', () => { + let wrapper; + + const initCveIdRequest = () => { + setFixtures(` +
+
+
+ `); + + const provide = { + iid: 'test', + fullPath: 'some/path', + issueTitle: 'Issue Title', + initialConfidential: true, + }; + + const CveIdRequestComponent = Vue.extend({ + ...CveIdRequest, + components: { + ...CveIdRequest.components, + transition: { + // disable animations + render(h) { + return h('div', this.$slots.default); + }, + }, + }, + }); + wrapper = shallowMount(CveIdRequestComponent, { + provide, + }); + }; + + beforeEach(() => { + initCveIdRequest(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('Renders the main "Request CVE ID" button', () => { + expect(wrapper.find('.js-cve-id-request-button').element).not.toBeNull(); + }); + + it('Renders the "help-button" by default', () => { + expect(wrapper.find('.help-button').element).not.toBeNull(); + }); + + describe('Help Pane', () => { + const helpButton = () => wrapper.find('.help-button').element; + const closeHelpButton = () => wrapper.find('.close-help-button').element; + const helpPane = () => wrapper.find('.cve-id-request-help-state').element; + + beforeEach(() => { + initCveIdRequest(); + return wrapper.vm.$nextTick(); + }); + + it('should not show the "Help" pane by default', () => { + expect(wrapper.vm.showHelpState).toBe(false); + expect(helpPane()).toBeUndefined(); + }); + + it('should show the "Help" pane when help button is clicked', () => { + helpButton().click(); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.showHelpState).toBe(true); + + // let animations run + jest.advanceTimersByTime(500); + + expect(helpPane()).not.toBeUndefined(); + }); + }); + + it('should not show the "Help" pane when help button is clicked and then closed', (done) => { + helpButton().click(); + + Vue.nextTick() + .then(() => closeHelpButton().click()) + .then(() => Vue.nextTick()) + .then(() => { + expect(wrapper.vm.showHelpState).toBe(false); + expect(helpPane()).toBeUndefined(); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/ee/spec/helpers/ee/cve_request_helper_spec.rb b/ee/spec/helpers/ee/cve_request_helper_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..0ba10c148d904d027b37ba15dbd4286666746e3d --- /dev/null +++ b/ee/spec/helpers/ee/cve_request_helper_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EE::CveRequestHelper do + let(:project) { create(:project) } + + describe '#request_cve_enabled_for_user?' do + where( + maintainer: [true, false], + request_cve_enabled: [true, false] + ) + with_them do + let(:user) { create_user(maintainer ? :maintainer : :developer) } + let(:issue) do + create(:issue, project: project, assignees: [user]) + end + + before do + # allow(helper).to receive(:can?) do |_user, perm, project| + # perm == :admin_project ? maintainer : false + # end + if maintainer + project.add_maintainer(user) + else + project.add_developer(user) + end + + allow(helper).to receive(:request_cve_enabled?).and_return(request_cve_enabled) + allow(helper).to receive(:can?) {|*args, **kwargs| Ability.allowed?(*args, **kwargs) } + end + + it "returns the correct value" do + res = helper.request_cve_enabled_for_user?(issue, user) + expected = maintainer && request_cve_enabled + expect(res).to equal(expected) + end + end + end + + describe '#request_cve_enabled?' do + where( + gitlab_com: [true, false], + setting_enabled: [true, false], + visibility: [:PUBLIC, :INTERNAL, :PRIVATE] + ) + with_them do + before do + allow(::Gitlab).to receive(:com?).and_return(gitlab_com) + vis_val = Gitlab::VisibilityLevel.const_get(visibility, false) + project.visibility_level = vis_val + project.save! + + security_setting = ProjectSecuritySetting.safe_find_or_create_for(project) + security_setting.cve_id_request_enabled = setting_enabled + security_setting.save! + end + + it "returns the correct value" do + expected = gitlab_com && setting_enabled && visibility == :PUBLIC + expect(helper.request_cve_enabled?(project)).to equal(expected) + end + end + end + + def create_user(access_level_trait) + user = create(:user) + create(:project_member, access_level_trait, user: user, project: project) + user + end +end diff --git a/ee/spec/helpers/projects_helper_spec.rb b/ee/spec/helpers/projects_helper_spec.rb index 3913cc9b8faa34985375bb098b0481eec2ccff5d..976bbc1ff8068dd2aa567cce63e172fda81e55c3 100644 --- a/ee/spec/helpers/projects_helper_spec.rb +++ b/ee/spec/helpers/projects_helper_spec.rb @@ -354,6 +354,45 @@ end end + describe '#project_permissions_settings' do + let(:perm_settings) { helper.project_permissions_settings(project) } + + context 'default settings' do + it 'cveIdRequestEnabled defaults to true' do + expect(perm_settings[:cveIdRequestEnabled]).to equal(true) + end + end + + context 'security_settings.cve_id_request_enabled' do + tests = [true, false] + tests.each do |cve_enabled| + context "is #{cve_enabled}" do + before do + security_setting = create(:project_security_setting, project: project, cve_id_request_enabled: cve_enabled) + security_setting.save! + end + + it "sets cveIdRequestEnabled to #{cve_enabled}" do + expect(perm_settings[:cveIdRequestEnabled]).to equal(cve_enabled) + end + end + end + end + end + + describe '#project_permissions_panel_data' do + let(:panel_data) { helper.project_permissions_panel_data(project) } + + before do + allow(helper).to receive(:current_user).and_return(project.owner) + allow(helper).to receive(:can?).and_return(false) + end + + it 'sets requestCveAvailable' do + expect(panel_data).to include(requestCveAvailable: helper.request_cve_available?(project)) + end + end + describe '#can_view_operations_tab?' do let_it_be(:user) { create(:user) } diff --git a/ee/spec/serializers/ee/issue_sidebar_basic_entity_spec.rb b/ee/spec/serializers/ee/issue_sidebar_basic_entity_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..820f9a469bbf3838fc7115c503d1793826ae86f7 --- /dev/null +++ b/ee/spec/serializers/ee/issue_sidebar_basic_entity_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe EE::IssueSidebarBasicEntity do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:issue) { create(:issue, project: project, assignees: [user]) } + + context "When serializing" do + let(:test_value) { 'TEST VALUE' } + + before do + allow_any_instance_of(EE::CveRequestHelper).to receive(:request_cve_enabled_for_user?).and_return(test_value) + end + + it 'uses the value from request_cve_enabled_for_user' do + serializer = IssueSerializer.new(current_user: user, project: project) + data = serializer.represent(issue, serializer: 'sidebar') + expect(data[:request_cve_enabled_for_user]).to equal(test_value) + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 627576d816d48e406b2c56bd5944da81e8cf7eae..e9f33c2670415f4d1c8ec6b8d735b01a54f0fa97 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4924,6 +4924,24 @@ msgstr "" msgid "CPU" msgstr "" +msgid "CVE|As a maintainer, requesting a CVE for a vulnerability in your project will help your users stay secure and informed." +msgstr "" + +msgid "CVE|Common Vulnerability Enumeration (CVE) identifiers are used to track distinct vulnerabilities in specific versions of code." +msgstr "" + +msgid "CVE|Create CVE ID Request" +msgstr "" + +msgid "CVE|Enable CVE ID requests in the issue sidebar" +msgstr "" + +msgid "CVE|Request CVE ID" +msgstr "" + +msgid "CVE|Why Request a CVE ID?" +msgstr "" + msgid "Callback URL" msgstr "" diff --git a/spec/frontend/pages/projects/shared/permissions/components/project_feature_labeled_toggle_spec.js b/spec/frontend/pages/projects/shared/permissions/components/project_feature_labeled_toggle_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c8fba52f288d69e76d0f62750558b995ed20645b --- /dev/null +++ b/spec/frontend/pages/projects/shared/permissions/components/project_feature_labeled_toggle_spec.js @@ -0,0 +1,73 @@ +import { mount, shallowMount } from '@vue/test-utils'; + +import projectFeatureLabeledToggle from '~/pages/projects/shared/permissions/components/project_feature_labeled_toggle.vue'; +import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue'; + +describe('Project Feature Labeled Toggle', () => { + const defaultProps = { + name: 'test', + label: 'TEST', + value: true, + disabledInput: false, + helpPath: 'HELP PATH', + }; + let wrapper; + + const mountComponent = (customProps) => { + const propsData = { ...defaultProps, ...customProps }; + return shallowMount(projectFeatureLabeledToggle, { propsData }); + }; + + beforeEach(() => { + wrapper = mountComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Feature label', () => { + it('should render the correct label', () => { + expect(wrapper.find('span').element.innerHTML.trim()).toBe(defaultProps.label); + }); + }); + + describe('Feature toggle', () => { + it('should enable the feature toggle if the value is true', () => { + wrapper.setProps({ value: true }); + expect(wrapper.find(projectFeatureToggle).props().value).toBe(true); + }); + + it('should disable the feature toggle if the value is false', () => { + wrapper.setProps({ value: false }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.find(projectFeatureToggle).props().value).toBe(false); + }); + }); + + it('should disable the feature toggle if disabledInput is set', () => { + wrapper.setProps({ disabledInput: true }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.find(projectFeatureToggle).props().disabledInput).toBe(true); + }); + }); + + it('should emit a change event when the feature toggle changes', () => { + // Needs to be fully mounted to be able to trigger the click event on the internal button + wrapper = mount(projectFeatureLabeledToggle, { propsData: defaultProps }); + + expect(wrapper.emitted().change).toBeUndefined(); + wrapper + .find(projectFeatureToggle) + .find('button') + .trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.emitted().change.length).toBe(1); + expect(wrapper.emitted().change[0]).toEqual([false]); + }); + }); + }); +}); diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index a62977f30c305cd0ac1921c18f0ffc4b3bbcc87e..8c3bca8f81a605b82156fe79dd4a5a2464d6953a 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -28,6 +28,7 @@ const defaultProps = { showDefaultAwardEmojis: true, allowEditingCommitMessages: false, }, + isGitlabCom: true, canDisableEmails: true, canChangeVisibilityLevel: true, allowedVisibilityOptions: [0, 10, 20], @@ -359,7 +360,7 @@ describe('Settings Panel', () => { const repositoryFeatureToggleButton = findRepositoryFeatureSetting().find('button'); const lfsFeatureToggleButton = findLFSFeatureToggle().find('button'); - const isToggleButtonChecked = toggleButton => toggleButton.classes('is-checked'); + const isToggleButtonChecked = (toggleButton) => toggleButton.classes('is-checked'); // assert the initial state expect(isToggleButtonChecked(lfsFeatureToggleButton)).toBe(true); diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 93f4405fc9c4c365f8cbd7adef70c42f35f1769b..a337981fa3e8d950f22c6688c24929b00f261c69 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -1,816 +1,816 @@ --- Issue: -- id -- title -- assignee_id -- author_id -- project_id -- created_at -- updated_at -- position -- branch_name -- description -- state -- state_id -- iid -- updated_by_id -- confidential -- closed_at -- closed_by_id -- due_date -- moved_to_id -- duplicated_to_id -- promoted_to_epic_id -- lock_version -- milestone_id -- weight -- time_estimate -- relative_position -- external_author -- last_edited_at -- last_edited_by_id -- discussion_locked -- health_status -- external_key -- issue_type + - id + - title + - assignee_id + - author_id + - project_id + - created_at + - updated_at + - position + - branch_name + - description + - state + - state_id + - iid + - updated_by_id + - confidential + - closed_at + - closed_by_id + - due_date + - moved_to_id + - duplicated_to_id + - promoted_to_epic_id + - lock_version + - milestone_id + - weight + - time_estimate + - relative_position + - external_author + - last_edited_at + - last_edited_by_id + - discussion_locked + - health_status + - external_key + - issue_type Event: -- id -- target_type -- project_id -- group_id -- created_at -- updated_at -- action -- author_id -- fingerprint + - id + - target_type + - project_id + - group_id + - created_at + - updated_at + - action + - author_id + - fingerprint WikiPage::Meta: -- id -- title -- project_id + - id + - title + - project_id WikiPage::Slug: -- id -- wiki_page_meta_id -- slug + - id + - wiki_page_meta_id + - slug PushEventPayload: -- commit_count -- action -- ref_type -- commit_from -- commit_to -- ref -- commit_title -- ref_count + - commit_count + - action + - ref_type + - commit_from + - commit_to + - ref + - commit_title + - ref_count Note: -- id -- note -- noteable_type -- author_id -- created_at -- updated_at -- project_id -- attachment -- line_code -- commit_id -- system -- st_diff -- updated_by_id -- type -- position -- original_position -- change_position -- resolved_at -- resolved_by_id -- resolved_by_push -- discussion_id -- original_discussion_id -- confidential + - id + - note + - noteable_type + - author_id + - created_at + - updated_at + - project_id + - attachment + - line_code + - commit_id + - system + - st_diff + - updated_by_id + - type + - position + - original_position + - change_position + - resolved_at + - resolved_by_id + - resolved_by_push + - discussion_id + - original_discussion_id + - confidential LabelLink: -- id -- target_type -- created_at -- updated_at + - id + - target_type + - created_at + - updated_at ProjectLabel: -- id -- title -- color -- group_id -- project_id -- type -- created_at -- updated_at -- template -- description -- priority + - id + - title + - color + - group_id + - project_id + - type + - created_at + - updated_at + - template + - description + - priority Milestone: -- id -- title -- project_id -- group_id -- description -- due_date -- start_date -- created_at -- updated_at -- state -- iid + - id + - title + - project_id + - group_id + - description + - due_date + - start_date + - created_at + - updated_at + - state + - iid ProjectSnippet: -- id -- title -- description -- content -- author_id -- project_id -- created_at -- updated_at -- file_name -- type -- visibility_level + - id + - title + - description + - content + - author_id + - project_id + - created_at + - updated_at + - file_name + - type + - visibility_level Release: -- id -- name -- tag -- sha -- description -- author_id -- project_id -- created_at -- updated_at -- released_at + - id + - name + - tag + - sha + - description + - author_id + - project_id + - created_at + - updated_at + - released_at Releases::Evidence: -- id -- summary -- created_at -- updated_at + - id + - summary + - created_at + - updated_at Releases::Link: -- id -- url -- name -- filepath -- link_type -- created_at -- updated_at + - id + - url + - name + - filepath + - link_type + - created_at + - updated_at ProjectMember: -- id -- access_level -- source_type -- user_id -- notification_level -- type -- created_at -- updated_at -- created_by_id -- invite_email -- invite_token -- invite_accepted_at -- requested_at -- expires_at -- ldap -- override + - id + - access_level + - source_type + - user_id + - notification_level + - type + - created_at + - updated_at + - created_by_id + - invite_email + - invite_token + - invite_accepted_at + - requested_at + - expires_at + - ldap + - override User: -- id -- username -- email + - id + - username + - email MergeRequest: -- id -- target_branch -- source_branch -- source_project_id -- author_id -- assignee_id -- title -- created_at -- updated_at -- state -- state_id -- merge_status -- target_project_id -- iid -- description -- position -- updated_by_id -- merge_error -- merge_params -- merge_when_pipeline_succeeds -- merge_user_id -- merge_commit_sha -- squash_commit_sha -- in_progress_merge_commit_sha -- lock_version -- milestone_id -- approvals_before_merge -- rebase_commit_sha -- time_estimate -- squash -- last_edited_at -- last_edited_by_id -- head_pipeline_id -- discussion_locked -- allow_maintainer_to_push -- merge_ref_sha + - id + - target_branch + - source_branch + - source_project_id + - author_id + - assignee_id + - title + - created_at + - updated_at + - state + - state_id + - merge_status + - target_project_id + - iid + - description + - position + - updated_by_id + - merge_error + - merge_params + - merge_when_pipeline_succeeds + - merge_user_id + - merge_commit_sha + - squash_commit_sha + - in_progress_merge_commit_sha + - lock_version + - milestone_id + - approvals_before_merge + - rebase_commit_sha + - time_estimate + - squash + - last_edited_at + - last_edited_by_id + - head_pipeline_id + - discussion_locked + - allow_maintainer_to_push + - merge_ref_sha MergeRequestDiff: -- id -- state -- merge_request_id -- created_at -- updated_at -- base_commit_sha -- real_size -- head_commit_sha -- start_commit_sha -- commits_count -- files_count + - id + - state + - merge_request_id + - created_at + - updated_at + - base_commit_sha + - real_size + - head_commit_sha + - start_commit_sha + - commits_count + - files_count MergeRequestDiffCommit: -- merge_request_diff_id -- relative_order -- sha -- authored_date -- committed_date -- author_name -- author_email -- committer_name -- committer_email -- message + - merge_request_diff_id + - relative_order + - sha + - authored_date + - committed_date + - author_name + - author_email + - committer_name + - committer_email + - message MergeRequestDiffFile: -- merge_request_diff_id -- relative_order -- new_file -- renamed_file -- deleted_file -- new_path -- old_path -- a_mode -- b_mode -- too_large -- binary + - merge_request_diff_id + - relative_order + - new_file + - renamed_file + - deleted_file + - new_path + - old_path + - a_mode + - b_mode + - too_large + - binary MergeRequestContextCommit: -- id -- authored_date -- committed_date -- relative_order -- sha -- author_name -- author_email -- committer_name -- committer_email -- message -- merge_request_id + - id + - authored_date + - committed_date + - relative_order + - sha + - author_name + - author_email + - committer_name + - committer_email + - message + - merge_request_id MergeRequestContextCommitDiffFile: -- sha -- relative_order -- new_file -- renamed_file -- deleted_file -- new_path -- old_path -- a_mode -- b_mode -- too_large -- binary -- text + - sha + - relative_order + - new_file + - renamed_file + - deleted_file + - new_path + - old_path + - a_mode + - b_mode + - too_large + - binary + - text MergeRequest::Metrics: -- id -- created_at -- updated_at -- merge_request_id -- pipeline_id -- latest_closed_by_id -- latest_closed_at -- merged_by_id -- merged_at -- latest_build_started_at -- latest_build_finished_at -- first_deployed_to_production_at -- first_comment_at -- first_commit_at -- last_commit_at -- diff_size -- modified_paths_size -- commits_count -- first_approved_at -- first_reassigned_at -- added_lines -- target_project_id -- removed_lines + - id + - created_at + - updated_at + - merge_request_id + - pipeline_id + - latest_closed_by_id + - latest_closed_at + - merged_by_id + - merged_at + - latest_build_started_at + - latest_build_finished_at + - first_deployed_to_production_at + - first_comment_at + - first_commit_at + - last_commit_at + - diff_size + - modified_paths_size + - commits_count + - first_approved_at + - first_reassigned_at + - added_lines + - target_project_id + - removed_lines Ci::Pipeline: -- id -- project_id -- source -- ref -- sha -- before_sha -- source_sha -- target_sha -- push_data -- created_at -- updated_at -- tag -- yaml_errors -- committed_at -- status -- started_at -- finished_at -- duration -- user_id -- lock_version -- auto_canceled_by_id -- pipeline_schedule_id -- config_source -- failure_reason -- protected -- iid -- merge_request_id -- external_pull_request_id + - id + - project_id + - source + - ref + - sha + - before_sha + - source_sha + - target_sha + - push_data + - created_at + - updated_at + - tag + - yaml_errors + - committed_at + - status + - started_at + - finished_at + - duration + - user_id + - lock_version + - auto_canceled_by_id + - pipeline_schedule_id + - config_source + - failure_reason + - protected + - iid + - merge_request_id + - external_pull_request_id Ci::Stage: -- id -- name -- status -- position -- lock_version -- project_id -- pipeline_id -- created_at -- updated_at + - id + - name + - status + - position + - lock_version + - project_id + - pipeline_id + - created_at + - updated_at CommitStatus: -- id -- project_id -- status -- finished_at -- trace -- created_at -- updated_at -- started_at -- runner_id -- coverage -- commit_id -- commands -- job_id -- name -- deploy -- options -- allow_failure -- stage -- trigger_request_id -- stage_idx -- stage_id -- tag -- ref -- user_id -- type -- target_url -- description -- artifacts_file -- artifacts_file_store -- artifacts_metadata -- artifacts_metadata_store -- erased_by_id -- erased_at -- artifacts_expire_at -- environment -- artifacts_size -- when -- yaml_variables -- queued_at -- token -- lock_version -- coverage_regex -- auto_canceled_by_id -- retried -- protected -- failure_reason -- scheduled_at -- upstream_pipeline_id -- interruptible -- processed -- scheduling_type + - id + - project_id + - status + - finished_at + - trace + - created_at + - updated_at + - started_at + - runner_id + - coverage + - commit_id + - commands + - job_id + - name + - deploy + - options + - allow_failure + - stage + - trigger_request_id + - stage_idx + - stage_id + - tag + - ref + - user_id + - type + - target_url + - description + - artifacts_file + - artifacts_file_store + - artifacts_metadata + - artifacts_metadata_store + - erased_by_id + - erased_at + - artifacts_expire_at + - environment + - artifacts_size + - when + - yaml_variables + - queued_at + - token + - lock_version + - coverage_regex + - auto_canceled_by_id + - retried + - protected + - failure_reason + - scheduled_at + - upstream_pipeline_id + - interruptible + - processed + - scheduling_type Ci::Variable: -- id -- project_id -- key -- value -- encrypted_value -- encrypted_value_salt -- encrypted_value_iv + - id + - project_id + - key + - value + - encrypted_value + - encrypted_value_salt + - encrypted_value_iv Ci::Trigger: -- id -- token -- project_id -- created_at -- updated_at -- owner_id -- description -- ref + - id + - token + - project_id + - created_at + - updated_at + - owner_id + - description + - ref Ci::PipelineSchedule: -- id -- description -- ref -- cron -- cron_timezone -- next_run_at -- project_id -- owner_id -- active -- created_at -- updated_at + - id + - description + - ref + - cron + - cron_timezone + - next_run_at + - project_id + - owner_id + - active + - created_at + - updated_at Clusters::Cluster: -- id -- user_id -- enabled -- name -- provider_type -- platform_type -- created_at -- updated_at + - id + - user_id + - enabled + - name + - provider_type + - platform_type + - created_at + - updated_at Clusters::Project: -- id -- project_id -- cluster_id -- created_at -- updated_at + - id + - project_id + - cluster_id + - created_at + - updated_at Clusters::Providers::Gcp: -- id -- cluster_id -- status -- status_reason -- gcp_project_id -- zone -- num_nodes -- machine_type -- operation_id -- endpoint -- encrypted_access_token -- encrypted_access_token_iv -- created_at -- updated_at + - id + - cluster_id + - status + - status_reason + - gcp_project_id + - zone + - num_nodes + - machine_type + - operation_id + - endpoint + - encrypted_access_token + - encrypted_access_token_iv + - created_at + - updated_at Clusters::Platforms::Kubernetes: -- id -- cluster_id -- api_url -- ca_cert -- namespace -- username -- encrypted_password -- encrypted_password_iv -- encrypted_token -- encrypted_token_iv -- created_at -- updated_at + - id + - cluster_id + - api_url + - ca_cert + - namespace + - username + - encrypted_password + - encrypted_password_iv + - encrypted_token + - encrypted_token_iv + - created_at + - updated_at DeployKey: -- id -- user_id -- created_at -- updated_at -- key -- title -- type -- fingerprint -- public -- can_push -- last_used_at + - id + - user_id + - created_at + - updated_at + - key + - title + - type + - fingerprint + - public + - can_push + - last_used_at ProjectHook: -- id -- url -- project_id -- created_at -- updated_at -- type -- service_id -- push_events -- push_events_branch_filter -- issues_events -- merge_requests_events -- tag_push_events -- note_events -- pipeline_events -- enable_ssl_verification -- job_events -- wiki_page_events -- token -- group_id -- confidential_issues_events -- confidential_note_events -- repository_update_events -- releases_events + - id + - url + - project_id + - created_at + - updated_at + - type + - service_id + - push_events + - push_events_branch_filter + - issues_events + - merge_requests_events + - tag_push_events + - note_events + - pipeline_events + - enable_ssl_verification + - job_events + - wiki_page_events + - token + - group_id + - confidential_issues_events + - confidential_note_events + - repository_update_events + - releases_events ProtectedBranch: -- id -- project_id -- name -- created_at -- updated_at -- code_owner_approval_required + - id + - project_id + - name + - created_at + - updated_at + - code_owner_approval_required ProtectedTag: -- id -- project_id -- name -- created_at -- updated_at + - id + - project_id + - name + - created_at + - updated_at Project: -- description -- issues_enabled -- merge_requests_enabled -- wiki_enabled -- snippets_enabled -- visibility_level -- archived -- created_at -- updated_at -- last_activity_at -- star_count -- ci_id -- shared_runners_enabled -- build_coverage_regex -- build_allow_git_fetchs -- build_timeout -- pending_delete -- public_builds -- last_repository_check_failed -- last_repository_check_at -- collapse_outdated_diff_comments -- container_registry_enabled -- only_allow_merge_if_pipeline_succeeds -- has_external_issue_tracker -- request_access_enabled -- has_external_wiki -- only_allow_merge_if_all_discussions_are_resolved -- remove_source_branch_after_merge -- auto_cancel_pending_pipelines -- printing_merge_request_link_enabled -- resolve_outdated_diff_discussions -- build_allow_git_fetch -- merge_requests_template -- merge_requests_rebase_enabled -- approvals_before_merge -- merge_requests_author_approval -- reset_approvals_on_push -- disable_overriding_approvers_per_merge_request -- merge_requests_ff_only_enabled -- issues_template -- repository_size_limit -- sync_time -- service_desk_enabled -- last_repository_updated_at -- ci_config_path -- delete_error -- merge_requests_ff_only_enabled -- merge_requests_rebase_enabled -- jobs_cache_index -- external_authorization_classification_label -- external_webhook_token -- pages_https_only -- merge_requests_disable_committers_approval -- require_password_to_approve -- autoclose_referenced_issues -- suggestion_commit_message + - description + - issues_enabled + - merge_requests_enabled + - wiki_enabled + - snippets_enabled + - visibility_level + - archived + - created_at + - updated_at + - last_activity_at + - star_count + - ci_id + - shared_runners_enabled + - build_coverage_regex + - build_allow_git_fetchs + - build_timeout + - pending_delete + - public_builds + - last_repository_check_failed + - last_repository_check_at + - collapse_outdated_diff_comments + - container_registry_enabled + - only_allow_merge_if_pipeline_succeeds + - has_external_issue_tracker + - request_access_enabled + - has_external_wiki + - only_allow_merge_if_all_discussions_are_resolved + - remove_source_branch_after_merge + - auto_cancel_pending_pipelines + - printing_merge_request_link_enabled + - resolve_outdated_diff_discussions + - build_allow_git_fetch + - merge_requests_template + - merge_requests_rebase_enabled + - approvals_before_merge + - merge_requests_author_approval + - reset_approvals_on_push + - disable_overriding_approvers_per_merge_request + - merge_requests_ff_only_enabled + - issues_template + - repository_size_limit + - sync_time + - service_desk_enabled + - last_repository_updated_at + - ci_config_path + - delete_error + - merge_requests_ff_only_enabled + - merge_requests_rebase_enabled + - jobs_cache_index + - external_authorization_classification_label + - external_webhook_token + - pages_https_only + - merge_requests_disable_committers_approval + - require_password_to_approve + - autoclose_referenced_issues + - suggestion_commit_message ProjectTracingSetting: -- external_url + - external_url Author: -- name + - name ProjectFeature: -- id -- project_id -- merge_requests_access_level -- forking_access_level -- issues_access_level -- wiki_access_level -- snippets_access_level -- builds_access_level -- repository_access_level -- pages_access_level -- metrics_dashboard_access_level -- requirements_access_level -- operations_access_level -- created_at -- updated_at + - id + - project_id + - merge_requests_access_level + - forking_access_level + - issues_access_level + - wiki_access_level + - snippets_access_level + - builds_access_level + - repository_access_level + - pages_access_level + - metrics_dashboard_access_level + - requirements_access_level + - operations_access_level + - created_at + - updated_at ProtectedBranch::MergeAccessLevel: -- id -- protected_branch_id -- access_level -- created_at -- updated_at -- user_id -- group_id + - id + - protected_branch_id + - access_level + - created_at + - updated_at + - user_id + - group_id ProtectedBranch::PushAccessLevel: -- id -- protected_branch_id -- access_level -- created_at -- updated_at -- user_id -- group_id -- deploy_key_id + - id + - protected_branch_id + - access_level + - created_at + - updated_at + - user_id + - group_id + - deploy_key_id ProtectedBranch::UnprotectAccessLevel: -- id -- protected_branch_id -- access_level -- created_at -- updated_at -- user_id -- group_id + - id + - protected_branch_id + - access_level + - created_at + - updated_at + - user_id + - group_id ProtectedTag::CreateAccessLevel: -- id -- protected_tag_id -- access_level -- created_at -- updated_at -- user_id -- group_id + - id + - protected_tag_id + - access_level + - created_at + - updated_at + - user_id + - group_id AwardEmoji: -- id -- user_id -- name -- awardable_type -- created_at -- updated_at + - id + - user_id + - name + - awardable_type + - created_at + - updated_at LabelPriority: -- id -- project_id -- priority -- created_at -- updated_at + - id + - project_id + - priority + - created_at + - updated_at Timelog: -- id -- time_spent -- merge_request_id -- user_id -- spent_at -- created_at -- updated_at + - id + - time_spent + - merge_request_id + - user_id + - spent_at + - created_at + - updated_at ProjectAutoDevops: -- id -- enabled -- domain -- deploy_strategy -- project_id -- created_at -- updated_at + - id + - enabled + - domain + - deploy_strategy + - project_id + - created_at + - updated_at IssueAssignee: -- user_id + - user_id ProjectCustomAttribute: -- id -- created_at -- updated_at -- project_id -- key -- value + - id + - created_at + - updated_at + - project_id + - key + - value PrometheusMetric: -- id -- created_at -- updated_at -- project_id -- y_label -- unit -- legend -- title -- query -- group -- common -- identifier -- dashboard_path + - id + - created_at + - updated_at + - project_id + - y_label + - unit + - legend + - title + - query + - group + - common + - identifier + - dashboard_path PrometheusAlert: -- threshold -- operator -- environment_id -- project_id -- prometheus_metric_id + - threshold + - operator + - environment_id + - project_id + - prometheus_metric_id Badge: -- id -- name -- link_url -- image_url -- project_id -- group_id -- created_at -- updated_at -- type + - id + - name + - link_url + - image_url + - project_id + - group_id + - created_at + - updated_at + - type ProjectCiCdSetting: -- group_runners_enabled + - group_runners_enabled ProjectSetting: -- allow_merge_on_skipped_pipeline -- has_confluence -- has_vulnerabilities + - allow_merge_on_skipped_pipeline + - has_confluence + - has_vulnerabilities ProtectedEnvironment: -- id -- project_id -- name -- created_at -- updated_at + - id + - project_id + - name + - created_at + - updated_at ProtectedEnvironment::DeployAccessLevel: -- id -- protected_environment_id -- access_level -- created_at -- updated_at -- user_id -- group_id + - id + - protected_environment_id + - access_level + - created_at + - updated_at + - user_id + - group_id ResourceLabelEvent: -- id -- action -- merge_request_id -- label_id -- user_id -- created_at + - id + - action + - merge_request_id + - label_id + - user_id + - created_at ErrorTracking::ProjectErrorTrackingSetting: -- api_url -- project_id -- project_name -- organization_name + - api_url + - project_id + - project_name + - organization_name SentryIssue: -- id -- sentry_issue_identifier + - id + - sentry_issue_identifier Suggestion: -- id -- relative_order -- applied -- commit_id -- from_content -- to_content -- outdated -- lines_above -- lines_below + - id + - relative_order + - applied + - commit_id + - from_content + - to_content + - outdated + - lines_above + - lines_below MergeRequestAssignee: -- id -- user_id -- merge_request_id + - id + - user_id + - merge_request_id ProjectMetricsSetting: -- project_id -- external_dashboard_url -- created_at -- updated_at -- dashboard_timezone + - project_id + - external_dashboard_url + - created_at + - updated_at + - dashboard_timezone Board: -- id -- project_id -- created_at -- updated_at -- group_id -- milestone_id -- iteration_id -- weight -- name -- hide_backlog_list -- hide_closed_list + - id + - project_id + - created_at + - updated_at + - group_id + - milestone_id + - iteration_id + - weight + - name + - hide_backlog_list + - hide_closed_list List: -- id -- board_id -- label_id -- list_type -- position -- created_at -- updated_at -- milestone_id -- user_id -- max_issue_count -- max_issue_weight -- limit_metric + - id + - board_id + - label_id + - list_type + - position + - created_at + - updated_at + - milestone_id + - user_id + - max_issue_count + - max_issue_weight + - limit_metric ExternalPullRequest: -- id -- created_at -- updated_at -- project_id -- pull_request_iid -- status -- source_branch -- target_branch -- source_repository -- target_repository -- source_sha -- target_sha + - id + - created_at + - updated_at + - project_id + - pull_request_iid + - status + - source_branch + - target_branch + - source_repository + - target_repository + - source_sha + - target_sha DesignManagement::Design: -- id -- iid -- project_id -- filename -- relative_position + - id + - iid + - project_id + - filename + - relative_position DesignManagement::Action: -- id -- event -- image_v432x230 + - id + - event + - image_v432x230 DesignManagement::Version: -- id -- created_at -- sha -- author_id + - id + - created_at + - sha + - author_id ZoomMeeting: -- id -- project_id -- issue_status -- url -- created_at -- updated_at + - id + - project_id + - issue_status + - url + - created_at + - updated_at ServiceDeskSetting: -- project_id -- issue_template_key -- project_key + - project_id + - issue_template_key + - project_key ContainerExpirationPolicy: -- created_at -- updated_at -- next_run_at -- project_id -- name_regex -- name_regex_keep -- cadence -- older_than -- keep_n -- enabled + - created_at + - updated_at + - next_run_at + - project_id + - name_regex + - name_regex_keep + - cadence + - older_than + - keep_n + - enabled Epic: - id - milestone_id @@ -861,6 +861,7 @@ ProjectSecuritySetting: - auto_fix_sast - created_at - updated_at + - cve_id_request_enabled IssuableSla: - issue_id - due_at @@ -881,8 +882,8 @@ PushRule: - commit_committer_check - regexp_uses_re2 MergeRequest::CleanupSchedule: -- id -- scheduled_at -- completed_at -- created_at -- updated_at + - id + - scheduled_at + - completed_at + - created_at + - updated_at