kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
247 lines
7.7 KiB
247 lines
7.7 KiB
#-- encoding: UTF-8
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 2012-2022 the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See COPYRIGHT and LICENSE files for more details.
|
|
#++
|
|
|
|
module Authentication
|
|
class OmniauthService
|
|
include Contracted
|
|
|
|
attr_accessor :auth_hash,
|
|
:strategy,
|
|
:controller,
|
|
:contract,
|
|
:user_attributes,
|
|
:identity_url,
|
|
:user
|
|
|
|
delegate :session, to: :controller
|
|
|
|
def initialize(strategy:, auth_hash:, controller:)
|
|
self.strategy = strategy
|
|
self.auth_hash = auth_hash
|
|
self.controller = controller
|
|
self.contract = ::Authentication::OmniauthAuthHashContract.new(auth_hash)
|
|
end
|
|
|
|
def call(additional_user_params = nil)
|
|
inspect_response(Logger::DEBUG)
|
|
|
|
unless contract.validate
|
|
result = ServiceResult.new(success: false, errors: contract.errors)
|
|
Rails.logger.error do
|
|
"[OmniAuth strategy #{strategy.name}] Failed to process omniauth response for #{auth_uid}: #{result.message}"
|
|
end
|
|
inspect_response(Logger::ERROR)
|
|
|
|
return result
|
|
end
|
|
|
|
# Create or update the user from omniauth
|
|
# and assign non-nil parameters from the registration form - if any
|
|
assignable_params = (additional_user_params || {}).reject { |_, v| v.nil? }
|
|
update_user_from_omniauth!(assignable_params)
|
|
|
|
# If we have a new or invited user, we still need to register them
|
|
activation_call = activate_user!
|
|
|
|
# The user should be logged in now
|
|
tap_service_result activation_call
|
|
end
|
|
|
|
private
|
|
|
|
##
|
|
# Inspect the response object, trying to find out what got returned
|
|
def inspect_response(log_level)
|
|
case strategy
|
|
when ::OmniAuth::Strategies::SAML
|
|
::OpenProject::AuthSaml::Inspector.inspect_response(auth_hash) do |message|
|
|
Rails.logger.add log_level, message
|
|
end
|
|
else
|
|
Rails.logger.add(log_level) do
|
|
"[OmniAuth strategy #{strategy.name}] Returning from omniauth with hash " \
|
|
"#{auth_hash&.to_hash.inspect} Valid? #{auth_hash&.valid?}"
|
|
end
|
|
end
|
|
rescue StandardError => e
|
|
OpenProject.logger.error "[OmniAuth strategy #{strategy.name}] Failed to inspect OmniAuth response: #{e.message}"
|
|
end
|
|
|
|
##
|
|
# After login flow
|
|
def tap_service_result(call)
|
|
if call.success? && user.active?
|
|
OpenProject::OmniAuth::Authorization.after_login! user, auth_hash, self
|
|
end
|
|
|
|
call
|
|
end
|
|
|
|
##
|
|
# After validating the omniauth hash
|
|
# and the authorization is successful,
|
|
#
|
|
# login the user by locating or creating it
|
|
def update_user_from_omniauth!(additional_user_params)
|
|
# Find or create the user from the auth hash
|
|
self.user_attributes = build_omniauth_hash_to_user_attributes.merge(additional_user_params)
|
|
self.identity_url = user_attributes[:identity_url]
|
|
self.user = lookup_or_initialize_user
|
|
|
|
# Assign or update the user with the omniauth attributes
|
|
update_attributes
|
|
end
|
|
|
|
##
|
|
# Try to find or create the user
|
|
# in the following order:
|
|
#
|
|
# 1. Look for an active invitation token
|
|
# 2. Look for an existing user for the current identity_url
|
|
# 3. Look for an existing user that we can remap (IF remapping is allowed)
|
|
# 4. Try to register a new user and activate according to settings
|
|
def lookup_or_initialize_user
|
|
find_invited_user ||
|
|
find_existing_user ||
|
|
remap_existing_user ||
|
|
initialize_new_user
|
|
end
|
|
|
|
##
|
|
# Return an invited user, if there is a token
|
|
def find_invited_user
|
|
return unless session.include?(:invitation_token)
|
|
|
|
tok = Token::Invitation.find_by value: session[:invitation_token]
|
|
return unless tok
|
|
|
|
tok.user.tap do |user|
|
|
user.identity_url = user_attributes[:identity_url]
|
|
tok.destroy
|
|
session.delete :invitation_token
|
|
end
|
|
end
|
|
|
|
##
|
|
# Find an existing user by the identity url
|
|
def find_existing_user
|
|
User.find_by(identity_url: identity_url)
|
|
end
|
|
|
|
##
|
|
# Allow to map existing users with an Omniauth source if the login
|
|
# already exists, and no existing auth source or omniauth provider is
|
|
# linked
|
|
def remap_existing_user
|
|
return unless Setting.oauth_allow_remapping_of_existing_users?
|
|
|
|
User.find_by_login(user_attributes[:login])
|
|
end
|
|
|
|
##
|
|
# Create the new user and try to activate it
|
|
# according to settings and system limits
|
|
def initialize_new_user
|
|
User.new(identity_url: user_attributes[:identity_url])
|
|
end
|
|
|
|
##
|
|
# Update or assign the user attributes
|
|
def update_attributes
|
|
if user.new_record? || user.invited?
|
|
user.register unless user.invited?
|
|
|
|
::Users::SetAttributesService
|
|
.new(user: User.system, model: user, contract_class: ::Users::UpdateContract)
|
|
.call(user_attributes)
|
|
.result
|
|
else
|
|
# Update the user, but never change the admin flag
|
|
::Users::UpdateService
|
|
.new(user: User.system, model: user)
|
|
.call(user_attributes.except(:admin))
|
|
end
|
|
end
|
|
|
|
def activate_user!
|
|
if user.new_record? || user.invited?
|
|
::Users::RegisterUserService
|
|
.new(user)
|
|
.call
|
|
else
|
|
ServiceResult.new(success: true, result: user)
|
|
end
|
|
end
|
|
|
|
##
|
|
# Maps the omniauth attribute hash
|
|
# to our internal user attributes
|
|
def build_omniauth_hash_to_user_attributes
|
|
info = auth_hash[:info]
|
|
|
|
attribute_map = {
|
|
login: info[:email],
|
|
mail: info[:email],
|
|
firstname: info[:first_name] || info[:name],
|
|
lastname: info[:last_name],
|
|
identity_url: identity_url_from_omniauth
|
|
}
|
|
|
|
# Allow strategies to override mapping
|
|
if strategy.respond_to?(:omniauth_hash_to_user_attributes)
|
|
attribute_map.merge!(strategy.omniauth_hash_to_user_attributes(auth_hash))
|
|
end
|
|
|
|
# Remove any nil values to avoid
|
|
# overriding existing attributes
|
|
attribute_map.compact!
|
|
|
|
Rails.logger.debug { "Mapped auth_hash user attributes #{attribute_map.inspect}" }
|
|
attribute_map
|
|
end
|
|
|
|
##
|
|
# Allow strategies to map a value for uid instead
|
|
# of always taking the global UID.
|
|
# For SAML, the global UID may change with every session
|
|
# (in case of transient nameIds)
|
|
def identity_url_from_omniauth
|
|
identifier = auth_hash[:info][:uid] || auth_hash[:uid]
|
|
"#{auth_hash[:provider]}:#{identifier}"
|
|
end
|
|
|
|
##
|
|
# Try to provide some context of the auth_hash in case of errors
|
|
def auth_uid
|
|
hash = (auth_hash || {})
|
|
hash.dig(:info, :uid) || hash.dig(:uid) || 'unknown'
|
|
end
|
|
end
|
|
end
|
|
|