Commit 5544f203 authored by James Lopez's avatar James Lopez

Merge branch '213566-deploy-token-npm' into 'master'

Enable NPM packages with deploy token access

See merge request gitlab-org/gitlab!31264
parents ad21ea7e e748c2a2
---
title: Enable deploy token authentication for the NPM registry
merge_request: 31264
author:
type: added
...@@ -100,14 +100,15 @@ configure GitLab as a remote registry. ...@@ -100,14 +100,15 @@ configure GitLab as a remote registry.
If a project is private or you want to upload an NPM package to GitLab, If a project is private or you want to upload an NPM package to GitLab,
credentials will need to be provided for authentication. [Personal access tokens](../../profile/personal_access_tokens.md) credentials will need to be provided for authentication. [Personal access tokens](../../profile/personal_access_tokens.md)
and [deploy tokens](../../project/deploy_tokens/index.md)
are preferred, but support is available for [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow). are preferred, but support is available for [OAuth tokens](../../../api/oauth2.md#resource-owner-password-credentials-flow).
CAUTION: **2FA is only supported with personal access tokens:** CAUTION: **Two-factor authentication (2FA) is only supported with personal access tokens:**
If you have 2FA enabled, you need to use a [personal access token](../../profile/personal_access_tokens.md) with OAuth headers with the scope set to `api`. Standard OAuth tokens won't be able to authenticate to the GitLab NPM Registry. If you have 2FA enabled, you need to use a [personal access token](../../profile/personal_access_tokens.md) with OAuth headers with the scope set to `api` or a [deploy token](../../project/deploy_tokens/index.md) with `read_package_registry` or `write_package_registry` scopes. Standard OAuth tokens won't be able to authenticate to the GitLab NPM Registry.
### Authenticating with a personal access token ### Authenticating with a personal access token or deploy token
To authenticate with a [personal access token](../../profile/personal_access_tokens.md), To authenticate with a [personal access token](../../profile/personal_access_tokens.md) or [deploy token](../../project/deploy_tokens/index.md),
set your NPM configuration: set your NPM configuration:
```shell ```shell
...@@ -125,7 +126,7 @@ npm config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_au ...@@ -125,7 +126,7 @@ npm config set '//gitlab.com/api/v4/projects/<your_project_id>/packages/npm/:_au
``` ```
Replace `<your_project_id>` with your project ID which can be found on the home page Replace `<your_project_id>` with your project ID which can be found on the home page
of your project and `<your_token>` with your personal access token. of your project and `<your_token>` with your personal access token or deploy token.
If you have a self-managed GitLab installation, replace `gitlab.com` with your If you have a self-managed GitLab installation, replace `gitlab.com` with your
domain name. domain name.
...@@ -160,7 +161,7 @@ Then, you could run `npm publish` either locally or via GitLab CI/CD: ...@@ -160,7 +161,7 @@ Then, you could run `npm publish` either locally or via GitLab CI/CD:
> [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.
If you’re using NPM with GitLab CI/CD, a CI job token can be used instead of a personal access 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 will inherit the permissions of the user that generates the pipeline. The token will inherit the permissions of the user that generates the pipeline.
Add a corresponding section to your `.npmrc` file: Add a corresponding section to your `.npmrc` file:
...@@ -286,7 +287,7 @@ page. ...@@ -286,7 +287,7 @@ page.
## Publishing a package with CI/CD ## Publishing a package with CI/CD
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 in your commands. `CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
A simple example `.gitlab-ci.yml` file for publishing NPM packages: A simple example `.gitlab-ci.yml` file for publishing NPM packages:
...@@ -323,7 +324,7 @@ info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation abo ...@@ -323,7 +324,7 @@ info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation abo
``` ```
In this case, try adding this to your `.npmrc` file (and replace `<your_token>` In this case, try adding this to your `.npmrc` file (and replace `<your_token>`
with your personal access token): with your personal access token or deploy token):
```text ```text
//gitlab.com/api/v4/projects/:_authToken=<your_token> //gitlab.com/api/v4/projects/:_authToken=<your_token>
......
...@@ -106,7 +106,7 @@ module API ...@@ -106,7 +106,7 @@ module API
params do params do
requires :package_name, type: String, desc: 'Package name' requires :package_name, type: String, desc: 'Package name'
end end
route_setting :authentication, job_token_allowed: true route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get 'packages/npm/*package_name', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do get 'packages/npm/*package_name', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do
package_name = params[:package_name] package_name = params[:package_name]
...@@ -137,7 +137,7 @@ module API ...@@ -137,7 +137,7 @@ module API
requires :package_name, type: String, desc: 'Package name' requires :package_name, type: String, desc: 'Package name'
requires :file_name, type: String, desc: 'Package file name' requires :file_name, type: String, desc: 'Package file name'
end end
route_setting :authentication, job_token_allowed: true route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
get ':id/packages/npm/*package_name/-/*file_name', format: false do get ':id/packages/npm/*package_name/-/*file_name', format: false do
authorize_read_package!(user_project) authorize_read_package!(user_project)
...@@ -159,7 +159,7 @@ module API ...@@ -159,7 +159,7 @@ module API
requires :package_name, type: String, desc: 'Package name' requires :package_name, type: String, desc: 'Package name'
requires :versions, type: Hash, desc: 'Package version info' requires :versions, type: Hash, desc: 'Package version info'
end end
route_setting :authentication, job_token_allowed: true route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do
authorize_create_package!(user_project) authorize_create_package!(user_project)
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe API::NpmPackages do describe API::NpmPackages do
include EE::PackagesManagerApiSpecHelpers
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) } let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
...@@ -10,6 +12,8 @@ describe API::NpmPackages do ...@@ -10,6 +12,8 @@ describe API::NpmPackages do
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) } 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(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job) { create(:ci_build, user: user) } let_it_be(:job) { create(:ci_build, user: user) }
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 before do
project.add_developer(user) project.add_developer(user)
...@@ -34,6 +38,12 @@ describe API::NpmPackages do ...@@ -34,6 +38,12 @@ describe API::NpmPackages do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
it 'returns the package info with deploy token' do
get_package_with_deploy_token(package)
expect_a_valid_package_response
end
end end
describe 'GET /api/v4/packages/npm/*package_name' do describe 'GET /api/v4/packages/npm/*package_name' do
...@@ -132,8 +142,8 @@ describe API::NpmPackages do ...@@ -132,8 +142,8 @@ describe API::NpmPackages do
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
end end
def get_package(package, params = {}) def get_package(package, params = {}, headers = {})
get api("/packages/npm/#{package.name}"), params: params get api("/packages/npm/#{package.name}"), params: params, headers: headers
end end
def get_package_with_token(package, params = {}) def get_package_with_token(package, params = {})
...@@ -143,6 +153,10 @@ describe API::NpmPackages do ...@@ -143,6 +153,10 @@ describe API::NpmPackages do
def get_package_with_job_token(package, params = {}) def get_package_with_job_token(package, params = {})
get_package(package, params.merge(job_token: job.token)) get_package(package, params.merge(job_token: job.token))
end 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 describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
......
...@@ -107,9 +107,12 @@ module Gitlab ...@@ -107,9 +107,12 @@ module Gitlab
def deploy_token_from_request def deploy_token_from_request
return unless route_authentication_setting[:deploy_token_allowed] return unless route_authentication_setting[:deploy_token_allowed]
token = current_request.env[DEPLOY_TOKEN_HEADER].presence token = current_request.env[DEPLOY_TOKEN_HEADER].presence || parsed_oauth_token
DeployToken.active.find_by_token(token) deploy_token = DeployToken.active.find_by_token(token)
@current_authenticated_deploy_token = deploy_token # rubocop:disable Gitlab/ModuleWithInstanceVariables
deploy_token
end end
def find_runner_from_token def find_runner_from_token
...@@ -122,6 +125,9 @@ module Gitlab ...@@ -122,6 +125,9 @@ module Gitlab
end end
def validate_access_token!(scopes: []) def validate_access_token!(scopes: [])
# return early if we've already authenticated via a deploy token
return if @current_authenticated_deploy_token.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
return unless access_token return unless access_token
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
......
...@@ -220,6 +220,24 @@ describe Gitlab::Auth::AuthFinders do ...@@ -220,6 +220,24 @@ describe Gitlab::Auth::AuthFinders do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
end end
context 'with oauth headers' do
before do
set_header('HTTP_AUTHORIZATION', "Bearer #{deploy_token.token}")
end
it { is_expected.to eq deploy_token }
it_behaves_like 'an unauthenticated route'
context 'with invalid token' do
before do
set_header('HTTP_AUTHORIZATION', "Bearer invalid_token")
end
it { is_expected.to be_nil }
end
end
end end
describe '#find_user_from_access_token' do describe '#find_user_from_access_token' do
......
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