Commit 914dd34b authored by Steve Abrams's avatar Steve Abrams Committed by Michael Kozono

Rubygems dependencies endpoint

Adds rubygems dependency resolver service
and route to return dependencies for
requested gems.
parent 6f95667d
# frozen_string_literal: true
module Packages
module Rubygems
class DependencyResolverService < BaseService
include Gitlab::Utils::StrongMemoize
DEFAULT_PLATFORM = 'ruby'
def execute
return ServiceResponse.error(message: "forbidden", http_status: :forbidden) unless Ability.allowed?(current_user, :read_package, project)
return ServiceResponse.error(message: "#{gem_name} not found", http_status: :not_found) if packages.empty?
payload = packages.map do |package|
dependencies = package.dependency_links.map do |link|
[link.dependency.name, link.dependency.version_pattern]
end
{
name: gem_name,
number: package.version,
platform: DEFAULT_PLATFORM,
dependencies: dependencies
}
end
ServiceResponse.success(payload: payload)
end
private
def packages
strong_memoize(:packages) do
project.packages.with_name(gem_name)
end
end
def gem_name
params[:gem_name]
end
end
end
end
......@@ -26,7 +26,7 @@ module API
before do
require_packages_enabled!
authenticate!
authenticate_non_get!
not_found! unless Feature.enabled?(:rubygem_packages, user_project)
end
......@@ -118,11 +118,24 @@ module API
detail 'This feature was introduced in GitLab 13.9'
end
params do
optional :gems, type: String, desc: 'Comma delimited gem names'
optional :gems, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma delimited gem names'
end
get 'dependencies' do
# To be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/299282
not_found!
authorize_read_package!
if params[:gems].blank?
status :ok
else
results = params[:gems].map do |gem_name|
service_result = Packages::Rubygems::DependencyResolverService.new(user_project, current_user, gem_name: gem_name).execute
render_api_error!(service_result.message, service_result.http_status) if service_result.error?
service_result.payload
end
content_type 'application/octet-stream'
Marshal.dump(results.flatten)
end
end
end
end
......
......@@ -277,6 +277,10 @@ FactoryBot.define do
factory :packages_dependency, class: 'Packages::Dependency' do
sequence(:name) { |n| "@test/package-#{n}"}
sequence(:version_pattern) { |n| "~6.2.#{n}" }
trait(:rubygems) do
sequence(:name) { |n| "gem-dependency-#{n}"}
end
end
factory :packages_dependency_link, class: 'Packages::DependencyLink' do
......@@ -289,6 +293,11 @@ FactoryBot.define do
link.nuget_metadatum = build(:nuget_dependency_link_metadatum)
end
end
trait(:rubygems) do
package { association(:rubygems_package) }
dependency { association(:packages_dependency, :rubygems) }
end
end
factory :nuget_dependency_link_metadatum, class: 'Packages::Nuget::DependencyLinkMetadatum' do
......
......@@ -44,7 +44,7 @@ RSpec.describe API::RubygemPackages do
end
shared_examples 'without authentication' do
it_behaves_like 'returning response status', :unauthorized
it_behaves_like 'returning response status', :not_found
end
shared_examples 'with authentication' do
......@@ -276,10 +276,65 @@ RSpec.describe API::RubygemPackages do
end
describe 'GET /api/v4/projects/:project_id/packages/rubygems/api/v1/dependencies' do
let_it_be(:package) { create(:rubygems_package, project: project) }
let(:url) { api("/projects/#{project.id}/packages/rubygems/api/v1/dependencies") }
subject { get(url, headers: headers) }
subject { get(url, headers: headers, params: params) }
it_behaves_like 'an unimplemented route'
context 'with valid project' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :guest | true | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :developer | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | false | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :guest | false | :personal_access_token | true | 'dependency endpoint success' | :success
:public | :developer | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :anonymous | false | :personal_access_token | true | 'dependency endpoint success' | :success
:private | :developer | true | :personal_access_token | true | 'dependency endpoint success' | :success
:private | :guest | true | :personal_access_token | true | 'rejects rubygems packages access' | :forbidden
:private | :developer | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | true | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:private | :guest | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:private | :developer | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :anonymous | false | :personal_access_token | true | 'rejects rubygems packages access' | :not_found
:public | :developer | true | :job_token | true | 'dependency endpoint success' | :success
:public | :guest | true | :job_token | true | 'dependency endpoint success' | :success
:public | :developer | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | false | :job_token | true | 'dependency endpoint success' | :success
:public | :guest | false | :job_token | true | 'dependency endpoint success' | :success
:public | :developer | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :guest | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | true | :job_token | true | 'dependency endpoint success' | :success
:private | :guest | true | :job_token | true | 'rejects rubygems packages access' | :forbidden
:private | :developer | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | true | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | false | :job_token | true | 'rejects rubygems packages access' | :not_found
:private | :guest | false | :job_token | true | 'rejects rubygems packages access' | :not_found
:private | :developer | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :guest | false | :job_token | false | 'rejects rubygems packages access' | :unauthorized
:public | :developer | true | :deploy_token | true | 'dependency endpoint success' | :success
:public | :developer | true | :deploy_token | false | 'rejects rubygems packages access' | :unauthorized
:private | :developer | true | :deploy_token | true | 'dependency endpoint success' | :success
:private | :developer | true | :deploy_token | false | 'rejects rubygems packages access' | :unauthorized
end
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
let(:headers) { user_role == :anonymous ? {} : { 'HTTP_AUTHORIZATION' => token } }
let(:params) { {} }
before do
project.update!(visibility: visibility.to_s)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Packages::Rubygems::DependencyResolverService do
let_it_be(:project) { create(:project, :private) }
let_it_be(:package) { create(:package, project: project) }
let_it_be(:user) { create(:user) }
let(:gem_name) { package.name }
let(:service) { described_class.new(project, user, gem_name: gem_name) }
describe '#execute' do
subject { service.execute }
context 'user without access' do
it 'returns a service error' do
expect(subject.error?).to be(true)
expect(subject.message).to eq('forbidden')
end
end
context 'user with access' do
before do
project.add_developer(user)
end
context 'when no package is found' do
let(:gem_name) { nil }
it 'returns a service error', :aggregate_failures do
expect(subject.error?).to be(true)
expect(subject.message).to eq("#{gem_name} not found")
end
end
context 'package without dependencies' do
it 'returns an empty dependencies array' do
expected_result = [{
name: package.name,
number: package.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: []
}]
expect(subject.payload).to eq(expected_result)
end
end
context 'package with dependencies' do
let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)}
it 'returns a set of dependencies' do
expected_result = [{
name: package.name,
number: package.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: [
[dependency_link.dependency.name, dependency_link.dependency.version_pattern],
[dependency_link2.dependency.name, dependency_link2.dependency.version_pattern],
[dependency_link3.dependency.name, dependency_link3.dependency.version_pattern]
]
}]
expect(subject.payload).to eq(expected_result)
end
end
context 'package with multiple versions' do
let(:dependency_link) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link2) { create(:packages_dependency_link, :rubygems, package: package)}
let(:dependency_link3) { create(:packages_dependency_link, :rubygems, package: package)}
let(:package2) { create(:package, project: project, name: package.name, version: '9.9.9') }
let(:dependency_link4) { create(:packages_dependency_link, :rubygems, package: package2)}
it 'returns a set of dependencies' do
expected_result = [{
name: package.name,
number: package.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: [
[dependency_link.dependency.name, dependency_link.dependency.version_pattern],
[dependency_link2.dependency.name, dependency_link2.dependency.version_pattern],
[dependency_link3.dependency.name, dependency_link3.dependency.version_pattern]
]
}, {
name: package2.name,
number: package2.version,
platform: described_class::DEFAULT_PLATFORM,
dependencies: [
[dependency_link4.dependency.name, dependency_link4.dependency.version_pattern]
]
}]
expect(subject.payload).to eq(expected_result)
end
end
end
end
end
......@@ -128,3 +128,50 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe
end
end
end
RSpec.shared_examples 'dependency endpoint success' 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
raise 'Status is not :success' if status != :success
context 'with no params', :aggregate_failures do
it 'returns empty' do
subject
expect(response.body).to eq('200')
expect(response).to have_gitlab_http_status(status)
end
end
context 'with gems params' do
let(:params) { { gems: 'foo,bar' } }
let(:expected_response) { Marshal.dump(%w(result result)) }
it 'returns successfully', :aggregate_failures do
service_result = double('DependencyResolverService', execute: ServiceResponse.success(payload: 'result'))
expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result)
expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'bar').and_return(service_result)
subject
expect(response.body).to eq(expected_response) # rubocop:disable Security/MarshalLoad
expect(response).to have_gitlab_http_status(status)
end
it 'rejects if the service fails', :aggregate_failures do
service_result = double('DependencyResolverService', execute: ServiceResponse.error(message: 'rejected', http_status: :bad_request))
expect(Packages::Rubygems::DependencyResolverService).to receive(:new).with(project, anything, gem_name: 'foo').and_return(service_result)
subject
expect(response.body).to match(/rejected/)
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment