Commit 349027ef authored by David Fernandez's avatar David Fernandez Committed by Rémy Coutable

Add API method listing packages within a group

This API method is similar to the one listing packages within a project
but for the group level

Rename the existing packages API class to make it more explicit that it
  is for the project level
Add the packages API class for the group level
Add the related access checks in `GroupPolicy`
Refactor packages API specs so that both levels share a common examples
Add the docs for the new group level API method
parent 8f145c96
...@@ -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