From fc33ed785b2ee4df845ec126504be61bafcb9d7e Mon Sep 17 00:00:00 2001 From: "j.seto" Date: Thu, 2 May 2024 19:43:05 -0400 Subject: [PATCH 1/2] Add services for create integration exclusions For integrations at the instance level we have custom settings that can be configured at the group and project levels by project/group maintainers. For instance specific integrations (e.g Beyond Identity) this change introduces a similar concept except the only difference in the custom setting is whether or not the integration is active such settings are only configurable by instance admins. This commit introduces services to create and destroy such settings. Where destroying in this context is just using the same settings as the instance integration.a Contributes to: https://gitlab.com/gitlab-org/gitlab/-/issues/454372 Changelog: added --- app/models/integration.rb | 4 ++ .../integrations/exclusions/create_service.rb | 44 +++++++++++++ .../exclusions/destroy_service.rb | 39 +++++++++++ .../integrations/beyond_identity_check.rb | 2 +- .../beyond_identity_check_spec.rb | 28 +++++++- .../exclusions/create_service_spec.rb | 66 +++++++++++++++++++ .../exclusions/destroy_service_spec.rb | 48 ++++++++++++++ 7 files changed, 227 insertions(+), 4 deletions(-) create mode 100644 app/services/integrations/exclusions/create_service.rb create mode 100644 app/services/integrations/exclusions/destroy_service.rb create mode 100644 spec/services/integrations/exclusions/create_service_spec.rb create mode 100644 spec/services/integrations/exclusions/destroy_service_spec.rb diff --git a/app/models/integration.rb b/app/models/integration.rb index 68852b0c8f2b9f..9252c8b38cd379 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 00000000000000..db6c373175736a --- /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 00000000000000..9d6de9689215e6 --- /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/lib/gitlab/checks/integrations/beyond_identity_check.rb b/lib/gitlab/checks/integrations/beyond_identity_check.rb index 0dcf7f3446f14b..8960a550869459 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/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb b/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb index 7e3b4b206ff439..9781f9384f3d38 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/services/integrations/exclusions/create_service_spec.rb b/spec/services/integrations/exclusions/create_service_spec.rb new file mode 100644 index 00000000000000..ae28b18f969410 --- /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 00000000000000..2d7ae99bc6d443 --- /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 -- GitLab From a69c8e4f6f0b109c0362610d22c190ca2ca76fcc Mon Sep 17 00:00:00 2001 From: "j.seto" Date: Thu, 2 May 2024 19:59:43 -0400 Subject: [PATCH 2/2] Add graphql resources for integration exclusions --- .../integrations/exclusions/create.rb | 45 ++++++++ .../integrations/exclusions/delete.rb | 42 +++++++ .../integrations/exclusions_resolver.rb | 23 ++++ .../types/integrations/exclusion_type.rb | 14 +++ .../integrations/integration_type_enum.rb | 12 ++ app/graphql/types/mutation_type.rb | 2 + app/graphql/types/query_type.rb | 5 + doc/api/graphql/reference/index.md | 106 ++++++++++++++++++ spec/graphql/types/query_type_spec.rb | 10 ++ .../graphql/integrations/exclusions_spec.rb | 58 ++++++++++ .../integrations/exclusions/create_spec.rb | 60 ++++++++++ .../integrations/exclusions/delete_spec.rb | 61 ++++++++++ .../types/query_type_shared_context.rb | 1 + 13 files changed, 439 insertions(+) create mode 100644 app/graphql/mutations/integrations/exclusions/create.rb create mode 100644 app/graphql/mutations/integrations/exclusions/delete.rb create mode 100644 app/graphql/resolvers/integrations/exclusions_resolver.rb create mode 100644 app/graphql/types/integrations/exclusion_type.rb create mode 100644 app/graphql/types/integrations/integration_type_enum.rb create mode 100644 spec/requests/api/graphql/integrations/exclusions_spec.rb create mode 100644 spec/requests/api/graphql/mutations/integrations/exclusions/create_spec.rb create mode 100644 spec/requests/api/graphql/mutations/integrations/exclusions/delete_spec.rb diff --git a/app/graphql/mutations/integrations/exclusions/create.rb b/app/graphql/mutations/integrations/exclusions/create.rb new file mode 100644 index 00000000000000..97233f6f0a4d2f --- /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 00000000000000..8792ed2bc331ed --- /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 00000000000000..a2cae8b1f0e65d --- /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 00000000000000..0cd07f74238453 --- /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 00000000000000..4e6b8c8005c2e7 --- /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 c821e4ad6ab033..648c3b7c282f29 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 1091ca2a788c38..76b5f7a6d05a11 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/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index af5601e9db4cf0..f47b2af628e605 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/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 0b5739be9a1145..ec4a1aa15f789c 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/requests/api/graphql/integrations/exclusions_spec.rb b/spec/requests/api/graphql/integrations/exclusions_spec.rb new file mode 100644 index 00000000000000..4a785310ebc54f --- /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 00000000000000..5d3e19c100dfa6 --- /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 00000000000000..1548ac5ed1775c --- /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/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb index 391336526e3f60..cea5ce74f0771c 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, -- GitLab