diff --git a/doc/administration/packages/index.md b/doc/administration/packages/index.md index f33c215fdb8b80207520e9d33923c96c1316c296..536b6a5f246cfbca1f016ce26fb286351e77c8a0 100644 --- a/doc/administration/packages/index.md +++ b/doc/administration/packages/index.md @@ -119,9 +119,6 @@ upload packages: } ``` - NOTE: **Note:** - Some build tools, like Gradle, must make `HEAD` requests to Amazon S3 to pull a dependency’s metadata. The `gitlab_rails['packages_object_store_proxy_download']` property must be set to `true`. Without this setting, GitLab won't act as a proxy to the Amazon S3 service, and will instead return the signed URL. This will cause a `HTTP 403 Forbidden` response, since Amazon S3 expects a signed URL. - 1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect. diff --git a/ee/changelogs/unreleased/32102-fix-maven-head-redirects.yml b/ee/changelogs/unreleased/32102-fix-maven-head-redirects.yml new file mode 100644 index 0000000000000000000000000000000000000000..edb6a77707cdf6c83b20b139d527dd9a71e70a0f --- /dev/null +++ b/ee/changelogs/unreleased/32102-fix-maven-head-redirects.yml @@ -0,0 +1,6 @@ +--- +title: Maven packages API allows HEAD requests to package files when using Amazon + S3 as a object storage backend +merge_request: 27612 +author: +type: fixed diff --git a/ee/lib/api/maven_packages.rb b/ee/lib/api/maven_packages.rb index 21aaa2d8cf402d879e72f118b1309b850225e727..07d9487e95751c6410cbb04cf98136433f0aab20 100644 --- a/ee/lib/api/maven_packages.rb +++ b/ee/lib/api/maven_packages.rb @@ -50,6 +50,31 @@ def find_project_by_path(path) def jar_file?(format) format == 'jar' end + + def present_carrierwave_file_with_head_support!(file, supports_direct_download: true) + if head_request_on_aws_file?(file, supports_direct_download) && !file.file_storage? + return redirect(signed_head_url(file)) + end + + present_carrierwave_file!(file, supports_direct_download: supports_direct_download) + end + + def signed_head_url(file) + fog_storage = ::Fog::Storage.new(file.fog_credentials) + fog_dir = fog_storage.directories.new(key: file.fog_directory) + fog_file = fog_dir.files.new(key: file.path) + expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration + + fog_file.collection.head_url(fog_file.key, expire_at) + end + + def head_request_on_aws_file?(file, supports_direct_download) + Gitlab.config.packages.object_store.enabled && + supports_direct_download && + file.class.direct_download_enabled? && + request.head? && + file.fog_credentials[:provider] == 'AWS' + end end desc 'Download the maven package file at instance level' do @@ -85,8 +110,7 @@ def jar_file?(format) package_file.file_sha1 else track_event('pull_package') if jar_file?(format) - - present_carrierwave_file!(package_file.file) + present_carrierwave_file_with_head_support!(package_file.file) end end @@ -126,7 +150,7 @@ def jar_file?(format) else track_event('pull_package') if jar_file?(format) - present_carrierwave_file!(package_file.file) + present_carrierwave_file_with_head_support!(package_file.file) end end end @@ -166,7 +190,7 @@ def jar_file?(format) else track_event('pull_package') if jar_file?(format) - present_carrierwave_file!(package_file.file) + present_carrierwave_file_with_head_support!(package_file.file) end end diff --git a/ee/spec/requests/api/maven_packages_spec.rb b/ee/spec/requests/api/maven_packages_spec.rb index 64f9d03e5559ce9e316aa097be8f3ebdfadce8e4..a6211de5f18ea3c9eb699f25c4a1f22b47079a36 100644 --- a/ee/spec/requests/api/maven_packages_spec.rb +++ b/ee/spec/requests/api/maven_packages_spec.rb @@ -29,6 +29,54 @@ end end + shared_examples 'processing HEAD requests' do + subject { head api(url) } + + before do + allow_any_instance_of(::Packages::PackageFileUploader).to receive(:fog_credentials).and_return(object_storage_credentials) + stub_package_file_object_storage(enabled: object_storage_enabled) + end + + context 'with object storage enabled' do + let(:object_storage_enabled) { true } + + before do + allow_any_instance_of(::Packages::PackageFileUploader).to receive(:file_storage?).and_return(false) + end + + context 'non AWS provider' do + let(:object_storage_credentials) { { provider: 'Google' } } + + it 'does not generated a signed url for head' do + expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url) + + subject + end + end + + context 'with AWS provider' do + let(:object_storage_credentials) { { provider: 'AWS', aws_access_key_id: 'test', aws_secret_access_key: 'test' } } + + it 'generates a signed url for head' do + expect_any_instance_of(Fog::AWS::Storage::Files).to receive(:head_url).and_call_original + + subject + end + end + end + + context 'with object storage disabled' do + let(:object_storage_enabled) { false } + let(:object_storage_credentials) { {} } + + it 'does not generate a signed url for head' do + expect_any_instance_of(Fog::AWS::Storage::Files).not_to receive(:head_url) + + subject + end + end + end + describe 'GET /api/v4/packages/maven/*path/:file_name' do let(:package) { create(:maven_package, project: project, name: project.full_path) } @@ -149,6 +197,15 @@ def download_file_with_token(file_name, params = {}, request_headers = headers_w end end + describe 'HEAD /api/v4/packages/maven/*path/:file_name' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:package) { create(:maven_package, project: project, name: project.full_path) } + let_it_be(:package_file) { package.package_files.where('file_name like ?', '%.xml').first } + let(:url) { "/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" } + + it_behaves_like 'processing HEAD requests' + end + describe 'GET /api/v4/groups/:id/-/packages/maven/*path/:file_name' do before do project.team.truncate @@ -262,6 +319,16 @@ def download_file_with_token(file_name, params = {}, request_headers = headers_w end end + describe 'HEAD /api/v4/groups/:id/-/packages/maven/*path/:file_name' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :public, namespace: group) } + let_it_be(:package) { create(:maven_package, project: project, name: project.full_path) } + let_it_be(:package_file) { package.package_files.where('file_name like ?', '%.xml').first } + let(:url) { "/groups/#{group.id}/-/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" } + + it_behaves_like 'processing HEAD requests' + end + describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do context 'a public project' do subject { download_file(package_file.file_name) } @@ -340,6 +407,15 @@ def download_file_with_token(file_name, params = {}, request_headers = headers_w end end + describe 'HEAD /api/v4/projects/:id/packages/maven/*path/:file_name' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:package) { create(:maven_package, project: project, name: project.full_path) } + let_it_be(:package_file) { package.package_files.where('file_name like ?', '%.xml').first } + let(:url) { "/projects/#{project.id}/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" } + + it_behaves_like 'processing HEAD requests' + end + describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name/authorize' do it 'rejects a malicious request' do put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), params: {}, headers: headers_with_token