Commit 8953e5c0 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '10003-api-endpoint-to-list-the-packages-of-a-group' into 'master'

API endpoint to list the packages of a group

See merge request gitlab-org/gitlab!18871
parents 8f145c96 349027ef
...@@ -44,6 +44,7 @@ class GroupPolicy < BasePolicy ...@@ -44,6 +44,7 @@ class GroupPolicy < BasePolicy
rule { public_group }.policy do rule { public_group }.policy do
enable :read_group enable :read_group
enable :read_package
end end
rule { logged_in_viewable }.enable :read_group rule { logged_in_viewable }.enable :read_group
...@@ -70,7 +71,10 @@ class GroupPolicy < BasePolicy ...@@ -70,7 +71,10 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace rule { has_access }.enable :read_namespace
rule { developer }.enable :admin_milestone rule { developer }.policy do
enable :admin_milestone
enable :read_package
end
rule { reporter }.policy do rule { reporter }.policy do
enable :read_container_image enable :read_container_image
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
This is the API docs of [GitLab Packages](../administration/packages/index.md). This is the API docs of [GitLab Packages](../administration/packages/index.md).
## List project packages ## List packages
### Within a project
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/9259) in GitLab 11.8. > [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/9259) in GitLab 11.8.
...@@ -42,6 +44,47 @@ Example response: ...@@ -42,6 +44,47 @@ Example response:
By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination). By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination).
### Within a group
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/18871) in GitLab 12.5.
Get a list of project packages at the group level.
When accessed without authentication, only packages of public projects are returned.
```
GET /groups/:id/packages
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | ID or [URL-encoded path of the group](README.md#namespaced-path-encoding). |
| `exclude_subgroups` | boolean | false | If the param is included as true, packages from projects from subgroups are not listed. Default is `false`. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/group/:id/packages?exclude_subgroups=true
```
Example response:
```json
[
{
"id": 1,
"name": "com/mycompany/my-app",
"version": "1.0-SNAPSHOT",
"package_type": "maven"
},
{
"id": 2,
"name": "@foo/bar",
"version": "1.0.3",
"package_type": "npm"
}
]
```
By default, the `GET` request will return 20 results, since the API is [paginated](README.md#pagination).
## Get a project package ## Get a project package
> [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/9667) in GitLab 11.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/merge_requests/9667) in GitLab 11.9.
......
...@@ -5,7 +5,7 @@ module PackagesAccess ...@@ -5,7 +5,7 @@ module PackagesAccess
included do included do
before_action :verify_packages_enabled! before_action :verify_packages_enabled!
before_action :authorize_read_package! before_action :verify_read_package!
end end
private private
...@@ -14,4 +14,8 @@ module PackagesAccess ...@@ -14,4 +14,8 @@ module PackagesAccess
render_404 unless Gitlab.config.packages.enabled && render_404 unless Gitlab.config.packages.enabled &&
project.feature_available?(:packages) project.feature_available?(:packages)
end end
def verify_read_package!
authorize_read_package!(project)
end
end end
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
module Packages module Packages
class GroupPackagesFinder class GroupPackagesFinder
attr_reader :current_user, :group attr_reader :current_user, :group, :params
def initialize(current_user, group, params = {}) def initialize(current_user, group, params = { exclude_subgroups: false })
@current_user = current_user @current_user = current_user
@group = group @group = group
@params = params @params = params
...@@ -28,12 +28,22 @@ module Packages ...@@ -28,12 +28,22 @@ module Packages
def group_projects_visible_to_current_user def group_projects_visible_to_current_user
::Project ::Project
.in_namespace(group.self_and_descendants.select(:id)) .in_namespace(groups)
.public_or_visible_to_user(current_user, Gitlab::Access::REPORTER) .public_or_visible_to_user(current_user, Gitlab::Access::REPORTER)
end end
def package_type def package_type
@params[:package_type].presence @params[:package_type].presence
end end
def groups
return [group] if exclude_subgroups?
group.self_and_descendants
end
def exclude_subgroups?
params[:exclude_subgroups]
end
end end
end end
---
title: API endpoint to list the packages of a group
merge_request: 18871
author:
type: added
# frozen_string_literal: true
module API
class GroupPackages < Grape::API
include PaginationParams
before do
authorize_packages_access!(user_group)
end
helpers ::API::Helpers::PackagesHelpers
params do
requires :id, type: String, desc: "Group's ID or path"
optional :exclude_subgroups, type: Boolean, default: false, desc: 'Determines if subgroups should be excluded'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get all project packages' do
detail 'This feature was introduced in GitLab 12.5'
success EE::API::Entities::Package
end
params do
use :pagination
end
get ':id/packages' do
packages = Packages::GroupPackagesFinder.new(
current_user,
user_group,
exclude_subgroups: params[:exclude_subgroups]
).execute
present paginate(packages), with: EE::API::Entities::Package
end
end
end
end
...@@ -7,14 +7,19 @@ module API ...@@ -7,14 +7,19 @@ module API
not_found! unless ::Gitlab.config.packages.enabled not_found! unless ::Gitlab.config.packages.enabled
end end
def authorize_packages_feature! def authorize_packages_access!(subject)
forbidden! unless user_project.feature_available?(:packages) require_packages_enabled!
authorize_packages_feature!(subject)
authorize_read_package!(subject)
end end
def authorize_download_package! def authorize_packages_feature!(subject)
authorize!(:read_package, user_project) forbidden! unless subject.feature_available?(:packages)
end
def authorize_read_package!(subject)
authorize!(:read_package, subject)
end end
alias_method :authorize_read_package!, :authorize_download_package!
def authorize_create_package! def authorize_create_package!
authorize!(:create_package, user_project) authorize!(:create_package, user_project)
......
...@@ -64,12 +64,12 @@ module API ...@@ -64,12 +64,12 @@ module API
# the endpoint that includes project id # the endpoint that includes project id
project = find_project_by_path(params[:path]) project = find_project_by_path(params[:path])
authorize!(:read_package, project) authorize_read_package!(project)
package = ::Packages::MavenPackageFinder package = ::Packages::MavenPackageFinder
.new(params[:path], current_user, project: project).execute! .new(params[:path], current_user, project: project).execute!
forbidden! unless package.project.feature_available?(:packages) authorize_packages_feature!(package.project)
package_file = ::Packages::PackageFileFinder package_file = ::Packages::PackageFileFinder
.new(package, file_name).execute! .new(package, file_name).execute!
...@@ -106,9 +106,8 @@ module API ...@@ -106,9 +106,8 @@ module API
package = ::Packages::MavenPackageFinder package = ::Packages::MavenPackageFinder
.new(params[:path], current_user, group: group).execute! .new(params[:path], current_user, group: group).execute!
forbidden! unless package.project.feature_available?(:packages) authorize_packages_feature!(package.project)
authorize_read_package!(package.project)
authorize!(:read_package, package.project)
package_file = ::Packages::PackageFileFinder package_file = ::Packages::PackageFileFinder
.new(package, file_name).execute! .new(package, file_name).execute!
...@@ -129,7 +128,7 @@ module API ...@@ -129,7 +128,7 @@ module API
end end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do before do
authorize_packages_feature! authorize_packages_feature!(user_project)
end end
desc 'Download the maven package file' do desc 'Download the maven package file' do
...@@ -141,7 +140,7 @@ module API ...@@ -141,7 +140,7 @@ module API
end end
route_setting :authentication, job_token_allowed: true route_setting :authentication, job_token_allowed: true
get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
authorize_download_package! authorize_read_package!(user_project)
file_name, format = extract_format(params[:file_name]) file_name, format = extract_format(params[:file_name])
......
...@@ -33,8 +33,8 @@ module API ...@@ -33,8 +33,8 @@ module API
project = find_project_by_package_name(package_name) project = find_project_by_package_name(package_name)
authorize!(:read_package, project) authorize_read_package!(project)
forbidden! unless project.feature_available?(:packages) authorize_packages_feature!(project)
packages = ::Packages::NpmPackagesFinder packages = ::Packages::NpmPackagesFinder
.new(project, package_name).execute .new(project, package_name).execute
...@@ -48,7 +48,7 @@ module API ...@@ -48,7 +48,7 @@ module API
end end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do before do
authorize_packages_feature! authorize_packages_feature!(user_project)
end end
desc 'Download the NPM tarball' do desc 'Download the NPM tarball' do
...@@ -59,7 +59,7 @@ module API ...@@ -59,7 +59,7 @@ module API
requires :file_name, type: String, desc: 'Package file name' requires :file_name, type: String, desc: 'Package file name'
end end
get ':id/packages/npm/*package_name/-/*file_name', format: false do get ':id/packages/npm/*package_name/-/*file_name', format: false do
authorize_download_package! authorize_read_package!(user_project)
package = user_project.packages.npm package = user_project.packages.npm
.by_name_and_file_name(params[:package_name], params[:file_name]) .by_name_and_file_name(params[:package_name], params[:file_name])
......
...@@ -5,9 +5,7 @@ module API ...@@ -5,9 +5,7 @@ module API
include PaginationParams include PaginationParams
before do before do
require_packages_enabled! authorize_packages_access!(user_project)
authorize_packages_feature!
authorize_read_package!
end end
helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::PackagesHelpers
......
# frozen_string_literal: true # frozen_string_literal: true
module API module API
class Packages < Grape::API class ProjectPackages < Grape::API
include PaginationParams include PaginationParams
before do before do
require_packages_enabled! authorize_packages_access!(user_project)
authorize_packages_feature!
authorize_read_package!
end end
helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::PackagesHelpers
......
...@@ -32,7 +32,8 @@ module EE ...@@ -32,7 +32,8 @@ module EE
mount ::API::ConanPackages mount ::API::ConanPackages
mount ::API::MavenPackages mount ::API::MavenPackages
mount ::API::NpmPackages mount ::API::NpmPackages
mount ::API::Packages mount ::API::ProjectPackages
mount ::API::GroupPackages
mount ::API::PackageFiles mount ::API::PackageFiles
mount ::API::Scim mount ::API::Scim
mount ::API::ManagedLicenses mount ::API::ManagedLicenses
......
...@@ -2,19 +2,21 @@ ...@@ -2,19 +2,21 @@
require 'spec_helper' require 'spec_helper'
describe Packages::GroupPackagesFinder do describe Packages::GroupPackagesFinder do
let_it_be(:user) { create(:user) } let(:user) { create(:user) }
let_it_be(:group) { create(:group) } let(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) } let(:project) { create(:project, namespace: group) }
let(:another_group) { create(:group) }
before do before do
group.add_developer(user) group.add_developer(user)
end end
describe '#execute' do describe '#execute' do
subject { described_class.new(user, group).execute } let(:params) { { exclude_subgroups: false } }
subject { described_class.new(user, group, params).execute }
shared_examples 'with package type' do |package_type| shared_examples 'with package type' do |package_type|
subject { described_class.new(user, group, package_type: package_type).execute } let(:params) { { exclude_subgroups: false, package_type: package_type } }
it { is_expected.to match_array([send("package_#{package_type}")]) } it { is_expected.to match_array([send("package_#{package_type}")]) }
end end
...@@ -24,11 +26,25 @@ describe Packages::GroupPackagesFinder do ...@@ -24,11 +26,25 @@ describe Packages::GroupPackagesFinder do
end end
context 'group has packages' do context 'group has packages' do
let(:package1) { create(:maven_package, project: project) } let!(:package1) { create(:maven_package, project: project) }
let(:package2) { create(:maven_package, project: project) } let!(:package2) { create(:maven_package, project: project) }
let_it_be(:package3) { create(:maven_package) } let!(:package3) { create(:maven_package) }
it { is_expected.to match_array([package1, package2]) } it { is_expected.to match_array([package1, package2]) }
context 'subgroup has packages' do
let(:subgroup) { create(:group, parent: group) }
let(:subproject) { create(:project, namespace: subgroup) }
let!(:package4) { create(:npm_package, project: subproject) }
it { is_expected.to match_array([package1, package2, package4]) }
context 'excluding subgroups' do
let(:params) { { exclude_subgroups: true } }
it { is_expected.to match_array([package1, package2]) }
end
end
end end
context 'group has package of all types' do context 'group has package of all types' do
...@@ -50,7 +66,7 @@ describe Packages::GroupPackagesFinder do ...@@ -50,7 +66,7 @@ describe Packages::GroupPackagesFinder do
end end
context 'package type is nil' do context 'package type is nil' do
let_it_be(:package1) { create(:maven_package, project: project) } let!(:package1) { create(:maven_package, project: project) }
subject { described_class.new(user, group, package_type: nil).execute } subject { described_class.new(user, group, package_type: nil).execute }
......
# frozen_string_literal: true
require 'spec_helper'
describe API::GroupPackages do
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
let!(:package1) { create(:npm_package, project: project) }
let!(:package2) { create(:npm_package, project: project) }
let(:user) { create(:user) }
subject { get api(url) }
describe 'GET /groups/:id/packages' do
let(:url) { "/groups/#{group.id}/packages" }
context 'with packages feature enabled' do
before do
stub_licensed_features(packages: true)
end
context 'with private group' do
let(:group) { create(:group, :private) }
let(:subgroup) { create(:group, :private, parent: group) }
let(:project) { create(:project, :private, namespace: group) }
let(:subproject) { create(:project, :private, namespace: subgroup) }
context 'with unauthenticated user' do
it_behaves_like 'rejects packages access', :group, :no_type, :not_found
end
context 'with authenticated user' do
subject { get api(url, user) }
it_behaves_like 'returns packages', :group, :owner
it_behaves_like 'returns packages', :group, :maintainer
it_behaves_like 'returns packages', :group, :developer
it_behaves_like 'rejects packages access', :group, :reporter, :forbidden
it_behaves_like 'rejects packages access', :group, :guest, :forbidden
context 'with subgroup' do
let(:subgroup) { create(:group, :private, parent: group) }
let(:subproject) { create(:project, :private, namespace: subgroup) }
let!(:package3) { create(:npm_package, project: subproject) }
it_behaves_like 'returns packages with subgroups', :group, :owner
it_behaves_like 'returns packages with subgroups', :group, :maintainer
it_behaves_like 'returns packages with subgroups', :group, :developer
it_behaves_like 'rejects packages access', :group, :reporter, :forbidden
it_behaves_like 'rejects packages access', :group, :guest, :forbidden
context 'excluding subgroup' do
let(:url) { "/groups/#{group.id}/packages?exclude_subgroups=true" }
it_behaves_like 'returns packages', :group, :owner
it_behaves_like 'returns packages', :group, :maintainer
it_behaves_like 'returns packages', :group, :developer
it_behaves_like 'rejects packages access', :group, :reporter, :forbidden
it_behaves_like 'rejects packages access', :group, :guest, :forbidden
end
end
end
end
context 'with public group' do
context 'with unauthenticated user' do
it_behaves_like 'returns packages', :group, :no_type
end
context 'with authenticated user' do
subject { get api(url, user) }
it_behaves_like 'returns packages', :group, :owner
it_behaves_like 'returns packages', :group, :maintainer
it_behaves_like 'returns packages', :group, :developer
it_behaves_like 'returns packages', :group, :reporter
it_behaves_like 'returns packages', :group, :guest
end
end
context 'with pagination params' do
let!(:package3) { create(:npm_package, project: project) }
let!(:package4) { create(:npm_package, project: project) }
it_behaves_like 'returns paginated packages'
end
end
context 'with packages feature disabled' do
before do
stub_licensed_features(packages: false)
end
it_behaves_like 'rejects packages access', :group, :no_type, :forbidden
end
end
end
...@@ -2,81 +2,57 @@ ...@@ -2,81 +2,57 @@
require 'spec_helper' require 'spec_helper'
describe API::Packages do describe API::ProjectPackages do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let(:package) { create(:npm_package, project: project) } let!(:package1) { create(:npm_package, project: project) }
let(:package_url) { "/projects/#{project.id}/packages/#{package.id}" } let(:package_url) { "/projects/#{project.id}/packages/#{package1.id}" }
let(:another_package) { create(:npm_package) } let!(:package2) { create(:npm_package, project: project) }
let!(:another_package) { create(:npm_package) }
let(:no_package_url) { "/projects/#{project.id}/packages/0" } let(:no_package_url) { "/projects/#{project.id}/packages/0" }
let(:wrong_package_url) { "/projects/#{project.id}/packages/#{another_package.id}" } let(:wrong_package_url) { "/projects/#{project.id}/packages/#{another_package.id}" }
describe 'GET /projects/:id/packages' do describe 'GET /projects/:id/packages' do
let(:url) { "/projects/#{project.id}/packages" } let(:url) { "/projects/#{project.id}/packages" }
subject { get api(url) }
context 'packages feature enabled' do context 'packages feature enabled' do
before do before do
stub_licensed_features(packages: true) stub_licensed_features(packages: true)
end end
context 'project is public' do context 'project is public' do
it 'returns 200' do it_behaves_like 'returns packages', :project, :no_type
get api(url)
expect(response).to have_gitlab_http_status(200)
end
end end
context 'project is private' do context 'project is private' do
let(:project) { create(:project, :private) } let(:project) { create(:project, :private) }
it 'returns 404 for non authenticated user' do context 'for unauthenticated user' do
get api(url) it_behaves_like 'rejects packages access', :project, :no_type, :not_found
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 for a user without access to the project' do
get api(no_package_url, user)
expect(response).to have_gitlab_http_status(404)
end end
it 'returns 200 and valid response schema' do context 'for authenticated user' do
project.add_maintainer(user) subject { get api(url, user) }
get api(url, user)
expect(response).to have_gitlab_http_status(200) it_behaves_like 'returns packages', :project, :maintainer
expect(response).to match_response_schema('public_api/v4/packages/packages', dir: 'ee') it_behaves_like 'returns packages', :project, :developer
it_behaves_like 'returns packages', :project, :reporter
it_behaves_like 'rejects packages access', :project, :no_type, :not_found
it_behaves_like 'rejects packages access', :project, :guest, :forbidden
end end
end end
context 'with pagination params' do context 'with pagination params' do
let(:per_page) { 2 }
let!(:package1) { create(:npm_package, project: project) }
let!(:package2) { create(:npm_package, project: project) }
let!(:package3) { create(:maven_package, project: project) } let!(:package3) { create(:maven_package, project: project) }
let!(:package4) { create(:maven_package, project: project) }
before do context 'with pagination params' do
project.add_maintainer(user) let!(:package3) { create(:npm_package, project: project) }
stub_licensed_features(packages: true) let!(:package4) { create(:npm_package, project: project) }
end
context 'when viewing the first page' do
it 'returns first 2 packages' do
get api(url, user), params: { page: 1, per_page: per_page }
expect_paginated_array_response([package1.id, package2.id])
end
end
context 'viewing the second page' do
it 'returns the last package' do
get api(url, user), params: { page: 2, per_page: per_page }
expect_paginated_array_response([package3.id]) it_behaves_like 'returns paginated packages'
end
end end
end end
end end
......
# frozen_string_literal: true
shared_examples 'returns packages' do |container_type, user_type|
context "for #{user_type}" do
before do
send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type
end
it 'returns success response' do
subject
expect(response).to have_gitlab_http_status(:success)
end
it 'returns a valid response schema' do
subject
expect(response).to match_response_schema('public_api/v4/packages/packages', dir: 'ee')
end
it 'returns two packages' do
subject
expect(json_response.length).to eq(2)
expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id)
end
end
end
shared_examples 'returns packages with subgroups' do |container_type, user_type|
context "with subgroups for #{user_type}" do
before do
send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type
end
it 'returns success response' do
subject
expect(response).to have_gitlab_http_status(:success)
end
it 'returns a valid response schema' do
subject
expect(response).to match_response_schema('public_api/v4/packages/packages', dir: 'ee')
end
it 'returns three packages' do
subject
expect(json_response.length).to eq(3)
expect(json_response.map { |package| package['id'] }).to contain_exactly(package1.id, package2.id, package3.id)
end
end
end
shared_examples 'rejects packages access' do |container_type, user_type, status|
context "for #{user_type}" do
before do
send(container_type)&.send("add_#{user_type}", user) unless user_type == :no_type
end
it "returns #{status}" do
subject
expect(response).to have_gitlab_http_status(status)
end
end
end
shared_examples 'returns paginated packages' do
let(:per_page) { 2 }
context 'when viewing the first page' do
let(:page) { 1 }
it 'returns first 2 packages' do
get api(url, user), params: { page: page, per_page: per_page }
expect_paginated_array_response([package1.id, package2.id])
end
end
context 'when viewing the second page' do
let(:page) { 2 }
it 'returns first 2 packages' do
get api(url, user), params: { page: page, per_page: per_page }
expect_paginated_array_response([package3.id, package4.id])
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