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: ...@@ -163,6 +163,34 @@ the `distributionManagement` section:
If you have a self-hosted GitLab installation, replace `gitlab.com` with your If you have a self-hosted GitLab installation, replace `gitlab.com` with your
domain name. 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 ## Uploading packages
Once you have set up the [authorization](#authorizing-with-the-gitlab-maven-repository) Once you have set up the [authorization](#authorizing-with-the-gitlab-maven-repository)
......
# frozen_string_literal: true # frozen_string_literal: true
class Packages::MavenPackageFinder 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 @path = path
@current_user = current_user
@project = project @project = project
@group = group
end end
def execute def execute
packages.last packages_with_path.last
end end
def execute! def execute!
packages.last! packages_with_path.last!
end end
private private
def scope def base
if project if project
project.packages packages_for_a_single_project
elsif group
packages_for_multiple_projects
else else
::Packages::Package.all packages
end end
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 def packages
scope.joins(:maven_metadatum) ::Packages::Package.all
.where(packages_maven_metadata: { path: path }) 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 end
# rubocop: enable CodeReuse/ActiveRecord
end end
...@@ -11,4 +11,14 @@ class Packages::Package < ActiveRecord::Base ...@@ -11,4 +11,14 @@ class Packages::Package < ActiveRecord::Base
validates :name, validates :name,
presence: true, presence: true,
format: { with: Gitlab::Regex.package_name_regex } 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 end
...@@ -5,7 +5,7 @@ module Packages ...@@ -5,7 +5,7 @@ module Packages
def execute def execute
package = ::Packages::MavenPackageFinder package = ::Packages::MavenPackageFinder
.new(params[:path], project).execute .new(params[:path], current_user, project: project).execute
unless package unless package
if params[:file_name] == MAVEN_METADATA_FILE 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 ...@@ -76,7 +76,8 @@ module API
authorize!(:read_package, project) 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) forbidden! unless package.project.feature_available?(:packages)
...@@ -93,6 +94,46 @@ module API ...@@ -93,6 +94,46 @@ module API
end end
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 params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
...@@ -115,7 +156,7 @@ module API ...@@ -115,7 +156,7 @@ module API
file_name, format = extract_format(params[:file_name]) file_name, format = extract_format(params[:file_name])
package = ::Packages::MavenPackageFinder package = ::Packages::MavenPackageFinder
.new(params[:path], user_project).execute! .new(params[:path], current_user, project: user_project).execute!
package_file = ::Packages::PackageFileFinder package_file = ::Packages::PackageFileFinder
.new(package, file_name).execute! .new(package, file_name).execute!
......
...@@ -2,19 +2,25 @@ ...@@ -2,19 +2,25 @@
require 'spec_helper' require 'spec_helper'
describe Packages::MavenPackageFinder do 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) } let(:package) { create(:maven_package, project: project) }
before do
group.add_developer(user)
end
describe '#execute!' do describe '#execute!' do
context 'within the project' do context 'within the project' do
it 'returns a package' 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) expect(finder.execute!).to eq(package)
end end
it 'raises an error' do 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) expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end end
...@@ -22,13 +28,27 @@ describe Packages::MavenPackageFinder do ...@@ -22,13 +28,27 @@ describe Packages::MavenPackageFinder do
context 'across all projects' do context 'across all projects' do
it 'returns a package' 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) expect(finder.execute!).to eq(package)
end end
it 'raises an error' do 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) expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
end end
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
require 'spec_helper' require 'spec_helper'
describe API::MavenPackages do describe API::MavenPackages do
let(:user) { create(:user) } let(:group) { create(:group) }
let(:project) { create(:project, :public) } let(:user) { create(:user) }
let(:project) { create(:project, :public, namespace: group) }
let(:personal_access_token) { create(:personal_access_token, user: user) } let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } 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 } } let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
...@@ -125,6 +126,111 @@ describe API::MavenPackages do ...@@ -125,6 +126,111 @@ describe API::MavenPackages do
end end
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 describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
let(:package) { create(:maven_package, project: project) } let(:package) { create(:maven_package, project: project) }
let(:maven_metadatum) { package.maven_metadatum } 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