Commit 0b81b5ac authored by Z.J. van de Weg's avatar Z.J. van de Weg
Browse files

Create read_registry scope with JWT auth

This is the first commit doing mainly 3 things:
1. create a new scope and allow users to use it
2. Have the JWTController respond correctly on this
3. Updates documentation to suggest usage of PATs

There is one gotcha, there will be no support for impersonation tokens, as this
seems not needed.

Fixes gitlab-org/gitlab-ce#19219
parent cfaf45c4a5a7
......@@ -20,13 +20,15 @@ def auth
private
def authenticate_project_or_user
@authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_authentication_abilities)
@authentication_result = Gitlab::Auth::Result.new(nil, nil, :none, Gitlab::Auth.read_api_abilities)
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
render_unauthorized unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
if @authentication_result.failed? ||
(@authentication_result.actor.present? && !@authentication_result.actor.is_a?(User))
render_unauthorized
end
end
rescue Gitlab::Auth::MissingPersonalTokenError
render_missing_personal_token
......
......@@ -38,7 +38,7 @@ def personal_access_token_params
end
def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES
@scopes = Gitlab::Auth::AVAILABLE_SCOPES
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
......
......@@ -15,11 +15,10 @@ class PersonalAccessToken < ActiveRecord::Base
scope :without_impersonation, -> { where(impersonation: false) }
validates :scopes, presence: true
validate :validate_api_scopes
validate :validate_scopes
def revoke!
self.revoked = true
self.save
update!(revoked: true)
end
def active?
......@@ -28,9 +27,9 @@ def active?
protected
def validate_api_scopes
unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) }
errors.add :scopes, "can only contain API scopes"
def validate_scopes
unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes"
end
end
end
---
title: Allow pulling of container images using personal access tokens
merge_request: 11845
author:
......@@ -106,12 +106,14 @@ Make sure that your GitLab Runner is configured to allow building Docker images
following the [Using Docker Build](../../ci/docker/using_docker_build.md)
and [Using the GitLab Container Registry documentation](../../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
## Limitations
## Using with private projects
In order to use a container image from your private project as an `image:` in
your `.gitlab-ci.yml`, you have to follow the
[Using a private Docker Registry][private-docker]
documentation. This workflow will be simplified in the future.
If a project is private, credentials will need to be provided for authorization.
The preferred way to do this, is by using personal access tokens, which can be
created under `/profile/personal_access_tokens`. The minimal scope needed is:
`read_registry`.
This feature was introduced in GitLab 9.3.
## Troubleshooting the GitLab Container Registry
......@@ -257,4 +259,3 @@ Once the right permissions were set, the error will go away.
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
......@@ -2,6 +2,8 @@ module Gitlab
module Auth
MissingPersonalTokenError = Class.new(StandardError)
REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
API_SCOPES = [:api, :read_user].freeze
......@@ -11,8 +13,10 @@ module Auth
# Default scopes for OAuth applications that don't define their own
DEFAULT_SCOPES = [:api].freeze
AVAILABLE_SCOPES = (API_SCOPES + REGISTRY_SCOPES).freeze
# Other available scopes
OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
OPTIONAL_SCOPES = (AVAILABLE_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
class << self
def find_for_git_client(login, password, project:, ip:)
......@@ -26,8 +30,8 @@ def find_for_git_client(login, password, project:, ip:)
build_access_token_check(login, password) ||
lfs_token_check(login, password) ||
oauth_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
personal_access_token_check(password) ||
user_with_password_for_git(login, password) ||
Gitlab::Auth::Result.new
rate_limit!(ip, success: result.success?, login: login)
......@@ -103,15 +107,16 @@ def user_with_password_for_git(login, password)
raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)
Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_api_abilities)
end
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
if valid_oauth_token?(token)
user = User.find_by(id: token.resource_owner_id)
Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)
Gitlab::Auth::Result.new(user, nil, :oauth, full_api_abilities)
end
end
end
......@@ -121,17 +126,26 @@ def personal_access_token_check(password)
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
if token && valid_api_token?(token)
Gitlab::Auth::Result.new(token.user, nil, :personal_token, full_authentication_abilities)
if token && valid_scoped_token?(token, scopes: AVAILABLE_SCOPES.map(&:to_s))
Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes))
end
end
def valid_oauth_token?(token)
token && token.accessible? && valid_api_token?(token)
token && token.accessible? && valid_scoped_token?(token)
end
def valid_api_token?(token)
AccessTokenValidationService.new(token).include_any_scope?(['api'])
def valid_scoped_token?(token, scopes: %w[api])
AccessTokenValidationService.new(token).include_any_scope?(scopes)
end
def abilities_for_scope(scopes)
abilities = Set.new
abilities.merge(full_api_abilities) if scopes.include?("api")
abilities << :read_container_image if scopes.include?("read_registry")
abilities.to_a
end
def lfs_token_check(login, password)
......@@ -150,9 +164,9 @@ def lfs_token_check(login, password)
authentication_abilities =
if token_handler.user?
full_authentication_abilities
full_api_abilities
else
read_authentication_abilities
read_api_abilities
end
if Devise.secure_compare(token_handler.token, password)
......@@ -188,7 +202,7 @@ def build_authentication_abilities
]
end
def read_authentication_abilities
def read_api_abilities
[
:read_project,
:download_code,
......@@ -196,8 +210,8 @@ def read_authentication_abilities
]
end
def full_authentication_abilities
read_authentication_abilities + [
def full_api_abilities
read_api_abilities + [
:push_code,
:create_container_image
]
......
......@@ -15,6 +15,10 @@ def lfs_deploy_token?(for_project)
def success?
actor.present? || type == :ci
end
def failed?
!success?
end
end
end
end
......@@ -143,6 +143,13 @@ def operation
expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities))
end
it 'succeeds for personal access tokens with the `read_registry` scope' do
personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image]))
end
it 'succeeds if it is an impersonation token' do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
......
......@@ -35,6 +35,16 @@
end
end
describe 'revoke!' do
let(:active_personal_access_token) { create(:personal_access_token) }
it 'revokes the token' do
active_personal_access_token.revoke!
expect(active_personal_access_token.revoked?).to be true
end
end
context "validations" do
let(:personal_access_token) { build(:personal_access_token) }
......@@ -51,11 +61,17 @@
expect(personal_access_token).to be_valid
end
it "rejects creating a token with non-API scopes" do
it "allows creating a token with read_registry scope" do
personal_access_token.scopes = [:read_registry]
expect(personal_access_token).to be_valid
end
it "rejects creating a token with unavailable scopes" do
personal_access_token.scopes = [:openid, :api]
expect(personal_access_token).not_to be_valid
expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes"
expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes"
end
end
end
......@@ -41,6 +41,19 @@
it { expect(response).to have_http_status(401) }
end
context 'using personal access tokens' do
let(:user) { create(:user) }
let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) }
let(:headers) { { authorization: credentials('personal_access_token', pat.token) } }
subject! { get '/jwt/auth', parameters, headers }
it 'authenticates correctly' do
expect(response).to have_http_status(200)
expect(service_class).to have_received(:new).with(nil, user, parameters)
end
end
end
context 'using User login' do
......@@ -89,7 +102,7 @@
end
it 'allows read access' do
expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities)
expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_api_abilities)
get '/jwt/auth', parameters
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