Commit 0dcb587a authored by Sean McGivern's avatar Sean McGivern Committed by Rémy Coutable

Merge branch '25482-fix-api-sudo' into 'master'

API: Memoize the current_user so that the sudo can work properly

Closes #25482

See merge request !8017
Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 1747798d
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.12.13
- API: Memoize the current_user so that the sudo can work properly. !8017
v 8.12.12 v 8.12.12
- Replace MR access checks with use of MergeRequestsFinder - Replace MR access checks with use of MergeRequestsFinder
- Reenables /user API request to return private-token if user is admin and request is made with sudo - Reenables /user API request to return private-token if user is admin and request is made with sudo
......
...@@ -275,8 +275,9 @@ class User < ActiveRecord::Base ...@@ -275,8 +275,9 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token personal_access_token.user if personal_access_token
end end
def by_username_or_id(name_or_id) # Returns a user for the given SSH key.
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i) def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
end end
def build_user(attrs = {}) def build_user(attrs = {})
......
---
title: 'API: Memoize the current_user so that sudo can work properly'
merge_request: 8017
author:
...@@ -12,68 +12,37 @@ module API ...@@ -12,68 +12,37 @@ module API
nil nil
end end
def private_token def declared_params(options = {})
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER] options = { include_parent_namespaces: false }.merge(options)
end declared(params, options).to_h.symbolize_keys
def warden
env['warden']
end
# Check the Rails session for valid authentication details
#
# Until CSRF protection is added to the API, disallow this method for
# state-changing endpoints
def find_user_from_warden
warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def find_user_by_private_token
token = private_token
return nil unless token.present?
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end end
def current_user def current_user
@current_user ||= find_user_by_private_token return @current_user if defined?(@current_user)
@current_user ||= doorkeeper_guard
@current_user ||= find_user_from_warden
unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
return nil
end
identifier = sudo_identifier @current_user = initial_current_user
if identifier sudo!
# We check for private_token because we cannot allow PAT to be used
forbidden!('Must be admin to use sudo') unless @current_user.is_admin?
forbidden!('Private token must be specified in order to use sudo') unless private_token_used?
@impersonator = @current_user
@current_user = User.by_username_or_id(identifier)
not_found!("No user id or username for: #{identifier}") if @current_user.nil?
end
@current_user @current_user
end end
def sudo_identifier def sudo?
identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] initial_current_user != current_user
# Regex for integers
if !!(identifier =~ /\A[0-9]+\z/)
identifier.to_i
else
identifier
end
end end
def user_project def user_project
@project ||= find_project(params[:id]) @project ||= find_project(params[:id])
end end
def find_user(id)
if id =~ /^\d+$/
User.find_by(id: id)
else
User.find_by(username: id)
end
end
def find_project(id) def find_project(id)
project = Project.find_with_namespace(id) || Project.find_by(id: id) project = Project.find_with_namespace(id) || Project.find_by(id: id)
...@@ -401,6 +370,69 @@ module API ...@@ -401,6 +370,69 @@ module API
private private
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
def warden
env['warden']
end
# Check the Rails session for valid authentication details
#
# Until CSRF protection is added to the API, disallow this method for
# state-changing endpoints
def find_user_from_warden
warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def find_user_by_private_token
token = private_token
return nil unless token.present?
User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
end
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
@initial_current_user ||= find_user_by_private_token
@initial_current_user ||= doorkeeper_guard
@initial_current_user ||= find_user_from_warden
unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
@initial_current_user = nil
end
@initial_current_user
end
def sudo!
return unless sudo_identifier
return unless initial_current_user
unless initial_current_user.is_admin?
forbidden!('Must be admin to use sudo')
end
# Only private tokens should be used for the SUDO feature
unless private_token == initial_current_user.private_token
forbidden!('Private token must be specified in order to use sudo')
end
sudoed_user = find_user(sudo_identifier)
if sudoed_user
@current_user = sudoed_user
else
not_found!("No user id or username for: #{sudo_identifier}")
end
end
def sudo_identifier
@sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER]
end
def add_pagination_headers(paginated_data) def add_pagination_headers(paginated_data)
header 'X-Total', paginated_data.total_count.to_s header 'X-Total', paginated_data.total_count.to_s
header 'X-Total-Pages', paginated_data.total_pages.to_s header 'X-Total-Pages', paginated_data.total_pages.to_s
...@@ -433,10 +465,6 @@ module API ...@@ -433,10 +465,6 @@ module API
links.join(', ') links.join(', ')
end end
def private_token_used?
private_token == @current_user.private_token
end
def secret_token def secret_token
File.read(Gitlab.config.gitlab_shell.secret_file).chomp File.read(Gitlab.config.gitlab_shell.secret_file).chomp
end end
......
...@@ -327,7 +327,7 @@ module API ...@@ -327,7 +327,7 @@ module API
# Example Request: # Example Request:
# GET /user # GET /user
get do get do
present @current_user, with: @impersonator ? Entities::UserWithPrivateToken : Entities::UserPublic present current_user, with: sudo? ? Entities::UserWithPrivateToken : Entities::UserPublic
end end
# Get currently authenticated user's keys # Get currently authenticated user's keys
......
...@@ -599,14 +599,94 @@ describe User, models: true do ...@@ -599,14 +599,94 @@ describe User, models: true do
end end
end end
describe 'by_username_or_id' do describe '.search_with_secondary_emails' do
let(:user1) { create(:user, username: 'foo') } def search_with_secondary_emails(query)
described_class.search_with_secondary_emails(query)
it "gets the correct user" do end
expect(User.by_username_or_id(user1.id)).to eq(user1)
expect(User.by_username_or_id('foo')).to eq(user1) let!(:user) { create(:user) }
expect(User.by_username_or_id(-1)).to be_nil let!(:email) { create(:email) }
expect(User.by_username_or_id('bar')).to be_nil
it 'returns users with a matching name' do
expect(search_with_secondary_emails(user.name)).to eq([user])
end
it 'returns users with a partially matching name' do
expect(search_with_secondary_emails(user.name[0..2])).to eq([user])
end
it 'returns users with a matching name regardless of the casing' do
expect(search_with_secondary_emails(user.name.upcase)).to eq([user])
end
it 'returns users with a matching email' do
expect(search_with_secondary_emails(user.email)).to eq([user])
end
it 'returns users with a partially matching email' do
expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
end
it 'returns users with a matching email regardless of the casing' do
expect(search_with_secondary_emails(user.email.upcase)).to eq([user])
end
it 'returns users with a matching username' do
expect(search_with_secondary_emails(user.username)).to eq([user])
end
it 'returns users with a partially matching username' do
expect(search_with_secondary_emails(user.username[0..2])).to eq([user])
end
it 'returns users with a matching username regardless of the casing' do
expect(search_with_secondary_emails(user.username.upcase)).to eq([user])
end
it 'returns users with a matching whole secondary email' do
expect(search_with_secondary_emails(email.email)).to eq([email.user])
end
it 'returns users with a matching part of secondary email' do
expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
end
it 'return users with a matching part of secondary email regardless of case' do
expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
end
it 'returns multiple users with matching secondary emails' do
email1 = create(:email, email: '1_testemail@example.com')
email2 = create(:email, email: '2_testemail@example.com')
email3 = create(:email, email: 'other@email.com')
email3.user.update_attributes!(email: 'another@mail.com')
expect(
search_with_secondary_emails('testemail@example.com').map(&:id)
).to include(email1.user.id, email2.user.id)
expect(
search_with_secondary_emails('testemail@example.com').map(&:id)
).not_to include(email3.user.id)
end
end
describe '.find_by_ssh_key_id' do
context 'using an existing SSH key ID' do
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
it 'returns the corresponding User' do
expect(described_class.find_by_ssh_key_id(key.id)).to eq(user)
end
end
context 'using an invalid SSH key ID' do
it 'returns nil' do
expect(described_class.find_by_ssh_key_id(-1)).to be_nil
end
end end
end end
......
...@@ -2,7 +2,6 @@ require 'spec_helper' ...@@ -2,7 +2,6 @@ require 'spec_helper'
describe API::Helpers, api: true do describe API::Helpers, api: true do
include API::Helpers include API::Helpers
include ApiHelpers
include SentryHelper include SentryHelper
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -13,18 +12,18 @@ describe API::Helpers, api: true do ...@@ -13,18 +12,18 @@ describe API::Helpers, api: true do
let(:env) { { 'REQUEST_METHOD' => 'GET' } } let(:env) { { 'REQUEST_METHOD' => 'GET' } }
let(:request) { Rack::Request.new(env) } let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier) def set_env(user_or_token, identifier)
clear_env clear_env
clear_param clear_param
env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
env[API::Helpers::SUDO_HEADER] = identifier env[API::Helpers::SUDO_HEADER] = identifier.to_s
end end
def set_param(token_usr, identifier) def set_param(user_or_token, identifier)
clear_env clear_env
clear_param clear_param
params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
params[API::Helpers::SUDO_PARAM] = identifier params[API::Helpers::SUDO_PARAM] = identifier.to_s
end end
def clear_env def clear_env
...@@ -163,6 +162,13 @@ describe API::Helpers, api: true do ...@@ -163,6 +162,13 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user) expect(current_user).to eq(user)
end end
it 'memoize the current_user: sudo permissions are not run against the sudoed user' do
set_env(admin, user.id)
expect(current_user).to eq(user)
expect(current_user).to eq(user)
end
it 'handles sudo to oneself' do it 'handles sudo to oneself' do
set_env(admin, admin.id) set_env(admin, admin.id)
...@@ -294,33 +300,48 @@ describe API::Helpers, api: true do ...@@ -294,33 +300,48 @@ describe API::Helpers, api: true do
end end
end end
describe '.sudo_identifier' do describe '.sudo?' do
it "returns integers when input is an int" do context 'when no sudo env or param is passed' do
set_env(admin, '123') before do
expect(sudo_identifier).to eq(123) doorkeeper_guard_returns(nil)
set_env(admin, '0001234567890') end
expect(sudo_identifier).to eq(1234567890)
it 'returns false' do
set_param(admin, '123') expect(sudo?).to be_falsy
expect(sudo_identifier).to eq(123) end
set_param(admin, '0001234567890')
expect(sudo_identifier).to eq(1234567890)
end end
it "returns string when input is an is not an int" do context 'when sudo env or param is passed', 'user is not an admin' do
set_env(admin, '12.30') before do
expect(sudo_identifier).to eq("12.30") set_env(user, '123')
set_env(admin, 'hello') end
expect(sudo_identifier).to eq('hello')
set_env(admin, ' 123') it 'returns an 403 Forbidden' do
expect(sudo_identifier).to eq(' 123') expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
end
set_param(admin, '12.30') end
expect(sudo_identifier).to eq("12.30")
set_param(admin, 'hello') context 'when sudo env or param is passed', 'user is admin' do
expect(sudo_identifier).to eq('hello') context 'personal access token is used' do
set_param(admin, ' 123') before do
expect(sudo_identifier).to eq(' 123') personal_access_token = create(:personal_access_token, user: admin)
set_env(personal_access_token.token, user.id)
end
it 'returns an 403 Forbidden' do
expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Private token must be specified in order to use sudo"}'
end
end
context 'private access token is used' do
before do
set_env(admin.private_token, user.id)
end
it 'returns true' do
expect(sudo?).to be_truthy
end
end
end end
end end
......
...@@ -597,13 +597,12 @@ describe API::API, api: true do ...@@ -597,13 +597,12 @@ describe API::API, api: true do
end end
describe "GET /user" do describe "GET /user" do
let(:personal_access_token) { create(:personal_access_token, user: user) } let(:personal_access_token) { create(:personal_access_token, user: user).token }
let(:private_token) { user.private_token }
context 'with regular user' do context 'with regular user' do
context 'with personal access token' do context 'with personal access token' do
it 'returns 403 without private token when sudo is defined' do it 'returns 403 without private token when sudo is defined' do
get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") get api("/user?private_token=#{personal_access_token}&sudo=123")
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
...@@ -611,7 +610,7 @@ describe API::API, api: true do ...@@ -611,7 +610,7 @@ describe API::API, api: true do
context 'with private token' do context 'with private token' do
it 'returns 403 without private token when sudo defined' do it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{private_token}&sudo=#{user.id}") get api("/user?private_token=#{user.private_token}&sudo=123")
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
...@@ -622,40 +621,44 @@ describe API::API, api: true do ...@@ -622,40 +621,44 @@ describe API::API, api: true do
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public') expect(response).to match_response_schema('user/public')
expect(json_response['id']).to eq(user.id)
end end
end end
context 'with admin' do context 'with admin' do
let(:user) { create(:admin) } let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token }
context 'with personal access token' do context 'with personal access token' do
it 'returns 403 without private token when sudo defined' do it 'returns 403 without private token when sudo defined' do
get api("/user?private_token=#{personal_access_token.token}&sudo=#{user.id}") get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}")
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
it 'returns current user without private token when sudo not defined' do it 'returns initial current user without private token when sudo not defined' do
get api("/user?private_token=#{personal_access_token.token}") get api("/user?private_token=#{admin_personal_access_token}")
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public') expect(response).to match_response_schema('user/public')
expect(json_response['id']).to eq(admin.id)
end end
end end
context 'with private token' do context 'with private token' do
it 'returns current user with private token when sudo defined' do it 'returns sudoed user with private token when sudo defined' do
get api("/user?private_token=#{private_token}&sudo=#{user.id}") get api("/user?private_token=#{admin.private_token}&sudo=#{user.id}")
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/login') expect(response).to match_response_schema('user/login')
expect(json_response['id']).to eq(user.id)
end end
it 'returns current user without private token when sudo not defined' do it 'returns initial current user without private token when sudo not defined' do
get api("/user?private_token=#{private_token}") get api("/user?private_token=#{admin.private_token}")
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
expect(response).to match_response_schema('user/public') expect(response).to match_response_schema('user/public')
expect(json_response['id']).to eq(admin.id)
end end
end end
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