diff --git a/doc/api/project_packages_protection_rules.md b/doc/api/project_packages_protection_rules.md index 8972606c55870993469f8f513e59f4c056a8404b..d4eaad99270ff49fee6b26c5cb808a5eda129a87 100644 --- a/doc/api/project_packages_protection_rules.md +++ b/doc/api/project_packages_protection_rules.md @@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated description: "Documentation for the REST API for Package Protection Rules in GitLab." --- -## Package protection rules API +# Package protection rules API DETAILS: **Tier:** Free, Premium, Ultimate @@ -21,7 +21,58 @@ 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 +## List package protection rules + +Gets a list of package protection rules from a project. + +```plaintext +GET /api/v4/projects/:id/packages/protection/rules +``` + +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. | + +If successful, returns [`200`](rest/index.md#status-codes) and a list of package protection rules. + +Can return the following status codes: + +- `200 OK`: A list of package protection rules. +- `401 Unauthorized`: The access token is invalid. +- `403 Forbidden`: The user does not have permission to list package protection rules for this project. +- `404 Not Found`: The project was not found. + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/7/packages/protection/rules" +``` + +Example response: + +```json +[ + { + "id": 1, + "project_id": 7, + "package_name_pattern": "@flightjs/flight-package-0", + "package_type": "npm", + "push_protected_up_to_access_level": "maintainer" + }, + { + "id": 2, + "project_id": 7, + "package_name_pattern": "@flightjs/flight-package-1", + "package_type": "npm", + "push_protected_up_to_access_level": "maintainer" + } +] +``` + +## Delete a package protection rule Deletes a package protection rule from a project. @@ -42,6 +93,7 @@ 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. +- `401 Unauthorized`: The access token is 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. @@ -49,5 +101,5 @@ Example request: ```shell curl --request DELETE --header "PRIVATE-TOKEN: " \ - --url "https://gitlab.example.com/api/v4/projects/5/packages/protection/rules/32" + --url "https://gitlab.example.com/api/v4/projects/7/packages/protection/rules/32" ``` diff --git a/lib/api/entities/projects/packages/protection/rule.rb b/lib/api/entities/projects/packages/protection/rule.rb new file mode 100644 index 0000000000000000000000000000000000000000..6f6e38da0aade8c17bcab45b146b33b0b36a3c8d --- /dev/null +++ b/lib/api/entities/projects/packages/protection/rule.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + module Projects + module Packages + module Protection + class Rule < Grape::Entity + expose :id, documentation: { type: 'integer', example: 1 } + expose :project_id, documentation: { type: 'integer', example: 1 } + expose :package_name_pattern, documentation: { type: 'string', example: 'flightjs/flight' } + expose :package_type, documentation: { type: 'string', example: 'npm' } + expose :push_protected_up_to_access_level, documentation: { type: 'string', example: 'maintainer' } + end + end + end + end + end +end diff --git a/lib/api/project_packages_protection_rules.rb b/lib/api/project_packages_protection_rules.rb index 3b684aaa1ec73a160b04b48345f18a2cf163d097..bcabecf47f662a6ed18900b3428ad73da03540bc 100644 --- a/lib/api/project_packages_protection_rules.rb +++ b/lib/api/project_packages_protection_rules.rb @@ -5,22 +5,38 @@ class ProjectPackagesProtectionRules < ::API::Base feature_category :package_registry helpers ::API::Helpers::PackagesHelpers - before do + after_validation do if Feature.disabled?(:packages_protected_packages, user_project) render_api_error!("'packages_protected_packages' feature flag is disabled", :not_found) end authenticate! + authorize_admin_package! 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 'Get list of package protection rules for a project' do + success Entities::Projects::Packages::Protection::Rule + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[projects] + is_array true + end + get ':id/packages/protection/rules' do + present user_project.package_protection_rules, with: Entities::Projects::Packages::Protection::Rule + end + desc 'Delete package protection rule' do success code: 204, message: '204 No Content' failure [ { code: 400, message: 'Bad Request' }, + { code: 401, message: 'Unauthorized' }, { code: 403, message: 'Forbidden' }, { code: 404, message: 'Not Found' } ] @@ -30,8 +46,6 @@ class ProjectPackagesProtectionRules < ::API::Base 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| diff --git a/spec/lib/api/entities/projects/packages/protection/rule_spec.rb b/spec/lib/api/entities/projects/packages/protection/rule_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f18626a053e5ca838bca7b638faf7a7e738ee5e5 --- /dev/null +++ b/spec/lib/api/entities/projects/packages/protection/rule_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Projects::Packages::Protection::Rule, feature_category: :package_registry do + let(:package_protection_rule) { create(:package_protection_rule) } + + subject(:entity) { described_class.new(package_protection_rule).as_json } + + it 'exposes correct attributes' do + expect(entity.keys).to match_array [ + :id, + :project_id, + :package_name_pattern, + :package_type, + :push_protected_up_to_access_level + ] + end +end diff --git a/spec/requests/api/project_packages_protection_rules_spec.rb b/spec/requests/api/project_packages_protection_rules_spec.rb index 304ce170c514757b5a1de008f4cdb007afcd7189..20079cfb867a745321d36b5401501895e567d573 100644 --- a/spec/requests/api/project_packages_protection_rules_spec.rb +++ b/spec/requests/api/project_packages_protection_rules_spec.rb @@ -10,28 +10,73 @@ 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 + let_it_be(:api_user) { create(:user) } - shared_examples 'rejecting project packages protection rules request' do |user_type, status| - context "for #{user_type}" do - let(:api_user) { users[user_type] } + let_it_be(:invalid_token) { 'invalid-token123' } + let_it_be(:headers_with_invalid_token) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => invalid_token } } + + shared_examples 'rejecting project packages protection rules request' do |user_role, status| + context "for #{user_role}" do + before do + project.send(:"add_#{user_role}", api_user) if user_role + end it_behaves_like 'returning response status', status end end + describe 'GET /projects/:id/packages/protection/rules' do + let(:url) { "/projects/#{project.id}/packages/protection/rules" } + + subject(:get_package_rules) { get(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', nil, :not_found + + context 'for maintainer' do + let(:api_user) { maintainer } + + let_it_be(:other_package_protection_rule) do + create(:package_protection_rule, project: project, package_name_pattern: "@my-scope/my-package-*") + end + + it 'gets the package protection rules' do + get_package_rules + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(2) + end + + context 'when the project id is invalid' do + let(:url) { "/projects/invalid/packages/protection/rules" } + + 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" } + + 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 + + context 'with invalid token' do + subject(:get_package_rules) { get(api(url), headers: headers_with_invalid_token) } + + it_behaves_like 'rejecting project packages protection rules request', nil, :unauthorized + 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}" } @@ -40,7 +85,7 @@ 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 + it_behaves_like 'rejecting project packages protection rules request', nil, :not_found context 'for maintainer' do let(:api_user) { maintainer } @@ -91,5 +136,11 @@ it_behaves_like 'rejecting project packages protection rules request', :maintainer, :not_found end + + context 'with invalid token' do + subject(:delete_package_rules) { delete(api(url), headers: headers_with_invalid_token) } + + it_behaves_like 'rejecting project packages protection rules request', nil, :unauthorized + end end end