diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml index d57a9c944b9259880338817f4160653cf8304d8b..37e27ae737b71f2a5e3810e589d0745a3e84978b 100644 --- a/doc/api/openapi/openapi_v2.yaml +++ b/doc/api/openapi/openapi_v2.yaml @@ -30291,7 +30291,7 @@ paths: operationId: deleteApiV4ProjectsIdRegistryProtectionRepositoryRulesProtectionRuleId "/api/v4/projects/{id}/registry/protection/tag/rules": get: - summary: Gets a list of container protection tag rules for a project + summary: Gets a list of container protection tag rules for a project. description: This feature was introduced in GitLab 18.7. produces: - application/json @@ -30303,7 +30303,7 @@ paths: required: true responses: '200': - description: Gets a list of container protection tag rules for a project + description: Gets a list of container protection tag rules for a project. schema: type: array items: @@ -30317,6 +30317,44 @@ paths: tags: - projects operationId: getApiV4ProjectsIdRegistryProtectionTagRules + post: + summary: Create a container protection tag rule for a project. 5 rule limit + per project. + description: This feature was introduced in GitLab 18.7. + produces: + - application/json + consumes: + - application/json + parameters: + - in: path + name: id + description: The ID or URL-encoded path of the project. + type: string + required: true + - name: postApiV4ProjectsIdRegistryProtectionTagRules + in: body + required: true + schema: + "$ref": "#/definitions/postApiV4ProjectsIdRegistryProtectionTagRules" + responses: + '201': + description: Create a container protection tag rule for a project. 5 rule + limit per project. + schema: + "$ref": "#/definitions/API_Entities_Projects_ContainerRegistry_Protection_TagRule" + '400': + description: Bad Request + '401': + description: Unauthorized + '403': + description: Forbidden + '404': + description: Not Found + '422': + description: Unprocessable Entity + tags: + - projects + operationId: postApiV4ProjectsIdRegistryProtectionTagRules "/api/v4/projects/{id}/debian_distributions": post: summary: Create a Debian Distribution @@ -63062,6 +63100,35 @@ definitions: type: string example: maintainer description: API_Entities_Projects_ContainerRegistry_Protection_TagRule model + postApiV4ProjectsIdRegistryProtectionTagRules: + type: object + properties: + tag_name_pattern: + type: string + description: Container tag name pattern protected by the protection rule. + For example, `v*-release`. Wildcard character `*` allowed. + minimum_access_level_for_push: + type: string + description: Minimum GitLab access level required to push container tags. + For example, Maintainer, Owner, or Admin. + enum: + - maintainer + - owner + - admin + minimum_access_level_for_delete: + type: string + description: Minimum GitLab access level required to delete container tags. + For example, Maintainer, Owner, or Admin. + enum: + - maintainer + - owner + - admin + required: + - tag_name_pattern + - minimum_access_level_for_push + - minimum_access_level_for_delete + description: Create a container protection tag rule for a project. 5 rule limit + per project. postApiV4ProjectsIdDebianDistributions: type: object properties: diff --git a/lib/api/project_container_registry_protection_tag_rules.rb b/lib/api/project_container_registry_protection_tag_rules.rb index 515bf18934f4bc1d5dabdf65ce1637ef59a520c4..8b4fe7a410076a29df599db9e953ba984644beb2 100644 --- a/lib/api/project_container_registry_protection_tag_rules.rb +++ b/lib/api/project_container_registry_protection_tag_rules.rb @@ -15,7 +15,7 @@ class ProjectContainerRegistryProtectionTagRules < ::API::Base resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource ':id/registry/protection/tag/rules' do - desc 'Gets a list of container protection tag rules for a project' do + desc 'Gets a list of container protection tag rules for a project.' do detail 'This feature was introduced in GitLab 18.7.' success Entities::Projects::ContainerRegistry::Protection::TagRule failure [ @@ -30,6 +30,43 @@ class ProjectContainerRegistryProtectionTagRules < ::API::Base present user_project.container_registry_protection_tag_rules.mutable, with: Entities::Projects::ContainerRegistry::Protection::TagRule end + + desc 'Create a container protection tag rule for a project. 5 rule limit per project.' do + detail 'This feature was introduced in GitLab 18.7.' + success Entities::Projects::ContainerRegistry::Protection::TagRule + 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] + end + params do + requires :tag_name_pattern, type: String, + desc: 'Container tag name pattern protected by the protection rule. ' \ + 'For example, `v*-release`. Wildcard character `*` allowed.' + requires :minimum_access_level_for_push, type: String, + values: ContainerRegistry::Protection::TagRule.minimum_access_level_for_pushes.keys, + desc: 'Minimum GitLab access level required to push container tags. ' \ + 'For example, Maintainer, Owner, or Admin.' + requires :minimum_access_level_for_delete, type: String, + values: ContainerRegistry::Protection::TagRule.minimum_access_level_for_deletes.keys, + desc: 'Minimum GitLab access level required to delete container tags. ' \ + 'For example, Maintainer, Owner, or Admin.' + end + post do + response = + ::ContainerRegistry::Protection::CreateTagRuleService + .new(project: user_project, current_user: current_user, params: declared_params) + .execute + + render_api_error!(response.message, :unprocessable_entity) if response.error? + + present response[:container_protection_tag_rule], + with: Entities::Projects::ContainerRegistry::Protection::TagRule + end end end end diff --git a/spec/requests/api/project_container_registry_protection_tag_rules_spec.rb b/spec/requests/api/project_container_registry_protection_tag_rules_spec.rb index 1270983c61791c6d5a7bf534a87179d9b5ff7430..7a4687d7de33480a97178cfbee33a577556ac646 100644 --- a/spec/requests/api/project_container_registry_protection_tag_rules_spec.rb +++ b/spec/requests/api/project_container_registry_protection_tag_rules_spec.rb @@ -4,6 +4,8 @@ RSpec.describe API::ProjectContainerRegistryProtectionTagRules, :aggregate_failures, feature_category: :container_registry do + include ContainerRegistryHelpers + let_it_be(:project) { create(:project, :private) } let_it_be(:other_project) { create(:project, :private) } let_it_be(:tag_rule) do @@ -14,11 +16,13 @@ let_it_be(:admin) { create(:admin) } let_it_be(:api_user) { create(:user) } - let_it_be(:path) { 'registry/protection/tag/rules' } - let_it_be(:url) { "/projects/#{project.id}/#{path}" } let_it_be(:invalid_token) { 'invalid-token123' } + let_it_be(:headers_with_invalid_token) { { Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => invalid_token } } describe 'GET /projects/:id/registry/protection/tag/rules' do + let(:path) { 'registry/protection/tag/rules' } + let(:url) { "/projects/#{project.id}/#{path}" } + subject(:get_tag_rules) { get(api(url, api_user)) } shared_examples 'returning tag protection rules' do @@ -84,7 +88,119 @@ end context 'with invalid token' do - subject(:get_tag_rules) { get(api(url), headers: { 'PRIVATE-TOKEN' => invalid_token }) } + subject(:get_tag_rules) { get(api(url), headers: headers_with_invalid_token) } + + it_behaves_like 'returning response status', :unauthorized + end + end + + describe 'POST /projects/:id/registry/protection/tag/rules' do + let(:path) { 'registry/protection/tag/rules' } + let(:url) { "/projects/#{project.id}/#{path}" } + + let(:params) do + { + tag_name_pattern: 'release-*', + minimum_access_level_for_push: 'maintainer', + minimum_access_level_for_delete: 'owner' + } + end + + subject(:post_tag_rule) { post(api(url, api_user), params: params) } + + before do + stub_gitlab_api_client_to_support_gitlab_api(supported: true) + end + + shared_examples 'allowed to create tag protection rule' do + it 'creates a tag protection rule' do + expect { post_tag_rule }.to change { ContainerRegistry::Protection::TagRule.count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'tag_name_pattern' => 'release-*', + 'minimum_access_level_for_push' => 'maintainer', + 'minimum_access_level_for_delete' => 'owner' + ) + end + + context 'with missing tag_name_pattern' do + let(:params) { super().except(:tag_name_pattern) } + + it_behaves_like 'not creating a tag protection rule', :bad_request + end + + context 'with missing minimum_access_level_for_push' do + let(:params) { super().except(:minimum_access_level_for_push) } + + it_behaves_like 'not creating a tag protection rule', :bad_request + end + + context 'with missing minimum_access_level_for_delete' do + let(:params) { super().except(:minimum_access_level_for_delete) } + + it_behaves_like 'not creating a tag protection rule', :bad_request + end + + context 'with invalid minimum_access_level_for_push' do + let(:params) { super().merge(minimum_access_level_for_push: 'invalid_role') } + + it_behaves_like 'not creating a tag protection rule', :bad_request + end + + context 'with invalid minimum_access_level_for_delete' do + let(:params) { super().merge(minimum_access_level_for_delete: 'invalid_role') } + + it_behaves_like 'not creating a tag protection rule', :bad_request + end + + context 'with already existing tag_name_pattern' do + let(:params) { super().merge(tag_name_pattern: tag_rule.tag_name_pattern) } + + it_behaves_like 'not creating a tag protection rule', :unprocessable_entity + end + + context 'when the GitLab API is not supported' do + before do + stub_gitlab_api_client_to_support_gitlab_api(supported: false) + end + + it_behaves_like 'not creating a tag protection rule', :unprocessable_entity + + it 'returns error message' do + post_tag_rule + + expect(json_response).to eq({ 'message' => 'GitLab container registry API not supported' }) + end + end + + it_behaves_like 'rejecting protection rules request when invalid project' + end + + shared_examples 'not creating a tag protection rule' do |status| + it "does not create a tag protection rule and returns #{status}" do + expect { post_tag_rule }.to not_change(ContainerRegistry::Protection::TagRule, :count) + + expect(response).to have_gitlab_http_status(status) + end + end + + it_behaves_like 'rejecting project protection rules request when not enough permissions' + + context 'for maintainer' do + let(:api_user) { maintainer } + + it_behaves_like 'allowed to create tag protection rule' + end + + context 'for admin' do + subject(:post_tag_rule) { post(api(url, admin, admin_mode: true), params: params) } + + it_behaves_like 'allowed to create tag protection rule' + end + + context 'with invalid token' do + subject(:post_tag_rule) { post(api(url), headers: headers_with_invalid_token, params: params) } it_behaves_like 'returning response status', :unauthorized end