From 3a4912ca82357813c3ae7652442e5c3f0eaf229e Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Sat, 8 Jun 2024 06:30:03 -0700 Subject: [PATCH 1/4] Protected containers: Add GET to REST API for container protection rules Adds a GET route to the REST API to retrieve a list of container protection rules. Issue https://gitlab.com/gitlab-org/gitlab/-/issues/457518 Changelog: added --- ...ect_container_registry_protection_rules.md | 73 +++++++++++++++++ lib/api/api.rb | 1 + .../container_registry/protection/rule.rb | 19 +++++ ...ect_container_registry_protection_rules.rb | 36 +++++++++ .../protection/rule_spec.rb | 19 +++++ ...ontainer_registry_protection_rules_spec.rb | 80 +++++++++++++++++++ 6 files changed, 228 insertions(+) create mode 100644 doc/api/project_container_registry_protection_rules.md create mode 100644 lib/api/entities/projects/container_registry/protection/rule.rb create mode 100644 lib/api/project_container_registry_protection_rules.rb create mode 100644 spec/lib/api/entities/projects/container_registry/protection/rule_spec.rb create mode 100644 spec/requests/api/project_container_registry_protection_rules_spec.rb 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 00000000000000..7232c2a2c8ee78 --- /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:** GitLab.com, Self-managed +**Status:** Experiment + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155798) in GitLab 17.1 [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 manages the protection rules for container registries within 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 1eb4c60af12c75..ff3a158fbe8fa6 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 00000000000000..8d55d6504b14c2 --- /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 00000000000000..e27eeb1781ff22 --- /dev/null +++ b/lib/api/project_container_registry_protection_rules.rb @@ -0,0 +1,36 @@ +# 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 + + 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 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 + end + 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 00000000000000..e39ba0383b0062 --- /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 00000000000000..91acdb9d0daf27 --- /dev/null +++ b/spec/requests/api/project_container_registry_protection_rules_spec.rb @@ -0,0 +1,80 @@ +# 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 registry 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/registry/protection/rules' do + let(:url) { "/projects/#{project.id}/registry/protection/rules" } + + subject(:get_container_registry_rules) { get(api(url, api_user)) } + + it_behaves_like 'rejecting project container registry protection rules request', :reporter, :forbidden + it_behaves_like 'rejecting project container registry protection rules request', :developer, :forbidden + it_behaves_like 'rejecting project container registry protection rules request', :guest, :forbidden + it_behaves_like 'rejecting project container registry protection rules request', nil, :not_found + + context 'for maintainer' do + let(:api_user) { maintainer } + + 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' do + get_container_registry_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/registry/protection/rules" } + + it_behaves_like 'rejecting project container registry protection rules request', :maintainer, :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 'rejecting project container registry protection rules request', :maintainer, :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 'rejecting project container registry protection rules request', :maintainer, :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 'rejecting project container registry protection rules request', nil, :unauthorized + end + end +end -- GitLab From 6bbd849a9399beb149d8a05183d9496bb04021db Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Wed, 12 Jun 2024 05:07:51 -0700 Subject: [PATCH 2/4] Protected containers: Add GET to REST API for container protection rules Adds a GET route to the REST API to retrieve a list of container protection rules. Based on review feedback this commit removes GitLab.com from the Offering list and removes repetitiveness from the specs. Issue https://gitlab.com/gitlab-org/gitlab/-/issues/457518 Changelog: added --- ...ect_container_registry_protection_rules.md | 4 +-- ...ontainer_registry_protection_rules_spec.rb | 35 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/doc/api/project_container_registry_protection_rules.md b/doc/api/project_container_registry_protection_rules.md index 7232c2a2c8ee78..ab9119abd475c3 100644 --- a/doc/api/project_container_registry_protection_rules.md +++ b/doc/api/project_container_registry_protection_rules.md @@ -9,7 +9,7 @@ description: "Documentation for the REST API for container registry protection r DETAILS: **Tier:** Free, Premium, Ultimate -**Offering:** GitLab.com, Self-managed +**Offering:** Self-managed **Status:** Experiment > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155798) in GitLab 17.1 [with a flag](../administration/feature_flags.md) named `container_registry_protected_containers`. Disabled by default. @@ -19,7 +19,7 @@ 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 container registries within a project. This feature is an experiment. +This API endpoint manages the protection rules for container registries in a project. This feature is an experiment. ## List container registry protection rules diff --git a/spec/requests/api/project_container_registry_protection_rules_spec.rb b/spec/requests/api/project_container_registry_protection_rules_spec.rb index 91acdb9d0daf27..ec759bf8ecbf26 100644 --- a/spec/requests/api/project_container_registry_protection_rules_spec.rb +++ b/spec/requests/api/project_container_registry_protection_rules_spec.rb @@ -15,13 +15,22 @@ 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 registry protection rules request' do |user_role, status| - context "for #{user_role}" do + 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', status + it_behaves_like 'returning response status', params[:status] end end @@ -30,20 +39,20 @@ subject(:get_container_registry_rules) { get(api(url, api_user)) } - it_behaves_like 'rejecting project container registry protection rules request', :reporter, :forbidden - it_behaves_like 'rejecting project container registry protection rules request', :developer, :forbidden - it_behaves_like 'rejecting project container registry protection rules request', :guest, :forbidden - it_behaves_like 'rejecting project container registry protection rules request', nil, :not_found + context 'when not enough permissions' do + it_behaves_like 'rejecting project container protection rules request' + end context 'for maintainer' do let(:api_user) { maintainer } let_it_be(:other_container_registry_protection_rule) do - create(:container_registry_protection_rule, project: project, + 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' do + it 'gets the container registry protection rules', :aggregate_failures do get_container_registry_rules expect(response).to have_gitlab_http_status(:ok) @@ -53,13 +62,13 @@ context 'when the project id is invalid' do let(:url) { "/projects/invalid/registry/protection/rules" } - it_behaves_like 'rejecting project container registry protection rules request', :maintainer, :not_found + 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 'rejecting project container registry protection rules request', :maintainer, :not_found + it_behaves_like 'returning response status', :not_found end context 'when container_registry_protected_containers is disabled' do @@ -67,14 +76,14 @@ stub_feature_flags(container_registry_protected_containers: false) end - it_behaves_like 'rejecting project container registry protection rules request', :maintainer, :not_found + 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 'rejecting project container registry protection rules request', nil, :unauthorized + it_behaves_like 'returning response status', :unauthorized end end end -- GitLab From a4346876f60ae38a4348a4da00b4880cbcb84163 Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Mon, 17 Jun 2024 12:56:36 -0700 Subject: [PATCH 3/4] Protected containers: Add GET to REST API for container protection rules Adds a GET route to the REST API to retrieve a list of container protection rules. Based on review feedback this specs the content of the json response. Issue https://gitlab.com/gitlab-org/gitlab/-/issues/457518 Changelog: added --- ...ect_container_registry_protection_rules.md | 2 +- ...ect_container_registry_protection_rules.rb | 10 ------ ...ontainer_registry_protection_rules_spec.rb | 36 ++++++++++++++----- 3 files changed, 29 insertions(+), 19 deletions(-) diff --git a/doc/api/project_container_registry_protection_rules.md b/doc/api/project_container_registry_protection_rules.md index ab9119abd475c3..99a0e1fad659ff 100644 --- a/doc/api/project_container_registry_protection_rules.md +++ b/doc/api/project_container_registry_protection_rules.md @@ -12,7 +12,7 @@ DETAILS: **Offering:** Self-managed **Status:** Experiment -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155798) in GitLab 17.1 [with a flag](../administration/feature_flags.md) named `container_registry_protected_containers`. Disabled by default. +> - [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. diff --git a/lib/api/project_container_registry_protection_rules.rb b/lib/api/project_container_registry_protection_rules.rb index e27eeb1781ff22..70a2671a259c27 100644 --- a/lib/api/project_container_registry_protection_rules.rb +++ b/lib/api/project_container_registry_protection_rules.rb @@ -17,16 +17,6 @@ class ProjectContainerRegistryProtectionRules < ::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 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 - end get ':id/registry/protection/rules' do present user_project.container_registry_protection_rules, with: Entities::Projects::ContainerRegistry::Protection::Rule diff --git a/spec/requests/api/project_container_registry_protection_rules_spec.rb b/spec/requests/api/project_container_registry_protection_rules_spec.rb index ec759bf8ecbf26..8489d72427cfac 100644 --- a/spec/requests/api/project_container_registry_protection_rules_spec.rb +++ b/spec/requests/api/project_container_registry_protection_rules_spec.rb @@ -46,21 +46,41 @@ context 'for maintainer' do let(:api_user) { maintainer } - 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") + 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 'gets the container registry protection rules', :aggregate_failures do + it 'contains the content of a container registry protection rule', :aggregate_failures do get_container_registry_rules - expect(response).to have_gitlab_http_status(:ok) - expect(json_response.count).to eq(2) + json_protection_rule = json_response.first + expect(json_protection_rule['project_id']).to eq(container_registry_protection_rule.project.id) + expect(json_protection_rule['repository_path_pattern']).to( + eq(container_registry_protection_rule.repository_path_pattern) + ) + expect(json_protection_rule['minimum_access_level_for_push']).to( + eq(container_registry_protection_rule.minimum_access_level_for_push) + ) + expect(json_protection_rule['minimum_access_level_for_delete']).to( + eq(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" } + let(:url) { '/projects/invalid/registry/protection/rules' } it_behaves_like 'returning response status', :not_found end -- GitLab From eb7c386e62c01bf0e3c3549922ba325bbb15ee16 Mon Sep 17 00:00:00 2001 From: Nicholas Wittstruck Date: Sun, 23 Jun 2024 14:43:27 +0000 Subject: [PATCH 4/4] Protected containers: Add GET to REST API for container protection rules Adds a GET route to the REST API to retrieve a list of container protection rules. Based on review feedback this uses an include matcher to check for the content of the protection rule. Issues https://gitlab.com/gitlab-org/gitlab/-/issues/457518 Changelog: added --- ...project_container_registry_protection_rules.rb | 13 +++++++++++++ ...ct_container_registry_protection_rules_spec.rb | 15 +++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/api/project_container_registry_protection_rules.rb b/lib/api/project_container_registry_protection_rules.rb index 70a2671a259c27..233f88c3b4f711 100644 --- a/lib/api/project_container_registry_protection_rules.rb +++ b/lib/api/project_container_registry_protection_rules.rb @@ -13,6 +13,19 @@ class ProjectContainerRegistryProtectionRules < ::API::Base 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 diff --git a/spec/requests/api/project_container_registry_protection_rules_spec.rb b/spec/requests/api/project_container_registry_protection_rules_spec.rb index 8489d72427cfac..dd00a28519b2df 100644 --- a/spec/requests/api/project_container_registry_protection_rules_spec.rb +++ b/spec/requests/api/project_container_registry_protection_rules_spec.rb @@ -66,16 +66,11 @@ it 'contains the content of a container registry protection rule', :aggregate_failures do get_container_registry_rules - json_protection_rule = json_response.first - expect(json_protection_rule['project_id']).to eq(container_registry_protection_rule.project.id) - expect(json_protection_rule['repository_path_pattern']).to( - eq(container_registry_protection_rule.repository_path_pattern) - ) - expect(json_protection_rule['minimum_access_level_for_push']).to( - eq(container_registry_protection_rule.minimum_access_level_for_push) - ) - expect(json_protection_rule['minimum_access_level_for_delete']).to( - eq(container_registry_protection_rule.minimum_access_level_for_delete) + 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 -- GitLab