Commit a68d1255 authored by Robert Hunt's avatar Robert Hunt Committed by Toon Claes

Add compliance framework to projects

Created the initial column migration

Created the index migration

Updated structure.sql

Create new setting panel for compliance frameworks

Created a new Vue component to render the panel and updated the relevant
JS files to load it.

Updated the HAML templates to show the new compliance settings panel

Adding compliance_framework attribute to projects

Added limitation so compliance only shows up when the user is a
group/project admin and is using Gold/Ultimate

Updated vue to use project input name

Converted from vue to haml

Removed all vue app stuff

Added new dropdown to haml

Added new compliance framework concern to the project model
which handles the validation and constant list

Moved the acronyms to the beginning of the options

Replaced the tooltip with description text

Updated text and added changelog

Moved changelog to ee

Remove index as covered by the project_id index

Increase framework length limit to 64

Changed to use with_lock_retries

Rebuild structure.sql

Remove unnecessary s_

Change migration to use integers for the enum

Ordered license feature list

Rebuilt structure.sql file
parent 0f9003c7
......@@ -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
......
......@@ -3830,6 +3830,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 ""
......@@ -5236,6 +5239,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 ""
......@@ -17967,6 +17988,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