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 ...@@ -26,7 +26,7 @@ module API
before do before do
require_packages_enabled! require_packages_enabled!
authenticate! authenticate_non_get!
not_found! unless Feature.enabled?(:rubygem_packages, user_project) not_found! unless Feature.enabled?(:rubygem_packages, user_project)
end end
...@@ -118,11 +118,24 @@ module API ...@@ -118,11 +118,24 @@ module API
detail 'This feature was introduced in GitLab 13.9' detail 'This feature was introduced in GitLab 13.9'
end end
params do 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 end
get 'dependencies' do get 'dependencies' do
# To be implemented in https://gitlab.com/gitlab-org/gitlab/-/issues/299282 authorize_read_package!
not_found!
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 end
end end
......
...@@ -277,6 +277,10 @@ FactoryBot.define do ...@@ -277,6 +277,10 @@ FactoryBot.define do
factory :packages_dependency, class: 'Packages::Dependency' do factory :packages_dependency, class: 'Packages::Dependency' do
sequence(:name) { |n| "@test/package-#{n}"} sequence(:name) { |n| "@test/package-#{n}"}
sequence(:version_pattern) { |n| "~6.2.#{n}" } sequence(:version_pattern) { |n| "~6.2.#{n}" }
trait(:rubygems) do
sequence(:name) { |n| "gem-dependency-#{n}"}
end
end end
factory :packages_dependency_link, class: 'Packages::DependencyLink' do factory :packages_dependency_link, class: 'Packages::DependencyLink' do
...@@ -289,6 +293,11 @@ FactoryBot.define do ...@@ -289,6 +293,11 @@ FactoryBot.define do
link.nuget_metadatum = build(:nuget_dependency_link_metadatum) link.nuget_metadatum = build(:nuget_dependency_link_metadatum)
end end
end end
trait(:rubygems) do
package { association(:rubygems_package) }
dependency { association(:packages_dependency, :rubygems) }
end
end end
factory :nuget_dependency_link_metadatum, class: 'Packages::Nuget::DependencyLinkMetadatum' do factory :nuget_dependency_link_metadatum, class: 'Packages::Nuget::DependencyLinkMetadatum' do
......
...@@ -44,7 +44,7 @@ RSpec.describe API::RubygemPackages do ...@@ -44,7 +44,7 @@ RSpec.describe API::RubygemPackages do
end end
shared_examples 'without authentication' do shared_examples 'without authentication' do
it_behaves_like 'returning response status', :unauthorized it_behaves_like 'returning response status', :not_found
end end
shared_examples 'with authentication' do shared_examples 'with authentication' do
...@@ -276,10 +276,65 @@ RSpec.describe API::RubygemPackages do ...@@ -276,10 +276,65 @@ RSpec.describe API::RubygemPackages do
end end
describe 'GET /api/v4/projects/:project_id/packages/rubygems/api/v1/dependencies' do 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") } 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
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 ...@@ -128,3 +128,50 @@ RSpec.shared_examples 'process rubygems upload' do |user_type, status, add_membe
end end
end 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