mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2024-11-29 10:24:20 +01:00
125 lines
5.3 KiB
Ruby
125 lines
5.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Add single sign on functionalities to the user model
|
|
module SingleSignOnConcern
|
|
extend ActiveSupport::Concern
|
|
require 'sso_logger'
|
|
|
|
included do
|
|
# enable OmniAuth authentication only if needed
|
|
devise :omniauthable, omniauth_providers: [AuthProvider.active.strategy_name.to_sym] unless
|
|
AuthProvider.active.providable_type == DatabaseProvider.name
|
|
|
|
## Retrieve the requested data in the User and user's Profile tables
|
|
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
|
|
def get_data_from_sso_mapping(sso_mapping)
|
|
service = UserGetterService.new(self)
|
|
service.read_attribute(sso_mapping)
|
|
end
|
|
|
|
## Set some data on the current user, according to the sso_key given
|
|
## @param sso_mapping {String} must be of form 'user._field_' or 'profile._field_'. Eg. 'user.email'
|
|
## @param data {*} the data to put in the given key. Eg. 'user@example.com'
|
|
def set_data_from_sso_mapping(sso_mapping, data)
|
|
return if data.nil? || data.blank?
|
|
|
|
service = UserSetterService.new(self)
|
|
service.assign_attibute(sso_mapping, data)
|
|
|
|
return if mapped_from_sso&.include?(sso_mapping)
|
|
|
|
self.mapped_from_sso = [mapped_from_sso, sso_mapping].compact.join(',')
|
|
end
|
|
|
|
## used to allow the migration of existing users between authentication providers
|
|
def generate_auth_migration_token
|
|
update(auth_token: Devise.friendly_token)
|
|
end
|
|
|
|
## link the current user to the given provider (omniauth attributes hash)
|
|
## and remove the auth_token to mark his account as "migrated"
|
|
def link_with_omniauth_provider(auth)
|
|
active_provider = AuthProvider.active
|
|
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
|
|
|
|
if User.where(provider: auth.provider, uid: auth.uid).size.positive?
|
|
raise DuplicateIndexError, "This #{active_provider.name} account is already linked to an existing user"
|
|
end
|
|
|
|
update(provider: auth.provider, uid: auth.uid, auth_token: nil)
|
|
end
|
|
|
|
## Merge the provided User's SSO details into the current user and drop the provided user to ensure the unity
|
|
## @param sso_user {User} the provided user will be DELETED after the merge was successful
|
|
def merge_from_sso(sso_user)
|
|
logger = SsoLogger.new
|
|
logger.debug "[User::merge_from_sso] initiated with parameter #{sso_user}"
|
|
# update the attributes to link the account to the sso account
|
|
self.provider = sso_user.provider
|
|
self.uid = sso_user.uid
|
|
|
|
# remove the token
|
|
self.auth_token = nil
|
|
self.merged_at = Time.current
|
|
|
|
# check that the email duplication was resolved
|
|
if sso_user.email.end_with? '-duplicate'
|
|
email_addr = sso_user.email.match(/^<([^>]+)>.{20}-duplicate$/)[1]
|
|
logger.error 'duplicate email was not resolved'
|
|
raise(DuplicateIndexError, email_addr) unless email_addr == email
|
|
end
|
|
|
|
# update the user's profile to set the data managed by the SSO
|
|
auth_provider = AuthProvider.from_strategy_name(sso_user.provider)
|
|
logger.debug "found auth_provider=#{auth_provider&.name}"
|
|
auth_provider&.sso_fields&.each do |field|
|
|
value = sso_user.get_data_from_sso_mapping(field)
|
|
logger.debug "mapping sso field #{field} with value=#{value}"
|
|
# We do not merge the email field if its end with the special value '-duplicate' as this means
|
|
# that the user is currently merging with the account that have the same email than the sso.
|
|
# Moreover, if the user is an administrator, we must keep him in his group
|
|
unless (field == 'user.email' && value.end_with?('-duplicate')) || (field == 'user.group_id' && admin?)
|
|
set_data_from_sso_mapping(field, value)
|
|
end
|
|
end
|
|
|
|
# run the account transfer in an SQL transaction to ensure data integrity
|
|
begin
|
|
User.transaction do
|
|
# remove the temporary account
|
|
logger.debug 'removing the temporary user'
|
|
sso_user.destroy
|
|
# finally, save the new details
|
|
logger.debug 'saving the updated user'
|
|
save!
|
|
end
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
logger.error "error while merging user #{sso_user.id} into #{id}: #{e.message}"
|
|
raise e
|
|
end
|
|
end
|
|
end
|
|
|
|
class_methods do
|
|
def from_omniauth(auth)
|
|
logger = SsoLogger.new
|
|
logger.debug "[User::from_omniauth] initiated with parameter #{auth}"
|
|
active_provider = AuthProvider.active
|
|
raise SecurityError, 'The identity provider does not match the activated one' if active_provider.strategy_name != auth.provider
|
|
|
|
where(provider: auth.provider, uid: auth.uid).first_or_create.tap do |user|
|
|
# execute this regardless of whether record exists or not (-> User#tap)
|
|
# this will init or update the user thanks to the information retrieved from the SSO
|
|
logger.debug user.id.nil? ? 'no user found, creating a new one' : "found user id=#{user.id}"
|
|
user.profile ||= Profile.new
|
|
auth.info.mapping.each do |key, value|
|
|
logger.debug "mapping info #{key} with value=#{value}"
|
|
user.set_data_from_sso_mapping(key, value)
|
|
end
|
|
logger.debug 'generating a new password'
|
|
user.password = SecurePassword.generate
|
|
end
|
|
end
|
|
end
|
|
end
|