Move all API authentication code to APIGuard

This commit is contained in:
Douwe Maan 2017-09-27 15:56:48 +02:00
parent ad5b96952e
commit 025c6eeaa1
5 changed files with 113 additions and 110 deletions

View file

@ -1,4 +1,6 @@
class OauthAccessToken < Doorkeeper::AccessToken class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User' belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application' belongs_to :application, class_name: 'Doorkeeper::Application'
alias_method :user, :resource_owner
end end

View file

@ -42,6 +42,38 @@ module API
# Helper Methods for Grape Endpoint # Helper Methods for Grape Endpoint
module HelperMethods module HelperMethods
def find_current_user
user =
find_user_from_private_token ||
find_user_from_oauth_token ||
find_user_from_warden
return nil unless user
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
private
def find_user_from_private_token
token_string = private_token.to_s
return nil unless token_string.present?
user =
find_user_by_authentication_token(token_string) ||
find_user_by_personal_access_token(token_string)
raise UnauthorizedError unless user
user
end
# Invokes the doorkeeper guard. # Invokes the doorkeeper guard.
# #
# If token is presented and valid, then it sets @current_user. # If token is presented and valid, then it sets @current_user.
@ -60,9 +92,53 @@ module API
# scopes: (optional) scopes required for this guard. # scopes: (optional) scopes required for this guard.
# Defaults to empty array. # Defaults to empty array.
# #
def doorkeeper_guard(scopes: []) def find_user_from_oauth_token
access_token = find_access_token access_token = find_oauth_access_token
return nil unless access_token return unless access_token
find_user_by_access_token(access_token)
end
def find_user_by_authentication_token(token_string)
User.find_by_authentication_token(token_string)
end
def find_user_by_personal_access_token(token_string)
access_token = PersonalAccessToken.find_by_token(token_string)
return unless access_token
find_user_by_access_token(access_token)
end
# Check the Rails session for valid authentication details
def find_user_from_warden
warden.try(:authenticate) if verified_request?
end
def warden
env['warden']
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
def verified_request?
Gitlab::RequestForgeryProtection.verified?(env)
end
def find_oauth_access_token
return @oauth_access_token if defined?(@oauth_access_token)
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return @oauth_access_token = nil unless token
@oauth_access_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless @oauth_access_token
@oauth_access_token.revoke_previous_refresh_token!
@oauth_access_token
end
def find_user_by_access_token(access_token)
scopes = scopes_registered_for_endpoint
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE when AccessTokenValidationService::INSUFFICIENT_SCOPE
@ -75,55 +151,30 @@ module API
raise RevokedError raise RevokedError
when AccessTokenValidationService::VALID when AccessTokenValidationService::VALID
User.find(access_token.resource_owner_id) access_token.user
end end
end end
def find_user_by_private_token(scopes: [])
token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
return nil unless token_string.present?
user =
find_user_by_authentication_token(token_string) ||
find_user_by_personal_access_token(token_string, scopes)
raise UnauthorizedError unless user
user
end
private
def find_user_by_authentication_token(token_string)
User.find_by_authentication_token(token_string)
end
def find_user_by_personal_access_token(token_string, scopes)
access_token = PersonalAccessToken.active.find_by_token(token_string)
return unless access_token
if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes)
User.find(access_token.user_id)
end
end
def find_access_token
return @access_token if defined?(@access_token)
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return @access_token = nil unless token
@access_token = Doorkeeper::AccessToken.by_token(token)
raise UnauthorizedError unless @access_token
@access_token.revoke_previous_refresh_token!
@access_token
end
def doorkeeper_request def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env) @doorkeeper_request ||= ActionDispatch::Request.new(env)
end end
# An array of scopes that were registered (using `allow_access_with_scope`)
# for the current endpoint class. It also returns scopes registered on
# `API::API`, since these are meant to apply to all API routes.
def scopes_registered_for_endpoint
@scopes_registered_for_endpoint ||=
begin
endpoint_classes = [options[:for].presence, ::API::API].compact
endpoint_classes.reduce([]) do |memo, endpoint|
if endpoint.respond_to?(:allowed_scopes)
memo.concat(endpoint.allowed_scopes)
else
memo
end
end
end
end
end end
module ClassMethods module ClassMethods

View file

@ -3,8 +3,6 @@ module API
include Gitlab::Utils include Gitlab::Utils
include Helpers::Pagination include Helpers::Pagination
UnauthorizedError = Class.new(StandardError)
SUDO_HEADER = "HTTP_SUDO".freeze SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo SUDO_PARAM = :sudo
@ -379,47 +377,16 @@ module API
private private
def private_token
params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
end
def warden
env['warden']
end
# Check if the request is GET/HEAD, or if CSRF token is valid.
def verified_request?
Gitlab::RequestForgeryProtection.verified?(env)
end
# Check the Rails session for valid authentication details
def find_user_from_warden
warden.try(:authenticate) if verified_request?
end
def initial_current_user def initial_current_user
return @initial_current_user if defined?(@initial_current_user) return @initial_current_user if defined?(@initial_current_user)
begin begin
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user } @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
rescue APIGuard::UnauthorizedError, UnauthorizedError rescue APIGuard::UnauthorizedError
unauthorized! unauthorized!
end end
end end
def find_current_user
user =
find_user_by_private_token(scopes: scopes_registered_for_endpoint) ||
doorkeeper_guard(scopes: scopes_registered_for_endpoint) ||
find_user_from_warden
return nil unless user
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
def sudo! def sudo!
return unless sudo_identifier return unless sudo_identifier
return unless initial_current_user return unless initial_current_user
@ -479,22 +446,5 @@ module API
exception.status == 500 exception.status == 500
end end
# An array of scopes that were registered (using `allow_access_with_scope`)
# for the current endpoint class. It also returns scopes registered on
# `API::API`, since these are meant to apply to all API routes.
def scopes_registered_for_endpoint
@scopes_registered_for_endpoint ||=
begin
endpoint_classes = [options[:for].presence, ::API::API].compact
endpoint_classes.reduce([]) do |memo, endpoint|
if endpoint.respond_to?(:allowed_scopes)
memo.concat(endpoint.allowed_scopes)
else
memo
end
end
end
end
end end
end end

View file

@ -222,13 +222,6 @@ describe API::Helpers do
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error /401/
end end
it "returns a 401 response for a token without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/
end
it "leaves user as is when sudo not specified" do it "leaves user as is when sudo not specified" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user) expect(current_user).to eq(user)
@ -238,18 +231,25 @@ describe API::Helpers do
expect(current_user).to eq(user) expect(current_user).to eq(user)
end end
it "does not allow tokens without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
end
it 'does not allow revoked tokens' do it 'does not allow revoked tokens' do
personal_access_token.revoke! personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error API::APIGuard::RevokedError
end end
it 'does not allow expired tokens' do it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago) personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error API::APIGuard::ExpiredError
end end
end end

View file

@ -27,10 +27,10 @@ shared_examples_for 'allows the "read_user" scope' do
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
end end
it 'returns a "401" response' do it 'returns a "403" response' do
get api_call.call(path, user, personal_access_token: token) get api_call.call(path, user, personal_access_token: token)
expect(response).to have_http_status(401) expect(response).to have_http_status(403)
end end
end end
end end
@ -74,10 +74,10 @@ shared_examples_for 'does not allow the "read_user" scope' do
context 'when the requesting token has the "read_user" scope' do context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
it 'returns a "401" response' do it 'returns a "403" response' do
post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3) post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3)
expect(response).to have_http_status(401) expect(response).to have_http_status(403)
end end
end end
end end