From cbad917bccc3b9f69d50dd3202c30fdd184bdf17 Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Fri, 31 May 2024 05:30:26 -0700 Subject: [PATCH 1/3] Protected packages: REST API GET package protection rules Adds a GET route to the REST API to get a list of package protection rules for a project. Changelog: added --- doc/api/project_packages_protection_rules.md | 52 ++++++++++++++++++- .../projects/packages/protection/rule.rb | 18 +++++++ lib/api/project_packages_protection_rules.rb | 16 +++++- .../project_packages_protection_rules_spec.rb | 46 ++++++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 lib/api/entities/projects/packages/protection/rule.rb diff --git a/doc/api/project_packages_protection_rules.md b/doc/api/project_packages_protection_rules.md index 8972606c558709..8e374c088201b8 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,55 @@ 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. +- `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/5/packages/protection/rules" +``` + +Example response: + +```json +[ + { + "project_id": 7, + "package_name_pattern": "@flightjs/flight-package-0", + "package_type": "npm", + "push_protected_up_to_access_level": "maintainer" + }, + { + "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. 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 00000000000000..4d24899e95573e --- /dev/null +++ b/lib/api/entities/projects/packages/protection/rule.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module API + module Entities + module Projects + module Packages + module Protection + class Rule < Grape::Entity + 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 3b684aaa1ec73a..7585ad9f9c3845 100644 --- a/lib/api/project_packages_protection_rules.rb +++ b/lib/api/project_packages_protection_rules.rb @@ -5,7 +5,7 @@ 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 @@ -17,6 +17,20 @@ class ProjectPackagesProtectionRules < ::API::Base 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: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[projects] + is_array true + end + get ':id/packages/protection/rules' do + authorize_admin_package! + 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 [ diff --git a/spec/requests/api/project_packages_protection_rules_spec.rb b/spec/requests/api/project_packages_protection_rules_spec.rb index 304ce170c51475..d19c4be909ed8f 100644 --- a/spec/requests/api/project_packages_protection_rules_spec.rb +++ b/spec/requests/api/project_packages_protection_rules_spec.rb @@ -32,6 +32,52 @@ 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', :anonymous, :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 + 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}" } -- GitLab From 49ce81864850656426a586b0f52bda252aa9fe8b Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Sun, 2 Jun 2024 04:37:41 -0700 Subject: [PATCH 2/3] Protected packages: REST API GET package protection rules Adds a GET route to the REST API to get a list of package protection rules for a project. Based on review feedback this commit reduces the number of users created by assigning different roles during the test execution. Changelog: added --- lib/api/project_packages_protection_rules.rb | 4 +-- .../project_packages_protection_rules_spec.rb | 26 ++++++------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/lib/api/project_packages_protection_rules.rb b/lib/api/project_packages_protection_rules.rb index 7585ad9f9c3845..693dd47ce80a95 100644 --- a/lib/api/project_packages_protection_rules.rb +++ b/lib/api/project_packages_protection_rules.rb @@ -11,6 +11,7 @@ class ProjectPackagesProtectionRules < ::API::Base end authenticate! + authorize_admin_package! end params do @@ -27,7 +28,6 @@ class ProjectPackagesProtectionRules < ::API::Base is_array true end get ':id/packages/protection/rules' do - authorize_admin_package! present user_project.package_protection_rules, with: Entities::Projects::Packages::Protection::Rule end @@ -44,8 +44,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/requests/api/project_packages_protection_rules_spec.rb b/spec/requests/api/project_packages_protection_rules_spec.rb index d19c4be909ed8f..9a74ff545efcc6 100644 --- a/spec/requests/api/project_packages_protection_rules_spec.rb +++ b/spec/requests/api/project_packages_protection_rules_spec.rb @@ -10,23 +10,13 @@ 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] } + 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 @@ -40,7 +30,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 } @@ -86,7 +76,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 } -- GitLab From 7b0447b5cf22abbcb02d7607cf99567fced9969e Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Wed, 5 Jun 2024 01:52:25 -0700 Subject: [PATCH 3/3] Protected packages: REST API GET package protection rules Adds a GET route to the REST API to get a list of package protection rules for a project. Based on review feedback this commit increases the test coverage by testing unauthorized access, as well as testing the API::Entity Changelog: added --- doc/api/project_packages_protection_rules.md | 8 ++++++-- .../projects/packages/protection/rule.rb | 1 + lib/api/project_packages_protection_rules.rb | 2 ++ .../projects/packages/protection/rule_spec.rb | 19 +++++++++++++++++++ .../project_packages_protection_rules_spec.rb | 15 +++++++++++++++ 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 spec/lib/api/entities/projects/packages/protection/rule_spec.rb diff --git a/doc/api/project_packages_protection_rules.md b/doc/api/project_packages_protection_rules.md index 8e374c088201b8..d4eaad99270ff4 100644 --- a/doc/api/project_packages_protection_rules.md +++ b/doc/api/project_packages_protection_rules.md @@ -40,6 +40,7 @@ If successful, returns [`200`](rest/index.md#status-codes) and a list of package 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. @@ -47,7 +48,7 @@ Example request: ```shell curl --header "PRIVATE-TOKEN: " \ - --url "https://gitlab.example.com/api/v4/projects/5/packages/protection/rules" + --url "https://gitlab.example.com/api/v4/projects/7/packages/protection/rules" ``` Example response: @@ -55,12 +56,14 @@ 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", @@ -90,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. @@ -97,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 index 4d24899e95573e..6f6e38da0aade8 100644 --- a/lib/api/entities/projects/packages/protection/rule.rb +++ b/lib/api/entities/projects/packages/protection/rule.rb @@ -6,6 +6,7 @@ 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' } diff --git a/lib/api/project_packages_protection_rules.rb b/lib/api/project_packages_protection_rules.rb index 693dd47ce80a95..bcabecf47f662a 100644 --- a/lib/api/project_packages_protection_rules.rb +++ b/lib/api/project_packages_protection_rules.rb @@ -21,6 +21,7 @@ class ProjectPackagesProtectionRules < ::API::Base 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' } ] @@ -35,6 +36,7 @@ class ProjectPackagesProtectionRules < ::API::Base 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' } ] 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 00000000000000..f18626a053e5ca --- /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 9a74ff545efcc6..20079cfb867a74 100644 --- a/spec/requests/api/project_packages_protection_rules_spec.rb +++ b/spec/requests/api/project_packages_protection_rules_spec.rb @@ -12,6 +12,9 @@ let_it_be(:maintainer) { create(:user, maintainer_of: [project, other_project]) } let_it_be(:api_user) { create(:user) } + 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 @@ -66,6 +69,12 @@ 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 @@ -127,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 -- GitLab