Commit af8391fe authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'refactor/bypass-session-admin-mode' into 'master'

Bypass Session for API & Sesssionless Endpoints in Admin Mode

Closes #42685

See merge request gitlab-org/gitlab!25056
parents abda9897 cbb0f861
...@@ -34,6 +34,7 @@ class ApplicationController < ActionController::Base ...@@ -34,6 +34,7 @@ class ApplicationController < ActionController::Base
before_action :check_impersonation_availability before_action :check_impersonation_availability
before_action :required_signup_info before_action :required_signup_info
around_action :sessionless_bypass_admin_mode!, if: :sessionless_user?
around_action :set_current_context around_action :set_current_context
around_action :set_locale around_action :set_locale
around_action :set_session_storage around_action :set_session_storage
......
...@@ -5,12 +5,6 @@ ...@@ -5,12 +5,6 @@
# Controller concern to handle PAT, RSS, and static objects token authentication methods # Controller concern to handle PAT, RSS, and static objects token authentication methods
# #
module SessionlessAuthentication module SessionlessAuthentication
extend ActiveSupport::Concern
included do
before_action :enable_admin_mode!, if: :sessionless_user?
end
# This filter handles personal access tokens, atom requests with rss tokens, and static object tokens # This filter handles personal access tokens, atom requests with rss tokens, and static object tokens
def authenticate_sessionless_user!(request_format) def authenticate_sessionless_user!(request_format)
user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format) user = Gitlab::Auth::RequestAuthenticator.new(request).find_sessionless_user(request_format)
...@@ -32,9 +26,9 @@ module SessionlessAuthentication ...@@ -32,9 +26,9 @@ module SessionlessAuthentication
end end
end end
def enable_admin_mode! def sessionless_bypass_admin_mode!(&block)
return unless Feature.enabled?(:user_mode_in_session) return yield unless Feature.enabled?(:user_mode_in_session)
current_user_mode.enable_sessionless_admin_mode! Gitlab::Auth::CurrentUserMode.bypass_session!(current_user.id, &block)
end end
end end
...@@ -15,6 +15,11 @@ class GraphqlController < ApplicationController ...@@ -15,6 +15,11 @@ class GraphqlController < ApplicationController
before_action :authorize_access_api! before_action :authorize_access_api!
before_action(only: [:execute]) { authenticate_sessionless_user!(:api) } before_action(only: [:execute]) { authenticate_sessionless_user!(:api) }
# Since we deactivate authentication from the main ApplicationController and
# defer it to :authorize_access_api!, we need to override the bypass session
# callback execution order here
around_action :sessionless_bypass_admin_mode!, if: :sessionless_user?
def execute def execute
result = multiplex? ? execute_multiplex : execute_query result = multiplex? ? execute_multiplex : execute_query
......
---
title: Sessionless and API endpoints bypass session for admin mode
merge_request: 25056
author: Diego Louzán
type: changed
...@@ -50,17 +50,13 @@ module API ...@@ -50,17 +50,13 @@ module API
user = find_user_from_sources user = find_user_from_sources
return unless user return unless user
# Sessions are enforced to be unavailable for API calls, so ignore them for admin mode
Gitlab::Auth::CurrentUserMode.bypass_session!(user.id) if Feature.enabled?(:user_mode_in_session)
unless api_access_allowed?(user) unless api_access_allowed?(user)
forbidden!(api_access_denied_message(user)) forbidden!(api_access_denied_message(user))
end end
# Set admin mode for API requests (if admin)
if Feature.enabled?(:user_mode_in_session)
current_user_mode = Gitlab::Auth::CurrentUserMode.new(user)
current_user_mode.enable_sessionless_admin_mode!
end
user user
end end
...@@ -154,19 +150,13 @@ module API ...@@ -154,19 +150,13 @@ module API
end end
class AdminModeMiddleware < ::Grape::Middleware::Base class AdminModeMiddleware < ::Grape::Middleware::Base
def initialize(app, **options) def after
super # Use a Grape middleware since the Grape `after` blocks might run
end # before we are finished rendering the `Grape::Entity` classes
Gitlab::Auth::CurrentUserMode.reset_bypass_session! if Feature.enabled?(:user_mode_in_session)
def call(env) # Explicit nil is needed or the api call return value will be overwritten
if Feature.enabled?(:user_mode_in_session) nil
session = {}
Gitlab::Session.with_session(session) do
app.call(env)
end
else
app.call(env)
end
end end
end end
end end
......
...@@ -23,15 +23,26 @@ module Gitlab ...@@ -23,15 +23,26 @@ module Gitlab
class << self class << self
# Admin mode activation requires storing a flag in the user session. Using this # Admin mode activation requires storing a flag in the user session. Using this
# method when scheduling jobs in Sidekiq will bypass the session check for a # method when scheduling jobs in sessionless environments (e.g. Sidekiq, API)
# user that was already in admin mode # will bypass the session check for a user that was already in admin mode
#
# If passed a block, it will surround the block execution and reset the session
# bypass at the end; otherwise use manually '.reset_bypass_session!'
def bypass_session!(admin_id) def bypass_session!(admin_id)
Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id
Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}") Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}")
yield if block_given?
ensure begin
yield
ensure
reset_bypass_session!
end
end
end
def reset_bypass_session!
Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY) Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY)
end end
...@@ -90,10 +101,6 @@ module Gitlab ...@@ -90,10 +101,6 @@ module Gitlab
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
end end
def enable_sessionless_admin_mode!
request_admin_mode! && enable_admin_mode!(skip_password_validation: true)
end
def disable_admin_mode! def disable_admin_mode!
return unless user&.admin? return unless user&.admin?
......
...@@ -3,294 +3,330 @@ ...@@ -3,294 +3,330 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store do describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store do
include_context 'custom session'
let(:user) { build_stubbed(:user) } let(:user) { build_stubbed(:user) }
subject { described_class.new(user) } subject { described_class.new(user) }
before do context 'when session is available' do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session]) include_context 'custom session'
end
shared_examples 'admin mode cannot be enabled' do
it 'is false by default' do
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil)
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with empty params' do
subject.enable_admin_mode!
expect(subject.admin_mode?).to be(false) before do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
end end
it 'disable has no effect' do shared_examples 'admin mode cannot be enabled' do
subject.enable_admin_mode! it 'is false by default' do
subject.disable_admin_mode! expect(subject.admin_mode?).to be(false)
end
expect(subject.admin_mode?).to be(false)
end
context 'skipping password validation' do
it 'cannot be enabled with a valid password' do it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true) subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(false) expect(subject.admin_mode?).to be(false)
end end
it 'cannot be enabled with an invalid password' do it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true) subject.enable_admin_mode!(password: nil)
expect(subject.admin_mode?).to be(false) expect(subject.admin_mode?).to be(false)
end end
end
end
describe '#admin_mode?' do it 'cannot be enabled with empty params' do
context 'when the user is a regular user' do subject.enable_admin_mode!
it_behaves_like 'admin mode cannot be enabled'
context 'bypassing session' do expect(subject.admin_mode?).to be(false)
it_behaves_like 'admin mode cannot be enabled' do
around do |example|
described_class.bypass_session!(user.id) { example.run }
end
end
end end
end
context 'when the user is an admin' do
let(:user) { build_stubbed(:user, :admin) }
context 'when admin mode not requested' do it 'disable has no effect' do
it 'is false by default' do subject.enable_admin_mode!
expect(subject.admin_mode?).to be(false) subject.disable_admin_mode!
end
it 'raises exception if we try to enable it' do
expect do
subject.enable_admin_mode!(password: user.password)
end.to raise_error(::Gitlab::Auth::CurrentUserMode::NotRequestedError)
expect(subject.admin_mode?).to be(false) expect(subject.admin_mode?).to be(false)
end
end end
context 'when admin mode requested first' do context 'skipping password validation' do
before do it 'cannot be enabled with a valid password' do
subject.request_admin_mode! subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
end
it 'is false by default' do
expect(subject.admin_mode?).to be(false) expect(subject.admin_mode?).to be(false)
end end
it 'cannot be enabled with an invalid password' do it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil) subject.enable_admin_mode!(skip_password_validation: true)
expect(subject.admin_mode?).to be(false) expect(subject.admin_mode?).to be(false)
end end
end
end
it 'can be enabled with a valid password' do describe '#admin_mode?' do
subject.enable_admin_mode!(password: user.password) context 'when the user is a regular user' do
it_behaves_like 'admin mode cannot be enabled'
expect(subject.admin_mode?).to be(true) context 'bypassing session' do
it_behaves_like 'admin mode cannot be enabled' do
around do |example|
described_class.bypass_session!(user.id) { example.run }
end
end
end end
end
it 'can be disabled' do context 'when the user is an admin' do
subject.enable_admin_mode!(password: user.password) let(:user) { build_stubbed(:user, :admin) }
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(false) context 'when admin mode not requested' do
it 'is false by default' do
expect(subject.admin_mode?).to be(false)
end
it 'raises exception if we try to enable it' do
expect do
subject.enable_admin_mode!(password: user.password)
end.to raise_error(::Gitlab::Auth::CurrentUserMode::NotRequestedError)
expect(subject.admin_mode?).to be(false)
end
end end
it 'will expire in the future' do context 'when admin mode requested first' do
subject.enable_admin_mode!(password: user.password) before do
expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present' subject.request_admin_mode!
end
Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do it 'is false by default' do
# in the future this will be a new request, simulate by clearing the RequestStore expect(subject.admin_mode?).to be(false)
Gitlab::SafeRequestStore.clear! end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil)
expect(subject.admin_mode?).to be(false), 'admin mode did not expire in the future' expect(subject.admin_mode?).to be(false)
end end
end
context 'skipping password validation' do
it 'can be enabled with a valid password' do it 'can be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true) subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true) expect(subject.admin_mode?).to be(true)
end end
it 'can be enabled with an invalid password' do it 'can be disabled' do
subject.enable_admin_mode!(skip_password_validation: true) subject.enable_admin_mode!(password: user.password)
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(true) expect(subject.admin_mode?).to be(false)
end end
end
context 'with two independent sessions' do it 'will expire in the future' do
let(:another_session) { {} } subject.enable_admin_mode!(password: user.password)
let(:another_subject) { described_class.new(user) } expect(subject.admin_mode?).to be(true), 'admin mode is not active in the present'
before do Timecop.freeze(Gitlab::Auth::CurrentUserMode::MAX_ADMIN_MODE_TIME.from_now) do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session, another_session]) # in the future this will be a new request, simulate by clearing the RequestStore
Gitlab::SafeRequestStore.clear!
expect(subject.admin_mode?).to be(false), 'admin mode did not expire in the future'
end
end end
it 'can be enabled in one and seen in the other' do context 'skipping password validation' do
Gitlab::Session.with_session(another_session) do it 'can be enabled with a valid password' do
another_subject.request_admin_mode! subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
another_subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true)
end end
expect(subject.admin_mode?).to be(true) it 'can be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true)
expect(subject.admin_mode?).to be(true)
end
end end
end
end
context 'bypassing session' do context 'with two independent sessions' do
it 'is active by default' do let(:another_session) { {} }
described_class.bypass_session!(user.id) do let(:another_subject) { described_class.new(user) }
expect(subject.admin_mode?).to be(true)
before do
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session, another_session])
end
it 'can be enabled in one and seen in the other' do
Gitlab::Session.with_session(another_session) do
another_subject.request_admin_mode!
another_subject.enable_admin_mode!(password: user.password)
end
expect(subject.admin_mode?).to be(true)
end
end end
end end
it 'enable has no effect' do context 'bypassing session' do
described_class.bypass_session!(user.id) do it 'is active by default' do
subject.request_admin_mode! described_class.bypass_session!(user.id) do
subject.enable_admin_mode!(password: user.password) expect(subject.admin_mode?).to be(true)
end
end
expect(subject.admin_mode?).to be(true) it 'enable has no effect' do
described_class.bypass_session!(user.id) do
subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true)
end
end end
end
it 'disable has no effect' do it 'disable has no effect' do
described_class.bypass_session!(user.id) do described_class.bypass_session!(user.id) do
subject.disable_admin_mode! subject.disable_admin_mode!
expect(subject.admin_mode?).to be(true) expect(subject.admin_mode?).to be(true)
end
end end
end end
end end
end end
end
describe '#enable_admin_mode!' do describe '#enable_admin_mode!' do
let(:user) { build_stubbed(:user, :admin) } let(:user) { build_stubbed(:user, :admin) }
it 'creates a timestamp in the session' do it 'creates a timestamp in the session' do
subject.request_admin_mode! subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password) subject.enable_admin_mode!(password: user.password)
expect(session).to include(expected_session_entry(be_within(1.second).of Time.now)) expect(session).to include(expected_session_entry(be_within(1.second).of Time.now))
end
end end
end
describe '#enable_sessionless_admin_mode!' do describe '#disable_admin_mode!' do
let(:user) { build_stubbed(:user, :admin) } let(:user) { build_stubbed(:user, :admin) }
it 'enabled admin mode without password' do it 'sets the session timestamp to nil' do
subject.enable_sessionless_admin_mode! subject.request_admin_mode!
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(true) expect(session).to include(expected_session_entry(be_nil))
end
end end
end
describe '#disable_admin_mode!' do describe '.with_current_request_admin_mode' do
let(:user) { build_stubbed(:user, :admin) } context 'with a regular user' do
it 'user is not available inside nor outside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be_nil
end
it 'sets the session timestamp to nil' do expect(described_class.bypass_session_admin_id).to be_nil
subject.request_admin_mode! end
subject.disable_admin_mode! end
expect(session).to include(expected_session_entry(be_nil)) context 'with an admin user' do
end let(:user) { build_stubbed(:user, :admin) }
end
describe '.bypass_session!' do context 'admin mode is disabled' do
context 'with a regular user' do it 'user is not available inside nor outside the yielded block' do
it 'admin mode is false' do described_class.with_current_admin(user) do
described_class.bypass_session!(user.id) do expect(described_class.current_admin).to be_nil
expect(subject.admin_mode?).to be(false) end
expect(described_class.bypass_session_admin_id).to be(user.id)
expect(described_class.bypass_session_admin_id).to be_nil
end
end end
expect(described_class.bypass_session_admin_id).to be_nil context 'admin mode is enabled' do
end before do
end subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password)
end
context 'with an admin user' do it 'user is available only inside the yielded block' do
let(:user) { build_stubbed(:user, :admin) } described_class.with_current_admin(user) do
expect(described_class.current_admin).to be(user)
end
it 'admin mode is true' do expect(described_class.current_admin).to be_nil
described_class.bypass_session!(user.id) do end
expect(subject.admin_mode?).to be(true)
expect(described_class.bypass_session_admin_id).to be(user.id)
end end
expect(described_class.bypass_session_admin_id).to be_nil
end end
end end
end
describe '.with_current_request_admin_mode' do def expected_session_entry(value_matcher)
context 'with a regular user' do {
it 'user is not available inside nor outside the yielded block' do Gitlab::Auth::CurrentUserMode::SESSION_STORE_KEY => a_hash_including(
described_class.with_current_admin(user) do Gitlab::Auth::CurrentUserMode::ADMIN_MODE_START_TIME_KEY => value_matcher)
expect(described_class.current_admin).to be_nil }
end end
end
expect(described_class.bypass_session_admin_id).to be_nil context 'when no session available' do
around do |example|
Gitlab::Session.with_session(nil) do
example.run
end end
end end
context 'with an admin user' do describe '.bypass_session!' do
let(:user) { build_stubbed(:user, :admin) } context 'when providing a block' do
context 'with a regular user' do
it 'admin mode is false' do
described_class.bypass_session!(user.id) do
expect(Gitlab::Session.current).to be_nil
expect(subject.admin_mode?).to be(false)
expect(described_class.bypass_session_admin_id).to be(user.id)
end
context 'admin mode is disabled' do expect(described_class.bypass_session_admin_id).to be_nil
it 'user is not available inside nor outside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be_nil
end end
end
expect(described_class.bypass_session_admin_id).to be_nil context 'with an admin user' do
let(:user) { build_stubbed(:user, :admin) }
it 'admin mode is true' do
described_class.bypass_session!(user.id) do
expect(Gitlab::Session.current).to be_nil
expect(subject.admin_mode?).to be(true)
expect(described_class.bypass_session_admin_id).to be(user.id)
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end end
end end
context 'admin mode is enabled' do context 'when not providing a block' do
before do context 'with a regular user' do
subject.request_admin_mode! it 'admin mode is false' do
subject.enable_admin_mode!(password: user.password) described_class.bypass_session!(user.id)
end
it 'user is available only inside the yielded block' do expect(Gitlab::Session.current).to be_nil
described_class.with_current_admin(user) do expect(subject.admin_mode?).to be(false)
expect(described_class.current_admin).to be(user) expect(described_class.bypass_session_admin_id).to be(user.id)
described_class.reset_bypass_session!
expect(described_class.bypass_session_admin_id).to be_nil
end end
end
expect(described_class.current_admin).to be_nil context 'with an admin user' do
let(:user) { build_stubbed(:user, :admin) }
it 'admin mode is true' do
described_class.bypass_session!(user.id)
expect(Gitlab::Session.current).to be_nil
expect(subject.admin_mode?).to be(true)
expect(described_class.bypass_session_admin_id).to be(user.id)
described_class.reset_bypass_session!
expect(described_class.bypass_session_admin_id).to be_nil
end
end end
end end
end end
end end
def expected_session_entry(value_matcher)
{
Gitlab::Auth::CurrentUserMode::SESSION_STORE_KEY => a_hash_including(
Gitlab::Auth::CurrentUserMode::ADMIN_MODE_START_TIME_KEY => value_matcher)
}
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe API::APIGuard::AdminModeMiddleware, :do_not_mock_admin_mode, :request_store do
let(:user) { create(:admin) }
it 'is loaded' do
expect(API::API.middleware).to include([:use, described_class])
end
context 'when there is an exception in the api call' do
let(:app) do
Class.new(API::API) do
get 'willfail' do
raise StandardError.new('oh noes!')
end
end
end
it 'resets admin mode' do
Gitlab::Auth::CurrentUserMode.bypass_session!(user.id)
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be(user.id)
expect(Gitlab::Auth::CurrentUserMode).to receive(:reset_bypass_session!).and_call_original
get api('/willfail')
expect(response.status).to eq(500)
expect(response.body).to include('oh noes!')
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
end
end
end
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
# Based on spec/requests/api/groups_spec.rb # Based on spec/requests/api/groups_spec.rb
# Should follow closely in order to ensure all situations are covered # Should follow closely in order to ensure all situations are covered
describe 'getting group information' do describe 'getting group information', :do_not_mock_admin_mode do
include GraphqlHelpers include GraphqlHelpers
include UploadHelpers include UploadHelpers
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe 'Mark snippet as spam' do describe 'Mark snippet as spam', :do_not_mock_admin_mode do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe API::Users do describe API::Users, :do_not_mock_admin_mode do
let(:user) { create(:user, username: 'user.with.dot') } let(:user) { create(:user, username: 'user.with.dot') }
let(:admin) { create(:admin) } let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) } let(:key) { create(:key, user: user) }
......
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