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
validate :valid_npm_package_name, 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_like, ->(name) { where(arel_table[:name].matches(name)) }
......
......@@ -52,7 +52,7 @@ module Packages
end
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
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
detail 'This feature was introduced in GitLab 12.6'
end
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)'
use :workhorse_upload_params
end
put do
upload_package_file(:recipe_file)
......@@ -235,7 +229,7 @@ module API
detail 'This feature was introduced in GitLab 12.6'
end
put 'authorize' do
authorize_workhorse
authorize_workhorse!(project)
end
end
......@@ -256,20 +250,14 @@ module API
detail 'This feature was introduced in GitLab 12.6'
end
put 'authorize' do
authorize_workhorse
authorize_workhorse!(project)
end
desc 'Upload package files' do
detail 'This feature was introduced in GitLab 12.6'
end
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)'
use :workhorse_upload_params
end
put do
upload_package_file(:package_file)
......@@ -380,38 +368,20 @@ module API
end
def upload_package_file(file_type)
authorize_upload
uploaded_file = UploadedFile.from_params(params, :file, ::Packages::PackageFileUploader.workhorse_local_upload_path)
bad_request!('Missing package file!') unless uploaded_file
authorize_upload!(project)
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?
# 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
Gitlab::ErrorTracking.track_exception(e, file_name: params[:file_name], project_id: project.id)
forbidden!
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
personal_access_token = find_personal_access_token_from_conan_jwt ||
find_personal_access_token_from_http_basic_auth
......
......@@ -7,26 +7,41 @@ module API
not_found! unless ::Gitlab.config.packages.enabled
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!
authorize_packages_feature!(subject)
authorize_read_package!(subject)
end
def authorize_packages_feature!(subject)
forbidden! unless subject.feature_available?(:packages)
end
def authorize_workhorse!(subject = user_project)
authorize_upload!(subject)
def authorize_read_package!(subject)
authorize!(:read_package, subject)
end
Gitlab::Workhorse.verify_api_request!(headers)
def authorize_create_package!
authorize!(:create_package, user_project)
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
::Packages::PackageFileUploader.workhorse_authorize(has_length: true)
end
def authorize_destroy_package!
authorize!(:destroy_package, user_project)
def authorize_upload!(subject = user_project)
authorize_create_package!(subject)
require_gitlab_workhorse!
end
end
end
......
......@@ -3,8 +3,19 @@
module API
module Helpers
module PackagesManagerClientsHelpers
extend Grape::API::Helpers
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
return unless headers
......@@ -15,6 +26,12 @@ module API
PersonalAccessToken.find_by_token(token)
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
......@@ -14,8 +14,15 @@ module API
AUTHENTICATE_REALM_NAME = 'GitLab Nuget Package Registry'
POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze
PACKAGE_FILENAME = 'package.nupkg'
PACKAGE_FILETYPE = 'application/octet-stream'
default_format :json
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
helpers do
def find_personal_access_token
find_personal_access_token_from_http_basic_auth
......@@ -29,14 +36,20 @@ module API
project = find_project(id)
unless project && can?(current_user, :read_project, project)
return unauthorized_project_message
return unauthorized_or! { not_found! }
end
project
end
def unauthorized_project_message
current_user ? not_found! : unauthorized_with_header!
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!
......@@ -47,7 +60,6 @@ module API
before do
require_packages_enabled!
authenticate_non_get!
end
params do
......@@ -70,6 +82,38 @@ module API
present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project),
with: EE::API::Entities::Nuget::ServiceIndex
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
......
......@@ -30,6 +30,12 @@ FactoryBot.define do
end
end
factory :nuget_package do
sequence(:name) { |n| "NugetPackage#{n}"}
version { '1.0.0' }
package_type { :nuget }
end
factory :conan_package do
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'
describe API::Helpers::PackagesManagerClientsHelpers do
let_it_be(:personal_access_token) { create(:personal_access_token) }
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 }
describe '#find_personal_access_token_from_http_basic_auth' do
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 }
......@@ -42,6 +42,38 @@ describe API::Helpers::PackagesManagerClientsHelpers do
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)
ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
end
......
......@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::ConanPackages do
include WorkhorseHelpers
include EE::PackagesManagerApiSpecHelpers
let(:package) { create(:conan_package) }
let_it_be(:personal_access_token) { create(:personal_access_token) }
......@@ -39,7 +40,7 @@ describe API::ConanPackages do
it 'responds with 200 OK when valid token is provided' do
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.headers['X-Conan-Server-Capabilities']).to eq("")
......@@ -47,27 +48,27 @@ describe API::ConanPackages 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)
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)
end
it 'responds with 401 Unauthorized when invalid user is provided' do
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)
end
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))
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)
end
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)
end
......@@ -154,7 +155,7 @@ describe API::ConanPackages do
end
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)
end
......@@ -259,7 +260,7 @@ describe API::ConanPackages do
context 'recipe endpoints' do
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(:presenter) { double('ConanPackagePresenter') }
......@@ -431,7 +432,7 @@ describe API::ConanPackages do
context 'file endpoints' do
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 }
shared_examples 'denies download with no token' do
......@@ -547,7 +548,7 @@ describe API::ConanPackages do
let(:jwt) { build_jwt(personal_access_token) }
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(: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"}
shared_examples 'uploads a package file' do
......@@ -646,17 +647,7 @@ describe API::ConanPackages do
end
end
context 'and 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
it_behaves_like 'background upload schedules a file migration'
end
end
......@@ -783,26 +774,5 @@ describe API::ConanPackages do
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
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
......@@ -2,6 +2,9 @@
require 'spec_helper'
describe API::NugetPackages do
include WorkhorseHelpers
include EE::PackagesManagerApiSpecHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
......@@ -24,30 +27,30 @@ describe API::NugetPackages do
context 'with valid project' do
using RSpec::Parameterized::TableSyntax
where(:project_visibility_level, :user_role, :member, :wrong_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | false | 'returns nuget service index' | :success
'PUBLIC' | :guest | true | false | 'returns nuget service index' | :success
'PUBLIC' | :developer | true | true | 'returns nuget service index' | :success
'PUBLIC' | :guest | true | true | 'returns nuget service index' | :success
'PUBLIC' | :developer | false | false | 'returns nuget service index' | :success
'PUBLIC' | :guest | false | false | 'returns nuget service index' | :success
'PUBLIC' | :developer | false | true | 'returns nuget service index' | :success
'PUBLIC' | :guest | false | true | 'returns nuget service index' | :success
'PUBLIC' | :anonymous | false | false | 'returns nuget service index' | :success
'PRIVATE' | :developer | true | false | 'returns nuget service index' | :success
'PRIVATE' | :guest | true | false | 'rejects nuget packages access' | :forbidden
'PRIVATE' | :developer | true | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | true | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :developer | false | false | 'rejects nuget packages access' | :not_found
'PRIVATE' | :guest | false | false | 'rejects nuget packages access' | :not_found
'PRIVATE' | :developer | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :guest | false | true | 'rejects nuget packages access' | :unauthorized
'PRIVATE' | :anonymous | false | false | 'rejects nuget packages access' | :unauthorized
where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process nuget service index request' | :success
'PUBLIC' | :guest | true | true | 'process nuget service index request' | :success
'PUBLIC' | :developer | true | false | 'process nuget service index request' | :success
'PUBLIC' | :guest | true | false | 'process nuget service index request' | :success
'PUBLIC' | :developer | false | true | 'process nuget service index request' | :success
'PUBLIC' | :guest | false | true | 'process nuget service index request' | :success
'PUBLIC' | :developer | false | false | 'process nuget service index request' | :success
'PUBLIC' | :guest | false | false | 'process nuget service index request' | :success
'PUBLIC' | :anonymous | false | true | 'process nuget service index request' | :success
'PRIVATE' | :developer | true | true | 'process nuget service index request' | :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
with_them do
let(:token) { wrong_token ? 'wrong' : personal_access_token.token }
let(:headers) { user_role == :anonymous ? {} : build_auth_headers(basic_http_auth(user.username, token)) }
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : build_basic_auth_header(user.username, token) }
subject { get api(url), headers: headers }
......@@ -63,45 +66,91 @@ describe API::NugetPackages do
end
end
context 'with an unknown project' do
let(:project) { OpenStruct.new(id: 1234567890) }
it_behaves_like 'rejects nuget access with unknown project id'
context 'as anonymous' do
it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized
it_behaves_like 'rejects nuget access with invalid project id'
end
context 'as authenticated user' do
subject { get api(url), headers: build_auth_headers(basic_http_auth(user.username, personal_access_token.token)) }
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 a project id with invalid integers' do
using RSpec::Parameterized::TableSyntax
context 'with packages features disabled' do
before do
stub_licensed_features(packages: false)
end
let(:project) { OpenStruct.new(id: id) }
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 }
where(:id, :status) do
'/../' | :unauthorized
'' | :not_found
'%20' | :unauthorized
'%2e%2e%2f' | :unauthorized
'NaN' | :unauthorized
00002345 | :unauthorized
'anything25' | :unauthorized
context 'with packages features enabled' do
before do
stub_licensed_features(packages: true)
end
with_them do
it_behaves_like 'rejects nuget packages access', :anonymous, params[:status]
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 workhorse authorization' | :success
'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 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
context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" }
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) }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
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
......@@ -122,11 +171,95 @@ describe API::NugetPackages do
end
end
def build_auth_headers(value)
{ 'HTTP_AUTHORIZATION' => value }
describe 'PUT /api/v4/projects/:id/packages/nuget' 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_it_be(:file_name) { 'package.nupkg' }
let(:url) { "/projects/#{project.id}/packages/nuget" }
let(:headers) { {} }
let(:params) { { file: temp_file(file_name) } }
subject do
workhorse_finalize(
api(url),
method: :put,
file_key: :file,
params: params,
headers: headers
)
end
def basic_http_auth(username, password)
ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
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
# 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
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
before do
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
expect(json_response).to match_schema('public_api/v4/packages/nuget/service_index', dir: 'ee')
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
......@@ -103,3 +103,17 @@ shared_examples 'returns paginated packages' do
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
render_api_error!('204 No Content', 204)
end
def created!
render_api_error!('201 Created', 201)
end
def accepted!
render_api_error!('202 Accepted', 202)
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