Commit e9a97d90 authored by Sean McGivern's avatar Sean McGivern

Merge branch '36423-nuget-group-level-project-api' into 'master'

Add the NuGet group level API

See merge request gitlab-org/gitlab!48356
parents dea3d7c4 27668bbb
# frozen_string_literal: true
module Packages
module FinderHelper
extend ActiveSupport::Concern
private
def packages_visible_to_user(user, within_group:)
return ::Packages::Package.none unless within_group
return ::Packages::Package.none unless Ability.allowed?(user, :read_package, within_group)
projects = projects_visible_to_reporters(user, within_group.self_and_descendants.select(:id))
::Packages::Package.for_projects(projects.select(:id))
end
def projects_visible_to_user(user, within_group:)
return ::Project.none unless within_group
return ::Project.none unless Ability.allowed?(user, :read_package, within_group)
projects_visible_to_reporters(user, within_group.self_and_descendants.select(:id))
end
def projects_visible_to_reporters(user, namespace_ids)
::Project.in_namespace(namespace_ids)
.public_or_visible_to_user(user, ::Gitlab::Access::REPORTER)
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Packages module Packages
module Nuget module Nuget
class PackageFinder class PackageFinder
include ::Packages::FinderHelper
MAX_PACKAGES_COUNT = 50 MAX_PACKAGES_COUNT = 50
def initialize(project, package_name:, package_version: nil, limit: MAX_PACKAGES_COUNT) def initialize(current_user, project_or_group, package_name:, package_version: nil, limit: MAX_PACKAGES_COUNT)
@project = project @current_user = current_user
@project_or_group = project_or_group
@package_name = package_name @package_name = package_name
@package_version = package_version @package_version = package_version
@limit = limit @limit = limit
...@@ -17,15 +21,32 @@ module Packages ...@@ -17,15 +21,32 @@ module Packages
private private
def base
if project?
@project_or_group.packages
elsif group?
packages_visible_to_user(@current_user, within_group: @project_or_group)
else
::Packages::Package.none
end
end
def packages def packages
result = @project.packages result = base.nuget
.nuget .has_version
.has_version .processed
.processed .with_name_like(@package_name)
.with_name_like(@package_name)
result = result.with_version(@package_version) if @package_version.present? result = result.with_version(@package_version) if @package_version.present?
result result
end end
def project?
@project_or_group.is_a?(::Project)
end
def group?
@project_or_group.is_a?(::Group)
end
end end
end end
end end
...@@ -21,8 +21,11 @@ module Packages ...@@ -21,8 +21,11 @@ module Packages
VERSION = '3.0.0'.freeze VERSION = '3.0.0'.freeze
def initialize(project) PROJECT_LEVEL_SERVICES = %i[download publish].freeze
@project = project GROUP_LEVEL_SERVICES = %i[search metadata].freeze
def initialize(project_or_group)
@project_or_group = project_or_group
end end
def version def version
...@@ -30,16 +33,21 @@ module Packages ...@@ -30,16 +33,21 @@ module Packages
end end
def resources def resources
[ available_services.map { |service| build_service(service) }
build_service(:download), .flatten
build_service(:search),
build_service(:publish),
build_service(:metadata)
].flatten
end end
private private
def available_services
case scope
when :group
GROUP_LEVEL_SERVICES
when :project
(GROUP_LEVEL_SERVICES + PROJECT_LEVEL_SERVICES).flatten
end
end
def build_service(service_type) def build_service(service_type)
url = build_service_url(service_type) url = build_service_url(service_type)
comment = SERVICE_COMMENTS[service_type] comment = SERVICE_COMMENTS[service_type]
...@@ -50,36 +58,72 @@ module Packages ...@@ -50,36 +58,72 @@ module Packages
end end
def build_service_url(service_type) def build_service_url(service_type)
base_path = api_v4_projects_packages_nuget_path(id: @project.id)
full_path = case service_type full_path = case service_type
when :download when :download
api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path( download_service_url
{
id: @project.id,
package_name: nil,
package_version: nil,
package_filename: nil
},
true
)
when :search when :search
"#{base_path}/query" search_service_url
when :metadata when :metadata
api_v4_projects_packages_nuget_metadata_package_name_package_version_path( metadata_service_url
{
id: @project.id,
package_name: nil,
package_version: nil
},
true
)
when :publish when :publish
base_path publish_service_url
end end
expose_url(full_path) expose_url(full_path)
end end
def scope
return :project if @project_or_group.is_a?(::Project)
return :group if @project_or_group.is_a?(::Group)
end
def download_service_url
params = {
id: @project_or_group.id,
package_name: nil,
package_version: nil,
package_filename: nil
}
api_v4_projects_packages_nuget_download_package_name_package_version_package_filename_path(
params,
true
)
end
def metadata_service_url
params = {
id: @project_or_group.id,
package_name: nil,
package_version: nil
}
case scope
when :group
api_v4_groups_packages_nuget_metadata_package_name_package_version_path(
params,
true
)
when :project
api_v4_projects_packages_nuget_metadata_package_name_package_version_path(
params,
true
)
end
end
def search_service_url
case scope
when :group
api_v4_groups_packages_nuget_query_path(id: @project_or_group.id)
when :project
api_v4_projects_packages_nuget_query_path(id: @project_or_group.id)
end
end
def publish_service_url
api_v4_projects_packages_nuget_path(id: @project_or_group.id)
end
end end
end end
end end
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Packages module Packages
module Nuget module Nuget
class SearchService < BaseService class SearchService < BaseService
include ::Packages::FinderHelper
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include ActiveRecord::ConnectionAdapters::Quoting include ActiveRecord::ConnectionAdapters::Quoting
...@@ -16,8 +17,11 @@ module Packages ...@@ -16,8 +17,11 @@ module Packages
padding: 0 padding: 0
}.freeze }.freeze
def initialize(project, search_term, options = {}) RESULT = Struct.new(:results, :total_count, keyword_init: true).freeze
@project = project
def initialize(current_user, project_or_group, search_term, options = {})
@current_user = current_user
@project_or_group = project_or_group
@search_term = search_term @search_term = search_term
@options = DEFAULT_OPTIONS.merge(options) @options = DEFAULT_OPTIONS.merge(options)
...@@ -26,8 +30,8 @@ module Packages ...@@ -26,8 +30,8 @@ module Packages
end end
def execute def execute
OpenStruct.new( RESULT.new(
total_count: package_names.total_count, total_count: non_paginated_matching_package_names.count,
results: search_packages results: search_packages
) )
end end
...@@ -39,52 +43,104 @@ module Packages ...@@ -39,52 +43,104 @@ module Packages
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24182#technical-notes # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24182#technical-notes
# and https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource # and https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
subquery_name = :partition_subquery subquery_name = :partition_subquery
arel_table = Arel::Table.new(:partition_subquery) arel_table = Arel::Table.new(subquery_name)
column_names = Packages::Package.column_names.map do |cn| column_names = Packages::Package.column_names.map do |cn|
"#{subquery_name}.#{quote_column_name(cn)}" "#{subquery_name}.#{quote_column_name(cn)}"
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
pkgs = Packages::Package.select(column_names.join(',')) pkgs = Packages::Package
.from(package_names_partition, subquery_name) pkgs = pkgs.with(project_ids_cte.to_arel) if use_project_ids_cte?
.where(arel_table[:row_number].lteq(MAX_VERSIONS_PER_PACKAGE)) pkgs = pkgs.select(column_names.join(','))
.from(package_names_partition, subquery_name)
.where(arel_table[:row_number].lteq(MAX_VERSIONS_PER_PACKAGE))
return pkgs if include_prerelease_versions? return pkgs if include_prerelease_versions?
# we can't use pkgs.without_version_like since we have a custom from # we can't use pkgs.without_version_like since we have a custom from
pkgs.where.not(arel_table[:version].matches(PRE_RELEASE_VERSION_MATCHING_TERM)) pkgs.where.not(arel_table[:version].matches(PRE_RELEASE_VERSION_MATCHING_TERM))
# rubocop: enable CodeReuse/ActiveRecord
end end
def package_names_partition def package_names_partition
# rubocop: disable CodeReuse/ActiveRecord
table_name = quote_table_name(Packages::Package.table_name) table_name = quote_table_name(Packages::Package.table_name)
name_column = "#{table_name}.#{quote_column_name('name')}" name_column = "#{table_name}.#{quote_column_name('name')}"
created_at_column = "#{table_name}.#{quote_column_name('created_at')}" created_at_column = "#{table_name}.#{quote_column_name('created_at')}"
select_sql = "ROW_NUMBER() OVER (PARTITION BY #{name_column} ORDER BY #{created_at_column} DESC) AS row_number, #{table_name}.*" select_sql = "ROW_NUMBER() OVER (PARTITION BY #{name_column} ORDER BY #{created_at_column} DESC) AS row_number, #{table_name}.*"
@project.packages nuget_packages.select(select_sql)
.select(select_sql) .with_name(paginated_matching_package_names)
.nuget .where(project_id: project_ids)
.has_version # rubocop: enable CodeReuse/ActiveRecord
.without_nuget_temporary_name
.with_name(package_names)
end end
def package_names def paginated_matching_package_names
strong_memoize(:package_names) do pkgs = base_matching_package_names
pkgs = @project.packages pkgs.page(0) # we're using a padding
.nuget .per(per_page)
.has_version .padding(padding)
.without_nuget_temporary_name end
.order_name
.select_distinct_name def non_paginated_matching_package_names
# rubocop: disable CodeReuse/ActiveRecord
pkgs = base_matching_package_names
pkgs = pkgs.with(project_ids_cte.to_arel) if use_project_ids_cte?
pkgs
# rubocop: enable CodeReuse/ActiveRecord
end
def base_matching_package_names
strong_memoize(:base_matching_package_names) do
# rubocop: disable CodeReuse/ActiveRecord
pkgs = nuget_packages.order_name
.select_distinct_name
.where(project_id: project_ids)
pkgs = pkgs.without_version_like(PRE_RELEASE_VERSION_MATCHING_TERM) unless include_prerelease_versions? pkgs = pkgs.without_version_like(PRE_RELEASE_VERSION_MATCHING_TERM) unless include_prerelease_versions?
pkgs = pkgs.search_by_name(@search_term) if @search_term.present? pkgs = pkgs.search_by_name(@search_term) if @search_term.present?
pkgs.page(0) # we're using a padding pkgs
.per(per_page) # rubocop: enable CodeReuse/ActiveRecord
.padding(padding)
end end
end end
def nuget_packages
Packages::Package.nuget
.has_version
.without_nuget_temporary_name
end
def project_ids_cte
return unless use_project_ids_cte?
strong_memoize(:project_ids_cte) do
query = projects_visible_to_user(@current_user, within_group: @project_or_group)
Gitlab::SQL::CTE.new(:project_ids, query.select(:id))
end
end
def project_ids
return @project_or_group.id if project?
if use_project_ids_cte?
# rubocop: disable CodeReuse/ActiveRecord
Project.select(:id)
.from(project_ids_cte.table)
# rubocop: enable CodeReuse/ActiveRecord
end
end
def use_project_ids_cte?
group?
end
def project?
@project_or_group.is_a?(::Project)
end
def group?
@project_or_group.is_a?(::Group)
end
def include_prerelease_versions? def include_prerelease_versions?
@options[:include_prerelease_versions] @options[:include_prerelease_versions]
end end
......
---
title: Add the NuGet group level API
merge_request: 48356
author:
type: added
...@@ -60,6 +60,21 @@ NuGet CLI. ...@@ -60,6 +60,21 @@ NuGet CLI.
mono nuget.exe mono nuget.exe
``` ```
## Use the GitLab endpoint for NuGet Packages
To use the GitLab endpoint for NuGet Packages, choose an option:
- **Project-level**: Use when you have few NuGet packages and they are not in
the same GitLab group.
- **Group-level**: Use when you have many NuGet packages in different within the
same GitLab group.
Some features such as [publishing](#publish-a-nuget-package) a package are only available on the project-level endpoint.
WARNING:
Because of how NuGet handles credentials, the Package Registry rejects anonymous requests on the group-level endpoint.
To work around this limitation, set up [authentication](#add-the-package-registry-as-a-source-for-nuget-packages).
## Add the Package Registry as a source for NuGet packages ## Add the Package Registry as a source for NuGet packages
To publish and install packages to the Package Registry, you must add the To publish and install packages to the Package Registry, you must add the
...@@ -75,7 +90,9 @@ Prerequisites: ...@@ -75,7 +90,9 @@ Prerequisites:
with the scope set to `read_package_registry`, `write_package_registry`, or with the scope set to `read_package_registry`, `write_package_registry`, or
both. both.
- A name for your source. - A name for your source.
- Your project ID, which is found on your project's home page. - Depending on the [endpoint level](#use-the-gitlab-endpoint-for-nuget-packages) you use, either:
- Your project ID, which is found on your project's home page.
- Your group ID, which is found on your group's home page.
You can now add a new source to NuGet with: You can now add a new source to NuGet with:
...@@ -85,7 +102,9 @@ You can now add a new source to NuGet with: ...@@ -85,7 +102,9 @@ You can now add a new source to NuGet with:
### Add a source with the NuGet CLI ### Add a source with the NuGet CLI
To add the Package Registry as a source with `nuget`: #### Project-level endpoint
To use the [project-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endpoint, add the Package Registry as a source with `nuget`:
```shell ```shell
nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token> nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
...@@ -99,9 +118,27 @@ For example: ...@@ -99,9 +118,27 @@ For example:
nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/projects/10/packages/nuget/index.json" -UserName carol -Password 12345678asdf nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/projects/10/packages/nuget/index.json" -UserName carol -Password 12345678asdf
``` ```
#### Group-level endpoint
To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endpoint, add the Package Registry as a source with `nuget`:
```shell
nuget source Add -Name <source_name> -Source "https://gitlab.example.com/api/v4/groups/<your_group_id>/packages/nuget/index.json" -UserName <gitlab_username or deploy_token_username> -Password <gitlab_personal_access_token or deploy_token>
```
- `<source_name>` is the desired source name.
For example:
```shell
nuget source Add -Name "GitLab" -Source "https://gitlab.example.com/api/v4/groups/23/packages/nuget/index.json" -UserName carol -Password 12345678asdf
```
### Add a source with Visual Studio ### Add a source with Visual Studio
To add the Package Registry as a source with Visual Studio: #### Project-level endpoint
To use the [project-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endpoint, add the Package Registry as a source with Visual Studio:
1. Open [Visual Studio](https://visualstudio.microsoft.com/vs/). 1. Open [Visual Studio](https://visualstudio.microsoft.com/vs/).
1. In Windows, select **File > Options**. On macOS, select **Visual Studio > Preferences**. 1. In Windows, select **File > Options**. On macOS, select **Visual Studio > Preferences**.
...@@ -126,9 +163,38 @@ The source is displayed in your list. ...@@ -126,9 +163,38 @@ The source is displayed in your list.
If you get a warning, ensure that the **Location**, **Username**, and If you get a warning, ensure that the **Location**, **Username**, and
**Password** are correct. **Password** are correct.
#### Group-level endpoint
To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) NuGet endpoint, add the Package Registry as a source with Visual Studio:
1. Open [Visual Studio](https://visualstudio.microsoft.com/vs/).
1. In Windows, select **File > Options**. On macOS, select **Visual Studio > Preferences**.
1. In the **NuGet** section, select **Sources** to view a list of all your NuGet sources.
1. Select **Add**.
1. Complete the following fields:
- **Name**: Name for the source.
- **Location**: `https://gitlab.example.com/api/v4/group/<your_group_id>/packages/nuget/index.json`,
where `<your_group_id>` is your group ID, and `gitlab.example.com` is
your domain name.
- **Username**: Your GitLab username or deploy token username.
- **Password**: Your personal access token or deploy token.
![Visual Studio Adding a NuGet source](img/visual_studio_adding_nuget_source.png)
1. Click **Save**.
The source is displayed in your list.
![Visual Studio NuGet source added](img/visual_studio_nuget_source_added.png)
If you get a warning, ensure that the **Location**, **Username**, and
**Password** are correct.
### Add a source with the .NET CLI ### Add a source with the .NET CLI
To add the Package Registry as a source for .NET: #### Project-level endpoint
To use the [project-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Registry as a source for .NET:
1. In the root of your project, create a file named `nuget.config`. 1. In the root of your project, create a file named `nuget.config`.
1. Add this content: 1. Add this content:
...@@ -138,7 +204,30 @@ To add the Package Registry as a source for .NET: ...@@ -138,7 +204,30 @@ To add the Package Registry as a source for .NET:
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<add key="gitlab" value="https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/nuget/index.json" /> <add key="gitlab" value="https://gitlab.example.com/api/v4/project/<your_project_id>/packages/nuget/index.json" />
</packageSources>
<packageSourceCredentials>
<gitlab>
<add key="Username" value="<gitlab_username or deploy_token_username>" />
<add key="ClearTextPassword" value="<gitlab_personal_access_token or deploy_token>" />
</gitlab>
</packageSourceCredentials>
</configuration>
```
#### Group-level endpoint
To use the [group-level](#use-the-gitlab-endpoint-for-nuget-packages) Package Registry as a source for .NET:
1. In the root of your project, create a file named `nuget.config`.
1. Add this content:
```xml
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="gitlab" value="https://gitlab.example.com/api/v4/group/<your_group_id>/packages/nuget/index.json" />
</packageSources> </packageSources>
<packageSourceCredentials> <packageSourceCredentials>
<gitlab> <gitlab>
...@@ -151,6 +240,10 @@ To add the Package Registry as a source for .NET: ...@@ -151,6 +240,10 @@ To add the Package Registry as a source for .NET:
## Publish a NuGet package ## Publish a NuGet package
Prerequisite:
- Set up the [source](#https://docs.gitlab.com/ee/user/packages/nuget_repository/#add-the-package-registry-as-a-source-for-nuget-packages) with a [project-level endpoint](#use-the-gitlab-endpoint-for-nuget-packages).
When publishing packages: When publishing packages:
- The Package Registry on GitLab.com can store up to 500 MB of content. - The Package Registry on GitLab.com can store up to 500 MB of content.
...@@ -164,9 +257,10 @@ When publishing packages: ...@@ -164,9 +257,10 @@ When publishing packages:
### Publish a package with the NuGet CLI ### Publish a package with the NuGet CLI
Prerequisite: Prerequisites:
- [A NuGet package created with NuGet CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package). - [A NuGet package created with NuGet CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package).
- Set a [project-level endpoint](#use-the-gitlab-endpoint-for-nuget-packages).
Publish a package by running this command: Publish a package by running this command:
...@@ -179,9 +273,10 @@ nuget push <package_file> -Source <source_name> ...@@ -179,9 +273,10 @@ nuget push <package_file> -Source <source_name>
### Publish a package with the .NET CLI ### Publish a package with the .NET CLI
Prerequisite: Prerequisites:
- [A NuGet package created with .NET CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package-dotnet-cli). - [A NuGet package created with .NET CLI](https://docs.microsoft.com/en-us/nuget/create-packages/creating-a-package-dotnet-cli).
- Set a [project-level endpoint](#use-the-gitlab-endpoint-for-nuget-packages).
Publish a package by running this command: Publish a package by running this command:
......
...@@ -212,6 +212,7 @@ module API ...@@ -212,6 +212,7 @@ module API
mount ::API::GroupPackages mount ::API::GroupPackages
mount ::API::PackageFiles mount ::API::PackageFiles
mount ::API::NugetProjectPackages mount ::API::NugetProjectPackages
mount ::API::NugetGroupPackages
mount ::API::PypiPackages mount ::API::PypiPackages
mount ::API::ComposerPackages mount ::API::ComposerPackages
mount ::API::ConanProjectPackages mount ::API::ConanProjectPackages
......
...@@ -19,29 +19,37 @@ module API ...@@ -19,29 +19,37 @@ module API
included do included do
helpers do helpers do
def find_packages def find_packages(package_name)
packages = package_finder.execute packages = package_finder(package_name).execute
not_found!('Packages') unless packages.exists? not_found!('Packages') unless packages.exists?
packages packages
end end
def find_package def find_package(package_name, package_version)
package = package_finder(package_version: params[:package_version]).execute package = package_finder(package_name, package_version).execute
.first .first
not_found!('Package') unless package not_found!('Package') unless package
package package
end end
def package_finder(finder_params = {}) def package_finder(package_name, package_version = nil)
::Packages::Nuget::PackageFinder.new( ::Packages::Nuget::PackageFinder.new(
authorized_user_project, current_user,
**finder_params.merge(package_name: params[:package_name]) project_or_group,
package_name: package_name,
package_version: package_version
) )
end end
def search_packages(search_term, search_options)
::Packages::Nuget::SearchService
.new(current_user, project_or_group, params[:q], search_options)
.execute
end
end end
# https://docs.microsoft.com/en-us/nuget/api/service-index # https://docs.microsoft.com/en-us/nuget/api/service-index
...@@ -52,11 +60,11 @@ module API ...@@ -52,11 +60,11 @@ module API
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get 'index', format: :json do get 'index', format: :json do
authorize_read_package!(authorized_user_project) authorize_read_package!(project_or_group)
track_package_event('cli_metadata', :nuget, category: 'API::NugetPackages') track_package_event('cli_metadata', :nuget, category: 'API::NugetPackages')
present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project), present ::Packages::Nuget::ServiceIndexPresenter.new(project_or_group),
with: ::API::Entities::Nuget::ServiceIndex with: ::API::Entities::Nuget::ServiceIndex
end end
# https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
...@@ -64,8 +72,8 @@ module API ...@@ -64,8 +72,8 @@ module API
requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX
end end
namespace '/metadata/*package_name' do namespace '/metadata/*package_name' do
before do after_validation do
authorize_read_package!(authorized_user_project) authorize_read_package!(project_or_group)
end end
desc 'The NuGet Metadata Service - Package name level' do desc 'The NuGet Metadata Service - Package name level' do
...@@ -75,7 +83,7 @@ module API ...@@ -75,7 +83,7 @@ module API
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get 'index', format: :json do get 'index', format: :json do
present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages), present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages(params[:package_name])),
with: ::API::Entities::Nuget::PackagesMetadata with: ::API::Entities::Nuget::PackagesMetadata
end end
...@@ -89,7 +97,7 @@ module API ...@@ -89,7 +97,7 @@ module API
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get '*package_version', format: :json do get '*package_version', format: :json do
present ::Packages::Nuget::PackageMetadataPresenter.new(find_package), present ::Packages::Nuget::PackageMetadataPresenter.new(find_package(params[:package_name], params[:package_version])),
with: ::API::Entities::Nuget::PackageMetadata with: ::API::Entities::Nuget::PackageMetadata
end end
end end
...@@ -102,8 +110,8 @@ module API ...@@ -102,8 +110,8 @@ module API
optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true
end end
namespace '/query' do namespace '/query' do
before do after_validation do
authorize_read_package!(authorized_user_project) authorize_read_package!(project_or_group)
end end
desc 'The NuGet Search Service' do desc 'The NuGet Search Service' do
...@@ -118,14 +126,13 @@ module API ...@@ -118,14 +126,13 @@ module API
per_page: params[:take], per_page: params[:take],
padding: params[:skip] padding: params[:skip]
} }
search = ::Packages::Nuget::SearchService
.new(authorized_user_project, params[:q], search_options) results = search_packages(params[:q], search_options)
.execute
track_package_event('search_package', :nuget, category: 'API::NugetPackages') track_package_event('search_package', :nuget, category: 'API::NugetPackages')
present ::Packages::Nuget::SearchResultsPresenter.new(search), present ::Packages::Nuget::SearchResultsPresenter.new(results),
with: ::API::Entities::Nuget::SearchResults with: ::API::Entities::Nuget::SearchResults
end end
end end
end end
......
...@@ -12,6 +12,7 @@ module API ...@@ -12,6 +12,7 @@ module API
end end
include Constants include Constants
include Gitlab::Utils::StrongMemoize
def unauthorized_user_project def unauthorized_user_project
@unauthorized_user_project ||= find_project(params[:id]) @unauthorized_user_project ||= find_project(params[:id])
...@@ -35,6 +36,18 @@ module API ...@@ -35,6 +36,18 @@ module API
project project
end end
def find_authorized_group!
strong_memoize(:authorized_group) do
group = find_group(params[:id])
unless group && can?(current_user, :read_group, group)
next unauthorized_or! { not_found! }
end
group
end
end
def authorize!(action, subject = :global, reason = nil) def authorize!(action, subject = :global, reason = nil)
return if can?(current_user, action, subject) return if can?(current_user, action, subject)
......
# frozen_string_literal: true
# NuGet Package Manager Client API
#
# These API endpoints are not meant to be consumed directly by users. They are
# called by the NuGet package manager client when users run commands
# like `nuget install` or `nuget push`.
#
# This is the group level API.
module API
class NugetGroupPackages < ::API::Base
helpers ::API::Helpers::PackagesManagerClientsHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
feature_category :package_registry
default_format :json
rescue_from ArgumentError do |e|
render_api_error!(e.message, 400)
end
after_validation do
require_packages_enabled!
end
helpers do
def project_or_group
find_authorized_group!
end
def require_authenticated!
unauthorized! unless current_user
end
end
params do
requires :id, type: String, desc: 'The ID of a group', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
end
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
namespace ':id/packages/nuget' do
after_validation do
# This API can't be accessed anonymously
require_authenticated!
end
include ::API::Concerns::Packages::NugetEndpoints
end
end
end
end
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
# These API endpoints are not meant to be consumed directly by users. They are # These API endpoints are not meant to be consumed directly by users. They are
# called by the NuGet package manager client when users run commands # called by the NuGet package manager client when users run commands
# like `nuget install` or `nuget push`. # like `nuget install` or `nuget push`.
#
# This is the project level API.
module API module API
class NugetProjectPackages < ::API::Base class NugetProjectPackages < ::API::Base
helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::PackagesManagerClientsHelpers
...@@ -20,10 +22,16 @@ module API ...@@ -20,10 +22,16 @@ module API
render_api_error!(e.message, 400) render_api_error!(e.message, 400)
end end
before do after_validation do
require_packages_enabled! require_packages_enabled!
end end
helpers do
def project_or_group
authorized_user_project
end
end
params do params do
requires :id, type: String, desc: 'The ID of a project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX requires :id, type: String, desc: 'The ID of a project', regexp: ::API::Concerns::Packages::NugetEndpoints::POSITIVE_INTEGER_REGEX
end end
...@@ -31,10 +39,6 @@ module API ...@@ -31,10 +39,6 @@ module API
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
before do
authorized_user_project
end
namespace ':id/packages/nuget' do namespace ':id/packages/nuget' do
include ::API::Concerns::Packages::NugetEndpoints include ::API::Concerns::Packages::NugetEndpoints
...@@ -50,24 +54,19 @@ module API ...@@ -50,24 +54,19 @@ module API
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
put do put do
authorize_upload!(authorized_user_project) authorize_upload!(project_or_group)
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size)
file_params = params.merge( file_params = params.merge(
file: params[:package], file: params[:package],
file_name: PACKAGE_FILENAME file_name: PACKAGE_FILENAME
) )
package = ::Packages::Nuget::CreatePackageService.new( package = ::Packages::Nuget::CreatePackageService.new(project_or_group, current_user, declared_params.merge(build: current_authenticated_job))
authorized_user_project, .execute
current_user,
declared_params.merge(build: current_authenticated_job)
).execute
package_file = ::Packages::CreatePackageFileService.new( package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job))
package, .execute
file_params.merge(build: current_authenticated_job)
).execute
track_package_event('push_package', :nuget, category: 'API::NugetPackages') track_package_event('push_package', :nuget, category: 'API::NugetPackages')
...@@ -75,7 +74,7 @@ module API ...@@ -75,7 +74,7 @@ module API
created! created!
rescue ObjectStorage::RemoteStoreError => e rescue ObjectStorage::RemoteStoreError => e
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id }) Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id })
forbidden! forbidden!
end end
...@@ -84,9 +83,9 @@ module API ...@@ -84,9 +83,9 @@ module API
put 'authorize' do put 'authorize' do
authorize_workhorse!( authorize_workhorse!(
subject: authorized_user_project, subject: project_or_group,
has_length: false, has_length: false,
maximum_size: authorized_user_project.actual_limits.nuget_max_file_size maximum_size: project_or_group.actual_limits.nuget_max_file_size
) )
end end
...@@ -95,8 +94,8 @@ module API ...@@ -95,8 +94,8 @@ module API
requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX
end end
namespace '/download/*package_name' do namespace '/download/*package_name' do
before do after_validation do
authorize_read_package!(authorized_user_project) authorize_read_package!(project_or_group)
end end
desc 'The NuGet Content Service - index request' do desc 'The NuGet Content Service - index request' do
...@@ -106,7 +105,7 @@ module API ...@@ -106,7 +105,7 @@ module API
route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true route_setting :authentication, deploy_token_allowed: true, job_token_allowed: :basic_auth, basic_auth_personal_access_token: true
get 'index', format: :json do get 'index', format: :json do
present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages), present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages(params[:package_name])),
with: ::API::Entities::Nuget::PackagesVersions with: ::API::Entities::Nuget::PackagesVersions
end end
...@@ -122,7 +121,7 @@ module API ...@@ -122,7 +121,7 @@ module API
get '*package_version/*package_filename', format: :nupkg do get '*package_version/*package_filename', format: :nupkg do
filename = "#{params[:package_filename]}.#{params[:format]}" filename = "#{params[:package_filename]}.#{params[:format]}"
package_file = ::Packages::PackageFileFinder.new(find_package, filename, with_file_name_like: true) package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true)
.execute .execute
not_found!('Package') unless package_file not_found!('Package') unless package_file
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Packages::FinderHelper do
describe '#packages_visible_to_user' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:project1) { create(:project, namespace: group) }
let_it_be(:package1) { create(:package, project: project1) }
let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) }
let_it_be(:package2) { create(:package, project: project2) }
let(:finder_class) do
Class.new do
include ::Packages::FinderHelper
def initialize(user)
@current_user = user
end
def execute(group)
packages_visible_to_user(@current_user, within_group: group)
end
end
end
let(:finder) { finder_class.new(user) }
subject { finder.execute(group) }
shared_examples 'returning both packages' do
it { is_expected.to contain_exactly(package1, package2) }
end
shared_examples 'returning package1' do
it { is_expected.to eq [package1]}
end
shared_examples 'returning no packages' do
it { is_expected.to be_empty }
end
where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :maintainer | 'returning both packages'
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :developer | 'returning both packages'
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :guest | 'returning both packages'
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :anonymous | 'returning both packages'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :maintainer | 'returning both packages'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :developer | 'returning both packages'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :guest | 'returning package1'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :anonymous | 'returning package1'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both packages'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning package1'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning package1'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both packages'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning no packages'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning no packages'
end
with_them do
before do
unless user_role == :anonymous
group.send("add_#{user_role}", user)
subgroup.send("add_#{user_role}", user)
project1.send("add_#{user_role}", user)
project2.send("add_#{user_role}", user)
end
project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
end
it_behaves_like params[:shared_example_name]
end
end
describe '#projects_visible_to_user' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:project1) { create(:project, namespace: group) }
let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) }
let(:finder_class) do
Class.new do
include ::Packages::FinderHelper
def initialize(user)
@current_user = user
end
def execute(group)
projects_visible_to_user(@current_user, within_group: group)
end
end
end
let(:finder) { finder_class.new(user) }
subject { finder.execute(group) }
shared_examples 'returning both projects' do
it { is_expected.to contain_exactly(project1, project2) }
end
shared_examples 'returning project1' do
it { is_expected.to eq [project1]}
end
shared_examples 'returning no project' do
it { is_expected.to be_empty }
end
where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :maintainer | 'returning both projects'
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :developer | 'returning both projects'
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :guest | 'returning both projects'
'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :anonymous | 'returning both projects'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :maintainer | 'returning both projects'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :developer | 'returning both projects'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :guest | 'returning project1'
'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :anonymous | 'returning project1'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both projects'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both projects'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning project1'
'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning project1'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both projects'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both projects'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning no project'
'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning no project'
end
with_them do
before do
unless user_role == :anonymous
group.send("add_#{user_role}", user)
subgroup.send("add_#{user_role}", user)
project1.send("add_#{user_role}", user)
project2.send("add_#{user_role}", user)
end
project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false))
subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
end
it_behaves_like params[:shared_example_name]
end
end
end
...@@ -2,74 +2,117 @@ ...@@ -2,74 +2,117 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Packages::Nuget::PackageFinder do RSpec.describe Packages::Nuget::PackageFinder do
let_it_be(:package1) { create(:nuget_package) } let_it_be(:user) { create(:user) }
let_it_be(:project) { package1.project } let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, namespace: subgroup) }
let_it_be(:package1) { create(:nuget_package, project: project) }
let_it_be(:package2) { create(:nuget_package, name: package1.name, version: '2.0.0', project: project) } let_it_be(:package2) { create(:nuget_package, name: package1.name, version: '2.0.0', project: project) }
let_it_be(:package3) { create(:nuget_package, name: 'Another.Dummy.Package', project: project) } let_it_be(:package3) { create(:nuget_package, name: 'Another.Dummy.Package', project: project) }
let_it_be(:other_package_1) { create(:nuget_package, name: package1.name, version: package1.version) }
let_it_be(:other_package_2) { create(:nuget_package, name: package1.name, version: package2.version) }
let(:package_name) { package1.name } let(:package_name) { package1.name }
let(:package_version) { nil } let(:package_version) { nil }
let(:limit) { 50 } let(:limit) { 50 }
describe '#execute!' do describe '#execute!' do
subject { described_class.new(project, package_name: package_name, package_version: package_version, limit: limit).execute } subject { described_class.new(user, target, package_name: package_name, package_version: package_version, limit: limit).execute }
it { is_expected.to match_array([package1, package2]) } shared_examples 'handling all the conditions' do
it { is_expected.to match_array([package1, package2]) }
context 'with lower case package name' do context 'with lower case package name' do
let(:package_name) { package1.name.downcase } let(:package_name) { package1.name.downcase }
it { is_expected.to match_array([package1, package2]) } it { is_expected.to match_array([package1, package2]) }
end end
context 'with unknown package name' do context 'with unknown package name' do
let(:package_name) { 'foobar' } let(:package_name) { 'foobar' }
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
context 'with valid version' do context 'with valid version' do
let(:package_version) { '2.0.0' } let(:package_version) { '2.0.0' }
it { is_expected.to match_array([package2]) } it { is_expected.to match_array([package2]) }
end end
context 'with unknown version' do context 'with unknown version' do
let(:package_version) { 'foobar' } let(:package_version) { 'foobar' }
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
context 'with limit hit' do
let_it_be(:package4) { create(:nuget_package, name: package1.name, project: project) }
let_it_be(:package5) { create(:nuget_package, name: package1.name, project: project) }
let_it_be(:package6) { create(:nuget_package, name: package1.name, project: project) }
let(:limit) { 2 }
it { is_expected.to match_array([package5, package6]) }
end
context 'with downcase package name' do
let(:package_name) { package1.name.downcase }
it { is_expected.to match_array([package1, package2]) }
end
context 'with limit hit' do context 'with prefix wildcard' do
let_it_be(:package4) { create(:nuget_package, name: package1.name, project: project) } let(:package_name) { "%#{package1.name[3..-1]}" }
let_it_be(:package5) { create(:nuget_package, name: package1.name, project: project) }
let_it_be(:package6) { create(:nuget_package, name: package1.name, project: project) }
let(:limit) { 2 }
it { is_expected.to match_array([package5, package6]) } it { is_expected.to match_array([package1, package2]) }
end
context 'with suffix wildcard' do
let(:package_name) { "#{package1.name[0..-3]}%" }
it { is_expected.to match_array([package1, package2]) }
end
context 'with surrounding wildcards' do
let(:package_name) { "%#{package1.name[3..-3]}%" }
it { is_expected.to match_array([package1, package2]) }
end
end end
context 'with downcase package name' do context 'with a project' do
let(:package_name) { package1.name.downcase } let(:target) { project }
it { is_expected.to match_array([package1, package2]) } before do
project.add_developer(user)
end
it_behaves_like 'handling all the conditions'
end end
context 'with prefix wildcard' do context 'with a subgroup' do
let(:package_name) { "%#{package1.name[3..-1]}" } let(:target) { subgroup }
it { is_expected.to match_array([package1, package2]) } before do
subgroup.add_developer(user)
end
it_behaves_like 'handling all the conditions'
end end
context 'with suffix wildcard' do context 'with a group' do
let(:package_name) { "#{package1.name[0..-3]}%" } let(:target) { group }
it { is_expected.to match_array([package1, package2]) } before do
group.add_developer(user)
end
it_behaves_like 'handling all the conditions'
end end
context 'with surrounding wildcards' do context 'with nil' do
let(:package_name) { "%#{package1.name[3..-3]}%" } let(:target) { nil }
it { is_expected.to match_array([package1, package2]) } it { is_expected.to be_empty }
end end
end end
end end
...@@ -4,25 +4,64 @@ require 'spec_helper' ...@@ -4,25 +4,64 @@ require 'spec_helper'
RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do RSpec.describe ::Packages::Nuget::ServiceIndexPresenter do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:presenter) { described_class.new(project) } let_it_be(:group) { create(:group) }
let(:presenter) { described_class.new(target) }
describe '#version' do describe '#version' do
subject { presenter.version } subject { presenter.version }
it { is_expected.to eq '3.0.0' } context 'for a group' do
let(:target) { group }
it { is_expected.to eq '3.0.0' }
end
context 'for a project' do
let(:target) { project }
it { is_expected.to eq '3.0.0' }
end
end end
describe '#resources' do describe '#resources' do
subject { presenter.resources } subject { presenter.resources }
it 'has valid resources' do shared_examples 'returning valid resources' do |resources_count: 8, include_publish_service: true|
expect(subject.size).to eq 8 it 'has valid resources' do
subject.each do |resource| expect(subject.size).to eq resources_count
%i[@id @type comment].each do |field| subject.each do |resource|
expect(resource).to have_key(field) %i[@id @type comment].each do |field|
expect(resource[field]).to be_a(String) expect(resource).to have_key(field)
expect(resource[field]).to be_a(String)
end
end
end
it "does #{'not ' unless include_publish_service}return the publish resource" do
services_types = subject.map { |res| res[:@type] }
described_class::SERVICE_VERSIONS[:publish].each do |publish_service_version|
if include_publish_service
expect(services_types).to include(publish_service_version)
else
expect(services_types).not_to include(publish_service_version)
end
end end
end end
end end
context 'for a group' do
let(:target) { group }
# at the group level we don't have the publish and download service
it_behaves_like 'returning valid resources', resources_count: 6, include_publish_service: false
end
context 'for a project' do
let(:target) { project }
it_behaves_like 'returning valid resources'
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::NugetGroupPackages do
include_context 'nuget api setup'
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:group) { create(:group) }
let_it_be_with_reload(:subgroup) { create(:group, parent: group) }
let_it_be_with_reload(:project) { create(:project, namespace: subgroup) }
let_it_be(:deploy_token) { create(:deploy_token, :group, read_package_registry: true, write_package_registry: true) }
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: group) }
let(:target_type) { 'groups' }
shared_examples 'handling all endpoints' do
describe 'GET /api/v4/groups/:id/packages/nuget' do
it_behaves_like 'handling nuget service requests', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
let(:url) { "/groups/#{target.id}/packages/nuget/index.json" }
end
end
describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/index' do
it_behaves_like 'handling nuget metadata requests with package name', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
end
end
describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/*package_version' do
it_behaves_like 'handling nuget metadata requests with package name and package version', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/#{package.version}.json" }
end
end
describe 'GET /api/v4/groups/:id/packages/nuget/query' do
it_behaves_like 'handling nuget search requests', anonymous_requests_example_name: 'rejects nuget packages access', anonymous_requests_status: :unauthorized do
let(:url) { "/groups/#{target.id}/packages/nuget/query?#{query_parameters.to_query}" }
end
end
end
context 'with a subgroup' do
# Bug: deploy tokens at parent group will not see the subgroup.
# https://gitlab.com/gitlab-org/gitlab/-/issues/285495
let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: deploy_token, group: subgroup) }
let(:target) { subgroup }
it_behaves_like 'handling all endpoints'
def update_visibility_to(visibility)
project.update!(visibility_level: visibility)
subgroup.update!(visibility_level: visibility)
end
end
context 'a group' do
let(:target) { group }
it_behaves_like 'handling all endpoints'
context 'with dummy packages and anonymous request' do
let_it_be(:package_name) { 'Dummy.Package' }
let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) }
let_it_be(:tags) { packages.each { |pkg| create(:packages_tag, package: pkg, name: 'test') } }
let(:search_term) { 'umm' }
let(:take) { 26 }
let(:skip) { 0 }
let(:include_prereleases) { true }
let(:query_parameters) { { q: search_term, take: take, skip: skip, prerelease: include_prereleases } }
subject { get api(url), headers: {}}
shared_examples 'handling mixed visibilities' do
where(:group_visibility, :subgroup_visibility, :expected_status) do
'PUBLIC' | 'PUBLIC' | :unauthorized
'PUBLIC' | 'INTERNAL' | :unauthorized
'PUBLIC' | 'PRIVATE' | :unauthorized
'INTERNAL' | 'INTERNAL' | :unauthorized
'INTERNAL' | 'PRIVATE' | :unauthorized
'PRIVATE' | 'PRIVATE' | :unauthorized
end
with_them do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false))
group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false))
end
it_behaves_like 'returning response status', params[:expected_status]
end
end
describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/index' do
it_behaves_like 'handling mixed visibilities' do
let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/index.json" }
end
end
describe 'GET /api/v4/groups/:id/packages/nuget/metadata/*package_name/*package_version' do
it_behaves_like 'handling mixed visibilities' do
let(:url) { "/groups/#{target.id}/packages/nuget/metadata/#{package_name}/#{packages.first.version}.json" }
end
end
describe 'GET /api/v4/groups/:id/packages/nuget/query' do
it_behaves_like 'handling mixed visibilities' do
let(:url) { "/groups/#{target.id}/packages/nuget/query?#{query_parameters.to_query}" }
end
end
end
def update_visibility_to(visibility)
project.update!(visibility_level: visibility)
subgroup.update!(visibility_level: visibility)
group.update!(visibility_level: visibility)
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
RSpec.describe Packages::Nuget::SearchService do RSpec.describe Packages::Nuget::SearchService do
let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:project) { create(:project, namespace: subgroup) }
let_it_be(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') } let_it_be(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') }
let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') } let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') }
let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') } let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') }
...@@ -16,94 +20,126 @@ RSpec.describe Packages::Nuget::SearchService do ...@@ -16,94 +20,126 @@ RSpec.describe Packages::Nuget::SearchService do
let(:options) { { include_prerelease_versions: include_prerelease_versions, per_page: per_page, padding: padding } } let(:options) { { include_prerelease_versions: include_prerelease_versions, per_page: per_page, padding: padding } }
describe '#execute' do describe '#execute' do
subject { described_class.new(project, search_term, options).execute } subject { described_class.new(user, target, search_term, options).execute }
it { expect_search_results 3, package_a, packages_b, packages_c } shared_examples 'handling all the conditions' do
it { expect_search_results 3, package_a, packages_b, packages_c }
context 'with a smaller per page count' do context 'with a smaller per page count' do
let(:per_page) { 2 } let(:per_page) { 2 }
it { expect_search_results 3, package_a, packages_b } it { expect_search_results 3, package_a, packages_b }
end end
context 'with 0 per page count' do context 'with 0 per page count' do
let(:per_page) { 0 } let(:per_page) { 0 }
it { expect_search_results 3, [] } it { expect_search_results 3, [] }
end end
context 'with a negative per page count' do context 'with a negative per page count' do
let(:per_page) { -1 } let(:per_page) { -1 }
it { expect { subject }.to raise_error(ArgumentError, 'negative per_page') } it { expect { subject }.to raise_error(ArgumentError, 'negative per_page') }
end end
context 'with a padding' do context 'with a padding' do
let(:padding) { 2 } let(:padding) { 2 }
it { expect_search_results 3, packages_c } it { expect_search_results 3, packages_c }
end end
context 'with a too big padding' do context 'with a too big padding' do
let(:padding) { 5 } let(:padding) { 5 }
it { expect_search_results 3, [] } it { expect_search_results 3, [] }
end end
context 'with a negative padding' do context 'with a negative padding' do
let(:padding) { -1 } let(:padding) { -1 }
it { expect { subject }.to raise_error(ArgumentError, 'negative padding') } it { expect { subject }.to raise_error(ArgumentError, 'negative padding') }
end end
context 'with search term' do context 'with search term' do
let(:search_term) { 'umm' } let(:search_term) { 'umm' }
it { expect_search_results 3, package_a, packages_b, packages_c } it { expect_search_results 3, package_a, packages_b, packages_c }
end end
context 'with nil search term' do context 'with nil search term' do
let(:search_term) { nil } let(:search_term) { nil }
it { expect_search_results 4, package_a, packages_b, packages_c, package_d } it { expect_search_results 4, package_a, packages_b, packages_c, package_d }
end end
context 'with empty search term' do context 'with empty search term' do
let(:search_term) { '' } let(:search_term) { '' }
it { expect_search_results 4, package_a, packages_b, packages_c, package_d } it { expect_search_results 4, package_a, packages_b, packages_c, package_d }
end end
context 'with prefix search term' do context 'with prefix search term' do
let(:search_term) { 'dummy' } let(:search_term) { 'dummy' }
it { expect_search_results 3, package_a, packages_b, packages_c } it { expect_search_results 3, package_a, packages_b, packages_c }
end end
context 'with suffix search term' do
let(:search_term) { 'packagec' }
it { expect_search_results 1, packages_c }
end
context 'with pre release packages' do
let_it_be(:package_e) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1-alpha') }
context 'including them' do
it { expect_search_results 4, package_a, packages_b, packages_c, package_e }
end
context 'excluding them' do
let(:include_prerelease_versions) { false }
context 'with suffix search term' do it { expect_search_results 3, package_a, packages_b, packages_c }
let(:search_term) { 'packagec' }
it { expect_search_results 1, packages_c } context 'when mixed with release versions' do
let_it_be(:package_e_release) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1') }
it { expect_search_results 4, package_a, packages_b, packages_c, package_e_release }
end
end
end
end end
context 'with pre release packages' do context 'with project' do
let_it_be(:package_e) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1-alpha') } let(:target) { project }
context 'including them' do before do
it { expect_search_results 4, package_a, packages_b, packages_c, package_e } project.add_developer(user)
end end
context 'excluding them' do it_behaves_like 'handling all the conditions'
let(:include_prerelease_versions) { false } end
it { expect_search_results 3, package_a, packages_b, packages_c } context 'with subgroup' do
let(:target) { subgroup }
context 'when mixed with release versions' do before do
let_it_be(:package_e_release) { create(:nuget_package, project: project, name: 'DummyPackageE', version: '3.2.1') } subgroup.add_developer(user)
end
it { expect_search_results 4, package_a, packages_b, packages_c, package_e_release } it_behaves_like 'handling all the conditions'
end end
context 'with group' do
let(:target) { group }
before do
group.add_developer(user)
end end
it_behaves_like 'handling all the conditions'
end end
def expect_search_results(total_count, *results) def expect_search_results(total_count, *results)
......
# frozen_string_literal: true
RSpec.shared_context 'nuget api setup' do
include WorkhorseHelpers
include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers
let_it_be(:user) { create(:user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add_member = true| RSpec.shared_examples 'rejects nuget packages access' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returning response status', status it_behaves_like 'returning response status', status
...@@ -21,7 +21,7 @@ end ...@@ -21,7 +21,7 @@ end
RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget service index request' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returning response status', status it_behaves_like 'returning response status', status
...@@ -37,7 +37,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu ...@@ -37,7 +37,7 @@ RSpec.shared_examples 'process nuget service index request' do |user_type, statu
end end
context 'with invalid format' do context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/index.xls" } let(:url) { "/#{target_type}/#{target.id}/packages/nuget/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end end
...@@ -57,7 +57,7 @@ end ...@@ -57,7 +57,7 @@ end
RSpec.shared_examples 'process nuget metadata request at package name level' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget metadata request at package name level' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returning response status', status it_behaves_like 'returning response status', status
...@@ -65,7 +65,7 @@ RSpec.shared_examples 'process nuget metadata request at package name level' do ...@@ -65,7 +65,7 @@ RSpec.shared_examples 'process nuget metadata request at package name level' do
it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata' it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/packages_metadata'
context 'with invalid format' do context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/index.xls" } let(:url) { "/#{target_type}/#{target.id}/packages/nuget/metadata/#{package_name}/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end end
...@@ -83,7 +83,7 @@ end ...@@ -83,7 +83,7 @@ end
RSpec.shared_examples 'process nuget metadata request at package name and package version level' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget metadata request at package name and package version level' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returning response status', status it_behaves_like 'returning response status', status
...@@ -91,7 +91,7 @@ RSpec.shared_examples 'process nuget metadata request at package name and packag ...@@ -91,7 +91,7 @@ RSpec.shared_examples 'process nuget metadata request at package name and packag
it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata' it_behaves_like 'returning nuget metadata json response with json schema', 'public_api/v4/packages/nuget/package_metadata'
context 'with invalid format' do context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" } let(:url) { "/#{target_type}/#{target.id}/packages/nuget/metadata/#{package_name}/#{package.version}.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end end
...@@ -109,7 +109,7 @@ end ...@@ -109,7 +109,7 @@ end
RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returning response status', status it_behaves_like 'returning response status', status
...@@ -128,7 +128,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta ...@@ -128,7 +128,7 @@ RSpec.shared_examples 'process nuget workhorse authorization' do |user_type, sta
end end
before do before do
project.add_maintainer(user) target.add_maintainer(user)
end end
it_behaves_like 'returning response status', :forbidden it_behaves_like 'returning response status', :forbidden
...@@ -141,18 +141,18 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = ...@@ -141,18 +141,18 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
it 'creates package files' do it 'creates package files' do
expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once expect(::Packages::Nuget::ExtractionWorker).to receive(:perform_async).once
expect { subject } expect { subject }
.to change { project.packages.count }.by(1) .to change { target.packages.count }.by(1)
.and change { Packages::PackageFile.count }.by(1) .and change { Packages::PackageFile.count }.by(1)
expect(response).to have_gitlab_http_status(status) expect(response).to have_gitlab_http_status(status)
package_file = project.packages.last.package_files.reload.last package_file = target.packages.last.package_files.reload.last
expect(package_file.file_name).to eq('package.nupkg') expect(package_file.file_name).to eq('package.nupkg')
end end
end end
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
context 'with object storage disabled' do context 'with object storage disabled' do
...@@ -206,7 +206,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member = ...@@ -206,7 +206,7 @@ RSpec.shared_examples 'process nuget upload' do |user_type, status, add_member =
context 'with crafted package.path param' do context 'with crafted package.path param' do
let(:crafted_file) { Tempfile.new('nuget.crafted.package.path') } let(:crafted_file) { Tempfile.new('nuget.crafted.package.path') }
let(:url) { "/projects/#{project.id}/packages/nuget?package.path=#{crafted_file.path}" } let(:url) { "/#{target_type}/#{target.id}/packages/nuget?package.path=#{crafted_file.path}" }
let(:params) { { file: temp_file(file_name) } } let(:params) { { file: temp_file(file_name) } }
let(:file_key) { :file } let(:file_key) { :file }
...@@ -255,7 +255,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s ...@@ -255,7 +255,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returning response status', status it_behaves_like 'returning response status', status
...@@ -263,7 +263,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s ...@@ -263,7 +263,7 @@ RSpec.shared_examples 'process nuget download versions request' do |user_type, s
it_behaves_like 'returns a valid nuget download versions json response' it_behaves_like 'returns a valid nuget download versions json response'
context 'with invalid format' do context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package_name}/index.xls" } let(:url) { "/#{target_type}/#{target.id}/packages/nuget/download/#{package_name}/index.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end end
...@@ -281,7 +281,7 @@ end ...@@ -281,7 +281,7 @@ end
RSpec.shared_examples 'process nuget download content request' do |user_type, status, add_member = true| RSpec.shared_examples 'process nuget download content request' do |user_type, status, add_member = true|
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returning response status', status it_behaves_like 'returning response status', status
...@@ -295,7 +295,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st ...@@ -295,7 +295,7 @@ RSpec.shared_examples 'process nuget download content request' do |user_type, st
end end
context 'with invalid format' do context 'with invalid format' do
let(:url) { "/projects/#{project.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" } let(:url) { "/#{target_type}/#{target.id}/packages/nuget/download/#{package.name}/#{package.version}/#{package.name}.#{package.version}.xls" }
it_behaves_like 'rejects nuget packages access', :anonymous, :not_found it_behaves_like 'rejects nuget packages access', :anonymous, :not_found
end end
...@@ -331,7 +331,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ ...@@ -331,7 +331,7 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_
context "for user type #{user_type}" do context "for user type #{user_type}" do
before do before do
project.send("add_#{user_type}", user) if add_member && user_type != :anonymous target.send("add_#{user_type}", user) if add_member && user_type != :anonymous
end end
it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1] it_behaves_like 'returns a valid json search response', status, 4, [1, 5, 5, 1]
...@@ -370,20 +370,20 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_ ...@@ -370,20 +370,20 @@ RSpec.shared_examples 'process nuget search request' do |user_type, status, add_
end end
end end
RSpec.shared_examples 'rejects nuget access with invalid project id' do RSpec.shared_examples 'rejects nuget access with invalid target id' do
context 'with a project id with invalid integers' do context 'with a target id with invalid integers' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
let(:project) { OpenStruct.new(id: id) } let(:target) { OpenStruct.new(id: id) }
where(:id, :status) do where(:id, :status) do
'/../' | :unauthorized '/../' | :bad_request
'' | :not_found '' | :not_found
'%20' | :unauthorized '%20' | :bad_request
'%2e%2e%2f' | :unauthorized '%2e%2e%2f' | :bad_request
'NaN' | :unauthorized 'NaN' | :bad_request
00002345 | :unauthorized 00002345 | :unauthorized
'anything25' | :unauthorized 'anything25' | :bad_request
end end
with_them do with_them do
...@@ -392,9 +392,9 @@ RSpec.shared_examples 'rejects nuget access with invalid project id' do ...@@ -392,9 +392,9 @@ RSpec.shared_examples 'rejects nuget access with invalid project id' do
end end
end end
RSpec.shared_examples 'rejects nuget access with unknown project id' do RSpec.shared_examples 'rejects nuget access with unknown target id' do
context 'with an unknown project' do context 'with an unknown target' do
let(:project) { OpenStruct.new(id: 1234567890) } let(:target) { OpenStruct.new(id: 1234567890) }
context 'as anonymous' do context 'as anonymous' do
it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized it_behaves_like 'rejects nuget packages access', :anonymous, :unauthorized
......
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