Commit 85a85254 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch...

Merge branch '216088-disable-container-expiration-policy-when-invalid-regex-is-present' into 'master'

Validate regex before enqueuing a CleanupContainerRepositoryWorker job

See merge request gitlab-org/gitlab!34282
parents 1710adae 3ed2efb3
...@@ -52,4 +52,8 @@ class ContainerExpirationPolicy < ApplicationRecord ...@@ -52,4 +52,8 @@ class ContainerExpirationPolicy < ApplicationRecord
def set_next_run_at def set_next_run_at
self.next_run_at = Time.zone.now + ChronicDuration.parse(cadence).seconds self.next_run_at = Time.zone.now + ChronicDuration.parse(cadence).seconds
end end
def disable!
update_attribute(:enabled, false)
end
end end
# frozen_string_literal: true # frozen_string_literal: true
class ContainerExpirationPolicyService < BaseService class ContainerExpirationPolicyService < BaseService
InvalidPolicyError = Class.new(StandardError)
def execute(container_expiration_policy) def execute(container_expiration_policy)
unless container_expiration_policy.valid?
container_expiration_policy.disable!
raise InvalidPolicyError
end
container_expiration_policy.schedule_next_run! container_expiration_policy.schedule_next_run!
container_expiration_policy.container_repositories.find_each do |container_repository| container_expiration_policy.container_repositories.find_each do |container_repository|
......
...@@ -6,6 +6,7 @@ module Projects ...@@ -6,6 +6,7 @@ module Projects
def execute(container_repository) def execute(container_repository)
return error('feature disabled') unless can_use? return error('feature disabled') unless can_use?
return error('access denied') unless can_destroy? return error('access denied') unless can_destroy?
return error('invalid regex') unless valid_regex?
tags = container_repository.tags tags = container_repository.tags
tags = without_latest(tags) tags = without_latest(tags)
...@@ -76,6 +77,17 @@ module Projects ...@@ -76,6 +77,17 @@ module Projects
def can_use? def can_use?
Feature.enabled?(:container_registry_cleanup, project, default_enabled: true) Feature.enabled?(:container_registry_cleanup, project, default_enabled: true)
end end
def valid_regex?
%w(name_regex_delete name_regex name_regex_keep).each do |param_name|
regex = params[param_name]
Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
end
true
rescue RegexpError => e
Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
false
end
end end
end end
end end
...@@ -12,6 +12,8 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo ...@@ -12,6 +12,8 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
user: container_expiration_policy.project.owner) do |project:, user:| user: container_expiration_policy.project.owner) do |project:, user:|
ContainerExpirationPolicyService.new(project, user) ContainerExpirationPolicyService.new(project, user)
.execute(container_expiration_policy) .execute(container_expiration_policy)
rescue ContainerExpirationPolicyService::InvalidPolicyError => e
Gitlab::ErrorTracking.log_exception(e, container_expiration_policy_id: container_expiration_policy.id)
end end
end end
end end
......
---
title: Validate regex before sending them to CleanupContainerRepositoryWorker
merge_request: 34282
author:
type: added
...@@ -7,3 +7,4 @@ Grape::Validations.register_validator(:git_sha, ::API::Validations::Validators:: ...@@ -7,3 +7,4 @@ Grape::Validations.register_validator(:git_sha, ::API::Validations::Validators::
Grape::Validations.register_validator(:integer_none_any, ::API::Validations::Validators::IntegerNoneAny) Grape::Validations.register_validator(:integer_none_any, ::API::Validations::Validators::IntegerNoneAny)
Grape::Validations.register_validator(:array_none_any, ::API::Validations::Validators::ArrayNoneAny) Grape::Validations.register_validator(:array_none_any, ::API::Validations::Validators::ArrayNoneAny)
Grape::Validations.register_validator(:check_assignees_count, ::API::Validations::Validators::CheckAssigneesCount) Grape::Validations.register_validator(:check_assignees_count, ::API::Validations::Validators::CheckAssigneesCount)
Grape::Validations.register_validator(:untrusted_regexp, ::API::Validations::Validators::UntrustedRegexp)
...@@ -240,9 +240,9 @@ DELETE /projects/:id/registry/repositories/:repository_id/tags ...@@ -240,9 +240,9 @@ DELETE /projects/:id/registry/repositories/:repository_id/tags
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. |
| `repository_id` | integer | yes | The ID of registry repository. | | `repository_id` | integer | yes | The ID of registry repository. |
| `name_regex` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`. **Note:** `name_regex` is deprecated in favor of `name_regex_delete`.| | `name_regex` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`. **Note:** `name_regex` is deprecated in favor of `name_regex_delete`. This field is validated. |
| `name_regex_delete` | string | yes | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`.| | `name_regex_delete` | string | yes | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to delete. To delete all tags specify `.*`. This field is validated. |
| `name_regex_keep` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to keep. This value will override any matches from `name_regex_delete`. Note: setting to `.*` will result in a no-op. | | `name_regex_keep` | string | no | The [re2](https://github.com/google/re2/wiki/Syntax) regex of the name to keep. This value will override any matches from `name_regex_delete`. This field is validated. Note: setting to `.*` will result in a no-op. |
| `keep_n` | integer | no | The amount of latest tags of given name to keep. | | `keep_n` | integer | no | The amount of latest tags of given name to keep. |
| `older_than` | string | no | Tags to delete that are older than the given time, written in human readable form `1h`, `1d`, `1month`. | | `older_than` | string | no | Tags to delete that are older than the given time, written in human readable form `1h`, `1d`, `1month`. |
......
...@@ -70,11 +70,11 @@ module API ...@@ -70,11 +70,11 @@ module API
end end
params do params do
requires :repository_id, type: Integer, desc: 'The ID of the repository' requires :repository_id, type: Integer, desc: 'The ID of the repository'
optional :name_regex_delete, type: String, desc: 'The tag name regexp to delete, specify .* to delete all' optional :name_regex_delete, type: String, untrusted_regexp: true, desc: 'The tag name regexp to delete, specify .* to delete all'
optional :name_regex, type: String, desc: 'The tag name regexp to delete, specify .* to delete all' optional :name_regex, type: String, untrusted_regexp: true, desc: 'The tag name regexp to delete, specify .* to delete all'
# require either name_regex (deprecated) or name_regex_delete, it is ok to have both # require either name_regex (deprecated) or name_regex_delete, it is ok to have both
at_least_one_of :name_regex, :name_regex_delete at_least_one_of :name_regex, :name_regex_delete
optional :name_regex_keep, type: String, desc: 'The tag name regexp to retain' optional :name_regex_keep, type: String, untrusted_regexp: true, desc: 'The tag name regexp to retain'
optional :keep_n, type: Integer, desc: 'Keep n of latest tags with matching name' optional :keep_n, type: Integer, desc: 'Keep n of latest tags with matching name'
optional :older_than, type: String, desc: 'Delete older than: 1h, 1d, 1month' optional :older_than, type: String, desc: 'Delete older than: 1h, 1d, 1month'
end end
......
# frozen_string_literal: true
module API
module Validations
module Validators
class UntrustedRegexp < Grape::Validations::Base
def validate_param!(attr_name, params)
value = params[attr_name]
return unless value
Gitlab::UntrustedRegexp.new(value)
rescue RegexpError => e
message = "is an invalid regexp: #{e.message}"
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe API::Validations::Validators::UntrustedRegexp do
include ApiValidatorsHelpers
subject do
described_class.new(['test'], {}, false, scope.new)
end
context 'valid regex' do
it 'does not raise a validation error' do
expect_no_validation_error('test' => 'test')
expect_no_validation_error('test' => '.*')
expect_no_validation_error('test' => Gitlab::Regex.environment_name_regex_chars)
end
end
context 'invalid regex' do
it 'raises a validation error' do
expect_validation_error('test' => '[')
expect_validation_error('test' => '*foobar')
expect_validation_error('test' => '?foobar')
expect_validation_error('test' => '\A[^/%\s]+(..\z')
end
end
end
...@@ -103,4 +103,14 @@ RSpec.describe ContainerExpirationPolicy, type: :model do ...@@ -103,4 +103,14 @@ RSpec.describe ContainerExpirationPolicy, type: :model do
end end
end end
end end
describe '#disable!' do
let_it_be(:container_expiration_policy) { create(:container_expiration_policy) }
subject { container_expiration_policy.disable! }
it 'disables the container expiration policy' do
expect { subject }.to change { container_expiration_policy.reload.enabled }.from(true).to(false)
end
end
end end
...@@ -223,6 +223,40 @@ describe API::ProjectContainerRepositories do ...@@ -223,6 +223,40 @@ describe API::ProjectContainerRepositories do
expect(response).to have_gitlab_http_status(:accepted) expect(response).to have_gitlab_http_status(:accepted)
end end
end end
context 'with invalid regex' do
let(:invalid_regex) { '*v10.' }
let(:lease_key) { "container_repository:cleanup_tags:#{root_repository.id}" }
RSpec.shared_examples 'rejecting the invalid regex' do |param_name|
it 'does not enqueue a job' do
expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
subject
end
it_behaves_like 'returning response status', :bad_request
it 'returns an error message' do
subject
expect(json_response['error']).to include("#{param_name} is an invalid regexp")
end
end
before do
stub_last_activity_update
stub_exclusive_lease(lease_key, timeout: 1.hour)
end
%i[name_regex_delete name_regex name_regex_keep].each do |param_name|
context "for #{param_name}" do
let(:params) { { param_name => invalid_regex } }
it_behaves_like 'rejecting the invalid regex', param_name
end
end
end
end end
end end
......
...@@ -27,5 +27,20 @@ describe ContainerExpirationPolicyService do ...@@ -27,5 +27,20 @@ describe ContainerExpirationPolicyService do
expect(container_expiration_policy.next_run_at).to be > Time.zone.now expect(container_expiration_policy.next_run_at).to be > Time.zone.now
end end
context 'with an invalid container expiration policy' do
before do
allow(container_expiration_policy).to receive(:valid?).and_return(false)
end
it 'disables it' do
expect(container_expiration_policy).not_to receive(:schedule_next_run!)
expect(CleanupContainerRepositoryWorker).not_to receive(:perform_async)
expect { subject }
.to change { container_expiration_policy.reload.enabled }.from(true).to(false)
.and raise_error(ContainerExpirationPolicyService::InvalidPolicyError)
end
end
end end
end end
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
describe Projects::ContainerRepository::CleanupTagsService do describe Projects::ContainerRepository::CleanupTagsService do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :private) } let_it_be(:project, reload: true) { create(:project, :private) }
let_it_be(:repository) { create(:container_repository, :root, project: project) } let_it_be(:repository) { create(:container_repository, :root, project: project) }
let(:service) { described_class.new(project, user, params) } let(:service) { described_class.new(project, user, params) }
...@@ -72,6 +72,47 @@ describe Projects::ContainerRepository::CleanupTagsService do ...@@ -72,6 +72,47 @@ describe Projects::ContainerRepository::CleanupTagsService do
end end
end end
context 'with invalid regular expressions' do
RSpec.shared_examples 'handling an invalid regex' do
it 'keeps all tags' do
expect(Projects::ContainerRepository::DeleteTagsService)
.not_to receive(:new)
subject
end
it 'returns an error' do
response = subject
expect(response[:status]).to eq(:error)
expect(response[:message]).to eq('invalid regex')
end
it 'calls error tracking service' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).and_call_original
subject
end
end
context 'when name_regex_delete is invalid' do
let(:params) { { 'name_regex_delete' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
context 'when name_regex is invalid' do
let(:params) { { 'name_regex' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
context 'when name_regex_keep is invalid' do
let(:params) { { 'name_regex_keep' => '*test*' } }
it_behaves_like 'handling an invalid regex'
end
end
context 'when delete regex matching specific tags is used' do context 'when delete regex matching specific tags is used' do
let(:params) do let(:params) do
{ 'name_regex_delete' => 'C|D' } { 'name_regex_delete' => 'C|D' }
......
...@@ -53,5 +53,22 @@ describe ContainerExpirationPolicyWorker do ...@@ -53,5 +53,22 @@ describe ContainerExpirationPolicyWorker do
subject subject
end end
end end
context 'an invalid policy' do
let_it_be(:container_expiration_policy) { create(:container_expiration_policy, :runnable) }
let_it_be(:user) {container_expiration_policy.project.owner }
before do
container_expiration_policy.update_column(:name_regex, '*production')
end
it 'runs the policy and tracks an error' do
expect(ContainerExpirationPolicyService)
.to receive(:new).with(container_expiration_policy.project, user).and_call_original
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(ContainerExpirationPolicyService::InvalidPolicyError), container_expiration_policy_id: container_expiration_policy.id)
expect { subject }.to change { container_expiration_policy.reload.enabled }.from(true).to(false)
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment