diff --git a/doc/api/project_container_registry_protection_rules.md b/doc/api/project_container_registry_protection_rules.md index cdbd8be23ee3835a30f5077a25e94edd1b38aa45..5aa07210481ae6b127c79c45de7b6b5a6b973011 100644 --- a/doc/api/project_container_registry_protection_rules.md +++ b/doc/api/project_container_registry_protection_rules.md @@ -115,3 +115,46 @@ curl --request POST \ "minimum_access_level_for_delete": "maintainer" }' ``` + +## Update a container registry protection rule + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/457518) in GitLab 17.2. + +Update a container registry protection rule for a project. + +```plaintext +PATCH /api/v4/projects/:id/registry/protection/rules/: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. | +| `protection_rule_id` | integer | Yes | ID of the protection rule to be updated. | +| `repository_path_pattern` | string | No | Container repository path pattern protected by the protection rule. For example `flight/flight-*`. Wildcard character `*` allowed. | +| `minimum_access_level_for_push` | string | No | Minimum GitLab access level to allow to push container images to the container registry. For example `maintainer`, `owner` or `admin`. Must be provided when `minimum_access_level_for_delete` is not set. To unset the value, use an empty string `""`. | +| `minimum_access_level_for_delete` | string | No | Minimum GitLab access level to allow to delete container images in the container registry. For example `maintainer`, `owner`, `admin`. Must be provided when `minimum_access_level_for_push` is not set. To unset the value, use an empty string `""`. | + +If successful, returns [`200`](rest/index.md#status-codes) and the updated protection rule. + +Can return the following status codes: + +- `200 OK`: The protection rule was patched successfully. +- `400 Bad Request`: The patch is invalid. +- `401 Unauthorized`: The access token is invalid. +- `403 Forbidden`: The user does not have permission to patch the protection rule. +- `404 Not Found`: The project was not found. +- `422 Unprocessable Entity`: The protection rule could not be patched, for example, because the `repository_path_pattern` is already taken. + +Example request: + +```shell +curl --request PATCH \ + --header "PRIVATE-TOKEN: " \ + --header "Content-Type: application/json" \ + --url "https://gitlab.example.com/api/v4/projects/7/registry/protection/rules/32" \ + --data '{ + "repository_path_pattern": "flight/flight-*" + }' +``` diff --git a/lib/api/project_container_registry_protection_rules.rb b/lib/api/project_container_registry_protection_rules.rb index 55520481e120e3e16042c846d3a10605900e065b..4bac0cefde28fcfce7d554cc7246610750101173 100644 --- a/lib/api/project_container_registry_protection_rules.rb +++ b/lib/api/project_container_registry_protection_rules.rb @@ -69,6 +69,48 @@ class ProjectContainerRegistryProtectionRules < ::API::Base present response[:container_registry_protection_rule], with: Entities::Projects::ContainerRegistry::Protection::Rule end + + params do + requires :protection_rule_id, type: Integer, + desc: 'The ID of the container protection rule' + end + resource ':protection_rule_id' do + desc 'Update a container protection rule for a project' do + success Entities::Projects::ContainerRegistry::Protection::Rule + failure [ + { code: 400, message: 'Bad Request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not Found' }, + { code: 422, message: 'Unprocessable Entity' } + ] + tags %w[projects] + hidden true + end + params do + optional :repository_path_pattern, type: String, + desc: 'Container repository path pattern protected by the protection rule. + For example `flight/flight-*`. Wildcard character `*` allowed.' + optional :minimum_access_level_for_push, type: String, + values: ContainerRegistry::Protection::Rule.minimum_access_level_for_pushes.keys << "", + desc: 'Minimum GitLab access level to allow to push container images to the container registry. + For example maintainer, owner or admin. To unset the value, use an empty string `""`.' + optional :minimum_access_level_for_delete, type: String, + values: ContainerRegistry::Protection::Rule.minimum_access_level_for_deletes.keys << "", + desc: 'Minimum GitLab access level to allow to delete container images in the container registry. + For example maintainer, owner or admin. To unset the value, use an empty string `""`.' + end + patch do + protection_rule = user_project.container_registry_protection_rules.find(params[:protection_rule_id]) + response = ::ContainerRegistry::Protection::UpdateRuleService.new(protection_rule, + current_user: current_user, params: declared_params(include_missing: false)).execute + + render_api_error!({ error: response.message }, :unprocessable_entity) if response.error? + + present response[:container_registry_protection_rule], + with: Entities::Projects::ContainerRegistry::Protection::Rule + end + end end 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 index 4c29daf73ec5948aa4b3407a6226e3a613309b05..1aba9958b384c83e7528889a20cffde950d71d25 100644 --- a/spec/requests/api/project_container_registry_protection_rules_spec.rb +++ b/spec/requests/api/project_container_registry_protection_rules_spec.rb @@ -62,6 +62,26 @@ end end + shared_examples 'rejecting container registry protection rules request when handling rule ids' do + context 'when the rule id is invalid' do + let(:url) { "/projects/#{project.id}/registry/protection/rules/invalid" } + + it_behaves_like 'returning response status', :bad_request + end + + context 'when the rule id does not exist' do + let(:url) { "/projects/#{project.id}/registry/protection/rules/#{non_existing_record_id}" } + + it_behaves_like 'returning response status', :not_found + end + + context 'when the container registry protection rule does belong to another project' do + let(:url) { "/projects/#{other_project.id}/registry/protection/rules/#{container_registry_protection_rule.id}" } + + it_behaves_like 'returning response status', :not_found + end + end + describe 'GET /projects/:id/registry/protection/rules' do let(:url) { "/projects/#{project.id}/registry/protection/rules" } @@ -201,4 +221,126 @@ it_behaves_like 'returning response status', :unauthorized end end + + describe 'PATCH /projects/:id/registry/protection/rules/:protection_rule_id' do + let(:url) { "/projects/#{project.id}/registry/protection/rules/#{container_registry_protection_rule.id}" } + + subject(:patch_container_registry_protection_rule) { patch(api(url, api_user), params: params) } + + it_behaves_like 'rejecting project container protection rules request when not enough permissions' + + context 'for maintainer' do + let(:api_user) { maintainer } + let_it_be(:changed_repository_path_pattern) do + "#{container_registry_protection_rule.repository_path_pattern}-changed" + end + + context 'with full changeset' do + before do + params[:repository_path_pattern] = changed_repository_path_pattern + end + + it 'updates a container registry protection rule' do + patch_container_registry_protection_rule + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to match(hash_including({ + 'project_id' => container_registry_protection_rule.project.id, + 'repository_path_pattern' => changed_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 + end + + context 'with a single change' do + let(:params) { { repository_path_pattern: changed_repository_path_pattern } } + + it 'updates a container registry protection rule' do + patch_container_registry_protection_rule + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response["repository_path_pattern"]).to eq(changed_repository_path_pattern) + end + end + + context 'with minimum_access_level_to_push set to nil' do + before do + params[:minimum_access_level_for_push] = "" + end + + it 'clears the minimum_access_level_to_push' do + patch_container_registry_protection_rule + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response["minimum_access_level_for_push"]).to be_nil + end + + context 'with minimum_access_level_to_delete set to nil as well' do + before do + params[:minimum_access_level_for_delete] = "" + end + + it_behaves_like 'returning response status', :unprocessable_entity + end + end + + context 'with minimum_access_level_to_delete set to nil' do + before do + params[:minimum_access_level_for_delete] = "" + end + + it 'clears the minimum_access_level_to_delete' do + patch_container_registry_protection_rule + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response["minimum_access_level_for_delete"]).to be_nil + end + + context 'with minimum_access_level_to_push set to nil as well' do + before do + params[:minimum_access_level_for_push] = "" + end + + it_behaves_like 'returning response status', :unprocessable_entity + end + end + + context 'with invalid repository_path_pattern' do + before do + params[:repository_path_pattern] = "not in enum" + end + + it_behaves_like 'returning response status', :unprocessable_entity + end + + context 'with invalid minimum_access_level_for_push' do + before do + params[:minimum_access_level_for_push] = "not in enum" + end + + it_behaves_like 'returning response status', :bad_request + end + + context 'with already existing repository_path_pattern' do + before do + other_protection_rule = create(:container_registry_protection_rule, project: project, + repository_path_pattern: "#{project.full_path}/path") + params[:repository_path_pattern] = other_protection_rule.repository_path_pattern + end + + it_behaves_like 'returning response status', :unprocessable_entity + end + + it_behaves_like 'rejecting container registry protection rules request when handling rule ids' + it_behaves_like 'rejecting container registry protection rules request when enough permissions' + end + + context 'with invalid token' do + subject(:patch_container_registry_protection_rule) do + patch(api(url), headers: headers_with_invalid_token, params: params) + end + + it_behaves_like 'returning response status', :unauthorized + end + end end