Commit 50f7d343 authored by Nick Thomas's avatar Nick Thomas

Merge branch '210073-pypi-api-skeleton-and-authentication' into 'master'

PyPI: API skeleton and authentication

See merge request gitlab-org/gitlab!27030
parents f0543d2c 43a27fa8
...@@ -31,7 +31,7 @@ class Packages::Package < ApplicationRecord ...@@ -31,7 +31,7 @@ class Packages::Package < ApplicationRecord
validate :valid_npm_package_name, if: :npm? validate :valid_npm_package_name, if: :npm?
validate :package_already_taken, if: :npm? validate :package_already_taken, if: :npm?
enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4 } enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4, pypi: 5 }
scope :with_name, ->(name) { where(name: name) } scope :with_name, ->(name) { where(name: name) }
scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) } scope :with_name_like, ->(name) { where(arel_table[:name].matches(name)) }
......
# frozen_string_literal: true
module API
module Helpers
module Packages
module BasicAuthHelpers
module Constants
AUTHENTICATE_REALM_HEADER = 'Www-Authenticate: Basic realm'
AUTHENTICATE_REALM_NAME = 'GitLab Packages Registry'
end
include Constants
def find_personal_access_token
find_personal_access_token_from_http_basic_auth
end
def authorized_user_project
@authorized_user_project ||= authorized_project_find!(params[:id])
end
def authorized_project_find!(id)
project = find_project(id)
unless project && can?(current_user, :read_project, project)
return unauthorized_or! { not_found! }
end
project
end
def authorize!(action, subject = :global, reason = nil)
return if can?(current_user, action, subject)
unauthorized_or! { forbidden!(reason) }
end
def unauthorized_or!
current_user ? yield : unauthorized_with_header!
end
def unauthorized_with_header!
header(AUTHENTICATE_REALM_HEADER, AUTHENTICATE_REALM_NAME)
unauthorized!
end
end
end
end
end
# 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
class PypiPackages < Grape::API
helpers ::API::Helpers::PackagesManagerClientsHelpers
helpers ::API::Helpers::RelatedResourcesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
include ::API::Helpers::Packages::BasicAuthHelpers::Constants
default_format :json
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
before do
require_packages_enabled!
end
params do
requires :id, type: Integer, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
unless ::Feature.enabled?(:pypi_packages, authorized_user_project)
not_found!
end
authorize_packages_feature!(authorized_user_project)
end
namespace ':id/packages/pypi' do
desc 'The PyPi package download endpoint' do
detail 'This feature was introduced in GitLab 12.10'
end
params do
requires :file_identifier, type: String, desc: 'The PyPi package file identifier', file_path: true
end
get 'files/*file_identifier', :txt do
authorize_read_package!(authorized_user_project)
end
desc 'The PyPi Simple Endpoint' do
detail 'This feature was introduced in GitLab 12.10'
end
params do
requires :package_name, type: String, file_path: true, desc: 'The PyPi package name'
end
get 'simple/*package_name', format: :txt do
authorize_read_package!(authorized_user_project)
end
desc 'The PyPi Package upload endpoint' do
detail 'This feature was introduced in GitLab 12.10'
end
params do
use :workhorse_upload_params
end
post do
authorize_upload!(authorized_user_project)
created!
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:name], project_id: authorized_user_project.id })
forbidden!
end
post 'authorize' do
authorize_workhorse!(subject: authorized_user_project, has_length: false)
end
end
end
end
end
...@@ -32,6 +32,7 @@ module EE ...@@ -32,6 +32,7 @@ module EE
mount ::API::ProjectMirror mount ::API::ProjectMirror
mount ::API::ProjectPushRule mount ::API::ProjectPushRule
mount ::API::NugetPackages mount ::API::NugetPackages
mount ::API::PypiPackages
mount ::API::ConanPackages mount ::API::ConanPackages
mount ::API::MavenPackages mount ::API::MavenPackages
mount ::API::NpmPackages mount ::API::NpmPackages
......
...@@ -49,6 +49,12 @@ FactoryBot.define do ...@@ -49,6 +49,12 @@ FactoryBot.define do
end end
end end
factory :pypi_package do
sequence(:name) { |n| "pypi-package-#{n}"}
sequence(:version) { |n| "1.0.#{n}" }
package_type { :pypi }
end
factory :conan_package do factory :conan_package do
conan_metadatum conan_metadatum
......
This diff is collapsed.
# frozen_string_literal: true
RSpec.shared_examples 'process PyPi api request' do |user_type, status, add_member = true|
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
it_behaves_like 'returning response status', status
end
end
RSpec.shared_examples 'rejects PyPI access with unknown project id' do
context 'with an unknown project' do
let(:project) { OpenStruct.new(id: 1234567890) }
context 'as anonymous' do
it_behaves_like 'process PyPi api request', :anonymous, :unauthorized
end
context 'as authenticated user' do
subject { get api(url), headers: build_basic_auth_header(user.username, personal_access_token.token) }
it_behaves_like 'process PyPi api request', :anonymous, :not_found
end
end
end
RSpec.shared_examples 'rejects PyPI packages access with packages features disabled' do
context 'with packages features disabled' do
before do
stub_licensed_features(packages: false)
end
it_behaves_like 'process PyPi api request', :anonymous, :forbidden
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