1184 lines
43 KiB
Ruby
1184 lines
43 KiB
Ruby
# frozen_string_literal: true
|
|
require 'spec_helper'
|
|
|
|
RSpec.describe API::MavenPackages, feature_category: :package_registry do
|
|
using RSpec::Parameterized::TableSyntax
|
|
include WorkhorseHelpers
|
|
|
|
include_context 'workhorse headers'
|
|
|
|
let_it_be_with_refind(:package_settings) { create(:namespace_package_setting, :group) }
|
|
let_it_be_with_refind(:group) { package_settings.namespace }
|
|
let_it_be(:user) { create(:user) }
|
|
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
|
|
let_it_be(:package, reload: true) { create(:maven_package, project: project, name: project.full_path) }
|
|
let_it_be(:maven_metadatum, reload: true) { package.maven_metadatum }
|
|
let_it_be(:package_file) { package.package_files.with_file_name_like('%.xml').first }
|
|
let_it_be(:jar_file) { package.package_files.with_file_name_like('%.jar').first }
|
|
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
|
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running, project: project) }
|
|
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
|
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
|
|
let_it_be(:deploy_token_for_group) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
|
|
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token_for_group, group: group) }
|
|
|
|
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, user: user, property: 'i_package_maven_user' } }
|
|
|
|
let(:package_name) { 'com/example/my-app' }
|
|
let(:headers) { workhorse_headers }
|
|
let(:headers_with_token) { headers.merge('Private-Token' => personal_access_token.token) }
|
|
let(:group_deploy_token_headers) { { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token_for_group.token } }
|
|
|
|
let(:headers_with_deploy_token) do
|
|
headers.merge(
|
|
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
|
|
)
|
|
end
|
|
|
|
let(:version) { '1.0-SNAPSHOT' }
|
|
let(:param_path) { "#{package_name}/#{version}" }
|
|
|
|
before do
|
|
project.add_developer(user)
|
|
end
|
|
|
|
shared_examples 'handling groups and subgroups for' do |shared_example_name, visibilities: { public: :redirect }|
|
|
context 'within a group' do
|
|
visibilities.each do |visibility, not_found_response|
|
|
context "that is #{visibility}" do
|
|
before do
|
|
group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
|
|
end
|
|
|
|
it_behaves_like shared_example_name, not_found_response
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'within a subgroup' do
|
|
let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
|
|
|
|
before do
|
|
move_project_to_namespace(subgroup)
|
|
end
|
|
|
|
visibilities.each do |visibility, not_found_response|
|
|
context "that is #{visibility}" do
|
|
before do
|
|
subgroup.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
|
|
group.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
|
|
end
|
|
|
|
it_behaves_like shared_example_name, not_found_response
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'handling groups, subgroups and user namespaces for' do |shared_example_name, visibilities: { public: :redirect }|
|
|
it_behaves_like 'handling groups and subgroups for', shared_example_name, visibilities: visibilities
|
|
|
|
context 'within a user namespace' do
|
|
before do
|
|
move_project_to_namespace(user.namespace)
|
|
end
|
|
|
|
visibilities.each do |visibility|
|
|
context "that is #{visibility}" do
|
|
before do
|
|
user.namespace.update!(visibility_level: Gitlab::VisibilityLevel.level_value(visibility.to_s))
|
|
end
|
|
|
|
it_behaves_like shared_example_name
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'tracking the file download event' do
|
|
context 'with jar file' do
|
|
let_it_be(:package_file) { jar_file }
|
|
|
|
it_behaves_like 'a package tracking event', described_class.name, 'pull_package'
|
|
end
|
|
end
|
|
|
|
shared_examples 'processing HEAD requests' do |instance_level: false|
|
|
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
|
|
|
|
expect(response).to have_gitlab_http_status(:redirect)
|
|
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
|
|
|
|
context 'with a non existing maven path' do
|
|
let(:path) { 'foo/bar/1.2.3' }
|
|
|
|
it_behaves_like 'returning response status', instance_level ? :forbidden : :redirect
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'downloads with a deploy token' do
|
|
context 'successful download' do
|
|
subject do
|
|
download_file(
|
|
file_name: package_file.file_name,
|
|
request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token }
|
|
)
|
|
end
|
|
|
|
it 'allows download with deploy token' do
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('application/octet-stream')
|
|
end
|
|
|
|
it 'allows download with deploy token with only write_package_registry scope' do
|
|
deploy_token.update!(read_package_registry: false)
|
|
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('application/octet-stream')
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'downloads with a job token' do
|
|
context 'with a running job' do
|
|
it 'allows download with job token' do
|
|
download_file(file_name: package_file.file_name, params: { job_token: job.token })
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('application/octet-stream')
|
|
end
|
|
end
|
|
|
|
context 'with a finished job' do
|
|
before do
|
|
job.update!(status: :failed)
|
|
end
|
|
|
|
it 'returns unauthorized error' do
|
|
download_file(file_name: package_file.file_name, params: { job_token: job.token })
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'successfully returning the file' do
|
|
it 'returns the file', :aggregate_failures do
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('application/octet-stream')
|
|
end
|
|
end
|
|
|
|
shared_examples 'file download in FIPS mode' do
|
|
context 'in FIPS mode', :fips_mode do
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
it 'rejects the request for an md5 file' do
|
|
download_file(file_name: package_file.file_name + '.md5')
|
|
|
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'forwarding package requests' do
|
|
context 'request forwarding' do
|
|
include_context 'dependency proxy helpers context'
|
|
|
|
subject { download_file(file_name: package_name) }
|
|
|
|
shared_examples 'redirecting the request' do
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
|
|
shared_examples 'package not found' do
|
|
it_behaves_like 'returning response status', :not_found
|
|
end
|
|
|
|
where(:forward, :package_in_project, :shared_examples_name) do
|
|
true | true | 'successfully returning the file'
|
|
true | false | 'redirecting the request'
|
|
false | true | 'successfully returning the file'
|
|
false | false | 'package not found'
|
|
end
|
|
|
|
with_them do
|
|
let(:package_name) { package_in_project ? package_file.file_name : 'foo' }
|
|
|
|
before do
|
|
allow_fetch_cascade_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward)
|
|
end
|
|
|
|
it_behaves_like params[:shared_examples_name]
|
|
end
|
|
|
|
context 'with maven_central_request_forwarding disabled' do
|
|
where(:forward, :package_in_project, :shared_examples_name) do
|
|
true | true | 'successfully returning the file'
|
|
true | false | 'package not found'
|
|
false | true | 'successfully returning the file'
|
|
false | false | 'package not found'
|
|
end
|
|
|
|
with_them do
|
|
let(:package_name) { package_in_project ? package_file.file_name : 'foo' }
|
|
|
|
before do
|
|
stub_feature_flags(maven_central_request_forwarding: false)
|
|
allow_fetch_cascade_application_setting(attribute: 'maven_package_requests_forwarding', return_value: forward)
|
|
end
|
|
|
|
it_behaves_like params[:shared_examples_name]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v4/packages/maven/*path/:file_name' do
|
|
context 'a public project' do
|
|
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
|
|
|
|
subject { download_file(file_name: package_file.file_name) }
|
|
|
|
shared_examples 'getting a file' do
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'bumping the package last downloaded at field'
|
|
it_behaves_like 'successfully returning the file'
|
|
it_behaves_like 'file download in FIPS mode'
|
|
|
|
it 'returns sha1 of the file' do
|
|
download_file(file_name: package_file.file_name + '.sha1')
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('text/plain')
|
|
expect(response.body).to eq(package_file.file_sha1)
|
|
end
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :forbidden
|
|
end
|
|
|
|
it 'returns not found when a package is not found' do
|
|
finder = double('finder', execute: nil)
|
|
expect(::Packages::Maven::PackageFinder).to receive(:new).and_return(finder)
|
|
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file'
|
|
end
|
|
|
|
context 'internal project' do
|
|
before do
|
|
project.team.truncate
|
|
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
|
end
|
|
|
|
subject { download_file_with_token(file_name: package_file.file_name) }
|
|
|
|
shared_examples 'getting a file' do
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'bumping the package last downloaded at field'
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
it 'denies download when no private token' do
|
|
download_file(file_name: package_file.file_name)
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
it_behaves_like 'downloads with a job token'
|
|
it_behaves_like 'downloads with a deploy token'
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :forbidden
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found }
|
|
end
|
|
|
|
context 'private project' do
|
|
subject { download_file_with_token(file_name: package_file.file_name) }
|
|
|
|
before do
|
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
|
end
|
|
|
|
shared_examples 'getting a file' do
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'bumping the package last downloaded at field'
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
it 'denies download when not enough permissions' do
|
|
unless project.root_namespace == user.namespace
|
|
project.add_guest(user)
|
|
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
end
|
|
|
|
it 'denies download when no private token' do
|
|
download_file(file_name: package_file.file_name)
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
it_behaves_like 'downloads with a job token'
|
|
it_behaves_like 'downloads with a deploy token'
|
|
|
|
it 'does not allow download by a unauthorized deploy token with same id as a user with access' do
|
|
unauthorized_deploy_token = create(:deploy_token, read_package_registry: true, write_package_registry: true)
|
|
|
|
another_user = create(:user)
|
|
project.add_developer(another_user)
|
|
|
|
# We force the id of the deploy token and the user to be the same
|
|
unauthorized_deploy_token.update!(id: another_user.id)
|
|
|
|
download_file(
|
|
file_name: package_file.file_name,
|
|
request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => unauthorized_deploy_token.token }
|
|
)
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :forbidden
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'handling groups, subgroups and user namespaces for', 'getting a file', visibilities: { public: :redirect, internal: :not_found, private: :not_found }
|
|
end
|
|
|
|
context 'project name is different from a package name' do
|
|
before do
|
|
maven_metadatum.update!(path: "wrong_name/#{package.version}")
|
|
end
|
|
|
|
it 'rejects request' do
|
|
download_file(file_name: package_file.file_name)
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
end
|
|
|
|
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
|
|
get api("/packages/maven/#{path}/#{file_name}"), params: params, headers: request_headers
|
|
end
|
|
|
|
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path)
|
|
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path)
|
|
end
|
|
end
|
|
|
|
describe 'HEAD /api/v4/packages/maven/*path/:file_name' do
|
|
let(:path) { package.maven_metadatum.path }
|
|
let(:url) { "/packages/maven/#{path}/#{package_file.file_name}" }
|
|
|
|
shared_examples 'heading a file' do
|
|
it_behaves_like 'processing HEAD requests', instance_level: true
|
|
end
|
|
|
|
it_behaves_like 'handling groups, subgroups and user namespaces for', 'heading a file'
|
|
end
|
|
|
|
describe 'GET /api/v4/groups/:id/-/packages/maven/*path/:file_name' do
|
|
before do
|
|
project.team.truncate
|
|
group.add_developer(user)
|
|
end
|
|
|
|
it_behaves_like 'forwarding package requests'
|
|
|
|
context 'a public project' do
|
|
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
|
|
|
|
subject { download_file(file_name: package_file.file_name) }
|
|
|
|
shared_examples 'getting a file for a group' do
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'bumping the package last downloaded at field'
|
|
it_behaves_like 'successfully returning the file'
|
|
it_behaves_like 'file download in FIPS mode'
|
|
|
|
it 'returns sha1 of the file' do
|
|
download_file(file_name: package_file.file_name + '.sha1')
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('text/plain')
|
|
expect(response.body).to eq(package_file.file_sha1)
|
|
end
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'handling groups and subgroups for', 'getting a file for a group'
|
|
end
|
|
|
|
context 'internal project' do
|
|
before do
|
|
group.member(user).destroy!
|
|
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
|
end
|
|
|
|
subject { download_file_with_token(file_name: package_file.file_name) }
|
|
|
|
shared_examples 'getting a file for a group' do |not_found_response|
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'bumping the package last downloaded at field'
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
it 'forwards download when no private token' do
|
|
download_file(file_name: package_file.file_name)
|
|
|
|
expect(response).to have_gitlab_http_status(not_found_response)
|
|
end
|
|
|
|
it_behaves_like 'downloads with a job token'
|
|
it_behaves_like 'downloads with a deploy token'
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { internal: :not_found, public: :redirect }
|
|
end
|
|
|
|
context 'private project' do
|
|
before do
|
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
|
end
|
|
|
|
subject { download_file_with_token(file_name: package_file.file_name) }
|
|
|
|
shared_examples 'getting a file for a group' do |not_found_response|
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'bumping the package last downloaded at field'
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
it 'denies download when not enough permissions' do
|
|
group.add_guest(user)
|
|
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:redirect)
|
|
end
|
|
|
|
it 'denies download when no private token' do
|
|
download_file(file_name: package_file.file_name)
|
|
|
|
expect(response).to have_gitlab_http_status(not_found_response)
|
|
end
|
|
|
|
it_behaves_like 'downloads with a job token'
|
|
it_behaves_like 'downloads with a deploy token'
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
|
|
context 'with group deploy token' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, request_headers: group_deploy_token_headers) }
|
|
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
it 'returns the file with only write_package_registry scope' do
|
|
deploy_token_for_group.update!(read_package_registry: false)
|
|
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('application/octet-stream')
|
|
end
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: group_deploy_token_headers) }
|
|
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'handling groups and subgroups for', 'getting a file for a group', visibilities: { private: :not_found, internal: :not_found, public: :redirect }
|
|
|
|
context 'with a reporter from a subgroup accessing the root group' do
|
|
let_it_be(:root_group) { create(:group, :private) }
|
|
let_it_be(:group) { create(:group, :private, parent: root_group) }
|
|
|
|
subject { download_file_with_token(file_name: package_file.file_name, request_headers: headers_with_token, group_id: root_group.id) }
|
|
|
|
before do
|
|
project.update!(namespace: group)
|
|
group.add_reporter(user)
|
|
end
|
|
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) }
|
|
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'maven metadata file' do
|
|
let_it_be(:sub_group1) { create(:group, parent: group) }
|
|
let_it_be(:sub_group2) { create(:group, parent: group) }
|
|
let_it_be(:project1) { create(:project, :private, group: sub_group1) }
|
|
let_it_be(:project2) { create(:project, :private, group: sub_group2) }
|
|
let_it_be(:project3) { create(:project, :private, group: sub_group1) }
|
|
let_it_be(:package_name) { 'foo' }
|
|
let_it_be(:package1) { create(:maven_package, project: project1, name: package_name, version: nil) }
|
|
let_it_be(:package_file1) { create(:package_file, :xml, package: package1, file_name: 'maven-metadata.xml') }
|
|
let_it_be(:package2) { create(:maven_package, project: project2, name: package_name, version: nil) }
|
|
let_it_be(:package_file2) { create(:package_file, :xml, package: package2, file_name: 'maven-metadata.xml') }
|
|
let_it_be(:package3) { create(:maven_package, project: project3, name: package_name, version: nil) }
|
|
let_it_be(:package_file3) { create(:package_file, :xml, package: package3, file_name: 'maven-metadata.xml') }
|
|
|
|
let(:maven_metadatum) { package3.maven_metadatum }
|
|
|
|
subject { download_file_with_token(file_name: package_file3.file_name) }
|
|
|
|
before do
|
|
sub_group1.add_developer(user)
|
|
sub_group2.add_developer(user)
|
|
# the package with the most recently published file should be returned
|
|
create(:package_file, :xml, package: package2)
|
|
end
|
|
|
|
context 'in multiple versionless packages' do
|
|
it 'downloads the file' do
|
|
expect(::Packages::PackageFileFinder)
|
|
.to receive(:new).with(package2, 'maven-metadata.xml').and_call_original
|
|
|
|
subject
|
|
end
|
|
end
|
|
|
|
context 'in multiple snapshot packages' do
|
|
before do
|
|
version = '1.0.0-SNAPSHOT'
|
|
[package1, package2, package3].each do |pkg|
|
|
pkg.update!(version: version)
|
|
|
|
pkg.maven_metadatum.update!(path: "#{pkg.name}/#{pkg.version}")
|
|
end
|
|
end
|
|
|
|
it 'downloads the file' do
|
|
expect(::Packages::PackageFileFinder)
|
|
.to receive(:new).with(package3, 'maven-metadata.xml').and_call_original
|
|
|
|
subject
|
|
end
|
|
end
|
|
end
|
|
|
|
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path, group_id: group.id)
|
|
get api("/groups/#{group_id}/-/packages/maven/#{path}/#{file_name}"), params: params, headers: request_headers
|
|
end
|
|
|
|
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path, group_id: group.id)
|
|
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path, group_id: group_id)
|
|
end
|
|
end
|
|
|
|
describe 'HEAD /api/v4/groups/:id/-/packages/maven/*path/:file_name' do
|
|
let(:path) { package.maven_metadatum.path }
|
|
let(:url) { "/groups/#{group.id}/-/packages/maven/#{path}/#{package_file.file_name}" }
|
|
|
|
it_behaves_like 'handling groups and subgroups for', 'processing HEAD requests'
|
|
end
|
|
|
|
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
|
|
context 'a public project' do
|
|
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace, property: 'i_package_maven_user' } }
|
|
|
|
subject { download_file(file_name: package_file.file_name) }
|
|
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'successfully returning the file'
|
|
it_behaves_like 'file download in FIPS mode'
|
|
|
|
%w[sha1 md5].each do |format|
|
|
it "returns #{format} of the file" do
|
|
download_file(file_name: package_file.file_name + ".#{format}")
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq('text/plain')
|
|
expect(response.body).to eq(package_file.send("file_#{format}".to_sym))
|
|
end
|
|
end
|
|
|
|
context 'when the repository is disabled' do
|
|
before do
|
|
project.project_feature.update!(
|
|
# Disable merge_requests and builds as well, since merge_requests and
|
|
# builds cannot have higher visibility than repository.
|
|
merge_requests_access_level: ProjectFeature::DISABLED,
|
|
builds_access_level: ProjectFeature::DISABLED,
|
|
repository_access_level: ProjectFeature::DISABLED)
|
|
end
|
|
|
|
it_behaves_like 'successfully returning the file'
|
|
end
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
end
|
|
|
|
context 'private project' do
|
|
before do
|
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
|
end
|
|
|
|
subject { download_file_with_token(file_name: package_file.file_name) }
|
|
|
|
it_behaves_like 'tracking the file download event'
|
|
it_behaves_like 'bumping the package last downloaded at field'
|
|
it_behaves_like 'successfully returning the file'
|
|
|
|
it 'denies download when not enough permissions' do
|
|
project.add_guest(user)
|
|
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
it 'denies download when no private token' do
|
|
download_file(file_name: package_file.file_name)
|
|
|
|
expect(response).to have_gitlab_http_status(:not_found)
|
|
end
|
|
|
|
context 'with access to package registry for everyone' do
|
|
subject { download_file(file_name: package_file.file_name) }
|
|
|
|
before do
|
|
project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC)
|
|
end
|
|
|
|
it_behaves_like 'successfully returning the file'
|
|
end
|
|
|
|
it_behaves_like 'downloads with a job token'
|
|
it_behaves_like 'downloads with a deploy token'
|
|
|
|
context 'with a non existing maven path' do
|
|
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
|
|
|
it_behaves_like 'returning response status', :redirect
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'forwarding package requests'
|
|
|
|
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
|
|
get api("/projects/#{project.id}/packages/maven/" \
|
|
"#{path}/#{file_name}"), params: params, headers: request_headers
|
|
end
|
|
|
|
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path)
|
|
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path)
|
|
end
|
|
end
|
|
|
|
describe 'HEAD /api/v4/projects/:id/packages/maven/*path/:file_name' do
|
|
let(:path) { package.maven_metadatum.path }
|
|
let(:url) { "/projects/#{project.id}/packages/maven/#{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"), headers: headers_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
end
|
|
|
|
it 'authorizes posting package with a valid token' do
|
|
authorize_upload_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
|
|
expect(json_response['TempPath']).not_to be_nil
|
|
end
|
|
|
|
it 'rejects request without a valid token' do
|
|
headers_with_token['Private-Token'] = 'foo'
|
|
|
|
authorize_upload_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
end
|
|
|
|
it 'rejects request without a valid permission' do
|
|
project.add_guest(user)
|
|
|
|
authorize_upload_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
it 'rejects requests that did not go through gitlab-workhorse' do
|
|
headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
|
|
|
|
authorize_upload_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
it 'authorizes upload with job token' do
|
|
authorize_upload(job_token: job.token)
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
end
|
|
|
|
it 'authorizes upload with deploy token' do
|
|
authorize_upload({}, headers_with_deploy_token)
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
end
|
|
|
|
it 'rejects requests by a unauthorized deploy token with same id as a user with access' do
|
|
unauthorized_deploy_token = create(:deploy_token, read_package_registry: true, write_package_registry: true)
|
|
|
|
another_user = create(:user)
|
|
project.add_developer(another_user)
|
|
|
|
# We force the id of the deploy token and the user to be the same
|
|
unauthorized_deploy_token.update!(id: another_user.id)
|
|
|
|
authorize_upload({}, headers.merge(Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => unauthorized_deploy_token.token))
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
def authorize_upload(params = {}, request_headers = headers)
|
|
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/maven-metadata.xml/authorize"), params: params, headers: request_headers
|
|
end
|
|
|
|
def authorize_upload_with_token(params = {}, request_headers = headers_with_token)
|
|
authorize_upload(params, request_headers)
|
|
end
|
|
end
|
|
|
|
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name' do
|
|
let(:send_rewritten_field) { true }
|
|
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.jar') }
|
|
|
|
before do
|
|
# by configuring this path we allow to pass temp file from any path
|
|
allow(Packages::PackageFileUploader).to receive(:workhorse_upload_path).and_return('/')
|
|
end
|
|
|
|
it 'rejects requests without a file from workhorse' do
|
|
upload_file_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
end
|
|
|
|
it 'rejects request without a token' do
|
|
upload_file
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
end
|
|
|
|
context 'without workhorse rewritten field' do
|
|
let(:send_rewritten_field) { false }
|
|
|
|
it 'rejects the request' do
|
|
upload_file_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
end
|
|
end
|
|
|
|
context 'when params from workhorse are correct' do
|
|
let(:params) { { file: file_upload } }
|
|
|
|
subject { upload_file_with_token(params: params) }
|
|
|
|
context 'FIPS mode', :fips_mode do
|
|
it_behaves_like 'package workhorse uploads'
|
|
|
|
it 'rejects the request for md5 file' do
|
|
upload_file_with_token(params: params, file_extension: 'jar.md5')
|
|
|
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
|
end
|
|
end
|
|
|
|
context 'file size is too large' do
|
|
it 'rejects the request' do
|
|
allow_next_instance_of(UploadedFile) do |uploaded_file|
|
|
allow(uploaded_file).to receive(:size).and_return(project.actual_limits.maven_max_file_size + 1)
|
|
end
|
|
|
|
upload_file_with_token(params: params)
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
end
|
|
end
|
|
|
|
it 'rejects a malicious request' do
|
|
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2f.ssh%2fauthorized_keys"), params: params, headers: headers_with_token
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
end
|
|
|
|
it_behaves_like 'package workhorse uploads'
|
|
|
|
context 'event tracking' do
|
|
it_behaves_like 'a package tracking event', described_class.name, 'push_package'
|
|
|
|
context 'when the package file fails to be created' do
|
|
before do
|
|
allow_next_instance_of(::Packages::CreatePackageFileService) do |create_package_file_service|
|
|
allow(create_package_file_service).to receive(:execute).and_raise(StandardError)
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'not a package tracking event'
|
|
end
|
|
end
|
|
|
|
it 'creates package and stores package file' do
|
|
expect_use_primary
|
|
|
|
expect { upload_file_with_token(params: params) }.to change { project.packages.count }.by(1)
|
|
.and change { Packages::Maven::Metadatum.count }.by(1)
|
|
.and change { Packages::PackageFile.count }.by(1)
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(jar_file.file_name).to eq(file_upload.original_filename)
|
|
end
|
|
|
|
it 'allows upload with running job token' do
|
|
upload_file(params: params.merge(job_token: job.token))
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(project.reload.packages.last.original_build_info.pipeline).to eq job.pipeline
|
|
end
|
|
|
|
it 'rejects upload without running job token' do
|
|
job.update!(status: :failed)
|
|
upload_file(params: params.merge(job_token: job.token))
|
|
|
|
expect(response).to have_gitlab_http_status(:unauthorized)
|
|
end
|
|
|
|
it 'allows upload with deploy token' do
|
|
upload_file(params: params, request_headers: headers_with_deploy_token)
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
end
|
|
|
|
it 'rejects uploads by a unauthorized deploy token with same id as a user with access' do
|
|
unauthorized_deploy_token = create(:deploy_token, read_package_registry: true, write_package_registry: true)
|
|
|
|
another_user = create(:user)
|
|
project.add_developer(another_user)
|
|
|
|
# We force the id of the deploy token and the user to be the same
|
|
unauthorized_deploy_token.update!(id: another_user.id)
|
|
|
|
upload_file(
|
|
params: params,
|
|
request_headers: headers.merge(Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => unauthorized_deploy_token.token)
|
|
)
|
|
|
|
expect(response).to have_gitlab_http_status(:forbidden)
|
|
end
|
|
|
|
context 'file name is too long' do
|
|
let(:file_name) { 'a' * (Packages::Maven::FindOrCreatePackageService::MAX_FILE_NAME_LENGTH + 1) }
|
|
|
|
it 'rejects request' do
|
|
expect { upload_file_with_token(params: params, file_name: file_name) }.not_to change { project.packages.count }
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
expect(json_response['message']).to include('File name is too long')
|
|
end
|
|
end
|
|
|
|
context 'version is not correct' do
|
|
let(:version) { '$%123' }
|
|
|
|
it 'rejects request' do
|
|
expect { upload_file_with_token(params: params) }.not_to change { project.packages.count }
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
expect(json_response['message']).to include('Validation failed')
|
|
end
|
|
end
|
|
|
|
context 'when package duplicates are not allowed' do
|
|
let(:package_name) { package.name }
|
|
let(:version) { package.version }
|
|
|
|
before do
|
|
package_settings.update!(maven_duplicates_allowed: false)
|
|
end
|
|
|
|
shared_examples 'storing the package file' do |file_name: 'my-app-1.0-20180724.124855-1'|
|
|
it 'stores the file', :aggregate_failures do
|
|
expect { upload_file_with_token(params: params, file_name: file_name) }.to change { package.package_files.count }.by(1)
|
|
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
expect(jar_file.file_name).to eq(file_upload.original_filename)
|
|
end
|
|
end
|
|
|
|
it 'rejects the request', :aggregate_failures do
|
|
expect { upload_file_with_token(params: params) }.not_to change { package.package_files.count }
|
|
|
|
expect(response).to have_gitlab_http_status(:bad_request)
|
|
expect(json_response['message']).to include('Duplicate package is not allowed')
|
|
end
|
|
|
|
context 'when uploading to the versionless package which contains metadata about all versions' do
|
|
let(:version) { nil }
|
|
let(:param_path) { package_name }
|
|
let!(:package) { create(:maven_package, project: project, version: version, name: project.full_path) }
|
|
|
|
it_behaves_like 'storing the package file'
|
|
end
|
|
|
|
context 'when uploading different non-duplicate files to the same package' do
|
|
let!(:package) { create(:maven_package, project: project, name: project.full_path) }
|
|
|
|
before do
|
|
package_file = package.package_files.find_by(file_name: 'my-app-1.0-20180724.124855-1.jar')
|
|
package_file.destroy!
|
|
end
|
|
|
|
it_behaves_like 'storing the package file'
|
|
end
|
|
|
|
context 'when the package name matches the exception regex' do
|
|
before do
|
|
package_settings.update!(maven_duplicate_exception_regex: '.*')
|
|
end
|
|
|
|
it_behaves_like 'storing the package file'
|
|
end
|
|
|
|
context 'when uploading a similar package file name with a classifier' do
|
|
it_behaves_like 'storing the package file', file_name: 'my-app-1.0-20180724.124855-1-javadoc'
|
|
end
|
|
end
|
|
|
|
context 'for sha1 file' do
|
|
let(:dummy_package) { double(Packages::Package) }
|
|
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/maven/my-app-1.0-20180724.124855-1.pom.sha1') }
|
|
let(:stored_sha1) { File.read(file_upload.path) }
|
|
|
|
subject(:upload) { upload_file_with_token(params: params, file_extension: 'pom.sha1') }
|
|
|
|
before do
|
|
# The sha verification done by the maven api is between:
|
|
# - the sha256 set by workhorse helpers
|
|
# - the sha256 of the sha1 of the uploaded package file
|
|
# We're going to send `file_upload` for the sha1 and stub the sha1 of the package file so that
|
|
# both sha256 being the same
|
|
allow(::Packages::PackageFileFinder).to receive(:new).and_return(double(execute!: dummy_package))
|
|
allow(dummy_package).to receive(:file_sha1).and_return(stored_sha1)
|
|
end
|
|
|
|
it 'returns no content' do
|
|
expect_use_primary
|
|
|
|
upload
|
|
|
|
expect(response).to have_gitlab_http_status(:no_content)
|
|
end
|
|
|
|
context 'when the stored sha1 is not the same' do
|
|
let(:sent_sha1) { File.read(file_upload.path) }
|
|
let(:stored_sha1) { 'wrong sha1' }
|
|
|
|
it 'logs an error and returns conflict' do
|
|
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(
|
|
instance_of(ArgumentError),
|
|
message: 'maven package file sha1 conflict',
|
|
stored_sha1: stored_sha1,
|
|
received_sha256: Digest::SHA256.hexdigest(sent_sha1),
|
|
sha256_hexdigest_of_stored_sha1: Digest::SHA256.hexdigest(stored_sha1)
|
|
)
|
|
|
|
upload
|
|
|
|
expect(response).to have_gitlab_http_status(:conflict)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'for md5 file' do
|
|
subject { upload_file_with_token(params: params, file_extension: 'jar.md5') }
|
|
|
|
it 'returns an empty body' do
|
|
expect_use_primary
|
|
|
|
subject
|
|
|
|
expect(response.body).to eq('')
|
|
expect(response).to have_gitlab_http_status(:ok)
|
|
end
|
|
|
|
context 'with FIPS mode enabled', :fips_mode do
|
|
it 'rejects the request' do
|
|
subject
|
|
|
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'reading fingerprints from UploadedFile instance' do
|
|
let(:file) { Packages::Package.last.package_files.with_format('%.jar').last }
|
|
|
|
subject { upload_file_with_token(params: params) }
|
|
|
|
before do
|
|
allow_next_instance_of(UploadedFile) do |uploaded_file|
|
|
allow(uploaded_file).to receive(:size).and_return(123)
|
|
allow(uploaded_file).to receive(:sha1).and_return('sha1')
|
|
allow(uploaded_file).to receive(:md5).and_return('md5')
|
|
end
|
|
end
|
|
|
|
context 'when feature flag is enabled' do
|
|
it 'sets size, sha1 and md5 fingerprints from uploaded file' do
|
|
subject
|
|
|
|
expect(file.size).to eq(123)
|
|
expect(file.file_sha1).to eq('sha1')
|
|
expect(file.file_md5).to eq('md5')
|
|
end
|
|
end
|
|
|
|
context 'when feature flag is disabled' do
|
|
before do
|
|
stub_feature_flags(read_fingerprints_from_uploaded_file_in_maven_upload: false)
|
|
end
|
|
|
|
it 'does not read fingerprints from uploaded file' do
|
|
subject
|
|
|
|
expect(file.size).not_to eq(123)
|
|
expect(file.file_sha1).not_to eq('sha1')
|
|
expect(file.file_md5).not_to eq('md5')
|
|
end
|
|
end
|
|
end
|
|
|
|
def expect_use_primary
|
|
lb_session = ::Gitlab::Database::LoadBalancing::Session.current
|
|
|
|
expect(lb_session).to receive(:use_primary).and_call_original
|
|
|
|
allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
|
|
end
|
|
end
|
|
|
|
def upload_file(params: {}, request_headers: headers, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1')
|
|
url = "/projects/#{project.id}/packages/maven/#{param_path}/#{file_name}.#{file_extension}"
|
|
workhorse_finalize(
|
|
api(url),
|
|
method: :put,
|
|
file_key: :file,
|
|
params: params,
|
|
headers: request_headers,
|
|
send_rewritten_field: send_rewritten_field
|
|
)
|
|
end
|
|
|
|
def upload_file_with_token(params: {}, request_headers: headers_with_token, file_extension: 'jar', file_name: 'my-app-1.0-20180724.124855-1')
|
|
upload_file(params: params, request_headers: request_headers, file_name: file_name, file_extension: file_extension)
|
|
end
|
|
end
|
|
|
|
def move_project_to_namespace(namespace)
|
|
project.update!(namespace: namespace)
|
|
package.update!(name: project.full_path)
|
|
maven_metadatum.update!(path: "#{package.name}/#{package.version}")
|
|
end
|
|
end
|