diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index c2483ff496d72fffb4e3c87607feef1f624015ef..8b8c71d7fda53545c943311741ca3b45707e71f8 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -93,6 +93,7 @@ The following API resources are available in the project context: | [Projects](projects.md) including setting Webhooks | `/projects`, `/projects/:id/hooks` (also available for users) | | [Protected branches](protected_branches.md) | `/projects/:id/protected_branches` | | [Protected environments](protected_environments.md) | `/projects/:id/protected_environments` | +| [Protected package rules](project_packages_protection_rules.md) | `/projects/:id/protection/rules` | | [Protected tags](protected_tags.md) | `/projects/:id/protected_tags` | | [PyPI packages](packages/pypi.md) | `/projects/:id/packages/pypi` (also available for groups) | | [Release links](releases/links.md) | `/projects/:id/releases/.../assets/links` | diff --git a/doc/api/project_packages_protection_rules.md b/doc/api/project_packages_protection_rules.md new file mode 100644 index 0000000000000000000000000000000000000000..8972606c55870993469f8f513e59f4c056a8404b --- /dev/null +++ b/doc/api/project_packages_protection_rules.md @@ -0,0 +1,53 @@ +--- +stage: Package +group: Package Registry +info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments" +description: "Documentation for the REST API for Package Protection Rules in GitLab." +--- + +## Package protection rules API + +DETAILS: +**Tier:** Free, Premium, Ultimate +**Offering:** GitLab.com, Self-managed +**Status:** Experiment + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151741) in GitLab 17.1 [with a flag](../administration/feature_flags.md) named `packages_protected_packages`. Disabled by default. + +FLAG: +The availability of this feature is controlled by a feature flag. +For more information, see the history. +This feature is available for testing, but not ready for production use. + +This API manages the protection rules for packages. This feature is an experiment. + +### Delete a package protection rule + +Deletes a package protection rule from a project. + +```plaintext +DELETE /api/v4/projects/:id/packages/protection/rules/:package_protection_rule_id +``` + +Supported attributes: + +| Attribute | Type | Required | Description | +|-------------------------------|-----------------|----------|--------------------------------| +| `id` | integer/string | Yes | ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | +| `package_protection_rule_id` | integer | Yes | ID of the package protection rule to be deleted. | + +If successful, returns [`204 No Content`](rest/index.md#status-codes). + +Can return the following status codes: + +- `204 No Content`: The package protection rule was deleted successfully. +- `400 Bad Request`: The `id` or the `package_protection_rule_id` are missing or are invalid. +- `403 Forbidden`: The user does not have permission to delete the package protection rule. +- `404 Not Found`: The project or the package protection rule was not found. + +Example request: + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/5/packages/protection/rules/32" +``` diff --git a/lib/api/api.rb b/lib/api/api.rb index 3a5c4f4e1e5d4bec4af0dd1c6dcf4e5ffe525aec..081c4ce9391a22bf4596cfa26737c9b7b8075533 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -309,9 +309,10 @@ def initialize(location_url) mount ::API::ProjectImport mount ::API::ProjectJobTokenScope mount ::API::ProjectPackages + mount ::API::ProjectPackagesProtectionRules mount ::API::ProjectRepositoryStorageMoves - mount ::API::ProjectSnippets mount ::API::ProjectSnapshots + mount ::API::ProjectSnippets mount ::API::ProjectStatistics mount ::API::ProjectTemplates mount ::API::Projects diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index c2b5f31dc77c567f8219ca19c82bfa2b6a15695e..1d823ca9352f83691d4973797af1ec3f4540f5ba 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -17,6 +17,10 @@ def require_dependency_proxy_enabled! not_found! unless ::Gitlab.config.dependency_proxy.enabled end + def authorize_admin_package!(subject = user_project) + authorize!(:admin_package, subject) + end + def authorize_read_package!(subject = user_project) authorize!(:read_package, subject.try(:packages_policy_subject) || subject) end diff --git a/lib/api/project_packages_protection_rules.rb b/lib/api/project_packages_protection_rules.rb new file mode 100644 index 0000000000000000000000000000000000000000..3b684aaa1ec73a160b04b48345f18a2cf163d097 --- /dev/null +++ b/lib/api/project_packages_protection_rules.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module API + class ProjectPackagesProtectionRules < ::API::Base + feature_category :package_registry + helpers ::API::Helpers::PackagesHelpers + + before do + if Feature.disabled?(:packages_protected_packages, user_project) + render_api_error!("'packages_protected_packages' feature flag is disabled", :not_found) + end + + authenticate! + end + + params do + requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Delete package protection rule' do + success code: 204, message: '204 No Content' + failure [ + { code: 400, message: 'Bad Request' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[projects] + end + params do + requires :package_protection_rule_id, type: Integer, desc: 'The ID of the package protection rule' + end + delete ':id/packages/protection/rules/:package_protection_rule_id' do + authorize_admin_package! + + package_protection_rule = user_project.package_protection_rules.find(params[:package_protection_rule_id]) + + destroy_conditionally!(package_protection_rule) do |package_protection_rule| + response = ::Packages::Protection::DeleteRuleService.new(package_protection_rule, + current_user: current_user).execute + + render_api_error!({ error: response.message }, :bad_request) if response.error? + end + end + end + end +end diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb index 51819b4910e839768c21e674d311dee4e753d25b..f44581dcd853dc71ce97a435e0ae7247ca5ed442 100644 --- a/spec/lib/api/helpers/packages_helpers_spec.rb +++ b/spec/lib/api/helpers/packages_helpers_spec.rb @@ -62,7 +62,7 @@ end end - %i[create_package destroy_package].each do |action| + %i[create_package destroy_package admin_package].each do |action| describe "authorize_#{action}!" do subject { helper.send("authorize_#{action}!", project) } diff --git a/spec/requests/api/project_packages_protection_rules_spec.rb b/spec/requests/api/project_packages_protection_rules_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..304ce170c514757b5a1de008f4cdb007afcd7189 --- /dev/null +++ b/spec/requests/api/project_packages_protection_rules_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ProjectPackagesProtectionRules, feature_category: :package_registry do + include ExclusiveLeaseHelpers + + let_it_be(:project) { create(:project, :private) } + let_it_be(:other_project) { create(:project, :private) } + let_it_be(:package_protection_rule) { create(:package_protection_rule, project: project) } + + let_it_be(:maintainer) { create(:user, maintainer_of: [project, other_project]) } + let_it_be(:developer) { create(:user, developer_of: [project]) } + let_it_be(:reporter) { create(:user, reporter_of: [project]) } + let_it_be(:guest) { create(:user, guest_of: [project]) } + + let(:users) do + { + anonymous: nil, + developer: developer, + guest: guest, + maintainer: maintainer, + reporter: reporter + } + end + + shared_examples 'rejecting project packages protection rules request' do |user_type, status| + context "for #{user_type}" do + let(:api_user) { users[user_type] } + + it_behaves_like 'returning response status', status + end + end + + describe 'DELETE /projects/:id/packages/protection/rules/:package_protection_rule_id' do + let(:url) { "/projects/#{project.id}/packages/protection/rules/#{package_protection_rule.id}" } + + subject(:destroy_package_rule) { delete(api(url, api_user)) } + + it_behaves_like 'rejecting project packages protection rules request', :reporter, :forbidden + it_behaves_like 'rejecting project packages protection rules request', :developer, :forbidden + it_behaves_like 'rejecting project packages protection rules request', :guest, :forbidden + it_behaves_like 'rejecting project packages protection rules request', :anonymous, :not_found + + context 'for maintainer' do + let(:api_user) { maintainer } + + it 'deletes the package protection rule' do + destroy_package_rule + expect do + Packages::Protection::Rule.find(package_protection_rule.id) + end.to raise_error(ActiveRecord::RecordNotFound) + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when the package protection rule does belong to another project' do + let(:url) { "/projects/#{other_project.id}/packages/protection/rules/#{package_protection_rule.id}" } + + it_behaves_like 'rejecting project packages protection rules request', :maintainer, :not_found + end + + context 'when the project id is invalid' do + let(:url) { "/projects/invalid/packages/protection/rules/#{package_protection_rule.id}" } + + it_behaves_like 'rejecting project packages protection rules request', :maintainer, :not_found + end + + context 'when the project id does not exist' do + let(:url) { "/projects/#{non_existing_record_id}/packages/protection/rules/#{package_protection_rule.id}" } + + it_behaves_like 'rejecting project packages protection rules request', :maintainer, :not_found + end + + context 'when the rule id is invalid' do + let(:url) { "/projects/#{project.id}/packages/protection/rules/invalid" } + + it_behaves_like 'rejecting project packages protection rules request', :maintainer, :bad_request + end + + context 'when the rule id does not exist' do + let(:url) { "/projects/#{project.id}/packages/protection/rules/#{non_existing_record_id}" } + + it_behaves_like 'rejecting project packages protection rules request', :maintainer, :not_found + end + + context 'when packages_protected_packages is disabled' do + before do + stub_feature_flags(packages_protected_packages: false) + end + + it_behaves_like 'rejecting project packages protection rules request', :maintainer, :not_found + end + end +end