Commit d885bee6 authored by David Fernandez's avatar David Fernandez

Add Push Service endpoint in the NuGet API

Add Packages::Nuget::CreatePackageService
Centralize shared logic and helpers between NuGet and Conan APIs
parent f588407e
...@@ -27,7 +27,7 @@ class Packages::Package < ApplicationRecord ...@@ -27,7 +27,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 } enum package_type: { maven: 1, npm: 2, conan: 3, nuget: 4 }
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)) }
......
...@@ -52,7 +52,7 @@ module Packages ...@@ -52,7 +52,7 @@ module Packages
end end
def build_service_url(service_type) def build_service_url(service_type)
base_path = "#{api_v4_projects_path(id: project.id)}/packages/nuget" base_path = api_v4_projects_packages_nuget_path(id: project.id)
full_path = case service_type full_path = case service_type
when :download when :download
......
# frozen_string_literal: true
module Packages
module Nuget
class CreatePackageService < BaseService
PACKAGE_NAME = 'NuGet.Package'
PACKAGE_VERSION = '0.0.0'
def execute
project.packages.nuget.create!(
name: PACKAGE_NAME,
version: PACKAGE_VERSION
)
end
end
end
end
...@@ -219,13 +219,7 @@ module API ...@@ -219,13 +219,7 @@ module API
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
params do params do
optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)' use :workhorse_upload_params
optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
end end
put do put do
upload_package_file(:recipe_file) upload_package_file(:recipe_file)
...@@ -235,7 +229,7 @@ module API ...@@ -235,7 +229,7 @@ module API
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
put 'authorize' do put 'authorize' do
authorize_workhorse authorize_workhorse!(project)
end end
end end
...@@ -256,20 +250,14 @@ module API ...@@ -256,20 +250,14 @@ module API
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
put 'authorize' do put 'authorize' do
authorize_workhorse authorize_workhorse!(project)
end end
desc 'Upload package files' do desc 'Upload package files' do
detail 'This feature was introduced in GitLab 12.6' detail 'This feature was introduced in GitLab 12.6'
end end
params do params do
optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)' use :workhorse_upload_params
optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
end end
put do put do
upload_package_file(:package_file) upload_package_file(:package_file)
...@@ -380,38 +368,20 @@ module API ...@@ -380,38 +368,20 @@ module API
end end
def upload_package_file(file_type) def upload_package_file(file_type)
authorize_upload authorize_upload!(project)
uploaded_file = UploadedFile.from_params(params, :file, ::Packages::PackageFileUploader.workhorse_local_upload_path)
bad_request!('Missing package file!') unless uploaded_file
current_package = package || ::Packages::Conan::CreatePackageService.new(project, current_user, params).execute current_package = package || ::Packages::Conan::CreatePackageService.new(project, current_user, params).execute
track_event('push_package') if params[:file_name] == ::Packages::ConanFileMetadatum::PACKAGE_BINARY && params['file.size'].positive? track_event('push_package') if params[:file_name] == ::Packages::ConanFileMetadatum::PACKAGE_BINARY && params['file.size'].positive?
# conan sends two upload requests, the first has no file, so we skip record creation if file.size == 0 # conan sends two upload requests, the first has no file, so we skip record creation if file.size == 0
::Packages::Conan::CreatePackageFileService.new(current_package, uploaded_file, params.merge(conan_file_type: file_type)).execute unless params['file.size'] == 0 ::Packages::Conan::CreatePackageFileService.new(current_package, uploaded_package_file, params.merge(conan_file_type: file_type)).execute unless params['file.size'] == 0
rescue ObjectStorage::RemoteStoreError => e rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, file_name: params[:file_name], project_id: project.id) Gitlab::ErrorTracking.track_exception(e, file_name: params[:file_name], project_id: project.id)
forbidden! forbidden!
end end
def authorize_workhorse
authorize_upload
Gitlab::Workhorse.verify_api_request!(headers)
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
::Packages::PackageFileUploader.workhorse_authorize(has_length: true)
end
def authorize_upload
authorize!(:create_package, project)
require_gitlab_workhorse!
end
def find_personal_access_token def find_personal_access_token
personal_access_token = find_personal_access_token_from_conan_jwt || personal_access_token = find_personal_access_token_from_conan_jwt ||
find_personal_access_token_from_http_basic_auth find_personal_access_token_from_http_basic_auth
......
...@@ -7,26 +7,41 @@ module API ...@@ -7,26 +7,41 @@ module API
not_found! unless ::Gitlab.config.packages.enabled not_found! unless ::Gitlab.config.packages.enabled
end end
def authorize_packages_access!(subject) def authorize_packages_feature!(subject = user_project)
forbidden! unless subject.feature_available?(:packages)
end
def authorize_read_package!(subject = user_project)
authorize!(:read_package, subject)
end
def authorize_create_package!(subject = user_project)
authorize!(:create_package, subject)
end
def authorize_destroy_package!(subject = user_project)
authorize!(:destroy_package, subject)
end
def authorize_packages_access!(subject = user_project)
require_packages_enabled! require_packages_enabled!
authorize_packages_feature!(subject) authorize_packages_feature!(subject)
authorize_read_package!(subject) authorize_read_package!(subject)
end end
def authorize_packages_feature!(subject) def authorize_workhorse!(subject = user_project)
forbidden! unless subject.feature_available?(:packages) authorize_upload!(subject)
end
def authorize_read_package!(subject) Gitlab::Workhorse.verify_api_request!(headers)
authorize!(:read_package, subject)
end
def authorize_create_package! status 200
authorize!(:create_package, user_project) content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
::Packages::PackageFileUploader.workhorse_authorize(has_length: true)
end end
def authorize_destroy_package! def authorize_upload!(subject = user_project)
authorize!(:destroy_package, user_project) authorize_create_package!(subject)
require_gitlab_workhorse!
end end
end end
end end
......
...@@ -3,8 +3,19 @@ ...@@ -3,8 +3,19 @@
module API module API
module Helpers module Helpers
module PackagesManagerClientsHelpers module PackagesManagerClientsHelpers
extend Grape::API::Helpers
include ::API::Helpers::PackagesHelpers include ::API::Helpers::PackagesHelpers
params :workhorse_upload_params do
optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
end
def find_personal_access_token_from_http_basic_auth def find_personal_access_token_from_http_basic_auth
return unless headers return unless headers
...@@ -15,6 +26,12 @@ module API ...@@ -15,6 +26,12 @@ module API
PersonalAccessToken.find_by_token(token) PersonalAccessToken.find_by_token(token)
end end
def uploaded_package_file
uploaded_file = UploadedFile.from_params(params, :file, ::Packages::PackageFileUploader.workhorse_local_upload_path)
bad_request!('Missing package file!') unless uploaded_file
uploaded_file
end
end end
end end
end end
...@@ -14,8 +14,15 @@ module API ...@@ -14,8 +14,15 @@ module API
AUTHENTICATE_REALM_NAME = 'GitLab Nuget Package Registry' AUTHENTICATE_REALM_NAME = 'GitLab Nuget Package Registry'
POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze
PACKAGE_FILENAME = 'package.nupkg'
PACKAGE_FILETYPE = 'application/octet-stream'
default_format :json default_format :json
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
helpers do helpers do
def find_personal_access_token def find_personal_access_token
find_personal_access_token_from_http_basic_auth find_personal_access_token_from_http_basic_auth
...@@ -29,14 +36,20 @@ module API ...@@ -29,14 +36,20 @@ module API
project = find_project(id) project = find_project(id)
unless project && can?(current_user, :read_project, project) unless project && can?(current_user, :read_project, project)
return unauthorized_project_message return unauthorized_or! { not_found! }
end end
project project
end end
def unauthorized_project_message def authorize!(action, subject = :global, reason = nil)
current_user ? not_found! : unauthorized_with_header! return if can?(current_user, action, subject)
unauthorized_or! { forbidden!(reason) }
end
def unauthorized_or!
current_user ? yield : unauthorized_with_header!
end end
def unauthorized_with_header! def unauthorized_with_header!
...@@ -47,7 +60,6 @@ module API ...@@ -47,7 +60,6 @@ module API
before do before do
require_packages_enabled! require_packages_enabled!
authenticate_non_get!
end end
params do params do
...@@ -70,6 +82,38 @@ module API ...@@ -70,6 +82,38 @@ module API
present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project), present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project),
with: EE::API::Entities::Nuget::ServiceIndex with: EE::API::Entities::Nuget::ServiceIndex
end end
# https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
desc 'The NuGet Package Content endpoint' do
detail 'This feature was introduced in GitLab 12.6'
end
params do
use :workhorse_upload_params
end
put do
authorize_upload!(authorized_user_project)
package = ::Packages::Nuget::CreatePackageService.new(authorized_user_project, current_user).execute
file_params = params.merge(
file: uploaded_package_file,
file_name: PACKAGE_FILENAME,
file_type: PACKAGE_FILETYPE
)
track_event('push_package')
::Packages::CreatePackageFileService.new(package, file_params).execute
created!
rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
forbidden!
end
put 'authorize' do
authorize_workhorse!(authorized_user_project)
end
end end
end end
end end
......
...@@ -30,6 +30,12 @@ FactoryBot.define do ...@@ -30,6 +30,12 @@ FactoryBot.define do
end end
end end
factory :nuget_package do
sequence(:name) { |n| "NugetPackage#{n}"}
version { '1.0.0' }
package_type { :nuget }
end
factory :conan_package do factory :conan_package do
conan_metadatum conan_metadatum
......
# frozen_string_literal: true
require 'spec_helper'
describe API::Helpers::PackagesHelpers do
let_it_be(:helper) { Class.new.include(described_class).new }
let_it_be(:project) { create(:project) }
describe 'authorize_packages_access!' do
subject { helper.authorize_packages_access!(project) }
it 'authorizes packages access' do
expect(helper).to receive(:require_packages_enabled!)
expect(helper).to receive(:authorize_packages_feature!).with(project)
expect(helper).to receive(:authorize_read_package!).with(project)
expect(subject).to eq nil
end
end
describe 'authorize_packages_feature!' do
let(:feature_enabled) { true }
subject { helper.authorize_packages_feature!(project) }
before do
allow(project).to receive(:feature_available?).with(:packages).and_return(feature_enabled)
end
context 'with feature enabled' do
it "doesn't call forbidden!" do
expect(helper).to receive(:forbidden!).never
expect(subject).to eq nil
end
end
context 'with feature disabled' do
let(:feature_enabled) { false }
it 'calls forbidden!' do
expect(helper).to receive(:forbidden!).once
subject
end
end
end
%i[read_package create_package destroy_package].each do |action|
describe "authorize_#{action}!" do
subject { helper.send("authorize_#{action}!", project) }
it 'calls authorize!' do
expect(helper).to receive(:authorize!).with(action, project)
expect(subject).to eq nil
end
end
end
describe 'require_packages_enabled!' do
let(:packages_enabled) { true }
subject { helper.require_packages_enabled! }
before do
allow(::Gitlab.config.packages).to receive(:enabled).and_return(packages_enabled)
end
context 'with packages enabled' do
it "doesn't call not_found!" do
expect(helper).to receive(:not_found!).never
expect(subject).to eq nil
end
end
context 'with package disabled' do
let(:packages_enabled) { false }
it 'calls not_found!' do
expect(helper).to receive(:not_found!).once
subject
end
end
end
describe '#authorize_workhorse!' do
let_it_be(:headers) { {} }
subject { helper.authorize_workhorse!(project) }
before do
allow(helper).to receive(:headers).and_return(headers)
end
it 'authorizes workhorse' do
expect(helper).to receive(:authorize_upload!).with(project)
expect(helper).to receive(:status).with(200)
expect(helper).to receive(:content_type).with(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(Gitlab::Workhorse).to receive(:verify_api_request!).with(headers)
expect(::Packages::PackageFileUploader).to receive(:workhorse_authorize).with(has_length: true)
expect(subject).to eq nil
end
end
describe '#authorize_upload!' do
subject { helper.authorize_upload!(project) }
it 'authorizes the upload' do
expect(helper).to receive(:authorize_create_package!).with(project)
expect(helper).to receive(:require_gitlab_workhorse!)
expect(subject).to eq nil
end
end
end
...@@ -5,11 +5,11 @@ require 'spec_helper' ...@@ -5,11 +5,11 @@ require 'spec_helper'
describe API::Helpers::PackagesManagerClientsHelpers do describe API::Helpers::PackagesManagerClientsHelpers do
let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:personal_access_token) { create(:personal_access_token) }
let_it_be(:username) { personal_access_token.user.username } let_it_be(:username) { personal_access_token.user.username }
let_it_be(:helper) { Class.new.include(described_class).new }
let(:password) { personal_access_token.token } let(:password) { personal_access_token.token }
describe '#find_personal_access_token_from_http_basic_auth' do describe '#find_personal_access_token_from_http_basic_auth' do
let(:headers) { { Authorization: basic_http_auth(username, password) } } let(:headers) { { Authorization: basic_http_auth(username, password) } }
let(:helper) { Class.new.include(described_class).new }
subject { helper.find_personal_access_token_from_http_basic_auth } subject { helper.find_personal_access_token_from_http_basic_auth }
...@@ -42,6 +42,38 @@ describe API::Helpers::PackagesManagerClientsHelpers do ...@@ -42,6 +42,38 @@ describe API::Helpers::PackagesManagerClientsHelpers do
end end
end end
describe '#uploaded_package_file' do
let_it_be(:params) { {} }
subject { helper.uploaded_package_file }
before do
allow(helper).to receive(:params).and_return(params)
end
context 'with valid uploaded package file' do
let_it_be(:uploaded_file) { Object.new }
before do
allow(UploadedFile).to receive(:from_params).and_return(uploaded_file)
end
it { is_expected.to be uploaded_file }
end
context 'with invalid uploaded package file' do
before do
allow(UploadedFile).to receive(:from_params).and_return(nil)
end
it 'fails with bad_request!' do
expect(helper).to receive(:bad_request!)
expect(subject).to be nil
end
end
end
def basic_http_auth(username, password) def basic_http_auth(username, password)
ActionController::HttpAuthentication::Basic.encode_credentials(username, password) ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
end end
......
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::ConanPackages do describe API::ConanPackages do
include WorkhorseHelpers include WorkhorseHelpers
include EE::PackagesManagerApiSpecHelpers
let(:package) { create(:conan_package) } let(:package) { create(:conan_package) }
let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:personal_access_token) { create(:personal_access_token) }
...@@ -39,7 +40,7 @@ describe API::ConanPackages do ...@@ -39,7 +40,7 @@ describe API::ConanPackages do
it 'responds with 200 OK when valid token is provided' do it 'responds with 200 OK when valid token is provided' do
jwt = build_jwt(personal_access_token) jwt = build_jwt(personal_access_token)
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded) get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Conan-Server-Capabilities']).to eq("") expect(response.headers['X-Conan-Server-Capabilities']).to eq("")
...@@ -47,27 +48,27 @@ describe API::ConanPackages do ...@@ -47,27 +48,27 @@ describe API::ConanPackages do
it 'responds with 401 Unauthorized when invalid access token ID is provided' do it 'responds with 401 Unauthorized when invalid access token ID is provided' do
jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id) jwt = build_jwt(double(id: 12345), user_id: personal_access_token.user_id)
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded) get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:unauthorized)
end end
it 'responds with 401 Unauthorized when invalid user is provided' do it 'responds with 401 Unauthorized when invalid user is provided' do
jwt = build_jwt(personal_access_token, user_id: 12345) jwt = build_jwt(personal_access_token, user_id: 12345)
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded) get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:unauthorized)
end end
it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do it 'responds with 401 Unauthorized when the provided JWT is signed with different secret' do
jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32)) jwt = build_jwt(personal_access_token, secret: SecureRandom.base64(32))
get api('/packages/conan/v1/ping'), headers: build_auth_headers(jwt.encoded) get api('/packages/conan/v1/ping'), headers: build_token_auth_header(jwt.encoded)
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:unauthorized)
end end
it 'responds with 401 Unauthorized when invalid JWT is provided' do it 'responds with 401 Unauthorized when invalid JWT is provided' do
get api('/packages/conan/v1/ping'), headers: build_auth_headers('invalid-jwt') get api('/packages/conan/v1/ping'), headers: build_token_auth_header('invalid-jwt')
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:unauthorized)
end end
...@@ -154,7 +155,7 @@ describe API::ConanPackages do ...@@ -154,7 +155,7 @@ describe API::ConanPackages do
end end
it 'responds with a 401 Unauthorized when an invalid token is used' do it 'responds with a 401 Unauthorized when an invalid token is used' do
get api('/packages/conan/v1/users/check_credentials'), headers: build_auth_headers('invalid-token') get api('/packages/conan/v1/users/check_credentials'), headers: build_token_auth_header('invalid-token')
expect(response).to have_gitlab_http_status(:unauthorized) expect(response).to have_gitlab_http_status(:unauthorized)
end end
...@@ -259,7 +260,7 @@ describe API::ConanPackages do ...@@ -259,7 +260,7 @@ describe API::ConanPackages do
context 'recipe endpoints' do context 'recipe endpoints' do
let(:jwt) { build_jwt(personal_access_token) } let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) } let(:headers) { build_token_auth_header(jwt.encoded) }
let(:conan_package_reference) { '123456789' } let(:conan_package_reference) { '123456789' }
let(:presenter) { double('ConanPackagePresenter') } let(:presenter) { double('ConanPackagePresenter') }
...@@ -431,7 +432,7 @@ describe API::ConanPackages do ...@@ -431,7 +432,7 @@ describe API::ConanPackages do
context 'file endpoints' do context 'file endpoints' do
let(:jwt) { build_jwt(personal_access_token) } let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_auth_headers(jwt.encoded) } let(:headers) { build_token_auth_header(jwt.encoded) }
let(:recipe_path) { package.conan_recipe_path } let(:recipe_path) { package.conan_recipe_path }
shared_examples 'denies download with no token' do shared_examples 'denies download with no token' do
...@@ -547,7 +548,7 @@ describe API::ConanPackages do ...@@ -547,7 +548,7 @@ describe API::ConanPackages do
let(:jwt) { build_jwt(personal_access_token) } let(:jwt) { build_jwt(personal_access_token) }
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } } let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:headers_with_token) { build_auth_headers(jwt.encoded).merge(workhorse_header) } let(:headers_with_token) { build_token_auth_header(jwt.encoded).merge(workhorse_header) }
let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"} let(:recipe_path) { "foo/bar/#{project.full_path.tr('/', '+')}/baz"}
shared_examples 'uploads a package file' do shared_examples 'uploads a package file' do
...@@ -646,17 +647,7 @@ describe API::ConanPackages do ...@@ -646,17 +647,7 @@ describe API::ConanPackages do
end end
end end
context 'and background upload enabled' do it_behaves_like 'background upload schedules a file migration'
before do
stub_package_file_object_storage(background_upload: true)
end
it 'schedules migration of file to object storage' do
expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('Packages::PackageFileUploader', 'Packages::PackageFile', :file, kind_of(Numeric))
subject
end
end
end end
end end
...@@ -783,26 +774,5 @@ describe API::ConanPackages do ...@@ -783,26 +774,5 @@ describe API::ConanPackages do
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package' it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
end end
end end
def temp_file(package_tmp)
upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
file_path = "#{upload_path}/#{package_tmp}"
FileUtils.mkdir_p(upload_path)
File.write(file_path, 'test')
UploadedFile.new(file_path, filename: File.basename(file_path))
end
end
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
def build_auth_headers(token)
{ 'HTTP_AUTHORIZATION' => "Bearer #{token}" }
end end
end end
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
require 'spec_helper' require 'spec_helper'
describe API::NugetPackages do describe API::NugetPackages do
include WorkhorseHelpers
include EE::PackagesManagerApiSpecHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) } let_it_be(:project) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) } let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
...@@ -24,30 +27,30 @@ describe API::NugetPackages do ...@@ -24,30 +27,30 @@ describe API::NugetPackages do
context 'with valid project' do context 'with valid project' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
where(:project_visibility_level, :user_role, :member, :wrong_token, :shared_examples_name, :expected_status) do where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | false | 'returns nuget service index' | :success 'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
'PUBLIC' | :guest | true | false | 'returns nuget service index' | :success 'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
'PUBLIC' | :developer | true | true | 'returns nuget service index' | :success 'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success
'PUBLIC' | :guest | true | true | 'returns nuget service index' | :success 'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success
'PUBLIC' | :developer | false | false | 'returns nuget service index' | :success 'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
'PUBLIC' | :guest | false | false | 'returns nuget service index' | :success 'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
'PUBLIC' | :developer | false | true | 'returns nuget service index' | :success 'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success
'PUBLIC' | :guest | false | true | 'returns nuget service index' | :success 'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success
'PUBLIC' | :anonymous | false | false | 'returns nuget service index' | :success 'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success
'PRIVATE' | :developer | true | false | 'returns nuget service index' | :success 'PRIVATE' | :developer | true | true | 'process nuget service index request' | :success
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :forbidden 'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | true | 'rejects nuget packages access' | :unauthorized 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :unauthorized 'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :not_found 'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :not_found 'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :unauthorized 'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :unauthorized 'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | false | 'rejects nuget packages access' | :unauthorized 'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
end end
with_them do with_them do
let(:token) { wrong_token ? 'wrong' : personal_access_token.token } let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : build_auth_headers(basic_http_auth(user.username, token)) } let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) }
subject { get api(url), headers: headers } subject { get api(url), headers: headers }
...@@ -63,45 +66,91 @@ describe API::NugetPackages do ...@@ -63,45 +66,91 @@ describe API::NugetPackages do
end end
end end
context 'with an unknown project' do it_behaves_like 'rejects nuget access with unknown project id'
let(:project) { OpenStruct.new(id: 1234567890) }
context 'as anonymous' do it_behaves_like 'rejects nuget access with invalid project id'
it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized end
end
context 'with feature flag disabled' do
before do
stub_feature_flags(nuget_package_registry: { enabled: false, thing: project })
end
context 'as authenticated user' do it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
subject { get api(url), headers: build_auth_headers(basic_http_auth(user.username, personal_access_token.token)) } end
end
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found context 'with packages features disabled' do
end before do
stub_licensed_features(packages: false)
end
it_behaves_like 'rejects nuget packages access', :anonymous, :forbidden
end
end
describe 'PUT /api/v4/projects/:id/packages/nuget/authorize' do
let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let(:url) { "/projects/#{project.id}/packages/nuget/authorize" }
let(:headers) { {} }
subject { put api(url), headers: headers }
context 'with packages features enabled' do
before do
stub_licensed_features(packages: true)
end
context 'with feature flag enabled' do
before do
stub_feature_flags(nuget_package_registry: { enabled: true, thing: project })
end end
context 'with a project id with invalid integers' do context 'with valid project' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:project) { OpenStruct.new(id: id) } where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget workhorse authorization' | :success
where(:id, :status) do 'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'/../' | :unauthorized 'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'' | :not_found 'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'%20' | :unauthorized 'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
'%2e%2e%2f' | :unauthorized 'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
'NaN' | :unauthorized 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
00002345 | :unauthorized 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'anything25' | :unauthorized 'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | true | true | 'process nuget workhorse authorization' | :success
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
end end
with_them do with_them do
it_behaves_like 'rejects nuget packages access', :anonymous, params[:status] let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
end
after do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end end
end end
context 'with invalid format' do it_behaves_like 'rejects nuget access with unknown project id'
let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found it_behaves_like 'rejects nuget access with invalid project id'
end
end end
context 'with feature flag disabled' do context 'with feature flag disabled' do
...@@ -122,11 +171,95 @@ describe API::NugetPackages do ...@@ -122,11 +171,95 @@ describe API::NugetPackages do
end end
end end
def build_auth_headers(value) describe 'PUT /api/v4/projects/:id/packages/nuget' do
{ 'HTTP_AUTHORIZATION' => value } let_it_be(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
end let_it_be(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
let_it_be(:file_name) { 'package.nupkg' }
let(:url) { "/projects/#{project.id}/packages/nuget" }
let(:headers) { {} }
let(:params) { { file: temp_file(file_name) } }
def basic_http_auth(username, password) subject do
ActionController::HttpAuthentication::Basic.encode_credentials(username, password) workhorse_finalize(
api(url),
method: :put,
file_key: :file,
params: params,
headers: headers
)
end
context 'with packages features enabled' do
before do
stub_licensed_features(packages: true)
end
context 'with feature flag enabled' do
before do
stub_feature_flags(nuget_package_registry: { enabled: true, thing: project })
end
context 'with valid project' do
using RSpec::Parameterized::TableSyntax
where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget upload' | :created
'PUBLIC' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :guest | false | true | 'rejects nuget packages access' | :forbidden
'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | true | true | 'process nuget upload' | :created
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects nuget packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:user_headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) }
let(:headers) { user_headers.merge(workhorse_header) }
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
end
after do
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
it_behaves_like 'rejects nuget access with unknown project id'
it_behaves_like 'rejects nuget access with invalid project id'
end
context 'with feature flag disabled' do
before do
stub_feature_flags(nuget_package_registry: { enabled: false, thing: project })
end
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end
end
context 'with packages features disabled' do
before do
stub_licensed_features(packages: false)
end
it_behaves_like 'rejects nuget packages access', :anonymous, :forbidden
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Packages::Nuget::CreatePackageService do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:params) { {} }
describe '#execute' do
subject { described_class.new(project, user, params).execute }
it 'creates the package' do
expect { subject }.to change { Packages::Package.count }.by(1)
package = Packages::Package.last
expect(package).to be_valid
expect(package.name).to eq(Packages::Nuget::CreatePackageService::PACKAGE_NAME)
expect(package.version).to eq(Packages::Nuget::CreatePackageService::PACKAGE_VERSION)
expect(package.package_type).to eq('nuget')
end
end
end
# frozen_string_literal: true
module EE
module PackagesManagerApiSpecHelpers
def build_auth_headers(value)
{ 'HTTP_AUTHORIZATION' => value }
end
def build_basic_auth_header(username, password)
build_auth_headers(ActionController::HttpAuthentication::Basic.encode_credentials(username, password))
end
def build_token_auth_header(token)
build_auth_headers("Bearer #{token}")
end
def build_jwt(personal_access_token, secret: jwt_secret, user_id: nil)
JSONWebToken::HMACToken.new(secret).tap do |jwt|
jwt['pat'] = personal_access_token.id
jwt['u'] = user_id || personal_access_token.user_id
end
end
def temp_file(package_tmp)
upload_path = ::Packages::PackageFileUploader.workhorse_local_upload_path
file_path = "#{upload_path}/#{package_tmp}"
FileUtils.mkdir_p(upload_path)
File.write(file_path, 'test')
UploadedFile.new(file_path, filename: File.basename(file_path))
end
end
end
...@@ -18,7 +18,7 @@ shared_examples 'rejects nuget packages access' do |user_type, status, add_membe ...@@ -18,7 +18,7 @@ shared_examples 'rejects nuget packages access' do |user_type, status, add_membe
end end
end end
shared_examples 'returns nuget service index' do |user_type, status, add_member = true| shared_examples 'process nuget service index request' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
...@@ -38,5 +38,146 @@ shared_examples 'returns nuget service index' do |user_type, status, add_member ...@@ -38,5 +38,146 @@ shared_examples 'returns nuget service index' do |user_type, status, add_member
expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index', dir: 'ee') expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index', dir: 'ee')
end end
context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end
end
end
shared_examples 'process nuget workhorse authorization' 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
it 'has the proper content type' do
subject
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
context 'with a request that bypassed gitlab-workhorse' do
let(:headers) do
build_basic_auth_header(user.username, personal_access_token.token)
.merge(workhorse_header)
.tap { |h| h.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER) }
end
before do
project.add_maintainer(user)
end
it_behaves_like 'returning response status', :error
end
end
end
shared_examples 'process nuget upload' do |user_type, status, add_member = true|
shared_examples 'creates nuget package files' do
it 'creates package files' do
expect { subject }
.to change { project.packages.count }.by(1)
.and change { Packages::PackageFile.count }.by(1)
expect(response).to have_gitlab_http_status(status)
package_file = project.packages.last.package_files.reload.last
expect(package_file.file_name).to eq('package.nupkg')
expect(package_file.file_type).to eq(0)
end
end
context "for user type #{user_type}" do
before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end
context 'with object storage disabled' do
context 'without a file from workhorse' do
let(:params) { { file: nil } }
it_behaves_like 'returning response status', :bad_request
end
context 'with correct params' do
it_behaves_like 'creates nuget package files'
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
end
end
context 'with object storage enabled' do
context 'and direct upload enabled' do
let!(:fog_connection) do
stub_package_file_object_storage(direct_upload: true)
end
let(:tmp_object) do
fog_connection.directories.new(key: 'packages').files.create(
key: "tmp/uploads/#{file_name}",
body: 'content'
)
end
let(:fog_file) { fog_to_uploaded_file(tmp_object) }
let(:params) { { file: fog_file, 'file.remote_id' => file_name } }
it_behaves_like 'creates nuget package files'
['123123', '../../123123'].each do |remote_id|
context "with invalid remote_id: #{remote_id}" do
let(:params) do
{
file: fog_file,
'file.remote_id' => remote_id
}
end
it_behaves_like 'returning response status', :forbidden
end
end
end
it_behaves_like 'background upload schedules a file migration'
end
end
end
shared_examples 'rejects nuget access with invalid project id' do
context 'with a project id with invalid integers' do
using RSpec::Parameterized::TableSyntax
let(:project) { OpenStruct.new(id: id) }
where(:id, :status) do
'/../' | :unauthorized
'' | :not_found
'%20' | :unauthorized
'%2e%2e%2f' | :unauthorized
'NaN' | :unauthorized
00002345 | :unauthorized
'anything25' | :unauthorized
end
with_them do
it_behaves_like 'rejects nuget packages access', :anonymous, params[:status]
end
end
end
shared_examples 'rejects nuget 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 'rejects nuget packages access', :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 'rejects nuget packages access', :anonymous, :not_found
end
end end
end end
...@@ -103,3 +103,17 @@ shared_examples 'returns paginated packages' do ...@@ -103,3 +103,17 @@ shared_examples 'returns paginated packages' do
end end
end end
end end
shared_examples 'background upload schedules a file migration' do
context 'background upload enabled' do
before do
stub_package_file_object_storage(background_upload: true)
end
it 'schedules migration of file to object storage' do
expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('Packages::PackageFileUploader', 'Packages::PackageFile', :file, kind_of(Numeric))
subject
end
end
end
...@@ -363,6 +363,10 @@ module API ...@@ -363,6 +363,10 @@ module API
render_api_error!('204 No Content', 204) render_api_error!('204 No Content', 204)
end end
def created!
render_api_error!('201 Created', 201)
end
def accepted! def accepted!
render_api_error!('202 Accepted', 202) render_api_error!('202 Accepted', 202)
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