diff --git a/db/docs/approval_policy_merge_request_bypass_events.yml b/db/docs/approval_policy_merge_request_bypass_events.yml new file mode 100644 index 0000000000000000000000000000000000000000..915374adbeee16356debf9da5ffc8ed18842d55f --- /dev/null +++ b/db/docs/approval_policy_merge_request_bypass_events.yml @@ -0,0 +1,14 @@ + +--- +table_name: approval_policy_merge_request_bypass_events +classes: +- Security::ApprovalPolicyMergeRequestBypassEvent +feature_categories: +- security_policy_management +description: Stores events of bypasses of approval policies for merge requests. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/199151 +milestone: '18.3' +gitlab_schema: gitlab_main_cell +sharding_key: + project_id: projects +table_size: small diff --git a/db/migrate/20250723205109_create_approval_policy_merge_request_bypass_events.rb b/db/migrate/20250723205109_create_approval_policy_merge_request_bypass_events.rb new file mode 100644 index 0000000000000000000000000000000000000000..e5da4e148bd5303adaad88015edb16eb2f1fba66 --- /dev/null +++ b/db/migrate/20250723205109_create_approval_policy_merge_request_bypass_events.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CreateApprovalPolicyMergeRequestBypassEvents < Gitlab::Database::Migration[2.3] + milestone '18.3' + + def up + create_table :approval_policy_merge_request_bypass_events do |t| + t.references :project, null: false, foreign_key: { on_delete: :cascade }, index: false + t.bigint :merge_request_id, null: false, + index: { name: 'index_approval_policy_merge_request_bypass_events_on_mr_id' } + t.bigint :security_policy_id, null: false, + index: { name: 'index_approval_policy_merge_request_bypass_events_on_policy_id' } + t.bigint :user_id, null: true, + index: { name: 'index_approval_policy_merge_request_bypass_events_on_user_id' } + + t.timestamps_with_timezone null: false + + # rubocop:disable Migration/AddLimitToTextColumns -- combined with check constraint + t.text :reason, null: false + t.check_constraint "length(trim(reason)) BETWEEN 1 AND 1024", + name: check_constraint_name(:approval_policy_merge_request_bypass_events, :reason, 'length_between_1_and_1024') + # rubocop:enable Migration/AddLimitToTextColumns + end + + add_index :approval_policy_merge_request_bypass_events, + [:project_id, :merge_request_id, :security_policy_id], + unique: true, + name: 'idx_approval_policy_mr_bypass_events_on_project_mr_policy' + end + + def down + drop_table :approval_policy_merge_request_bypass_events + end +end diff --git a/db/post_migrate/20250725154259_add_approval_policy_merge_request_bypass_events_fks.rb b/db/post_migrate/20250725154259_add_approval_policy_merge_request_bypass_events_fks.rb new file mode 100644 index 0000000000000000000000000000000000000000..f04edb659b14a6fa3228e618509847f21f20a612 --- /dev/null +++ b/db/post_migrate/20250725154259_add_approval_policy_merge_request_bypass_events_fks.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class AddApprovalPolicyMergeRequestBypassEventsFks < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.3' + + def up + add_concurrent_foreign_key :approval_policy_merge_request_bypass_events, :merge_requests, + column: :merge_request_id, + on_delete: :cascade + add_concurrent_foreign_key :approval_policy_merge_request_bypass_events, :security_policies, + column: :security_policy_id, + on_delete: :cascade + add_concurrent_foreign_key :approval_policy_merge_request_bypass_events, :users, column: :user_id, + on_delete: :nullify + end + + def down + with_lock_retries do + remove_foreign_key :approval_policy_merge_request_bypass_events, column: :merge_request_id + end + with_lock_retries do + remove_foreign_key :approval_policy_merge_request_bypass_events, column: :security_policy_id + end + with_lock_retries do + remove_foreign_key :approval_policy_merge_request_bypass_events, column: :user_id + end + end +end diff --git a/db/schema_migrations/20250723205109 b/db/schema_migrations/20250723205109 new file mode 100644 index 0000000000000000000000000000000000000000..6616ef75e68d6f098c09f3388dd356567c7d8805 --- /dev/null +++ b/db/schema_migrations/20250723205109 @@ -0,0 +1 @@ +c0f735594416c69457d4d55297c9c25b675118592e1fff534918a0d49d149d33 \ No newline at end of file diff --git a/db/schema_migrations/20250725154259 b/db/schema_migrations/20250725154259 new file mode 100644 index 0000000000000000000000000000000000000000..96484725a44a177758e545251355a975fbf850ae --- /dev/null +++ b/db/schema_migrations/20250725154259 @@ -0,0 +1 @@ +33a85d33bdff307f4eecebafe77c258d2d2c8d3286c075c71bb302508b39496b \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c0f91e4ef13b227abae0e9863339b60ef5e9b6b7..d0986196abd2f1004c6cbf36cb2a0028a1926fa7 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9950,6 +9950,27 @@ CREATE SEQUENCE approval_merge_request_rules_users_id_seq ALTER SEQUENCE approval_merge_request_rules_users_id_seq OWNED BY approval_merge_request_rules_users.id; +CREATE TABLE approval_policy_merge_request_bypass_events ( + id bigint NOT NULL, + project_id bigint NOT NULL, + merge_request_id bigint NOT NULL, + security_policy_id bigint NOT NULL, + user_id bigint, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + reason text NOT NULL, + CONSTRAINT check_3169f0d109 CHECK (((length(TRIM(BOTH FROM reason)) >= 1) AND (length(TRIM(BOTH FROM reason)) <= 1024))) +); + +CREATE SEQUENCE approval_policy_merge_request_bypass_events_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE approval_policy_merge_request_bypass_events_id_seq OWNED BY approval_policy_merge_request_bypass_events.id; + CREATE TABLE approval_policy_rule_project_links ( id bigint NOT NULL, project_id bigint NOT NULL, @@ -27690,6 +27711,8 @@ ALTER TABLE ONLY approval_merge_request_rules_groups ALTER COLUMN id SET DEFAULT ALTER TABLE ONLY approval_merge_request_rules_users ALTER COLUMN id SET DEFAULT nextval('approval_merge_request_rules_users_id_seq'::regclass); +ALTER TABLE ONLY approval_policy_merge_request_bypass_events ALTER COLUMN id SET DEFAULT nextval('approval_policy_merge_request_bypass_events_id_seq'::regclass); + ALTER TABLE ONLY approval_policy_rule_project_links ALTER COLUMN id SET DEFAULT nextval('approval_policy_rule_project_links_id_seq'::regclass); ALTER TABLE ONLY approval_policy_rules ALTER COLUMN id SET DEFAULT nextval('approval_policy_rules_id_seq'::regclass); @@ -29804,6 +29827,9 @@ ALTER TABLE ONLY approval_merge_request_rules ALTER TABLE ONLY approval_merge_request_rules_users ADD CONSTRAINT approval_merge_request_rules_users_pkey PRIMARY KEY (id); +ALTER TABLE ONLY approval_policy_merge_request_bypass_events + ADD CONSTRAINT approval_policy_merge_request_bypass_events_pkey PRIMARY KEY (id); + ALTER TABLE ONLY approval_policy_rule_project_links ADD CONSTRAINT approval_policy_rule_project_links_pkey PRIMARY KEY (id); @@ -34080,6 +34106,8 @@ CREATE INDEX idx_approval_merge_request_rules_on_scan_result_policy_id ON approv CREATE INDEX idx_approval_mr_rules_on_config_id_and_id_and_updated_at ON approval_merge_request_rules USING btree (security_orchestration_policy_configuration_id, id, updated_at); +CREATE UNIQUE INDEX idx_approval_policy_mr_bypass_events_on_project_mr_policy ON approval_policy_merge_request_bypass_events USING btree (project_id, merge_request_id, security_policy_id); + CREATE INDEX idx_approval_policy_rule_project_links_on_project_id_and_id ON approval_policy_rule_project_links USING btree (project_id, id); CREATE INDEX idx_approval_policy_rules_security_policy_id_id ON approval_policy_rules USING btree (security_policy_id, id); @@ -34912,6 +34940,12 @@ CREATE INDEX index_approval_merge_request_rules_users_2 ON approval_merge_reques CREATE INDEX index_approval_mr_rules_on_project_id_policy_rule_id_and_id ON approval_merge_request_rules USING btree (security_orchestration_policy_configuration_id, approval_policy_rule_id, id); +CREATE INDEX index_approval_policy_merge_request_bypass_events_on_mr_id ON approval_policy_merge_request_bypass_events USING btree (merge_request_id); + +CREATE INDEX index_approval_policy_merge_request_bypass_events_on_policy_id ON approval_policy_merge_request_bypass_events USING btree (security_policy_id); + +CREATE INDEX index_approval_policy_merge_request_bypass_events_on_user_id ON approval_policy_merge_request_bypass_events USING btree (user_id); + CREATE UNIQUE INDEX index_approval_policy_rule_on_project_and_rule ON approval_policy_rule_project_links USING btree (approval_policy_rule_id, project_id); CREATE INDEX index_approval_policy_rules_on_policy_management_project_id ON approval_policy_rules USING btree (security_policy_management_project_id); @@ -42894,6 +42928,9 @@ ALTER TABLE ONLY deployment_approvals ALTER TABLE ONLY project_relation_export_uploads ADD CONSTRAINT fk_0f7fad01a3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY approval_policy_merge_request_bypass_events + ADD CONSTRAINT fk_0fae251483 FOREIGN KEY (security_policy_id) REFERENCES security_policies(id) ON DELETE CASCADE; + ALTER TABLE ONLY board_assignees ADD CONSTRAINT fk_105c1d6d08 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -44205,6 +44242,9 @@ ALTER TABLE ONLY ml_candidates ALTER TABLE ONLY subscription_add_on_purchases ADD CONSTRAINT fk_a1db288990 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY approval_policy_merge_request_bypass_events + ADD CONSTRAINT fk_a24f768758 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY protected_environment_approval_rules ADD CONSTRAINT fk_a3cc825836 FOREIGN KEY (protected_environment_project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -44958,6 +44998,9 @@ ALTER TABLE ONLY zoekt_indices ALTER TABLE ONLY status_check_responses ADD CONSTRAINT fk_f3953d86c6 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; +ALTER TABLE ONLY approval_policy_merge_request_bypass_events + ADD CONSTRAINT fk_f39e177609 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; + ALTER TABLE ONLY user_group_member_roles ADD CONSTRAINT fk_f3b8fc5e4e FOREIGN KEY (shared_with_group_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -45327,6 +45370,9 @@ ALTER TABLE ONLY project_ci_feature_usages ALTER TABLE ONLY packages_tags ADD CONSTRAINT fk_rails_1dfc868911 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE; +ALTER TABLE ONLY approval_policy_merge_request_bypass_events + ADD CONSTRAINT fk_rails_1ebbdcc530 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY boards_epic_board_positions ADD CONSTRAINT fk_rails_1ecfd9f2de FOREIGN KEY (epic_id) REFERENCES epics(id) ON DELETE CASCADE; diff --git a/ee/app/models/ee/merge_request.rb b/ee/app/models/ee/merge_request.rb index 9312575c780dea4658f4909d894d7ee340f01303..f21f711e8a9996cb553a32a736baa921992dabdd 100644 --- a/ee/app/models/ee/merge_request.rb +++ b/ee/app/models/ee/merge_request.rb @@ -92,6 +92,8 @@ def set_applicable_when_copying_rules(applicable_ids) 'Security::ScanResultPolicyViolation', inverse_of: :merge_request has_many :merge_request_stage_events, class_name: 'Analytics::CycleAnalytics::MergeRequestStageEvent' + has_many :approval_policy_merge_request_bypass_events, + class_name: 'Security::ApprovalPolicyMergeRequestBypassEvent', inverse_of: :merge_request # WIP v2 approval rules as part of https://gitlab.com/groups/gitlab-org/-/epics/12955 has_many :v2_approval_rules_merge_requests, class_name: 'MergeRequests::ApprovalRulesMergeRequest', diff --git a/ee/app/models/security/approval_policy_merge_request_bypass_event.rb b/ee/app/models/security/approval_policy_merge_request_bypass_event.rb new file mode 100644 index 0000000000000000000000000000000000000000..bd13dc0ae835a0a1f0dd99e705e1a915edf0eed6 --- /dev/null +++ b/ee/app/models/security/approval_policy_merge_request_bypass_event.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Security + class ApprovalPolicyMergeRequestBypassEvent < ApplicationRecord + self.table_name = 'approval_policy_merge_request_bypass_events' + + belongs_to :project + belongs_to :security_policy, class_name: 'Security::Policy' + belongs_to :merge_request + belongs_to :user, optional: true + + validates :reason, presence: true, length: { maximum: 1024 } + validates_uniqueness_of :project_id, scope: [:merge_request_id, :security_policy_id] + end +end diff --git a/ee/app/models/security/policy.rb b/ee/app/models/security/policy.rb index 212bb27f294042a985330410f1c64d99707c2751..6e80a415a750dde5fa3f0bb7242c63c0ddebc81e 100644 --- a/ee/app/models/security/policy.rb +++ b/ee/app/models/security/policy.rb @@ -34,7 +34,11 @@ class Policy < ApplicationRecord has_many :projects, through: :security_policy_project_links - has_many :security_pipeline_execution_project_schedules, class_name: 'Security::PipelineExecutionProjectSchedule', + has_many :security_pipeline_execution_project_schedules, + class_name: 'Security::PipelineExecutionProjectSchedule', + foreign_key: :security_policy_id, inverse_of: :security_policy + has_many :approval_policy_merge_request_bypass_events, + class_name: 'Security::ApprovalPolicyMergeRequestBypassEvent', foreign_key: :security_policy_id, inverse_of: :security_policy enum :type, { diff --git a/ee/spec/factories/security/approval_policy_merge_request_bypass_events.rb b/ee/spec/factories/security/approval_policy_merge_request_bypass_events.rb new file mode 100644 index 0000000000000000000000000000000000000000..a9662e7ce17f97518cfa16d458552b351f1b53d8 --- /dev/null +++ b/ee/spec/factories/security/approval_policy_merge_request_bypass_events.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :approval_policy_merge_request_bypass_event, class: 'Security::ApprovalPolicyMergeRequestBypassEvent' do + project + security_policy + merge_request + user + reason { 'test' } + end +end diff --git a/ee/spec/models/merge_request_spec.rb b/ee/spec/models/merge_request_spec.rb index 51098914ebcefdc464861d7cc845a17cf541bb8b..54d2a39147edb558781eae65b73f35bdf39f43d1 100644 --- a/ee/spec/models/merge_request_spec.rb +++ b/ee/spec/models/merge_request_spec.rb @@ -26,6 +26,7 @@ it { is_expected.to have_many(:approved_by_users) } it { is_expected.to have_one(:merge_train_car) } it { is_expected.to have_many(:approval_rules) } + it { is_expected.to have_many(:approval_policy_merge_request_bypass_events) } it { is_expected.to have_many(:approval_merge_request_rule_sources).through(:approval_rules) } it { is_expected.to have_many(:approval_project_rules).through(:approval_merge_request_rule_sources) } it { is_expected.to have_many(:status_check_responses).class_name('MergeRequests::StatusCheckResponse').inverse_of(:merge_request) } diff --git a/ee/spec/models/security/approval_policy_merge_request_bypass_event_spec.rb b/ee/spec/models/security/approval_policy_merge_request_bypass_event_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ee9a8eca7136958fe70efcd139f16f983225e4df --- /dev/null +++ b/ee/spec/models/security/approval_policy_merge_request_bypass_event_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::ApprovalPolicyMergeRequestBypassEvent, feature_category: :security_policy_management do + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:security_policy) } + it { is_expected.to belong_to(:user).optional } + it { is_expected.to belong_to(:merge_request) } + end + + describe 'validations' do + subject { build(:approval_policy_merge_request_bypass_event, security_policy: create(:security_policy)) } + + it { is_expected.to validate_presence_of(:reason) } + it { is_expected.to validate_length_of(:reason).is_at_most(1024) } + it { is_expected.to validate_uniqueness_of(:project_id).scoped_to([:merge_request_id, :security_policy_id]) } + end +end diff --git a/ee/spec/models/security/policy_spec.rb b/ee/spec/models/security/policy_spec.rb index b6c98f9741d9240466c5210bebce635261d5a069..5c49c9e8645c26ce4f2484b06d6a49e0171af78d 100644 --- a/ee/spec/models/security/policy_spec.rb +++ b/ee/spec/models/security/policy_spec.rb @@ -12,6 +12,7 @@ it { is_expected.to have_many(:projects).through(:security_policy_project_links) } it { is_expected.to have_one(:security_pipeline_execution_policy_config_link) } it { is_expected.to have_many(:security_pipeline_execution_project_schedules) } + it { is_expected.to have_many(:approval_policy_merge_request_bypass_events) } it do is_expected.to validate_uniqueness_of(:security_orchestration_policy_configuration_id).scoped_to(%i[type diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 99095ac909c317fceeb023bb909f82e2a7bf010f..6a99724c7ff2656dde7bb7448e4cc96a325fffce 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -287,6 +287,7 @@ merge_requests: - running_scan_result_policy_violations - failed_scan_result_policy_violations - approval_metrics +- approval_policy_merge_request_bypass_events external_pull_requests: - project merge_request_diff: