From 2f45d3bcf0f28d4cd4124b4c9722edc1d3085201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Fri, 9 Dec 2016 18:48:20 +0100 Subject: API: Memoize the current_user so that the sudo can work properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The issue was arising when `#current_user` was called a second time after a user was impersonated: the `User#is_admin?` check would be performed on it and it would fail. Signed-off-by: Rémy Coutable --- spec/requests/api/helpers_spec.rb | 425 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 425 insertions(+) create mode 100644 spec/requests/api/helpers_spec.rb (limited to 'spec/requests/api/helpers_spec.rb') diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb new file mode 100644 index 00000000000..573154f69b4 --- /dev/null +++ b/spec/requests/api/helpers_spec.rb @@ -0,0 +1,425 @@ +require 'spec_helper' + +describe API::Helpers, api: true do + include API::Helpers + include SentryHelper + + let(:user) { create(:user) } + let(:admin) { create(:admin) } + let(:key) { create(:key, user: user) } + + let(:params) { {} } + let(:env) { { 'REQUEST_METHOD' => 'GET' } } + let(:request) { Rack::Request.new(env) } + + def set_env(user_or_token, identifier) + clear_env + clear_param + 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 + end + + def set_param(user_or_token, identifier) + clear_env + clear_param + 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 + end + + def clear_env + env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) + env.delete(API::Helpers::SUDO_HEADER) + end + + def clear_param + params.delete(API::Helpers::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) + raise Exception.new("#{status} - #{message}") + end + + describe ".current_user" do + subject { current_user } + + describe "Warden authentication" do + before { doorkeeper_guard_returns false } + + context "with invalid credentials" do + context "GET request" do + before { env['REQUEST_METHOD'] = 'GET' } + it { is_expected.to be_nil } + end + end + + context "with valid credentials" do + before { warden_authenticate_returns user } + + context "GET request" do + before { env['REQUEST_METHOD'] = 'GET' } + it { is_expected.to eq(user) } + end + + context "HEAD request" do + before { env['REQUEST_METHOD'] = 'HEAD' } + it { is_expected.to eq(user) } + end + + context "PUT request" do + before { env['REQUEST_METHOD'] = 'PUT' } + it { is_expected.to be_nil } + end + + context "POST request" do + before { env['REQUEST_METHOD'] = 'POST' } + it { is_expected.to be_nil } + end + + context "DELETE request" do + before { env['REQUEST_METHOD'] = 'DELETE' } + it { is_expected.to be_nil } + end + end + end + + describe "when authenticating using a user's private token" do + it "returns nil for an invalid token" do + env[API::Helpers::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::Helpers::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::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::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) } + + it "returns nil for an invalid token" do + env[API::Helpers::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::Helpers::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 "leaves user as is when sudo not specified" do + env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to eq(user) + clear_env + params[API::Helpers::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::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + 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::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + 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(route_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(route_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) + end + + it 'returns a 401 response' do + expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' + end + end + + context 'current_user is present' do + before do + expect_any_instance_of(self.class).to receive(:current_user).and_return(true) + end + + it 'does not raise an error' do + expect { authenticate! }.not_to raise_error + end + end + end +end -- cgit v1.2.1