c946ee1282
Signed-off-by: Rémy Coutable <remy@rymai.me>
536 lines
15 KiB
Ruby
536 lines
15 KiB
Ruby
require 'spec_helper'
|
|
|
|
describe API::Helpers do
|
|
include API::APIGuard::HelperMethods
|
|
include described_class
|
|
include SentryHelper
|
|
|
|
let(:user) { create(:user) }
|
|
let(:admin) { create(:admin) }
|
|
let(:key) { create(:key, user: user) }
|
|
|
|
let(:params) { {} }
|
|
let(:csrf_token) { SecureRandom.base64(ActionController::RequestForgeryProtection::AUTHENTICITY_TOKEN_LENGTH) }
|
|
let(:env) do
|
|
{
|
|
'rack.input' => '',
|
|
'rack.session' => {
|
|
_csrf_token: csrf_token
|
|
},
|
|
'REQUEST_METHOD' => 'GET'
|
|
}
|
|
end
|
|
let(:header) { }
|
|
|
|
before do
|
|
allow_any_instance_of(self.class).to receive(:options).and_return({})
|
|
end
|
|
|
|
def set_env(user_or_token, identifier)
|
|
clear_env
|
|
clear_param
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
|
|
env[API::Helpers::SUDO_HEADER] = identifier.to_s
|
|
end
|
|
|
|
def set_param(user_or_token, identifier)
|
|
clear_env
|
|
clear_param
|
|
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
|
|
params[API::Helpers::SUDO_PARAM] = identifier.to_s
|
|
end
|
|
|
|
def clear_env
|
|
env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
|
|
env.delete(API::Helpers::SUDO_HEADER)
|
|
end
|
|
|
|
def clear_param
|
|
params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
|
|
params.delete(API::Helpers::SUDO_PARAM)
|
|
end
|
|
|
|
def warden_authenticate_returns(value)
|
|
warden = double("warden", authenticate: value)
|
|
env['warden'] = warden
|
|
end
|
|
|
|
def doorkeeper_guard_returns(value)
|
|
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { value }
|
|
end
|
|
|
|
def error!(message, status, header)
|
|
raise Exception.new("#{status} - #{message}")
|
|
end
|
|
|
|
describe ".current_user" do
|
|
subject { current_user }
|
|
|
|
describe "Warden authentication", :allow_forgery_protection do
|
|
before do
|
|
doorkeeper_guard_returns false
|
|
end
|
|
|
|
context "with invalid credentials" do
|
|
context "GET request" do
|
|
before do
|
|
env['REQUEST_METHOD'] = 'GET'
|
|
end
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
end
|
|
|
|
context "with valid credentials" do
|
|
before do
|
|
warden_authenticate_returns user
|
|
end
|
|
|
|
context "GET request" do
|
|
before do
|
|
env['REQUEST_METHOD'] = 'GET'
|
|
end
|
|
|
|
it { is_expected.to eq(user) }
|
|
end
|
|
|
|
context "HEAD request" do
|
|
before do
|
|
env['REQUEST_METHOD'] = 'HEAD'
|
|
end
|
|
|
|
it { is_expected.to eq(user) }
|
|
end
|
|
|
|
context "PUT request" do
|
|
before do
|
|
env['REQUEST_METHOD'] = 'PUT'
|
|
end
|
|
|
|
context 'without CSRF token' do
|
|
it { is_expected.to be_nil }
|
|
end
|
|
|
|
context 'with CSRF token' do
|
|
before do
|
|
env['HTTP_X_CSRF_TOKEN'] = csrf_token
|
|
end
|
|
|
|
it { is_expected.to eq(user) }
|
|
end
|
|
end
|
|
|
|
context "POST request" do
|
|
before do
|
|
env['REQUEST_METHOD'] = 'POST'
|
|
end
|
|
|
|
context 'without CSRF token' do
|
|
it { is_expected.to be_nil }
|
|
end
|
|
|
|
context 'with CSRF token' do
|
|
before do
|
|
env['HTTP_X_CSRF_TOKEN'] = csrf_token
|
|
end
|
|
|
|
it { is_expected.to eq(user) }
|
|
end
|
|
end
|
|
|
|
context "DELETE request" do
|
|
before do
|
|
env['REQUEST_METHOD'] = 'DELETE'
|
|
end
|
|
|
|
context 'without CSRF token' do
|
|
it { is_expected.to be_nil }
|
|
end
|
|
|
|
context 'with CSRF token' do
|
|
before do
|
|
env['HTTP_X_CSRF_TOKEN'] = csrf_token
|
|
end
|
|
|
|
it { is_expected.to eq(user) }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "when authenticating using a user's private token" do
|
|
it "returns nil for an invalid token" do
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
|
|
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
|
|
|
|
expect(current_user).to be_nil
|
|
end
|
|
|
|
it "returns nil for a user without access" do
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
|
|
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
|
|
|
|
expect(current_user).to be_nil
|
|
end
|
|
|
|
it "leaves user as is when sudo not specified" do
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
|
|
|
|
expect(current_user).to eq(user)
|
|
|
|
clear_env
|
|
|
|
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
|
|
|
|
expect(current_user).to eq(user)
|
|
end
|
|
end
|
|
|
|
describe "when authenticating using a user's personal access tokens" do
|
|
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
|
|
|
before do
|
|
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
|
|
end
|
|
|
|
it "returns nil for an invalid token" do
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
|
|
|
|
expect(current_user).to be_nil
|
|
end
|
|
|
|
it "returns nil for a user without access" do
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
|
|
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
|
|
|
|
expect(current_user).to be_nil
|
|
end
|
|
|
|
it "returns nil 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 be_nil
|
|
end
|
|
|
|
it "leaves user as is when sudo not specified" do
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
|
|
expect(current_user).to eq(user)
|
|
clear_env
|
|
params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
|
|
|
|
expect(current_user).to eq(user)
|
|
end
|
|
|
|
it 'does not allow revoked tokens' do
|
|
personal_access_token.revoke!
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
|
|
|
|
expect(current_user).to be_nil
|
|
end
|
|
|
|
it 'does not allow expired tokens' do
|
|
personal_access_token.update_attributes!(expires_at: 1.day.ago)
|
|
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
|
|
|
|
expect(current_user).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'sudo usage' do
|
|
context 'with admin' do
|
|
context 'with header' do
|
|
context 'with id' do
|
|
it 'changes current_user to sudo' do
|
|
set_env(admin, user.id)
|
|
|
|
expect(current_user).to eq(user)
|
|
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
|
|
set_env(admin, admin.id)
|
|
|
|
expect(current_user).to eq(admin)
|
|
end
|
|
|
|
it 'throws an error when user cannot be found' do
|
|
id = user.id + admin.id
|
|
expect(user.id).not_to eq(id)
|
|
expect(admin.id).not_to eq(id)
|
|
|
|
set_env(admin, id)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
end
|
|
|
|
context 'with username' do
|
|
it 'changes current_user to sudo' do
|
|
set_env(admin, user.username)
|
|
|
|
expect(current_user).to eq(user)
|
|
end
|
|
|
|
it 'handles sudo to oneself' do
|
|
set_env(admin, admin.username)
|
|
|
|
expect(current_user).to eq(admin)
|
|
end
|
|
|
|
it "throws an error when the user cannot be found for a given username" do
|
|
username = "#{user.username}#{admin.username}"
|
|
expect(user.username).not_to eq(username)
|
|
expect(admin.username).not_to eq(username)
|
|
|
|
set_env(admin, username)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with param' do
|
|
context 'with id' do
|
|
it 'changes current_user to sudo' do
|
|
set_param(admin, user.id)
|
|
|
|
expect(current_user).to eq(user)
|
|
end
|
|
|
|
it 'handles sudo to oneself' do
|
|
set_param(admin, admin.id)
|
|
|
|
expect(current_user).to eq(admin)
|
|
end
|
|
|
|
it 'handles sudo to oneself using string' do
|
|
set_env(admin, user.id.to_s)
|
|
|
|
expect(current_user).to eq(user)
|
|
end
|
|
|
|
it 'throws an error when user cannot be found' do
|
|
id = user.id + admin.id
|
|
expect(user.id).not_to eq(id)
|
|
expect(admin.id).not_to eq(id)
|
|
|
|
set_param(admin, id)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
end
|
|
|
|
context 'with username' do
|
|
it 'changes current_user to sudo' do
|
|
set_param(admin, user.username)
|
|
|
|
expect(current_user).to eq(user)
|
|
end
|
|
|
|
it 'handles sudo to oneself' do
|
|
set_param(admin, admin.username)
|
|
|
|
expect(current_user).to eq(admin)
|
|
end
|
|
|
|
it "throws an error when the user cannot be found for a given username" do
|
|
username = "#{user.username}#{admin.username}"
|
|
expect(user.username).not_to eq(username)
|
|
expect(admin.username).not_to eq(username)
|
|
|
|
set_param(admin, username)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with regular user' do
|
|
context 'with env' do
|
|
it 'changes current_user to sudo when admin and user id' do
|
|
set_env(user, admin.id)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
|
|
it 'changes current_user to sudo when admin and user username' do
|
|
set_env(user, admin.username)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
end
|
|
|
|
context 'with params' do
|
|
it 'changes current_user to sudo when admin and user id' do
|
|
set_param(user, admin.id)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
|
|
it 'changes current_user to sudo when admin and user username' do
|
|
set_param(user, admin.username)
|
|
|
|
expect { current_user }.to raise_error(Exception)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.sudo?' do
|
|
context 'when no sudo env or param is passed' do
|
|
before do
|
|
doorkeeper_guard_returns(nil)
|
|
end
|
|
|
|
it 'returns false' do
|
|
expect(sudo?).to be_falsy
|
|
end
|
|
end
|
|
|
|
context 'when sudo env or param is passed', 'user is not an admin' do
|
|
before do
|
|
set_env(user, '123')
|
|
end
|
|
|
|
it 'returns an 403 Forbidden' do
|
|
expect { sudo? }.to raise_error '403 - {"message"=>"403 Forbidden - Must be admin to use sudo"}'
|
|
end
|
|
end
|
|
|
|
context 'when sudo env or param is passed', 'user is admin' do
|
|
context 'personal access token is used' do
|
|
before do
|
|
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
|
|
|
|
describe '.handle_api_exception' do
|
|
before do
|
|
allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
|
|
allow_any_instance_of(self.class).to receive(:rack_response)
|
|
end
|
|
|
|
it 'does not report a MethodNotAllowed exception to Sentry' do
|
|
exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' })
|
|
allow(exception).to receive(:backtrace).and_return(caller)
|
|
|
|
expect(Raven).not_to receive(:capture_exception).with(exception)
|
|
|
|
handle_api_exception(exception)
|
|
end
|
|
|
|
it 'does report RuntimeError to Sentry' do
|
|
exception = RuntimeError.new('test error')
|
|
allow(exception).to receive(:backtrace).and_return(caller)
|
|
|
|
expect_any_instance_of(self.class).to receive(:sentry_context)
|
|
expect(Raven).to receive(:capture_exception).with(exception)
|
|
|
|
handle_api_exception(exception)
|
|
end
|
|
end
|
|
|
|
describe '.authenticate_non_get!' do
|
|
%w[HEAD GET].each do |method_name|
|
|
context "method is #{method_name}" do
|
|
before do
|
|
expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name))
|
|
end
|
|
|
|
it 'does not raise an error' do
|
|
expect_any_instance_of(self.class).not_to receive(:authenticate!)
|
|
|
|
expect { authenticate_non_get! }.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
|
|
%w[POST PUT PATCH DELETE].each do |method_name|
|
|
context "method is #{method_name}" do
|
|
before do
|
|
expect_any_instance_of(self.class).to receive(:route).and_return(double(request_method: method_name))
|
|
end
|
|
|
|
it 'calls authenticate!' do
|
|
expect_any_instance_of(self.class).to receive(:authenticate!)
|
|
|
|
authenticate_non_get!
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '.authenticate!' do
|
|
context 'current_user is nil' do
|
|
before do
|
|
expect_any_instance_of(self.class).to receive(:current_user).and_return(nil)
|
|
allow_any_instance_of(self.class).to receive(:initial_current_user).and_return(nil)
|
|
end
|
|
|
|
it 'returns a 401 response' do
|
|
expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
|
|
end
|
|
end
|
|
|
|
context 'current_user is present' do
|
|
let(:user) { build(:user) }
|
|
|
|
before do
|
|
expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
|
|
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
|
|
end
|
|
|
|
it 'does not raise an error' do
|
|
expect { authenticate! }.not_to raise_error
|
|
end
|
|
end
|
|
|
|
context 'current_user is blocked' do
|
|
let(:user) { build(:user, :blocked) }
|
|
|
|
before do
|
|
expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
|
|
end
|
|
|
|
it 'raises an error' do
|
|
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
|
|
|
|
expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
|
|
end
|
|
|
|
it "doesn't raise an error if an admin user is impersonating a blocked user (via sudo)" do
|
|
admin_user = build(:user, :admin)
|
|
|
|
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(admin_user)
|
|
|
|
expect { authenticate! }.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
end
|