summaryrefslogtreecommitdiff
path: root/lib/gitlab/auth
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/auth')
-rw-r--r--lib/gitlab/auth/blocked_user_tracker.rb4
-rw-r--r--lib/gitlab/auth/database/authentication.rb2
-rw-r--r--lib/gitlab/auth/ldap/access.rb43
-rw-r--r--lib/gitlab/auth/ldap/adapter.rb43
-rw-r--r--lib/gitlab/auth/ldap/authentication.rb22
-rw-r--r--lib/gitlab/auth/ldap/config.rb18
-rw-r--r--lib/gitlab/auth/ldap/ldap_connection_error.rb7
-rw-r--r--lib/gitlab/auth/ldap/user.rb9
-rw-r--r--lib/gitlab/auth/o_auth/authentication.rb1
-rw-r--r--lib/gitlab/auth/o_auth/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/o_auth/user.rb21
-rw-r--r--lib/gitlab/auth/omniauth_identity_linker_base.rb51
-rw-r--r--lib/gitlab/auth/request_authenticator.rb2
-rw-r--r--lib/gitlab/auth/saml/auth_hash.rb15
-rw-r--r--lib/gitlab/auth/saml/config.rb8
-rw-r--r--lib/gitlab/auth/saml/identity_linker.rb8
-rw-r--r--lib/gitlab/auth/saml/user.rb21
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb33
-rw-r--r--lib/gitlab/auth/user_auth_finders.rb28
19 files changed, 290 insertions, 54 deletions
diff --git a/lib/gitlab/auth/blocked_user_tracker.rb b/lib/gitlab/auth/blocked_user_tracker.rb
index dae03a179e4..7609a7b04f6 100644
--- a/lib/gitlab/auth/blocked_user_tracker.rb
+++ b/lib/gitlab/auth/blocked_user_tracker.rb
@@ -17,7 +17,9 @@ module Gitlab
# message passed along by Warden.
return unless message == User::BLOCKED_MESSAGE
- login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
+ # Check for either LDAP or regular GitLab account logins
+ login = env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'username') ||
+ env.dig(ACTIVE_RECORD_REQUEST_PARAMS, 'user', 'login')
return unless login.present?
diff --git a/lib/gitlab/auth/database/authentication.rb b/lib/gitlab/auth/database/authentication.rb
index 260a77058a4..1234ace0334 100644
--- a/lib/gitlab/auth/database/authentication.rb
+++ b/lib/gitlab/auth/database/authentication.rb
@@ -8,7 +8,7 @@ module Gitlab
def login(login, password)
return false unless Gitlab::CurrentSettings.password_authentication_enabled_for_git?
- user&.valid_password?(password)
+ return user if user&.valid_password?(password)
end
end
end
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
index 77c0ddc2d48..865185eb5db 100644
--- a/lib/gitlab/auth/ldap/access.rb
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -6,7 +6,7 @@ module Gitlab
module Auth
module LDAP
class Access
- attr_reader :provider, :user
+ attr_reader :provider, :user, :ldap_identity
def self.open(user, &block)
Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
@@ -14,9 +14,12 @@ module Gitlab
end
end
- def self.allowed?(user)
+ def self.allowed?(user, options = {})
self.open(user) do |access|
+ # Whether user is allowed, or not, we should update
+ # permissions to keep things clean
if access.allowed?
+ access.update_user
Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
true
@@ -29,7 +32,8 @@ module Gitlab
def initialize(user, adapter = nil)
@adapter = adapter
@user = user
- @provider = user.ldap_identity.provider
+ @ldap_identity = user.ldap_identity
+ @provider = adapter&.provider || ldap_identity&.provider
end
def allowed?
@@ -40,7 +44,7 @@ module Gitlab
end
# Block user in GitLab if he/she was blocked in AD
- if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
+ if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(ldap_identity.extern_uid, adapter)
block_user(user, 'is disabled in Active Directory')
false
else
@@ -52,6 +56,8 @@ module Gitlab
block_user(user, 'does not exist anymore')
false
end
+ rescue LDAPConnectionError
+ false
end
def adapter
@@ -62,27 +68,44 @@ module Gitlab
Gitlab::Auth::LDAP::Config.new(provider)
end
+ def find_ldap_user
+ Gitlab::Auth::LDAP::Person.find_by_dn(ldap_identity.extern_uid, adapter)
+ end
+
def ldap_user
- @ldap_user ||= Gitlab::Auth::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ return unless provider
+
+ @ldap_user ||= find_ldap_user
end
def block_user(user, reason)
user.ldap_block
- Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
- "blocking Gitlab user \"#{user.name}\" (#{user.email})"
- )
+ if provider
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ else
+ Gitlab::AppLogger.info(
+ "Account is not provided by LDAP, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
end
def unblock_user(user, reason)
user.activate
Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "LDAP account \"#{ldap_identity.extern_uid}\" #{reason}, " \
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
+
+ def update_user
+ # no-op in CE
+ end
end
end
end
diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb
index caf2d18c668..82ff1e77e5c 100644
--- a/lib/gitlab/auth/ldap/adapter.rb
+++ b/lib/gitlab/auth/ldap/adapter.rb
@@ -2,6 +2,9 @@ module Gitlab
module Auth
module LDAP
class Adapter
+ SEARCH_RETRY_FACTOR = [1, 1, 2, 3].freeze
+ MAX_SEARCH_RETRIES = Rails.env.test? ? 1 : SEARCH_RETRY_FACTOR.size.freeze
+
attr_reader :provider, :ldap
def self.open(provider, &block)
@@ -16,7 +19,7 @@ module Gitlab
def initialize(provider, ldap = nil)
@provider = provider
- @ldap = ldap || Net::LDAP.new(config.adapter_options)
+ @ldap = ldap || renew_connection_adapter
end
def config
@@ -47,8 +50,10 @@ module Gitlab
end
def ldap_search(*args)
+ retries ||= 0
+
# Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
- Timeout.timeout(config.timeout) do
+ Timeout.timeout(timeout_time(retries)) do
results = ldap.search(*args)
if results.nil?
@@ -63,16 +68,26 @@ module Gitlab
results
end
end
- rescue Net::LDAP::Error => error
- Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
- []
- rescue Timeout::Error
- Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
- []
+ rescue Net::LDAP::Error, Timeout::Error => error
+ retries += 1
+ error_message = connection_error_message(error)
+
+ Rails.logger.warn(error_message)
+
+ if retries < MAX_SEARCH_RETRIES
+ renew_connection_adapter
+ retry
+ else
+ raise LDAPConnectionError, error_message
+ end
end
private
+ def timeout_time(retry_number)
+ SEARCH_RETRY_FACTOR[retry_number] * config.timeout
+ end
+
def user_options(fields, value, limit)
options = {
attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config),
@@ -104,6 +119,18 @@ module Gitlab
filter
end
end
+
+ def connection_error_message(exception)
+ if exception.is_a?(Timeout::Error)
+ "LDAP search timed out after #{config.timeout} seconds"
+ else
+ "LDAP search raised exception #{exception.class}: #{exception.message}"
+ end
+ end
+
+ def renew_connection_adapter
+ @ldap = Net::LDAP.new(config.adapter_options)
+ end
end
end
end
diff --git a/lib/gitlab/auth/ldap/authentication.rb b/lib/gitlab/auth/ldap/authentication.rb
index e70c3ab6b46..7c134fb6438 100644
--- a/lib/gitlab/auth/ldap/authentication.rb
+++ b/lib/gitlab/auth/ldap/authentication.rb
@@ -12,30 +12,26 @@ module Gitlab
return unless Gitlab::Auth::LDAP::Config.enabled?
return unless login.present? && password.present?
- auth = nil
- # loop through providers until valid bind
+ # return found user that was authenticated by first provider for given login credentials
providers.find do |provider|
auth = new(provider)
- auth.login(login, password) # true will exit the loop
+ break auth.user if auth.login(login, password) # true will exit the loop
end
-
- # If (login, password) was invalid for all providers, the value of auth is now the last
- # Gitlab::Auth::LDAP::Authentication instance we tried.
- auth.user
end
def self.providers
Gitlab::Auth::LDAP::Config.providers
end
- attr_accessor :ldap_user
-
def login(login, password)
- @ldap_user = adapter.bind_as(
+ result = adapter.bind_as(
filter: user_filter(login),
size: 1,
password: password
)
+ return unless result
+
+ @user = Gitlab::Auth::LDAP::User.find_by_uid_and_provider(result.dn, provider)
end
def adapter
@@ -56,12 +52,6 @@ module Gitlab
filter
end
-
- def user
- return unless ldap_user
-
- Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
- end
end
end
end
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
index 77185f52ced..d4415eaa6dc 100644
--- a/lib/gitlab/auth/ldap/config.rb
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -11,6 +11,8 @@ module Gitlab
attr_accessor :provider, :options
+ InvalidProvider = Class.new(StandardError)
+
def self.enabled?
Gitlab.config.ldap.enabled
end
@@ -22,6 +24,10 @@ module Gitlab
def self.available_servers
return [] unless enabled?
+ _available_servers
+ end
+
+ def self._available_servers
Array.wrap(servers.first)
end
@@ -34,7 +40,7 @@ module Gitlab
end
def self.invalid_provider(provider)
- raise "Unknown provider (#{provider}). Available providers: #{providers}"
+ raise InvalidProvider.new("Unknown provider (#{provider}). Available providers: #{providers}")
end
def initialize(provider)
@@ -84,13 +90,17 @@ module Gitlab
end
def base
- options['base']
+ @base ||= Person.normalize_dn(options['base'])
end
def uid
options['uid']
end
+ def label
+ options['label']
+ end
+
def sync_ssh_keys?
sync_ssh_keys.present?
end
@@ -132,6 +142,10 @@ module Gitlab
options['timeout'].to_i
end
+ def external_groups
+ options['external_groups'] || []
+ end
+
def has_auth?
options['password'] || options['bind_dn']
end
diff --git a/lib/gitlab/auth/ldap/ldap_connection_error.rb b/lib/gitlab/auth/ldap/ldap_connection_error.rb
new file mode 100644
index 00000000000..ef0a695742b
--- /dev/null
+++ b/lib/gitlab/auth/ldap/ldap_connection_error.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module Auth
+ module LDAP
+ LDAPConnectionError = Class.new(StandardError)
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
index 068212d9a21..922d0567d99 100644
--- a/lib/gitlab/auth/ldap/user.rb
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -8,6 +8,8 @@ module Gitlab
module Auth
module LDAP
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
class << self
def find_by_uid_and_provider(uid, provider)
identity = ::Identity.with_extern_uid(provider, uid).take
@@ -29,7 +31,8 @@ module Gitlab
self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end
- def changed?
+ override :should_save?
+ def should_save?
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
@@ -41,6 +44,10 @@ module Gitlab
Gitlab::Auth::LDAP::Access.allowed?(gl_user)
end
+ def valid_sign_in?
+ allowed? && super
+ end
+
def ldap_config
Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
end
diff --git a/lib/gitlab/auth/o_auth/authentication.rb b/lib/gitlab/auth/o_auth/authentication.rb
index ed03b9f8b40..d4e7f35c857 100644
--- a/lib/gitlab/auth/o_auth/authentication.rb
+++ b/lib/gitlab/auth/o_auth/authentication.rb
@@ -12,6 +12,7 @@ module Gitlab
@user = user
end
+ # Implementation must return user object if login successful
def login(login, password)
raise NotImplementedError
end
diff --git a/lib/gitlab/auth/o_auth/identity_linker.rb b/lib/gitlab/auth/o_auth/identity_linker.rb
new file mode 100644
index 00000000000..de92d7a214d
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module OAuth
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index b6a96081278..e7283b2f9e8 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -30,6 +30,10 @@ module Gitlab
gl_user.try(:valid?)
end
+ def valid_sign_in?
+ valid? && persisted?
+ end
+
def save(provider = 'OAuth')
raise SigninDisabledForProviderError if oauth_provider_disabled?
raise SignupDisabledError unless gl_user
@@ -64,8 +68,22 @@ module Gitlab
user
end
+ def find_and_update!
+ save if should_save?
+
+ gl_user
+ end
+
+ def bypass_two_factor?
+ false
+ end
+
protected
+ def should_save?
+ true
+ end
+
def add_or_update_user_identities
return unless gl_user
@@ -124,6 +142,9 @@ module Gitlab
Gitlab::Auth::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
Gitlab::Auth::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
Gitlab::Auth::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
+
+ rescue Gitlab::Auth::LDAP::LDAPConnectionError
+ nil
end
def ldap_config
diff --git a/lib/gitlab/auth/omniauth_identity_linker_base.rb b/lib/gitlab/auth/omniauth_identity_linker_base.rb
new file mode 100644
index 00000000000..f79ce6bb809
--- /dev/null
+++ b/lib/gitlab/auth/omniauth_identity_linker_base.rb
@@ -0,0 +1,51 @@
+module Gitlab
+ module Auth
+ class OmniauthIdentityLinkerBase
+ attr_reader :current_user, :oauth
+
+ def initialize(current_user, oauth)
+ @current_user = current_user
+ @oauth = oauth
+ @changed = false
+ end
+
+ def link
+ save if identity.new_record?
+ end
+
+ def changed?
+ @changed
+ end
+
+ def failed?
+ error_message.present?
+ end
+
+ def error_message
+ identity.validate
+
+ identity.errors.full_messages.join(', ')
+ end
+
+ private
+
+ def save
+ @changed = identity.save
+ end
+
+ def identity
+ @identity ||= current_user.identities
+ .with_extern_uid(provider, uid)
+ .first_or_initialize(extern_uid: uid)
+ end
+
+ def provider
+ oauth['provider']
+ end
+
+ def uid
+ oauth['uid']
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/request_authenticator.rb b/lib/gitlab/auth/request_authenticator.rb
index a0b5cd868c3..66de52506ce 100644
--- a/lib/gitlab/auth/request_authenticator.rb
+++ b/lib/gitlab/auth/request_authenticator.rb
@@ -16,7 +16,7 @@ module Gitlab
end
def find_sessionless_user
- find_user_from_access_token || find_user_from_rss_token
+ find_user_from_access_token || find_user_from_feed_token
rescue Gitlab::Auth::AuthenticationError
nil
end
diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb
index c345a7e3f6c..3bc5e2864df 100644
--- a/lib/gitlab/auth/saml/auth_hash.rb
+++ b/lib/gitlab/auth/saml/auth_hash.rb
@@ -6,6 +6,17 @@ module Gitlab
Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
end
+ def authn_context
+ response_object = auth_hash.extra[:response_object]
+ return nil if response_object.blank?
+
+ document = response_object.decrypted_document
+ document ||= response_object.document
+ return nil if document.blank?
+
+ extract_authn_context(document)
+ end
+
private
def get_raw(key)
@@ -13,6 +24,10 @@ module Gitlab
# otherwise just the first value is returned
auth_hash.extra[:raw_info].all[key]
end
+
+ def extract_authn_context(document)
+ REXML::XPath.first(document, "//saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef/text()").to_s
+ end
end
end
end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
index 2760b1a3247..625dab7c6f4 100644
--- a/lib/gitlab/auth/saml/config.rb
+++ b/lib/gitlab/auth/saml/config.rb
@@ -7,6 +7,10 @@ module Gitlab
Gitlab::Auth::OAuth::Provider.config_for('saml')
end
+ def upstream_two_factor_authn_contexts
+ options.args[:upstream_two_factor_authn_contexts]
+ end
+
def groups
options[:groups_attribute]
end
@@ -14,6 +18,10 @@ module Gitlab
def external_groups
options[:external_groups]
end
+
+ def admin_groups
+ options[:admin_groups]
+ end
end
end
end
diff --git a/lib/gitlab/auth/saml/identity_linker.rb b/lib/gitlab/auth/saml/identity_linker.rb
new file mode 100644
index 00000000000..7e4b191d512
--- /dev/null
+++ b/lib/gitlab/auth/saml/identity_linker.rb
@@ -0,0 +1,8 @@
+module Gitlab
+ module Auth
+ module Saml
+ class IdentityLinker < OmniauthIdentityLinkerBase
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
index d4024e9ec39..6c3b75f3eb0 100644
--- a/lib/gitlab/auth/saml/user.rb
+++ b/lib/gitlab/auth/saml/user.rb
@@ -7,6 +7,8 @@ module Gitlab
module Auth
module Saml
class User < Gitlab::Auth::OAuth::User
+ extend ::Gitlab::Utils::Override
+
def save
super('SAML')
end
@@ -18,29 +20,36 @@ module Gitlab
user ||= find_or_build_ldap_user if auto_link_ldap_user?
user ||= build_new_user if signup_enabled?
- if external_users_enabled? && user
- # Check if there is overlap between the user's groups and the external groups
- # setting then set user as external or internal.
- user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty?
+ if user
+ user.external = !(auth_hash.groups & saml_config.external_groups).empty? if external_users_enabled?
end
user
end
- def changed?
+ override :should_save?
+ def should_save?
return true unless gl_user
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
+ def bypass_two_factor?
+ saml_config.upstream_two_factor_authn_contexts&.include?(auth_hash.authn_context)
+ end
+
protected
+ def saml_config
+ Gitlab::Auth::Saml::Config
+ end
+
def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user
end
def external_users_enabled?
- !Gitlab::Auth::Saml::Config.external_groups.nil?
+ !saml_config.external_groups.nil?
end
def auth_hash=(auth_hash)
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
new file mode 100644
index 00000000000..1893cb001b2
--- /dev/null
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -0,0 +1,33 @@
+module Gitlab
+ module Auth
+ class UserAccessDeniedReason
+ def initialize(user)
+ @user = user
+ end
+
+ def rejection_message
+ case rejection_type
+ when :internal
+ "This action cannot be performed by internal users"
+ when :terms_not_accepted
+ "You (#{@user.to_reference}) must accept the Terms of Service in order to perform this action. "\
+ "Please access GitLab from a web browser to accept these terms."
+ else
+ "Your account has been blocked."
+ end
+ end
+
+ private
+
+ def rejection_type
+ if @user.internal?
+ :internal
+ elsif @user.required_terms_not_accepted?
+ :terms_not_accepted
+ else
+ :blocked
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/user_auth_finders.rb b/lib/gitlab/auth/user_auth_finders.rb
index cf02030c577..c7993665421 100644
--- a/lib/gitlab/auth/user_auth_finders.rb
+++ b/lib/gitlab/auth/user_auth_finders.rb
@@ -1,9 +1,5 @@
module Gitlab
module Auth
- #
- # Exceptions
- #
-
AuthenticationError = Class.new(StandardError)
MissingTokenError = Class.new(AuthenticationError)
TokenNotFoundError = Class.new(AuthenticationError)
@@ -29,13 +25,15 @@ module Gitlab
current_request.env['warden']&.authenticate if verified_request?
end
- def find_user_from_rss_token
- return unless current_request.path.ends_with?('.atom') || current_request.format.atom?
+ def find_user_from_feed_token
+ return unless rss_request? || ics_request?
- token = current_request.params[:rss_token].presence
+ # NOTE: feed_token was renamed from rss_token but both needs to be supported because
+ # users might have already added the feed to their RSS reader before the rename
+ token = current_request.params[:feed_token].presence || current_request.params[:rss_token].presence
return unless token
- User.find_by_rss_token(token) || raise(UnauthorizedError)
+ User.find_by_feed_token(token) || raise(UnauthorizedError)
end
def find_user_from_access_token
@@ -61,6 +59,12 @@ module Gitlab
private
+ def route_authentication_setting
+ return {} unless respond_to?(:route_setting)
+
+ route_setting(:authentication) || {}
+ end
+
def access_token
strong_memoize(:access_token) do
find_oauth_access_token || find_personal_access_token
@@ -102,6 +106,14 @@ module Gitlab
def current_request
@current_request ||= ensure_action_dispatch_request(request)
end
+
+ def rss_request?
+ current_request.path.ends_with?('.atom') || current_request.format.atom?
+ end
+
+ def ics_request?
+ current_request.path.ends_with?('.ics') || current_request.format.ics?
+ end
end
end
end