222 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			222 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| require 'spec_helper'
 | |
| 
 | |
| describe Gitlab::GitAccessSnippet do
 | |
|   include ProjectHelpers
 | |
|   include TermsHelper
 | |
|   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(: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 { pull_access_check }.to raise_forbidden(described_class::ERROR_MESSAGES[:authentication_mechanism])
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   describe 'when feature flag :version_snippets is disabled' do
 | |
|     before do
 | |
|       stub_feature_flags(version_snippets: false)
 | |
|     end
 | |
| 
 | |
|     it 'does not allow push and pull access' do
 | |
|       expect { pull_access_check }.to raise_project_not_found
 | |
|     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_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_snippet_not_found
 | |
|       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(message)
 | |
|       expect { pull_access_check }.to raise_forbidden(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
 | |
|   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'
 | |
|     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'
 | |
|     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_error(described_class::NotFoundError)
 | |
|             expect { pull_access_check }.to raise_error(described_class::NotFoundError)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     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, :_expected_count) do
 | |
|       permission_table_for_personal_snippet_access
 | |
|     end
 | |
| 
 | |
|     with_them do
 | |
|       it "respects accessibility" do
 | |
|         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
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   context 'when geo is enabled', if: Gitlab.ee? do
 | |
|     let(:user) { snippet.author }
 | |
|     let!(:primary_node) { FactoryBot.create(:geo_node, :primary) }
 | |
| 
 | |
|     # Without override, push access would return Gitlab::GitAccessResult::CustomAction
 | |
|     it 'skips geo for snippet' do
 | |
|       allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
 | |
|       allow(::Gitlab::Geo).to receive(:secondary_with_primary?).and_return(true)
 | |
| 
 | |
|       expect { push_access_check }.to raise_forbidden(/You can't push code to a read-only GitLab instance/)
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   context 'when changes are specific' do
 | |
|     let(:changes) { 'oldrev newrev ref' }
 | |
|     let(:user) { snippet.author }
 | |
| 
 | |
|     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(:exec).and_call_original
 | |
|       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(:exec).and_raise(Gitlab::GitAccess::ForbiddenError, 'foo')
 | |
|       end
 | |
| 
 | |
|       expect { push_access_check }.to raise_forbidden('foo')
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   private
 | |
| 
 | |
|   def raise_snippet_not_found
 | |
|     raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:snippet_not_found])
 | |
|   end
 | |
| 
 | |
|   def raise_project_not_found
 | |
|     raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
 | |
|   end
 | |
| 
 | |
|   def raise_forbidden(message)
 | |
|     raise_error(Gitlab::GitAccess::ForbiddenError, message)
 | |
|   end
 | |
| end
 |