Commit 3d35fb7b authored by Mehmet Emin INAC's avatar Mehmet Emin INAC Committed by David Fernandez

Allow enabling/disabling the "Security & Compliance" features

This change will allow users to enable/disable the features provided
under the "Security & Compliance" roof for projects.
parent 83d210a9
......@@ -75,6 +75,11 @@ export default {
required: false,
default: false,
},
securityAndComplianceAvailable: {
type: Boolean,
required: false,
default: false,
},
visibilityHelpPath: {
type: String,
required: false,
......@@ -141,6 +146,7 @@ export default {
metricsDashboardAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
analyticsAccessLevel: featureAccessLevel.EVERYONE,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
operationsAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryEnabled: true,
lfsEnabled: true,
......@@ -264,6 +270,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.requirementsAccessLevel,
);
this.securityAndComplianceAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.securityAndComplianceAccessLevel,
);
this.operationsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.operationsAccessLevel,
......@@ -552,6 +562,17 @@ export default {
name="project[project_feature_attributes][requirements_access_level]"
/>
</project-setting-row>
<project-setting-row
v-if="securityAndComplianceAvailable"
:label="s__('ProjectSettings|Security & Compliance')"
:help-text="s__('ProjectSettings|Security & Compliance for this project')"
>
<project-feature-setting
v-model="securityAndComplianceAccessLevel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][security_and_compliance_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="wiki-settings"
:label="s__('ProjectSettings|Wiki')"
......
......@@ -3,6 +3,7 @@ export default {
return {
packagesEnabled: false,
requirementsEnabled: false,
securityAndComplianceEnabled: false,
};
},
watch: {
......
......@@ -212,6 +212,7 @@
= render_if_exists "layouts/nav/test_cases_link", project: @project
- if project_nav_tab? :security_and_compliance
= render_if_exists 'layouts/nav/sidebar/project_security_link' # EE-specific
- if project_nav_tab? :operations
......
---
title: Add `security_and_compliance_access_level` column into the `project_features`
table
merge_request: 52551
author:
type: added
# frozen_string_literal: true
class AddSecurityDashboardAccessLevelIntoProjectFeatures < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
PRIVATE_ACCESS_LEVEL = 10
def up
with_lock_retries do
add_column :project_features, :security_and_compliance_access_level, :integer, default: PRIVATE_ACCESS_LEVEL, null: false
end
end
def down
with_lock_retries do
remove_column :project_features, :security_and_compliance_access_level
end
end
end
b5c219d1b1443ddf482f26d8280a1c7318456affce3ad57a082eb8f9efc32206
\ No newline at end of file
......@@ -15837,7 +15837,8 @@ CREATE TABLE project_features (
metrics_dashboard_access_level integer,
requirements_access_level integer DEFAULT 20 NOT NULL,
operations_access_level integer DEFAULT 20 NOT NULL,
analytics_access_level integer DEFAULT 20 NOT NULL
analytics_access_level integer DEFAULT 20 NOT NULL,
security_and_compliance_access_level integer DEFAULT 10 NOT NULL
);
CREATE SEQUENCE project_features_id_seq
......
......@@ -3,6 +3,7 @@ export default {
return {
packagesEnabled: true,
requirementsEnabled: true,
securityAndComplianceEnabled: true,
};
},
watch: {
......
......@@ -49,7 +49,7 @@ module EE
override :project_feature_attributes
def project_feature_attributes
super + [:requirements_access_level]
super + [:requirements_access_level, :security_and_compliance_access_level]
end
override :project_params_attributes
......
......@@ -59,14 +59,16 @@ module EE
override :project_permissions_settings
def project_permissions_settings(project)
super.merge(
requirementsAccessLevel: project.requirements_access_level
requirementsAccessLevel: project.requirements_access_level,
securityAndComplianceAccessLevel: project.security_and_compliance_access_level
)
end
override :project_permissions_panel_data
def project_permissions_panel_data(project)
super.merge(
requirementsAvailable: project.feature_available?(:requirements)
requirementsAvailable: project.feature_available?(:requirements),
securityAndComplianceAvailable: project.feature_available?(:security_and_compliance)
)
end
......@@ -322,7 +324,9 @@ module EE
private
def get_project_security_nav_tabs(project, current_user)
nav_tabs = []
return [] unless can?(current_user, :access_security_and_compliance, project)
nav_tabs = [:security_and_compliance]
if can?(current_user, :read_project_security_dashboard, project)
nav_tabs << :security
......
......@@ -12,5 +12,13 @@ module EE
def requirements_access_level=(value)
write_feature_attribute_string(:requirements_access_level, value)
end
def security_and_compliance_enabled=(value)
write_feature_attribute_boolean(:security_and_compliance_access_level, value)
end
def security_and_compliance_access_level=(value)
write_feature_attribute_string(:security_and_compliance_access_level, value)
end
end
end
......@@ -199,7 +199,7 @@ module EE
delegate :closest_gitlab_subscription, to: :namespace
delegate :jira_vulnerabilities_integration_enabled?, to: :jira_service, allow_nil: true
delegate :requirements_access_level, to: :project_feature, allow_nil: true
delegate :requirements_access_level, :security_and_compliance_access_level, to: :project_feature, allow_nil: true
delegate :pipeline_configuration_full_path, to: :compliance_management_framework, allow_nil: true
alias_attribute :compliance_pipeline_configuration_full_path, :pipeline_configuration_full_path
......
......@@ -4,7 +4,7 @@ module EE
module ProjectFeature
extend ActiveSupport::Concern
EE_FEATURES = %i(requirements).freeze
EE_FEATURES = %i(requirements security_and_compliance).freeze
prepended do
set_available_features(EE_FEATURES)
......@@ -19,6 +19,7 @@ module EE
end
default_value_for :requirements_access_level, value: Featurable::ENABLED, allows_nil: false
default_value_for :security_and_compliance_access_level, value: Featurable::PRIVATE, allows_nil: false
private
......
......@@ -12,6 +12,7 @@ class License < ApplicationRecord
EE_ALL_PLANS = [STARTER_PLAN, PREMIUM_PLAN, ULTIMATE_PLAN].freeze
EES_FEATURES = %i[
security_and_compliance
audit_events
blocked_issues
board_iteration_lists
......
......@@ -111,6 +111,11 @@ module EE
@subject.feature_available?(:reject_unsigned_commits)
end
with_scope :subject
condition(:security_and_compliance_enabled) do
@subject.feature_available?(:security_and_compliance) && feature_available?(:security_and_compliance)
end
with_scope :subject
condition(:security_dashboard_enabled) do
@subject.feature_available?(:security_dashboard)
......@@ -214,6 +219,10 @@ module EE
rule { can?(:read_project) & iterations_available }.enable :read_iteration
rule { security_and_compliance_enabled & can?(:developer_access) }.policy do
enable :access_security_and_compliance
end
rule { security_dashboard_enabled & can?(:developer_access) }.policy do
enable :read_vulnerability
enable :read_vulnerability_scanner
......
---
title: Implement "Security & Compliance" visibility settings
merge_request: 52551
author:
type: added
......@@ -31,6 +31,9 @@ module EE
expose :requirements_enabled do |project, options|
project.feature_available?(:requirements, options[:current_user])
end
expose :security_and_compliance_enabled do |project, options|
project.feature_available?(:security_and_compliance, options[:current_user])
end
expose :compliance_frameworks do |project, _|
[project.compliance_framework_setting&.compliance_management_framework&.name].compact
end
......
......@@ -16,7 +16,8 @@ module EE
:metrics_dashboard_access_level,
:analytics_access_level,
:operations_access_level,
:requirements_access_level].freeze
:requirements_access_level,
:security_and_compliance_access_level].freeze
def initialize(current_user, model, project)
@project = project
......
......@@ -266,44 +266,81 @@ RSpec.describe ProjectsHelper do
describe '#get_project_nav_tabs' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
subject { helper.get_project_nav_tabs(project, user) }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).and_return(false)
allow(helper).to receive(:can?).with(user, ability, project).and_return(feature_available?)
end
describe 'tabs' do
where(:ability, :nav_tabs) do
:read_dependencies | [:dependencies]
:read_feature_flag | [:operations]
:read_licenses | [:licenses]
:read_project_security_dashboard | [:security, :security_configuration]
:read_threat_monitoring | [:threat_monitoring]
:read_incident_management_oncall_schedule | [:oncall_schedule]
end
with_them do
let_it_be(:user) { create(:user) }
context 'when the feature is available' do
let(:feature_available?) { true }
before do
allow(helper).to receive(:can?) { false }
allow(helper).to receive(:current_user).and_return(user)
it { is_expected.to include(*nav_tabs) }
end
subject do
helper.send(:get_project_nav_tabs, project, user)
context 'when the feature is not available' do
let(:feature_available?) { false }
it { is_expected.not_to include(*nav_tabs) }
end
end
end
context 'when the feature is not available' do
before do
allow(helper).to receive(:can?).with(user, ability, project).and_return(false)
describe 'Security & Compliance tabs' do
where(:ability, :nav_tabs) do
:read_project_security_dashboard | [:security, :security_configuration]
:read_on_demand_scans | [:on_demand_scans]
:read_dependencies | [:dependencies]
:read_licenses | [:licenses]
:read_threat_monitoring | [:threat_monitoring]
end
it 'does not include the nav tabs' do
is_expected.not_to include(*nav_tabs)
with_them do
before do
allow(helper).to receive(:can?).with(user, :access_security_and_compliance, project).and_return(security_compliance_available?)
end
context 'when the "Security & Compliance" is accessible' do
let(:security_compliance_available?) { true }
context 'when the feature is not available' do
let(:feature_available?) { false }
it { is_expected.not_to include(*nav_tabs) }
end
context 'when the feature is available' do
before do
allow(helper).to receive(:can?).with(user, ability, project).and_return(true)
let(:feature_available?) { true }
it { is_expected.to include(*nav_tabs) }
end
end
it 'includes the nav tabs' do
is_expected.to include(*nav_tabs)
context 'when the "Security & Compliance" is not accessible' do
let(:security_compliance_available?) { false }
context 'when the feature is not available' do
let(:feature_available?) { false }
it { is_expected.not_to include(*nav_tabs) }
end
context 'when the feature is available' do
let(:feature_available?) { true }
it { is_expected.not_to include(*nav_tabs) }
end
end
end
end
......@@ -317,6 +354,7 @@ RSpec.describe ProjectsHelper do
before do
allow(helper).to receive(:can?).and_return(false)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :access_security_and_compliance, project).and_return(true)
end
context 'when user can read project security dashboard and audit events' do
......@@ -369,6 +407,7 @@ RSpec.describe ProjectsHelper do
before do
allow(helper).to receive(:can?).and_return(false)
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :access_security_and_compliance, project).and_return(true)
end
context 'when user can read project security dashboard and audit events' do
......@@ -510,4 +549,26 @@ RSpec.describe ProjectsHelper do
end
end
end
describe '#project_permissions_settings' do
let(:expected_settings) { { requirementsAccessLevel: 20, securityAndComplianceAccessLevel: 10 } }
subject { helper.project_permissions_settings(project) }
it { is_expected.to include(expected_settings) }
end
describe '#project_permissions_panel_data' do
let(:user) { instance_double(User, admin?: false) }
let(:expected_data) { { requirementsAvailable: false, securityAndComplianceAvailable: true } }
subject { helper.project_permissions_panel_data(project) }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).and_return(false)
end
it { is_expected.to include(expected_data) }
end
end
......@@ -515,6 +515,92 @@ RSpec.describe ProjectPolicy do
end
end
describe 'access_security_and_compliance' do
context 'when the "Security & Compliance" is enabled' do
before do
project.project_feature.update!(security_and_compliance_access_level: Featurable::PRIVATE)
end
%w[owner maintainer developer].each do |role|
context "when the role is #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_allowed(:access_security_and_compliance) }
end
end
context 'with admin' do
let(:current_user) { admin }
context 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:access_security_and_compliance) }
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
end
%w[reporter guest].each do |role|
context "when the role is #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
end
context 'with non member' do
let(:current_user) { non_member }
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
end
context 'when the "Security & Compliance" is not enabled' do
before do
project.project_feature.update!(security_and_compliance_access_level: Featurable::DISABLED)
end
%w[owner maintainer developer reporter guest].each do |role|
context "when the role is #{role}" do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
end
context 'with admin' do
let(:current_user) { admin }
context 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
end
context 'with non member' do
let(:current_user) { non_member }
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
context 'with anonymous' do
let(:current_user) { anonymous }
it { is_expected.to be_disallowed(:access_security_and_compliance) }
end
end
end
shared_context 'when security dashboard feature is not available' do
before do
stub_licensed_features(security_dashboard: false)
......
......@@ -93,9 +93,44 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
allow(view).to receive(:can?).with(nil, :read_dependencies, project).and_return(can_read_dependencies)
allow(view).to receive(:can?).with(nil, :read_project_security_dashboard, project).and_return(can_read_dashboard)
allow(view).to receive(:can?).with(nil, :read_project_audit_events, project).and_return(can_read_project_audit_events)
allow(view).to receive(:can?).with(nil, :access_security_and_compliance, project).and_return(can_access_security_and_compliance)
render
end
describe 'when the "Security & Compliance" is not available' do
let(:can_access_security_and_compliance) { false }
describe 'when the user has full permissions' do
let(:can_read_dashboard) { true }
let(:can_read_dependencies) { true }
let(:can_read_project_audit_events) { true }
it 'top level navigation link is visible' do
expect(rendered).not_to have_link('Security & Compliance', href: project_security_dashboard_index_path(project))
end
it 'security dashboard link is visible' do
expect(rendered).not_to have_link('Security Dashboard', href: project_security_dashboard_index_path(project))
end
it 'security configuration link is visible' do
expect(rendered).not_to have_link('Configuration', href: project_security_configuration_path(project))
end
it 'dependency list link is visible' do
expect(rendered).not_to have_link('Dependency List', href: project_dependencies_path(project))
end
it 'audit events link is visible' do
expect(rendered).not_to have_link('Audit Events', href: project_audit_events_path(project))
end
end
end
describe 'when the "Security & Compliance" is available' do
let(:can_access_security_and_compliance) { true }
describe 'when the user has full permissions' do
let(:can_read_dashboard) { true }
let(:can_read_dependencies) { true }
......@@ -226,6 +261,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
end
end
describe 'Settings > Operations' do
it 'is not visible when no valid license' do
......
......@@ -22973,6 +22973,12 @@ msgstr ""
msgid "ProjectSettings|Requirements management system for this project"
msgstr ""
msgid "ProjectSettings|Security & Compliance"
msgstr ""
msgid "ProjectSettings|Security & Compliance for this project"
msgstr ""
msgid "ProjectSettings|Set the default behavior and availability of this option in merge requests. Changes made are also applied to existing merge requests."
msgstr ""
......
......@@ -583,6 +583,7 @@ ProjectFeature:
- requirements_access_level
- analytics_access_level
- operations_access_level
- security_and_compliance_access_level
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
......
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