gitlab-org--gitlab-foss/spec/lib/gitlab/git_access_snippet_spec.rb

447 lines
14 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GitAccessSnippet do
include ProjectHelpers
include TermsHelper
include AdminModeHelper
include_context 'ProjectPolicyTable context'
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:snippet) { create(:project_snippet, :public, :repository, project: project) }
let_it_be(:migration_bot) { User.migration_bot }
let(:repository) { snippet.repository }
let(:actor) { user }
let(:protocol) { 'ssh' }
let(:changes) { Gitlab::GitAccess::ANY }
let(:authentication_abilities) { [:download_code, :push_code] }
let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) }
subject(:access) { Gitlab::GitAccessSnippet.new(actor, snippet, protocol, authentication_abilities: authentication_abilities) }
describe 'when actor is a DeployKey' do
let(:actor) { build(:deploy_key) }
it 'does not allow push and pull access' do
expect { push_access_check }.to raise_forbidden(:authentication_mechanism)
expect { pull_access_check }.to raise_forbidden(:authentication_mechanism)
end
end
describe 'when snippet repository is read-only' do
it 'does not allow push and allows pull access' do
allow(snippet).to receive(:repository_read_only?).and_return(true)
expect { push_access_check }.to raise_forbidden(:read_only)
expect { pull_access_check }.not_to raise_error
end
end
shared_examples 'actor is migration bot' do
context 'when user is the migration bot' do
let(:user) { migration_bot }
it 'can perform git operations' do
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
end
end
describe '#check_snippet_accessibility!' do
context 'when the snippet exists' do
it 'allows access' do
project.add_developer(actor)
expect { pull_access_check }.not_to raise_error
end
end
context 'when the snippet is nil' do
let(:snippet) { nil }
it 'blocks access with "not found"' do
expect { pull_access_check }.to raise_not_found(:snippet_not_found)
end
end
context 'when the snippet does not have a repository' do
let(:snippet) { build_stubbed(:personal_snippet) }
it 'blocks access with "not found"' do
expect { pull_access_check }.to raise_not_found(:no_repo)
end
end
end
context 'terms are enforced', :aggregate_failures do
before do
enforce_terms
end
let(:user) { snippet.author }
it 'blocks access when the user did not accept terms' do
message = /must accept the Terms of Service in order to perform this action/
expect { push_access_check }.to raise_forbidden_with_message(message)
expect { pull_access_check }.to raise_forbidden_with_message(message)
end
it 'allows access when the user accepted the terms' do
accept_terms(user)
expect { push_access_check }.not_to raise_error
expect { pull_access_check }.not_to raise_error
end
it_behaves_like 'actor is migration bot' do
before do
expect(migration_bot.required_terms_not_accepted?).to be_truthy
end
end
end
context 'project snippet accessibility', :aggregate_failures do
let(:snippet) { create(:project_snippet, :private, :repository, project: project) }
let(:user) { membership == :author ? snippet.author : create_user_from_membership(project, membership) }
shared_examples_for 'checks accessibility' do
[:anonymous, :non_member, :guest, :reporter, :maintainer, :admin, :author].each do |membership|
context membership.to_s do
let(:membership) { membership }
it 'respects accessibility' do
if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error
else
expect { push_access_check }.to raise_error(described_class::ForbiddenError)
end
if Ability.allowed?(user, :read_snippet, snippet)
expect { pull_access_check }.not_to raise_error
else
expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end
end
end
end
end
context 'when project is public' do
it_behaves_like 'checks accessibility'
it_behaves_like 'actor is migration bot'
end
context 'when project is public but snippet feature is private' do
let(:project) { create(:project, :public) }
before do
update_feature_access_level(project, :private)
end
it_behaves_like 'checks accessibility'
it_behaves_like 'actor is migration bot'
end
context 'when project is not accessible' do
let(:project) { create(:project, :private) }
[:anonymous, :non_member].each do |membership|
context membership.to_s do
let(:membership) { membership }
it 'respects accessibility' do
expect { push_access_check }.to raise_not_found(:project_not_found)
expect { pull_access_check }.to raise_not_found(:project_not_found)
end
end
end
it_behaves_like 'actor is migration bot'
end
context 'when project is archived' do
let(:project) { create(:project, :public, :archived) }
[:anonymous, :non_member].each do |membership|
context membership.to_s do
let(:membership) { membership }
it 'cannot perform git operations' do
expect { push_access_check }.to raise_error(described_class::ForbiddenError)
expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end
end
end
[:guest, :reporter, :maintainer, :author].each do |membership|
context membership.to_s do
let(:membership) { membership }
it 'cannot perform git pushes' do
expect { push_access_check }.to raise_error(described_class::ForbiddenError)
expect { pull_access_check }.not_to raise_error
end
end
end
context 'admin' do
let(:membership) { :admin }
context 'when admin mode is enabled', :enable_admin_mode do
it 'cannot perform git pushes' do
expect { push_access_check }.to raise_error(described_class::ForbiddenError)
expect { pull_access_check }.not_to raise_error
end
end
context 'when admin mode is disabled' do
it 'cannot perform git operations' do
expect { push_access_check }.to raise_error(described_class::ForbiddenError)
expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end
end
end
it_behaves_like 'actor is migration bot'
end
context 'when snippet feature is disabled' do
let(:project) { create(:project, :public, :snippets_disabled) }
[:anonymous, :non_member, :author, :admin].each do |membership|
context membership.to_s do
let(:membership) { membership }
it 'cannot perform git operations' do
expect { push_access_check }.to raise_error(described_class::ForbiddenError)
expect { pull_access_check }.to raise_error(described_class::ForbiddenError)
end
end
end
it_behaves_like 'actor is migration bot'
end
end
context 'personal snippet accessibility', :aggregate_failures do
let(:snippet) { create(:personal_snippet, snippet_level, :repository) }
let(:user) { membership == :author ? snippet.author : create_user_from_membership(nil, membership) }
where(:snippet_level, :membership, :admin_mode, :_expected_count) do
permission_table_for_personal_snippet_access
end
with_them do
it "respects accessibility" do
enable_admin_mode!(user) if admin_mode
error_class = described_class::ForbiddenError
if Ability.allowed?(user, :update_snippet, snippet)
expect { push_access_check }.not_to raise_error
else
expect { push_access_check }.to raise_error(error_class)
end
if Ability.allowed?(user, :read_snippet, snippet)
expect { pull_access_check }.not_to raise_error
else
expect { pull_access_check }.to raise_error(error_class)
end
end
it_behaves_like 'actor is migration bot'
end
end
context 'when changes are specific' do
let(:changes) { "2d1db523e11e777e49377cfb22d368deec3f0793 ddd0f15ae83993f5cb66a927a28673882e99100b master" }
let(:user) { snippet.author }
shared_examples 'snippet checks' do
it 'does not raise error if SnippetCheck does not raise error' do
expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
expect(check).to receive(:validate!).and_call_original
end
expect_next_instance_of(Gitlab::Checks::PushFileCountCheck) do |check|
expect(check).to receive(:validate!)
end
expect { push_access_check }.not_to raise_error
end
it 'raises error if SnippetCheck raises error' do
expect_next_instance_of(Gitlab::Checks::SnippetCheck) do |check|
allow(check).to receive(:validate!).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo')
end
expect { push_access_check }.to raise_forbidden_with_message('foo')
end
it 'sets the file count limit from Snippet class' do
service = double
expect(service).to receive(:validate!).and_return(nil)
expect(Snippet).to receive(:max_file_limit).and_return(5)
expect(Gitlab::Checks::PushFileCountCheck).to receive(:new).with(anything, hash_including(limit: 5)).and_return(service)
push_access_check
end
end
it_behaves_like 'snippet checks'
context 'when user is migration bot' do
let(:user) { migration_bot }
it_behaves_like 'snippet checks'
end
end
describe 'repository size restrictions' do
let(:snippet) { create(:personal_snippet, :public, :repository) }
let(:actor) { snippet.author }
let(:oldrev) { TestEnv::BRANCH_SHA["snippet/single-file"] }
let(:newrev) { TestEnv::BRANCH_SHA["snippet/edit-file"] }
let(:ref) { "refs/heads/snippet/edit-file" }
let(:changes) { "#{oldrev} #{newrev} #{ref}" }
shared_examples 'migration bot does not err' do
let(:actor) { migration_bot }
it 'does not err' do
expect(snippet.repository_size_checker).not_to receive(:above_size_limit?)
expect { push_access_check }.not_to raise_error
end
end
shared_examples_for 'a push to repository already over the limit' do
it 'errs' do
expect(snippet.repository_size_checker).to receive(:above_size_limit?).and_return(true)
expect do
push_access_check
end.to raise_error(described_class::ForbiddenError, /Your push has been rejected/)
end
it_behaves_like 'migration bot does not err'
end
shared_examples_for 'a push to repository below the limit' do
it 'does not err' do
expect(snippet.repository_size_checker).to receive(:above_size_limit?).and_return(false)
expect(snippet.repository_size_checker)
.to receive(:changes_will_exceed_size_limit?)
.with(change_size)
.and_return(false)
expect { push_access_check }.not_to raise_error
end
it_behaves_like 'migration bot does not err'
end
shared_examples_for 'a push to repository to make it over the limit' do
it 'errs' do
expect(snippet.repository_size_checker).to receive(:above_size_limit?).and_return(false)
expect(snippet.repository_size_checker)
.to receive(:changes_will_exceed_size_limit?)
.with(change_size)
.and_return(true)
expect do
push_access_check
end.to raise_error(described_class::ForbiddenError, /Your push to this repository would cause it to exceed the size limit/)
end
it_behaves_like 'migration bot does not err'
end
context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is set' do
let(:change_size) { 100 }
before do
allow(Gitlab::Git::HookEnv)
.to receive(:all)
.with(repository.gl_repository)
.and_return({ 'GIT_OBJECT_DIRECTORY_RELATIVE' => 'objects' })
# Stub the object directory size to "simulate" quarantine size
allow(repository).to receive(:object_directory_size).and_return(change_size)
end
it_behaves_like 'a push to repository already over the limit'
it_behaves_like 'a push to repository below the limit'
it_behaves_like 'a push to repository to make it over the limit'
end
context 'when GIT_OBJECT_DIRECTORY_RELATIVE env var is not set' do
let(:change_size) { 200 }
before do
allow(snippet.repository).to receive(:new_blobs).and_return(
[double(:blob, size: change_size)]
)
end
it_behaves_like 'a push to repository already over the limit'
it_behaves_like 'a push to repository below the limit'
it_behaves_like 'a push to repository to make it over the limit'
end
end
describe 'HEAD realignment' do
let_it_be(:snippet) { create(:project_snippet, :private, :repository, project: project) }
shared_examples 'HEAD is updated to the snippet default branch' do
let(:actor) { snippet.author }
specify do
expect(snippet).to receive(:change_head_to_default_branch).and_call_original
subject
end
context 'when an error is raised' do
let(:actor) { nil }
it 'does not realign HEAD' do
expect(snippet).not_to receive(:change_head_to_default_branch).and_call_original
expect { subject }.to raise_error(described_class::ForbiddenError)
end
end
end
it_behaves_like 'HEAD is updated to the snippet default branch' do
subject { push_access_check }
end
it_behaves_like 'HEAD is updated to the snippet default branch' do
subject { pull_access_check }
end
end
private
def raise_not_found(message_key)
raise_error(described_class::NotFoundError, described_class.error_message(message_key))
end
def raise_forbidden(message_key)
raise_error(Gitlab::GitAccess::ForbiddenError, described_class.error_message(message_key))
end
def raise_forbidden_with_message(message)
raise_error(Gitlab::GitAccess::ForbiddenError, message)
end
end