Commit da6b9e51 authored by Toon Claes's avatar Toon Claes

Merge branch 'add-compliance-framework-setting-to-projects' into 'master'

Add compliance framework setting to projects

See merge request gitlab-org/gitlab!28182
parents 958021a7 a68d1255
......@@ -20,6 +20,8 @@
= f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control"
%p.form-text.text-muted= _('Separate topics with commas.')
= render_if_exists 'compliance_management/compliance_framework/project_settings', f: f
.row
.form-group.col-md-9
= f.label :description, _('Project description (optional)'), class: 'label-bold'
......
# frozen_string_literal: true
class AddProjectComplianceFrameworkSettingsTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
create_table :project_compliance_framework_settings, id: false do |t|
t.references :project, primary_key: true, null: false, index: true, foreign_key: { on_delete: :cascade }
t.integer :framework, null: false, limit: 2
end
end
end
def down
with_lock_retries do
drop_table :project_compliance_framework_settings
end
end
end
......@@ -4722,6 +4722,20 @@ CREATE SEQUENCE public.project_ci_cd_settings_id_seq
ALTER SEQUENCE public.project_ci_cd_settings_id_seq OWNED BY public.project_ci_cd_settings.id;
CREATE TABLE public.project_compliance_framework_settings (
project_id bigint NOT NULL,
framework smallint NOT NULL
);
CREATE SEQUENCE public.project_compliance_framework_settings_project_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.project_compliance_framework_settings_project_id_seq OWNED BY public.project_compliance_framework_settings.project_id;
CREATE TABLE public.project_custom_attributes (
id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
......@@ -7315,6 +7329,8 @@ ALTER TABLE ONLY public.project_auto_devops ALTER COLUMN id SET DEFAULT nextval(
ALTER TABLE ONLY public.project_ci_cd_settings ALTER COLUMN id SET DEFAULT nextval('public.project_ci_cd_settings_id_seq'::regclass);
ALTER TABLE ONLY public.project_compliance_framework_settings ALTER COLUMN project_id SET DEFAULT nextval('public.project_compliance_framework_settings_project_id_seq'::regclass);
ALTER TABLE ONLY public.project_custom_attributes ALTER COLUMN id SET DEFAULT nextval('public.project_custom_attributes_id_seq'::regclass);
ALTER TABLE ONLY public.project_daily_statistics ALTER COLUMN id SET DEFAULT nextval('public.project_daily_statistics_id_seq'::regclass);
......@@ -8144,6 +8160,9 @@ ALTER TABLE ONLY public.project_auto_devops
ALTER TABLE ONLY public.project_ci_cd_settings
ADD CONSTRAINT project_ci_cd_settings_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.project_compliance_framework_settings
ADD CONSTRAINT project_compliance_framework_settings_pkey PRIMARY KEY (project_id);
ALTER TABLE ONLY public.project_custom_attributes
ADD CONSTRAINT project_custom_attributes_pkey PRIMARY KEY (id);
......@@ -9736,6 +9755,8 @@ CREATE UNIQUE INDEX index_project_auto_devops_on_project_id ON public.project_au
CREATE UNIQUE INDEX index_project_ci_cd_settings_on_project_id ON public.project_ci_cd_settings USING btree (project_id);
CREATE INDEX index_project_compliance_framework_settings_on_project_id ON public.project_compliance_framework_settings USING btree (project_id);
CREATE INDEX index_project_custom_attributes_on_key_and_value ON public.project_custom_attributes USING btree (key, value);
CREATE UNIQUE INDEX index_project_custom_attributes_on_project_id_and_key ON public.project_custom_attributes USING btree (project_id, key);
......@@ -11391,6 +11412,9 @@ ALTER TABLE ONLY public.prometheus_alerts
ALTER TABLE ONLY public.term_agreements
ADD CONSTRAINT fk_rails_6ea6520e4a FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.project_compliance_framework_settings
ADD CONSTRAINT fk_rails_6f5294f16c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.users_security_dashboard_projects
ADD CONSTRAINT fk_rails_6f6cf8e66e FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
......@@ -13002,6 +13026,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200330121000
20200330123739
20200330132913
20200331132103
20200331195952
20200331220930
20200402123926
......
......@@ -89,6 +89,8 @@ module EE
attrs += merge_request_rules_params
attrs += compliance_framework_params
if allow_mirror_params?
attrs + mirror_params
else
......@@ -134,6 +136,12 @@ module EE
project&.feature_available?(:merge_pipelines)
end
def compliance_framework_params
return [] unless current_user.can?(:admin_compliance_framework, project)
[compliance_framework_setting_attributes: [:framework]]
end
def log_audit_event(message:)
AuditEvents::CustomAuditEventService.new(
current_user,
......
# frozen_string_literal: true
module ComplianceManagement
module ComplianceFramework
module ProjectSettingsHelper
def compliance_framework_options
option_values = compliance_framework_option_values
ProjectSettings.frameworks.map { |k, _v| [option_values.fetch(k.to_sym), k] }
end
def compliance_framework_option_values
{
gdpr: s_('ComplianceFramework|GDPR - General Data Protection Regulation'),
hipaa: s_('ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act'),
pci_dss: s_('ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard'),
soc_2: s_('ComplianceFramework|SOC 2 - Service Organization Control 2'),
sox: s_('ComplianceFramework|SOX - Sarbanes-Oxley')
}.freeze
end
end
end
end
# frozen_string_literal: true
module ComplianceManagement
module ComplianceFramework
class ProjectSettings < ApplicationRecord
self.table_name = 'project_compliance_framework_settings'
self.primary_key = :project_id
belongs_to :project
enum framework: {
gdpr: 1, # General Data Protection Regulation
hipaa: 2, # Health Insurance Portability and Accountability Act
pci_dss: 3, # Payment Card Industry-Data Security Standard
soc_2: 4, # Service Organization Control 2
sox: 5 # Sarbanes-Oxley
}
validates :project, presence: true
validates :framework, uniqueness: { scope: [:project_id] }
validates :framework, inclusion: { in: self.frameworks.keys }
end
end
end
......@@ -48,6 +48,7 @@ module EE
has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :feature_usage, class_name: 'ProjectFeatureUsage'
has_one :status_page_setting, inverse_of: :project
has_one :compliance_framework_setting, class_name: 'ComplianceManagement::ComplianceFramework::ProjectSettings', inverse_of: :project
has_many :reviews, inverse_of: :project
has_many :approvers, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
......@@ -180,6 +181,7 @@ module EE
accepts_nested_attributes_for :tracing_setting, update_only: true, allow_destroy: true
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
alias_attribute :fallback_approvals_required, :approvals_before_merge
end
......
......@@ -107,6 +107,7 @@ class License < ApplicationRecord
EEU_FEATURES = EEP_FEATURES + %i[
cluster_health
compliance_framework
container_scanning
credentials_inventory
dast
......
......@@ -36,6 +36,8 @@ module EE
with_scope :subject
condition(:requirements_available) { @subject.feature_available?(:requirements) }
condition(:compliance_framework_available) { @subject.feature_available?(:compliance_framework, @user) }
with_scope :global
condition(:is_development) { Rails.env.development? }
......@@ -364,6 +366,8 @@ module EE
rule { requirements_available & owner }.enable :destroy_requirement
rule { compliance_framework_available & can?(:admin_project) }.enable :admin_compliance_framework
rule { status_page_available & can?(:developer_access) }.enable :publish_status_page
end
......
......@@ -18,6 +18,8 @@ module EE
return project
end
compliance_framework_setting
result = super do
# Repository size limit comes as MB from the view
project.repository_size_limit = ::Gitlab::Utils.try_megabytes_to_bytes(limit) if limit
......@@ -48,6 +50,18 @@ module EE
mirror_user_id == project.mirror_user&.id
end
def compliance_framework_setting
settings = params[:compliance_framework_setting_attributes]
return unless settings.present?
unless can?(current_user, :admin_compliance_framework, project)
params.delete(:compliance_framework_setting_attributes)
return
end
settings.merge!(_destroy: settings[:framework].blank?)
end
def log_audit_events
EE::Audit::ProjectChangesAuditor.new(current_user, project).execute
end
......
- return unless current_user.can?(:admin_compliance_framework, @project)
.row
.form-group.col-md-9.mb-5
= f.fields_for :compliance_framework_setting, ComplianceManagement::ComplianceFramework::ProjectSettings.new do |cf|
= cf.label :framework, _('Compliance framework (optional)'), class: 'label-bold'
%p.text-secondary= _('Select required regulatory standard')
= cf.select :framework, options_for_select(compliance_framework_options, @project.compliance_framework_setting&.framework), { selected: '', disabled: '', prompt: _('Choose your framework'), include_blank: _('None') }, class: 'form-control'
---
title: Add a compliance framework setting to project
merge_request: 28182
author:
type: added
......@@ -376,6 +376,47 @@ describe ProjectsController do
end
end
end
context 'compliance framework settings' do
let(:framework) { ComplianceManagement::ComplianceFramework::ProjectSettings.frameworks.keys.sample }
let(:params) { { compliance_framework_setting_attributes: { framework: framework } } }
context 'when unlicensed' do
before do
stub_licensed_features(compliance_framework: false)
end
it 'ignores any compliance framework params' do
put :update,
params: {
namespace_id: project.namespace,
id: project,
project: params
}
project.reload
expect(project.compliance_framework_setting).to be_nil
end
end
context 'when licensed' do
before do
stub_licensed_features(compliance_framework: true)
end
it 'sets the compliance framework' do
put :update,
params: {
namespace_id: project.namespace,
id: project,
project: params
}
project.reload
expect(project.compliance_framework_setting.framework).to eq(framework)
end
end
end
end
describe '#download_export' do
......
# frozen_string_literal: true
FactoryBot.define do
factory :compliance_framework_project_setting, class: 'ComplianceManagement::ComplianceFramework::ProjectSettings' do
project
framework { ComplianceManagement::ComplianceFramework::ProjectSettings.frameworks.keys.sample }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ComplianceManagement::ComplianceFramework::ProjectSettingsHelper do
let(:frameworks) { ComplianceManagement::ComplianceFramework::ProjectSettings.frameworks.keys }
let(:descriptions) { helper.compliance_framework_option_values }
describe '#compliance_framework_options' do
it 'has all the descriptions' do
expect(helper.compliance_framework_options.map(&:first)).to eq(descriptions.map(&:last))
end
it 'has all the frameworks' do
expect(helper.compliance_framework_options.map(&:last)).to eq(frameworks)
end
end
describe '#compliance_framework_option_values' do
it 'returns a hash' do
expect(helper.compliance_framework_option_values).to be_a_kind_of(Hash)
end
it 'is the same length as frameworks' do
expect(helper.compliance_framework_option_values.length).to equal(frameworks.length)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe ComplianceManagement::ComplianceFramework::ProjectSettings do
let(:known_frameworks) { ComplianceManagement::ComplianceFramework::ProjectSettings.frameworks.keys }
subject { build :compliance_framework_project_setting }
describe 'Associations' do
it 'belongs to project' do
expect(subject).to belong_to(:project)
end
end
describe 'Validations' do
it 'confirms the presence of project' do
expect(subject).to validate_presence_of(:project)
end
it 'confirms that the framework is unique for the project' do
expect(subject).to validate_uniqueness_of(:framework).scoped_to(:project_id).ignoring_case_sensitivity
end
it 'allows all known frameworks' do
expect(subject).to allow_values(*known_frameworks).for(:framework)
end
it 'invalidates an unknown framework' do
expect { build :compliance_framework_project_setting, framework: 'ABCDEFGH' }.to raise_error(ArgumentError).with_message(/is not a valid framework/)
end
end
end
......@@ -23,6 +23,7 @@ describe Project do
it { is_expected.to have_one(:import_state).class_name('ProjectImportState') }
it { is_expected.to have_one(:repository_state).class_name('ProjectRepositoryState').inverse_of(:project) }
it { is_expected.to have_one(:status_page_setting).class_name('StatusPageSetting') }
it { is_expected.to have_one(:compliance_framework_setting).class_name('ComplianceManagement::ComplianceFramework::ProjectSettings') }
it { is_expected.to have_many(:reviews).inverse_of(:project) }
it { is_expected.to have_many(:path_locks) }
......
......@@ -1281,4 +1281,37 @@ describe ProjectPolicy do
it_behaves_like 'resource with requirement permissions' do
let(:resource) { project }
end
describe ':compliance_framework_available' do
using RSpec::Parameterized::TableSyntax
let(:policy) { :admin_compliance_framework }
where(:role, :feature_enabled, :allowed) do
:guest | false | false
:guest | true | false
:reporter | false | false
:reporter | true | false
:developer | false | false
:developer | true | false
:maintainer | false | false
:maintainer | true | true
:owner | false | false
:owner | true | true
:admin | false | false
:admin | true | true
end
with_them do
let(:current_user) { public_send(role) }
before do
stub_licensed_features(compliance_framework: feature_enabled)
end
it do
is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy))
end
end
end
end
......@@ -235,6 +235,70 @@ describe Projects::UpdateService, '#execute' do
end
end
context 'when compliance frameworks is set' do
let(:project_setting) { create(:compliance_framework_project_setting) }
before do
stub_licensed_features(compliance_framework: true)
project.update!(compliance_framework_setting: project_setting)
end
context 'when framework is not blank' do
let(:framework) { ComplianceManagement::ComplianceFramework::ProjectSettings.frameworks.keys.without(project_setting.framework).sample }
let(:opts) { { compliance_framework_setting_attributes: { framework: framework } } }
it 'saves the framework' do
update_project(project, user, opts)
expect(project.reload.compliance_framework_setting.framework).to eq(framework)
end
end
context 'when framework is blank' do
let(:opts) { { compliance_framework_setting_attributes: { framework: '' } } }
it 'removes the framework record' do
update_project(project, user, opts)
expect(project.reload.compliance_framework_setting).to be_nil
end
end
end
context 'when compliance framework feature is disabled' do
before do
stub_licensed_features(compliance_framework: false)
end
context 'the project had the feature before' do
let(:project_setting) { create(:compliance_framework_project_setting) }
before do
project.update!(compliance_framework_setting: project_setting)
end
let(:framework) { ComplianceManagement::ComplianceFramework::ProjectSettings.frameworks.keys.without(project_setting.framework).sample }
let(:opts) { { compliance_framework_setting_attributes: { framework: framework } } }
it 'does not save the new framework and retains the old setting' do
update_project(project, user, opts)
expect(project.reload.compliance_framework_setting.framework).to eq(project_setting.framework)
end
end
context 'the project never had the feature' do
let(:framework) { ComplianceManagement::ComplianceFramework::ProjectSettings.frameworks.keys.sample }
let(:opts) { { compliance_framework_setting_attributes: { framework: framework } } }
it 'does not save the framework' do
update_project(project, user, opts)
expect(project.reload.compliance_framework_setting).to be_nil
end
end
end
it 'returns an error result when record cannot be updated' do
admin = create(:admin)
......
......@@ -129,6 +129,7 @@ module API
:avatar,
:suggestion_commit_message,
:repository_storage,
:compliance_framework_setting,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,
......
......@@ -159,6 +159,7 @@ excluded_attributes:
- :max_artifacts_size
- :marked_for_deletion_at
- :marked_for_deletion_by_user_id
- :compliance_framework_setting
namespaces:
- :runners_token
- :runners_token_encrypted
......
......@@ -3833,6 +3833,9 @@ msgstr ""
msgid "Choose which status most accurately reflects the current state of this issue:"
msgstr ""
msgid "Choose your framework"
msgstr ""
msgid "CiStatusLabel|canceled"
msgstr ""
......@@ -5239,6 +5242,24 @@ msgstr ""
msgid "Compliance Dashboard"
msgstr ""
msgid "Compliance framework (optional)"
msgstr ""
msgid "ComplianceFramework|GDPR - General Data Protection Regulation"
msgstr ""
msgid "ComplianceFramework|HIPAA - Health Insurance Portability and Accountability Act"
msgstr ""
msgid "ComplianceFramework|PCI-DSS - Payment Card Industry-Data Security Standard"
msgstr ""
msgid "ComplianceFramework|SOC 2 - Service Organization Control 2"
msgstr ""
msgid "ComplianceFramework|SOX - Sarbanes-Oxley"
msgstr ""
msgid "Confidence: %{confidence}"
msgstr ""
......@@ -17970,6 +17991,9 @@ msgstr ""
msgid "Select projects you want to import."
msgstr ""
msgid "Select required regulatory standard"
msgstr ""
msgid "Select shards to replicate"
msgstr ""
......
......@@ -477,6 +477,7 @@ project:
- export_jobs
- daily_report_results
- jira_imports
- compliance_framework_setting
award_emoji:
- awardable
- user
......
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