Commit 70b19fbd authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'if-40385-prohibit_impersonation' into 'master'

Add config to prohibit impersonation

See merge request gitlab-org/gitlab-ce!23338
parents 29901131 bd3a4840
...@@ -5,23 +5,12 @@ class Admin::ImpersonationsController < Admin::ApplicationController ...@@ -5,23 +5,12 @@ class Admin::ImpersonationsController < Admin::ApplicationController
before_action :authenticate_impersonator! before_action :authenticate_impersonator!
def destroy def destroy
original_user = current_user original_user = stop_impersonation
warden.set_user(impersonator, scope: :user)
Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}")
session[:impersonator_id] = nil
redirect_to admin_user_path(original_user), status: :found redirect_to admin_user_path(original_user), status: :found
end end
private private
def impersonator
@impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
end
def authenticate_impersonator! def authenticate_impersonator!
render_404 unless impersonator && impersonator.admin? && !impersonator.blocked? render_404 unless impersonator && impersonator.admin? && !impersonator.blocked?
end end
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class Admin::UsersController < Admin::ApplicationController class Admin::UsersController < Admin::ApplicationController
before_action :user, except: [:index, :new, :create] before_action :user, except: [:index, :new, :create]
before_action :check_impersonation_availability, only: :impersonate
def index def index
@users = User.order_name_asc.filter(params[:filter]) @users = User.order_name_asc.filter(params[:filter])
...@@ -227,4 +228,8 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -227,4 +228,8 @@ class Admin::UsersController < Admin::ApplicationController
result[:status] == :success result[:status] == :success
end end
def check_impersonation_availability
access_denied! unless Gitlab.config.gitlab.impersonation_enabled
end
end end
...@@ -28,6 +28,7 @@ class ApplicationController < ActionController::Base ...@@ -28,6 +28,7 @@ class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller? before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller? before_action :require_email, unless: :devise_controller?
before_action :set_usage_stats_consent_flag before_action :set_usage_stats_consent_flag
before_action :check_impersonation_availability
around_action :set_locale around_action :set_locale
...@@ -462,4 +463,28 @@ class ApplicationController < ActionController::Base ...@@ -462,4 +463,28 @@ class ApplicationController < ActionController::Base
.new(settings, current_user, application_setting_params) .new(settings, current_user, application_setting_params)
.execute .execute
end end
def check_impersonation_availability
return unless session[:impersonator_id]
unless Gitlab.config.gitlab.impersonation_enabled
stop_impersonation
access_denied! _('Impersonation has been disabled')
end
end
def stop_impersonation
impersonated_user = current_user
Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{impersonated_user.username}")
warden.set_user(impersonator, scope: :user)
session[:impersonator_id] = nil
impersonated_user
end
def impersonator
@impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id]
end
end end
...@@ -70,6 +70,10 @@ module UsersHelper ...@@ -70,6 +70,10 @@ module UsersHelper
end end
end end
def impersonation_enabled?
Gitlab.config.gitlab.impersonation_enabled
end
private private
def get_profile_tabs def get_profile_tabs
......
...@@ -6,6 +6,7 @@ class AccessTokenValidationService ...@@ -6,6 +6,7 @@ class AccessTokenValidationService
EXPIRED = :expired EXPIRED = :expired
REVOKED = :revoked REVOKED = :revoked
INSUFFICIENT_SCOPE = :insufficient_scope INSUFFICIENT_SCOPE = :insufficient_scope
IMPERSONATION_DISABLED = :impersonation_disabled
attr_reader :token, :request attr_reader :token, :request
...@@ -24,6 +25,11 @@ class AccessTokenValidationService ...@@ -24,6 +25,11 @@ class AccessTokenValidationService
elsif !self.include_any_scope?(scopes) elsif !self.include_any_scope?(scopes)
return INSUFFICIENT_SCOPE return INSUFFICIENT_SCOPE
elsif token.respond_to?(:impersonation) &&
token.impersonation &&
!Gitlab.config.gitlab.impersonation_enabled
return IMPERSONATION_DISABLED
else else
return VALID return VALID
end end
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%span.cred (Admin) %span.cred (Admin)
.float-right .float-right
- if @user != current_user && @user.can?(:log_in) - if impersonation_enabled? && @user != current_user && @user.can?(:log_in)
= link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info" = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
......
---
title: Add config to prohibit impersonation
merge_request: 23338
author:
type: added
...@@ -114,6 +114,9 @@ production: &base ...@@ -114,6 +114,9 @@ production: &base
# The default is 'shared/cache/archive/' relative to the root of the Rails app. # The default is 'shared/cache/archive/' relative to the root of the Rails app.
# repository_downloads_path: shared/cache/archive/ # repository_downloads_path: shared/cache/archive/
## Impersonation settings
impersonation_enabled: true
## Reply by email ## Reply by email
# Allow users to comment on issues and merge requests by replying to notification emails. # Allow users to comment on issues and merge requests by replying to notification emails.
# For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html # For documentation on how to set this up, see http://doc.gitlab.com/ce/administration/reply_by_email.html
......
...@@ -153,6 +153,7 @@ Settings.gitlab['domain_whitelist'] ||= [] ...@@ -153,6 +153,7 @@ Settings.gitlab['domain_whitelist'] ||= []
Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['trusted_proxies'] ||= []
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
# #
......
...@@ -225,6 +225,43 @@ For more information, refer to the ...@@ -225,6 +225,43 @@ For more information, refer to the
Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
`private_token` parameter or the `Private-Token` header. `private_token` parameter or the `Private-Token` header.
#### Disable impersonation
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/40385) in GitLab
11.6.
By default, impersonation is enabled. To disable impersonation, GitLab must be
reconfigured:
**For Omnibus installations**
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['impersonation_enabled'] = false
```
1. Save the file and [reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
GitLab for the changes to take effect.
To re-enable impersonation, remove this configuration and reconfigure GitLab.
---
**For installations from source**
1. Edit `config/gitlab.yml`:
```yaml
gitlab:
impersonation_enabled: false
```
1. Save the file and [restart](../administration/restart_gitlab.md#installations-from-source)
GitLab for the changes to take effect.
To re-enable impersonation, remove this configuration and restart GitLab.
### Sudo ### Sudo
NOTE: **Note:** NOTE: **Note:**
...@@ -540,7 +577,7 @@ When you try to access an API URL that does not exist you will receive 404 Not F ...@@ -540,7 +577,7 @@ When you try to access an API URL that does not exist you will receive 404 Not F
``` ```
HTTP/1.1 404 Not Found HTTP/1.1 404 Not Found
Content-Type: application/json Content-Type: application/json
{ { f
"error": "404 Not Found" "error": "404 Not Found"
} }
``` ```
......
...@@ -94,6 +94,7 @@ module API ...@@ -94,6 +94,7 @@ module API
Gitlab::Auth::TokenNotFoundError, Gitlab::Auth::TokenNotFoundError,
Gitlab::Auth::ExpiredError, Gitlab::Auth::ExpiredError,
Gitlab::Auth::RevokedError, Gitlab::Auth::RevokedError,
Gitlab::Auth::ImpersonationDisabled,
Gitlab::Auth::InsufficientScopeError] Gitlab::Auth::InsufficientScopeError]
base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend base.__send__(:rescue_from, *error_classes, oauth2_bearer_token_error_handler) # rubocop:disable GitlabSecurity/PublicSend
...@@ -121,6 +122,11 @@ module API ...@@ -121,6 +122,11 @@ module API
:invalid_token, :invalid_token,
"Token was revoked. You have to re-authorize from the user.") "Token was revoked. You have to re-authorize from the user.")
when Gitlab::Auth::ImpersonationDisabled
Rack::OAuth2::Server::Resource::Bearer::Unauthorized.new(
:invalid_token,
"Token is an impersonation token but impersonation was disabled.")
when Gitlab::Auth::InsufficientScopeError when Gitlab::Auth::InsufficientScopeError
# FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2) # FIXME: ForbiddenError (inherited from Bearer::Forbidden of Rack::Oauth2)
# does not include WWW-Authenticate header, which breaks the standard. # does not include WWW-Authenticate header, which breaks the standard.
......
...@@ -7,6 +7,7 @@ module Gitlab ...@@ -7,6 +7,7 @@ module Gitlab
TokenNotFoundError = Class.new(AuthenticationError) TokenNotFoundError = Class.new(AuthenticationError)
ExpiredError = Class.new(AuthenticationError) ExpiredError = Class.new(AuthenticationError)
RevokedError = Class.new(AuthenticationError) RevokedError = Class.new(AuthenticationError)
ImpersonationDisabled = Class.new(AuthenticationError)
UnauthorizedError = Class.new(AuthenticationError) UnauthorizedError = Class.new(AuthenticationError)
class InsufficientScopeError < AuthenticationError class InsufficientScopeError < AuthenticationError
...@@ -67,6 +68,8 @@ module Gitlab ...@@ -67,6 +68,8 @@ module Gitlab
raise ExpiredError raise ExpiredError
when AccessTokenValidationService::REVOKED when AccessTokenValidationService::REVOKED
raise RevokedError raise RevokedError
when AccessTokenValidationService::IMPERSONATION_DISABLED
raise ImpersonationDisabled
end end
end end
......
...@@ -3448,6 +3448,9 @@ msgstr "" ...@@ -3448,6 +3448,9 @@ msgstr ""
msgid "ImageDiffViewer|Swipe" msgid "ImageDiffViewer|Swipe"
msgstr "" msgstr ""
msgid "Impersonation has been disabled"
msgstr ""
msgid "Import" msgid "Import"
msgstr "" msgstr ""
......
...@@ -264,5 +264,17 @@ describe Admin::UsersController do ...@@ -264,5 +264,17 @@ describe Admin::UsersController do
expect(flash[:alert]).to eq("You are now impersonating #{user.username}") expect(flash[:alert]).to eq("You are now impersonating #{user.username}")
end end
end end
context "when impersonation is disabled" do
before do
stub_config_setting(impersonation_enabled: false)
end
it "shows error page" do
post :impersonate, id: user.username
expect(response).to have_gitlab_http_status(404)
end
end
end end
end end
...@@ -205,77 +205,120 @@ describe "Admin::Users" do ...@@ -205,77 +205,120 @@ describe "Admin::Users" do
describe 'Impersonation' do describe 'Impersonation' do
let(:another_user) { create(:user) } let(:another_user) { create(:user) }
before do
visit admin_user_path(another_user)
end
context 'before impersonating' do context 'before impersonating' do
subject { visit admin_user_path(user_to_visit) }
let(:user_to_visit) { another_user }
context 'for other users' do
it 'shows impersonate button for other users' do it 'shows impersonate button for other users' do
subject
expect(page).to have_content('Impersonate') expect(page).to have_content('Impersonate')
end end
end
context 'for admin itself' do
let(:user_to_visit) { current_user }
it 'does not show impersonate button for admin itself' do it 'does not show impersonate button for admin itself' do
visit admin_user_path(current_user) subject
expect(page).not_to have_content('Impersonate') expect(page).not_to have_content('Impersonate')
end end
end
it 'does not show impersonate button for blocked user' do context 'for blocked user' do
before do
another_user.block another_user.block
end
visit admin_user_path(another_user) it 'does not show impersonate button for blocked user' do
subject
expect(page).not_to have_content('Impersonate') expect(page).not_to have_content('Impersonate')
end
end
another_user.activate context 'when impersonation is disabled' do
before do
stub_config_setting(impersonation_enabled: false)
end
it 'does not show impersonate button' do
subject
expect(page).not_to have_content('Impersonate')
end
end end
end end
context 'when impersonating' do context 'when impersonating' do
subject { click_link 'Impersonate' }
before do before do
click_link 'Impersonate' visit admin_user_path(another_user)
end end
it 'logs in as the user when impersonate is clicked' do it 'logs in as the user when impersonate is clicked' do
subject
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username) expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(another_user.username)
end end
it 'sees impersonation log out icon' do it 'sees impersonation log out icon' do
icon = first('.fa.fa-user-secret') subject
icon = first('.fa.fa-user-secret')
expect(icon).not_to be nil expect(icon).not_to be nil
end end
context 'a user with an expired password' do
before do
another_user.update(password_expires_at: Time.now - 5.minutes)
end
it 'does not redirect to password change page' do
subject
expect(current_path).to eq('/')
end
end
end
context 'ending impersonation' do
subject { find(:css, 'li.impersonation a').click }
before do
visit admin_user_path(another_user)
click_link 'Impersonate'
end
it 'logs out of impersonated user back to original user' do it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click subject
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username) expect(page.find(:css, '.header-user .profile-link')['data-user']).to eq(current_user.username)
end end
it 'is redirected back to the impersonated users page in the admin after stopping' do it 'is redirected back to the impersonated users page in the admin after stopping' do
find(:css, 'li.impersonation a').click subject
expect(current_path).to eq("/admin/users/#{another_user.username}") expect(current_path).to eq("/admin/users/#{another_user.username}")
end end
end
context 'when impersonating a user with an expired password' do context 'a user with an expired password' do
before do before do
another_user.update(password_expires_at: Time.now - 5.minutes) another_user.update(password_expires_at: Time.now - 5.minutes)
click_link 'Impersonate'
end
it 'does not redirect to password change page' do
expect(current_path).to eq('/')
end end
it 'is redirected back to the impersonated users page in the admin after stopping' do it 'is redirected back to the impersonated users page in the admin after stopping' do
find(:css, 'li.impersonation a').click subject
expect(current_path).to eq("/admin/users/#{another_user.username}") expect(current_path).to eq("/admin/users/#{another_user.username}")
end end
end end
end end
end
describe 'Two-factor Authentication status' do describe 'Two-factor Authentication status' do
it 'shows when enabled' do it 'shows when enabled' do
......
...@@ -279,5 +279,20 @@ describe Gitlab::Auth::UserAuthFinders do ...@@ -279,5 +279,20 @@ describe Gitlab::Auth::UserAuthFinders do
expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError) expect { validate_access_token!(scopes: [:sudo]) }.to raise_error(Gitlab::Auth::InsufficientScopeError)
end end
end end
context 'with impersonation token' do
let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
context 'when impersonation is disabled' do
before do
stub_config_setting(impersonation_enabled: false)
allow_any_instance_of(described_class).to receive(:access_token).and_return(personal_access_token)
end
it 'returns Gitlab::Auth::ImpersonationDisabled' do
expect { validate_access_token! }.to raise_error(Gitlab::Auth::ImpersonationDisabled)
end
end
end
end end
end end
...@@ -206,6 +206,19 @@ describe API::Helpers do ...@@ -206,6 +206,19 @@ describe API::Helpers do
expect { current_user }.to raise_error Gitlab::Auth::ExpiredError expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
end end
context 'when impersonation is disabled' do
let(:personal_access_token) { create(:personal_access_token, :impersonation, user: user) }
before do
stub_config_setting(impersonation_enabled: false)
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
end
it 'does not allow impersonation tokens' do
expect { current_user }.to raise_error Gitlab::Auth::ImpersonationDisabled
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