Commit 3f24f9ed authored by Douwe Maan's avatar Douwe Maan

Add sudo API scope

parent a1781a49
...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController ...@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES @scopes = Gitlab::Auth.available_scopes(current_user)
@impersonation_token ||= finder.build @impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute @inactive_impersonation_tokens = finder(state: 'inactive').execute
......
...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -39,7 +39,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end end
def set_index_vars def set_index_vars
@scopes = Gitlab::Auth.available_scopes @scopes = Gitlab::Auth.available_scopes(current_user)
@inactive_personal_access_tokens = finder(state: 'inactive').execute @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
......
...@@ -39,11 +39,8 @@ class AccessTokenValidationService ...@@ -39,11 +39,8 @@ class AccessTokenValidationService
token_scopes = token.scopes.map(&:to_sym) token_scopes = token.scopes.map(&:to_sym)
required_scopes.any? do |scope| required_scopes.any? do |scope|
if scope.respond_to?(:sufficient?) scope = API::Scope.new(scope) unless scope.is_a?(API::Scope)
scope.sufficient?(token_scopes, request) scope.sufficient?(token_scopes, request)
else
API::Scope.new(scope).sufficient?(token_scopes, request)
end
end end
end end
end end
......
...@@ -58,9 +58,10 @@ en: ...@@ -58,9 +58,10 @@ en:
expired: "The access token expired" expired: "The access token expired"
unknown: "The access token is invalid" unknown: "The access token is invalid"
scopes: scopes:
api: Access your API api: Access the authenticated user's API
read_user: Read user information read_user: Read the authenticated user's personal information
openid: Authenticate using OpenID Connect openid: Authenticate using OpenID Connect
sudo: Perform API actions as any user in the system (if the authenticated user is an admin)
flash: flash:
applications: applications:
......
...@@ -42,62 +42,42 @@ module API ...@@ -42,62 +42,42 @@ module API
# Helper Methods for Grape Endpoint # Helper Methods for Grape Endpoint
module HelperMethods module HelperMethods
def find_current_user def find_current_user!
user = user = find_user_from_access_token || find_user_from_warden
find_user_from_personal_access_token || return unless user
find_user_from_oauth_token ||
find_user_from_warden
return nil unless user forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user user
end end
def private_token def access_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] return @access_token if defined?(@access_token)
end
private
def find_user_from_personal_access_token
token_string = private_token.to_s
return nil unless token_string.present?
access_token = PersonalAccessToken.find_by_token(token_string) @access_token = find_oauth_access_token || find_personal_access_token
raise UnauthorizedError unless access_token end
user = find_user_by_access_token(access_token)
raise UnauthorizedError unless user def validate_access_token!(scopes: [])
return unless access_token
user case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
end
end end
# Invokes the doorkeeper guard. private
#
# If token is presented and valid, then it sets @current_user. def find_user_from_access_token
#
# If the token does not have sufficient scopes to cover the requred scopes,
# then it raises InsufficientScopeError.
#
# If the token is expired, then it raises ExpiredError.
#
# If the token is revoked, then it raises RevokedError.
#
# If the token is not found (nil), then it returns nil
#
# Arguments:
#
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
def find_user_from_oauth_token
access_token = find_oauth_access_token
return unless access_token return unless access_token
find_user_by_access_token(access_token) validate_access_token!
access_token.user || raise(UnauthorizedError)
end end
# Check the Rails session for valid authentication details # Check the Rails session for valid authentication details
...@@ -115,34 +95,26 @@ module API ...@@ -115,34 +95,26 @@ module API
end end
def find_oauth_access_token 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) token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return @oauth_access_token = nil unless token return unless token
@oauth_access_token = OauthAccessToken.by_token(token) # Expiration, revocation and scopes are verified in `find_user_by_access_token`
raise UnauthorizedError unless @oauth_access_token access_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless access_token
@oauth_access_token.revoke_previous_refresh_token! access_token.revoke_previous_refresh_token!
@oauth_access_token access_token
end end
def find_user_by_access_token(access_token) def find_personal_access_token
scopes = scopes_registered_for_endpoint token = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
return unless token.present?
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) # Expiration, revocation and scopes are verified in `find_user_by_access_token`
when AccessTokenValidationService::INSUFFICIENT_SCOPE access_token = PersonalAccessToken.find_by(token: token)
raise InsufficientScopeError.new(scopes) raise UnauthorizedError unless access_token
when AccessTokenValidationService::EXPIRED
raise ExpiredError
when AccessTokenValidationService::REVOKED
raise RevokedError
when AccessTokenValidationService::VALID access_token
access_token.user
end
end end
def doorkeeper_request def doorkeeper_request
...@@ -226,7 +198,7 @@ module API ...@@ -226,7 +198,7 @@ module API
class InsufficientScopeError < StandardError class InsufficientScopeError < StandardError
attr_reader :scopes attr_reader :scopes
def initialize(scopes) def initialize(scopes)
@scopes = scopes @scopes = scopes.map { |s| s.try(:name) || s }
end end
end end
end end
......
...@@ -41,6 +41,8 @@ module API ...@@ -41,6 +41,8 @@ module API
sudo! sudo!
validate_access_token!(scopes: scopes_registered_for_endpoint) unless sudo?
@current_user @current_user
end end
...@@ -385,7 +387,7 @@ module API ...@@ -385,7 +387,7 @@ module API
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 rescue APIGuard::UnauthorizedError
unauthorized! unauthorized!
end end
...@@ -393,24 +395,26 @@ module API ...@@ -393,24 +395,26 @@ module API
def sudo! def sudo!
return unless sudo_identifier return unless sudo_identifier
return unless initial_current_user
raise UnauthorizedError unless initial_current_user
unless initial_current_user.admin? unless initial_current_user.admin?
forbidden!('Must be admin to use sudo') forbidden!('Must be admin to use sudo')
end end
# Only private tokens should be used for the SUDO feature unless access_token
unless private_token == initial_current_user.private_token forbidden!('Must be authenticated using an OAuth or Personal Access Token to use sudo')
forbidden!('Private token must be specified in order to use sudo')
end end
validate_access_token!(scopes: [:sudo])
sudoed_user = find_user(sudo_identifier) sudoed_user = find_user(sudo_identifier)
if sudoed_user unless sudoed_user
@current_user = sudoed_user
else
not_found!("No user id or username for: #{sudo_identifier}") not_found!("No user id or username for: #{sudo_identifier}")
end end
@current_user = sudoed_user
end end
def sudo_identifier def sudo_identifier
......
...@@ -5,7 +5,7 @@ module Gitlab ...@@ -5,7 +5,7 @@ module Gitlab
REGISTRY_SCOPES = [:read_registry].freeze REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access # Scopes used for GitLab API access
API_SCOPES = [:api, :read_user].freeze API_SCOPES = [:api, :read_user, :sudo].freeze
# Scopes used for OpenID Connect # Scopes used for OpenID Connect
OPENID_SCOPES = [:openid].freeze OPENID_SCOPES = [:openid].freeze
...@@ -226,8 +226,10 @@ module Gitlab ...@@ -226,8 +226,10 @@ module Gitlab
[] []
end end
def available_scopes def available_scopes(current_user = nil)
API_SCOPES + registry_scopes scopes = API_SCOPES + registry_scopes
scopes.delete(:sudo) if current_user && !current_user.admin?
scopes
end end
# Other available scopes # Other available scopes
......
...@@ -5,7 +5,7 @@ describe Gitlab::Auth do ...@@ -5,7 +5,7 @@ describe Gitlab::Auth do
describe 'constants' do describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do it 'API_SCOPES contains all scopes for API access' do
expect(subject::API_SCOPES).to eq [:api, :read_user] expect(subject::API_SCOPES).to eq %i[api read_user sudo]
end end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
...@@ -19,7 +19,7 @@ describe Gitlab::Auth do ...@@ -19,7 +19,7 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
expect(subject.optional_scopes).to eq %i[read_user read_registry openid] expect(subject.optional_scopes).to eq %i[read_user sudo read_registry openid]
end end
context 'registry_scopes' do context 'registry_scopes' do
......
...@@ -39,20 +39,20 @@ describe 'doorkeeper access' do ...@@ -39,20 +39,20 @@ describe 'doorkeeper access' do
end end
describe "when user is blocked" do describe "when user is blocked" do
it "returns authentication error" do it "returns authorization error" do
user.block user.block
get api("/user"), access_token: token.token get api("/user"), access_token: token.token
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(403)
end end
end end
describe "when user is ldap_blocked" do describe "when user is ldap_blocked" do
it "returns authentication error" do it "returns authorization error" do
user.ldap_block user.ldap_block
get api("/user"), access_token: token.token get api("/user"), access_token: token.token
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(403)
end end
end end
end end
...@@ -173,18 +173,18 @@ describe API::Helpers do ...@@ -173,18 +173,18 @@ 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 user without access" do it "returns a 403 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error /403/
end end
it 'returns a 401 response for a user who is blocked' do it 'returns a 403 response for a user who is blocked' do
user.block! user.block!
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 /403/
end end
it "leaves user as is when sudo not specified" do it "leaves user as is when sudo not specified" do
......
...@@ -127,8 +127,8 @@ describe API::Users do ...@@ -127,8 +127,8 @@ describe API::Users do
context "when admin" do context "when admin" do
context 'when sudo is defined' do context 'when sudo is defined' do
it 'does not return 500' do it 'does not return 500' do
admin_personal_access_token = create(:personal_access_token, user: admin).token admin_personal_access_token = create(:personal_access_token, user: admin, scopes: [:sudo])
get api("/users?private_token=#{admin_personal_access_token}&sudo=#{user.id}", admin) get api("/users?sudo=#{user.id}", admin, personal_access_token: admin_personal_access_token)
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
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