From 006642716a9250db6010870be168e4c704dedaf8 Mon Sep 17 00:00:00 2001 From: Allison Browne Date: Thu, 26 Oct 2023 16:23:26 -0400 Subject: [PATCH 1/4] Disallow pipeline cancellation Add an ennummerable to persist what roles to restrict pipeline cancellation to. Add the policy logic to enforce the new restriction. Changelog: added EE: true Add specs for cancellation restriction update --- app/models/ci/project_mirror.rb | 2 +- app/policies/ci/pipeline_policy.rb | 15 +-- ...restrict_pipeline_cancellation_by_role.yml | 8 ++ ...d_pipeline_cancel_role_restriction_enum.rb | 18 +++ db/schema_migrations/20231024212214 | 1 + db/structure.sql | 9 +- ee/app/controllers/ee/projects_controller.rb | 1 + .../ci/project/cancellation_restriction.rb | 34 +++++ ee/app/models/ee/project.rb | 5 + ee/app/models/ee/project_ci_cd_setting.rb | 8 ++ .../models/gitlab_subscriptions/features.rb | 1 + ee/app/policies/ee/project_policy.rb | 11 ++ .../controllers/projects_controller_spec.rb | 36 +++++ .../project/cancellation_restriction_spec.rb | 126 ++++++++++++++++++ ee/spec/models/ee/project_spec.rb | 10 +- ee/spec/models/project_ci_cd_setting_spec.rb | 18 +++ ee/spec/policies/project_policy_spec.rb | 68 ++++++++++ 17 files changed, 358 insertions(+), 13 deletions(-) create mode 100644 config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml create mode 100644 db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb create mode 100644 db/schema_migrations/20231024212214 create mode 100644 ee/app/models/ci/project/cancellation_restriction.rb create mode 100644 ee/spec/models/ci/project/cancellation_restriction_spec.rb diff --git a/app/models/ci/project_mirror.rb b/app/models/ci/project_mirror.rb index 23cd5d92730a1d..19bb5d5d1596a1 100644 --- a/app/models/ci/project_mirror.rb +++ b/app/models/ci/project_mirror.rb @@ -6,7 +6,7 @@ module Ci class ProjectMirror < ApplicationRecord include FromUnion - belongs_to :project + belongs_to :project, class_name: '::Project' scope :by_namespace_id, -> (namespace_id) { where(namespace_id: namespace_id) } scope :by_project_id, -> (project_id) { where(project_id: project_id) } diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index bd6e580c33ae1b..3d43b38cd5df1d 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -27,10 +27,14 @@ class PipelinePolicy < BasePolicy prevent :read_pipeline end - rule { protected_ref }.prevent :update_pipeline + rule { protected_ref }.policy do + prevent :update_pipeline + enable :cancel_pipeline + end rule { can?(:public_access) & branch_allows_collaboration }.policy do enable :update_pipeline + enable :cancel_pipeline end rule { can?(:owner_access) }.policy do @@ -45,15 +49,6 @@ class PipelinePolicy < BasePolicy enable :read_pipeline_variable end - # TODO: splitting out cancel from update in Issue #20207 - rule { can?(:update_pipeline) }.policy do - enable :cancel_pipeline - end - - rule { ~can?(:update_pipeline) }.policy do - prevent :cancel_pipeline - end - rule { project_allows_read_dependency }.policy do enable :read_dependency end diff --git a/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml b/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml new file mode 100644 index 00000000000000..0ef8a5d38dbec3 --- /dev/null +++ b/config/feature_flags/development/restrict_pipeline_cancellation_by_role.yml @@ -0,0 +1,8 @@ +--- +name: restrict_pipeline_cancellation_by_role +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135047 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429699 +milestone: '16.6' +type: development +group: group::pipeline execution +default_enabled: false diff --git a/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb b/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb new file mode 100644 index 00000000000000..a1ba7a287a91e7 --- /dev/null +++ b/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddPipelineCancelRoleRestrictionEnum < Gitlab::Database::Migration[2.1] + def up + create_enum :restrict_pipeline_cancellation_role, %w[developer maintainer no_one] + + add_column :project_ci_cd_settings, :restrict_pipeline_cancellation_role, :enum, + enum_type: "restrict_pipeline_cancellation_role", default: 'developer', null: false + end + + def down + remove_column :project_ci_cd_settings, :restrict_pipeline_cancellation_role + + execute <<-SQL + DROP TYPE restrict_pipeline_cancellation_role + SQL + end +end diff --git a/db/schema_migrations/20231024212214 b/db/schema_migrations/20231024212214 new file mode 100644 index 00000000000000..d3ad27bd4dd9c6 --- /dev/null +++ b/db/schema_migrations/20231024212214 @@ -0,0 +1 @@ +c5884c327b3be31122ca36302f8fbd36666ddee07229480884c8c64af825c03f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0da8e0b7ce14b6..0e64a6bc785ae0 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10,6 +10,12 @@ CREATE EXTENSION IF NOT EXISTS btree_gist; CREATE EXTENSION IF NOT EXISTS pg_trgm; +CREATE TYPE restrict_pipeline_cancellation_role AS ENUM ( + 'developer', + 'maintainer', + 'no_one' +); + CREATE FUNCTION assign_p_ci_builds_id_value() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -21368,7 +21374,8 @@ CREATE TABLE project_ci_cd_settings ( allow_fork_pipelines_to_run_in_parent_project boolean DEFAULT true NOT NULL, inbound_job_token_scope_enabled boolean DEFAULT true NOT NULL, forward_deployment_rollback_allowed boolean DEFAULT true NOT NULL, - merge_trains_skip_train_allowed boolean DEFAULT false NOT NULL + merge_trains_skip_train_allowed boolean DEFAULT false NOT NULL, + restrict_pipeline_cancellation_role restrict_pipeline_cancellation_role DEFAULT 'developer'::restrict_pipeline_cancellation_role NOT NULL ); CREATE SEQUENCE project_ci_cd_settings_id_seq diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb index e901a242fa2590..0a49ce6ff4677e 100644 --- a/ee/app/controllers/ee/projects_controller.rb +++ b/ee/app/controllers/ee/projects_controller.rb @@ -130,6 +130,7 @@ def project_params_ee attrs << %i[merge_pipelines_enabled] if allow_merge_pipelines_params? attrs << %i[merge_trains_enabled] if allow_merge_trains_params? attrs << %i[merge_trains_skip_train_allowed] if allow_merge_trains_params? + attrs << %i[restrict_pipeline_cancellation_role] if project.ci_cancellation_restriction.enabled? attrs += merge_request_rules_params diff --git a/ee/app/models/ci/project/cancellation_restriction.rb b/ee/app/models/ci/project/cancellation_restriction.rb new file mode 100644 index 00000000000000..fc5a5cacf10e0a --- /dev/null +++ b/ee/app/models/ci/project/cancellation_restriction.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Ci + module Project + class CancellationRestriction + # Answers if cancellation restrictions are applied for piplines and processables + # based on the project + include Gitlab::Utils::StrongMemoize + + def initialize(project) + @project = project + @ci_settings = project.ci_cd_settings + end + + def maintainers_only_allowed? + return false unless enabled? + + @ci_settings.restrict_pipeline_cancellation_role_maintainer? + end + + def no_one_allowed? + return false unless enabled? + + @ci_settings.restrict_pipeline_cancellation_role_no_one? + end + + def enabled? + Feature.enabled?(:restrict_pipeline_cancellation_by_role, @project) && + @project.licensed_feature_available?(:ci_pipeline_cancellation_restrictions) + end + strong_memoize_attr :enabled? + end + end +end diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index 5c8deff985cd96..2778cbc203d1f7 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -342,6 +342,7 @@ def lock_for_confirmation!(id) delegate :merge_pipelines_enabled, :merge_pipelines_enabled=, to: :ci_cd_settings, allow_nil: true delegate :merge_trains_enabled, :merge_trains_enabled=, to: :ci_cd_settings, allow_nil: true delegate :merge_trains_skip_train_allowed, :merge_trains_skip_train_allowed=, to: :ci_cd_settings, allow_nil: true + delegate :restrict_pipeline_cancellation_role=, to: :ci_cd_settings, allow_nil: false delegate :auto_rollback_enabled, :auto_rollback_enabled=, to: :ci_cd_settings, allow_nil: true @@ -1101,6 +1102,10 @@ def downstream_projects_count downstream_project_subscriptions.count end + def ci_cancellation_restriction + ::Ci::Project::CancellationRestriction.new(self) + end + def merge_pipelines_enabled? return false unless ci_cd_settings diff --git a/ee/app/models/ee/project_ci_cd_setting.rb b/ee/app/models/ee/project_ci_cd_setting.rb index eefd0e7985c102..2150758ec28081 100644 --- a/ee/app/models/ee/project_ci_cd_setting.rb +++ b/ee/app/models/ee/project_ci_cd_setting.rb @@ -4,6 +4,14 @@ module EE module ProjectCiCdSetting extend ActiveSupport::Concern + prepended do + enum restrict_pipeline_cancellation_role: { + developer: 'developer', + maintainer: 'maintainer', + no_one: 'no_one' + }, _prefix: true + end + def merge_pipelines_enabled? project.feature_available?(:merge_pipelines) && super end diff --git a/ee/app/models/gitlab_subscriptions/features.rb b/ee/app/models/gitlab_subscriptions/features.rb index 4bf50f63407f10..48e796f868c06f 100644 --- a/ee/app/models/gitlab_subscriptions/features.rb +++ b/ee/app/models/gitlab_subscriptions/features.rb @@ -83,6 +83,7 @@ class Features ci_cd_projects ci_namespace_catalog ci_secrets_management + ci_pipeline_cancellation_restrictions cluster_agents_ci_impersonation cluster_agents_user_impersonation cluster_deployments diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 3ee7600382ada9..dcd85ef2555e49 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -331,6 +331,14 @@ module ProjectPolicy (custom_roles_allowed? && merge_requests_is_a_private_feature? && role_enables_admin_merge_request?)) end + condition(:ci_cancellation_maintainers_only, scope: :subject) do + project.ci_cancellation_restriction.maintainers_only_allowed? + end + + condition(:ci_cancellation_no_one, scope: :subject) do + project.ci_cancellation_restriction.no_one_allowed? + end + rule { visual_review_bot }.policy do prevent :read_note enable :create_note @@ -769,6 +777,9 @@ module ProjectPolicy rule { can?(:reporter_access) & observability_metrics_enabled }.policy do enable :read_observability_metrics end + + rule { ci_cancellation_maintainers_only & ~can?(:maintainer_access) }.prevent :cancel_pipeline + rule { ci_cancellation_no_one }.prevent :cancel_pipeline end override :lookup_access_level! diff --git a/ee/spec/controllers/projects_controller_spec.rb b/ee/spec/controllers/projects_controller_spec.rb index 4a062fc9222838..f34eb77a939a84 100644 --- a/ee/spec/controllers/projects_controller_spec.rb +++ b/ee/spec/controllers/projects_controller_spec.rb @@ -382,6 +382,42 @@ end end + context 'when restrict_pipeline_cancellation_role is specified' do + let(:settings) { project.ci_cd_settings } + let(:params) { { restrict_pipeline_cancellation_role: settings.class.restrict_pipeline_cancellation_roles[:maintainer] } } + let(:request) do + put :update, params: { namespace_id: project.namespace, id: project, project: params } + project.reload + end + + let(:maintainer_restriction) { settings.restrict_pipeline_cancellation_role_maintainer? } + let(:developer_restriction) { settings.restrict_pipeline_cancellation_role_developer? } + + context 'when the feature is enabled' do + before do + allow_next_instance_of(Ci::Project::CancellationRestriction) do |cr| + allow(cr).to receive(:enabled?).and_return(true) + end + end + + it 'updates the parameter' do + expect { request }.to change { project.reload.ci_cd_settings.restrict_pipeline_cancellation_role_maintainer? }.from(false).to(true) + end + end + + context 'when the feature is disabled' do + before do + allow_next_instance_of(Ci::Project::CancellationRestriction) do |cr| + allow(cr).to receive(:enabled?).and_return(false) + end + end + + it 'will not update the parameter' do + expect { request }.not_to change { developer_restriction } + end + end + end + context 'when only_allow_merge_if_all_status_checks_passed param is specified' do let(:params) { { project_setting_attributes: { only_allow_merge_if_all_status_checks_passed: true } } } diff --git a/ee/spec/models/ci/project/cancellation_restriction_spec.rb b/ee/spec/models/ci/project/cancellation_restriction_spec.rb new file mode 100644 index 00000000000000..1dcc7767036061 --- /dev/null +++ b/ee/spec/models/ci/project/cancellation_restriction_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Project::CancellationRestriction, feature_category: :continuous_integration do + let(:project) { create_default(:project) } + let(:cancellation_restriction) { described_class.new(project) } + let(:settings) { project.ci_cd_settings } + let(:roles) { settings.class.restrict_pipeline_cancellation_roles } + + describe '#maintainers_only_allowed?' do + context 'when cancellation restrictions are enabled' do + before do + stub_enabled + end + + it 'returns true if maintainers are the only ones allowed to cancel' do + settings.update!(restrict_pipeline_cancellation_role: roles[:maintainer]) + + expect(cancellation_restriction.maintainers_only_allowed?).to be_truthy + end + + [:no_one, :developer].each do |role| + it "returns false if #{role} is allowed to cancel" do + settings.update!(restrict_pipeline_cancellation_role: role) + + expect(cancellation_restriction.maintainers_only_allowed?).to be_falsy + end + end + end + + context 'when cancellation restrictions are disabled' do + before do + stub_disabled + end + + it 'returns false' do + expect(cancellation_restriction.maintainers_only_allowed?).to be_falsy + end + end + end + + describe '#no_one_allowed?' do + context 'when cancellation restrictions are enabled' do + before do + stub_enabled + end + + it 'returns true if no one is allowed to cancel' do + settings.update!(restrict_pipeline_cancellation_role: roles[:no_one]) + + expect(cancellation_restriction.no_one_allowed?).to be_truthy + end + + [:maintainer, :developer].each do |role| + it "returns false if #{role} is allowed to cancel" do + settings.update!(restrict_pipeline_cancellation_role: role) + + expect(cancellation_restriction.no_one_allowed?).to be_falsy + end + end + end + + context 'when cancellation restrictions are disabled' do + before do + stub_disabled + end + + it 'returns false' do + expect(cancellation_restriction.no_one_allowed?).to be_falsy + end + end + end + + describe '#enabled?' do + context 'when the feature is enabled and licensed' do + before do + stub_enabled + end + + it 'returns true' do + expect(cancellation_restriction.enabled?).to be_truthy + end + end + + context 'when the feature is disabled' do + before do + stub_feature_flags(restrict_pipeline_cancellation_by_role: false) + stub_licensed_features(ci_pipeline_cancellation_restrictions: true) + end + + it 'returns false' do + expect(cancellation_restriction.enabled?).to be_falsy + end + end + + context 'when the feature is enabled but not licensed' do + before do + stub_licensed_features(ci_pipeline_cancellation_restrictions: false) + end + + it 'returns false' do + expect(cancellation_restriction.enabled?).to be_falsy + end + end + + context 'when the feature is disabled and not licensed' do + before do + stub_disabled + end + + it 'returns false' do + expect(cancellation_restriction.enabled?).to be_falsy + end + end + end + + def stub_enabled + stub_licensed_features(ci_pipeline_cancellation_restrictions: true) + end + + def stub_disabled + stub_feature_flags(restrict_pipeline_cancellation_by_role: false) + stub_licensed_features(ci_pipeline_cancellation_restrictions: false) + end +end diff --git a/ee/spec/models/ee/project_spec.rb b/ee/spec/models/ee/project_spec.rb index 93d75fb0869733..42103dda90a890 100644 --- a/ee/spec/models/ee/project_spec.rb +++ b/ee/spec/models/ee/project_spec.rb @@ -93,7 +93,8 @@ 'auto_rollback_enabled' => '', 'merge_pipelines_enabled' => '', 'merge_trains_enabled' => '', - 'merge_trains_skip_train_allowed' => '' + 'merge_trains_skip_train_allowed' => '', + 'restrict_pipeline_cancellation_role=' => '' } end @@ -3575,6 +3576,13 @@ def stub_default_url_options(host) end end + describe '#ci_cancellation_restriction' do + it 'returns the initalized cancellation restrication object' do + expect(project.ci_cancellation_restriction.class).to be Ci::Project::CancellationRestriction + expect(project.ci_cancellation_restriction).to respond_to(:enabled?) + end + end + describe '#visible_approval_rules' do let(:scan_finding_rule) { create(:approval_project_rule, :scan_finding, project: project) } diff --git a/ee/spec/models/project_ci_cd_setting_spec.rb b/ee/spec/models/project_ci_cd_setting_spec.rb index 89cc8f9152967d..3f26577dcfdcdf 100644 --- a/ee/spec/models/project_ci_cd_setting_spec.rb +++ b/ee/spec/models/project_ci_cd_setting_spec.rb @@ -5,8 +5,26 @@ RSpec.describe ProjectCiCdSetting, feature_category: :continuous_integration do using RSpec::Parameterized::TableSyntax + let_it_be(:project) { create_default(:project) } + let(:settings) { project.reload.ci_cd_settings } + it { is_expected.to validate_inclusion_of(:merge_trains_skip_train_allowed).in_array([true, false]) } + describe '#restrict_pipeline_cancellation_role' do + it 'defines an enum' do + described_class.restrict_pipeline_cancellation_roles.each_key do |role| + settings.update!(restrict_pipeline_cancellation_role: role) + expect(settings.restrict_pipeline_cancellation_role).to eq role + end + end + + it 'defaults to developer' do + expect(settings.restrict_pipeline_cancellation_role).to eq( + self.class.restrict_pipeline_cancellation_role[:developer] + ) + end + end + describe '#merge_pipelines_enabled?' do subject { project.merge_pipelines_enabled? } diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 534112338d1dd9..6e20b25c351361 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -1679,6 +1679,74 @@ end end + describe 'prevents cancel pipeline when cancellation is restricted' do + using RSpec::Parameterized::TableSyntax + + let(:ability) { :cancel_pipeline } + + context 'when feature is enabled' do + where(:restricted_role, :actual_role, :allowed) do + :developer | :guest | false + :developer | :reporter | false + :developer | :developer | true + :developer | :maintainer | true + :developer | :owner | true + :maintainer | :guest | false + :maintainer | :reporter | false + :maintainer | :developer | false + :maintainer | :maintainer | true + :maintainer | :owner | true + :no_one | :guest | false + :no_one | :reporter | false + :no_one | :developer | false + :no_one | :maintainer | false + :no_one | :owner | false + end + + with_them do + let(:current_user) { public_send(actual_role) } + + before do + stub_licensed_features(ci_pipeline_cancellation_restrictions: true) + project.update!(restrict_pipeline_cancellation_role: restricted_role) + end + + it { is_expected.to(allowed ? be_allowed(ability) : be_disallowed(ability)) } + end + end + + context 'when feature is disabled' do + where(:restricted_role, :actual_role, :allowed) do + :developer | :guest | false + :developer | :reporter | false + :developer | :developer | true + :developer | :maintainer | true + :developer | :owner | true + :maintainer | :guest | false + :maintainer | :reporter | false + :maintainer | :developer | true + :maintainer | :maintainer | true + :maintainer | :owner | true + :no_one | :guest | false + :no_one | :reporter | false + :no_one | :developer | true + :no_one | :maintainer | true + :no_one | :owner | true + end + + with_them do + let(:current_user) { public_send(actual_role) } + + before do + stub_feature_flags(restrict_pipeline_cancellation_by_role: false) + project.update!(restrict_pipeline_cancellation_role: restricted_role) + end + + it { is_expected.to(allowed ? be_allowed(ability) : be_disallowed(ability)) } + end + end + end + describe ':compliance_framework_available' do using RSpec::Parameterized::TableSyntax -- GitLab From 79d76da75edd227e8c23b5a8a7acb63ddadfaa39 Mon Sep 17 00:00:00 2001 From: Allison Browne Date: Tue, 31 Oct 2023 10:14:14 -0400 Subject: [PATCH 2/4] Remove namespacing because of failures When we add a new Ci::Project namespace in models the belongs_to :project within Ci namespaced classes becomes ambigious and tests are failing across the application because the belongs_to associations were expecting a class_name: now. Here we remove the namespace to avoid making more far reaching changes to the applicaiton. Fix some specs --- app/models/ci/project_mirror.rb | 2 +- app/policies/ci/pipeline_policy.rb | 2 +- app/services/ci/cancel_pipeline_service.rb | 13 ++++--- ee/app/controllers/ee/projects_controller.rb | 2 +- .../ci/project/cancellation_restriction.rb | 34 ------------------- .../ci/project_cancellation_restriction.rb | 32 +++++++++++++++++ ee/app/models/ee/project.rb | 4 +-- .../controllers/projects_controller_spec.rb | 4 +-- ... project_cancellation_restriction_spec.rb} | 2 +- ee/spec/models/ee/project_spec.rb | 2 +- ee/spec/models/project_ci_cd_setting_spec.rb | 2 +- spec/models/project_spec.rb | 1 + spec/requests/api/project_attributes.yml | 1 + 13 files changed, 52 insertions(+), 49 deletions(-) delete mode 100644 ee/app/models/ci/project/cancellation_restriction.rb create mode 100644 ee/app/models/ci/project_cancellation_restriction.rb rename ee/spec/models/ci/{project/cancellation_restriction_spec.rb => project_cancellation_restriction_spec.rb} (97%) diff --git a/app/models/ci/project_mirror.rb b/app/models/ci/project_mirror.rb index 19bb5d5d1596a1..23cd5d92730a1d 100644 --- a/app/models/ci/project_mirror.rb +++ b/app/models/ci/project_mirror.rb @@ -6,7 +6,7 @@ module Ci class ProjectMirror < ApplicationRecord include FromUnion - belongs_to :project, class_name: '::Project' + belongs_to :project scope :by_namespace_id, -> (namespace_id) { where(namespace_id: namespace_id) } scope :by_project_id, -> (project_id) { where(project_id: project_id) } diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 3d43b38cd5df1d..c01162a86dfbb5 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -29,7 +29,7 @@ class PipelinePolicy < BasePolicy rule { protected_ref }.policy do prevent :update_pipeline - enable :cancel_pipeline + prevent :cancel_pipeline end rule { can?(:public_access) & branch_allows_collaboration }.policy do diff --git a/app/services/ci/cancel_pipeline_service.rb b/app/services/ci/cancel_pipeline_service.rb index 52d38520fc3d90..38053b1392110f 100644 --- a/app/services/ci/cancel_pipeline_service.rb +++ b/app/services/ci/cancel_pipeline_service.rb @@ -24,11 +24,7 @@ def initialize( end def execute - unless can?(current_user, :cancel_pipeline, pipeline) - return ServiceResponse.error( - message: 'Insufficient permissions to cancel the pipeline', - reason: :insufficient_permissions) - end + return permission_error_response unless can?(current_user, :cancel_pipeline, pipeline) force_execute end @@ -103,6 +99,13 @@ def cancel_job(job) job.cancel end + def permission_error_response + ServiceResponse.error( + message: 'Insufficient permissions to cancel the pipeline', + reason: :insufficient_permissions + ) + end + # For parent child-pipelines only (not multi-project) def cancel_children pipeline.all_child_pipelines.each do |child_pipeline| diff --git a/ee/app/controllers/ee/projects_controller.rb b/ee/app/controllers/ee/projects_controller.rb index 0a49ce6ff4677e..b9c2c63c5d5aaa 100644 --- a/ee/app/controllers/ee/projects_controller.rb +++ b/ee/app/controllers/ee/projects_controller.rb @@ -130,7 +130,7 @@ def project_params_ee attrs << %i[merge_pipelines_enabled] if allow_merge_pipelines_params? attrs << %i[merge_trains_enabled] if allow_merge_trains_params? attrs << %i[merge_trains_skip_train_allowed] if allow_merge_trains_params? - attrs << %i[restrict_pipeline_cancellation_role] if project.ci_cancellation_restriction.enabled? + attrs << %i[restrict_pipeline_cancellation_role] if project&.ci_cancellation_restriction&.enabled? attrs += merge_request_rules_params diff --git a/ee/app/models/ci/project/cancellation_restriction.rb b/ee/app/models/ci/project/cancellation_restriction.rb deleted file mode 100644 index fc5a5cacf10e0a..00000000000000 --- a/ee/app/models/ci/project/cancellation_restriction.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Ci - module Project - class CancellationRestriction - # Answers if cancellation restrictions are applied for piplines and processables - # based on the project - include Gitlab::Utils::StrongMemoize - - def initialize(project) - @project = project - @ci_settings = project.ci_cd_settings - end - - def maintainers_only_allowed? - return false unless enabled? - - @ci_settings.restrict_pipeline_cancellation_role_maintainer? - end - - def no_one_allowed? - return false unless enabled? - - @ci_settings.restrict_pipeline_cancellation_role_no_one? - end - - def enabled? - Feature.enabled?(:restrict_pipeline_cancellation_by_role, @project) && - @project.licensed_feature_available?(:ci_pipeline_cancellation_restrictions) - end - strong_memoize_attr :enabled? - end - end -end diff --git a/ee/app/models/ci/project_cancellation_restriction.rb b/ee/app/models/ci/project_cancellation_restriction.rb new file mode 100644 index 00000000000000..8f2f2b1c8149c0 --- /dev/null +++ b/ee/app/models/ci/project_cancellation_restriction.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Ci + class ProjectCancellationRestriction + # Answers if cancellation restrictions are applied for piplines and processables + # based on the given project + include Gitlab::Utils::StrongMemoize + + def initialize(project) + @project = project + @ci_settings = project.ci_cd_settings + end + + def maintainers_only_allowed? + return false unless enabled? + + @ci_settings.restrict_pipeline_cancellation_role_maintainer? + end + + def no_one_allowed? + return false unless enabled? + + @ci_settings.restrict_pipeline_cancellation_role_no_one? + end + + def enabled? + Feature.enabled?(:restrict_pipeline_cancellation_by_role, @project) && + @project.licensed_feature_available?(:ci_pipeline_cancellation_restrictions) + end + strong_memoize_attr :enabled? + end +end diff --git a/ee/app/models/ee/project.rb b/ee/app/models/ee/project.rb index 2778cbc203d1f7..320abfc99f7da3 100644 --- a/ee/app/models/ee/project.rb +++ b/ee/app/models/ee/project.rb @@ -342,7 +342,7 @@ def lock_for_confirmation!(id) delegate :merge_pipelines_enabled, :merge_pipelines_enabled=, to: :ci_cd_settings, allow_nil: true delegate :merge_trains_enabled, :merge_trains_enabled=, to: :ci_cd_settings, allow_nil: true delegate :merge_trains_skip_train_allowed, :merge_trains_skip_train_allowed=, to: :ci_cd_settings, allow_nil: true - delegate :restrict_pipeline_cancellation_role=, to: :ci_cd_settings, allow_nil: false + delegate :restrict_pipeline_cancellation_role, :restrict_pipeline_cancellation_role=, to: :ci_cd_settings, allow_nil: false delegate :auto_rollback_enabled, :auto_rollback_enabled=, to: :ci_cd_settings, allow_nil: true @@ -1103,7 +1103,7 @@ def downstream_projects_count end def ci_cancellation_restriction - ::Ci::Project::CancellationRestriction.new(self) + ::Ci::ProjectCancellationRestriction.new(self) end def merge_pipelines_enabled? diff --git a/ee/spec/controllers/projects_controller_spec.rb b/ee/spec/controllers/projects_controller_spec.rb index f34eb77a939a84..bba65ca898d347 100644 --- a/ee/spec/controllers/projects_controller_spec.rb +++ b/ee/spec/controllers/projects_controller_spec.rb @@ -395,7 +395,7 @@ context 'when the feature is enabled' do before do - allow_next_instance_of(Ci::Project::CancellationRestriction) do |cr| + allow_next_instance_of(Ci::ProjectCancellationRestriction) do |cr| allow(cr).to receive(:enabled?).and_return(true) end end @@ -407,7 +407,7 @@ context 'when the feature is disabled' do before do - allow_next_instance_of(Ci::Project::CancellationRestriction) do |cr| + allow_next_instance_of(Ci::ProjectCancellationRestriction) do |cr| allow(cr).to receive(:enabled?).and_return(false) end end diff --git a/ee/spec/models/ci/project/cancellation_restriction_spec.rb b/ee/spec/models/ci/project_cancellation_restriction_spec.rb similarity index 97% rename from ee/spec/models/ci/project/cancellation_restriction_spec.rb rename to ee/spec/models/ci/project_cancellation_restriction_spec.rb index 1dcc7767036061..26f57ad4a94037 100644 --- a/ee/spec/models/ci/project/cancellation_restriction_spec.rb +++ b/ee/spec/models/ci/project_cancellation_restriction_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::Project::CancellationRestriction, feature_category: :continuous_integration do +RSpec.describe Ci::ProjectCancellationRestriction, feature_category: :continuous_integration do let(:project) { create_default(:project) } let(:cancellation_restriction) { described_class.new(project) } let(:settings) { project.ci_cd_settings } diff --git a/ee/spec/models/ee/project_spec.rb b/ee/spec/models/ee/project_spec.rb index 42103dda90a890..d9cc69b5c6522a 100644 --- a/ee/spec/models/ee/project_spec.rb +++ b/ee/spec/models/ee/project_spec.rb @@ -3578,7 +3578,7 @@ def stub_default_url_options(host) describe '#ci_cancellation_restriction' do it 'returns the initalized cancellation restrication object' do - expect(project.ci_cancellation_restriction.class).to be Ci::Project::CancellationRestriction + expect(project.ci_cancellation_restriction.class).to be Ci::ProjectCancellationRestriction expect(project.ci_cancellation_restriction).to respond_to(:enabled?) end end diff --git a/ee/spec/models/project_ci_cd_setting_spec.rb b/ee/spec/models/project_ci_cd_setting_spec.rb index 3f26577dcfdcdf..da788a6e04fecc 100644 --- a/ee/spec/models/project_ci_cd_setting_spec.rb +++ b/ee/spec/models/project_ci_cd_setting_spec.rb @@ -20,7 +20,7 @@ it 'defaults to developer' do expect(settings.restrict_pipeline_cancellation_role).to eq( - self.class.restrict_pipeline_cancellation_role[:developer] + described_class.restrict_pipeline_cancellation_roles[:developer] ) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5f1054a35bbae8..290f557cdd67da 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1182,6 +1182,7 @@ merge_trains_enabled auto_rollback_enabled merge_trains_skip_train_allowed + restrict_pipeline_cancellation_role ] end end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index ec98df22af792b..165ea7bf66e36a 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -98,6 +98,7 @@ ci_cd_settings: - merge_pipelines_enabled - auto_rollback_enabled - inbound_job_token_scope_enabled + - restrict_pipeline_cancellation_role remapped_attributes: default_git_depth: ci_default_git_depth forward_deployment_enabled: ci_forward_deployment_enabled -- GitLab From 6389294a6a41f9dccd4c00e5d63d3a0840001479 Mon Sep 17 00:00:00 2001 From: Allison Browne Date: Thu, 2 Nov 2023 14:09:16 -0400 Subject: [PATCH 3/4] Convert to a smallint from enum --- ..._add_pipeline_cancel_role_restriction_enum.rb | 10 ++-------- db/structure.sql | 8 +------- ee/app/models/ee/project_ci_cd_setting.rb | 6 +++--- ee/app/policies/ee/project_policy.rb | 11 +++++++++-- ee/spec/controllers/projects_controller_spec.rb | 2 +- ee/spec/models/ee/project_spec.rb | 12 +++++++++--- ee/spec/models/project_ci_cd_setting_spec.rb | 4 +--- ee/spec/policies/project_policy_spec.rb | 16 +++++++++++++--- spec/models/project_ci_cd_setting_spec.rb | 2 ++ 9 files changed, 41 insertions(+), 30 deletions(-) diff --git a/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb b/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb index a1ba7a287a91e7..ab26a1d783be2c 100644 --- a/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb +++ b/db/migrate/20231024212214_add_pipeline_cancel_role_restriction_enum.rb @@ -2,17 +2,11 @@ class AddPipelineCancelRoleRestrictionEnum < Gitlab::Database::Migration[2.1] def up - create_enum :restrict_pipeline_cancellation_role, %w[developer maintainer no_one] - - add_column :project_ci_cd_settings, :restrict_pipeline_cancellation_role, :enum, - enum_type: "restrict_pipeline_cancellation_role", default: 'developer', null: false + add_column :project_ci_cd_settings, :restrict_pipeline_cancellation_role, + :integer, limit: 2, default: 0, null: false end def down remove_column :project_ci_cd_settings, :restrict_pipeline_cancellation_role - - execute <<-SQL - DROP TYPE restrict_pipeline_cancellation_role - SQL end end diff --git a/db/structure.sql b/db/structure.sql index 0e64a6bc785ae0..c2a98c75d49fc5 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10,12 +10,6 @@ CREATE EXTENSION IF NOT EXISTS btree_gist; CREATE EXTENSION IF NOT EXISTS pg_trgm; -CREATE TYPE restrict_pipeline_cancellation_role AS ENUM ( - 'developer', - 'maintainer', - 'no_one' -); - CREATE FUNCTION assign_p_ci_builds_id_value() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -21375,7 +21369,7 @@ CREATE TABLE project_ci_cd_settings ( inbound_job_token_scope_enabled boolean DEFAULT true NOT NULL, forward_deployment_rollback_allowed boolean DEFAULT true NOT NULL, merge_trains_skip_train_allowed boolean DEFAULT false NOT NULL, - restrict_pipeline_cancellation_role restrict_pipeline_cancellation_role DEFAULT 'developer'::restrict_pipeline_cancellation_role NOT NULL + restrict_pipeline_cancellation_role smallint DEFAULT 0 NOT NULL ); CREATE SEQUENCE project_ci_cd_settings_id_seq diff --git a/ee/app/models/ee/project_ci_cd_setting.rb b/ee/app/models/ee/project_ci_cd_setting.rb index 2150758ec28081..b965e12023b818 100644 --- a/ee/app/models/ee/project_ci_cd_setting.rb +++ b/ee/app/models/ee/project_ci_cd_setting.rb @@ -6,9 +6,9 @@ module ProjectCiCdSetting prepended do enum restrict_pipeline_cancellation_role: { - developer: 'developer', - maintainer: 'maintainer', - no_one: 'no_one' + developer: 0, + maintainer: 1, + no_one: 2 }, _prefix: true end diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index dcd85ef2555e49..8f5dbfde6d4afa 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -778,8 +778,15 @@ module ProjectPolicy enable :read_observability_metrics end - rule { ci_cancellation_maintainers_only & ~can?(:maintainer_access) }.prevent :cancel_pipeline - rule { ci_cancellation_no_one }.prevent :cancel_pipeline + rule { ci_cancellation_maintainers_only & ~can?(:maintainer_access) }.policy do + prevent :cancel_pipeline + prevent :cancel_build + end + + rule { ci_cancellation_no_one }.policy do + prevent :cancel_pipeline + prevent :cancel_build + end end override :lookup_access_level! diff --git a/ee/spec/controllers/projects_controller_spec.rb b/ee/spec/controllers/projects_controller_spec.rb index bba65ca898d347..8ed0496151c64d 100644 --- a/ee/spec/controllers/projects_controller_spec.rb +++ b/ee/spec/controllers/projects_controller_spec.rb @@ -384,7 +384,7 @@ context 'when restrict_pipeline_cancellation_role is specified' do let(:settings) { project.ci_cd_settings } - let(:params) { { restrict_pipeline_cancellation_role: settings.class.restrict_pipeline_cancellation_roles[:maintainer] } } + let(:params) { { restrict_pipeline_cancellation_role: :maintainer } } let(:request) do put :update, params: { namespace_id: project.namespace, id: project, project: params } project.reload diff --git a/ee/spec/models/ee/project_spec.rb b/ee/spec/models/ee/project_spec.rb index d9cc69b5c6522a..693ac306871661 100644 --- a/ee/spec/models/ee/project_spec.rb +++ b/ee/spec/models/ee/project_spec.rb @@ -93,14 +93,20 @@ 'auto_rollback_enabled' => '', 'merge_pipelines_enabled' => '', 'merge_trains_enabled' => '', - 'merge_trains_skip_train_allowed' => '', - 'restrict_pipeline_cancellation_role=' => '' + 'merge_trains_skip_train_allowed' => '' } end - let(:exclude_attributes) { [] } + let(:exclude_attributes) do + [ + 'restrict_pipeline_cancellation_role' + ] + end end + # can't be tested above because it can be nil + it { is_expected.to delegate_method(:restrict_pipeline_cancellation_role).to(:ci_cd_settings) } + describe '#merge_pipelines_enabled?' do it_behaves_like 'a ci_cd_settings predicate method' do let(:delegated_method) { :merge_pipelines_enabled? } diff --git a/ee/spec/models/project_ci_cd_setting_spec.rb b/ee/spec/models/project_ci_cd_setting_spec.rb index da788a6e04fecc..2fc9db61ff8c89 100644 --- a/ee/spec/models/project_ci_cd_setting_spec.rb +++ b/ee/spec/models/project_ci_cd_setting_spec.rb @@ -19,9 +19,7 @@ end it 'defaults to developer' do - expect(settings.restrict_pipeline_cancellation_role).to eq( - described_class.restrict_pipeline_cancellation_roles[:developer] - ) + expect(settings.restrict_pipeline_cancellation_role).to eq('developer') end end diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 6e20b25c351361..d2390931ffd138 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -1679,11 +1679,9 @@ end end - describe 'prevents cancel pipeline when cancellation is restricted' do + shared_examples_for 'prevents CI cancellation ability' do using RSpec::Parameterized::TableSyntax - let(:ability) { :cancel_pipeline } - context 'when feature is enabled' do where(:restricted_role, :actual_role, :allowed) do :developer | :guest | false @@ -1747,6 +1745,18 @@ end end + describe 'prevents cancel_pipeline when CI cancllation restricted' do + let(:ability) { :cancel_pipeline } + + it_behaves_like 'prevents CI cancellation ability' + end + + describe 'prevents cancel_build when CI cancllation restricted' do + let(:ability) { :cancel_build } + + it_behaves_like 'prevents CI cancellation ability' + end + describe ':compliance_framework_available' do using RSpec::Parameterized::TableSyntax diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb index 1c53f6eae5233d..a85de37919c4ec 100644 --- a/spec/models/project_ci_cd_setting_spec.rb +++ b/spec/models/project_ci_cd_setting_spec.rb @@ -5,6 +5,8 @@ RSpec.describe ProjectCiCdSetting, feature_category: :continuous_integration do using RSpec::Parameterized::TableSyntax + let_it_be(:project) { create(:project) } + describe 'validations' do it 'validates default_git_depth is between 0 and 1000 or nil' do expect(subject).to validate_numericality_of(:default_git_depth) -- GitLab From 09f02744d7da6b52d26756b234c212ce27f9f8da Mon Sep 17 00:00:00 2001 From: Allison Browne Date: Fri, 3 Nov 2023 16:58:45 -0400 Subject: [PATCH 4/4] Code review feedback, minor spec changes --- ee/app/models/ci/project_cancellation_restriction.rb | 2 +- ee/spec/controllers/projects_controller_spec.rb | 5 +++-- spec/models/project_ci_cd_setting_spec.rb | 2 -- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ee/app/models/ci/project_cancellation_restriction.rb b/ee/app/models/ci/project_cancellation_restriction.rb index 8f2f2b1c8149c0..7eaca4f6a619df 100644 --- a/ee/app/models/ci/project_cancellation_restriction.rb +++ b/ee/app/models/ci/project_cancellation_restriction.rb @@ -2,7 +2,7 @@ module Ci class ProjectCancellationRestriction - # Answers if cancellation restrictions are applied for piplines and processables + # Checks if cancellation restrictions are applied for piplines and processables # based on the given project include Gitlab::Utils::StrongMemoize diff --git a/ee/spec/controllers/projects_controller_spec.rb b/ee/spec/controllers/projects_controller_spec.rb index 8ed0496151c64d..613d1a02c4d0ca 100644 --- a/ee/spec/controllers/projects_controller_spec.rb +++ b/ee/spec/controllers/projects_controller_spec.rb @@ -390,7 +390,6 @@ project.reload end - let(:maintainer_restriction) { settings.restrict_pipeline_cancellation_role_maintainer? } let(:developer_restriction) { settings.restrict_pipeline_cancellation_role_developer? } context 'when the feature is enabled' do @@ -401,7 +400,9 @@ end it 'updates the parameter' do - expect { request }.to change { project.reload.ci_cd_settings.restrict_pipeline_cancellation_role_maintainer? }.from(false).to(true) + expect { request }.to change { + project.reload.ci_cd_settings.restrict_pipeline_cancellation_role_maintainer? + }.from(false).to(true) end end diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb index a85de37919c4ec..1c53f6eae5233d 100644 --- a/spec/models/project_ci_cd_setting_spec.rb +++ b/spec/models/project_ci_cd_setting_spec.rb @@ -5,8 +5,6 @@ RSpec.describe ProjectCiCdSetting, feature_category: :continuous_integration do using RSpec::Parameterized::TableSyntax - let_it_be(:project) { create(:project) } - describe 'validations' do it 'validates default_git_depth is between 0 and 1000 or nil' do expect(subject).to validate_numericality_of(:default_git_depth) -- GitLab