Commit f1d6e8ab authored by Krasimir Angelov's avatar Krasimir Angelov

Implement generic packages download

Add API endpoint to pull generic packages. Related to
https://gitlab.com/gitlab-org/gitlab/-/issues/235493.
parent d260e7a9
# frozen_string_literal: true
module Packages
module Generic
class PackageFinder
def initialize(project)
@project = project
end
def execute!(package_name, package_version)
project
.packages
.generic
.by_name_and_version!(package_name, package_version)
end
private
attr_reader :project
end
end
end
...@@ -120,6 +120,10 @@ class Packages::Package < ApplicationRecord ...@@ -120,6 +120,10 @@ class Packages::Package < ApplicationRecord
.where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last! .where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last!
end end
def self.by_name_and_version!(name, version)
find_by!(name: name, version: version)
end
def self.pluck_names def self.pluck_names
pluck(:name) pluck(:name)
end end
......
...@@ -69,6 +69,25 @@ module API ...@@ -69,6 +69,25 @@ module API
forbidden! forbidden!
end end
params do
requires :package_name, type: String, desc: 'Package name'
requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
end
route_setting :authentication, job_token_allowed: true
get do
authorize_read_package!(project)
package = ::Packages::Generic::PackageFinder.new(project).execute!(params[:package_name], params[:package_version])
package_file = ::Packages::PackageFileFinder.new(package, params[:file_name]).execute!
track_event('pull_package')
present_carrierwave_file!(package_file.file)
end
end end
end end
end end
......
...@@ -140,6 +140,14 @@ FactoryBot.define do ...@@ -140,6 +140,14 @@ FactoryBot.define do
size { 1149.bytes } size { 1149.bytes }
end end
trait(:generic) do
package
file_fixture { 'spec/fixtures/packages/generic/myfile.tar.gz' }
file_name { "#{package.name}.tar.gz" }
file_sha256 { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' }
size { 1149.bytes }
end
trait(:object_storage) do trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE } file_store { Packages::PackageFileUploader::Store::REMOTE }
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Packages::Generic::PackageFinder do
let_it_be(:project) { create(:project) }
let_it_be(:package) { create(:generic_package, project: project) }
describe '#execute!' do
subject(:finder) { described_class.new(project) }
it 'finds package by name and version' do
found_package = finder.execute!(package.name, package.version)
expect(found_package).to eq(package)
end
it 'ignores packages with same name but different version' do
create(:generic_package, project: project, name: package.name, version: '3.1.4')
found_package = finder.execute!(package.name, package.version)
expect(found_package).to eq(package)
end
it 'raises ActiveRecord::RecordNotFound if package is not found' do
expect { finder.execute!(package.name, '3.1.4') }
.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
...@@ -33,7 +33,7 @@ RSpec.describe API::GenericPackages do ...@@ -33,7 +33,7 @@ RSpec.describe API::GenericPackages do
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token } { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token }
end end
describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize' do describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do
context 'with valid project' do context 'with valid project' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
...@@ -107,7 +107,7 @@ RSpec.describe API::GenericPackages do ...@@ -107,7 +107,7 @@ RSpec.describe API::GenericPackages do
end end
end end
describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz' do describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do
include WorkhorseHelpers include WorkhorseHelpers
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') } let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') }
...@@ -268,4 +268,97 @@ RSpec.describe API::GenericPackages do ...@@ -268,4 +268,97 @@ RSpec.describe API::GenericPackages do
) )
end end
end end
describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do
using RSpec::Parameterized::TableSyntax
let_it_be(:package) { create(:generic_package, project: project) }
let_it_be(:package_file) { create(:package_file, :generic, package: package) }
context 'authentication' do
where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do
'PUBLIC' | :developer | true | :personal_access_token | :success
'PUBLIC' | :guest | true | :personal_access_token | :success
'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized
'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized
'PUBLIC' | :developer | false | :personal_access_token | :success
'PUBLIC' | :guest | false | :personal_access_token | :success
'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized
'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized
'PUBLIC' | :anonymous | false | :none | :unauthorized
'PRIVATE' | :developer | true | :personal_access_token | :success
'PRIVATE' | :guest | true | :personal_access_token | :forbidden
'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized
'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized
'PRIVATE' | :developer | false | :personal_access_token | :not_found
'PRIVATE' | :guest | false | :personal_access_token | :not_found
'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized
'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized
'PRIVATE' | :anonymous | false | :none | :unauthorized
'PUBLIC' | :developer | true | :job_token | :success
'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized
'PUBLIC' | :developer | false | :job_token | :success
'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized
'PRIVATE' | :developer | true | :job_token | :success
'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized
'PRIVATE' | :developer | false | :job_token | :not_found
'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized
end
with_them do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false))
project.send("add_#{user_role}", user) if member? && user_role != :anonymous
end
it "responds with #{params[:expected_status]}" do
download_file(auth_header)
expect(response).to have_gitlab_http_status(expected_status)
end
end
end
context 'event tracking' do
before do
project.add_developer(user)
end
subject { download_file(personal_access_token_header) }
it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package'
end
it 'rejects a malicious request' do
project.add_developer(user)
download_file(personal_access_token_header, file_name: '%2e%2e%2f.ssh%2fauthorized_keys')
expect(response).to have_gitlab_http_status(:bad_request)
end
it 'responds with 404 Not Found for non existing package' do
project.add_developer(user)
download_file(personal_access_token_header, package_name: 'no-such-package')
expect(response).to have_gitlab_http_status(:not_found)
end
it 'responds with 404 Not Found for non existing package file' do
project.add_developer(user)
download_file(personal_access_token_header, file_name: 'no-such-file')
expect(response).to have_gitlab_http_status(:not_found)
end
def download_file(request_headers, package_name: nil, file_name: nil)
package_name ||= package.name
file_name ||= package_file.file_name
url = "/projects/#{project.id}/packages/generic/#{package_name}/#{package.version}/#{file_name}"
get api(url), headers: request_headers
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment