Commit 9468fe3b authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch '220985-add-npm-project-level-manager-api' into 'master'

NPM project level API

See merge request gitlab-org/gitlab!46867
parents ceb87bce 5abe5565
...@@ -12,6 +12,8 @@ module Packages ...@@ -12,6 +12,8 @@ module Packages
end end
def execute def execute
return Packages::Package.none unless project
packages packages
end end
......
---
title: NPM project level API
merge_request: 46867
author:
type: added
...@@ -81,6 +81,17 @@ To create a project: ...@@ -81,6 +81,17 @@ To create a project:
A `package.json` file is created. A `package.json` file is created.
## Use the GitLab endpoint for NPM packages
To use the GitLab endpoint for NPM packages, choose an option:
- **Project-level**: Use when you have few NPM packages and they are not in
the same GitLab group.
- **Instance-level**: Use when you have many NPM packages in different
GitLab groups or in their own namespace. Be sure to comply with the [package naming convention](#package-naming-convention).
Some features such as [publishing](#publish-an-npm-package) a package is only available on the project-level endpoint.
## Authenticate to the Package Registry ## Authenticate to the Package Registry
To authenticate to the Package Registry, you must use one of the following: To authenticate to the Package Registry, you must use one of the following:
...@@ -94,20 +105,19 @@ To authenticate to the Package Registry, you must use one of the following: ...@@ -94,20 +105,19 @@ To authenticate to the Package Registry, you must use one of the following:
### Authenticate with a personal access token or deploy token ### Authenticate with a personal access token or deploy token
To authenticate with a [personal access token](../../profile/personal_access_tokens.md) or [deploy token](../../project/deploy_tokens/index.md), To authenticate with the Package Registry, you will need a [personal access token](../../profile/personal_access_tokens.md) or [deploy token](../../project/deploy_tokens/index.md).
set your NPM configuration:
```shell #### Project-level NPM endpoint
# Set URL for your scoped packages
# For example, a package named `@foo/bar` uses this URL for download
npm config set @foo:registry https://gitlab.example.com/api/v4/packages/npm/
# Add the token for the scoped packages URL To use the [project-level](#use-the-gitlab-endpoint-for-npm-packages) NPM endpoint, set your NPM configuration:
# Use this to download `@foo/` packages from private projects
npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>" ```shell
# Set URL for your scoped packages.
# For example package with name `@foo/bar` will use this URL for download
npm config set @foo:registry https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/
# Add token for to publish to the package registry # Add the token for the scoped packages URL. Replace <your_project_id>
# Replace <your_project_id> with the project you want to publish your package to # with the project where your package is located.
npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>" npm config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
``` ```
...@@ -120,6 +130,28 @@ You should now be able to publish and install NPM packages in your project. ...@@ -120,6 +130,28 @@ You should now be able to publish and install NPM packages in your project.
If you encounter an error with [Yarn](https://classic.yarnpkg.com/en/), view If you encounter an error with [Yarn](https://classic.yarnpkg.com/en/), view
[troubleshooting steps](#troubleshooting). [troubleshooting steps](#troubleshooting).
#### Instance-level NPM endpoint
To use the [instance-level](#use-the-gitlab-endpoint-for-npm-packages) NPM endpoint, set your NPM configuration:
```shell
# Set URL for your scoped packages.
# For example package with name `@foo/bar` will use this URL for download
npm config set @foo:registry https://gitlab.example.com/api/v4/packages/npm/
# Add the token for the scoped packages URL. This will allow you to download
# `@foo/` packages from private projects.
npm config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>"
```
- `<your_token>` is your personal access token or deploy token.
- Replace `gitlab.example.com` with your domain name.
You should now be able to publish and install NPM packages in your project.
If you encounter an error with [Yarn](https://classic.yarnpkg.com/en/), view
[troubleshooting steps](#troubleshooting).
### Authenticate with a CI job token ### Authenticate with a CI job token
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9104) in GitLab Premium 12.5. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9104) in GitLab Premium 12.5.
...@@ -128,12 +160,22 @@ If you encounter an error with [Yarn](https://classic.yarnpkg.com/en/), view ...@@ -128,12 +160,22 @@ If you encounter an error with [Yarn](https://classic.yarnpkg.com/en/), view
If you're using NPM with GitLab CI/CD, a CI job token can be used instead of a personal access token or deploy token. If you're using NPM with GitLab CI/CD, a CI job token can be used instead of a personal access token or deploy token.
The token inherits the permissions of the user that generates the pipeline. The token inherits the permissions of the user that generates the pipeline.
Add a corresponding section to your `.npmrc` file: #### Project-level NPM endpoint
To use the [project-level](#use-the-gitlab-endpoint-for-npm-packages) NPM endpoint, add a corresponding section to your `.npmrc` file:
```ini
@foo:registry=https://gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/
//gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}
```
#### Instance-level NPM endpoint
To use the [instance-level](#use-the-gitlab-endpoint-for-npm-packages) NPM endpoint, add a corresponding section to your `.npmrc` file:
```ini ```ini
@foo:registry=https://gitlab.example.com/api/v4/packages/npm/ @foo:registry=https://gitlab.example.com/api/v4/packages/npm/
//gitlab.example.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN} //gitlab.example.com/api/v4/packages/npm/:_authToken=${CI_JOB_TOKEN}
//gitlab.example.com/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}
``` ```
#### Use variables to avoid hard-coding auth token values #### Use variables to avoid hard-coding auth token values
...@@ -194,27 +236,12 @@ the naming convention and run `npm publish` if necessary. ...@@ -194,27 +236,12 @@ the naming convention and run `npm publish` if necessary.
## Publish an NPM package ## Publish an NPM package
Before you can publish a package, you must specify the registry Prerequisites:
for NPM. To do this, add the following section to the bottom of `package.json`:
```json
"publishConfig": {
"@foo:registry":"https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/"
}
```
- `<your_project_id>` is your project ID, found on the project's home page.
- `@foo` is your scope.
- Replace `gitlab.example.com` with your domain name.
DANGER: **Warning:** - [Authenticate](#authenticate-to-the-package-registry) to the Package Registry.
The `publishConfig` entry in the `package.json` file is not respected, because of a - Set a [project-level NPM endpoint](#use-the-gitlab-endpoint-for-npm-packages).
[bug in NPM](https://github.com/npm/cli/issues/1994) version `7.x` and later. You must
use an earlier version of NPM, or temporarily set your `.npmrc` scope to
`@foo:registry=https://gitlab.example.com/api/v4/projects/<project_id>/packages/npm`.
After you have set up [authentication](#authenticate-to-the-package-registry), To upload an NPM package to your project, run this command:
you can upload an NPM package to your project:
```shell ```shell
npm publish npm publish
...@@ -227,6 +254,11 @@ a given scope, you get a `403 Forbidden!` error. ...@@ -227,6 +254,11 @@ a given scope, you get a `403 Forbidden!` error.
## Publish an NPM package by using CI/CD ## Publish an NPM package by using CI/CD
Prerequisites:
- [Authenticate](#authenticate-to-the-package-registry) to the Package Registry.
- Set a [project-level NPM endpoint](#use-the-gitlab-endpoint-for-npm-packages).
To work with NPM commands within [GitLab CI/CD](../../../ci/README.md), you can use To work with NPM commands within [GitLab CI/CD](../../../ci/README.md), you can use
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands. `CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
...@@ -390,9 +422,6 @@ should look like: ...@@ -390,9 +422,6 @@ should look like:
"name": "@foo/my-package", "name": "@foo/my-package",
"version": "1.0.0", "version": "1.0.0",
"description": "Example package for GitLab NPM registry", "description": "Example package for GitLab NPM registry",
"publishConfig": {
"@foo:registry":"https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/"
}
} }
``` ```
......
...@@ -218,7 +218,8 @@ module API ...@@ -218,7 +218,8 @@ module API
mount ::API::DebianGroupPackages mount ::API::DebianGroupPackages
mount ::API::DebianProjectPackages mount ::API::DebianProjectPackages
mount ::API::MavenPackages mount ::API::MavenPackages
mount ::API::NpmPackages mount ::API::NpmProjectPackages
mount ::API::NpmInstancePackages
mount ::API::GenericPackages mount ::API::GenericPackages
mount ::API::GoProxy mount ::API::GoProxy
mount ::API::Pages mount ::API::Pages
......
# frozen_string_literal: true
# NPM Package Manager Client API
#
# These API endpoints are not consumed directly by users, so there is no documentation for the
# individual endpoints. They are called by the NPM package manager client when users run commands
# like `npm install` or `npm publish`. The usage of the GitLab NPM registry is documented here:
# https://docs.gitlab.com/ee/user/packages/npm_registry/
#
# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
#
# Caution: This Concern has to be included at the end of the API class
# The last route of this Concern has a globbing wildcard that will match all urls.
# As such, routes declared after the last route of this Concern will not match any url.
module API
module Concerns
module Packages
module NpmEndpoints
extend ActiveSupport::Concern
included do
helpers ::API::Helpers::Packages::DependencyProxyHelpers
before do
require_packages_enabled!
authenticate_non_get!
end
params do
requires :package_name, type: String, desc: 'Package name'
end
namespace '-/package/*package_name' do
desc 'Get all tags for a given an NPM package' do
detail 'This feature was introduced in GitLab 12.7'
success ::API::Entities::NpmPackageTag
end
get 'dist-tags', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
bad_request!('Package Name') if package_name.blank?
authorize_read_package!(project)
packages = ::Packages::Npm::PackageFinder.new(project, package_name)
.execute
not_found! if packages.empty?
present ::Packages::Npm::PackagePresenter.new(package_name, packages),
with: ::API::Entities::NpmPackageTag
end
params do
requires :tag, type: String, desc: "Package dist-tag"
end
namespace 'dist-tags/:tag', requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
desc 'Create or Update the given tag for the given NPM package and version' do
detail 'This feature was introduced in GitLab 12.7'
end
put format: false do
package_name = params[:package_name]
version = env['api.request.body']
tag = params[:tag]
bad_request!('Package Name') if package_name.blank?
bad_request!('Version') if version.blank?
bad_request!('Tag') if tag.blank?
authorize_create_package!(project)
package = ::Packages::Npm::PackageFinder
.new(project, package_name)
.find_by_version(version)
not_found!('Package') unless package
::Packages::Npm::CreateTagService.new(package, tag).execute
no_content!
end
desc 'Deletes the given tag' do
detail 'This feature was introduced in GitLab 12.7'
end
delete format: false do
package_name = params[:package_name]
tag = params[:tag]
bad_request!('Package Name') if package_name.blank?
bad_request!('Tag') if tag.blank?
authorize_destroy_package!(project)
package_tag = ::Packages::TagsFinder
.new(project, package_name, package_type: :npm)
.find_by_name(tag)
not_found!('Package tag') unless package_tag
::Packages::RemoveTagService.new(package_tag).execute
no_content!
end
end
end
desc 'NPM registry metadata endpoint' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
packages = ::Packages::Npm::PackageFinder.new(project_or_nil, package_name)
.execute
redirect_request = project_or_nil.blank? || packages.empty?
redirect_registry_request(redirect_request, :npm, package_name: package_name) do
authorize_read_package!(project)
not_found!('Packages') if packages.empty?
present ::Packages::Npm::PackagePresenter.new(package_name, packages),
with: ::API::Entities::NpmPackage
end
end
end
end
end
end
end
# frozen_string_literal: true
module API
module Helpers
module Packages
module Npm
include Gitlab::Utils::StrongMemoize
include ::API::Helpers::PackagesHelpers
NPM_ENDPOINT_REQUIREMENTS = {
package_name: API::NO_SLASH_URL_PART_REGEX
}.freeze
def endpoint_scope
params[:id].present? ? :project : :instance
end
def project
strong_memoize(:project) do
case endpoint_scope
when :project
user_project
when :instance
# Simulate the same behavior as #user_project by re-using #find_project!
# but take care if the project_id is nil as #find_project! is not designed
# to handle it.
project_id = project_id_or_nil
not_found!('Project') unless project_id
find_project!(project_id)
end
end
end
def project_or_nil
# mainly used by the metadata endpoint where we need to get a project
# and return nil if not found (no errors should be raised)
strong_memoize(:project_or_nil) do
next unless project_id_or_nil
find_project(project_id_or_nil)
end
end
def project_id_or_nil
strong_memoize(:project_id_or_nil) do
case endpoint_scope
when :project
params[:id]
when :instance
::Packages::Package.npm
.with_name(params[:package_name])
.first
&.project_id
end
end
end
end
end
end
end
# frozen_string_literal: true
module API
class NpmInstancePackages < ::API::Base
helpers ::API::Helpers::Packages::Npm
feature_category :package_registry
rescue_from ActiveRecord::RecordInvalid do |e|
render_api_error!(e.message, 400)
end
namespace 'packages/npm' do
include ::API::Concerns::Packages::NpmEndpoints
end
end
end
# frozen_string_literal: true
module API
class NpmPackages < ::API::Base
helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::DependencyProxyHelpers
feature_category :package_registry
NPM_ENDPOINT_REQUIREMENTS = {
package_name: API::NO_SLASH_URL_PART_REGEX
}.freeze
rescue_from ActiveRecord::RecordInvalid do |e|
render_api_error!(e.message, 400)
end
before do
require_packages_enabled!
authenticate_non_get!
end
helpers do
def project_by_package_name
strong_memoize(:project_by_package_name) do
::Packages::Package.npm.with_name(params[:package_name]).first&.project
end
end
end
desc 'Get all tags for a given an NPM package' do
detail 'This feature was introduced in GitLab 12.7'
success ::API::Entities::NpmPackageTag
end
params do
requires :package_name, type: String, desc: 'Package name'
end
get 'packages/npm/-/package/*package_name/dist-tags', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
bad_request!('Package Name') if package_name.blank?
authorize_read_package!(project_by_package_name)
packages = ::Packages::Npm::PackageFinder.new(project_by_package_name, package_name)
.execute
present ::Packages::Npm::PackagePresenter.new(package_name, packages),
with: ::API::Entities::NpmPackageTag
end
params do
requires :package_name, type: String, desc: 'Package name'
requires :tag, type: String, desc: "Package dist-tag"
end
namespace 'packages/npm/-/package/*package_name/dist-tags/:tag', requirements: NPM_ENDPOINT_REQUIREMENTS do
desc 'Create or Update the given tag for the given NPM package and version' do
detail 'This feature was introduced in GitLab 12.7'
end
put format: false do
package_name = params[:package_name]
version = env['api.request.body']
tag = params[:tag]
bad_request!('Package Name') if package_name.blank?
bad_request!('Version') if version.blank?
bad_request!('Tag') if tag.blank?
authorize_create_package!(project_by_package_name)
package = ::Packages::Npm::PackageFinder
.new(project_by_package_name, package_name)
.find_by_version(version)
not_found!('Package') unless package
::Packages::Npm::CreateTagService.new(package, tag).execute
no_content!
end
desc 'Deletes the given tag' do
detail 'This feature was introduced in GitLab 12.7'
end
delete format: false do
package_name = params[:package_name]
tag = params[:tag]
bad_request!('Package Name') if package_name.blank?
bad_request!('Tag') if tag.blank?
authorize_destroy_package!(project_by_package_name)
package_tag = ::Packages::TagsFinder
.new(project_by_package_name, package_name, package_type: :npm)
.find_by_name(tag)
not_found!('Package tag') unless package_tag
::Packages::RemoveTagService.new(package_tag).execute
no_content!
end
end
desc 'NPM registry endpoint at instance level' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get 'packages/npm/*package_name', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name]
redirect_registry_request(project_by_package_name.blank?, :npm, package_name: package_name) do
authorize_read_package!(project_by_package_name)
packages = ::Packages::Npm::PackageFinder
.new(project_by_package_name, package_name).execute
present ::Packages::Npm::PackagePresenter.new(package_name, packages),
with: ::API::Entities::NpmPackage
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Download the NPM tarball' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
requires :file_name, type: String, desc: 'Package file name'
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/packages/npm/*package_name/-/*file_name', format: false do
authorize_read_package!(user_project)
package = user_project.packages.npm
.by_name_and_file_name(params[:package_name], params[:file_name])
package_file = ::Packages::PackageFileFinder
.new(package, params[:file_name]).execute!
track_package_event('pull_package', package)
present_carrierwave_file!(package_file.file)
end
desc 'Create NPM package' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
requires :versions, type: Hash, desc: 'Package version info'
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do
authorize_create_package!(user_project)
track_package_event('push_package', :npm)
created_package = ::Packages::Npm::CreatePackageService
.new(user_project, current_user, params.merge(build: current_authenticated_job)).execute
if created_package[:status] == :error
render_api_error!(created_package[:message], created_package[:http_status])
else
created_package
end
end
end
end
end
# frozen_string_literal: true
module API
class NpmProjectPackages < ::API::Base
helpers ::API::Helpers::Packages::Npm
feature_category :package_registry
rescue_from ActiveRecord::RecordInvalid do |e|
render_api_error!(e.message, 400)
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
namespace 'projects/:id/packages/npm' do
desc 'Download the NPM tarball' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
requires :file_name, type: String, desc: 'Package file name'
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get '*package_name/-/*file_name', format: false do
authorize_read_package!(project)
package = project.packages.npm
.by_name_and_file_name(params[:package_name], params[:file_name])
not_found!('Package') unless package
package_file = ::Packages::PackageFileFinder
.new(package, params[:file_name]).execute!
track_package_event('pull_package', package, category: 'API::NpmPackages')
present_carrierwave_file!(package_file.file)
end
desc 'Create NPM package' do
detail 'This feature was introduced in GitLab 11.8'
end
params do
requires :package_name, type: String, desc: 'Package name'
requires :versions, type: Hash, desc: 'Package version info'
end
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
put ':package_name', requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do
authorize_create_package!(project)
track_package_event('push_package', :npm, category: 'API::NpmPackages')
created_package = ::Packages::Npm::CreatePackageService
.new(project, current_user, params.merge(build: current_authenticated_job)).execute
if created_package[:status] == :error
render_api_error!(created_package[:message], created_package[:http_status])
else
created_package
end
end
include ::API::Concerns::Packages::NpmEndpoints
end
end
end
...@@ -16,6 +16,12 @@ RSpec.describe ::Packages::Npm::PackageFinder do ...@@ -16,6 +16,12 @@ RSpec.describe ::Packages::Npm::PackageFinder do
it { is_expected.to be_empty } it { is_expected.to be_empty }
end end
context 'with nil project' do
let(:project) { nil }
it { is_expected.to be_empty }
end
end end
describe '#find_by_version' do describe '#find_by_version' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::NpmInstancePackages do
include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do
it_behaves_like 'handling get metadata requests' do
let(:url) { api("/packages/npm/#{package_name}") }
end
end
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
it_behaves_like 'handling get dist tags requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags") }
end
end
describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling create dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
it_behaves_like 'handling delete dist tag requests', scope: :instance do
let(:url) { api("/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
end
...@@ -2,194 +2,72 @@ ...@@ -2,194 +2,72 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe API::NpmPackages do RSpec.describe API::NpmProjectPackages do
include PackagesManagerApiSpecHelpers include_context 'npm api setup'
include HttpBasicAuthHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
let_it_be(:package, reload: true) { create(:npm_package, project: project) }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
before do describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do
project.add_developer(user) it_behaves_like 'handling get metadata requests' do
end let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") }
shared_examples 'a package that requires auth' do
it 'returns the package info with oauth token' do
get_package_with_token(package)
expect_a_valid_package_response
end
it 'returns the package info with running job token' do
get_package_with_job_token(package)
expect_a_valid_package_response
end
it 'denies request without running job token' do
job.update!(status: :success)
get_package_with_job_token(package)
expect(response).to have_gitlab_http_status(:unauthorized)
end end
it 'denies request without oauth token' do
get_package(package)
expect(response).to have_gitlab_http_status(:forbidden)
end end
it 'returns the package info with deploy token' do describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
get_package_with_deploy_token(package) it_behaves_like 'handling get dist tags requests' do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") }
expect_a_valid_package_response
end end
end end
describe 'GET /api/v4/packages/npm/*package_name' do describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) } it_behaves_like 'handling create dist tag requests' do
let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) } let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
shared_examples 'returning the npm package info' do
it 'returns the package info' do
get_package(package)
expect_a_valid_package_response
end end
end end
shared_examples 'returning forbidden for unknown package' do describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
context 'with an unknown package' do it_behaves_like 'handling delete dist tag requests' do
it 'returns forbidden' do let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
get api("/packages/npm/unknown")
expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
end
context 'a public project' do
it_behaves_like 'returning the npm package info'
context 'with application setting enabled' do describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
before do let_it_be(:package_file) { package.package_files.first }
stub_application_setting(npm_package_requests_forwarding: true)
end
it_behaves_like 'returning the npm package info' let(:params) { {} }
let(:url) { api("/projects/#{project.id}/packages/npm/#{package_file.package.name}/-/#{package_file.file_name}") }
context 'with unknown package' do subject { get(url, params: params) }
subject { get api("/packages/npm/unknown") }
it 'returns a redirect' do shared_examples 'a package file that requires auth' do
it 'denies download with no token' do
subject subject
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:not_found)
expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown')
end
it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward'
end
end
context 'with application setting disabled' do
before do
stub_application_setting(npm_package_requests_forwarding: false)
end
it_behaves_like 'returning the npm package info'
it_behaves_like 'returning forbidden for unknown package'
end
context 'project path with a dot' do
before do
project.update!(path: 'foo.bar')
end
it_behaves_like 'returning the npm package info'
end
end
context 'internal project' do
before do
project.team.truncate
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it_behaves_like 'a package that requires auth'
end
context 'private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it_behaves_like 'a package that requires auth'
it 'denies request when not enough permissions' do
project.add_guest(user)
get_package_with_token(package)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
def get_package(package, params = {}, headers = {})
get api("/packages/npm/#{package.name}"), params: params, headers: headers
end
def get_package_with_token(package, params = {})
get_package(package, params.merge(access_token: token.token))
end
def get_package_with_job_token(package, params = {})
get_package(package, params.merge(job_token: job.token))
end
def get_package_with_deploy_token(package, params = {})
get_package(package, {}, build_token_auth_header(deploy_token.token))
end
end end
describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do context 'with access token' do
let_it_be(:package_file) { package.package_files.first } let(:params) { { access_token: token.token } }
shared_examples 'a package file that requires auth' do it 'returns the file' do
it 'returns the file with an access token' do subject
get_file_with_token(package_file)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream') expect(response.media_type).to eq('application/octet-stream')
end end
end
it 'returns the file with a job token' do context 'with job token' do
get_file_with_job_token(package_file) let(:params) { { job_token: job.token } }
it 'returns the file' do
subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/octet-stream') expect(response.media_type).to eq('application/octet-stream')
end end
it 'denies download with no token' do
get_file(package_file)
expect(response).to have_gitlab_http_status(:not_found)
end end
end end
context 'a public project' do context 'a public project' do
subject { get_file(package_file) }
it 'returns the file with no token needed' do it 'returns the file with no token needed' do
subject subject
...@@ -197,7 +75,7 @@ RSpec.describe API::NpmPackages do ...@@ -197,7 +75,7 @@ RSpec.describe API::NpmPackages do
expect(response.media_type).to eq('application/octet-stream') expect(response.media_type).to eq('application/octet-stream')
end end
it_behaves_like 'a package tracking event', described_class.name, 'pull_package' it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
end end
context 'private project' do context 'private project' do
...@@ -207,14 +85,18 @@ RSpec.describe API::NpmPackages do ...@@ -207,14 +85,18 @@ RSpec.describe API::NpmPackages do
it_behaves_like 'a package file that requires auth' it_behaves_like 'a package file that requires auth'
context 'with guest' do
let(:params) { { access_token: token.token } }
it 'denies download when not enough permissions' do it 'denies download when not enough permissions' do
project.add_guest(user) project.add_guest(user)
get_file_with_token(package_file) subject
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
end end
end
context 'internal project' do context 'internal project' do
before do before do
...@@ -223,19 +105,6 @@ RSpec.describe API::NpmPackages do ...@@ -223,19 +105,6 @@ RSpec.describe API::NpmPackages do
it_behaves_like 'a package file that requires auth' it_behaves_like 'a package file that requires auth'
end end
def get_file(package_file, params = {})
get api("/projects/#{project.id}/packages/npm/" \
"#{package_file.package.name}/-/#{package_file.file_name}"), params: params
end
def get_file_with_token(package_file, params = {})
get_file(package_file, params.merge(access_token: token.token))
end
def get_file_with_job_token(package_file, params = {})
get_file(package_file, params.merge(job_token: job.token))
end
end end
describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
...@@ -305,7 +174,7 @@ RSpec.describe API::NpmPackages do ...@@ -305,7 +174,7 @@ RSpec.describe API::NpmPackages do
context 'with access token' do context 'with access token' do
subject { upload_package_with_token(package_name, params) } subject { upload_package_with_token(package_name, params) }
it_behaves_like 'a package tracking event', described_class.name, 'push_package' it_behaves_like 'a package tracking event', 'API::NpmPackages', 'push_package'
it 'creates npm package with file' do it 'creates npm package with file' do
expect { subject } expect { subject }
...@@ -409,148 +278,4 @@ RSpec.describe API::NpmPackages do ...@@ -409,148 +278,4 @@ RSpec.describe API::NpmPackages do
.gsub('1.0.1', package_version)) .gsub('1.0.1', package_version))
end end
end end
describe 'GET /api/v4/packages/npm/-/package/*package_name/dist-tags' do
let_it_be(:package_tag1) { create(:packages_tag, package: package) }
let_it_be(:package_tag2) { create(:packages_tag, package: package) }
let(:package_name) { package.name }
let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags" }
subject { get api(url) }
context 'with public project' do
context 'with authenticated user' do
subject { get api(url, personal_access_token: personal_access_token) }
it_behaves_like 'returns package tags', :maintainer
it_behaves_like 'returns package tags', :developer
it_behaves_like 'returns package tags', :reporter
it_behaves_like 'returns package tags', :guest
end
context 'with unauthenticated user' do
it_behaves_like 'returns package tags', :no_type
end
end
context 'with private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
context 'with authenticated user' do
subject { get api(url, personal_access_token: personal_access_token) }
it_behaves_like 'returns package tags', :maintainer
it_behaves_like 'returns package tags', :developer
it_behaves_like 'returns package tags', :reporter
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :forbidden
end
end
end
describe 'PUT /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
let_it_be(:tag_name) { 'test' }
let(:package_name) { package.name }
let(:version) { package.version }
let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" }
subject { put api(url), env: { 'api.request.body': version } }
context 'with public project' do
context 'with authenticated user' do
subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } }
it_behaves_like 'create package tag', :maintainer
it_behaves_like 'create package tag', :developer
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end
context 'with private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
context 'with authenticated user' do
subject { put api(url, personal_access_token: personal_access_token), env: { 'api.request.body': version } }
it_behaves_like 'create package tag', :maintainer
it_behaves_like 'create package tag', :developer
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end
end
describe 'DELETE /api/v4/packages/npm/-/package/*package_name/dist-tags/:tag' do
let_it_be(:package_tag) { create(:packages_tag, package: package) }
let(:package_name) { package.name }
let(:tag_name) { package_tag.name }
let(:url) { "/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}" }
subject { delete api(url) }
context 'with public project' do
context 'with authenticated user' do
subject { delete api(url, personal_access_token: personal_access_token) }
it_behaves_like 'delete package tag', :maintainer
it_behaves_like 'rejects package tags access', :developer, :forbidden
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end
context 'with private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
context 'with authenticated user' do
subject { delete api(url, personal_access_token: personal_access_token) }
it_behaves_like 'delete package tag', :maintainer
it_behaves_like 'rejects package tags access', :developer, :forbidden
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end
end
def expect_a_valid_package_response
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/json')
expect(response).to match_response_schema('public_api/v4/packages/npm_package')
expect(json_response['name']).to eq(package.name)
expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
end
expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
end end
# frozen_string_literal: true
RSpec.shared_context 'npm api setup' do
include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
let_it_be(:package, reload: true) { create(:npm_package, project: project) }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:package_name) { package.name }
before do
project.add_developer(user)
end
end
# frozen_string_literal: true
RSpec.shared_examples 'handling get metadata requests' do
let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) }
let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) }
let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
let(:params) { {} }
let(:headers) { {} }
subject { get(url, params: params, headers: headers) }
shared_examples 'returning the npm package info' do
it 'returns the package info' do
subject
expect_a_valid_package_response
end
end
shared_examples 'a package that requires auth' do
it 'denies request without oauth token' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
context 'with oauth token' do
let(:params) { { access_token: token.token } }
it 'returns the package info with oauth token' do
subject
expect_a_valid_package_response
end
end
context 'with job token' do
let(:params) { { job_token: job.token } }
it 'returns the package info with running job token' do
subject
expect_a_valid_package_response
end
it 'denies request without running job token' do
job.update!(status: :success)
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with deploy token' do
let(:headers) { build_token_auth_header(deploy_token.token) }
it 'returns the package info with deploy token' do
subject
expect_a_valid_package_response
end
end
end
context 'a public project' do
it_behaves_like 'returning the npm package info'
context 'project path with a dot' do
before do
project.update!(path: 'foo.bar')
end
it_behaves_like 'returning the npm package info'
end
context 'with request forward disabled' do
before do
stub_application_setting(npm_package_requests_forwarding: false)
end
it_behaves_like 'returning the npm package info'
context 'with unknown package' do
let(:package_name) { 'unknown' }
it 'returns the proper response' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'with request forward enabled' do
before do
stub_application_setting(npm_package_requests_forwarding: true)
end
it_behaves_like 'returning the npm package info'
context 'with unknown package' do
let(:package_name) { 'unknown' }
it 'returns a redirect' do
subject
expect(response).to have_gitlab_http_status(:found)
expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown')
end
it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward'
end
end
end
context 'internal project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
it_behaves_like 'a package that requires auth'
end
context 'private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it_behaves_like 'a package that requires auth'
context 'with guest' do
let(:params) { { access_token: token.token } }
it 'denies request when not enough permissions' do
project.add_guest(user)
subject
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
def expect_a_valid_package_response
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/json')
expect(response).to match_response_schema('public_api/v4/packages/npm_package')
expect(json_response['name']).to eq(package.name)
expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
end
expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
end
RSpec.shared_examples 'handling get dist tags requests' do
let_it_be(:package_tag1) { create(:packages_tag, package: package) }
let_it_be(:package_tag2) { create(:packages_tag, package: package) }
let(:params) { {} }
subject { get(url, params: params) }
context 'with public project' do
context 'with authenticated user' do
let(:params) { { private_token: personal_access_token.token } }
it_behaves_like 'returns package tags', :maintainer
it_behaves_like 'returns package tags', :developer
it_behaves_like 'returns package tags', :reporter
it_behaves_like 'returns package tags', :guest
end
context 'with unauthenticated user' do
it_behaves_like 'returns package tags', :no_type
end
end
context 'with private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
context 'with authenticated user' do
let(:params) { { private_token: personal_access_token.token } }
it_behaves_like 'returns package tags', :maintainer
it_behaves_like 'returns package tags', :developer
it_behaves_like 'returns package tags', :reporter
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :not_found
end
end
end
RSpec.shared_examples 'handling create dist tag requests' do
let_it_be(:tag_name) { 'test' }
let(:params) { {} }
let(:env) { {} }
let(:version) { package.version }
subject { put(url, env: env, params: params) }
context 'with public project' do
context 'with authenticated user' do
let(:params) { { private_token: personal_access_token.token } }
let(:env) { { 'api.request.body': version } }
it_behaves_like 'create package tag', :maintainer
it_behaves_like 'create package tag', :developer
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end
end
RSpec.shared_examples 'handling delete dist tag requests' do
let_it_be(:package_tag) { create(:packages_tag, package: package) }
let(:params) { {} }
let(:tag_name) { package_tag.name }
subject { delete(url, params: params) }
context 'with public project' do
context 'with authenticated user' do
let(:params) { { private_token: personal_access_token.token } }
it_behaves_like 'delete package tag', :maintainer
it_behaves_like 'rejects package tags access', :developer, :forbidden
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end
context 'with private project' do
before do
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
context 'with authenticated user' do
let(:params) { { private_token: personal_access_token.token } }
it_behaves_like 'delete package tag', :maintainer
it_behaves_like 'rejects package tags access', :developer, :forbidden
it_behaves_like 'rejects package tags access', :reporter, :forbidden
it_behaves_like 'rejects package tags access', :guest, :forbidden
end
context 'with unauthenticated user' do
it_behaves_like 'rejects package tags access', :no_type, :unauthorized
end
end
end
...@@ -40,7 +40,7 @@ RSpec.shared_examples 'returns package tags' do |user_type| ...@@ -40,7 +40,7 @@ RSpec.shared_examples 'returns package tags' do |user_type|
context 'with invalid package name' do context 'with invalid package name' do
where(:package_name, :status) do where(:package_name, :status) do
'%20' | :bad_request '%20' | :bad_request
nil | :forbidden nil | :not_found
end end
with_them do with_them do
...@@ -95,7 +95,7 @@ RSpec.shared_examples 'create package tag' do |user_type| ...@@ -95,7 +95,7 @@ RSpec.shared_examples 'create package tag' do |user_type|
context 'with invalid package name' do context 'with invalid package name' do
where(:package_name, :status) do where(:package_name, :status) do
'unknown' | :forbidden 'unknown' | :not_found
'' | :not_found '' | :not_found
'%20' | :bad_request '%20' | :bad_request
end end
...@@ -160,7 +160,7 @@ RSpec.shared_examples 'delete package tag' do |user_type| ...@@ -160,7 +160,7 @@ RSpec.shared_examples 'delete package tag' do |user_type|
context 'with invalid package name' do context 'with invalid package name' do
where(:package_name, :status) do where(:package_name, :status) do
'unknown' | :forbidden 'unknown' | :not_found
'' | :not_found '' | :not_found
'%20' | :bad_request '%20' | :bad_request
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