diff --git a/app/graphql/mutations/integrations/exclusions/create.rb b/app/graphql/mutations/integrations/exclusions/create.rb index 483e3b0fba8d11949b3ff36d274909e1a18ec9b8..488fa149f749485dacd635710c871b56a9580e2b 100644 --- a/app/graphql/mutations/integrations/exclusions/create.rb +++ b/app/graphql/mutations/integrations/exclusions/create.rb @@ -6,7 +6,7 @@ module Exclusions class Create < BaseMutation graphql_name 'IntegrationExclusionCreate' include ResolvesIds - MAX_PROJECT_IDS = 100 + MAX_PROJECT_IDS = ::Integrations::Exclusions::CreateService::MAX_PROJECTS field :exclusions, [::Types::Integrations::ExclusionType], null: true, @@ -31,7 +31,7 @@ def resolve(integration_name:, project_ids:) raise Gitlab::Graphql::Errors::ArgumentError, "Count of projectIds should be less than #{MAX_PROJECT_IDS}" end - projects = Project.id_in(resolve_ids(project_ids)).limit(MAX_PROJECT_IDS) + projects = Project.id_in(resolve_ids(project_ids)).with_group.limit(MAX_PROJECT_IDS) result = ::Integrations::Exclusions::CreateService.new( current_user: current_user, diff --git a/app/graphql/mutations/integrations/exclusions/delete.rb b/app/graphql/mutations/integrations/exclusions/delete.rb index bf76c07a15084fa5b1f694fbba3c3194bae0d6c7..1315274652749801252846513f9c80f49f6a2156 100644 --- a/app/graphql/mutations/integrations/exclusions/delete.rb +++ b/app/graphql/mutations/integrations/exclusions/delete.rb @@ -34,8 +34,13 @@ def resolve(integration_name:, project_ids:) integration_name: integration_name ).execute + exclusions = result.payload + + # Integrations::Exclusions::DestroyService calls destroy_all in some circumstances which returns a frozen + # array. We call dup here to allow entries to be redacted by field extensions. + exclusions = exclusions.dup if exclusions.frozen? { - exclusions: result.payload, + exclusions: exclusions, errors: result.errors } end diff --git a/app/models/integration.rb b/app/models/integration.rb index 1bfa6972321692237c88339ed58feb521f6ae81d..05ff8a9785de7e979b439d0cefb0f4e11a5e9d34 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -129,7 +129,7 @@ def properties=(props) scope :with_default_settings, -> { where.not(inherit_from_id: nil) } scope :with_custom_settings, -> { where(inherit_from_id: nil) } scope :for_group, ->(group) { - types = available_integration_types(include_project_specific: false, include_instance_specific: false) + types = available_integration_types(include_project_specific: false) where(group_id: group, type: types) } @@ -627,6 +627,10 @@ def self.instance_specific? false end + def self.pluck_group_id + pluck(:group_id) + end + def form_fields fields.reject { _1[:api_only] == true || (_1.key?(:if) && _1[:if] != true) } end diff --git a/app/services/integrations/exclusions/base_service.rb b/app/services/integrations/exclusions/base_service.rb index 14119560754f73895b2cf1df8fa7f81e26a2f1f3..2a7cf6d1c3b7ee8c6263bc38666f345305842d64 100644 --- a/app/services/integrations/exclusions/base_service.rb +++ b/app/services/integrations/exclusions/base_service.rb @@ -3,30 +3,48 @@ module Integrations module Exclusions class BaseService - def initialize(current_user:, integration_name:, projects:) + include Gitlab::Utils::StrongMemoize + + def initialize(current_user:, integration_name:, projects: [], groups: []) @user = current_user @integration_name = integration_name @projects = projects + @groups = groups end - def execute + private + + attr_reader :user, :integration_name, :projects, :groups + + def validate return ServiceResponse.error(message: 'not authorized') unless allowed? return ServiceResponse.error(message: 'not instance specific') unless instance_specific_integration? - yield + ServiceResponse.success(payload: []) unless projects.present? || groups.present? end - private - - attr_reader :user, :integration_name, :projects - def allowed? user.can?(:admin_all_resources) end def instance_specific_integration? - Integration::INSTANCE_SPECIFIC_INTEGRATION_NAMES.include?(integration_name) + Integration.instance_specific_integration_names.include?(integration_name) + end + + def instance_integration + integration_model.for_instance.first + end + strong_memoize_attr :instance_integration + + def integration_model + Integration.integration_name_to_model(integration_name) + end + strong_memoize_attr :integration_model + + def integration_type + Integration.integration_name_to_type(integration_name) end + strong_memoize_attr :integration_type end end end diff --git a/app/services/integrations/exclusions/create_service.rb b/app/services/integrations/exclusions/create_service.rb index bcd40fb8eb12568cb3cfacff8e6a624a66c1023f..e204250d0919f72fd32324c9a198d5d180f7e2fa 100644 --- a/app/services/integrations/exclusions/create_service.rb +++ b/app/services/integrations/exclusions/create_service.rb @@ -1,20 +1,31 @@ # frozen_string_literal: true +# Exclusions are custom settings at the group or project level used to selectively deactivate an instance integration +# https://gitlab.com/gitlab-org/gitlab/-/issues/454372 module Integrations module Exclusions class CreateService < BaseService + MAX_PROJECTS = 100 + MAX_GROUPS = 100 + def execute - super do - break ServiceResponse.success(payload: []) unless projects.present? + result = validate + return result if result.present? - create_exclusions - end + return ServiceResponse.error(message: 'project limit exceeded') if projects_over_limit? + return ServiceResponse.error(message: 'group limit exceeded') if groups_over_limit? + + project_integrations = create_project_integrations + group_integrations = create_group_integrations + ServiceResponse.success(payload: group_integrations + project_integrations) end private - def create_exclusions - integration_type = Integration.integration_name_to_type(integration_name) + def create_project_integrations + projects = filtered_projects + return Integration.none unless projects.present? + integration_attrs = projects.map do |project| { project_id: project.id, @@ -25,7 +36,85 @@ def create_exclusions end result = Integration.upsert_all(integration_attrs, unique_by: [:project_id, :type_new]) - ServiceResponse.success(payload: Integration.id_in(result.rows.flatten)) + Integration.id_in(result.rows.flatten) + end + + def create_group_integrations + groups = filtered_groups + return Integration.none unless groups.present? + + integrations_for_groups = integration_model.for_group(groups) + existing_group_ids = integrations_for_groups.map(&:group_id).to_set + groups_missing_integrations = groups.reject do |g| + existing_group_ids.include?(g.id) + end + + integrations_to_update = integrations_for_groups.select do |integration| + integration.inherit_from_id.present? || integration.activated? + end + integration_ids_to_update = integrations_to_update.map(&:id) + integration_model.id_in(integration_ids_to_update).update_all(inherit_from_id: nil, active: false) + + integration_attrs = groups_missing_integrations.map do |g| + { + group_id: g.id, + active: false, + inherit_from_id: nil, + type_new: integration_type + } + end + + created_group_integration_ids = [] + if integration_attrs.present? + created_group_integration_ids = Integration.insert_all(integration_attrs, + returning: :id).rows.flatten + end + + new_exclusions = Integration.id_in(integration_ids_to_update + created_group_integration_ids) + new_exclusions.each do |integration| + PropagateIntegrationWorker.perform_async(integration.id) + end + new_exclusions + end + + # Exclusions for groups should propagate to subgroup children + # Skip creating integrations for subgroups and projects that would already be deactivated by an ancestor + # integration. + # Also skip for projects and groups that would be deactivated by creating an integration for another group in the + # same call to #execute. + def filtered_groups + group_ids = groups.map(&:id) + ancestor_integration_group_ids + groups.reject do |g| + g.ancestor_ids.intersect?(group_ids) + end + end + strong_memoize_attr :filtered_groups + + def filtered_projects + filtered_group_ids = filtered_groups.map(&:id) + ancestor_integration_group_ids + + projects.reject do |p| + p&.group&.self_and_ancestor_ids&.intersect?(filtered_group_ids) + end + end + strong_memoize_attr :filtered_projects + + def ancestor_integration_group_ids + integration_model + .with_custom_settings + .for_group( + (groups.flat_map(&:traversal_ids) + projects.flat_map { |p| p&.group&.traversal_ids }).compact.uniq + ).limit(MAX_GROUPS + MAX_PROJECTS) + .pluck_group_id + end + strong_memoize_attr :ancestor_integration_group_ids + + def projects_over_limit? + projects.size > MAX_PROJECTS + end + + def groups_over_limit? + groups.size > MAX_GROUPS end end end diff --git a/app/services/integrations/exclusions/destroy_service.rb b/app/services/integrations/exclusions/destroy_service.rb index 6fd1fd20161714167663c54fc794a4974cc85338..f09d509e88c77938cdc633073ce97328be5d2fdf 100644 --- a/app/services/integrations/exclusions/destroy_service.rb +++ b/app/services/integrations/exclusions/destroy_service.rb @@ -4,27 +4,35 @@ module Integrations module Exclusions class DestroyService < BaseService def execute - super do - destroy_exclusions - end + result = validate + return result if result.present? + + destroy_exclusions end private def destroy_exclusions - integration_class = Integration.integration_name_to_model(integration_name) - exclusions = integration_class.exclusions_for_project(projects) + exclusions = integration_model.from_union([ + integration_model.with_custom_settings.by_active_flag(false).for_group(groups), + integration_model.with_custom_settings.exclusions_for_project(projects) + ]) return ServiceResponse.success(payload: []) unless exclusions.present? - instance_integration = integration_class.for_instance.first - unless instance_integration - integration_class.id_in(exclusions.map(&:id)).delete_all - return ServiceResponse.success(payload: exclusions) + # rubocop:disable Cop/DestroyAll -- loading objects into memory to run callbacks and return objects + return ServiceResponse.success(payload: exclusions.destroy_all) + # rubocop:enable Cop/DestroyAll end ::Integrations::Propagation::BulkUpdateService.new(instance_integration, exclusions).execute + + group_exclusions = exclusions.select(&:group_level?) + group_exclusions.each do |exclusion| + PropagateIntegrationWorker.perform_async(exclusion.id) + end + ServiceResponse.success(payload: exclusions) end end diff --git a/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb b/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb index 6dac5ae23b051646d0a7e68d553712f127f6cd02..212862654e0d49d8bf76730d457c63c6c6a2ac2e 100644 --- a/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb +++ b/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe "Adding and removing exclusions to Beyond Identity integration", :sidekiq_inline, feature_category: :integrations do +RSpec.describe "Adding and removing exclusions to Beyond Identity integration", :sidekiq_inline, feature_category: :source_code_management do let_it_be_with_reload(:project) { create(:project, :in_subgroup) } let_it_be(:admin_user) { create :admin } - def create_exclusion + def create_exclusion_for_project Integrations::Exclusions::CreateService.new( current_user: admin_user, integration_name: 'beyond_identity', @@ -14,7 +14,7 @@ def create_exclusion ).execute end - def destroy_exclusion + def destroy_exclusion_for_project Integrations::Exclusions::DestroyService.new( current_user: admin_user, integration_name: 'beyond_identity', @@ -42,33 +42,103 @@ def destroy_exclusion context 'and the project is excluded from the integration' do before do - create_exclusion + create_exclusion_for_project end it { expect(project.reload.beyond_identity_integration).not_to be_activated } context 'and the exclusion is removed again' do before do - destroy_exclusion + destroy_exclusion_for_project end it { expect(project.reload.beyond_identity_integration).to be_activated } + it { expect(project.reload.beyond_identity_integration.inherit_from_id).to eq(instance_integration.id) } context 'and the exclusion is added again' do before do - create_exclusion + create_exclusion_for_project end it { expect(project.reload.beyond_identity_integration).not_to be_activated } end end + + context "and the project's group is excluded from the integration" do + let!(:create_group_exclusion) do + Integrations::Exclusions::CreateService.new( + current_user: admin_user, + integration_name: 'beyond_identity', + groups: [project.group] + ).execute + end + + it 'updates the project integration to inherit from the group' do + created_exclusion = create_group_exclusion.payload[0] + + expect(project.reload.beyond_identity_integration.inherit_from_id).to eq(created_exclusion.id) + expect(project.reload.beyond_identity_integration).not_to be_activated + end + end + end + + context "and the project's group is excluded from the integration" do + let!(:create_group_exclusion) do + Integrations::Exclusions::CreateService.new( + current_user: admin_user, + integration_name: 'beyond_identity', + groups: [project.group] + ).execute + end + + it 'updates the project integration to inherit from the group' do + created_exclusion = create_group_exclusion.payload[0] + + expect(project.reload.beyond_identity_integration.inherit_from_id).to eq(created_exclusion.id) + expect(project.reload.beyond_identity_integration).not_to be_activated + end + + context 'and the group exclusion is destroyed' do + before do + Integrations::Exclusions::DestroyService.new( + current_user: admin_user, + integration_name: 'beyond_identity', + groups: [project.group] + ).execute + end + + it 'updates the project integration to inherit from the instance' do + expect(project.reload.beyond_identity_integration.inherit_from_id).to eq(instance_integration.id) + expect(project.reload.beyond_identity_integration).to be_activated + end + end end end context 'when the instance integration has not been activated', :enable_admin_mode do + context "and the project's group is excluded from the integration" do + let!(:create_group_exclusion) do + Integrations::Exclusions::CreateService.new( + current_user: admin_user, + integration_name: 'beyond_identity', + groups: [project.group] + ).execute + end + + context 'and the integration is activated for the instance' do + let(:instance_integration) { create :beyond_identity_integration } + + before do + ::Integrations::PropagateService.new(instance_integration).execute + end + + it { expect(project.reload.beyond_identity_integration).not_to be_activated } + end + end + context 'and an exclusion is created' do before do - create_exclusion + create_exclusion_for_project end it { expect(project.reload.beyond_identity_integration).not_to be_activated } @@ -85,7 +155,7 @@ def destroy_exclusion context 'and the exclusion is deleted' do before do - destroy_exclusion + destroy_exclusion_for_project end it { expect(project.reload.beyond_identity_integration).to be_nil } diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb index 86ef06b43a01829b9f4b7644ff39418eb026e48c..cdb9daf6c97a0bebce90b6a0083a9eecbcf357c2 100644 --- a/spec/models/integration_spec.rb +++ b/spec/models/integration_spec.rb @@ -151,6 +151,14 @@ it 'returns the right group integration' do expect(described_class.for_group(group)).to contain_exactly(jira_group_integration) end + + context 'when there is an instance specific integration' do + let!(:beyond_identity_integration) { create(:beyond_identity_integration, instance: false, group: group) } + + it 'includes the instance specific integration' do + expect(described_class.for_group(group)).to include(jira_group_integration, beyond_identity_integration) + end + end end shared_examples 'hook scope' do |hook_type| diff --git a/spec/services/integrations/exclusions/base_service_spec.rb b/spec/services/integrations/exclusions/base_service_spec.rb deleted file mode 100644 index 401c44f6ea25ae8989287c7663301288d5ad2c08..0000000000000000000000000000000000000000 --- a/spec/services/integrations/exclusions/base_service_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Integrations::Exclusions::BaseService, feature_category: :integrations do - let(:integration_name) { 'beyond_identity' } - let_it_be(:admin_user) { create(:admin) } - let_it_be(:user) { create(:user) } - let(:current_user) { admin_user } - let_it_be(:project) { create(:project) } - let(:service) do - described_class.new(current_user: current_user, integration_name: integration_name, projects: [project]) - end - - subject(:execute) { service.execute } - - it_behaves_like 'exclusions base service' -end diff --git a/spec/services/integrations/exclusions/create_service_spec.rb b/spec/services/integrations/exclusions/create_service_spec.rb index 3ab333beb64d7e23087bbffe5a88c93559b020b3..618668dfbd297d374a73535180e306c235a75375 100644 --- a/spec/services/integrations/exclusions/create_service_spec.rb +++ b/spec/services/integrations/exclusions/create_service_spec.rb @@ -2,58 +2,188 @@ require 'spec_helper' -RSpec.describe Integrations::Exclusions::CreateService, feature_category: :integrations do +RSpec.describe Integrations::Exclusions::CreateService, feature_category: :source_code_management do let(:integration_name) { 'beyond_identity' } let_it_be(:admin_user) { create(:admin) } - let_it_be(:user) { create(:user) } let(:current_user) { admin_user } - let_it_be(:project) { create(:project) } + let_it_be(:project) { create(:project, :in_subgroup) } + let_it_be(:other_project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:other_group) { create(:group) } let(:projects) { [project] } + let(:groups) { [group] } let(:service) do - described_class.new(current_user: current_user, integration_name: integration_name, projects: projects) + described_class.new(current_user: current_user, integration_name: integration_name, projects: projects, + groups: groups) end describe '#execute', :enable_admin_mode do subject(:execute) { service.execute } - it_behaves_like 'exclusions base service' + it_behaves_like 'performs exclusions service validations' + + context 'when called with too many projects' do + before do + stub_const('Integrations::Exclusions::CreateService::MAX_PROJECTS', 0) + end + + it 'returns an error response' do + result = execute + + expect(result).to be_error + expect(result.message).to eq('project limit exceeded') + end + end + + context 'when called with too many groups' do + before do + stub_const('Integrations::Exclusions::CreateService::MAX_GROUPS', 0) + end + + it 'returns an error response' do + result = execute + + expect(result).to be_error + expect(result.message).to eq('group limit exceeded') + end + end + + it 'creates custom settings' do + expect { execute }.to change { Integration.count }.by(2) + + group_integration, project_integration = execute.payload + expect(PropagateIntegrationWorker.jobs).to contain_exactly( + a_hash_including('args' => [group_integration.id]) + ) + expect(group_integration.active).to be_falsey + expect(group_integration.inherit_from_id).to be_nil + expect(project_integration.active).to be_falsey + expect(project_integration.inherit_from_id).to be_nil + end + + context 'when there are no projects or groups passed' do + let(:projects) { [] } + let(:groups) { [] } + + it 'returns success response' do + expect(execute).to be_success + expect(execute.payload).to eq([]) + end + end context 'when there are existing custom settings' do - let!(:existing_integration) do - create(:beyond_identity_integration) + let_it_be(:instance_level_integration) { create(:beyond_identity_integration) } + let_it_be(:project_level_integration) do + create( + :beyond_identity_integration, + active: true, + project: other_project, + instance: false, + inherit_from_id: instance_level_integration.id + ) end - let!(:existing_integration2) do + let_it_be(:group_level_integration) do create( :beyond_identity_integration, active: true, - project: project, + group: other_group, instance: false, - inherit_from_id: existing_integration.id + inherit_from_id: instance_level_integration.id ) end - it 'updates those custom settings' do - execute - existing_integration2.reload - expect(existing_integration2.active).to be_falsey - expect(existing_integration2.inherit_from_id).to be_nil + let_it_be(:previously_excluded_group) { create(:group) } + let_it_be(:excluded_group_integration) do + create( + :beyond_identity_integration, + active: false, + group: previously_excluded_group, + instance: false, + inherit_from_id: nil + ) + end + + let(:expected_group_integration) do + Integrations::BeyondIdentity.find_by!(group: group, inherit_from_id: nil, active: false) + end + + let(:expected_project_integration) do + Integrations::BeyondIdentity.find_by!(project: project, inherit_from_id: nil, active: false) + end + + let(:projects) { [project, other_project] } + let(:groups) { [group, other_group] } + + it 'creates exclusions and updates existing ones' do + expect { execute }.to change { project_level_integration.reload.active }.from(true).to(false) + .and change { group_level_integration.reload.active }.from(true).to(false) + .and change { project_level_integration.inherit_from_id }.from(instance_level_integration.id).to(nil) + .and change { group_level_integration.inherit_from_id }.from(instance_level_integration.id).to(nil) + expect(PropagateIntegrationWorker.jobs).to contain_exactly( + a_hash_including('args' => [expected_group_integration.id]), + a_hash_including('args' => [group_level_integration.id]) + ) + end + + it 'returns the exclusions' do + expect(execute.payload).to contain_exactly( + group_level_integration, + project_level_integration, + expected_group_integration, + expected_project_integration + ) + end + + context 'when there are existing exclusions' do + let(:groups) { [previously_excluded_group] } + let(:projects) { [] } + + it 'does not propagate existing' do + result = execute + + expect(result.payload).to be_blank + expect(PropagateIntegrationWorker.jobs).to be_blank + end end end - it 'creates custom settings' do - expect { execute }.to change { Integration.count }.from(0).to(1) - created_integrations = execute.payload - expect(created_integrations.first.active).to be_falsey - expect(created_integrations.first.inherit_from_id).to be_nil + context 'when there are ancestor exclusions' do + let!(:ancestor_exclusion) do + create(:beyond_identity_integration, active: false, instance: false, inherit_from_id: nil, + group: project.root_namespace) + end + + let(:group_covered_by_ancestor_exclusion) { create(:group, parent: project.parent) } + let(:project_covered_by_ancestor_exclusion) { project } + + let(:projects) { [project_covered_by_ancestor_exclusion, other_project] } + let(:groups) { [group_covered_by_ancestor_exclusion, other_group] } + + it 'only creates exclusions for groups and projects not covered by ancestors with exclusions' do + expect { execute }.to change { Integration.count }.by(2) + group_integration, project_integration = execute.payload + expect(group_integration.group_id).to eq(other_group.id) + expect(project_integration.project_id).to eq(other_project.id) + + expect(group_integration.active).to be_falsey + expect(group_integration.inherit_from_id).to be_nil + expect(project_integration.active).to be_falsey + expect(project_integration.inherit_from_id).to be_nil + end end - context 'when there are no projects passed' do - let(:projects) { [] } + context 'when projects and groups are descendants of another group' do + let(:projects) { [project] } + let(:groups) { [project.parent, project.root_namespace] } - it 'returns success response' do - expect(execute).to be_success - expect(execute.payload).to eq([]) + it 'only creates exclusions for groups and projects not covered by ancestors' do + expect { execute }.to change { Integration.count }.by(1) + created_integration = execute.payload.first + expect(PropagateIntegrationWorker.jobs).to contain_exactly(a_hash_including('args' => [created_integration.id])) + expect(created_integration.group_id).to eq(project.root_namespace.id) + expect(created_integration.active).to be_falsey + expect(created_integration.inherit_from_id).to be_nil end end end diff --git a/spec/services/integrations/exclusions/destroy_service_spec.rb b/spec/services/integrations/exclusions/destroy_service_spec.rb index 8e1d51051632bc69f75de8cb25a68297ad8fad1a..93718013aeecc858bfb8f4a402be7f57b504b080 100644 --- a/spec/services/integrations/exclusions/destroy_service_spec.rb +++ b/spec/services/integrations/exclusions/destroy_service_spec.rb @@ -2,31 +2,70 @@ require 'spec_helper' -RSpec.describe Integrations::Exclusions::DestroyService, feature_category: :integrations do +RSpec.describe Integrations::Exclusions::DestroyService, feature_category: :source_code_management do let(:integration_name) { 'beyond_identity' } let_it_be(:admin_user) { create(:admin) } - let_it_be(:user) { create(:user) } let(:current_user) { admin_user } let_it_be(:project) { create(:project) } + let_it_be(:other_project) { create(:project, :in_subgroup) } + let(:projects) { [project] } + let(:groups) { [] } let(:service) do - described_class.new(current_user: current_user, integration_name: integration_name, projects: [project]) + described_class.new(current_user: current_user, integration_name: integration_name, projects: projects, + groups: groups) end describe '#execute', :enable_admin_mode do subject(:execute) { service.execute } - it_behaves_like 'exclusions base service' + it_behaves_like 'performs exclusions service validations' - context 'when there are existing custom settings' do + context 'when there is an exclusion for the project exists' do let!(:exclusion) do create(:beyond_identity_integration, active: false, project: project, instance: false, inherit_from_id: nil) end - it 'deletes the exclusions' do + it 'deletes the exclusion' do expect { execute }.to change { Integration.count }.from(1).to(0) expect(execute.payload).to contain_exactly(exclusion) end + context 'and there is an exclusion for a group' do + let!(:group_exclusion) do + create(:beyond_identity_integration, active: false, group: other_project.root_namespace, instance: false, + inherit_from_id: nil) + end + + context 'and the exclusion for that group is to be destroyed' do + let(:groups) { [group_exclusion.group] } + + it 'deletes the exclusions' do + expect { execute }.to change { Integration.count }.from(2).to(0) + expect(execute.payload).to contain_exactly(exclusion, group_exclusion) + end + end + + context 'and exclusions to be destroyed are inherited' do + let!(:inherited_project_exclusion) do + create(:beyond_identity_integration, active: false, project: other_project, instance: false, + inherit_from_id: group_exclusion.id) + end + + let!(:inherited_group_exclusion) do + create(:beyond_identity_integration, active: false, group: other_project.group, instance: false, + inherit_from_id: group_exclusion.id) + end + + let(:projects) { [other_project] } + let(:groups) { [other_project.group] } + + it 'does not delete the progagated settings' do + expect { execute }.not_to change { Integration.count } + expect(execute.payload).to be_empty + end + end + end + context 'and the integration is active for the instance' do let!(:instance_integration) { create(:beyond_identity_integration) } @@ -34,6 +73,45 @@ expect { execute }.to change { exclusion.reload.active }.from(false).to(true) expect(exclusion.inherit_from_id).to eq(instance_integration.id) end + + context 'and there is an exclusion for a group' do + let!(:group_exclusion) do + create(:beyond_identity_integration, active: false, group: other_project.root_namespace, instance: false, + inherit_from_id: nil) + end + + context 'and the exclusion for that group is to be destroyed' do + let(:groups) { [group_exclusion.group] } + + it 'updates the exclusion integrations to be active' do + expect { execute }.to change { exclusion.reload.active }.from(false).to(true) + .and change { exclusion.inherit_from_id }.from(nil).to(instance_integration.id) + .and change { group_exclusion.reload.active }.from(false).to(true) + .and change { group_exclusion.inherit_from_id }.from(nil).to(instance_integration.id) + end + end + + context 'and exclusions to be deleted are inherited' do + let!(:inherited_project_exclusion) do + create(:beyond_identity_integration, active: false, project: other_project, instance: false, + inherit_from_id: group_exclusion.id) + end + + let!(:inherited_group_exclusion) do + create(:beyond_identity_integration, active: false, group: other_project.group, instance: false, + inherit_from_id: group_exclusion.id) + end + + let(:projects) { [other_project] } + let(:groups) { [other_project.group] } + + it 'does not update inherited exclusions' do + execute + expect(inherited_project_exclusion.reload).not_to be_activated + expect(inherited_group_exclusion.reload).not_to be_activated + end + end + end end end end diff --git a/spec/support/shared_examples/integrations/exclusions/base_service_examples.rb b/spec/support/shared_examples/integrations/exclusions/exclusions_service_examples.rb similarity index 77% rename from spec/support/shared_examples/integrations/exclusions/base_service_examples.rb rename to spec/support/shared_examples/integrations/exclusions/exclusions_service_examples.rb index 0027f19d10247d224570ed307a5b3aad4766ddc3..5b96ac9cb383fe4b3b51c03d64d148800ee0efbc 100644 --- a/spec/support/shared_examples/integrations/exclusions/base_service_examples.rb +++ b/spec/support/shared_examples/integrations/exclusions/exclusions_service_examples.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -RSpec.shared_examples 'exclusions base service' do +RSpec.shared_examples 'performs exclusions service validations' do + subject(:execute) { service.execute } + context 'when the integration is not instance specific', :enable_admin_mode do let(:integration_name) { 'mock_ci' } @@ -11,7 +13,7 @@ end context 'when the user is not authorized', :enable_admin_mode do - let(:current_user) { user } + let(:current_user) { create(:user) } it 'returns an error response' do expect(execute).to be_error