diff --git a/app/graphql/mutations/integrations/exclusions/create.rb b/app/graphql/mutations/integrations/exclusions/create.rb new file mode 100644 index 0000000000000000000000000000000000000000..97233f6f0a4d2f5aa4cba652dea66e576c300bae --- /dev/null +++ b/app/graphql/mutations/integrations/exclusions/create.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Mutations + module Integrations + module Exclusions + class Create < BaseMutation + graphql_name 'IntegrationExclusionCreate' + include ResolvesIds + + field :exclusions, ::Types::Integrations::ExclusionType.connection_type, + null: true, + description: 'Integration exclusions created by the mutation.' + + argument :integration_name, + ::Types::Integrations::IntegrationTypeEnum, + required: true, + description: 'Type of integration to exclude.' + + argument :project_ids, + [::Types::GlobalIDType[::Project]], + required: true, + description: 'Ids of projects to exclude.' + + authorize :admin_all_resources + + def resolve(integration_name:, project_ids:) + authorize!(:global) + + projects = Project.id_in(resolve_ids(project_ids)) + + result = ::Integrations::Exclusions::CreateService.new( + current_user: current_user, + projects: projects, + integration_name: integration_name + ).execute + + { + exclusions: result.payload, + errors: [] + } + end + end + end + end +end diff --git a/app/graphql/mutations/integrations/exclusions/delete.rb b/app/graphql/mutations/integrations/exclusions/delete.rb new file mode 100644 index 0000000000000000000000000000000000000000..8792ed2bc331ed8d1aac3973e39b0a42afaea9aa --- /dev/null +++ b/app/graphql/mutations/integrations/exclusions/delete.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Mutations + module Integrations + module Exclusions + class Delete < BaseMutation + graphql_name 'IntegrationExclusionDelete' + + field :exclusion, ::Types::Integrations::ExclusionType, + null: true, + description: 'Project no longer excluded due to the mutation.' + + argument :integration_name, + ::Types::Integrations::IntegrationTypeEnum, + required: true, + description: 'Type of integration.' + + argument :project_id, + ::Types::GlobalIDType[::Project], + required: true, + description: 'Id of excluded project.' + + authorize :admin_all_resources + + def resolve(integration_name:, project_id:) + authorize!(:global) + + result = ::Integrations::Exclusions::DestroyService.new( + current_user: current_user, + project_id: project_id.model_id, + integration_name: integration_name + ).execute + + { + exclusion: result.payload, + errors: [] + } + end + end + end + end +end diff --git a/app/graphql/resolvers/integrations/exclusions_resolver.rb b/app/graphql/resolvers/integrations/exclusions_resolver.rb new file mode 100644 index 0000000000000000000000000000000000000000..a2cae8b1f0e65d9a74cd406fd816ccd3dcd3a1c5 --- /dev/null +++ b/app/graphql/resolvers/integrations/exclusions_resolver.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Resolvers + module Integrations + class ExclusionsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + type Types::Integrations::ExclusionType.connection_type, null: true + + argument :integration_name, Types::Integrations::IntegrationTypeEnum, + required: true, + description: 'Type of description.' + + def resolve(integration_name:) + authorize! + Integration.integration_name_to_model(integration_name).with_custom_settings.by_active_flag(false) + end + + def authorize! + raise_resource_not_available_error! unless context[:current_user].can_admin_all_resources? + end + end + end +end diff --git a/app/graphql/types/integrations/exclusion_type.rb b/app/graphql/types/integrations/exclusion_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..0cd07f74238453f838578280da163bac742cfe79 --- /dev/null +++ b/app/graphql/types/integrations/exclusion_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module Integrations + class ExclusionType < BaseObject + graphql_name 'IntegrationExclusion' + description 'An integration to override the level settings of instance specific integrations.' + authorize :admin_all_resources + + field :project, ::Types::ProjectType, + description: 'Project that has been excluded from the instance specific integration.' + end + end +end diff --git a/app/graphql/types/integrations/integration_type_enum.rb b/app/graphql/types/integrations/integration_type_enum.rb new file mode 100644 index 0000000000000000000000000000000000000000..4e6b8c8005c2e7eb15c42c0b161baa937a1e7d1a --- /dev/null +++ b/app/graphql/types/integrations/integration_type_enum.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Types + module Integrations + class IntegrationTypeEnum < BaseEnum + graphql_name 'IntegrationType' + description 'Integration Names' + + value 'BEYOND_IDENTITY', description: 'Beyond Identity.', value: 'beyond_identity' + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index c821e4ad6ab033f3e1f970b050e87277d457c587..648c3b7c282f2969d71a7d97d795943ecb9d3326 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -63,6 +63,8 @@ class MutationType < BaseObject mount_mutation Mutations::IncidentManagement::TimelineEvent::Update mount_mutation Mutations::IncidentManagement::TimelineEvent::Destroy mount_mutation Mutations::IncidentManagement::TimelineEventTag::Create + mount_mutation Mutations::Integrations::Exclusions::Create, alpha: { milestone: '17.0' } + mount_mutation Mutations::Integrations::Exclusions::Delete, alpha: { milestone: '17.0' } mount_mutation Mutations::Issues::Create mount_mutation Mutations::Issues::SetAssignees mount_mutation Mutations::Issues::SetCrmContacts diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 1091ca2a788c38bf55e4061a165f9ed1994a261b..76b5f7a6d05a11ec4527ba7f3bc2678220d1d687 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -222,6 +222,11 @@ class QueryType < ::Types::BaseObject description: 'Find machine learning models.', resolver: Resolvers::Ml::ModelDetailResolver + field :integration_exclusions, Types::Integrations::ExclusionType.connection_type, + null: true, + alpha: { milestone: '17.0' }, + resolver: Resolvers::Integrations::ExclusionsResolver + field :work_items_by_reference, null: true, alpha: { milestone: '16.7' }, diff --git a/app/models/integration.rb b/app/models/integration.rb index 68852b0c8f2b9fe79891d9f5b4421c03d4d3f873..9252c8b38cd379dc1b2950896247286408b09ceb 100644 --- a/app/models/integration.rb +++ b/app/models/integration.rb @@ -691,6 +691,10 @@ def toggle! active? ? deactivate! : activate! end + def self.exclusions_for_project(project) + where(project: project, active: false) + end + private # Ancestors sorted by hierarchy depth in bottom-top order. diff --git a/app/services/integrations/exclusions/create_service.rb b/app/services/integrations/exclusions/create_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..db6c373175736a6b58d0df8b07a1da3e1c59360d --- /dev/null +++ b/app/services/integrations/exclusions/create_service.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Integrations + module Exclusions + class CreateService + def initialize(current_user:, integration_name:, projects:) + @user = current_user + @integration_name = integration_name + @projects = projects + end + + attr_reader :user, :integration_name, :projects + + def execute + return ServiceResponse.error(message: 'not authorized') unless allowed? + return ServiceResponse.error(message: 'not instance specific') unless instance_specific_integration? + + integration_type = Integration.integration_name_to_type(integration_name) + + integration_attrs = projects.map do |project| + { + project_id: project.id, + type_new: integration_type, + active: false, + inherit_from_id: nil + } + end + + result = Integration.upsert_all(integration_attrs, unique_by: [:project_id, :type_new]) + ServiceResponse.success(payload: Integration.id_in(result.rows.flatten)) + end + + private + + def allowed? + user.can?(:admin_all_resources) + end + + def instance_specific_integration? + Integration::INSTANCE_SPECIFIC_INTEGRATION_NAMES.include?(integration_name) + end + end + end +end diff --git a/app/services/integrations/exclusions/destroy_service.rb b/app/services/integrations/exclusions/destroy_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..9d6de9689215e6e16eb312780951c8c6900b8f0f --- /dev/null +++ b/app/services/integrations/exclusions/destroy_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Integrations + module Exclusions + class DestroyService < ::BaseService + def initialize(current_user:, integration_name:, project_id:) + @user = current_user + @integration_name = integration_name + @project_id = project_id + end + + attr_reader :user, :integration_name, :project_id + + def execute + return ServiceResponse.error(message: 'not authorized') unless allowed? + return ServiceResponse.error(message: 'not instance specific') unless instance_specific_integration? + + integration_class = Integration.integration_name_to_model(integration_name) + exclusion = integration_class.exclusions_for_project(project_id).first + + instance_integration = integration_class.for_instance.first + return ServiceResponse.success(payload: exclusion&.destroy) unless instance_integration + + ::Integrations::Propagation::BulkUpdateService.new(instance_integration, [exclusion]).execute + ServiceResponse.success(payload: exclusion) + end + + private + + def allowed? + user.can?(:admin_all_resources) + end + + def instance_specific_integration? + Integration::INSTANCE_SPECIFIC_INTEGRATION_NAMES.include?(integration_name) + end + end + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index af5601e9db4cf020a7515f58610e8c52f0753e2f..f47b2af628e605dde0a13e99a262bc0d0605bb36 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -477,6 +477,24 @@ Fields related to Instance Security Dashboard. Returns [`InstanceSecurityDashboard`](#instancesecuritydashboard). +### `Query.integrationExclusions` + +DETAILS: +**Introduced** in GitLab 17.0. +**Status**: Experiment. + +Returns [`IntegrationExclusionConnection`](#integrationexclusionconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `integrationName` | [`IntegrationType!`](#integrationtype) | Type of description. | + ### `Query.issue` Find an issue. @@ -5360,6 +5378,53 @@ Input type: `InstanceGoogleCloudLoggingConfigurationUpdateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `instanceGoogleCloudLoggingConfiguration` | [`InstanceGoogleCloudLoggingConfigurationType`](#instancegooglecloudloggingconfigurationtype) | configuration updated. | +### `Mutation.integrationExclusionCreate` + +DETAILS: +**Introduced** in GitLab 17.0. +**Status**: Experiment. + +Input type: `IntegrationExclusionCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `integrationName` | [`IntegrationType!`](#integrationtype) | Type of integration to exclude. | +| `projectIds` | [`[ProjectID!]!`](#projectid) | Ids of projects to exclude. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + +### `Mutation.integrationExclusionDelete` + +DETAILS: +**Introduced** in GitLab 17.0. +**Status**: Experiment. + +Input type: `IntegrationExclusionDeleteInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `integrationName` | [`IntegrationType!`](#integrationtype) | Type of integration. | +| `projectId` | [`ProjectID!`](#projectid) | Id of excluded project. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `exclusion` | [`IntegrationExclusion`](#integrationexclusion) | Project no longer excluded due to the mutation. | + ### `Mutation.issuableResourceLinkCreate` Input type: `IssuableResourceLinkCreateInput` @@ -12440,6 +12505,29 @@ The edge type for [`InstanceGoogleCloudLoggingConfigurationType`](#instancegoogl | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`InstanceGoogleCloudLoggingConfigurationType`](#instancegooglecloudloggingconfigurationtype) | The item at the end of the edge. | +#### `IntegrationExclusionConnection` + +The connection type for [`IntegrationExclusion`](#integrationexclusion). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[IntegrationExclusionEdge]`](#integrationexclusionedge) | A list of edges. | +| `nodes` | [`[IntegrationExclusion]`](#integrationexclusion) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `IntegrationExclusionEdge` + +The edge type for [`IntegrationExclusion`](#integrationexclusion). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`IntegrationExclusion`](#integrationexclusion) | The item at the end of the edge. | + #### `IssuableResourceLinkConnection` The connection type for [`IssuableResourceLink`](#issuableresourcelink). @@ -22846,6 +22934,16 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount). | `severity` | [`[VulnerabilitySeverity!]`](#vulnerabilityseverity) | Filter vulnerabilities by severity. | | `state` | [`[VulnerabilityState!]`](#vulnerabilitystate) | Filter vulnerabilities by state. | +### `IntegrationExclusion` + +An integration to override the level settings of instance specific integrations. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `project` | [`Project`](#project) | Project that has been excluded from the instance specific integration. | + ### `IssuableResourceLink` Describes an issuable resource link for incident issues. @@ -33289,6 +33387,14 @@ Health status of an issue or epic for filtering. | `needsAttention` | Needs attention. | | `onTrack` | On track. | +### `IntegrationType` + +Integration Names. + +| Value | Description | +| ----- | ----------- | +| `BEYOND_IDENTITY` | Beyond Identity. | + ### `IssuableResourceLinkType` Issuable resource link type enum. diff --git a/lib/gitlab/checks/integrations/beyond_identity_check.rb b/lib/gitlab/checks/integrations/beyond_identity_check.rb index 0dcf7f3446f14b66b98a17a1bd06f56dc5e0d472..8960a550869459e1521d363f0ab5cf1d347703e9 100644 --- a/lib/gitlab/checks/integrations/beyond_identity_check.rb +++ b/lib/gitlab/checks/integrations/beyond_identity_check.rb @@ -10,7 +10,7 @@ class BeyondIdentityCheck < ::Gitlab::Checks::BaseBulkChecker def initialize(integration_check) @changes_access = integration_check.changes_access - @integration = ::Integrations::BeyondIdentity.for_instance.first + @integration = project.beyond_identity_integration end def validate! diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 0b5739be9a114582e86be12d265c6414b97b0ee6..ec4a1aa15f789c45a3a783e76b735c307a078fc8 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -155,4 +155,14 @@ is_expected.to have_graphql_resolver(Resolvers::Ml::ModelDetailResolver) end end + + describe 'integration_exclusions field' do + subject { described_class.fields['integrationExclusions'] } + + it 'returns metadata', :aggregate_failures do + is_expected.to have_graphql_arguments(:integrationName) + is_expected.to have_graphql_type(Types::Integrations::ExclusionType.connection_type) + is_expected.to have_graphql_resolver(Resolvers::Integrations::ExclusionsResolver) + end + end end diff --git a/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb b/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb index 7e3b4b206ff439fcf99b55c7cd2e26b887920b4c..9781f9384f3d38a43d50d624c9fe6d53508f37dc 100644 --- a/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb +++ b/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb @@ -2,18 +2,36 @@ require 'spec_helper' -RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_category: :source_code_management do +RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, :sidekiq_inline, feature_category: :source_code_management do include_context 'changes access checks context' let!(:beyond_identity_integration) { create(:beyond_identity_integration) } + let(:integration_exclusion) { nil } let(:integration_check) { Gitlab::Checks::IntegrationsCheck.new(changes_access) } subject(:check) { described_class.new(integration_check) } + before do + integration_exclusion + ::Integrations::PropagateService.new(beyond_identity_integration).execute + end + describe '#validate!' do + shared_examples_for 'exclusion from the check' do + context 'when the project is excluded from the check' do + let(:integration_exclusion) do + create(:beyond_identity_integration, active: false, project: project, inherit_from_id: nil, instance: false) + end + + it 'does not raise an error' do + expect { check.validate! }.not_to raise_error + end + end + end + context 'when commit without GPG signature' do - let_it_be(:project) { create(:project, :repository) } + let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:oldrev) { '1e292f8fedd741b75372e19097c76d327140c312' } let_it_be(:newrev) { '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' } @@ -27,6 +45,8 @@ .to raise_error(::Gitlab::GitAccess::ForbiddenError, 'Commit is not signed with a GPG signature') end + it_behaves_like 'exclusion from the check' + context 'when the push happens from web' do let(:protocol) { 'web' } @@ -56,7 +76,7 @@ end context 'when a commit with GPG signature' do - let_it_be(:project) { create(:project, :repository) } + let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:oldrev) { 'ddd0f15ae83993f5cb66a927a28673882e99100b' } let_it_be(:newrev) { 'f0a5ed60d24c98ec6d00ac010c1f3f01ee0a8373' } let!(:gpg_key) { create :gpg_key, externally_verified: true } @@ -66,6 +86,8 @@ project.repository.delete_branch('trailers') end + it_behaves_like 'exclusion from the check' + context 'and the signature is unverified' do it 'is rejected' do expect { check.validate! } diff --git a/spec/requests/api/graphql/integrations/exclusions_spec.rb b/spec/requests/api/graphql/integrations/exclusions_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4a785310ebc54fc63f77d20b683c8bb5942cbaf0 --- /dev/null +++ b/spec/requests/api/graphql/integrations/exclusions_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Querying for integration exclusions', feature_category: :integrations do + include GraphqlHelpers + let_it_be(:project) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:admin_user) { create(:admin) } + let_it_be(:user) { create(:user) } + let(:current_user) { admin_user } + let(:query) { graphql_query_for('integrationExclusions', args, fields) } + let(:args) { { 'integrationName' => :BEYOND_IDENTITY } } + let(:fields) do + <<~GRAPHQL + nodes { + project { + id + } + } + GRAPHQL + end + + context 'when the user is authorized' do + let!(:instance_integration) { create(:beyond_identity_integration) } + let!(:integration_exclusion) do + create(:beyond_identity_integration, active: false, instance: false, project: project2, inherit_from_id: nil) + end + + let!(:propagated_integration) do + create(:beyond_identity_integration, active: false, instance: false, project: project, + inherit_from_id: instance_integration.id) + end + + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query that returns data' + + it 'returns projects that are custom exclusions' do + nodes = graphql_data['integrationExclusions']['nodes'] + expect(nodes.size).to eq(1) + expect(nodes).to include(a_hash_including('project' => { 'id' => project2.to_global_id.to_s })) + end + end + + context 'when the user is not authorized' do + let(:current_user) { user } + + it 'responds with an error' do + post_graphql(query, current_user: current_user) + expect(graphql_errors.first['message']).to eq( + Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR + ) + end + end +end diff --git a/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb b/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5d3e19c100dfa63032384f475813a6bc8919671f --- /dev/null +++ b/spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Integrations::Exclusions::Create, feature_category: :integrations do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:admin_user) { create(:admin) } + let_it_be(:user) { create(:user) } + let(:current_user) { admin_user } + let(:mutation) { graphql_mutation(:integration_exclusion_create, args) } + let(:args) do + { + 'integrationName' => 'BEYOND_IDENTITY', + 'projectIds' => project_ids + } + end + + let(:project_ids) { [project.to_global_id.to_s] } + + subject(:resolve_mutation) { post_graphql_mutation(mutation, current_user: current_user) } + + context 'when the user is not authorized' do + let(:current_user) { user } + + it 'responds with an error' do + resolve_mutation + expect(graphql_errors.first['message']).to eq( + Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR + ) + end + end + + context 'when the user is authorized' do + let(:current_user) { admin_user } + + it 'creates inactive integrations for the projects' do + expect { resolve_mutation }.to change { Integration.count }.from(0).to(1) + end + + context 'when integrations exist for the projects' do + let!(:instance_exclusion) { create(:beyond_identity_integration) } + let!(:existing_exclusion) do + create(:beyond_identity_integration, project: project2, active: false, inherit_from_id: instance_exclusion.id, + instance: false) + end + + let(:project_ids) { [project, project2].map { |p| p.to_global_id.to_s } } + + it 'updates existing integrations and creates integrations for projects' do + expect { resolve_mutation }.to change { Integration.count }.from(2).to(3) + existing_exclusion.reload + expect(existing_exclusion).not_to be_active + expect(existing_exclusion.inherit_from_id).to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb b/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1548ac5ed1775c092e65d0a89cd01defbfddc3b7 --- /dev/null +++ b/spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Integrations::Exclusions::Delete, feature_category: :integrations do + include GraphqlHelpers + + let_it_be(:project) { create(:project) } + let_it_be(:admin_user) { create(:admin) } + let_it_be(:user) { create(:user) } + let(:current_user) { admin_user } + let(:mutation) { graphql_mutation(:integration_exclusion_delete, args) } + let(:args) do + { + 'integrationName' => 'BEYOND_IDENTITY', + 'projectId' => project_id + } + end + + let(:project_id) { project.to_global_id.to_s } + + subject(:resolve_mutation) { post_graphql_mutation(mutation, current_user: current_user) } + + context 'when the user is not authorized' do + let(:current_user) { user } + + it 'responds with an error' do + resolve_mutation + expect(graphql_errors.first['message']).to eq( + Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR + ) + end + end + + context 'when the user is authorized' do + let(:current_user) { admin_user } + + it 'returns nil' do + resolve_mutation + expect(graphql_data['integrationExclusionDelete']['exclusion']).to be_nil + end + + context 'and there are integrations' do + let!(:instance_integration) { create(:beyond_identity_integration) } + let!(:existing_exclusion) do + create(:beyond_identity_integration, project: project, active: false, inherit_from_id: nil, + instance: false) + end + + it 'the integration for the specified project' do + resolve_mutation + + existing_exclusion.reload + expect(existing_exclusion).to be_activated + expect(existing_exclusion.inherit_from_id).to eq(instance_integration.id) + exclusion_response = graphql_data['integrationExclusionDelete']['exclusion'] + expect(exclusion_response['project']['id']).to eq(project_id) + end + end + end +end diff --git a/spec/services/integrations/exclusions/create_service_spec.rb b/spec/services/integrations/exclusions/create_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ae28b18f9694102314c99c8075c3a18fcff78cff --- /dev/null +++ b/spec/services/integrations/exclusions/create_service_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::Exclusions::CreateService, 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 + + describe '#execute', :enable_admin_mode do + subject(:execute) { service.execute } + + context 'when the integration is not instance specific' do + let(:integration_name) { 'mock_ci' } + + it 'returns an error response' do + expect(execute).to be_error + expect(execute.message).to eq('not instance specific') + end + end + + context 'when the user is not authorized' do + let(:current_user) { user } + + it 'returns an error response' do + expect(execute).to be_error + expect(execute.message).to eq('not authorized') + end + end + + context 'when there are existing custom settings' do + let!(:existing_integration) do + create(:beyond_identity_integration) + end + + let!(:existing_integration2) do + create( + :beyond_identity_integration, + active: true, + project: project, + instance: false, + inherit_from_id: existing_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 + 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 + end + end +end diff --git a/spec/services/integrations/exclusions/destroy_service_spec.rb b/spec/services/integrations/exclusions/destroy_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d7ae99bc6d443a1937124147fa6f919e3cc0e56 --- /dev/null +++ b/spec/services/integrations/exclusions/destroy_service_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Integrations::Exclusions::DestroyService, 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, project_id: project.id) + end + + describe '#execute', :enable_admin_mode do + subject(:execute) { service.execute } + + context 'when the integration is not instance specific' do + let(:integration_name) { 'mock_ci' } + + it 'returns an error response' do + expect(execute).to be_error + expect(execute.message).to eq('not instance specific') + end + end + + context 'when the user is not authorized' do + let(:current_user) { user } + + it 'returns an error response' do + expect(execute).to be_error + expect(execute.message).to eq('not authorized') + end + end + + context 'when there are existing custom settings' do + let!(:instance_integration) { create(:beyond_identity_integration) } + let!(:exclusion) do + create(:beyond_identity_integration, active: false, project: project, instance: false, inherit_from_id: nil) + end + + it 'updates the exclusion integration to be active' do + expect { execute }.to change { exclusion.reload.active }.from(false).to(true) + expect(exclusion.inherit_from_id).to eq(instance_integration.id) + end + end + end +end diff --git a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb index 391336526e3f60162df03c4232a289e1899cf69e..cea5ce74f0771c48ca1836e68f44ab76f23a4188 100644 --- a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb +++ b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb @@ -18,6 +18,7 @@ :gitpod_enabled, :group, :groups, + :integration_exclusions, :issue, :issues, :jobs,