Commit ea4ef2b5 authored by Steve Abrams's avatar Steve Abrams Committed by Kamil Trzciński

Enable dependency proxy for private groups

- Add authentication service to return JWT when
users login to the dependency proxy

- Remove restriction of public groups only for
dependency proxy
parent 4be40767
# frozen_string_literal: true
module DependencyProxy
module Auth
extend ActiveSupport::Concern
included do
# We disable `authenticate_user!` since the `DependencyProxy::Auth` performs auth using JWT token
skip_before_action :authenticate_user!, raise: false
prepend_before_action :authenticate_user_from_jwt_token!
end
def authenticate_user_from_jwt_token!
return unless dependency_proxy_for_private_groups?
authenticate_with_http_token do |token, _|
user = user_from_token(token)
sign_in(user) if user
end
request_bearer_token! unless current_user
end
private
def dependency_proxy_for_private_groups?
Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: false)
end
def request_bearer_token!
# unfortunately, we cannot use https://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html#method-i-authentication_request
response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header
render plain: '', status: :unauthorized
end
def user_from_token(token)
token_payload = DependencyProxy::AuthTokenService.decoded_token_payload(token)
User.find(token_payload['user_id'])
rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
nil
end
end
end
# frozen_string_literal: true
module DependencyProxy
module GroupAccess
extend ActiveSupport::Concern
included do
before_action :verify_dependency_proxy_enabled!
before_action :authorize_read_dependency_proxy!
end
private
def verify_dependency_proxy_enabled!
render_404 unless group.dependency_proxy_feature_available?
end
def authorize_read_dependency_proxy!
access_denied! unless can?(current_user, :read_dependency_proxy, group)
end
def authorize_admin_dependency_proxy!
access_denied! unless can?(current_user, :admin_dependency_proxy, group)
end
end
end
# frozen_string_literal: true
module DependencyProxyAccess
extend ActiveSupport::Concern
included do
before_action :verify_dependency_proxy_enabled!
before_action :authorize_read_dependency_proxy!
end
private
def verify_dependency_proxy_enabled!
render_404 unless group.dependency_proxy_feature_available?
end
def authorize_read_dependency_proxy!
access_denied! unless can?(current_user, :read_dependency_proxy, group)
end
def authorize_admin_dependency_proxy!
access_denied! unless can?(current_user, :admin_dependency_proxy, group)
end
end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Groups module Groups
class DependencyProxiesController < Groups::ApplicationController class DependencyProxiesController < Groups::ApplicationController
include DependencyProxyAccess include DependencyProxy::GroupAccess
before_action :authorize_admin_dependency_proxy!, only: :update before_action :authorize_admin_dependency_proxy!, only: :update
before_action :dependency_proxy before_action :dependency_proxy
......
# frozen_string_literal: true
class Groups::DependencyProxyAuthController < ApplicationController
include DependencyProxy::Auth
feature_category :dependency_proxy
def authenticate
render plain: '', status: :ok
end
end
# frozen_string_literal: true # frozen_string_literal: true
class Groups::DependencyProxyForContainersController < Groups::ApplicationController class Groups::DependencyProxyForContainersController < Groups::ApplicationController
include DependencyProxyAccess include DependencyProxy::Auth
include DependencyProxy::GroupAccess
include SendFileUpload include SendFileUpload
before_action :ensure_token_granted! before_action :ensure_token_granted!
...@@ -9,7 +10,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro ...@@ -9,7 +10,7 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
attr_reader :token attr_reader :token
feature_category :package_registry feature_category :dependency_proxy
def manifest def manifest
result = DependencyProxy::PullManifestService.new(image, tag, token).execute result = DependencyProxy::PullManifestService.new(image, tag, token).execute
......
...@@ -11,7 +11,8 @@ class JwtController < ApplicationController ...@@ -11,7 +11,8 @@ class JwtController < ApplicationController
feature_category :authentication_and_authorization feature_category :authentication_and_authorization
SERVICES = { SERVICES = {
Auth::ContainerRegistryAuthenticationService::AUDIENCE => Auth::ContainerRegistryAuthenticationService ::Auth::ContainerRegistryAuthenticationService::AUDIENCE => ::Auth::ContainerRegistryAuthenticationService,
::Auth::DependencyProxyAuthenticationService::AUDIENCE => ::Auth::DependencyProxyAuthenticationService
}.freeze }.freeze
def auth def auth
......
# frozen_string_literal: true # frozen_string_literal: true
class DependencyProxy::Registry class DependencyProxy::Registry
AUTH_URL = 'https://auth.docker.io'.freeze AUTH_URL = 'https://auth.docker.io'
LIBRARY_URL = 'https://registry-1.docker.io/v2'.freeze LIBRARY_URL = 'https://registry-1.docker.io/v2'
PROXY_AUTH_URL = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, "jwt/auth")
class << self class << self
def auth_url(image) def auth_url(image)
...@@ -17,6 +18,10 @@ class DependencyProxy::Registry ...@@ -17,6 +18,10 @@ class DependencyProxy::Registry
"#{LIBRARY_URL}/#{image_path(image)}/blobs/#{blob_sha}" "#{LIBRARY_URL}/#{image_path(image)}/blobs/#{blob_sha}"
end end
def authenticate_header
"Bearer realm=\"#{PROXY_AUTH_URL}\",service=\"#{::Auth::DependencyProxyAuthenticationService::AUDIENCE}\""
end
private private
def image_path(image) def image_path(image)
......
# frozen_string_literal: true
module Auth
class DependencyProxyAuthenticationService < BaseService
AUDIENCE = 'dependency_proxy'
HMAC_KEY = 'gitlab-dependency-proxy'
DEFAULT_EXPIRE_TIME = 1.minute
def execute(authentication_abilities:)
return error('dependency proxy not enabled', 404) unless ::Gitlab.config.dependency_proxy.enabled
return error('access forbidden', 403) unless current_user
{ token: authorized_token.encoded }
end
class << self
include ::Gitlab::Utils::StrongMemoize
def secret
strong_memoize(:secret) do
OpenSSL::HMAC.hexdigest(
'sha256',
::Settings.attr_encrypted_db_key_base,
HMAC_KEY
)
end
end
def token_expire_at
Time.current + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes
end
end
private
def authorized_token
JSONWebToken::HMACToken.new(self.class.secret).tap do |token|
token['user_id'] = current_user.id
token.expire_time = self.class.token_expire_at
end
end
end
end
# frozen_string_literal: true
module DependencyProxy
class AuthTokenService < DependencyProxy::BaseService
attr_reader :token
def initialize(token)
@token = token
end
def execute
JSONWebToken::HMACToken.decode(token, ::Auth::DependencyProxyAuthenticationService.secret).first
end
class << self
def decoded_token_payload(token)
self.new(token).execute
end
end
end
end
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/packages/dependency_proxy/index') } - link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('user/packages/dependency_proxy/index') }
= _('Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } = _('Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- if @group.public? - if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: false) || @group.public?
- if can?(current_user, :admin_dependency_proxy, @group) - if can?(current_user, :admin_dependency_proxy, @group)
= form_for(@dependency_proxy, method: :put, url: group_dependency_proxy_path(@group)) do |f| = form_for(@dependency_proxy, method: :put, url: group_dependency_proxy_path(@group)) do |f|
.form-group .form-group
......
---
name: dependency_proxy_for_private_groups
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46042
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/276777
milestone: '13.7'
type: development
group: group::package
default_enabled: false
...@@ -125,7 +125,7 @@ end ...@@ -125,7 +125,7 @@ end
# Dependency proxy for containers # Dependency proxy for containers
# Because docker adds v2 prefix to URI this need to be outside of usual group routes # Because docker adds v2 prefix to URI this need to be outside of usual group routes
scope format: false do scope format: false do
get 'v2', to: proc { [200, {}, ['']] } # rubocop:disable Cop/PutGroupRoutesUnderScope get 'v2' => 'groups/dependency_proxy_auth#authenticate' # rubocop:disable Cop/PutGroupRoutesUnderScope
constraints image: Gitlab::PathRegex.container_image_regex, sha: Gitlab::PathRegex.container_image_blob_sha_regex do constraints image: Gitlab::PathRegex.container_image_regex, sha: Gitlab::PathRegex.container_image_blob_sha_regex do
get 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag' => 'groups/dependency_proxy_for_containers#manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope get 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag' => 'groups/dependency_proxy_for_containers#manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope
......
...@@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w ...@@ -8,6 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
> - [Support for private groups](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
> - Anonymous access to images in public groups is no longer available starting in [GitLab Premium](https://about.gitlab.com/pricing/) 13.7.
The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed
upstream images. upstream images.
...@@ -17,9 +19,7 @@ upstream image from a registry, acting as a pull-through cache. ...@@ -17,9 +19,7 @@ upstream image from a registry, acting as a pull-through cache.
## Prerequisites ## Prerequisites
To use the Dependency Proxy: The Dependency Proxy must be [enabled by an administrator](../../../administration/packages/dependency_proxy.md).
- Your group must be public. Authentication for private groups is [not supported yet](https://gitlab.com/gitlab-org/gitlab/-/issues/11582).
### Supported images and packages ### Supported images and packages
...@@ -58,6 +58,56 @@ Prerequisites: ...@@ -58,6 +58,56 @@ Prerequisites:
- Docker Hub must be available. Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/241639) - Docker Hub must be available. Follow [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/241639)
for progress on accessing images when Docker Hub is down. for progress on accessing images when Docker Hub is down.
### Authenticate with the Dependency Proxy
Because the Dependency Proxy is storing Docker images in a space associated with your group,
you must authenticate against the Dependency Proxy.
Follow the [instructions for using images from a private registry](../../../ci/docker/using_docker_images.md#define-an-image-from-a-private-container-registry),
but instead of using `registry.example.com:5000`, use your GitLab domain with no port `gitlab.example.com`.
For example, to manually log in:
```shell
docker login gitlab.example.com --username my_username --password my_password
```
You can authenticate using:
- Your GitLab username and password.
- A [personal access token](../../../user/profile/personal_access_tokens.md) with the scope set to `read_registry` and `write_registry`.
#### Authenticate within CI/CD
To work with the Dependency Proxy in [GitLab CI/CD](../../../ci/README.md), you can use
`CI_REGISTRY_USER` and `CI_REGISTRY_PASSWORD`.
```shell
docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" gitlab.example.com
```
You can use other [predefined variables](../../../ci/variables/predefined_variables.md)
to further generalize your CI script. For example:
```yaml
# .gitlab-ci.yml
dependency-proxy-pull-master:
# Official docker image.
image: docker:latest
stage: build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_SERVER_HOST":"$CI_SERVER_PORT"
script:
- docker pull "$CI_SERVER_HOST":"$CI_SERVER_PORT"/groupname/dependency_proxy/containers/alpine:latest
```
You can also use [custom environment variables](../../../ci/variables/README.md#custom-environment-variables) to store and access your personal access token or other valid credentials.
### Store a Docker image in Dependency Proxy cache
To store a Docker image in Dependency Proxy storage: To store a Docker image in Dependency Proxy storage:
1. Go to your group's **Packages & Registries > Dependency Proxy**. 1. Go to your group's **Packages & Registries > Dependency Proxy**.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::DependencyProxyAuthController do
include DependencyProxyHelpers
describe 'GET #authenticate' do
subject { get :authenticate }
context 'feature flag disabled' do
before do
stub_feature_flags(dependency_proxy_for_private_groups: false)
end
it 'returns successfully', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:success)
end
end
context 'without JWT' do
it 'returns unauthorized with oauth realm', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.headers['WWW-Authenticate']).to eq DependencyProxy::Registry.authenticate_header
end
end
context 'with valid JWT' do
let_it_be(:user) { create(:user) }
let(:jwt) { build_jwt(user) }
let(:token_header) { "Bearer #{jwt.encoded}" }
before do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:success) }
end
context 'with invalid JWT' do
context 'bad user' do
let(:jwt) { build_jwt(double('bad_user', id: 999)) }
let(:token_header) { "Bearer #{jwt.encoded}" }
before do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'token with no user id' do
let(:token_header) { "Bearer #{build_jwt.encoded}" }
before do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'expired token' do
let_it_be(:user) { create(:user) }
let(:jwt) { build_jwt(user, expire_time: Time.zone.now - 1.hour) }
let(:token_header) { "Bearer #{jwt.encoded}" }
before do
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
end
end
end
...@@ -3,8 +3,77 @@ ...@@ -3,8 +3,77 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Groups::DependencyProxyForContainersController do RSpec.describe Groups::DependencyProxyForContainersController do
include HttpBasicAuthHelpers
include DependencyProxyHelpers
let_it_be(:user) { create(:user) }
let(:group) { create(:group) } let(:group) { create(:group) }
let(:token_response) { { status: :success, token: 'abcd1234' } } let(:token_response) { { status: :success, token: 'abcd1234' } }
let(:jwt) { build_jwt(user) }
let(:token_header) { "Bearer #{jwt.encoded}" }
shared_examples 'without a token' do
before do
request.headers['HTTP_AUTHORIZATION'] = nil
end
context 'feature flag disabled' do
before do
stub_feature_flags(dependency_proxy_for_private_groups: false)
end
it { is_expected.to have_gitlab_http_status(:ok) }
end
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
shared_examples 'feature flag disabled with private group' do
before do
stub_feature_flags(dependency_proxy_for_private_groups: false)
end
it 'redirects', :aggregate_failures do
group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
subject
expect(response).to have_gitlab_http_status(:redirect)
expect(response.location).to end_with(new_user_session_path)
end
end
shared_examples 'without permission' do
context 'with invalid user' do
before do
user = double('bad_user', id: 999)
token_header = "Bearer #{build_jwt(user).encoded}"
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with valid user that does not have access' do
let(:group) { create(:group, :private) }
before do
user = double('bad_user', id: 999)
token_header = "Bearer #{build_jwt(user).encoded}"
request.headers['HTTP_AUTHORIZATION'] = token_header
end
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'when user is not found' do
before do
allow(User).to receive(:find).and_return(nil)
end
it { is_expected.to have_gitlab_http_status(:unauthorized) }
end
end
shared_examples 'not found when disabled' do shared_examples 'not found when disabled' do
context 'feature disabled' do context 'feature disabled' do
...@@ -27,6 +96,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -27,6 +96,8 @@ RSpec.describe Groups::DependencyProxyForContainersController do
allow_next_instance_of(DependencyProxy::RequestTokenService) do |instance| allow_next_instance_of(DependencyProxy::RequestTokenService) do |instance|
allow(instance).to receive(:execute).and_return(token_response) allow(instance).to receive(:execute).and_return(token_response)
end end
request.headers['HTTP_AUTHORIZATION'] = token_header
end end
describe 'GET #manifest' do describe 'GET #manifest' do
...@@ -46,6 +117,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -46,6 +117,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
enable_dependency_proxy enable_dependency_proxy
end end
it_behaves_like 'without a token'
it_behaves_like 'without permission'
it_behaves_like 'feature flag disabled with private group'
context 'remote token request fails' do context 'remote token request fails' do
let(:token_response) do let(:token_response) do
{ {
...@@ -113,6 +188,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do ...@@ -113,6 +188,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
enable_dependency_proxy enable_dependency_proxy
end end
it_behaves_like 'without a token'
it_behaves_like 'without permission'
it_behaves_like 'feature flag disabled with private group'
context 'remote blob request fails' do context 'remote blob request fails' do
let(:blob_response) do let(:blob_response) do
{ {
......
...@@ -79,6 +79,11 @@ RSpec.describe 'Group Dependency Proxy' do ...@@ -79,6 +79,11 @@ RSpec.describe 'Group Dependency Proxy' do
sign_in(developer) sign_in(developer)
end end
context 'feature flag is disabled' do
before do
stub_feature_flags(dependency_proxy_for_private_groups: false)
end
context 'group is private' do context 'group is private' do
let(:group) { create(:group, :private) } let(:group) { create(:group, :private) }
...@@ -88,6 +93,7 @@ RSpec.describe 'Group Dependency Proxy' do ...@@ -88,6 +93,7 @@ RSpec.describe 'Group Dependency Proxy' do
expect(page).to have_content('Dependency proxy feature is limited to public groups for now.') expect(page).to have_content('Dependency proxy feature is limited to public groups for now.')
end end
end end
end
context 'feature is disabled globally' do context 'feature is disabled globally' do
it 'renders 404 page' do it 'renders 404 page' do
......
...@@ -54,4 +54,11 @@ RSpec.describe DependencyProxy::Registry, type: :model do ...@@ -54,4 +54,11 @@ RSpec.describe DependencyProxy::Registry, type: :model do
end end
end end
end end
describe '#authenticate_header' do
it 'returns the OAuth realm and service header' do
expect(described_class.authenticate_header)
.to eq("Bearer realm=\"#{Gitlab.config.gitlab.url}/jwt/auth\",service=\"dependency_proxy\"")
end
end
end end
...@@ -5,13 +5,13 @@ require 'spec_helper' ...@@ -5,13 +5,13 @@ require 'spec_helper'
RSpec.describe JwtController do RSpec.describe JwtController do
include_context 'parsed logs' include_context 'parsed logs'
let(:service) { double(execute: {}) } let(:service) { double(execute: {} ) }
let(:service_class) { double(new: service) } let(:service_class) { Auth::ContainerRegistryAuthenticationService }
let(:service_name) { 'test' } let(:service_name) { 'container_registry' }
let(:parameters) { { service: service_name } } let(:parameters) { { service: service_name } }
before do before do
stub_const('JwtController::SERVICES', service_name => service_class) allow(service_class).to receive(:new).and_return(service)
end end
shared_examples 'user logging' do shared_examples 'user logging' do
...@@ -22,6 +22,7 @@ RSpec.describe JwtController do ...@@ -22,6 +22,7 @@ RSpec.describe JwtController do
end end
end end
context 'authenticating against container registry' do
context 'existing service' do context 'existing service' do
subject! { get '/jwt/auth', params: parameters } subject! { get '/jwt/auth', params: parameters }
...@@ -212,6 +213,77 @@ RSpec.describe JwtController do ...@@ -212,6 +213,77 @@ RSpec.describe JwtController do
it { expect(response).to have_gitlab_http_status(:not_found) } it { expect(response).to have_gitlab_http_status(:not_found) }
end end
def credentials(login, password)
ActionController::HttpAuthentication::Basic.encode_credentials(login, password)
end
end
context 'authenticating against dependency proxy' do
let_it_be(:user) { create(:user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :private, group: group) }
let_it_be(:group_deploy_token) { create(:deploy_token, :group, groups: [group]) }
let_it_be(:project_deploy_token) { create(:deploy_token, :project, projects: [project]) }
let_it_be(:service_name) { 'dependency_proxy' }
let(:headers) { { authorization: credentials(credential_user, credential_password) } }
let(:params) { { account: credential_user, client_id: 'docker', offline_token: true, service: service_name } }
before do
stub_config(dependency_proxy: { enabled: true })
end
subject { get '/jwt/auth', params: params, headers: headers }
shared_examples 'with valid credentials' do
it 'returns token successfully' do
subject
expect(response).to have_gitlab_http_status(:success)
expect(json_response['token']).to be_present
end
end
context 'with personal access token' do
let(:credential_user) { nil }
let(:credential_password) { personal_access_token.token }
it_behaves_like 'with valid credentials'
end
context 'with user credentials token' do
let(:credential_user) { user.username }
let(:credential_password) { user.password }
it_behaves_like 'with valid credentials'
end
context 'with group deploy token' do
let(:credential_user) { group_deploy_token.username }
let(:credential_password) { group_deploy_token.token }
it_behaves_like 'with valid credentials'
end
context 'with project deploy token' do
let(:credential_user) { project_deploy_token.username }
let(:credential_password) { project_deploy_token.token }
it_behaves_like 'with valid credentials'
end
context 'with invalid credentials' do
let(:credential_user) { 'foo' }
let(:credential_password) { 'bar' }
it 'returns unauthorized' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
def credentials(login, password) def credentials(login, password)
ActionController::HttpAuthentication::Basic.encode_credentials(login, password) ActionController::HttpAuthentication::Basic.encode_credentials(login, password)
end end
......
...@@ -81,6 +81,10 @@ RSpec.describe "Groups", "routing" do ...@@ -81,6 +81,10 @@ RSpec.describe "Groups", "routing" do
end end
describe 'dependency proxy for containers' do describe 'dependency proxy for containers' do
it 'routes to #authenticate' do
expect(get('/v2')).to route_to('groups/dependency_proxy_auth#authenticate')
end
context 'image name without namespace' do context 'image name without namespace' do
it 'routes to #manifest' do it 'routes to #manifest' do
expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6')) expect(get('/v2/gitlabhq/dependency_proxy/containers/ruby/manifests/2.3.6'))
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Auth::DependencyProxyAuthenticationService do
let_it_be(:user) { create(:user) }
let(:service) { Auth::DependencyProxyAuthenticationService.new(nil, user) }
before do
stub_config(dependency_proxy: { enabled: true })
end
describe '#execute' do
subject { service.execute(authentication_abilities: nil) }
context 'dependency proxy is not enabled' do
before do
stub_config(dependency_proxy: { enabled: false })
end
it 'returns not found' do
result = subject
expect(result[:http_status]).to eq(404)
expect(result[:message]).to eq('dependency proxy not enabled')
end
end
context 'without a user' do
let(:user) { nil }
it 'returns forbidden' do
result = subject
expect(result[:http_status]).to eq(403)
expect(result[:message]).to eq('access forbidden')
end
end
context 'with a user' do
it 'returns a token' do
expect(subject[:token]).not_to be_nil
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DependencyProxy::AuthTokenService do
include DependencyProxyHelpers
describe '.decoded_token_payload' do
let_it_be(:user) { create(:user) }
let_it_be(:token) { build_jwt(user) }
subject { described_class.decoded_token_payload(token.encoded) }
it 'returns the user' do
result = subject
expect(result['user_id']).to eq(user.id)
end
it 'raises an error if the token is expired' do
travel_to(Time.zone.now + Auth::DependencyProxyAuthenticationService.token_expire_at + 1.minute) do
expect { subject }.to raise_error(JWT::ExpiredSignature)
end
end
it 'raises an error if decoding fails' do
allow(JWT).to receive(:decode).and_raise(JWT::DecodeError)
expect { subject }.to raise_error(JWT::DecodeError)
end
it 'raises an error if signature is immature' do
allow(JWT).to receive(:decode).and_raise(JWT::ImmatureSignature)
expect { subject }.to raise_error(JWT::ImmatureSignature)
end
end
end
...@@ -25,6 +25,13 @@ module DependencyProxyHelpers ...@@ -25,6 +25,13 @@ module DependencyProxyHelpers
.to_return(status: status, body: body) .to_return(status: status, body: body)
end end
def build_jwt(user = nil, expire_time: nil)
JSONWebToken::HMACToken.new(::Auth::DependencyProxyAuthenticationService.secret).tap do |jwt|
jwt['user_id'] = user.id if user
jwt.expire_time = expire_time || jwt.issued_at + 1.minute
end
end
private private
def registry def registry
......
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