Commit 31a8ba8a authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Add group based Maven API endpoint

User will be able to download any maven package within the group with
URL like /api/v4/groups/GROUP_ID_OR_NAME/packages/maven
Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parent b7c0076e
......@@ -163,6 +163,34 @@ the `distributionManagement` section:
If you have a self-hosted GitLab installation, replace `gitlab.com` with your
domain name.
## Group level Maven endpoint
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/8798) in GitLab Premium 11.7.
If you rely on many packages, it might be inefficient to include the `repository` section
with a unique URL for each package. Instead, you can use the group level endpoint for
all your maven packages stored within one GitLab group. Only packages you have access to
will be available for download. Here's how the relevant `repository` section of
your `pom.xml` would look like:
```xml
<repositories>
<repository>
<id>gitlab-maven</id>
<url>https://gitlab.com/api/v4/groups/my-company/-/packages/maven</url>
</repository>
</repositories>
```
If you have a self-hosted GitLab installation, replace `gitlab.com` with your
domain name.
**Notes**:
- Group level endpoint works with any package names. That means if you have a flexibility of naming compared to instance level endpoint. However, that means GitLab will not guarantee uniqueness of package names withing the group. You can have two projects with a same package name and a package version. As result, GitLab will serve whichever one is more recent.
- You still need a project specific URL for uploading a package
in the `distributionManagement` section.
## Uploading packages
Once you have set up the [authorization](#authorizing-with-the-gitlab-maven-repository)
......
# frozen_string_literal: true
class Packages::MavenPackageFinder
attr_reader :path, :project
attr_reader :path, :current_user, :project, :group
def initialize(path, project = nil)
def initialize(path, current_user, project: nil, group: nil)
@path = path
@current_user = current_user
@project = project
@group = group
end
def execute
packages.last
packages_with_path.last
end
def execute!
packages.last!
packages_with_path.last!
end
private
def scope
def base
if project
project.packages
packages_for_a_single_project
elsif group
packages_for_multiple_projects
else
::Packages::Package.all
packages
end
end
# rubocop: disable CodeReuse/ActiveRecord
def packages_with_path
base.only_maven_packages_with_path(path)
end
# Produces a query that returns all packages.
def packages
scope.joins(:maven_metadatum)
.where(packages_maven_metadata: { path: path })
::Packages::Package.all
end
# Produces a query that retrieves packages from a single project.
def packages_for_a_single_project
project.packages
end
# Produces a query that retrieves packages from multiple projects that
# the current user can view within a group.
def packages_for_multiple_projects
::Packages::Package.for_projects(projects_visible_to_current_user)
end
# Returns the projects that the current user can view within a group.
def projects_visible_to_current_user
::Project
.in_namespace(group.self_and_descendants.select(:id))
.public_or_visible_to_user(current_user)
end
# rubocop: enable CodeReuse/ActiveRecord
end
......@@ -11,4 +11,14 @@ class Packages::Package < ActiveRecord::Base
validates :name,
presence: true,
format: { with: Gitlab::Regex.package_name_regex }
def self.for_projects(projects)
return none unless projects.any?
where(project_id: projects)
end
def self.only_maven_packages_with_path(path)
joins(:maven_metadatum).where(packages_maven_metadata: { path: path })
end
end
......@@ -5,7 +5,7 @@ module Packages
def execute
package = ::Packages::MavenPackageFinder
.new(params[:path], project).execute
.new(params[:path], current_user, project: project).execute
unless package
if params[:file_name] == MAVEN_METADATA_FILE
......
---
title: Add a group-level endpoint for downloading maven packages
merge_request: 8798
author:
type: added
......@@ -76,7 +76,8 @@ module API
authorize!(:read_package, project)
package = ::Packages::MavenPackageFinder.new(params[:path], project).execute!
package = ::Packages::MavenPackageFinder
.new(params[:path], current_user, project: project).execute!
forbidden! unless package.project.feature_available?(:packages)
......@@ -93,6 +94,46 @@ module API
end
end
desc 'Download the maven package file at a group level' do
detail 'This feature was introduced in GitLab 11.7'
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :path, type: String, desc: 'Package path'
requires :file_name, type: String, desc: 'Package file name'
end
route_setting :authentication, job_token_allowed: true
get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
file_name, format = extract_format(params[:file_name])
group = find_group(params[:id])
not_found!('Group') unless can?(current_user, :read_group, group)
package = ::Packages::MavenPackageFinder
.new(params[:path], current_user, group: group).execute!
forbidden! unless package.project.feature_available?(:packages)
authorize!(:read_package, package.project)
package_file = ::Packages::PackageFileFinder
.new(package, file_name).execute!
case format
when 'md5'
package_file.file_md5
when 'sha1'
package_file.file_sha1
when nil
present_carrierwave_file!(package_file.file)
end
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
......@@ -115,7 +156,7 @@ module API
file_name, format = extract_format(params[:file_name])
package = ::Packages::MavenPackageFinder
.new(params[:path], user_project).execute!
.new(params[:path], current_user, project: user_project).execute!
package_file = ::Packages::PackageFileFinder
.new(package, file_name).execute!
......
......@@ -2,19 +2,25 @@
require 'spec_helper'
describe Packages::MavenPackageFinder do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:package) { create(:maven_package, project: project) }
before do
group.add_developer(user)
end
describe '#execute!' do
context 'within the project' do
it 'returns a package' do
finder = described_class.new(package.maven_metadatum.path, project)
finder = described_class.new(package.maven_metadatum.path, user, project: project)
expect(finder.execute!).to eq(package)
end
it 'raises an error' do
finder = described_class.new('com/example/my-app/1.0-SNAPSHOT', project)
finder = described_class.new('com/example/my-app/1.0-SNAPSHOT', user, project: project)
expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end
......@@ -22,13 +28,27 @@ describe Packages::MavenPackageFinder do
context 'across all projects' do
it 'returns a package' do
finder = described_class.new(package.maven_metadatum.path)
finder = described_class.new(package.maven_metadatum.path, user)
expect(finder.execute!).to eq(package)
end
it 'raises an error' do
finder = described_class.new('com/example/my-app/1.0-SNAPSHOT', user)
expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'within a group' do
it 'returns a package' do
finder = described_class.new(package.maven_metadatum.path, user, group: group)
expect(finder.execute!).to eq(package)
end
it 'raises an error' do
finder = described_class.new('com/example/my-app/1.0-SNAPSHOT')
finder = described_class.new('com/example/my-app/1.0-SNAPSHOT', user, group: group)
expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end
......
......@@ -2,8 +2,9 @@
require 'spec_helper'
describe API::MavenPackages do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group) }
let(:user) { create(:user) }
let(:project) { create(:project, :public, namespace: group) }
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
......@@ -125,6 +126,111 @@ describe API::MavenPackages do
end
end
describe 'GET /api/v4/groups/:id/-/packages/maven/*path/:file_name' do
let(:package) { create(:maven_package, project: project) }
let(:maven_metadatum) { package.maven_metadatum }
let(:package_file_xml) { package.package_files.find_by(file_type: 'xml') }
before do
project.team.truncate
group.add_developer(user)
end
context 'a public project' do
it 'returns the file' do
download_file(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'returns sha1 of the file' do
download_file(package_file_xml.file_name + '.sha1')
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('text/plain')
expect(response.body).to eq(package_file_xml.file_sha1)
end
end
context 'internal project' do
before do
group.group_member(user).destroy
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it 'returns the file' do
download_file_with_token(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when no private token' do
download_file(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(404)
end
it 'allows download with job token' do
download_file(package_file_xml.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
context 'private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it 'returns the file' do
download_file_with_token(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
it 'denies download when not enough permissions' do
group.add_guest(user)
download_file_with_token(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(403)
end
it 'denies download when no private token' do
download_file(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(404)
end
it 'allows download with job token' do
download_file(package_file_xml.file_name, job_token: job.token)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type.to_s).to eq('application/octet-stream')
end
end
it 'rejects request if feature is not in the license' do
stub_licensed_features(packages: false)
download_file(package_file_xml.file_name)
expect(response).to have_gitlab_http_status(403)
end
def download_file(file_name, params = {}, request_headers = headers)
get api("/groups/#{group.id}/-/packages/maven/#{maven_metadatum.path}/#{file_name}"), params, request_headers
end
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token)
download_file(file_name, params, request_headers)
end
end
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
let(:package) { create(:maven_package, project: project) }
let(:maven_metadatum) { package.maven_metadatum }
......
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