diff --git a/doc/api/project_container_registry_protection_rules.md b/doc/api/project_container_registry_protection_rules.md new file mode 100644 index 0000000000000000000000000000000000000000..99a0e1fad659ffd2f0b9b98f5b73bbcabdbabd22 --- /dev/null +++ b/doc/api/project_container_registry_protection_rules.md @@ -0,0 +1,73 @@ +--- +stage: Package +group: Container 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 container registry protection rules in GitLab." +--- + +# Container registry protection rules API + +DETAILS: +**Tier:** Free, Premium, Ultimate +**Offering:** Self-managed +**Status:** Experiment + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155798) in GitLab 17.2 [with a flag](../administration/feature_flags.md) named `container_registry_protected_containers`. 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 endpoint manages the protection rules for container registries in a project. This feature is an experiment. + +## List container registry protection rules + +Gets a list of container registry protection rules from a project. + +```plaintext +GET /api/v4/projects/:id/registry/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 container registry protection rules. + +Can return the following status codes: + +- `200 OK`: A list of container registry protection rules. +- `401 Unauthorized`: The access token is invalid. +- `403 Forbidden`: The user does not have permission to list container registry 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/registry/protection/rules" +``` + +Example response: + +```json +[ + { + "id": 1, + "project_id": 7, + "repository_path_pattern": "flightjs/flight0", + "minimum_access_level_for_push": "maintainer", + "minimum_access_level_for_delete": "maintainer" + }, + { + "id": 2, + "project_id": 7, + "repository_path_pattern": "flightjs/flight1", + "minimum_access_level_for_push": "maintainer", + "minimum_access_level_for_delete": "maintainer" + }, +] +``` diff --git a/lib/api/api.rb b/lib/api/api.rb index 1eb4c60af12c755135de1e332a18ed73f1457c57..ff3a158fbe8fa6968b316bdb552b0c3ab09b67d0 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -302,6 +302,7 @@ def initialize(location_url) mount ::API::ProjectAvatar mount ::API::ProjectClusters mount ::API::ProjectContainerRepositories + mount ::API::ProjectContainerRegistryProtectionRules mount ::API::ProjectDebianDistributions mount ::API::ProjectEvents mount ::API::ProjectExport diff --git a/lib/api/entities/projects/container_registry/protection/rule.rb b/lib/api/entities/projects/container_registry/protection/rule.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d55d6504b14c2d4221207e72d79e1d0fb28bd59 --- /dev/null +++ b/lib/api/entities/projects/container_registry/protection/rule.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + module Projects + module ContainerRegistry + module Protection + class Rule < Grape::Entity + expose :id, documentation: { type: 'integer', example: 1 } + expose :project_id, documentation: { type: 'integer', example: 1 } + expose :repository_path_pattern, documentation: { type: 'string', example: 'flightjs/flight0' } + expose :minimum_access_level_for_push, documentation: { type: 'string', example: 'maintainer' } + expose :minimum_access_level_for_delete, documentation: { type: 'string', example: 'maintainer' } + end + end + end + end + end +end diff --git a/lib/api/project_container_registry_protection_rules.rb b/lib/api/project_container_registry_protection_rules.rb new file mode 100644 index 0000000000000000000000000000000000000000..233f88c3b4f7118ce311e9050acb94726e505c72 --- /dev/null +++ b/lib/api/project_container_registry_protection_rules.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module API + class ProjectContainerRegistryProtectionRules < ::API::Base + feature_category :container_registry + + after_validation do + if Feature.disabled?(:container_registry_protected_containers, user_project) + render_api_error!("'container_registry_protected_containers' feature flag is disabled", :not_found) + end + + authenticate! + authorize! :admin_container_image, user_project + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get list of container registry protection rules for a project' do + success Entities::Projects::ContainerRegistry::Protection::Rule + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' } + ] + tags %w[projects] + is_array true + hidden true + end + 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 + get ':id/registry/protection/rules' do + present user_project.container_registry_protection_rules, + with: Entities::Projects::ContainerRegistry::Protection::Rule + end + end + end +end diff --git a/spec/lib/api/entities/projects/container_registry/protection/rule_spec.rb b/spec/lib/api/entities/projects/container_registry/protection/rule_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..e39ba0383b0062f6c02a31bb1e9a52a74c8bc43c --- /dev/null +++ b/spec/lib/api/entities/projects/container_registry/protection/rule_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Entities::Projects::ContainerRegistry::Protection::Rule, feature_category: :container_registry do + let(:container_registry_protection_rule) { create(:container_registry_protection_rule) } + + subject(:entity) { described_class.new(container_registry_protection_rule).as_json } + + it 'exposes correct attributes' do + expect(entity.keys).to match_array [ + :id, + :project_id, + :repository_path_pattern, + :minimum_access_level_for_push, + :minimum_access_level_for_delete + ] + end +end diff --git a/spec/requests/api/project_container_registry_protection_rules_spec.rb b/spec/requests/api/project_container_registry_protection_rules_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..dd00a28519b2dfaec03d4f4d2fc55cd7aa7ab3db --- /dev/null +++ b/spec/requests/api/project_container_registry_protection_rules_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ProjectContainerRegistryProtectionRules, feature_category: :container_registry do + include ExclusiveLeaseHelpers + + let_it_be(:project) { create(:project, :private) } + let_it_be(:other_project) { create(:project, :private) } + let_it_be(:container_registry_protection_rule) { create(:container_registry_protection_rule, project: project) } + + 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 container protection rules request' do + using RSpec::Parameterized::TableSyntax + + where(:user_role, :status) do + :reporter | :forbidden + :developer | :forbidden + :guest | :forbidden + nil | :not_found + end + + with_them do + before do + project.send(:"add_#{user_role}", api_user) if user_role + end + + it_behaves_like 'returning response status', params[:status] + end + end + + describe 'GET /projects/:id/registry/protection/rules' do + let(:url) { "/projects/#{project.id}/registry/protection/rules" } + + subject(:get_container_registry_rules) { get(api(url, api_user)) } + + context 'when not enough permissions' do + it_behaves_like 'rejecting project container protection rules request' + end + + context 'for maintainer' do + let(:api_user) { maintainer } + + context 'with multiple container protection rules' do + let_it_be(:other_container_registry_protection_rule) do + create(:container_registry_protection_rule, + project: project, + repository_path_pattern: "#{container_registry_protection_rule.repository_path_pattern}-unique") + end + + it 'gets the container registry protection rules', :aggregate_failures do + get_container_registry_rules + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(2) + expect(json_response.pluck('id')).to match_array([container_registry_protection_rule.id, + other_container_registry_protection_rule.id]) + end + end + + it 'contains the content of a container registry protection rule', :aggregate_failures do + get_container_registry_rules + + expect(json_response.first).to include( + 'project_id' => container_registry_protection_rule.project.id, + 'repository_path_pattern' => container_registry_protection_rule.repository_path_pattern, + 'minimum_access_level_for_push' => container_registry_protection_rule.minimum_access_level_for_push, + 'minimum_access_level_for_delete' => container_registry_protection_rule.minimum_access_level_for_delete + ) + end + + context 'when the project id is invalid' do + let(:url) { '/projects/invalid/registry/protection/rules' } + + it_behaves_like 'returning response status', :not_found + end + + context 'when the project id does not exist' do + let(:url) { "/projects/#{non_existing_record_id}/registry/protection/rules" } + + it_behaves_like 'returning response status', :not_found + end + + context 'when container_registry_protected_containers is disabled' do + before do + stub_feature_flags(container_registry_protected_containers: false) + end + + it_behaves_like 'returning response status', :not_found + end + end + + context 'with invalid token' do + subject(:get_container_registry_rules) { get(api(url), headers: headers_with_invalid_token) } + + it_behaves_like 'returning response status', :unauthorized + end + end +end