# frozen_string_literal: true require 'spec_helper' RSpec.describe Projects::RawController do include RepoHelpers let_it_be(:project) { create(:project, :public, :repository) } let(:inline) { nil } describe 'GET #show' do def get_show get(:show, params: { namespace_id: project.namespace, project_id: project, id: filepath, inline: inline }) end subject { get_show } shared_examples 'single Gitaly request' do it 'makes a single Gitaly request', :request_store, :clean_gitlab_redis_cache do # Warm up to populate repository cache get_show RequestStore.clear! expect { get_show }.to change { Gitlab::GitalyClient.get_request_count }.by(1) end end context 'regular filename' do let(:filepath) { 'master/README.md' } it 'delivers ASCII file' do subject expect(response).to have_gitlab_http_status(:ok) expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq 'true' expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end it_behaves_like 'project cache control headers' it_behaves_like 'content disposition headers' include_examples 'single Gitaly request' end context 'image header' do let(:filepath) { 'master/files/images/6049019_460s.jpg' } it 'leaves image content disposition' do subject expect(response).to have_gitlab_http_status(:ok) expect(response.header[Gitlab::Workhorse::DETECT_HEADER]).to eq "true" expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:') end it_behaves_like 'project cache control headers' it_behaves_like 'content disposition headers' include_examples 'single Gitaly request' end context 'with LFS files' do let(:filename) { 'lfs_object.iso' } let(:filepath) { "be93687/files/lfs/#{filename}" } it_behaves_like 'a controller that can serve LFS files' it_behaves_like 'project cache control headers' include_examples 'single Gitaly request' end context 'when the endpoint receives requests above the limit', :clean_gitlab_redis_cache do let(:file_path) { 'master/README.md' } before do stub_application_setting(raw_blob_request_limit: 5) end it 'prevents from accessing the raw file', :request_store do execute_raw_requests(requests: 5, project: project, file_path: file_path) expect { execute_raw_requests(requests: 1, project: project, file_path: file_path) } .to change { Gitlab::GitalyClient.get_request_count }.by(0) expect(response.body).to eq(_('You cannot access the raw file. Please wait a minute.')) expect(response).to have_gitlab_http_status(:too_many_requests) end it 'logs the event on auth.log' do attributes = { message: 'Application_Rate_Limiter_Request', env: :raw_blob_request_limit, remote_ip: '0.0.0.0', request_method: 'GET', path: "/#{project.full_path}/-/raw/#{file_path}" } expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once execute_raw_requests(requests: 6, project: project, file_path: file_path) end context 'when receiving an external storage request' do let(:token) { 'letmein' } before do stub_application_setting( static_objects_external_storage_url: 'https://cdn.gitlab.com', static_objects_external_storage_auth_token: token ) end it 'does not prevent from accessing the raw file' do request.headers['X-Gitlab-External-Storage-Token'] = token execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(response).to have_gitlab_http_status(:ok) end end context 'when the request uses a different version of a commit' do it 'prevents from accessing the raw file' do # 3 times with the normal sha commit_sha = project.repository.commit.sha file_path = "#{commit_sha}/README.md" execute_raw_requests(requests: 3, project: project, file_path: file_path) # 3 times with the modified version modified_sha = commit_sha.gsub(commit_sha[0..5], commit_sha[0..5].upcase) modified_path = "#{modified_sha}/README.md" execute_raw_requests(requests: 3, project: project, file_path: modified_path) expect(response.body).to eq(_('You cannot access the raw file. Please wait a minute.')) expect(response).to have_gitlab_http_status(:too_many_requests) end end context 'when the throttling has been disabled' do before do stub_application_setting(raw_blob_request_limit: 0) end it 'does not prevent from accessing the raw file' do execute_raw_requests(requests: 10, project: project, file_path: file_path) expect(response).to have_gitlab_http_status(:ok) end end context 'with case-sensitive files' do it 'prevents from accessing the specific file' do create_file_in_repo(project, 'master', 'master', 'readme.md', 'Add readme.md') create_file_in_repo(project, 'master', 'master', 'README.md', 'Add README.md') commit_sha = project.repository.commit.sha file_path = "#{commit_sha}/readme.md" # Accessing downcase version of readme execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(response.body).to eq(_('You cannot access the raw file. Please wait a minute.')) expect(response).to have_gitlab_http_status(:too_many_requests) # Accessing upcase version of readme file_path = "#{commit_sha}/README.md" execute_raw_requests(requests: 1, project: project, file_path: file_path) expect(response).to have_gitlab_http_status(:ok) end end end context 'as a sessionless user' do let_it_be(:project) { create(:project, :private, :repository) } let_it_be(:user) { create(:user, static_object_token: 'very-secure-token') } let_it_be(:file_path) { 'master/README.md' } before do project.add_developer(user) end context 'when no token is provided' do it 'redirects to sign in page' do execute_raw_requests(requests: 1, project: project, file_path: file_path) expect(response).to have_gitlab_http_status(:found) expect(response.location).to end_with('/users/sign_in') end end context 'when a token param is present' do context 'when token is correct' do it 'calls the action normally' do execute_raw_requests(requests: 1, project: project, file_path: file_path, token: user.static_object_token) expect(response).to have_gitlab_http_status(:ok) end end context 'when token is incorrect' do it 'redirects to sign in page' do execute_raw_requests(requests: 1, project: project, file_path: file_path, token: 'foobar') expect(response).to have_gitlab_http_status(:found) expect(response.location).to end_with('/users/sign_in') end end end context 'when a token header is present' do context 'when token is correct' do it 'calls the action normally' do request.headers['X-Gitlab-Static-Object-Token'] = user.static_object_token execute_raw_requests(requests: 1, project: project, file_path: file_path) expect(response).to have_gitlab_http_status(:ok) end end context 'when token is incorrect' do it 'redirects to sign in page' do request.headers['X-Gitlab-Static-Object-Token'] = 'foobar' execute_raw_requests(requests: 1, project: project, file_path: file_path) expect(response).to have_gitlab_http_status(:found) expect(response.location).to end_with('/users/sign_in') end end end end describe 'caching' do def request_file get(:show, params: { namespace_id: project.namespace, project_id: project, id: 'master/README.md' }) end it 'sets appropriate caching headers' do sign_in create(:user) request_file expect(response.cache_control[:public]).to eq(true) expect(response.cache_control[:max_age]).to eq(60) expect(response.cache_control[:no_store]).to be_nil end context 'when a public project has private repo' do let(:project) { create(:project, :public, :repository, :repository_private) } let(:user) { create(:user, maintainer_projects: [project]) } it 'does not set public caching header' do sign_in user request_file expect(response.header['Cache-Control']).to include('max-age=60, private') end end context 'when If-None-Match header is set' do it 'returns a 304 status' do request_file request.headers['If-None-Match'] = response.headers['ETag'] request_file expect(response).to have_gitlab_http_status(:not_modified) end end end end def execute_raw_requests(requests:, project:, file_path:, **params) requests.times do get :show, params: { namespace_id: project.namespace, project_id: project, id: file_path }.merge(params) end end end