diff --git a/app/graphql/mutations/container_registry/protection/rule/delete.rb b/app/graphql/mutations/container_registry/protection/rule/delete.rb new file mode 100644 index 0000000000000000000000000000000000000000..b1673b7c43e97092a829d01db7d42fc9042e2ab1 --- /dev/null +++ b/app/graphql/mutations/container_registry/protection/rule/delete.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Mutations + module ContainerRegistry + module Protection + module Rule + class Delete < ::Mutations::BaseMutation + graphql_name 'DeleteContainerRegistryProtectionRule' + description 'Deletes a container registry protection rule. ' \ + 'Available only when feature flag `container_registry_protected_containers` is enabled.' + + authorize :admin_container_image + + argument :id, + ::Types::GlobalIDType[::ContainerRegistry::Protection::Rule], + required: true, + description: 'Global ID of the container registry protection rule to delete.' + + field :container_registry_protection_rule, + Types::ContainerRegistry::Protection::RuleType, + null: true, + description: 'Container registry protection rule that was deleted successfully.' + + def resolve(id:, **_kwargs) + if Feature.disabled?(:container_registry_protected_containers) + raise_resource_not_available_error!("'container_registry_protected_containers' feature flag is disabled") + end + + container_registry_protection_rule = authorized_find!(id: id) + + response = ::ContainerRegistry::Protection::DeleteRuleService.new(container_registry_protection_rule, + current_user: current_user).execute + + { container_registry_protection_rule: response.payload[:container_registry_protection_rule], + errors: response.errors } + end + end + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 3a2dea92cdc2aab23f874b2d88b8d9e14f2600b4..632e1860ceb90e87b828a701f07eb42fe54e64bf 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -137,6 +137,7 @@ class MutationType < BaseObject mount_mutation Mutations::DesignManagement::Update mount_mutation Mutations::ContainerExpirationPolicies::Update mount_mutation Mutations::ContainerRegistry::Protection::Rule::Create, alpha: { milestone: '16.6' } + mount_mutation Mutations::ContainerRegistry::Protection::Rule::Delete, alpha: { milestone: '16.7' } mount_mutation Mutations::ContainerRepositories::Destroy mount_mutation Mutations::ContainerRepositories::DestroyTags mount_mutation Mutations::Ci::Catalog::Resources::Create, alpha: { milestone: '15.11' } diff --git a/app/services/container_registry/protection/delete_rule_service.rb b/app/services/container_registry/protection/delete_rule_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..bfd91c75b8bc23202343483fa4edd80b5e1f771a --- /dev/null +++ b/app/services/container_registry/protection/delete_rule_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ContainerRegistry + module Protection + class DeleteRuleService + include Gitlab::Allowable + + def initialize(container_registry_protection_rule, current_user:) + if container_registry_protection_rule.blank? || current_user.blank? + raise ArgumentError, + 'container_registry_protection_rule and current_user must be set' + end + + @container_registry_protection_rule = container_registry_protection_rule + @current_user = current_user + end + + def execute + unless can?(current_user, :admin_container_image, container_registry_protection_rule.project) + error_message = _('Unauthorized to delete a container registry protection rule') + return service_response_error(message: error_message) + end + + deleted_container_registry_protection_rule = container_registry_protection_rule.destroy! + + ServiceResponse.success( + payload: { container_registry_protection_rule: deleted_container_registry_protection_rule } + ) + rescue StandardError => e + service_response_error(message: e.message) + end + + private + + attr_reader :container_registry_protection_rule, :current_user + + def service_response_error(message:) + ServiceResponse.error( + message: message, + payload: { container_registry_protection_rule: nil } + ) + end + end + end +end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index bf25ee8182bf51473fcf67cbba78278643f1f04b..f66ee386b67d555de9ddae76b684625e0f541872 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3231,6 +3231,31 @@ Input type: `DeleteAnnotationInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +### `Mutation.deleteContainerRegistryProtectionRule` + +Deletes a container registry protection rule. Available only when feature flag `container_registry_protected_containers` is enabled. + +WARNING: +**Introduced** in 16.7. +This feature is an Experiment. It can be changed or removed at any time. + +Input type: `DeleteContainerRegistryProtectionRuleInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `id` | [`ContainerRegistryProtectionRuleID!`](#containerregistryprotectionruleid) | Global ID of the container registry protection rule to delete. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `containerRegistryProtectionRule` | [`ContainerRegistryProtectionRule`](#containerregistryprotectionrule) | Container registry protection rule that was deleted successfully. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.deletePackagesProtectionRule` Deletes a protection rule for packages. Available only when feature flag `packages_protected_packages` is enabled. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b2f358fa500ae2e266ad1659452519065bd15277..064b7cf1d4c6de49a647ba563bc508864169b072 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -51542,6 +51542,9 @@ msgstr "" msgid "Unauthorized to create an environment" msgstr "" +msgid "Unauthorized to delete a container registry protection rule" +msgstr "" + msgid "Unauthorized to delete a package protection rule" msgstr "" diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..126d4bfdd4acc1df595962bcdcbbbf1be69b64d1 --- /dev/null +++ b/spec/requests/api/graphql/mutations/container_registry/protection/rule/delete_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Deleting a container registry protection rule', :aggregate_failures, feature_category: :container_registry do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be_with_refind(:container_protection_rule) do + create(:container_registry_protection_rule, project: project) + end + + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + + let(:mutation) { graphql_mutation(:delete_container_registry_protection_rule, input) } + let(:mutation_response) { graphql_mutation_response(:delete_container_registry_protection_rule) } + let(:input) { { id: container_protection_rule.to_global_id } } + + subject(:post_graphql_mutation_delete_container_registry_protection_rule) do + post_graphql_mutation(mutation, current_user: current_user) + end + + shared_examples 'an erroneous reponse' do + it { post_graphql_mutation_delete_container_registry_protection_rule.tap { expect(mutation_response).to be_blank } } + + it do + expect { post_graphql_mutation_delete_container_registry_protection_rule } + .not_to change { ::ContainerRegistry::Protection::Rule.count } + end + end + + it_behaves_like 'a working GraphQL mutation' + + it 'responds with deleted container registry protection rule' do + expect { post_graphql_mutation_delete_container_registry_protection_rule } + .to change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(0) + + expect_graphql_errors_to_be_empty + + expect(mutation_response).to include( + 'errors' => be_blank, + 'containerRegistryProtectionRule' => { + 'id' => container_protection_rule.to_global_id.to_s, + 'containerPathPattern' => container_protection_rule.container_path_pattern, + 'deleteProtectedUpToAccessLevel' => container_protection_rule.delete_protected_up_to_access_level.upcase, + 'pushProtectedUpToAccessLevel' => container_protection_rule.push_protected_up_to_access_level.upcase + } + ) + end + + context 'with existing container registry protection rule belonging to other project' do + let_it_be(:container_protection_rule) do + create(:container_registry_protection_rule, container_path_pattern: 'protection_rule_other_project') + end + + it_behaves_like 'an erroneous reponse' + + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + + context 'with deleted container registry protection rule' do + let!(:container_protection_rule) do + create(:container_registry_protection_rule, project: project, + container_path_pattern: 'protection_rule_deleted').destroy! + end + + it_behaves_like 'an erroneous reponse' + + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + + context 'when current_user does not have permission' do + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:anonymous) { create(:user) } + + where(:current_user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it_behaves_like 'an erroneous reponse' + + it { is_expected.tap { expect_graphql_errors_to_include(/you don't have permission to perform this action/) } } + end + end + + context "when feature flag ':container_registry_protected_containers' disabled" do + before do + stub_feature_flags(container_registry_protected_containers: false) + end + + it_behaves_like 'an erroneous reponse' + + it do + post_graphql_mutation_delete_container_registry_protection_rule + + expect_graphql_errors_to_include(/'container_registry_protected_containers' feature flag is disabled/) + end + end +end diff --git a/spec/services/container_registry/protection/delete_rule_service_spec.rb b/spec/services/container_registry/protection/delete_rule_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bdc2ca727d2f953f43c5ee3925a0544045b9cb25 --- /dev/null +++ b/spec/services/container_registry/protection/delete_rule_service_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ContainerRegistry::Protection::DeleteRuleService, '#execute', feature_category: :container_registry do + let_it_be(:project) { create(:project) } + let_it_be(:current_user) { create(:user, maintainer_projects: [project]) } + let_it_be_with_refind(:container_registry_protection_rule) do + create(:container_registry_protection_rule, project: project) + end + + subject(:service_execute) do + described_class.new(container_registry_protection_rule, current_user: current_user).execute + end + + shared_examples 'a successful service response' do + it { is_expected.to be_success } + + it do + is_expected.to have_attributes( + errors: be_blank, + message: be_blank, + payload: { container_registry_protection_rule: container_registry_protection_rule } + ) + end + + it do + service_execute + + expect { container_registry_protection_rule.reload }.to raise_error ActiveRecord::RecordNotFound + end + end + + shared_examples 'an erroneous service response' do + it { is_expected.to be_error } + + it do + is_expected.to have_attributes(message: be_present, payload: { container_registry_protection_rule: be_blank }) + end + + it do + expect { service_execute }.not_to change { ContainerRegistry::Protection::Rule.count } + + expect { container_registry_protection_rule.reload }.not_to raise_error + end + end + + it_behaves_like 'a successful service response' + + it 'deletes the container registry protection rule in the database' do + expect { service_execute } + .to change { + project.reload.container_registry_protection_rules + }.from([container_registry_protection_rule]).to([]) + .and change { ::ContainerRegistry::Protection::Rule.count }.from(1).to(0) + end + + context 'with deleted container registry protection rule' do + let!(:container_registry_protection_rule) do + create(:container_registry_protection_rule, project: project, + container_path_pattern: 'protection_rule_deleted').destroy! + end + + it_behaves_like 'a successful service response' + end + + context 'when error occurs during delete operation' do + before do + allow(container_registry_protection_rule).to receive(:destroy!).and_raise(StandardError.new('Some error')) + end + + it_behaves_like 'an erroneous service response' + + it { is_expected.to have_attributes message: /Some error/ } + end + + context 'when current_user does not have permission' do + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } + let_it_be(:reporter) { create(:user).tap { |u| project.add_reporter(u) } } + let_it_be(:guest) { create(:user).tap { |u| project.add_guest(u) } } + let_it_be(:anonymous) { create(:user) } + + where(:current_user) do + [ref(:developer), ref(:reporter), ref(:guest), ref(:anonymous)] + end + + with_them do + it_behaves_like 'an erroneous service response' + + it { is_expected.to have_attributes message: /Unauthorized to delete a container registry protection rule/ } + end + end + + context 'without container registry protection rule' do + let(:container_registry_protection_rule) { nil } + + it { expect { service_execute }.to raise_error(ArgumentError) } + end + + context 'without current_user' do + let(:current_user) { nil } + let(:container_registry_protection_rule) { build_stubbed(:container_registry_protection_rule, project: project) } + + it { expect { service_execute }.to raise_error(ArgumentError) } + end +end