2020-07-28 23:09:34 +05:30
# frozen_string_literal: true
# PyPI Package Manager Client API
#
# These API endpoints are not meant to be consumed directly by users. They are
# called by the PyPI package manager client when users run commands
# like `pip install` or `twine upload`.
module API
2021-01-03 14:25:43 +05:30
class PypiPackages < :: API :: Base
2020-07-28 23:09:34 +05:30
helpers :: API :: Helpers :: PackagesManagerClientsHelpers
helpers :: API :: Helpers :: RelatedResourcesHelpers
helpers :: API :: Helpers :: Packages :: BasicAuthHelpers
2021-10-27 15:23:28 +05:30
helpers :: API :: Helpers :: Packages :: DependencyProxyHelpers
2020-07-28 23:09:34 +05:30
include :: API :: Helpers :: Packages :: BasicAuthHelpers :: Constants
2021-01-29 00:20:46 +05:30
feature_category :package_registry
2022-07-16 23:28:13 +05:30
urgency :low
2021-01-29 00:20:46 +05:30
2020-07-28 23:09:34 +05:30
default_format :json
rescue_from ArgumentError do | e |
render_api_error! ( e . message , 400 )
end
rescue_from ActiveRecord :: RecordInvalid do | e |
render_api_error! ( e . message , 400 )
end
before do
require_packages_enabled!
end
2021-09-04 01:27:46 +05:30
helpers do
params :package_download do
2023-03-04 22:38:38 +05:30
requires :file_identifier , type : String , desc : 'The PyPi package file identifier' , file_path : true , documentation : { example : 'my.pypi.package-0.0.1.tar.gz' }
requires :sha256 , type : String , desc : 'The PyPi package sha256 check sum' , documentation : { example : '5y57017232013c8ac80647f4ca153k3726f6cba62d055cd747844ed95b3c65ff' }
2021-09-04 01:27:46 +05:30
end
params :package_name do
2023-03-04 22:38:38 +05:30
requires :package_name , type : String , file_path : true , desc : 'The PyPi package name' , documentation : { example : 'my.pypi.package' }
2021-09-04 01:27:46 +05:30
end
2022-07-23 23:45:48 +05:30
def present_simple_index ( group_or_project )
authorize_read_package! ( group_or_project )
packages = Packages :: Pypi :: PackagesFinder . new ( current_user , group_or_project ) . execute
presenter = :: Packages :: Pypi :: SimpleIndexPresenter . new ( packages , group_or_project )
present_html ( presenter . body )
end
def present_simple_package ( group_or_project )
authorize_read_package! ( group_or_project )
track_simple_event ( group_or_project , 'list_package' )
packages = Packages :: Pypi :: PackagesFinder . new ( current_user , group_or_project , { package_name : params [ :package_name ] } ) . execute
empty_packages = packages . empty?
2022-11-25 23:54:43 +05:30
redirect_registry_request (
forward_to_registry : empty_packages ,
package_type : :pypi ,
target : group_or_project ,
package_name : params [ :package_name ]
) do
2022-07-23 23:45:48 +05:30
not_found! ( 'Package' ) if empty_packages
presenter = :: Packages :: Pypi :: SimplePackageVersionsPresenter . new ( packages , group_or_project )
present_html ( presenter . body )
end
end
def track_simple_event ( group_or_project , event_name )
if group_or_project . is_a? ( Project )
project = group_or_project
namespace = group_or_project . namespace
else
project = nil
namespace = group_or_project
end
track_package_event ( event_name , :pypi , project : project , namespace : namespace )
end
def present_html ( content )
# Adjusts grape output format
# to be HTML
content_type " text/html; charset=utf-8 "
env [ 'api.format' ] = :binary
body content
end
2022-09-01 20:07:04 +05:30
def ensure_group!
find_group ( params [ :id ] ) || not_found!
find_authorized_group!
end
2023-01-13 00:05:48 +05:30
def project! ( action : :read_package )
2022-09-01 20:07:04 +05:30
find_project ( params [ :id ] ) || not_found!
2023-01-13 00:05:48 +05:30
authorized_user_project ( action : action )
2022-09-01 20:07:04 +05:30
end
2023-03-04 22:38:38 +05:30
def validate_fips!
unprocessable_entity! if declared_params [ :sha256_digest ] . blank?
true
end
2021-09-04 01:27:46 +05:30
end
params do
2023-03-04 22:38:38 +05:30
requires :id , types : [ Integer , String ] , desc : 'The ID or full path of the group.'
2021-09-04 01:27:46 +05:30
end
resource :groups , requirements : API :: NAMESPACE_OR_PROJECT_REQUIREMENTS do
after_validation do
2022-09-01 20:07:04 +05:30
ensure_group!
2021-09-04 01:27:46 +05:30
end
namespace ':id/-/packages/pypi' do
2023-03-04 22:38:38 +05:30
desc 'Download a package file from a group' do
detail 'This feature was introduced in GitLab 13.12'
success code : 200
failure [
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' }
]
tags %w[ pypi_packages ]
end
2021-09-04 01:27:46 +05:30
params do
use :package_download
end
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
get 'files/:sha256/*file_identifier' do
2022-09-01 20:07:04 +05:30
group = find_authorized_group!
authorize_read_package! ( group )
2021-09-04 01:27:46 +05:30
filename = " #{ params [ :file_identifier ] } . #{ params [ :format ] } "
package = Packages :: Pypi :: PackageFinder . new ( current_user , group , { filename : filename , sha256 : params [ :sha256 ] } ) . execute
package_file = :: Packages :: PackageFileFinder . new ( package , filename , with_file_name_like : false ) . execute
2023-03-04 22:38:38 +05:30
track_package_event ( 'pull_package' , :pypi , namespace : group , project : package . project )
2021-09-04 01:27:46 +05:30
2022-10-11 01:57:18 +05:30
present_package_file! ( package_file , supports_direct_download : true )
2021-09-04 01:27:46 +05:30
end
2022-07-23 23:45:48 +05:30
desc 'The PyPi Simple Group Index Endpoint' do
detail 'This feature was introduced in GitLab 15.1'
2023-03-04 22:38:38 +05:30
success code : 200
failure [
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' }
]
tags %w[ pypi_packages ]
2022-07-23 23:45:48 +05:30
end
# An API entry point but returns an HTML file instead of JSON.
# PyPi simple API returns a list of packages as a simple HTML file.
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
get 'simple' , format : :txt do
present_simple_index ( find_authorized_group! )
end
desc 'The PyPi Simple Group Package Endpoint' do
2021-09-04 01:27:46 +05:30
detail 'This feature was introduced in GitLab 12.10'
2023-03-04 22:38:38 +05:30
success code : 200
failure [
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' }
]
tags %w[ pypi_packages ]
2021-09-04 01:27:46 +05:30
end
params do
use :package_name
end
2022-07-23 23:45:48 +05:30
# An API entry point but returns an HTML file instead of JSON.
2021-09-04 01:27:46 +05:30
# PyPi simple API returns the package descriptor as a simple HTML file.
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
get 'simple/*package_name' , format : :txt do
2022-07-23 23:45:48 +05:30
present_simple_package ( find_authorized_group! )
2021-09-04 01:27:46 +05:30
end
end
end
2020-07-28 23:09:34 +05:30
params do
2023-01-13 00:05:48 +05:30
requires :id , types : [ String , Integer ] , desc : 'The ID or URL-encoded path of the project'
2020-07-28 23:09:34 +05:30
end
resource :projects , requirements : API :: NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/packages/pypi' do
desc 'The PyPi package download endpoint' do
detail 'This feature was introduced in GitLab 12.10'
2023-03-04 22:38:38 +05:30
success code : 200
failure [
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' }
]
tags %w[ pypi_packages ]
2020-07-28 23:09:34 +05:30
end
params do
2021-09-04 01:27:46 +05:30
use :package_download
2020-07-28 23:09:34 +05:30
end
2020-11-24 15:15:51 +05:30
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
2020-07-28 23:09:34 +05:30
get 'files/:sha256/*file_identifier' do
2023-01-13 00:05:48 +05:30
project = project!
2020-07-28 23:09:34 +05:30
filename = " #{ params [ :file_identifier ] } . #{ params [ :format ] } "
2021-06-08 01:23:25 +05:30
package = Packages :: Pypi :: PackageFinder . new ( current_user , project , { filename : filename , sha256 : params [ :sha256 ] } ) . execute
2020-07-28 23:09:34 +05:30
package_file = :: Packages :: PackageFileFinder . new ( package , filename , with_file_name_like : false ) . execute
2021-09-04 01:27:46 +05:30
track_package_event ( 'pull_package' , :pypi , project : project , namespace : project . namespace )
2020-07-28 23:09:34 +05:30
2022-10-11 01:57:18 +05:30
present_package_file! ( package_file , supports_direct_download : true )
2020-07-28 23:09:34 +05:30
end
2022-07-23 23:45:48 +05:30
desc 'The PyPi Simple Project Index Endpoint' do
detail 'This feature was introduced in GitLab 15.1'
2023-03-04 22:38:38 +05:30
success code : 200
failure [
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' }
]
tags %w[ pypi_packages ]
2022-07-23 23:45:48 +05:30
end
# An API entry point but returns an HTML file instead of JSON.
# PyPi simple API returns a list of packages as a simple HTML file.
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
get 'simple' , format : :txt do
2023-01-13 00:05:48 +05:30
present_simple_index ( project! )
2022-07-23 23:45:48 +05:30
end
desc 'The PyPi Simple Project Package Endpoint' do
2020-07-28 23:09:34 +05:30
detail 'This feature was introduced in GitLab 12.10'
2023-03-04 22:38:38 +05:30
success code : 200
failure [
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' }
]
tags %w[ pypi_packages ]
2020-07-28 23:09:34 +05:30
end
params do
2021-09-04 01:27:46 +05:30
use :package_name
2020-07-28 23:09:34 +05:30
end
2022-07-23 23:45:48 +05:30
# An API entry point but returns an HTML file instead of JSON.
2020-07-28 23:09:34 +05:30
# PyPi simple API returns the package descriptor as a simple HTML file.
2020-11-24 15:15:51 +05:30
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
2020-07-28 23:09:34 +05:30
get 'simple/*package_name' , format : :txt do
2023-01-13 00:05:48 +05:30
present_simple_package ( project! )
2020-07-28 23:09:34 +05:30
end
desc 'The PyPi Package upload endpoint' do
detail 'This feature was introduced in GitLab 12.10'
2023-03-04 22:38:38 +05:30
success code : 201
failure [
{ code : 400 , message : 'Bad Request' } ,
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' } ,
{ code : 422 , message : 'Unprocessable Entity' }
]
tags %w[ pypi_packages ]
2020-07-28 23:09:34 +05:30
end
params do
2023-01-13 00:05:48 +05:30
requires :content , type : :: API :: Validations :: Types :: WorkhorseFile , desc : 'The package file to be published (generated by Multipart middleware)' , documentation : { type : 'file' }
2023-03-04 22:38:38 +05:30
requires :name , type : String , documentation : { example : 'my.pypi.package' }
requires :version , type : String , documentation : { example : '1.3.7' }
optional :requires_python , type : String , documentation : { example : '>=3.7' }
optional :md5_digest , type : String , documentation : { example : '900150983cd24fb0d6963f7d28e17f72' }
optional :sha256_digest , type : String , regexp : Gitlab :: Regex . sha256_regex , documentation : { example : 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad' }
2020-07-28 23:09:34 +05:30
end
2020-11-24 15:15:51 +05:30
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
2020-07-28 23:09:34 +05:30
post do
2023-01-13 00:05:48 +05:30
project = project! ( action : :read_project )
authorize_upload! ( project )
bad_request! ( 'File is too large' ) if project . actual_limits . exceeded? ( :pypi_max_file_size , params [ :content ] . size )
2020-07-28 23:09:34 +05:30
2023-01-13 00:05:48 +05:30
track_package_event ( 'push_package' , :pypi , project : project , user : current_user , namespace : project . namespace )
2020-07-28 23:09:34 +05:30
2023-03-04 22:38:38 +05:30
validate_fips! if Gitlab :: FIPS . enabled?
2022-08-13 15:12:31 +05:30
2020-07-28 23:09:34 +05:30
:: Packages :: Pypi :: CreatePackageService
2023-01-13 00:05:48 +05:30
. new ( project , current_user , declared_params . merge ( build : current_authenticated_job ) )
2020-07-28 23:09:34 +05:30
. execute
created!
rescue ObjectStorage :: RemoteStoreError = > e
Gitlab :: ErrorTracking . track_exception ( e , extra : { file_name : params [ :name ] , project_id : authorized_user_project . id } )
forbidden!
end
2023-03-04 22:38:38 +05:30
desc 'Authorize the PyPi package upload from workhorse' do
detail 'This feature was introduced in GitLab 12.10'
success code : 200
failure [
{ code : 401 , message : 'Unauthorized' } ,
{ code : 403 , message : 'Forbidden' } ,
{ code : 404 , message : 'Not Found' }
]
tags %w[ pypi_packages ]
end
2020-11-24 15:15:51 +05:30
route_setting :authentication , deploy_token_allowed : true , basic_auth_personal_access_token : true , job_token_allowed : :basic_auth
2020-07-28 23:09:34 +05:30
post 'authorize' do
2023-01-13 00:05:48 +05:30
project = project! ( action : :read_project )
2020-11-24 15:15:51 +05:30
authorize_workhorse! (
2023-01-13 00:05:48 +05:30
subject : project ,
2020-11-24 15:15:51 +05:30
has_length : false ,
2023-01-13 00:05:48 +05:30
maximum_size : project . actual_limits . pypi_max_file_size
2020-11-24 15:15:51 +05:30
)
2020-07-28 23:09:34 +05:30
end
end
end
end
end