Commit 3f9a2a5d authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'security-fj-add-snippet-repository-validation-bundle-import' into 'master'

Lack of validations importing snippet repository from bundle

Closes #159

See merge request gitlab-org/security/gitlab!608
parents 65c1f1a4 9f625452
# frozen_string_literal: true
module Snippets
class RepositoryValidationService
attr_reader :current_user, :snippet, :repository
RepositoryValidationError = Class.new(StandardError)
def initialize(user, snippet)
@current_user = user
@snippet = snippet
@repository = snippet.repository
end
def execute
if snippet.nil?
return service_response_error('No snippet found.', 404)
end
check_branch_count!
check_branch_name_default!
check_tag_count!
check_file_count!
check_size!
ServiceResponse.success(message: 'Valid snippet repository.')
rescue RepositoryValidationError => e
ServiceResponse.error(message: "Error: #{e.message}", http_status: 400)
end
private
def check_branch_count!
return if repository.branch_count == 1
raise RepositoryValidationError, _('Repository has more than one branch.')
end
def check_branch_name_default!
branches = repository.branch_names
return if branches.first == Gitlab::Checks::SnippetCheck::DEFAULT_BRANCH
raise RepositoryValidationError, _('Repository has an invalid default branch name.')
end
def check_tag_count!
return if repository.tag_count == 0
raise RepositoryValidationError, _('Repository has tags.')
end
def check_file_count!
file_count = repository.ls_files(nil).size
limit = Snippet.max_file_limit(current_user)
if file_count > limit
raise RepositoryValidationError, _('Repository files count over the limit')
end
if file_count == 0
raise RepositoryValidationError, _('Repository must contain at least 1 file.')
end
end
def check_size!
return unless snippet.repository_size_checker.above_size_limit?
raise RepositoryValidationError, _('Repository size is above the limit.')
end
end
end
---
title: Add snippet repository validation after bundle import
merge_request:
author:
type: security
......@@ -3,7 +3,7 @@
module Gitlab
module ImportExport
class SnippetRepoRestorer < RepoRestorer
attr_reader :snippet
attr_reader :snippet, :user
SnippetRepositoryError = Class.new(StandardError)
......@@ -33,6 +33,16 @@ module Gitlab
def create_repository_from_bundle
repository.create_from_bundle(path_to_bundle)
snippet.track_snippet_repository(repository.storage)
response = Snippets::RepositoryValidationService.new(user, snippet).execute
if response.error?
repository.remove
snippet.snippet_repository.delete
snippet.repository.expire_exists_cache
raise SnippetRepositoryError, _("Invalid repository bundle for snippet with id %{snippet_id}") % { snippet_id: snippet.id }
end
end
def create_repository_from_db
......
......@@ -12431,6 +12431,9 @@ msgstr ""
msgid "Invalid query"
msgstr ""
msgid "Invalid repository bundle for snippet with id %{snippet_id}"
msgstr ""
msgid "Invalid repository path"
msgstr ""
......@@ -19201,15 +19204,33 @@ msgstr ""
msgid "Repository cleanup has started. You will receive an email once the cleanup operation is complete."
msgstr ""
msgid "Repository files count over the limit"
msgstr ""
msgid "Repository has an invalid default branch name."
msgstr ""
msgid "Repository has more than one branch."
msgstr ""
msgid "Repository has no locks."
msgstr ""
msgid "Repository has tags."
msgstr ""
msgid "Repository maintenance"
msgstr ""
msgid "Repository mirroring"
msgstr ""
msgid "Repository must contain at least 1 file."
msgstr ""
msgid "Repository size is above the limit."
msgstr ""
msgid "Repository static objects"
msgstr ""
......
......@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, project: project, author: user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:snippet) { create(:project_snippet, project: project, author: user) }
let(:shared) { project.import_export_shared }
let(:exporter) { Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: user) }
let(:restorer) do
......@@ -57,33 +57,63 @@ RSpec.describe Gitlab::ImportExport::SnippetRepoRestorer do
it_behaves_like 'no bundle file present'
end
context 'when the snippet bundle exists' do
let!(:snippet_with_repo) { create(:project_snippet, :repository, project: project) }
context 'when the snippet repository bundle exists' do
let!(:snippet_with_repo) { create(:project_snippet, :repository, project: project, author: user) }
let(:bundle_path) { ::Gitlab::ImportExport.snippets_repo_bundle_path(shared.export_path) }
let(:snippet_bundle_path) { File.join(bundle_path, "#{snippet_with_repo.hexdigest}.bundle") }
let(:result) { exporter.save }
let(:repository) { snippet.repository }
before do
expect(exporter.save).to be_truthy
end
it 'creates the repository from the bundle' do
expect(snippet.repository_exists?).to be_falsey
expect(snippet.snippet_repository).to be_nil
expect(snippet.repository).to receive(:create_from_bundle).and_call_original
context 'when it is valid' do
before do
allow(repository).to receive(:branch_count).and_return(1)
allow(repository).to receive(:tag_count).and_return(0)
allow(repository).to receive(:branch_names).and_return(['master'])
allow(repository).to receive(:ls_files).and_return(['foo'])
end
expect(restorer.restore).to be_truthy
expect(snippet.repository_exists?).to be_truthy
expect(snippet.snippet_repository).not_to be_nil
end
it 'creates the repository from the bundle' do
expect(snippet.repository_exists?).to be_falsey
expect(snippet.snippet_repository).to be_nil
expect(repository).to receive(:create_from_bundle).and_call_original
it 'sets same shard in snippet repository as in the repository storage' do
expect(snippet).to receive(:repository_storage).and_return('picked')
expect(snippet.repository).to receive(:create_from_bundle)
expect(restorer.restore).to be_truthy
expect(snippet.repository_exists?).to be_truthy
expect(snippet.snippet_repository).not_to be_nil
end
restorer.restore
it 'sets same shard in snippet repository as in the repository storage' do
expect(repository).to receive(:storage).and_return('picked')
expect(repository).to receive(:create_from_bundle)
expect(snippet.snippet_repository.shard_name).to eq 'picked'
expect(restorer.restore).to be_truthy
expect(snippet.snippet_repository.shard_name).to eq 'picked'
end
end
context 'when it is invalid' do
it 'returns false and deletes the repository from disk and the database' do
gitlab_shell = Gitlab::Shell.new
shard_name = snippet.repository.shard
path = snippet.disk_path + '.git'
error_response = ServiceResponse.error(message: 'Foo', http_status: 400)
allow_next_instance_of(Snippets::RepositoryValidationService) do |instance|
allow(instance).to receive(:execute).and_return(error_response)
end
aggregate_failures do
expect(restorer.restore).to be false
expect(shared.errors.first).to match(/Invalid repository bundle/)
expect(snippet.repository_exists?).to eq false
expect(snippet.reload.snippet_repository).to be_nil
expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false
end
end
end
end
end
......@@ -38,6 +38,7 @@ RSpec.describe Gitlab::ImportExport::SnippetsRepoRestorer do
expect(snippet1.repository_exists?).to be false
expect(snippet2.repository_exists?).to be false
allow_any_instance_of(Snippets::RepositoryValidationService).to receive(:execute).and_return(ServiceResponse.success)
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet1, path_to_bundle: bundle_path(snippet1))).and_call_original
expect(Gitlab::ImportExport::SnippetRepoRestorer).to receive(:new).with(hash_including(snippet: snippet2, path_to_bundle: bundle_path(snippet2))).and_call_original
expect(restorer.restore).to be_truthy
......
# frozen_string_literal: true
require 'spec_helper'
describe Snippets::RepositoryValidationService do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :empty_repo, author: user) }
let(:repository) { snippet.repository }
let(:service) { described_class.new(user, snippet) }
subject { service.execute }
before do
allow(repository).to receive(:branch_count).and_return(1)
allow(repository).to receive(:ls_files).and_return(['foo'])
allow(repository).to receive(:branch_names).and_return(['master'])
end
it 'returns error when the repository has more than one branch' do
allow(repository).to receive(:branch_count).and_return(2)
expect(subject).to be_error
expect(subject.message).to match /Repository has more than one branch/
end
it 'returns error when existing branch name is not the default one' do
allow(repository).to receive(:branch_names).and_return(['foo'])
expect(subject).to be_error
expect(subject.message).to match /Repository has an invalid default branch name/
end
it 'returns error when the repository has tags' do
allow(repository).to receive(:tag_count).and_return(1)
expect(subject).to be_error
expect(subject.message).to match /Repository has tags/
end
it 'returns error when the repository has more file than the limit' do
limit = Snippet.max_file_limit(user) + 1
files = Array.new(limit) { FFaker::Filesystem.file_name }
allow(repository).to receive(:ls_files).and_return(files)
expect(subject).to be_error
expect(subject.message).to match /Repository files count over the limit/
end
it 'returns error when the repository has no files' do
allow(repository).to receive(:ls_files).and_return([])
expect(subject).to be_error
expect(subject.message).to match /Repository must contain at least 1 file/
end
it 'returns error when the repository size is over the limit' do
expect_any_instance_of(Gitlab::RepositorySizeChecker).to receive(:above_size_limit?).and_return(true)
expect(subject).to be_error
expect(subject.message).to match /Repository size is above the limit/
end
it 'returns success when no validation errors are raised' do
expect(subject).to be_success
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