2019-05-02 17:17:27 -05:00
# frozen_string_literal: true
2022-01-06 06:28:46 -06:00
class DiscourseConnect < DiscourseConnectBase
2023-01-09 06:20:10 -06:00
class BlankExternalId < StandardError
end
2023-12-15 09:46:04 -06:00
2023-01-09 06:20:10 -06:00
class BannedExternalId < StandardError
end
2019-06-10 19:04:26 -05:00
2014-02-24 21:30:49 -06:00
def self . sso_url
2021-02-08 04:04:33 -06:00
SiteSetting . discourse_connect_url
2014-02-24 21:30:49 -06:00
end
def self . sso_secret
2021-02-08 04:04:33 -06:00
SiteSetting . discourse_connect_secret
2014-02-24 21:30:49 -06:00
end
2021-02-18 04:35:10 -06:00
def self . generate_sso ( return_path = " / " , secure_session : )
sso = new ( secure_session : secure_session )
2014-02-24 21:30:49 -06:00
sso . nonce = SecureRandom . hex
2014-02-25 16:58:30 -06:00
sso . register_nonce ( return_path )
2014-11-26 00:25:54 -06:00
sso . return_sso_url = Discourse . base_url + " /session/sso_login "
2016-04-07 20:20:01 -05:00
sso
end
2021-02-18 04:35:10 -06: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-24 21:30:49 -06:00
end
2014-02-25 16:58:30 -06:00
def register_nonce ( return_path )
2014-02-24 21:30:49 -06:00
if nonce
2021-03-11 04:38:34 -06:00
if SiteSetting . discourse_connect_csrf_protection
2022-01-06 06:28:46 -06:00
@secure_session . set ( nonce_key , return_path , expires : DiscourseConnectBase . nonce_expiry_time )
2021-03-11 04:38:34 -06:00
else
2023-01-09 06:20:10 -06:00
Discourse . cache . write (
nonce_key ,
return_path ,
expires_in : DiscourseConnectBase . nonce_expiry_time ,
)
2021-03-11 04:38:34 -06:00
end
2014-02-24 21:30:49 -06:00
end
end
def nonce_valid?
2021-03-11 04:38:34 -06:00
if SiteSetting . discourse_connect_csrf_protection
nonce && @secure_session [ nonce_key ] . present?
else
nonce && Discourse . cache . read ( nonce_key ) . present?
end
2014-02-24 21:30:49 -06:00
end
2021-08-18 08:14:12 -05:00
def nonce_error
if Discourse . cache . read ( used_nonce_key ) . present?
" Nonce has already been used "
2021-11-09 11:39:05 -06:00
elsif SiteSetting . discourse_connect_csrf_protection
" Nonce is incorrect, was generated in a different browser session, or has expired "
2021-08-18 08:14:12 -05:00
else
2021-11-09 11:39:05 -06:00
" Nonce is incorrect, or has expired "
2021-08-18 08:14:12 -05:00
end
end
2014-02-25 16:58:30 -06:00
def return_path
2021-03-11 04:38:34 -06:00
if SiteSetting . discourse_connect_csrf_protection
@secure_session [ nonce_key ] || " / "
else
Discourse . cache . read ( nonce_key ) || " / "
end
2014-02-25 16:58:30 -06:00
end
2014-02-24 21:30:49 -06:00
def expire_nonce!
if nonce
2021-03-11 04:38:34 -06:00
if SiteSetting . discourse_connect_csrf_protection
@secure_session [ nonce_key ] = nil
else
Discourse . cache . delete nonce_key
end
2021-08-18 08:14:12 -05:00
2023-01-09 06:20:10 -06:00
Discourse . cache . write (
used_nonce_key ,
return_path ,
expires_in : DiscourseConnectBase . used_nonce_expiry_time ,
)
2014-02-24 21:30:49 -06:00
end
end
def nonce_key
" SSO_NONCE_ #{ nonce } "
end
2021-08-18 08:14:12 -05:00
def used_nonce_key
" USED_SSO_NONCE_ #{ nonce } "
end
2023-01-09 06:20:10 -06:00
BANNED_EXTERNAL_IDS = %w[ none nil blank null ]
2019-06-10 19:04:26 -05:00
2017-07-27 20:20:09 -05:00
def lookup_or_create_user ( ip_address = nil )
2019-06-10 19:04:26 -05:00
# we don't want to ban 0 from being an external id
external_id = self . external_id . to_s
2023-01-09 06:20:10 -06:00
raise BlankExternalId if external_id . blank?
2019-06-10 19:04:26 -05:00
2023-01-09 06:20:10 -06:00
raise BannedExternalId , external_id if BANNED_EXTERNAL_IDS . include? ( external_id . downcase )
2019-06-10 19:04:26 -05:00
2020-11-10 04:40:41 -06: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 08:41:59 -05:00
sso_record = SingleSignOnRecord . find_by ( external_id : external_id )
2014-04-15 00:53:48 -05:00
2016-09-01 21:04:22 -05:00
if sso_record && ( user = sso_record . user )
2014-02-24 21:30:49 -06:00
sso_record . last_payload = unsigned_payload
else
2015-02-23 14:58:45 -06:00
user = match_email_or_create_user ( ip_address )
2014-02-27 18:48:46 -06:00
sso_record = user . single_sign_on_record
end
2014-04-15 00:53:48 -05:00
2016-06-21 04:28:58 -05:00
# ensure it's not staged anymore
2020-03-17 10:48:24 -05:00
user . unstage!
2016-06-21 04:28:58 -05:00
2018-08-29 18:57:53 -05:00
change_external_attributes_and_override ( sso_record , user )
2014-02-24 21:30:49 -06:00
2015-05-19 11:16:02 -05:00
if sso_record && ( user = sso_record . user ) && ! user . active && ! require_activation
2014-02-25 17:28:03 -06:00
user . active = true
2014-06-02 02:32:39 -05:00
user . save!
2023-01-09 06:20:10 -06:00
user . enqueue_welcome_message ( " welcome_user " ) unless suppress_welcome_message
2017-06-14 12:20:18 -05:00
user . set_automatic_groups
2014-02-25 17:28:03 -06:00
end
2014-04-15 00:53:48 -05:00
2023-01-09 06:20:10 -06:00
custom_fields . each { | k , v | user . custom_fields [ k ] = v }
2014-04-21 22:52:13 -05:00
2015-02-23 14:58:45 -06:00
user . ip_address = ip_address
2016-05-17 02:31:34 -05:00
2014-11-26 19:39:00 -06:00
user . admin = admin unless admin . nil?
user . moderator = moderator unless moderator . nil?
2017-01-31 18:42:27 -06:00
user . title = title unless title . nil?
2014-02-27 18:48:46 -06:00
# optionally save the user and sso_record if they have changed
2016-08-28 20:28:19 -05:00
user . user_avatar . save! if user . user_avatar
2014-02-27 18:48:46 -06:00
user . save!
2016-05-17 02:31:34 -05:00
2023-01-09 06:20:10 -06:00
user . set_automatic_groups if @email_changed && user . active
2020-04-08 01:33:50 -05:00
2019-04-10 11:53:30 -05:00
# The user might require approval
user . create_reviewable
2021-02-08 04:04:33 -06:00
if bio && ( user . user_profile . bio_raw . blank? || SiteSetting . discourse_connect_overrides_bio )
2016-08-01 00:29:28 -05:00
user . user_profile . bio_raw = bio
user . user_profile . save!
end
2018-06-19 19:30:23 -05:00
if website
user . user_profile . website = website
user . user_profile . save!
end
2020-04-28 01:06:35 -05:00
if location
user . user_profile . location = location
user . user_profile . save!
end
2016-05-17 02:31:34 -05:00
unless admin . nil? && moderator . nil?
Group . refresh_automatic_groups! ( :admins , :moderators , :staff )
end
2014-02-27 18:48:46 -06:00
sso_record . save!
2014-02-25 17:28:03 -06:00
2023-01-09 06:20:10 -06:00
apply_group_rules ( sso_record . user ) if sso_record . user
2016-11-10 23:57:31 -06:00
2014-02-24 21:30:49 -06:00
sso_record && sso_record . user
end
2014-04-15 00:53:48 -05:00
2018-04-09 22:17:23 -05:00
def synchronize_groups ( user )
names = ( groups || " " ) . split ( " , " ) . map ( & :downcase )
2022-10-28 05:27:12 -05:00
current_groups = user . groups . where ( automatic : false )
2023-01-09 06:20:10 -06:00
desired_groups = Group . where ( " LOWER(NAME) in (?) AND NOT automatic " , names )
2018-04-09 22:17:23 -05:00
2022-10-28 05:27:12 -05: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 00:30:18 -05:00
end
2018-04-09 22:17:23 -05:00
2022-10-28 05:27:12 -05: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 03:25:26 -05:00
end
2018-04-09 22:17:23 -05:00
2022-10-25 03:25:26 -05: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-09 22:17:23 -05:00
end
end
2016-11-10 23:57:31 -06:00
def apply_group_rules ( user )
2021-02-08 04:04:33 -06:00
if SiteSetting . discourse_connect_overrides_groups
2018-04-09 22:17:23 -05:00
synchronize_groups ( user )
return
end
2022-10-25 03:25:26 -05:00
to_be_added = nil
2016-11-10 23:57:31 -06:00
if add_groups
2017-08-02 10:30:23 -05:00
split = add_groups . split ( " , " ) . map ( & :downcase )
2016-11-10 23:57:31 -06:00
if split . length > 0
2023-01-09 06:20:10 -06:00
to_be_added = Group . where ( " LOWER(name) in (?) AND NOT automatic " , split )
2022-10-25 03:25:26 -05:00
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-10 23:57:31 -06:00
end
end
end
2022-10-25 03:25:26 -05:00
to_be_removed = nil
2016-11-10 23:57:31 -06:00
if remove_groups
2017-08-11 17:09:22 -05:00
split = remove_groups . split ( " , " ) . map ( & :downcase )
2016-11-10 23:57:31 -06:00
if split . length > 0
2023-01-09 06:20:10 -06:00
to_be_removed =
Group
. joins ( :group_users )
. where ( automatic : false , group_users : { user_id : user . id } )
. where ( " LOWER(name) IN (?) " , split )
2022-10-25 03:25:26 -05:00
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-10 23:57:31 -06:00
end
end
end
2015-02-23 14:58:45 -06:00
def match_email_or_create_user ( ip_address )
2020-11-10 04:40:41 -06:00
# Use a mutex here to counter SSO requests that are sent at the same time with
2018-04-12 03:18:49 -05:00
# the same email payload
DistributedMutex . synchronize ( " discourse_single_sign_on_ #{ email } " ) do
2018-09-10 17:24:02 -05:00
user = User . find_by_email ( email ) if ! require_activation
2022-06-06 02:16:01 -05:00
2018-09-10 17:24:02 -05:00
if ! user
2018-04-12 03:18:49 -05:00
user_params = {
primary_email : UserEmail . new ( email : email , primary : true ) ,
2021-12-16 09:44:07 -06:00
name : resolve_name ,
username : resolve_username ,
2023-01-09 06:20:10 -06:00
ip_address : ip_address ,
2024-03-11 04:49:37 -05:00
registration_ip_address : ip_address ,
2018-04-12 03:18:49 -05:00
}
2018-08-29 18:57:53 -05:00
if SiteSetting . allow_user_locale && locale && LocaleSiteSetting . valid_value? ( locale )
user_params [ :locale ] = locale
end
2022-06-06 02:16:01 -05: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-27 13:53:33 -05: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 03:18:49 -05:00
2021-02-08 04:04:33 -06:00
if SiteSetting . verbose_discourse_connect_logging
2023-01-09 06:20:10 -06: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 } " ,
)
2018-04-12 03:18:49 -05:00
end
2017-11-07 05:38:36 -06:00
end
2014-02-24 21:30:49 -06:00
2018-04-12 03:18:49 -05: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?
2023-01-09 06:20:10 -06:00
Jobs . enqueue (
:download_avatar_from_url ,
2018-04-12 03:18:49 -05:00
url : avatar_url ,
user_id : user . id ,
2023-01-09 06:20:10 -06:00
override_gravatar : SiteSetting . discourse_connect_overrides_avatar ,
2018-04-12 03:18:49 -05:00
)
end
2018-05-07 03:03:26 -05:00
if profile_background_url . present?
2023-01-09 06:20:10 -06:00
Jobs . enqueue (
:download_profile_background_from_url ,
2018-05-07 03:03:26 -05:00
url : profile_background_url ,
user_id : user . id ,
2023-01-09 06:20:10 -06:00
is_card_background : false ,
2018-05-07 03:03:26 -05:00
)
end
if card_background_url . present?
2023-01-09 06:20:10 -06:00
Jobs . enqueue (
:download_profile_background_from_url ,
2018-05-07 03:03:26 -05:00
url : card_background_url ,
user_id : user . id ,
2023-01-09 06:20:10 -06:00
is_card_background : true ,
2018-05-07 03:03:26 -05:00
)
end
2018-04-12 03:18:49 -05: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 03:03:26 -05:00
external_avatar_url : avatar_url ,
external_profile_background_url : profile_background_url ,
2023-01-09 06:20:10 -06:00
external_card_background_url : card_background_url ,
2017-11-07 04:38:38 -06:00
)
end
2014-02-27 18:48:46 -06:00
end
2014-04-15 00:53:48 -05:00
2018-04-12 03:18:49 -05:00
user
end
2014-02-27 18:48:46 -06:00
end
2014-04-15 00:53:48 -05:00
2014-02-27 18:48:46 -06:00
def change_external_attributes_and_override ( sso_record , user )
2020-04-08 01:33:50 -05:00
@email_changed = false
2023-01-23 21:40:24 -06:00
if SiteSetting . auth_overrides_email && email . present? && user . email != Email . downcase ( email )
2014-02-27 18:48:46 -06:00
user . email = email
2017-05-16 15:18:18 -05:00
user . active = false if require_activation
2020-04-08 01:33:50 -05:00
@email_changed = true
2014-02-27 18:48:46 -06:00
end
2014-04-15 00:53:48 -05:00
2021-02-08 04:04:33 -06:00
if SiteSetting . auth_overrides_username? && username . present?
2021-12-02 07:42:23 -06:00
UsernameChanger . override ( user , username )
2014-02-27 18:48:46 -06:00
end
2014-04-15 00:53:48 -05:00
2021-02-08 04:04:33 -06:00
if SiteSetting . auth_overrides_name && user . name != name && name . present?
2015-05-06 22:52:26 -05:00
user . name = name || User . suggest_name ( username . blank? ? email : username )
2014-02-27 18:48:46 -06:00
end
2014-04-15 00:53:48 -05:00
2023-01-09 06:20:10 -06:00
if locale_force_update && SiteSetting . allow_user_locale && locale &&
LocaleSiteSetting . valid_value? ( locale )
2018-08-29 18:57:53 -05:00
user . locale = locale
end
2016-09-15 18:44:45 -05:00
avatar_missing = user . uploaded_avatar_id . nil? || ! Upload . exists? ( user . uploaded_avatar_id )
2015-01-28 09:47:59 -06:00
2023-01-09 06:20:10 -06:00
if ( avatar_missing || avatar_force_update || SiteSetting . discourse_connect_overrides_avatar ) &&
avatar_url . present?
2016-10-24 12:55:30 -05:00
avatar_changed = sso_record . external_avatar_url != avatar_url
2016-09-15 18:44:45 -05:00
2016-10-24 12:55:30 -05:00
if avatar_force_update || avatar_changed || avatar_missing
2023-01-09 06:20:10 -06:00
Jobs . enqueue (
:download_avatar_from_url ,
url : avatar_url ,
user_id : user . id ,
override_gravatar : SiteSetting . discourse_connect_overrides_avatar ,
)
2016-10-24 12:55:30 -05:00
end
2014-08-19 02:49:14 -05:00
end
2020-11-24 17:53:44 -06:00
if profile_background_url . present?
2023-01-09 06:20:10 -06:00
profile_background_missing =
user . user_profile . profile_background_upload . blank? ||
Upload . get_from_url ( user . user_profile . profile_background_upload . url ) . blank?
2020-11-24 17:53:44 -06:00
2021-02-08 04:04:33 -06:00
if profile_background_missing || SiteSetting . discourse_connect_overrides_profile_background
2023-01-09 06:20:10 -06:00
profile_background_changed =
sso_record . external_profile_background_url != profile_background_url
2020-11-24 17:53:44 -06:00
if profile_background_changed || profile_background_missing
2023-01-09 06:20:10 -06:00
Jobs . enqueue (
:download_profile_background_from_url ,
url : profile_background_url ,
user_id : user . id ,
is_card_background : false ,
2020-11-24 17:53:44 -06:00
)
end
2018-05-07 03:03:26 -05:00
end
end
2020-11-24 17:53:44 -06:00
if card_background_url . present?
2023-01-09 06:20:10 -06:00
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 09:16:30 -05:00
if card_background_missing || SiteSetting . discourse_connect_overrides_card_background
2020-11-24 17:53:44 -06:00
card_background_changed = sso_record . external_card_background_url != card_background_url
if card_background_changed || card_background_missing
2023-01-09 06:20:10 -06:00
Jobs . enqueue (
:download_profile_background_from_url ,
url : card_background_url ,
user_id : user . id ,
is_card_background : true ,
2020-11-24 17:53:44 -06:00
)
end
2018-05-07 03:03:26 -05:00
end
end
2014-02-27 18:48:46 -06: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 02:49:14 -05:00
sso_record . external_avatar_url = avatar_url
2018-05-07 03:03:26 -05:00
sso_record . external_profile_background_url = profile_background_url
sso_record . external_card_background_url = card_background_url
2014-02-27 18:48:46 -06:00
end
2021-12-16 09:44:07 -06:00
def resolve_username
2022-04-29 08:00:13 -05:00
suggester_input = [ username ]
suggester_input << name if SiteSetting . use_name_for_username_suggestions
2021-12-21 11:13:05 -06:00
suggester_input << email if SiteSetting . use_email_for_username_and_name_suggestions
UserNameSuggester . suggest ( * suggester_input )
2021-12-16 09:44:07 -06: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 03:25:26 -05:00
def add_user_to_groups ( user , groups )
groups . each do | group |
2022-10-28 05:27:12 -05: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 03:25:26 -05: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 00:53:48 -05:00
end