Fixes #2389 - Enhance LDAP login to not affect "failed logins".
This commit is contained in:
parent
28829c334b
commit
467bc03224
37 changed files with 1214 additions and 706 deletions
|
@ -38,9 +38,6 @@ RSpec/ContextWording:
|
||||||
- 'spec/jobs/imap_authentication_migration_cleanup_job_spec.rb'
|
- 'spec/jobs/imap_authentication_migration_cleanup_job_spec.rb'
|
||||||
- 'spec/jobs/ticket_article_communicate_email_job_spec.rb'
|
- 'spec/jobs/ticket_article_communicate_email_job_spec.rb'
|
||||||
- 'spec/lib/application_handle_info_spec.rb'
|
- 'spec/lib/application_handle_info_spec.rb'
|
||||||
- 'spec/lib/auth/developer_spec.rb'
|
|
||||||
- 'spec/lib/auth/ldap_spec.rb'
|
|
||||||
- 'spec/lib/auth_spec.rb'
|
|
||||||
- 'spec/lib/core_ext/string_spec.rb'
|
- 'spec/lib/core_ext/string_spec.rb'
|
||||||
- 'spec/lib/external_credential/google_spec.rb'
|
- 'spec/lib/external_credential/google_spec.rb'
|
||||||
- 'spec/lib/external_credential/microsoft365_spec.rb'
|
- 'spec/lib/external_credential/microsoft365_spec.rb'
|
||||||
|
@ -178,8 +175,6 @@ RSpec/ExampleLength:
|
||||||
- 'spec/jobs/migrate_ldap_samaccountname_to_uid_job_spec.rb'
|
- 'spec/jobs/migrate_ldap_samaccountname_to_uid_job_spec.rb'
|
||||||
- 'spec/jobs/ticket_user_ticket_counter_job_spec.rb'
|
- 'spec/jobs/ticket_user_ticket_counter_job_spec.rb'
|
||||||
- 'spec/jobs/user_device_log_job_spec.rb'
|
- 'spec/jobs/user_device_log_job_spec.rb'
|
||||||
- 'spec/lib/auth/internal_spec.rb'
|
|
||||||
- 'spec/lib/auth/ldap_spec.rb'
|
|
||||||
- 'spec/lib/auto_wizard_spec.rb'
|
- 'spec/lib/auto_wizard_spec.rb'
|
||||||
- 'spec/lib/core_ext/string_spec.rb'
|
- 'spec/lib/core_ext/string_spec.rb'
|
||||||
- 'spec/lib/external_credential/google_spec.rb'
|
- 'spec/lib/external_credential/google_spec.rb'
|
||||||
|
@ -395,8 +390,6 @@ RSpec/MessageSpies:
|
||||||
- 'spec/jobs/app_version_restart_job_spec.rb'
|
- 'spec/jobs/app_version_restart_job_spec.rb'
|
||||||
- 'spec/jobs/search_index_job_spec.rb'
|
- 'spec/jobs/search_index_job_spec.rb'
|
||||||
- 'spec/jobs/sla_ticket_rebuild_escalation_job_spec.rb'
|
- 'spec/jobs/sla_ticket_rebuild_escalation_job_spec.rb'
|
||||||
- 'spec/lib/auth/developer_spec.rb'
|
|
||||||
- 'spec/lib/auth/ldap_spec.rb'
|
|
||||||
- 'spec/lib/import/base_factory_examples.rb'
|
- 'spec/lib/import/base_factory_examples.rb'
|
||||||
- 'spec/lib/import/helper_spec.rb'
|
- 'spec/lib/import/helper_spec.rb'
|
||||||
- 'spec/lib/import/otrs/article/attachment_factory_spec.rb'
|
- 'spec/lib/import/otrs/article/attachment_factory_spec.rb'
|
||||||
|
@ -472,10 +465,6 @@ RSpec/MultipleExpectations:
|
||||||
- 'spec/jobs/migrate_ldap_samaccountname_to_uid_job_spec.rb'
|
- 'spec/jobs/migrate_ldap_samaccountname_to_uid_job_spec.rb'
|
||||||
- 'spec/jobs/search_index_job_spec.rb'
|
- 'spec/jobs/search_index_job_spec.rb'
|
||||||
- 'spec/jobs/ticket_user_ticket_counter_job_spec.rb'
|
- 'spec/jobs/ticket_user_ticket_counter_job_spec.rb'
|
||||||
- 'spec/lib/auth/developer_spec.rb'
|
|
||||||
- 'spec/lib/auth/internal_spec.rb'
|
|
||||||
- 'spec/lib/auth/ldap_spec.rb'
|
|
||||||
- 'spec/lib/auth_spec.rb'
|
|
||||||
- 'spec/lib/cache_spec.rb'
|
- 'spec/lib/cache_spec.rb'
|
||||||
- 'spec/lib/core_ext/string_spec.rb'
|
- 'spec/lib/core_ext/string_spec.rb'
|
||||||
- 'spec/lib/email_address_validation_spec.rb'
|
- 'spec/lib/email_address_validation_spec.rb'
|
||||||
|
@ -667,7 +656,6 @@ RSpec/VerifiedDoubles:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'spec/db/migrate/issue_2460_fix_corrupted_twitter_ids_spec.rb'
|
- 'spec/db/migrate/issue_2460_fix_corrupted_twitter_ids_spec.rb'
|
||||||
- 'spec/jobs/communicate_twitter_job_spec.rb'
|
- 'spec/jobs/communicate_twitter_job_spec.rb'
|
||||||
- 'spec/lib/auth/ldap_spec.rb'
|
|
||||||
- 'spec/lib/external_sync_spec.rb'
|
- 'spec/lib/external_sync_spec.rb'
|
||||||
- 'spec/lib/import/zendesk/object_attribute/base_examples.rb'
|
- 'spec/lib/import/zendesk/object_attribute/base_examples.rb'
|
||||||
- 'spec/lib/import/zendesk/object_attribute/checkbox_spec.rb'
|
- 'spec/lib/import/zendesk/object_attribute/checkbox_spec.rb'
|
||||||
|
|
|
@ -122,7 +122,7 @@ class App.User extends App.Model
|
||||||
false
|
false
|
||||||
|
|
||||||
maxLoginFailedReached: ->
|
maxLoginFailedReached: ->
|
||||||
return @login_failed > (App.Config.get('password_max_login_failed') || 10)
|
return @login_failed > (parseInt(App.Config.get('password_max_login_failed')))
|
||||||
|
|
||||||
imageUrl: ->
|
imageUrl: ->
|
||||||
return if !@image
|
return if !@image
|
||||||
|
|
|
@ -66,8 +66,8 @@ module ApplicationController::Authenticates
|
||||||
raise Exceptions::Forbidden, 'API password access disabled!'
|
raise Exceptions::Forbidden, 'API password access disabled!'
|
||||||
end
|
end
|
||||||
|
|
||||||
user = User.authenticate(username, password)
|
auth = Auth.new(username, password)
|
||||||
return authentication_check_prerequesits(user, 'basic_auth', auth_param) if user
|
return authentication_check_prerequesits(auth.user, 'basic_auth', auth_param) if auth.valid?
|
||||||
|
|
||||||
authentication_errors.push('Invalid BasicAuth credentials')
|
authentication_errors.push('Invalid BasicAuth credentials')
|
||||||
end
|
end
|
||||||
|
@ -146,14 +146,6 @@ module ApplicationController::Authenticates
|
||||||
raise Exceptions::NotAuthorized, authentication_errors.join(', ')
|
raise Exceptions::NotAuthorized, authentication_errors.join(', ')
|
||||||
end
|
end
|
||||||
|
|
||||||
def authenticate_with_password
|
|
||||||
user = User.authenticate(params[:username], params[:password])
|
|
||||||
raise_unified_login_error if !user
|
|
||||||
|
|
||||||
session.delete(:switched_from_user_id)
|
|
||||||
authentication_check_prerequesits(user, 'session', {})
|
|
||||||
end
|
|
||||||
|
|
||||||
def authentication_check_prerequesits(user, auth_type, auth_param)
|
def authentication_check_prerequesits(user, auth_type, auth_param)
|
||||||
raise Exceptions::Forbidden, 'Maintenance mode enabled!' if in_maintenance_mode?(user)
|
raise Exceptions::Forbidden, 'Maintenance mode enabled!' if in_maintenance_mode?(user)
|
||||||
|
|
||||||
|
|
|
@ -214,6 +214,14 @@ class SessionsController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def authenticate_with_password
|
||||||
|
auth = Auth.new(params[:username], params[:password])
|
||||||
|
raise_unified_login_error if !auth.valid?
|
||||||
|
|
||||||
|
session.delete(:switched_from_user_id)
|
||||||
|
authentication_check_prerequesits(auth.user, 'session', {})
|
||||||
|
end
|
||||||
|
|
||||||
def initiate_session_for(user)
|
def initiate_session_for(user)
|
||||||
request.env['rack.session.options'][:expire_after] = 1.year if params[:remember_me]
|
request.env['rack.session.options'][:expire_after] = 1.year if params[:remember_me]
|
||||||
session[:persistent] = true
|
session[:persistent] = true
|
||||||
|
|
|
@ -578,8 +578,9 @@ curl http://localhost/api/v1/users/password_change -v -u #{login}:#{password} -H
|
||||||
render json: { message: 'failed', notice: ['Current password needed!'] }, status: :ok
|
render json: { message: 'failed', notice: ['Current password needed!'] }, status: :ok
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
user = User.authenticate(current_user.login, params[:password_old])
|
|
||||||
if !user
|
current_password_verified = PasswordHash.verified?(current_user.password, params[:password_old])
|
||||||
|
if !current_password_verified
|
||||||
render json: { message: 'failed', notice: ['Current password is wrong!'] }, status: :ok
|
render json: { message: 'failed', notice: ['Current password is wrong!'] }, status: :ok
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
@ -596,20 +597,19 @@ curl http://localhost/api/v1/users/password_change -v -u #{login}:#{password} -H
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
user.update!(password: params[:password_new])
|
current_user.update!(password: params[:password_new])
|
||||||
|
|
||||||
if user.email.present?
|
if current_user.email.present?
|
||||||
NotificationFactory::Mailer.notification(
|
NotificationFactory::Mailer.notification(
|
||||||
template: 'password_change',
|
template: 'password_change',
|
||||||
user: user,
|
user: current_user,
|
||||||
objects: {
|
objects: {
|
||||||
user: user,
|
user: current_user,
|
||||||
current_user: current_user,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: { message: 'ok', user_login: user.login }, status: :ok
|
render json: { message: 'ok', user_login: current_user.login }, status: :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
|
@ -288,54 +288,6 @@ returns
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
||||||
authenticate user
|
|
||||||
|
|
||||||
result = User.authenticate(username, password)
|
|
||||||
|
|
||||||
returns
|
|
||||||
|
|
||||||
result = user_model # user model if authentication was successfully
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def self.authenticate(username, password)
|
|
||||||
|
|
||||||
# do not authenticate with nothing
|
|
||||||
return if username.blank? || password.blank?
|
|
||||||
|
|
||||||
user = User.identify(username)
|
|
||||||
return if !user
|
|
||||||
|
|
||||||
return if !Auth.can_login?(user)
|
|
||||||
|
|
||||||
return user if Auth.valid?(user, password)
|
|
||||||
|
|
||||||
sleep 1
|
|
||||||
user.login_failed += 1
|
|
||||||
user.save!
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
=begin
|
|
||||||
|
|
||||||
checks if a user has reached the maximum of failed login tries
|
|
||||||
|
|
||||||
user = User.find(123)
|
|
||||||
result = user.max_login_failed?
|
|
||||||
|
|
||||||
returns
|
|
||||||
|
|
||||||
result = true | false
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def max_login_failed?
|
|
||||||
max_login_failed = Setting.get('password_max_login_failed').to_i || 10
|
|
||||||
login_failed > max_login_failed
|
|
||||||
end
|
|
||||||
|
|
||||||
=begin
|
|
||||||
|
|
||||||
tries to find the matching instance by the given identifier. Currently email and login is supported.
|
tries to find the matching instance by the given identifier. Currently email and login is supported.
|
||||||
|
|
||||||
user = User.indentify('User123')
|
user = User.indentify('User123')
|
||||||
|
@ -352,6 +304,8 @@ returns
|
||||||
=end
|
=end
|
||||||
|
|
||||||
def self.identify(identifier)
|
def self.identify(identifier)
|
||||||
|
return if identifier.blank?
|
||||||
|
|
||||||
# try to find user based on login
|
# try to find user based on login
|
||||||
user = User.find_by(login: identifier.downcase)
|
user = User.find_by(login: identifier.downcase)
|
||||||
return user if user
|
return user if user
|
||||||
|
|
54
db/migrate/20210728103633_move_auth_backends_to_database.rb
Normal file
54
db/migrate/20210728103633_move_auth_backends_to_database.rb
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class MoveAuthBackendsToDatabase < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
|
||||||
|
# return if it's a new setup
|
||||||
|
return if !Setting.exists?(name: 'system_init_done')
|
||||||
|
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Authentication via %s',
|
||||||
|
name: 'auth_internal',
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
description: 'Enables user authentication via %s.',
|
||||||
|
preferences: {
|
||||||
|
title_i18n: ['internal database'],
|
||||||
|
description_i18n: ['internal database'],
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
priority: 1,
|
||||||
|
adapter: 'Auth::Internal',
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Authentication via %s',
|
||||||
|
name: 'auth_developer',
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
description: 'Enables user authentication via %s.',
|
||||||
|
preferences: {
|
||||||
|
title_i18n: ['developer password'],
|
||||||
|
description_i18n: ['developer password'],
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
priority: 2,
|
||||||
|
adapter: 'Auth::Developer',
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
|
||||||
|
begin
|
||||||
|
auth_ldap = Setting.find_by(name: 'auth_ldap')
|
||||||
|
|
||||||
|
auth_ldap.state_initial[:value][:priority] = 3
|
||||||
|
auth_ldap.state_current[:value][:priority] = 3
|
||||||
|
|
||||||
|
auth_ldap.save!
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Error while updating 'auth_ldap' Setting priority"
|
||||||
|
Rails.logger.error e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class SetUserSourceLdapFromExternalSync < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
return if !Setting.exists?(name: 'system_init_done')
|
||||||
|
|
||||||
|
ldap_user_ids = ExternalSync.where(
|
||||||
|
source: 'Ldap::User',
|
||||||
|
object: 'User'
|
||||||
|
).pluck(:o_id)
|
||||||
|
|
||||||
|
User.where(id: ldap_user_ids).find_each do |user|
|
||||||
|
user.update!(source: 'Ldap')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1122,6 +1122,38 @@ Setting.create_if_not_exists(
|
||||||
},
|
},
|
||||||
frontend: false
|
frontend: false
|
||||||
)
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Authentication via %s',
|
||||||
|
name: 'auth_internal',
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
description: 'Enables user authentication via %s.',
|
||||||
|
preferences: {
|
||||||
|
title_i18n: ['internal database'],
|
||||||
|
description_i18n: ['internal database'],
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
priority: 1,
|
||||||
|
adapter: 'Auth::Backend::Internal',
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
|
Setting.create_if_not_exists(
|
||||||
|
title: 'Authentication via %s',
|
||||||
|
name: 'auth_developer',
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
description: 'Enables user authentication via %s.',
|
||||||
|
preferences: {
|
||||||
|
title_i18n: ['developer password'],
|
||||||
|
description_i18n: ['developer password'],
|
||||||
|
permission: ['admin.security'],
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
priority: 2,
|
||||||
|
adapter: 'Auth::Backend::Developer',
|
||||||
|
},
|
||||||
|
frontend: false
|
||||||
|
)
|
||||||
Setting.create_if_not_exists(
|
Setting.create_if_not_exists(
|
||||||
title: 'Authentication via %s',
|
title: 'Authentication via %s',
|
||||||
name: 'auth_ldap',
|
name: 'auth_ldap',
|
||||||
|
@ -1133,7 +1165,8 @@ Setting.create_if_not_exists(
|
||||||
permission: ['admin.security'],
|
permission: ['admin.security'],
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
adapter: 'Auth::Ldap',
|
priority: 3,
|
||||||
|
adapter: 'Auth::Backend::Ldap',
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
port: 389,
|
port: 389,
|
||||||
bind_dn: 'cn=Manager,dc=example,dc=org',
|
bind_dn: 'cn=Manager,dc=example,dc=org',
|
||||||
|
|
131
lib/auth.rb
131
lib/auth.rb
|
@ -1,115 +1,58 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
class Auth
|
class Auth
|
||||||
include ApplicationLib
|
|
||||||
|
|
||||||
=begin
|
attr_reader :user, :password, :auth_user
|
||||||
|
|
||||||
checks if a given user can login. Checks for
|
delegate :user, to: :auth_user
|
||||||
- valid user
|
|
||||||
- active state
|
|
||||||
- max failed logins
|
|
||||||
|
|
||||||
result = Auth.can_login?(user)
|
attr_accessor :increase_login_failed_attempts
|
||||||
|
|
||||||
returns
|
# Initializes a Auth object for the given user.
|
||||||
|
#
|
||||||
|
# @param username [String] the user name for the user object which needs an authentication.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# auth = Auth.new('master@example.com', 'some+password')
|
||||||
|
def initialize(username, password)
|
||||||
|
@lookup_backend_instance = {}
|
||||||
|
|
||||||
result = true | false
|
@auth_user = username.present? ? Auth::User.new(username) : nil
|
||||||
|
@password = password
|
||||||
|
|
||||||
=end
|
@increase_login_failed_attempts = false
|
||||||
|
end
|
||||||
|
|
||||||
def self.can_login?(user)
|
# Validates the given credentials for the user to the configured auth backends which should
|
||||||
return false if !user.is_a?(User)
|
# be performed.
|
||||||
return false if !user.active?
|
#
|
||||||
|
# @return [Boolean] true if the user was authenticated, otherwise false.
|
||||||
|
def valid?
|
||||||
|
if !auth_user || !auth_user.can_login?
|
||||||
|
avoid_brute_force_attack
|
||||||
|
|
||||||
return true if !user.max_login_failed?
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
Rails.logger.info "Max login failed reached for user #{user.login}."
|
if backends.valid?
|
||||||
|
auth_user.update_last_login
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
avoid_brute_force_attack
|
||||||
|
|
||||||
|
auth_user.increase_login_failed if increase_login_failed_attempts
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
private
|
||||||
|
|
||||||
checks if a given user and password match against multiple auth backends
|
# Sleep for a second to avoid brute force attacks.
|
||||||
- valid user
|
def avoid_brute_force_attack
|
||||||
- active state
|
sleep 1
|
||||||
- max failed logins
|
|
||||||
|
|
||||||
result = Auth.valid?(user, password)
|
|
||||||
|
|
||||||
returns
|
|
||||||
|
|
||||||
result = true | false
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def self.valid?(user, password)
|
|
||||||
# try to login against configure auth backends
|
|
||||||
backends.any? do |config|
|
|
||||||
next if !backend_validates?(
|
|
||||||
config: config,
|
|
||||||
user: user,
|
|
||||||
password: password,
|
|
||||||
)
|
|
||||||
|
|
||||||
Rails.logger.info "Authentication against #{config[:adapter]} for user #{user.login} ok."
|
|
||||||
|
|
||||||
# remember last login date
|
|
||||||
user.update_last_login
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
=begin
|
def backends
|
||||||
|
Auth::Backend.new(self)
|
||||||
returns a list of all Auth backend configurations
|
|
||||||
|
|
||||||
result = Auth.backends
|
|
||||||
|
|
||||||
returns
|
|
||||||
|
|
||||||
result = [
|
|
||||||
{
|
|
||||||
adapter: 'Auth::Internal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
adapter: 'Auth::Developer',
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
|
|
||||||
=end
|
|
||||||
|
|
||||||
def self.backends
|
|
||||||
|
|
||||||
# use std. auth backends
|
|
||||||
config = [
|
|
||||||
{
|
|
||||||
adapter: 'Auth::Internal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
adapter: 'Auth::Developer',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# added configured backends
|
|
||||||
Setting.where(area: 'Security::Authentication').each do |setting|
|
|
||||||
next if setting.state_current[:value].blank?
|
|
||||||
|
|
||||||
config.push setting.state_current[:value]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
config
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.backend_validates?(config:, user:, password:)
|
|
||||||
return false if !config[:adapter]
|
|
||||||
|
|
||||||
instance = config[:adapter].constantize.new(config)
|
|
||||||
|
|
||||||
instance.valid?(user, password)
|
|
||||||
end
|
|
||||||
private_class_method :backend_validates?
|
|
||||||
end
|
end
|
||||||
|
|
41
lib/auth/backend.rb
Normal file
41
lib/auth/backend.rb
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
class Backend
|
||||||
|
|
||||||
|
attr_reader :auth
|
||||||
|
|
||||||
|
def initialize(auth)
|
||||||
|
@auth = auth
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
instances.any? do |instance|
|
||||||
|
next if !instance.valid?
|
||||||
|
|
||||||
|
Rails.logger.info "Authentication against #{instance.class.name} for user #{auth.user.login} ok."
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def instances
|
||||||
|
configs.filter_map do |config|
|
||||||
|
config[:adapter].constantize.new(config, auth)
|
||||||
|
rescue => e
|
||||||
|
Rails.logger.error "Failed to load Auth::Backend from Setting '#{config}'"
|
||||||
|
Rails.logger.error e
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def configs
|
||||||
|
Setting.where(area: 'Security::Authentication')
|
||||||
|
.map { |setting| setting.state_current[:value] } # extract current Setting value as config
|
||||||
|
.reject(&:blank?)
|
||||||
|
.sort { |a, b| a.fetch(:priority, 999) <=> b.fetch(:priority, 999) } # sort by priority and fallback to append if not set
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
40
lib/auth/backend/base.rb
Normal file
40
lib/auth/backend/base.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
class Backend
|
||||||
|
class Base
|
||||||
|
|
||||||
|
delegate :user, :password, to: :auth
|
||||||
|
|
||||||
|
attr_reader :config, :auth
|
||||||
|
|
||||||
|
# Base initialization for Auth backend object.
|
||||||
|
#
|
||||||
|
# @param config [Hash] backend configuration hash.
|
||||||
|
# @param auth [Auth] the Auth object for the authentication.
|
||||||
|
#
|
||||||
|
# @example
|
||||||
|
# auth = Auth::Backend::Internal.new('master@example.com', auth)
|
||||||
|
def initialize(config, auth)
|
||||||
|
@config = config
|
||||||
|
@auth = auth
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
return false if !perform?
|
||||||
|
|
||||||
|
authenticated?
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def perform?
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticated?
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
52
lib/auth/backend/developer.rb
Normal file
52
lib/auth/backend/developer.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
class Backend
|
||||||
|
class Developer < Auth::Backend::Base
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Special development/test password validation.
|
||||||
|
# * For the developer mode the password 'test' is allowed for every User.
|
||||||
|
# * For the test environment the password can be blank if also the user password is currently blank.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if the validation works, otherwise false.
|
||||||
|
def authenticated?
|
||||||
|
if valid_for_developer_mode? || valid_for_test_environment?
|
||||||
|
Rails.logger.info "System in test/developer mode, authentication for user #{user.login} ok."
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Overwrites the default behaviour to check for a allowed environment.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if the environment is development or test.
|
||||||
|
def perform?
|
||||||
|
allowed_environment?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check for allowed environments.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if one allowed environment is active.
|
||||||
|
def allowed_environment?
|
||||||
|
Setting.get('developer_mode') == true || Rails.env.test?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate password for test environment.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if password and user password is blank, otherwise false.
|
||||||
|
def valid_for_test_environment?
|
||||||
|
Rails.env.test? && password.blank? && user.password.blank?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Validate password for test environment.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if the given password is 'test', otherwise false.
|
||||||
|
def valid_for_developer_mode?
|
||||||
|
Setting.get('developer_mode') == true && password == 'test'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
42
lib/auth/backend/internal.rb
Normal file
42
lib/auth/backend/internal.rb
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
class Backend
|
||||||
|
class Internal < Auth::Backend::Base
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Validation against the internal database.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if the validation works, otherwise false.
|
||||||
|
def authenticated?
|
||||||
|
return true if hash_matches?
|
||||||
|
|
||||||
|
auth.increase_login_failed_attempts = true
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Overwrites the default behaviour to only perform this authentication if an internal password exists.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if a internal password for the user is present.
|
||||||
|
def perform?
|
||||||
|
return false if password.blank?
|
||||||
|
return false if !user.verified && user.source == 'signup'
|
||||||
|
|
||||||
|
user.password.present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def hash_matches?
|
||||||
|
# Because of legacy reason a special check exists and afterwards the
|
||||||
|
# password will be saved in the current format.
|
||||||
|
if PasswordHash.legacy?(user.password, password)
|
||||||
|
user.update!(password: password)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
PasswordHash.verified?(user.password, password)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
74
lib/auth/backend/ldap.rb
Normal file
74
lib/auth/backend/ldap.rb
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
class Backend
|
||||||
|
class Ldap < Auth::Backend::Base
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Validation against the configured ldap integration.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if the validation works, otherwise false.
|
||||||
|
def authenticated?
|
||||||
|
ldap_user = ::Ldap::User.new
|
||||||
|
|
||||||
|
# get from config or fallback to login
|
||||||
|
# for a list of user attributes which should
|
||||||
|
# be used for logging in
|
||||||
|
login_attributes = config[:login_attributes] || %w[login]
|
||||||
|
|
||||||
|
authed = login_attributes.any? do |attribute|
|
||||||
|
ldap_user.valid?(user[attribute], password)
|
||||||
|
end
|
||||||
|
|
||||||
|
log_auth_result(authed)
|
||||||
|
authed
|
||||||
|
rescue => e
|
||||||
|
message = "Can't connect to ldap backend #{e}"
|
||||||
|
Rails.logger.info message
|
||||||
|
Rails.logger.info e
|
||||||
|
log(
|
||||||
|
status: 'failed',
|
||||||
|
response: message,
|
||||||
|
)
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks the default behaviour and as a addition if the ldap integration is currently active.
|
||||||
|
#
|
||||||
|
# @returns [Boolean] true if the ldap integration is active and the default behaviour matches.
|
||||||
|
def perform?
|
||||||
|
user.source == 'Ldap' && Setting.get('ldap_integration')
|
||||||
|
end
|
||||||
|
|
||||||
|
# Logs the auth result
|
||||||
|
#
|
||||||
|
# @param authed [Boolean] true if the user is authed, otherwise false.
|
||||||
|
def log_auth_result(authed)
|
||||||
|
result = authed ? 'success' : 'failed'
|
||||||
|
log(
|
||||||
|
status: result,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Created the http log for the current authentication.
|
||||||
|
#
|
||||||
|
# @param status [String] the status of the ldap authentication.
|
||||||
|
# @param response [String] the response message.
|
||||||
|
def log(status:, response: nil)
|
||||||
|
HttpLog.create(
|
||||||
|
direction: 'out',
|
||||||
|
facility: 'ldap',
|
||||||
|
url: "bind -> #{user.login}",
|
||||||
|
status: status,
|
||||||
|
ip: nil,
|
||||||
|
request: { content: user.login },
|
||||||
|
response: { content: response || status },
|
||||||
|
method: 'tcp',
|
||||||
|
created_by_id: 1,
|
||||||
|
updated_by_id: 1,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,14 +0,0 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
|
|
||||||
class Auth
|
|
||||||
class Base
|
|
||||||
|
|
||||||
def initialize(config)
|
|
||||||
@config = config
|
|
||||||
end
|
|
||||||
|
|
||||||
def valid?(_user, _password)
|
|
||||||
raise "Missing implementation of method 'valid?' for class '#{self.class.name}'"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
|
|
||||||
class Auth
|
|
||||||
class Developer < Auth::Base
|
|
||||||
|
|
||||||
def valid?(user, password)
|
|
||||||
return false if user.blank?
|
|
||||||
return false if Setting.get('developer_mode') != true
|
|
||||||
return false if password != 'test'
|
|
||||||
|
|
||||||
Rails.logger.info "System in developer mode, authentication for user #{user.login} ok."
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,29 +0,0 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
|
|
||||||
class Auth
|
|
||||||
class Internal < Auth::Base
|
|
||||||
|
|
||||||
def valid?(user, password)
|
|
||||||
|
|
||||||
return false if user.blank?
|
|
||||||
|
|
||||||
if PasswordHash.legacy?(user.password, password)
|
|
||||||
update_password(user, password)
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
|
|
||||||
password_verified = PasswordHash.verified?(user.password, password)
|
|
||||||
|
|
||||||
raise Exceptions::Forbidden, 'Please verify your account before you can login!' if !user.verified && user.source == 'signup' && password_verified
|
|
||||||
|
|
||||||
password_verified
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def update_password(user, password)
|
|
||||||
user.password = PasswordHash.crypt(password)
|
|
||||||
user.save
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,59 +0,0 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
|
|
||||||
class Auth
|
|
||||||
class Ldap < Auth::Base
|
|
||||||
|
|
||||||
def valid?(user, password)
|
|
||||||
return false if !Setting.get('ldap_integration')
|
|
||||||
|
|
||||||
ldap_user = ::Ldap::User.new
|
|
||||||
|
|
||||||
# get from config or fallback to login
|
|
||||||
# for a list of user attributes which should
|
|
||||||
# be used for logging in
|
|
||||||
login_attributes = @config[:login_attributes] || %w[login]
|
|
||||||
|
|
||||||
authed = login_attributes.any? do |attribute|
|
|
||||||
ldap_user.valid?(user[attribute], password)
|
|
||||||
end
|
|
||||||
|
|
||||||
log_auth_result(user, authed)
|
|
||||||
authed
|
|
||||||
rescue => e
|
|
||||||
message = "Can't connect to ldap backend #{e}"
|
|
||||||
Rails.logger.info message
|
|
||||||
Rails.logger.info e
|
|
||||||
log(
|
|
||||||
user: user,
|
|
||||||
status: 'failed',
|
|
||||||
response: message,
|
|
||||||
)
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def log_auth_result(user, authed)
|
|
||||||
result = authed ? 'success' : 'failed'
|
|
||||||
log(
|
|
||||||
user: user,
|
|
||||||
status: result,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def log(user:, status:, response: nil)
|
|
||||||
HttpLog.create(
|
|
||||||
direction: 'out',
|
|
||||||
facility: 'ldap',
|
|
||||||
url: "bind -> #{user.login}",
|
|
||||||
status: status,
|
|
||||||
ip: nil,
|
|
||||||
request: { content: user.login },
|
|
||||||
response: { content: response || status },
|
|
||||||
method: 'tcp',
|
|
||||||
created_by_id: 1,
|
|
||||||
updated_by_id: 1,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
52
lib/auth/user.rb
Normal file
52
lib/auth/user.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Auth
|
||||||
|
class User < SimpleDelegator
|
||||||
|
|
||||||
|
attr_reader :user
|
||||||
|
|
||||||
|
def initialize(username)
|
||||||
|
@user = ::User.identify(username)
|
||||||
|
super(@user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if a given user can login. Check for the following criteria:
|
||||||
|
# * valid user
|
||||||
|
# * user is active
|
||||||
|
# * user has not reached the maximum of failed login tries
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the user can login, false otherwise.
|
||||||
|
def can_login?
|
||||||
|
return false if !exists?
|
||||||
|
return false if !active?
|
||||||
|
|
||||||
|
!max_login_failed?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Increase the current failed login count for the user.
|
||||||
|
def increase_login_failed
|
||||||
|
self.login_failed += 1
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Checks if a user has reached the maximum of failed login tries.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if the user has reached the maximum of failed login tries, otherwise false.
|
||||||
|
def max_login_failed?
|
||||||
|
max_login_failed = Setting.get('password_max_login_failed').to_i
|
||||||
|
return false if login_failed <= max_login_failed
|
||||||
|
|
||||||
|
Rails.logger.info "Max login failed reached for user #{login}."
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if a valid user exists.
|
||||||
|
#
|
||||||
|
# @return [Boolean] true if a valid user exists, otherwise false.
|
||||||
|
def exists?
|
||||||
|
present? && __getobj__.is_a?(::User)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -19,6 +19,9 @@ class Sequencer
|
||||||
# because otherwise disabled instances won't get
|
# because otherwise disabled instances won't get
|
||||||
# re-activated if they should get synced again
|
# re-activated if they should get synced again
|
||||||
active: true,
|
active: true,
|
||||||
|
|
||||||
|
# Set the source to 'Ldap' for the authentication handling.
|
||||||
|
source: 'Ldap',
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe SetUserSourceLdapFromExternalSync, type: :db_migration, db_strategy: :reset do
|
||||||
|
let(:users) { create_list(:user, 2) }
|
||||||
|
let(:other_user) { create(:user) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
2.times do |count|
|
||||||
|
index = count - 1
|
||||||
|
|
||||||
|
create(:external_sync,
|
||||||
|
source: 'Ldap::User',
|
||||||
|
source_id: "uid=#{users[index].login},ou=People,dc=example,dc=org",
|
||||||
|
object: 'User',
|
||||||
|
o_id: users[index].id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when having users from the ldap integration' do
|
||||||
|
it 'source key for users are filled' do
|
||||||
|
expect { migrate }.to change { users[0].reload.source }.to('Ldap').and change { users[1].reload.source }.to('Ldap')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'other user should not be touched' do
|
||||||
|
expect { migrate }.not_to change { other_user.reload.source }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,7 +3,7 @@
|
||||||
FactoryBot.define do
|
FactoryBot.define do
|
||||||
factory :setting do
|
factory :setting do
|
||||||
title { 'ABC API Token' }
|
title { 'ABC API Token' }
|
||||||
name { 'abc_api_token' }
|
name { Faker::Name.unique.name }
|
||||||
area { 'Integration::ABC' }
|
area { 'Integration::ABC' }
|
||||||
description { 'API Token for ABC to access ABC.' }
|
description { 'API Token for ABC to access ABC.' }
|
||||||
frontend { false }
|
frontend { false }
|
||||||
|
|
|
@ -1,21 +1,15 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
require 'lib/auth/backend_examples'
|
require 'lib/auth/backend/backend_examples'
|
||||||
|
|
||||||
RSpec.describe Auth::Base do
|
RSpec.describe Auth::Backend::Base do
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
let(:instance) { described_class.new({ adapter: described_class.name }) }
|
let(:auth) { Auth.new(user.login, 'not_used') }
|
||||||
|
let(:instance) { described_class.new({ adapter: described_class.name }, auth) }
|
||||||
|
|
||||||
describe '#valid?' do
|
describe '#valid?' do
|
||||||
it_behaves_like 'Auth backend'
|
it_behaves_like 'Auth backend'
|
||||||
|
|
||||||
it "requires an implementation of the 'valid?' method" do
|
|
||||||
|
|
||||||
expect do
|
|
||||||
instance.valid?(user, 'password')
|
|
||||||
end.to raise_error(RuntimeError)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
94
spec/lib/auth/backend/developer_spec.rb
Normal file
94
spec/lib/auth/backend/developer_spec.rb
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'lib/auth/backend/backend_examples'
|
||||||
|
|
||||||
|
RSpec.describe Auth::Backend::Developer do
|
||||||
|
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:password) { 'not_used' }
|
||||||
|
let(:auth) { Auth.new(user.login, password) }
|
||||||
|
let(:instance) { described_class.new({ adapter: described_class.name }, auth) }
|
||||||
|
|
||||||
|
describe '#valid?' do
|
||||||
|
it_behaves_like 'Auth backend'
|
||||||
|
|
||||||
|
context 'when Setting developer_mode is true' do
|
||||||
|
|
||||||
|
before do
|
||||||
|
Setting.set('developer_mode', true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is "test"' do
|
||||||
|
|
||||||
|
let(:password) { 'test' }
|
||||||
|
|
||||||
|
it 'authenticates' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password matches actual User password' do
|
||||||
|
|
||||||
|
let(:user) { create(:user, password: 'secure') }
|
||||||
|
let(:password) { user.password_plain }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when Rails.env is "test"' do
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Rails).to receive(:env) { 'test'.inquiry } # rubocop:disable Rails/Inquiry
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is blank' do
|
||||||
|
|
||||||
|
let(:password) { '' }
|
||||||
|
|
||||||
|
it 'authenticates' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password matches actual User password' do
|
||||||
|
|
||||||
|
let(:user) { create(:user, password: 'secure') }
|
||||||
|
let(:password) { user.password_plain }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when Rails.env is "production"' do
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Rails).to receive(:env) { 'production'.inquiry } # rubocop:disable Rails/Inquiry
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is blank' do
|
||||||
|
|
||||||
|
let(:password) { '' }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password matches actual User password' do
|
||||||
|
|
||||||
|
let(:user) { create(:user, password: 'secure') }
|
||||||
|
let(:password) { user.password_plain }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
89
spec/lib/auth/backend/internal_spec.rb
Normal file
89
spec/lib/auth/backend/internal_spec.rb
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'lib/auth/backend/backend_examples'
|
||||||
|
|
||||||
|
RSpec.describe Auth::Backend::Internal do
|
||||||
|
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:password) { 'secure' }
|
||||||
|
let(:auth) { Auth.new(user.login, password) }
|
||||||
|
let(:instance) { described_class.new({ adapter: described_class.name }, auth) }
|
||||||
|
|
||||||
|
describe '#valid?' do
|
||||||
|
it_behaves_like 'Auth backend'
|
||||||
|
|
||||||
|
context 'when password is given' do
|
||||||
|
let(:user) { create(:user, password: password) }
|
||||||
|
|
||||||
|
it 'authenticates' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when given password matches stored hash' do
|
||||||
|
|
||||||
|
let(:password) { user.password }
|
||||||
|
let(:user) { create(:user, password: 'secure') }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when given password is blank' do
|
||||||
|
let(:password) { '' }
|
||||||
|
let(:user) { create(:user, password: 'secure') }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with legacy SHA2 passwords' do
|
||||||
|
let(:user) { create(:user, password: PasswordHash.sha2(password)) }
|
||||||
|
|
||||||
|
it 'is password hash crypted' do
|
||||||
|
expect(PasswordHash.crypted?(user.password)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is password hash legacy' do
|
||||||
|
expect(PasswordHash.legacy?(user.password, password)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'valid authentication' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is password hash not legacy after validation' do
|
||||||
|
instance.valid?
|
||||||
|
expect(PasswordHash.legacy?(user.reload.password, password)).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'is password hash crypted after validation' do
|
||||||
|
instance.valid?
|
||||||
|
expect(PasswordHash.crypted?(user.password)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when affecting Auth#increase_login_failed_attempts' do
|
||||||
|
|
||||||
|
context 'when authentication fails' do
|
||||||
|
let(:password) { 'wrong' }
|
||||||
|
let(:user) { create(:user, password: 'secure') }
|
||||||
|
|
||||||
|
it 'sets Auth#increase_login_failed_attempts flag to true' do
|
||||||
|
expect { instance.valid? }.to change(auth, :increase_login_failed_attempts).from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when authentication succeeds' do
|
||||||
|
let(:user) { create(:user, password: password) }
|
||||||
|
|
||||||
|
it "doesn't change Auth#increase_login_failed_attempts flag" do
|
||||||
|
expect { instance.valid? }.not_to change(auth, :increase_login_failed_attempts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
91
spec/lib/auth/backend/ldap_spec.rb
Normal file
91
spec/lib/auth/backend/ldap_spec.rb
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
require 'lib/auth/backend/backend_examples'
|
||||||
|
|
||||||
|
RSpec.describe ::Auth::Backend::Ldap do
|
||||||
|
|
||||||
|
let(:user) { create(:user, source: 'Ldap') }
|
||||||
|
let(:password) { 'secure' }
|
||||||
|
let(:auth) { Auth.new(user.login, password) }
|
||||||
|
let(:config) do
|
||||||
|
{
|
||||||
|
adapter: described_class.name
|
||||||
|
}
|
||||||
|
end
|
||||||
|
let(:instance) { described_class.new(config, auth) }
|
||||||
|
let(:ldap_integration) { true }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Setting.set('ldap_integration', ldap_integration)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#valid?' do
|
||||||
|
let(:ldap_user) { instance_double(Ldap::User) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Ldap::User).to receive(:new).with(any_args).and_return(ldap_user)
|
||||||
|
allow(ldap_user).to receive(:valid?).with(any_args).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'Auth backend'
|
||||||
|
|
||||||
|
it 'authenticates users' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when custom login attribute is configured' do
|
||||||
|
|
||||||
|
let(:config) do
|
||||||
|
super().merge(
|
||||||
|
login_attributes: %w[firstname]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'authenticates' do
|
||||||
|
allow(ldap_user).to receive(:valid?).with(user.firstname, password).and_return(true)
|
||||||
|
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when Setting ldap_integration is false' do
|
||||||
|
|
||||||
|
let(:ldap_integration) { false }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when LDAP authentication fails' do
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
allow(ldap_user).to receive(:valid?).with(any_args).and_return(false)
|
||||||
|
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when User#source does not match Ldap' do
|
||||||
|
|
||||||
|
context 'when blank' do
|
||||||
|
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when some other value' do
|
||||||
|
|
||||||
|
let(:user) { create(:user, source: 'some other value') }
|
||||||
|
|
||||||
|
it "doesn't authenticate" do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
108
spec/lib/auth/backend_spec.rb
Normal file
108
spec/lib/auth/backend_spec.rb
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Auth::Backend do
|
||||||
|
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:password) { 'secure' }
|
||||||
|
let(:auth) { Auth.new(user.login, password) }
|
||||||
|
let(:instance) { described_class.new(auth) }
|
||||||
|
|
||||||
|
describe '#valid?' do
|
||||||
|
|
||||||
|
context 'when invalid Setting is present in DB' do
|
||||||
|
|
||||||
|
context 'when value is blank' do
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:setting,
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
state: {},)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't raise an exception" do
|
||||||
|
expect { instance.valid? }.not_to raise_exception
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when adapter can't be constantized" do
|
||||||
|
before do
|
||||||
|
create(:setting,
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
state: {
|
||||||
|
adapter: 'This::Will::Never::Work'
|
||||||
|
},)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't raise an exception" do
|
||||||
|
expect { instance.valid? }.not_to raise_exception
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when backend prioritization is relevant' do
|
||||||
|
|
||||||
|
let(:previous_class_namespace) { 'Auth::Backend::TopPrio' }
|
||||||
|
let(:later_class_namespace) { 'Auth::Backend::LeastPrio' }
|
||||||
|
|
||||||
|
let(:previous_backend_class) { Class.new(Auth::Backend::Base) }
|
||||||
|
let(:later_backend_class) { Class.new(Auth::Backend::Base) }
|
||||||
|
|
||||||
|
let(:previous_backend_instance) { instance_double(previous_class_namespace) }
|
||||||
|
let(:later_backend_instance) { instance_double(later_class_namespace) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_const previous_class_namespace, previous_backend_class
|
||||||
|
stub_const later_class_namespace, later_backend_class
|
||||||
|
|
||||||
|
Setting.where(area: 'Security::Authentication').destroy_all
|
||||||
|
|
||||||
|
create(:setting,
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
state: {
|
||||||
|
adapter: previous_class_namespace,
|
||||||
|
priority: 1
|
||||||
|
},)
|
||||||
|
create(:setting,
|
||||||
|
area: 'Security::Authentication',
|
||||||
|
state: {
|
||||||
|
adapter: later_class_namespace,
|
||||||
|
priority: 2
|
||||||
|
},)
|
||||||
|
|
||||||
|
allow(previous_class_namespace.constantize).to receive(:new).and_return(previous_backend_instance)
|
||||||
|
allow(later_class_namespace.constantize).to receive(:new).and_return(later_backend_instance)
|
||||||
|
|
||||||
|
allow(previous_backend_instance).to receive(:valid?)
|
||||||
|
allow(later_backend_instance).to receive(:valid?)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when previous backend was valid' do
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(previous_backend_instance).to receive(:valid?).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't call valid on later backend" do
|
||||||
|
instance.valid?
|
||||||
|
|
||||||
|
expect(later_backend_instance).not_to have_received(:valid?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when previous backend was not valid' do
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(previous_backend_instance).to receive(:valid?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls valid on later backend' do
|
||||||
|
instance.valid?
|
||||||
|
|
||||||
|
expect(later_backend_instance).to have_received(:valid?)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,49 +0,0 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
require 'lib/auth/backend_examples'
|
|
||||||
|
|
||||||
RSpec.describe Auth::Developer do
|
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
|
||||||
let(:instance) { described_class.new({ adapter: described_class.name }) }
|
|
||||||
|
|
||||||
describe '#valid?' do
|
|
||||||
it_behaves_like 'Auth backend'
|
|
||||||
|
|
||||||
it "authenticates users with password 'test'" do
|
|
||||||
|
|
||||||
allow(Setting).to receive(:get)
|
|
||||||
allow(Setting).to receive(:get).with('developer_mode').and_return(true)
|
|
||||||
|
|
||||||
result = instance.valid?(user, 'test')
|
|
||||||
|
|
||||||
expect(result).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'invalid' do
|
|
||||||
|
|
||||||
let(:password) { 'zammad' }
|
|
||||||
|
|
||||||
it "doesn't authenticate if developer mode is off" do
|
|
||||||
|
|
||||||
allow(Setting).to receive(:get)
|
|
||||||
allow(Setting).to receive(:get).with('developer_mode').and_return(false)
|
|
||||||
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it "doesn't authenticate with correct password" do
|
|
||||||
|
|
||||||
allow(Setting).to receive(:get)
|
|
||||||
allow(Setting).to receive(:get).with('developer_mode').and_return(true)
|
|
||||||
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,40 +0,0 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
require 'lib/auth/backend_examples'
|
|
||||||
|
|
||||||
RSpec.describe Auth::Internal do
|
|
||||||
|
|
||||||
let(:password) { 'zammad' }
|
|
||||||
let(:user) { create(:user, password: password) }
|
|
||||||
let(:instance) { described_class.new({ adapter: described_class.name }) }
|
|
||||||
|
|
||||||
describe '#valid?' do
|
|
||||||
it_behaves_like 'Auth backend'
|
|
||||||
|
|
||||||
it 'authenticates via password' do
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
expect(result).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "doesn't authenticate via plain password" do
|
|
||||||
result = instance.valid?(user, user.password)
|
|
||||||
expect(result).to be_falsy
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'converts legacy sha2 passwords' do
|
|
||||||
|
|
||||||
sha2 = PasswordHash.sha2(password)
|
|
||||||
user = create(:user, password: sha2)
|
|
||||||
|
|
||||||
expect(PasswordHash.crypted?(user.password)).to be true
|
|
||||||
expect(PasswordHash.legacy?(user.password, password)).to be true
|
|
||||||
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
expect(result).to be true
|
|
||||||
|
|
||||||
expect(PasswordHash.legacy?(user.password, password)).to be false
|
|
||||||
expect(PasswordHash.crypted?(user.password)).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,71 +0,0 @@
|
||||||
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
require 'lib/auth/backend_examples'
|
|
||||||
require 'auth/ldap'
|
|
||||||
|
|
||||||
RSpec.describe ::Auth::Ldap do
|
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
|
||||||
let(:password) { 'somepassword' }
|
|
||||||
let(:instance) { described_class.new({ adapter: described_class.name }) }
|
|
||||||
|
|
||||||
describe '#valid?' do
|
|
||||||
it_behaves_like 'Auth backend'
|
|
||||||
|
|
||||||
it 'authenticates users' do
|
|
||||||
|
|
||||||
allow(Setting).to receive(:get)
|
|
||||||
allow(Setting).to receive(:get).with('ldap_integration').and_return(true)
|
|
||||||
|
|
||||||
ldap_user = double(valid?: true)
|
|
||||||
allow(::Ldap::User).to receive(:new).and_return(ldap_user)
|
|
||||||
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
expect(result).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'authenticates via configurable user attributes' do
|
|
||||||
|
|
||||||
allow(Setting).to receive(:get)
|
|
||||||
allow(Setting).to receive(:get).with('ldap_integration').and_return(true)
|
|
||||||
|
|
||||||
instance = described_class.new(
|
|
||||||
adapter: described_class.name,
|
|
||||||
login_attributes: %w[firstname],
|
|
||||||
)
|
|
||||||
|
|
||||||
ldap_user = double
|
|
||||||
allow(ldap_user).to receive(:valid?).with(user.firstname, password).and_return(true)
|
|
||||||
|
|
||||||
allow(::Ldap::User).to receive(:new).and_return(ldap_user)
|
|
||||||
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
expect(result).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'invalid' do
|
|
||||||
|
|
||||||
it "doesn't authenticate if 'ldap_integration' Setting is disabled" do
|
|
||||||
|
|
||||||
allow(Setting).to receive(:get)
|
|
||||||
allow(Setting).to receive(:get).with('ldap_integration').and_return(false)
|
|
||||||
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it "doesn't authenticate if ldap says 'nope'" do
|
|
||||||
|
|
||||||
allow(Setting).to receive(:get)
|
|
||||||
allow(Setting).to receive(:get).with('ldap_integration').and_return(true)
|
|
||||||
|
|
||||||
ldap_user = double(valid?: false)
|
|
||||||
allow(::Ldap::User).to receive(:new).and_return(ldap_user)
|
|
||||||
|
|
||||||
result = instance.valid?(user, password)
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
97
spec/lib/auth/user_spec.rb
Normal file
97
spec/lib/auth/user_spec.rb
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe Auth::User do
|
||||||
|
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:instance) { described_class.new(user.login) }
|
||||||
|
|
||||||
|
describe '.can_login?' do
|
||||||
|
it 'responds to can_login?' do
|
||||||
|
expect(instance).to respond_to(:can_login?)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'check loginable' do
|
||||||
|
it 'checks if users can login' do
|
||||||
|
expect(instance.can_login?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'check not loginable' do
|
||||||
|
it 'check that user can not login' do
|
||||||
|
expect(instance.can_login?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid user login' do
|
||||||
|
include_examples 'check loginable'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with to many failed logins' do
|
||||||
|
let(:user) { create(:user, login_failed: 999) }
|
||||||
|
|
||||||
|
include_examples 'check not loginable'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with not active user' do
|
||||||
|
let(:user) { create(:user, active: false) }
|
||||||
|
|
||||||
|
include_examples 'check not loginable'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid instance username parameter' do
|
||||||
|
let(:instance) { described_class.new('not_existing') }
|
||||||
|
|
||||||
|
include_examples 'check not loginable'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty instance username parameter' do
|
||||||
|
let(:instance) { described_class.new('') }
|
||||||
|
|
||||||
|
include_examples 'check not loginable'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with given default password_max_login_failed' do
|
||||||
|
context 'with 5 attempts' do
|
||||||
|
let(:user) { create(:user, login_failed: 5) }
|
||||||
|
|
||||||
|
include_examples 'check loginable'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with 6 attempts' do
|
||||||
|
let(:user) { create(:user, login_failed: 6) }
|
||||||
|
|
||||||
|
include_examples 'check not loginable'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when "password_max_login_failed" Setting is changed' do
|
||||||
|
|
||||||
|
context 'when changed to lower value' do
|
||||||
|
before do
|
||||||
|
Setting.set('password_max_login_failed', 5)
|
||||||
|
user.update(login_failed: 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'check not loginable'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when changed to nil' do
|
||||||
|
before do
|
||||||
|
Setting.set('password_max_login_failed', nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'check loginable'
|
||||||
|
|
||||||
|
context 'when User login failed once' do
|
||||||
|
before do
|
||||||
|
user.update(login_failed: 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'check not loginable'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,75 +3,242 @@
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe Auth do
|
RSpec.describe Auth do
|
||||||
|
let(:password) { 'zammad' }
|
||||||
describe '.can_login?' do
|
let(:user) { create(:user, password: password) }
|
||||||
it 'responds to can_login?' do
|
let(:instance) { described_class.new(user.login, password) }
|
||||||
expect(described_class).to respond_to(:can_login?)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'checks if users can login' do
|
|
||||||
user = create(:user)
|
|
||||||
result = described_class.can_login?(user)
|
|
||||||
expect(result).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'not loginable' do
|
|
||||||
|
|
||||||
it 'fails if user has too many failed logins' do
|
|
||||||
user = create(:user, login_failed: 999)
|
|
||||||
result = described_class.can_login?(user)
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it "fails if user isn't active" do
|
|
||||||
user = create(:user, active: false)
|
|
||||||
result = described_class.can_login?(user)
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fails if parameter is no User instance' do
|
|
||||||
result = described_class.can_login?('user')
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'given default password_max_login_failed' do
|
|
||||||
it 'passes with 5 attempts' do
|
|
||||||
user = create(:user, login_failed: 5)
|
|
||||||
result = described_class.can_login?(user)
|
|
||||||
expect(result).to be true
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'fails with 6 attempts' do
|
|
||||||
user = create(:user, login_failed: 6)
|
|
||||||
result = described_class.can_login?(user)
|
|
||||||
expect(result).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.valid?' do
|
describe '.valid?' do
|
||||||
it 'responds to valid?' do
|
it 'responds to valid?' do
|
||||||
expect(described_class).to respond_to(:valid?)
|
expect(instance).to respond_to(:valid?)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'authenticates users' do
|
context 'with an internal user' do
|
||||||
password = 'zammad'
|
context 'with valid credentials' do
|
||||||
user = create(:user, password: password)
|
it 'check for valid credentials' do
|
||||||
result = described_class.valid?(user, password)
|
expect(instance.valid?).to be true
|
||||||
expect(result).to be true
|
end
|
||||||
|
|
||||||
|
it 'check for not increased failed login count' do
|
||||||
|
expect { instance.valid? }.not_to change { user.reload.login_failed }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not case-sensitive' do
|
||||||
|
let(:instance) { described_class.new(user.login.upcase, password) }
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.backends' do
|
context 'when email is used' do
|
||||||
it 'responds to backends' do
|
let(:instance) { described_class.new(user.email, password) }
|
||||||
expect(described_class).to respond_to(:backends)
|
|
||||||
|
it 'check for valid credentials' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns a list of Hashes' do
|
context 'when previous login was' do
|
||||||
result = described_class.backends
|
context 'when never logged in' do
|
||||||
expect(result).to be_an(Array)
|
it 'updates #last_login and #updated_at' do
|
||||||
expect(result.first).to be_a(Hash)
|
expect { instance.valid? }.to change { user.reload.last_login }.and change { user.reload.updated_at }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when less than 10 minutes ago' do
|
||||||
|
before do
|
||||||
|
instance.valid?
|
||||||
|
travel 9.minutes
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update #last_login and #updated_at' do
|
||||||
|
expect { instance.valid? }.to not_change { user.reload.last_login }.and not_change { user.reload.updated_at }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when more than 10 minutes ago' do
|
||||||
|
before do
|
||||||
|
instance.valid?
|
||||||
|
travel 11.minutes
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates #last_login and #updated_at' do
|
||||||
|
expect { instance.valid? }.to change { user.reload.last_login }.and change { user.reload.updated_at }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid user and invalid password' do
|
||||||
|
let(:instance) { described_class.new(user.login, 'wrong') }
|
||||||
|
|
||||||
|
it 'check for invalid credentials' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'check for increased failed login count' do
|
||||||
|
expect { instance.valid? }.to change { user.reload.login_failed }.from(0).to(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'failed login avoids brute force attack' do
|
||||||
|
allow(instance).to receive(:sleep)
|
||||||
|
instance.valid?
|
||||||
|
expect(instance).to have_received(:sleep).with(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with inactive user login' do
|
||||||
|
let(:user) { create(:user, active: false) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with non-existent user login' do
|
||||||
|
let(:instance) { described_class.new('not_existing', password) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty user login' do
|
||||||
|
let(:instance) { described_class.new('', password) }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is empty' do
|
||||||
|
before do
|
||||||
|
# Remove adapter from auth developer setting, to avoid execution for this test case, because of special empty
|
||||||
|
# password handling in adapter.
|
||||||
|
Setting.set('auth_developer', {})
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty password string' do
|
||||||
|
let(:password) { '' }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'check empty password' do
|
||||||
|
context 'when password is an empty string' do
|
||||||
|
let(:password) { '' }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when password is nil' do
|
||||||
|
let(:password) { nil }
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty password string when the stored password is an empty string' do
|
||||||
|
before { user.update_column(:password, '') }
|
||||||
|
|
||||||
|
include_examples 'check empty password'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with empty password string when the stored hash represents an empty string' do
|
||||||
|
before { user.update(password: PasswordHash.crypt('')) }
|
||||||
|
|
||||||
|
include_examples 'check empty password'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a ldap user' do
|
||||||
|
let(:password_ldap) { 'zammad_ldap' }
|
||||||
|
let(:ldap_user) { instance_double(Ldap::User) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
Setting.set('ldap_integration', true)
|
||||||
|
|
||||||
|
allow(Ldap::User).to receive(:new).with(any_args).and_return(ldap_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a ldap user without internal password' do
|
||||||
|
let(:user) { create(:user, source: 'Ldap') }
|
||||||
|
let(:password) { password_ldap }
|
||||||
|
|
||||||
|
context 'with valid credentials' do
|
||||||
|
before do
|
||||||
|
allow(ldap_user).to receive(:valid?).with(any_args).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid credentials' do
|
||||||
|
let(:password) { 'wrong' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(ldap_user).to receive(:valid?).with(any_args).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'check for not increased failed login count' do
|
||||||
|
expect { instance.valid? }.not_to change { user.reload.login_failed }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a ldap user which also has a internal password' do
|
||||||
|
let(:user) { create(:user, source: 'Ldap', password: password) }
|
||||||
|
let(:password) { password_ldap }
|
||||||
|
|
||||||
|
context 'with valid ldap credentials' do
|
||||||
|
before do
|
||||||
|
allow(ldap_user).to receive(:valid?).with(any_args).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid ldap credentials' do
|
||||||
|
let(:instance) { described_class.new(user.login, 'wrong') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(ldap_user).to receive(:valid?).with(any_args).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false' do
|
||||||
|
expect(instance.valid?).to be false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'check for not increased failed login count' do
|
||||||
|
expect { instance.valid? }.to change { user.reload.login_failed }.from(0).to(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with valid internal credentials' do
|
||||||
|
before do
|
||||||
|
allow(ldap_user).to receive(:valid?).with(any_args).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true' do
|
||||||
|
expect(instance.valid?).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -63,7 +63,7 @@ RSpec.describe AutoWizard do
|
||||||
.and change { User.last.firstname }.to('Test Master')
|
.and change { User.last.firstname }.to('Test Master')
|
||||||
.and change { User.last.lastname }.to('Agent')
|
.and change { User.last.lastname }.to('Agent')
|
||||||
.and change { User.last.email }.to('master_unit_test01@example.com')
|
.and change { User.last.email }.to('master_unit_test01@example.com')
|
||||||
.and change { User.authenticate(User.last.email, 'test') }.from(nil)
|
.and change { Auth.new(User.last.email, 'test').valid? }.from(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ RSpec.describe AutoWizard do
|
||||||
.and change { User.last.firstname }.to('Test Master')
|
.and change { User.last.firstname }.to('Test Master')
|
||||||
.and change { User.last.lastname }.to('Agent')
|
.and change { User.last.lastname }.to('Agent')
|
||||||
.and change { User.last.email }.to('master_unit_test01@example.com')
|
.and change { User.last.email }.to('master_unit_test01@example.com')
|
||||||
.and change { User.authenticate(User.last.email, 'test') }.from(nil)
|
.and change { Auth.new(User.last.email, 'test').valid? }.from(false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -35,154 +35,6 @@ RSpec.describe User, type: :model do
|
||||||
it_behaves_like 'UserPerformsGeoLookup'
|
it_behaves_like 'UserPerformsGeoLookup'
|
||||||
|
|
||||||
describe 'Class methods:' do
|
describe 'Class methods:' do
|
||||||
describe '.authenticate' do
|
|
||||||
subject(:user) { create(:user, password: password) }
|
|
||||||
|
|
||||||
let(:password) { Faker::Internet.password }
|
|
||||||
|
|
||||||
context 'with valid credentials' do
|
|
||||||
context 'using #login' do
|
|
||||||
it 'returns the matching user' do
|
|
||||||
expect(described_class.authenticate(user.login, password))
|
|
||||||
.to eq(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is not case-sensitive' do
|
|
||||||
expect(described_class.authenticate(user.login.upcase, password))
|
|
||||||
.to eq(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'using #email' do
|
|
||||||
it 'returns the matching user' do
|
|
||||||
expect(described_class.authenticate(user.email, password))
|
|
||||||
.to eq(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'is not case-sensitive' do
|
|
||||||
expect(described_class.authenticate(user.email.upcase, password))
|
|
||||||
.to eq(user)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'but exceeding failed login limit' do
|
|
||||||
before { user.update(login_failed: 999) }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate(user.login, password))
|
|
||||||
.to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when previous login was' do
|
|
||||||
context 'never' do
|
|
||||||
it 'updates #last_login and #updated_at' do
|
|
||||||
expect { described_class.authenticate(user.login, password) }
|
|
||||||
.to change { user.reload.last_login }
|
|
||||||
.and change { user.reload.updated_at }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'less than 10 minutes ago' do
|
|
||||||
before do
|
|
||||||
described_class.authenticate(user.login, password)
|
|
||||||
travel 9.minutes
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'does not update #last_login and #updated_at' do
|
|
||||||
expect { described_class.authenticate(user.login, password) }
|
|
||||||
.to not_change { user.reload.last_login }
|
|
||||||
.and not_change { user.reload.updated_at }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'more than 10 minutes ago' do
|
|
||||||
before do
|
|
||||||
described_class.authenticate(user.login, password)
|
|
||||||
travel 11.minutes
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'updates #last_login and #updated_at' do
|
|
||||||
expect { described_class.authenticate(user.login, password) }
|
|
||||||
.to change { user.reload.last_login }
|
|
||||||
.and change { user.reload.updated_at }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with valid user and invalid password' do
|
|
||||||
it 'increments failed login count' do
|
|
||||||
expect(described_class).to receive(:sleep).with(1)
|
|
||||||
expect { described_class.authenticate(user.login, password.next) }
|
|
||||||
.to change { user.reload.login_failed }.by(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class).to receive(:sleep).with(1)
|
|
||||||
expect(described_class.authenticate(user.login, password.next)).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with inactive user’s login' do
|
|
||||||
before { user.update(active: false) }
|
|
||||||
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate(user.login, password)).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with non-existent user login' do
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate('john.doe', password)).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with empty login string' do
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate('', password)).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with empty password string' do
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate(user.login, '')).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with empty password string when the stored password is an empty string' do
|
|
||||||
before { user.update_column(:password, '') }
|
|
||||||
|
|
||||||
context 'when password is an empty string' do
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate(user.login, '')).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when password is nil' do
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate(user.login, nil)).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'with empty password string when the stored hash represents an empty string' do
|
|
||||||
before { user.update(password: PasswordHash.crypt('')) }
|
|
||||||
|
|
||||||
context 'when password is an empty string' do
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate(user.login, '')).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when password is nil' do
|
|
||||||
it 'returns nil' do
|
|
||||||
expect(described_class.authenticate(user.login, nil)).to be(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '.identify' do
|
describe '.identify' do
|
||||||
it 'returns users by given login' do
|
it 'returns users by given login' do
|
||||||
expect(described_class.identify(user.login)).to eq(user)
|
expect(described_class.identify(user.login)).to eq(user)
|
||||||
|
@ -191,37 +43,14 @@ RSpec.describe User, type: :model do
|
||||||
it 'returns users by given email' do
|
it 'returns users by given email' do
|
||||||
expect(described_class.identify(user.email)).to eq(user)
|
expect(described_class.identify(user.email)).to eq(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'returns nil for empty username' do
|
||||||
|
expect(described_class.identify('')).to eq(nil)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'Instance methods:' do
|
describe 'Instance methods:' do
|
||||||
describe '#max_login_failed?' do
|
|
||||||
it { is_expected.to respond_to(:max_login_failed?) }
|
|
||||||
|
|
||||||
context 'with "password_max_login_failed" setting' do
|
|
||||||
before do
|
|
||||||
Setting.set('password_max_login_failed', 5)
|
|
||||||
user.update(login_failed: 5)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns true once user’s #login_failed count exceeds the setting' do
|
|
||||||
expect { user.update(login_failed: 6) }
|
|
||||||
.to change(user, :max_login_failed?).to(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'without password_max_login_failed setting' do
|
|
||||||
before do
|
|
||||||
Setting.set('password_max_login_failed', nil)
|
|
||||||
user.update(login_failed: 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'defaults to 0' do
|
|
||||||
expect { user.update(login_failed: 1) }
|
|
||||||
.to change(user, :max_login_failed?).to(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#out_of_office?' do
|
describe '#out_of_office?' do
|
||||||
context 'without any out_of_office_* attributes set' do
|
context 'without any out_of_office_* attributes set' do
|
||||||
|
|
|
@ -70,12 +70,6 @@ module ZammadSpecSupportRequest
|
||||||
password = options[:password] || user.password.to_s
|
password = options[:password] || user.password.to_s
|
||||||
login = options[:login] || user.login
|
login = options[:login] || user.login
|
||||||
|
|
||||||
# mock authentication otherwise login won't
|
|
||||||
# if user has no password (which is expensive to create)
|
|
||||||
if password.blank?
|
|
||||||
allow(User).to receive(:authenticate).with(login, '') { user.update_last_login }.and_return(user)
|
|
||||||
end
|
|
||||||
|
|
||||||
case via
|
case via
|
||||||
when :api_client
|
when :api_client
|
||||||
# ensure that always the correct header value is set
|
# ensure that always the correct header value is set
|
||||||
|
|
Loading…
Reference in a new issue