Skip to content

Commit

Permalink
Fixes issue #2983 - HTTP 401 responses causing issues with Basic Auth…
Browse files Browse the repository at this point in the history
…entication.
  • Loading branch information
thorsteneckel committed Feb 4, 2021
1 parent 5a65e5b commit 876c0b1
Show file tree
Hide file tree
Showing 52 changed files with 370 additions and 291 deletions.
Expand Up @@ -148,8 +148,8 @@ class ProfileOutOfOffice extends App.ControllerSubContent
data = JSON.parse(xhr.responseText)

# show error message
if xhr.status is 401 || error is 'Unauthorized'
message = '» ' + App.i18n.translateInline('Unauthorized') + ' «'
if xhr.status is 403 || error is 'Not authorized'
message = '» ' + App.i18n.translateInline('Not authorized') + ' «'
else if xhr.status is 404 || error is 'Not Found'
message = '» ' + App.i18n.translateInline('Not Found') + ' «'
else if data.error
Expand Down
4 changes: 2 additions & 2 deletions app/assets/javascripts/app/controllers/ticket_zoom.coffee
Expand Up @@ -115,8 +115,8 @@ class App.TicketZoom extends App.Controller
return

# show error message
if status is 401 || statusText is 'Unauthorized'
@taskHead = '» ' + App.i18n.translateInline('Unauthorized') + ' «'
if status is 403 || statusText is 'Not authorized'
@taskHead = '» ' + App.i18n.translateInline('Not authorized') + ' «'
@taskIconClass = 'diagonal-cross'
@renderScreenUnauthorized(objectName: 'Ticket')
else if status is 404 || statusText is 'Not Found'
Expand Down
3 changes: 2 additions & 1 deletion app/assets/javascripts/app/lib/app_post/ajax.coffee
Expand Up @@ -93,8 +93,9 @@ class _ajaxSingleton
# 200, all is fine
return if status is 200

# do not show any error message with code 401/404 (handled by controllers)
# do not show any error message for various 4** codes (handled by controllers)
return if status is 401
return if status is 403
return if status is 404
return if status is 422

Expand Down
29 changes: 20 additions & 9 deletions app/controllers/application_controller/authenticates.rb
Expand Up @@ -14,12 +14,12 @@ def permission_check(key)
)
return false if user

raise Exceptions::NotAuthorized, 'Not authorized (token)!'
raise Exceptions::Forbidden, 'Not authorized (token)!'
end

return false if current_user&.permissions?(key)

raise Exceptions::NotAuthorized, 'Not authorized (user)!'
raise Exceptions::Forbidden, 'Not authorized (user)!'
end

def authentication_check(auth_param = {})
Expand All @@ -33,7 +33,7 @@ def authentication_check(auth_param = {})

# return auth not ok
if !user
raise Exceptions::NotAuthorized, 'authentication failed'
raise Exceptions::Forbidden, 'Authentication required'
end

# return auth ok
Expand All @@ -45,32 +45,37 @@ def authentication_check_only(auth_param = {})
#logger.debug params.inspect
#logger.debug session.inspect
#logger.debug cookies.inspect
authentication_errors = []

# already logged in, early exit
if session.id && session[:user_id]
logger.debug { 'session based auth check' }
user = User.lookup(id: session[:user_id])
return authentication_check_prerequesits(user, 'session', auth_param) if user

authentication_errors.push("Can't find User with ID #{session[:user_id]} from Session")
end

# check http basic based authentication
authenticate_with_http_basic do |username, password|
request.session_options[:skip] = true # do not send a session cookie
logger.debug { "http basic auth check '#{username}'" }
if Setting.get('api_password_access') == false
raise Exceptions::NotAuthorized, 'API password access disabled!'
raise Exceptions::Forbidden, 'API password access disabled!'
end

user = User.authenticate(username, password)
return authentication_check_prerequesits(user, 'basic_auth', auth_param) if user

authentication_errors.push('Invalid BasicAuth credentials')
end

# check http token based authentication
authenticate_with_http_token do |token_string, _options|
logger.debug { "http token auth check '#{token_string}'" }
request.session_options[:skip] = true # do not send a session cookie
if Setting.get('api_token_access') == false
raise Exceptions::NotAuthorized, 'API token access disabled!'
raise Exceptions::Forbidden, 'API token access disabled!'
end

user = Token.check(
Expand Down Expand Up @@ -106,13 +111,15 @@ def authentication_check_only(auth_param = {})

@_token_auth = token_string # remember for permission_check
return authentication_check_prerequesits(user, 'token_auth', auth_param) if user

authentication_errors.push("Can't find User for Token")
end

# check oauth2 token based authentication
token = Doorkeeper::OAuth::Token.from_bearer_authorization(request)
if token
request.session_options[:skip] = true # do not send a session cookie
logger.debug { "oauth2 token auth check '#{token}'" }
logger.debug { "OAuth2 token auth check '#{token}'" }
access_token = Doorkeeper::AccessToken.by_token(token)

raise Exceptions::NotAuthorized, 'Invalid token!' if !access_token
Expand All @@ -128,9 +135,13 @@ def authentication_check_only(auth_param = {})

user = User.find(access_token.resource_owner_id)
return authentication_check_prerequesits(user, 'token_auth', auth_param) if user

authentication_errors.push("Can't find User with ID #{access_token.resource_owner_id} for OAuth2 token")
end

false
return false if authentication_errors.blank?

raise Exceptions::NotAuthorized, authentication_errors.join(', ')
end

def authenticate_with_password
Expand All @@ -142,15 +153,15 @@ def authenticate_with_password
end

def authentication_check_prerequesits(user, auth_type, auth_param)
raise Exceptions::NotAuthorized, 'Maintenance mode enabled!' if in_maintenance_mode?(user)
raise Exceptions::Forbidden, 'Maintenance mode enabled!' if in_maintenance_mode?(user)

raise_unified_login_error if !user.active

if auth_param[:permission]
ActiveSupport::Deprecation.warn("Parameter ':permission' is deprecated. Use Pundit policy and `authorize!` instead.")

if !user.permissions?(auth_param[:permission])
raise Exceptions::NotAuthorized, 'Not authorized (user)!'
raise Exceptions::Forbidden, 'Not authorized (user)!'
end
end

Expand Down
2 changes: 1 addition & 1 deletion app/controllers/application_controller/authorizes.rb
Expand Up @@ -11,7 +11,7 @@ def authorize!(record = policy_record, query = nil)
def authorized?(record = policy_record, query = nil)
authorize!(record, query)
true
rescue Exceptions::NotAuthorized, Pundit::NotAuthorizedError
rescue Exceptions::Forbidden, Pundit::NotAuthorizedError
false
end

Expand Down
20 changes: 16 additions & 4 deletions app/controllers/application_controller/handles_errors.rb
Expand Up @@ -11,6 +11,7 @@ module ApplicationController::HandlesErrors
rescue_from ArgumentError, with: :unprocessable_entity
rescue_from Exceptions::UnprocessableEntity, with: :unprocessable_entity
rescue_from Exceptions::NotAuthorized, with: :unauthorized
rescue_from Exceptions::Forbidden, with: :forbidden
rescue_from Pundit::NotAuthorizedError, with: :pundit_not_authorized_error
end

Expand Down Expand Up @@ -40,13 +41,21 @@ def unauthorized(e)
http_log
end

def forbidden(e)
logger.info { e }
error = humanize_error(e)
response.headers['X-Failure'] = error.fetch(:error_human, error[:error])
respond_to_exception(e, :forbidden)
http_log
end

def pundit_not_authorized_error(e)
logger.info { e }
# check if a special authorization_error should be shown in the result payload
# which was raised in one of the policies. Fall back to a simple "Not authorized"
# error to hide actual cause for security reasons.
exeption = e.policy.custom_exception || Exceptions::NotAuthorized.new
unauthorized(exeption)
exeption = e.policy&.custom_exception || Exceptions::Forbidden.new('Not authorized')
forbidden(exeption)
end

private
Expand Down Expand Up @@ -79,10 +88,13 @@ def humanize_error(e)
data[:error_human] = 'Object already exists!'
elsif e.message =~ /null value in column "(.+?)" violates not-null constraint/i || e.message =~ /Field '(.+?)' doesn't have a default value/i
data[:error_human] = "Attribute '#{$1}' required!"
elsif e.message == 'Exceptions::NotAuthorized'
elsif e.message == 'Exceptions::Forbidden'
data[:error] = 'Not authorized'
data[:error_human] = data[:error]
elsif [ActionController::RoutingError, ActiveRecord::RecordNotFound, Exceptions::UnprocessableEntity, Exceptions::NotAuthorized].include?(e.class)
elsif e.message == 'Exceptions::NotAuthorized'
data[:error] = 'Authorization failed'
data[:error_human] = data[:error]
elsif [ActionController::RoutingError, ActiveRecord::RecordNotFound, Exceptions::UnprocessableEntity, Exceptions::NotAuthorized, Exceptions::Forbidden].include?(e.class)
data[:error_human] = data[:error]
end

Expand Down
4 changes: 2 additions & 2 deletions app/controllers/application_controller/has_user.rb
Expand Up @@ -43,7 +43,7 @@ def current_user_on_behalf
return if !user_real

# check if the user has admin rights
raise Exceptions::NotAuthorized, "Current user has no permission to use 'X-On-Behalf-Of'!" if !user_real.permissions?('admin.user')
raise Exceptions::Forbidden, "Current user has no permission to use 'X-On-Behalf-Of'!" if !user_real.permissions?('admin.user')

# find user for execution based on the header
%i[id login email].each do |field|
Expand All @@ -55,7 +55,7 @@ def current_user_on_behalf
end

# no behalf of user found
raise Exceptions::NotAuthorized, "No such user '#{request.headers['X-On-Behalf-Of']}'"
raise Exceptions::Forbidden, "No such user '#{request.headers['X-On-Behalf-Of']}'"
end

def search_attributes(field)
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/attachments_controller.rb
Expand Up @@ -75,7 +75,7 @@ def sanitized_disposition
valid_disposition = %w[inline attachment]
return disposition if valid_disposition.include?(disposition)

raise Exceptions::NotAuthorized, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid."
raise Exceptions::Forbidden, "Invalid disposition #{disposition} requested. Only #{valid_disposition.join(', ')} are valid."
end

def verify_object_permissions
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/calendar_subscriptions_controller.rb
Expand Up @@ -7,7 +7,8 @@ class CalendarSubscriptionsController < ApplicationController
# @summary Returns an iCal file with all objects matching the calendar subscriptions preferences of the current user as events.
#
# @response_message 200 [String] iCal file ready to import in calendar applications.
# @response_message 401 Permission denied.
# @response_message 403 Forbidden / Invalid session.
# @response_message 422 Unprocessable Entity.
def all
calendar_subscriptions = CalendarSubscriptions.new(current_user)
ical = calendar_subscriptions.all
Expand All @@ -29,7 +30,8 @@ def all
# @summary Returns an iCal file of the given object (and method) matching the calendar subscriptions preferences of the current user as events.
#
# @response_message 200 [String] iCal file ready to import in calendar applications.
# @response_message 401 Permission denied.
# @response_message 403 Forbidden / Invalid session.
# @response_message 422 Unprocessable Entity.
def object
calendar_subscriptions = CalendarSubscriptions.new(current_user)
ical = calendar_subscriptions.generic(params[:object], params[:method])
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/channels_email_controller.rb
Expand Up @@ -267,7 +267,7 @@ def account_duplicate?(result, channel_id = nil)
def check_online_service
return true if !Setting.get('system_online_service')

raise Exceptions::NotAuthorized
raise Exceptions::Forbidden
end

def check_access(id = nil)
Expand All @@ -279,6 +279,6 @@ def check_access(id = nil)
channel = Channel.find(id)
return true if channel.preferences && !channel.preferences[:online_service_disable]

raise Exceptions::NotAuthorized
raise Exceptions::Forbidden
end
end
12 changes: 6 additions & 6 deletions app/controllers/form_controller.rb
Expand Up @@ -149,7 +149,7 @@ def submit
def authorize!(*)
super
rescue Pundit::NotAuthorizedError
raise Exceptions::NotAuthorized
raise Exceptions::Forbidden
end

def token_gen(fingerprint)
Expand All @@ -161,7 +161,7 @@ def token_gen(fingerprint)
def token_valid?(token, fingerprint)
if token.blank?
Rails.logger.info 'No token for form!'
raise Exceptions::NotAuthorized
raise Exceptions::Forbidden
end
begin
crypt = ActiveSupport::MessageEncryptor.new(Setting.get('application_secret')[0, 32])
Expand Down Expand Up @@ -205,15 +205,15 @@ def limit_reached?
# in elasticsearch7 "created_at:>now-1h" is not working. Needed to catch -2h
form_limit_by_ip_per_hour = Setting.get('form_ticket_create_by_ip_per_hour') || 20
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{remote_ip}' AND created_at:>now-2h", 'Ticket', limit: form_limit_by_ip_per_hour)
raise Exceptions::NotAuthorized if result.count >= form_limit_by_ip_per_hour.to_i
raise Exceptions::Forbidden if result.count >= form_limit_by_ip_per_hour.to_i

form_limit_by_ip_per_day = Setting.get('form_ticket_create_by_ip_per_day') || 240
result = SearchIndexBackend.search("preferences.form.remote_ip:'#{remote_ip}' AND created_at:>now-1d", 'Ticket', limit: form_limit_by_ip_per_day)
raise Exceptions::NotAuthorized if result.count >= form_limit_by_ip_per_day.to_i
raise Exceptions::Forbidden if result.count >= form_limit_by_ip_per_day.to_i

form_limit_per_day = Setting.get('form_ticket_create_per_day') || 5000
result = SearchIndexBackend.search('preferences.form.remote_ip:* AND created_at:>now-1d', 'Ticket', limit: form_limit_per_day)
raise Exceptions::NotAuthorized if result.count >= form_limit_per_day.to_i
raise Exceptions::Forbidden if result.count >= form_limit_per_day.to_i

false
end
Expand All @@ -222,6 +222,6 @@ def fingerprint_exists?
return true if params[:fingerprint].present? && params[:fingerprint].length > 30

Rails.logger.info 'No fingerprint given!'
raise Exceptions::NotAuthorized
raise Exceptions::Forbidden
end
end
4 changes: 2 additions & 2 deletions app/controllers/organizations_controller.rb
Expand Up @@ -265,7 +265,7 @@ def history
# @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/organizations/import_example
#
# @response_message 200 File download.
# @response_message 401 Invalid session.
# @response_message 403 Forbidden / Invalid session.
def import_example
send_data(
Organization.csv_example,
Expand All @@ -283,7 +283,7 @@ def import_example
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/organizations.csv' 'https://your.zammad/api/v1/organizations/import'
#
# @response_message 201 Import started.
# @response_message 401 Invalid session.
# @response_message 403 Forbidden / Invalid session.
def import_start
string = params[:data]
if string.blank? && params[:file].present?
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/sessions_controller.rb
Expand Up @@ -16,7 +16,7 @@ def create
end

def create_sso
raise Exceptions::NotAuthorized, 'SSO authentication disabled!' if !Setting.get('auth_sso')
raise Exceptions::Forbidden, 'SSO authentication disabled!' if !Setting.get('auth_sso')

user = begin
login = request.env['REMOTE_USER'] ||
Expand Down Expand Up @@ -150,7 +150,7 @@ def switch_to_user
def switch_back_to_user

# check if it's a switch back
raise Exceptions::NotAuthorized if !session[:switched_from_user_id]
raise Exceptions::Forbidden if !session[:switched_from_user_id]

user = User.lookup(id: session[:switched_from_user_id])
if !user
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/settings_controller.rb
Expand Up @@ -21,7 +21,7 @@ def show

# POST /settings
def create
raise Exceptions::NotAuthorized, 'Not authorized (feature not possible)'
raise Exceptions::Forbidden, 'Not authorized (feature not possible)'
end

# PUT /settings/1
Expand Down Expand Up @@ -84,7 +84,7 @@ def update_image

# DELETE /settings/1
def destroy
raise Exceptions::NotAuthorized, 'Not authorized (feature not possible)'
raise Exceptions::Forbidden, 'Not authorized (feature not possible)'
end

private
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/text_modules_controller.rb
Expand Up @@ -155,7 +155,7 @@ def destroy
# @example curl -u 'me@example.com:test' http://localhost:3000/api/v1/text_modules/import_example
#
# @response_message 200 File download.
# @response_message 401 Invalid session.
# @response_message 403 Forbidden / Invalid session.
def import_example
csv_string = TextModule.csv_example(
col_sep: params[:col_sep] || ',',
Expand All @@ -177,7 +177,7 @@ def import_example
# @example curl -u 'me@example.com:test' -F 'file=@/path/to/file/Textbausteine_final2.csv' 'https://your.zammad/api/v1/text_modules/import'
#
# @response_message 201 Import started.
# @response_message 401 Invalid session.
# @response_message 403 Forbidden / Invalid session.
def import_start
string = params[:data]
if string.blank? && params[:file].present?
Expand Down

0 comments on commit 876c0b1

Please sign in to comment.