2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2022-01-06 20:28:46 +08:00
class DiscourseConnect < DiscourseConnectBase
2019-06-11 08:04:26 +08:00
class BlankExternalId < StandardError
end
2023-12-15 23:46:04 +08:00
2019-06-11 08:04:26 +08:00
class BannedExternalId < StandardError
end
2014-02-25 11:30:49 +08:00
def self . sso_url
2021-02-08 18:04:33 +08:00
SiteSetting . discourse_connect_url
2014-02-25 11:30:49 +08:00
end
def self . sso_secret
2021-02-08 18:04:33 +08:00
SiteSetting . discourse_connect_secret
2014-02-25 11:30:49 +08:00
end
2021-02-18 18:35:10 +08:00
def self . generate_sso ( return_path = " / " , secure_session : )
sso = new ( secure_session : secure_session )
2014-02-25 11:30:49 +08:00
sso . nonce = SecureRandom . hex
2014-02-26 06:58:30 +08:00
sso . register_nonce ( return_path )
2014-11-26 14:25:54 +08:00
sso . return_sso_url = Discourse . base_url + " /session/sso_login "
2016-04-08 09:20:01 +08:00
sso
end
2021-02-18 18:35:10 +08:00
def self . generate_url ( return_path = " / " , secure_session : )
generate_sso ( return_path , secure_session : secure_session ) . to_url
end
def initialize ( secure_session : )
@secure_session = secure_session
2014-02-25 11:30:49 +08:00
end
2014-02-26 06:58:30 +08:00
def register_nonce ( return_path )
2014-02-25 11:30:49 +08:00
if nonce
2021-03-11 18:38:34 +08:00
if SiteSetting . discourse_connect_csrf_protection
2022-01-06 20:28:46 +08:00
@secure_session . set ( nonce_key , return_path , expires : DiscourseConnectBase . nonce_expiry_time )
2021-03-11 18:38:34 +08:00
else
2022-01-06 20:28:46 +08:00
Discourse . cache . write (
nonce_key ,
return_path ,
expires_in : DiscourseConnectBase . nonce_expiry_time ,
)
2021-03-11 18:38:34 +08:00
end
2014-02-25 11:30:49 +08:00
end
end
def nonce_valid?
2021-03-11 18:38:34 +08:00
if SiteSetting . discourse_connect_csrf_protection
nonce && @secure_session [ nonce_key ] . present?
else
nonce && Discourse . cache . read ( nonce_key ) . present?
end
2014-02-25 11:30:49 +08:00
end
2021-08-18 21:14:12 +08:00
def nonce_error
if Discourse . cache . read ( used_nonce_key ) . present?
" Nonce has already been used "
2021-11-10 01:39:05 +08:00
elsif SiteSetting . discourse_connect_csrf_protection
" Nonce is incorrect, was generated in a different browser session, or has expired "
2021-08-18 21:14:12 +08:00
else
2021-11-10 01:39:05 +08:00
" Nonce is incorrect, or has expired "
2021-08-18 21:14:12 +08:00
end
end
2014-02-26 06:58:30 +08:00
def return_path
2021-03-11 18:38:34 +08:00
if SiteSetting . discourse_connect_csrf_protection
@secure_session [ nonce_key ] || " / "
else
Discourse . cache . read ( nonce_key ) || " / "
end
2014-02-26 06:58:30 +08:00
end
2014-02-25 11:30:49 +08:00
def expire_nonce!
if nonce
2021-03-11 18:38:34 +08:00
if SiteSetting . discourse_connect_csrf_protection
@secure_session [ nonce_key ] = nil
else
Discourse . cache . delete nonce_key
end
2021-08-18 21:14:12 +08:00
2022-01-06 20:28:46 +08:00
Discourse . cache . write (
used_nonce_key ,
return_path ,
expires_in : DiscourseConnectBase . used_nonce_expiry_time ,
)
2014-02-25 11:30:49 +08:00
end
end
def nonce_key
" SSO_NONCE_ #{ nonce } "
end
2021-08-18 21:14:12 +08:00
def used_nonce_key
" USED_SSO_NONCE_ #{ nonce } "
end
2019-06-11 08:04:26 +08:00
BANNED_EXTERNAL_IDS = %w[ none nil blank null ]
2015-02-26 03:40:55 +08:00
def lookup_or_create_user ( ip_address = nil )
2019-06-11 08:04:26 +08:00
# we don't want to ban 0 from being an external id
external_id = self . external_id . to_s
raise BlankExternalId if external_id . blank?
raise BannedExternalId , external_id if BANNED_EXTERNAL_IDS . include? ( external_id . downcase )
2020-11-10 18:40:41 +08:00
# we protect here to ensure there is no situation where the same external id
# concurrently attempts to create or update sso records
#
# we can get duplicate HTTP requests quite easily (client rapid refresh) and this path does stuff such
# as updating groups for a users and so on that can happen even after the sso record and user is there
DistributedMutex . synchronize ( " sso_lookup_or_create_user_ #{ external_id } " ) do
lookup_or_create_user_unsafe ( ip_address )
end
end
private
def lookup_or_create_user_unsafe ( ip_address )
2014-05-06 21:41:59 +08:00
sso_record = SingleSignOnRecord . find_by ( external_id : external_id )
2014-04-15 13:53:48 +08:00
2016-09-02 10:04:22 +08:00
if sso_record && ( user = sso_record . user )
2014-02-25 11:30:49 +08:00
sso_record . last_payload = unsigned_payload
else
2015-02-24 04:58:45 +08:00
user = match_email_or_create_user ( ip_address )
2014-02-28 08:48:46 +08:00
sso_record = user . single_sign_on_record
end
2014-04-15 13:53:48 +08:00
2016-06-21 17:28:58 +08:00
# ensure it's not staged anymore
2020-03-17 23:48:24 +08:00
user . unstage!
2016-06-21 17:28:58 +08:00
2018-08-30 07:57:53 +08:00
change_external_attributes_and_override ( sso_record , user )
2014-02-25 11:30:49 +08:00
2015-05-20 00:16:02 +08:00
if sso_record && ( user = sso_record . user ) && ! user . active && ! require_activation
2014-02-26 07:28:03 +08:00
user . active = true
2014-06-02 15:32:39 +08:00
user . save!
2015-03-21 01:03:24 +08:00
user . enqueue_welcome_message ( " welcome_user " ) unless suppress_welcome_message
2017-06-15 01:20:18 +08:00
user . set_automatic_groups
2014-02-26 07:28:03 +08:00
end
2014-04-15 13:53:48 +08:00
2014-04-22 11:52:13 +08:00
custom_fields . each { | k , v | user . custom_fields [ k ] = v }
2015-02-24 04:58:45 +08:00
user . ip_address = ip_address
2016-05-17 15:31:34 +08:00
2014-11-27 09:39:00 +08:00
user . admin = admin unless admin . nil?
user . moderator = moderator unless moderator . nil?
2017-02-01 08:42:27 +08:00
user . title = title unless title . nil?
2014-02-28 08:48:46 +08:00
# optionally save the user and sso_record if they have changed
2016-08-29 09:28:19 +08:00
user . user_avatar . save! if user . user_avatar
2014-02-28 08:48:46 +08:00
user . save!
2016-05-17 15:31:34 +08:00
2020-04-08 14:33:50 +08:00
user . set_automatic_groups if @email_changed && user . active
2019-04-11 00:53:30 +08:00
# The user might require approval
user . create_reviewable
2021-02-08 18:04:33 +08:00
if bio && ( user . user_profile . bio_raw . blank? || SiteSetting . discourse_connect_overrides_bio )
2016-08-01 13:29:28 +08:00
user . user_profile . bio_raw = bio
user . user_profile . save!
end
2018-06-20 08:30:23 +08:00
if website
user . user_profile . website = website
user . user_profile . save!
end
2020-04-28 14:06:35 +08:00
if location
user . user_profile . location = location
user . user_profile . save!
end
2016-05-17 15:31:34 +08:00
unless admin . nil? && moderator . nil?
Group . refresh_automatic_groups! ( :admins , :moderators , :staff )
end
2014-02-28 08:48:46 +08:00
sso_record . save!
2014-02-26 07:28:03 +08:00
2016-11-11 13:57:31 +08:00
apply_group_rules ( sso_record . user ) if sso_record . user
2014-02-25 11:30:49 +08:00
sso_record && sso_record . user
end
2014-04-15 13:53:48 +08:00
2018-04-10 11:17:23 +08:00
def synchronize_groups ( user )
names = ( groups || " " ) . split ( " , " ) . map ( & :downcase )
2022-10-28 18:27:12 +08:00
current_groups = user . groups . where ( automatic : false )
desired_groups = Group . where ( " LOWER(NAME) in (?) AND NOT automatic " , names )
2018-04-10 11:17:23 +08:00
2022-10-28 18:27:12 +08:00
to_be_added = desired_groups
if current_groups . present?
to_be_added = to_be_added . where ( " groups.id NOT IN (?) " , current_groups . map ( & :id ) )
2018-04-10 13:30:18 +08:00
end
2018-04-10 11:17:23 +08:00
2022-10-28 18:27:12 +08:00
to_be_removed = current_groups
if desired_groups . present?
to_be_removed = to_be_removed . where ( " groups.id NOT IN (?) " , desired_groups . map ( & :id ) )
2022-10-25 16:25:26 +08:00
end
2018-04-10 11:17:23 +08:00
2022-10-25 16:25:26 +08:00
if to_be_added . present? || to_be_removed . present?
GroupUser . transaction do
add_user_to_groups ( user , to_be_added ) if to_be_added . present?
remove_user_from_groups ( user , to_be_removed ) if to_be_removed . present?
end
2018-04-10 11:17:23 +08:00
end
end
2016-11-11 13:57:31 +08:00
def apply_group_rules ( user )
2021-02-08 18:04:33 +08:00
if SiteSetting . discourse_connect_overrides_groups
2018-04-10 11:17:23 +08:00
synchronize_groups ( user )
return
end
2022-10-25 16:25:26 +08:00
to_be_added = nil
2016-11-11 13:57:31 +08:00
if add_groups
2017-08-02 23:30:23 +08:00
split = add_groups . split ( " , " ) . map ( & :downcase )
2016-11-11 13:57:31 +08:00
if split . length > 0
2022-10-25 16:25:26 +08:00
to_be_added = Group . where ( " LOWER(name) in (?) AND NOT automatic " , split )
if already_member = GroupUser . where ( user_id : user . id ) . pluck ( :group_id ) . presence
to_be_added = to_be_added . where ( " id NOT IN (?) " , already_member )
2016-11-11 13:57:31 +08:00
end
end
end
2022-10-25 16:25:26 +08:00
to_be_removed = nil
2016-11-11 13:57:31 +08:00
if remove_groups
2017-08-12 06:09:22 +08:00
split = remove_groups . split ( " , " ) . map ( & :downcase )
2016-11-11 13:57:31 +08:00
if split . length > 0
2022-10-25 16:25:26 +08:00
to_be_removed =
Group
. joins ( :group_users )
. where ( automatic : false , group_users : { user_id : user . id } )
. where ( " LOWER(name) IN (?) " , split )
end
end
if to_be_added || to_be_removed
GroupUser . transaction do
add_user_to_groups ( user , to_be_added ) if to_be_added
remove_user_from_groups ( user , to_be_removed ) if to_be_removed
2016-11-11 13:57:31 +08:00
end
end
end
2015-02-24 04:58:45 +08:00
def match_email_or_create_user ( ip_address )
2020-11-10 18:40:41 +08:00
# Use a mutex here to counter SSO requests that are sent at the same time with
2018-04-12 16:18:49 +08:00
# the same email payload
DistributedMutex . synchronize ( " discourse_single_sign_on_ #{ email } " ) do
2018-09-11 06:24:02 +08:00
user = User . find_by_email ( email ) if ! require_activation
2022-06-06 15:16:01 +08:00
2018-09-11 06:24:02 +08:00
if ! user
2018-04-12 16:18:49 +08:00
user_params = {
primary_email : UserEmail . new ( email : email , primary : true ) ,
2021-12-16 23:44:07 +08:00
name : resolve_name ,
username : resolve_username ,
2018-04-12 16:18:49 +08:00
ip_address : ip_address ,
}
2018-08-30 07:57:53 +08:00
if SiteSetting . allow_user_locale && locale && LocaleSiteSetting . valid_value? ( locale )
user_params [ :locale ] = locale
end
2022-06-06 15:16:01 +08:00
user = User . new ( user_params )
if SiteSetting . must_approve_users && EmailValidator . can_auto_approve_user? ( email )
ReviewableUser . set_approved_fields! ( user , Discourse . system_user )
end
2023-07-28 02:53:33 +08:00
begin
user . save!
rescue ActiveRecord :: RecordInvalid = > e
if SiteSetting . verbose_discourse_connect_logging
Rails . logger . error (
" Verbose SSO log: User creation failed. External id: #{ external_id } , New User (user_id: #{ user . id } ) Params: #{ user_params } User Params: #{ user . attributes } User Errors: #{ user . errors . full_messages } Email: #{ user . primary_email . attributes } Email Error: #{ user . primary_email . errors . full_messages } " ,
)
end
raise e
end
2018-04-12 16:18:49 +08:00
2021-02-08 18:04:33 +08:00
if SiteSetting . verbose_discourse_connect_logging
2018-04-12 16:18:49 +08:00
Rails . logger . warn (
" Verbose SSO log: New User (user_id: #{ user . id } ) Params: #{ user_params } User Params: #{ user . attributes } User Errors: #{ user . errors . full_messages } Email: #{ user . primary_email . attributes } Email Error: #{ user . primary_email . errors . full_messages } " ,
)
end
2017-11-07 19:38:36 +08:00
end
2014-02-25 11:30:49 +08:00
2018-04-12 16:18:49 +08:00
if user
if sso_record = user . single_sign_on_record
sso_record . last_payload = unsigned_payload
sso_record . external_id = external_id
else
if avatar_url . present?
Jobs . enqueue (
:download_avatar_from_url ,
url : avatar_url ,
user_id : user . id ,
2021-02-08 18:04:33 +08:00
override_gravatar : SiteSetting . discourse_connect_overrides_avatar ,
2018-04-12 16:18:49 +08:00
)
end
2018-05-07 16:03:26 +08:00
if profile_background_url . present?
Jobs . enqueue (
:download_profile_background_from_url ,
url : profile_background_url ,
user_id : user . id ,
is_card_background : false ,
)
end
if card_background_url . present?
Jobs . enqueue (
:download_profile_background_from_url ,
url : card_background_url ,
user_id : user . id ,
is_card_background : true ,
)
end
2018-04-12 16:18:49 +08:00
user . create_single_sign_on_record! (
last_payload : unsigned_payload ,
external_id : external_id ,
external_username : username ,
external_email : email ,
external_name : name ,
2018-05-07 16:03:26 +08:00
external_avatar_url : avatar_url ,
external_profile_background_url : profile_background_url ,
external_card_background_url : card_background_url ,
2017-11-07 18:38:38 +08:00
)
end
2014-02-28 08:48:46 +08:00
end
2014-04-15 13:53:48 +08:00
2018-04-12 16:18:49 +08:00
user
end
2014-02-28 08:48:46 +08:00
end
2014-04-15 13:53:48 +08:00
2014-02-28 08:48:46 +08:00
def change_external_attributes_and_override ( sso_record , user )
2020-04-08 14:33:50 +08:00
@email_changed = false
2023-01-24 11:40:24 +08:00
if SiteSetting . auth_overrides_email && email . present? && user . email != Email . downcase ( email )
2014-02-28 08:48:46 +08:00
user . email = email
2017-05-17 04:18:18 +08:00
user . active = false if require_activation
2020-04-08 14:33:50 +08:00
@email_changed = true
2014-02-28 08:48:46 +08:00
end
2014-04-15 13:53:48 +08:00
2021-02-08 18:04:33 +08:00
if SiteSetting . auth_overrides_username? && username . present?
2021-12-02 21:42:23 +08:00
UsernameChanger . override ( user , username )
2014-02-28 08:48:46 +08:00
end
2014-04-15 13:53:48 +08:00
2021-02-08 18:04:33 +08:00
if SiteSetting . auth_overrides_name && user . name != name && name . present?
2015-05-07 11:52:26 +08:00
user . name = name || User . suggest_name ( username . blank? ? email : username )
2014-02-28 08:48:46 +08:00
end
2014-04-15 13:53:48 +08:00
2018-08-30 07:57:53 +08:00
if locale_force_update && SiteSetting . allow_user_locale && locale &&
LocaleSiteSetting . valid_value? ( locale )
user . locale = locale
end
2016-09-16 07:44:45 +08:00
avatar_missing = user . uploaded_avatar_id . nil? || ! Upload . exists? ( user . uploaded_avatar_id )
2015-01-28 23:47:59 +08:00
2021-02-08 18:04:33 +08:00
if ( avatar_missing || avatar_force_update || SiteSetting . discourse_connect_overrides_avatar ) &&
avatar_url . present?
2016-10-25 01:55:30 +08:00
avatar_changed = sso_record . external_avatar_url != avatar_url
2016-09-16 07:44:45 +08:00
2016-10-25 01:55:30 +08:00
if avatar_force_update || avatar_changed || avatar_missing
2021-02-08 18:04:33 +08:00
Jobs . enqueue (
:download_avatar_from_url ,
url : avatar_url ,
user_id : user . id ,
override_gravatar : SiteSetting . discourse_connect_overrides_avatar ,
)
2016-10-25 01:55:30 +08:00
end
2014-08-19 15:49:14 +08:00
end
2020-11-25 07:53:44 +08:00
if profile_background_url . present?
profile_background_missing =
user . user_profile . profile_background_upload . blank? ||
Upload . get_from_url ( user . user_profile . profile_background_upload . url ) . blank?
2021-02-08 18:04:33 +08:00
if profile_background_missing || SiteSetting . discourse_connect_overrides_profile_background
2020-11-25 07:53:44 +08:00
profile_background_changed =
sso_record . external_profile_background_url != profile_background_url
if profile_background_changed || profile_background_missing
Jobs . enqueue (
:download_profile_background_from_url ,
url : profile_background_url ,
user_id : user . id ,
is_card_background : false ,
)
end
2018-05-07 16:03:26 +08:00
end
end
2020-11-25 07:53:44 +08:00
if card_background_url . present?
card_background_missing =
user . user_profile . card_background_upload . blank? ||
Upload . get_from_url ( user . user_profile . card_background_upload . url ) . blank?
2021-07-16 22:16:30 +08:00
if card_background_missing || SiteSetting . discourse_connect_overrides_card_background
2020-11-25 07:53:44 +08:00
card_background_changed = sso_record . external_card_background_url != card_background_url
if card_background_changed || card_background_missing
Jobs . enqueue (
:download_profile_background_from_url ,
url : card_background_url ,
user_id : user . id ,
is_card_background : true ,
)
end
2018-05-07 16:03:26 +08:00
end
end
2014-02-28 08:48:46 +08:00
# change external attributes for sso record
sso_record . external_username = username
sso_record . external_email = email
sso_record . external_name = name
2014-08-19 15:49:14 +08:00
sso_record . external_avatar_url = avatar_url
2018-05-07 16:03:26 +08:00
sso_record . external_profile_background_url = profile_background_url
sso_record . external_card_background_url = card_background_url
2014-02-28 08:48:46 +08:00
end
2021-12-16 23:44:07 +08:00
def resolve_username
2022-04-29 21:00:13 +08:00
suggester_input = [ username ]
suggester_input << name if SiteSetting . use_name_for_username_suggestions
2021-12-22 01:13:05 +08:00
suggester_input << email if SiteSetting . use_email_for_username_and_name_suggestions
UserNameSuggester . suggest ( * suggester_input )
2021-12-16 23:44:07 +08:00
end
def resolve_name
name_suggester_input = username . presence
if SiteSetting . use_email_for_username_and_name_suggestions
name_suggester_input = name_suggester_input || email
end
name . presence || User . suggest_name ( name_suggester_input )
end
2022-10-25 16:25:26 +08:00
def add_user_to_groups ( user , groups )
groups . each do | group |
2022-10-28 18:27:12 +08:00
GroupUser . create! ( user_id : user . id , group_id : group . id )
GroupActionLogger . new ( Discourse . system_user , group ) . log_add_user_to_group ( user )
2022-10-25 16:25:26 +08:00
end
end
def remove_user_from_groups ( user , groups )
GroupUser . where ( user_id : user . id , group_id : groups . map ( & :id ) ) . destroy_all
groups . each do | group |
GroupActionLogger . new ( Discourse . system_user , group ) . log_remove_user_from_group ( user )
end
end
2014-04-15 13:53:48 +08:00
end