mirror of
https://github.com/discourse/discourse.git
synced 2024-12-13 22:45:47 +08:00
57eecbef4b
If, for whatever reasons, the user's locale is "blank" and an admin is accepting their group membership request, there will be an error because we're generating posts with the locale of recipient. In order to fix this, we now use the `user.effective_locale` which takes care of multiple things, including returning the default locale when the user's locale is blank. Internal ref - t/132347
452 lines
14 KiB
Ruby
452 lines
14 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class DiscourseConnect < DiscourseConnectBase
|
|
class BlankExternalId < StandardError
|
|
end
|
|
|
|
class BannedExternalId < StandardError
|
|
end
|
|
|
|
def self.sso_url
|
|
SiteSetting.discourse_connect_url
|
|
end
|
|
|
|
def self.sso_secret
|
|
SiteSetting.discourse_connect_secret
|
|
end
|
|
|
|
def self.generate_sso(return_path = "/", secure_session:)
|
|
sso = new(secure_session: secure_session)
|
|
sso.nonce = SecureRandom.hex
|
|
sso.register_nonce(return_path)
|
|
sso.return_sso_url = Discourse.base_url + "/session/sso_login"
|
|
sso
|
|
end
|
|
|
|
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
|
|
end
|
|
|
|
def register_nonce(return_path)
|
|
if nonce
|
|
if SiteSetting.discourse_connect_csrf_protection
|
|
@secure_session.set(nonce_key, return_path, expires: DiscourseConnectBase.nonce_expiry_time)
|
|
else
|
|
Discourse.cache.write(
|
|
nonce_key,
|
|
return_path,
|
|
expires_in: DiscourseConnectBase.nonce_expiry_time,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def nonce_valid?
|
|
if SiteSetting.discourse_connect_csrf_protection
|
|
nonce && @secure_session[nonce_key].present?
|
|
else
|
|
nonce && Discourse.cache.read(nonce_key).present?
|
|
end
|
|
end
|
|
|
|
def nonce_error
|
|
if Discourse.cache.read(used_nonce_key).present?
|
|
"Nonce has already been used"
|
|
elsif SiteSetting.discourse_connect_csrf_protection
|
|
"Nonce is incorrect, was generated in a different browser session, or has expired"
|
|
else
|
|
"Nonce is incorrect, or has expired"
|
|
end
|
|
end
|
|
|
|
def return_path
|
|
if SiteSetting.discourse_connect_csrf_protection
|
|
@secure_session[nonce_key] || "/"
|
|
else
|
|
Discourse.cache.read(nonce_key) || "/"
|
|
end
|
|
end
|
|
|
|
def expire_nonce!
|
|
if nonce
|
|
if SiteSetting.discourse_connect_csrf_protection
|
|
@secure_session[nonce_key] = nil
|
|
else
|
|
Discourse.cache.delete nonce_key
|
|
end
|
|
|
|
Discourse.cache.write(
|
|
used_nonce_key,
|
|
return_path,
|
|
expires_in: DiscourseConnectBase.used_nonce_expiry_time,
|
|
)
|
|
end
|
|
end
|
|
|
|
def nonce_key
|
|
"SSO_NONCE_#{nonce}"
|
|
end
|
|
|
|
def used_nonce_key
|
|
"USED_SSO_NONCE_#{nonce}"
|
|
end
|
|
|
|
BANNED_EXTERNAL_IDS = %w[none nil blank null]
|
|
|
|
def lookup_or_create_user(ip_address = nil)
|
|
# 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)
|
|
|
|
# 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)
|
|
sso_record = SingleSignOnRecord.find_by(external_id: external_id)
|
|
|
|
if sso_record && (user = sso_record.user)
|
|
sso_record.last_payload = unsigned_payload
|
|
else
|
|
user = match_email_or_create_user(ip_address)
|
|
sso_record = user.single_sign_on_record
|
|
end
|
|
|
|
# ensure it's not staged anymore
|
|
user.unstage!
|
|
|
|
change_external_attributes_and_override(sso_record, user)
|
|
|
|
if sso_record && (user = sso_record.user) && !user.active && !require_activation
|
|
user.active = true
|
|
user.save!
|
|
user.enqueue_welcome_message("welcome_user") unless suppress_welcome_message
|
|
user.set_automatic_groups
|
|
end
|
|
|
|
custom_fields.each { |k, v| user.custom_fields[k] = v }
|
|
|
|
user.ip_address = ip_address
|
|
|
|
user.admin = admin unless admin.nil?
|
|
user.moderator = moderator unless moderator.nil?
|
|
|
|
user.title = title unless title.nil?
|
|
|
|
# optionally save the user and sso_record if they have changed
|
|
user.user_avatar.save! if user.user_avatar
|
|
user.save!
|
|
|
|
user.set_automatic_groups if @email_changed && user.active
|
|
|
|
# The user might require approval
|
|
user.create_reviewable
|
|
|
|
if bio && (user.user_profile.bio_raw.blank? || SiteSetting.discourse_connect_overrides_bio)
|
|
user.user_profile.bio_raw = bio
|
|
user.user_profile.save!
|
|
end
|
|
|
|
if website
|
|
user.user_profile.website = website
|
|
user.user_profile.save!
|
|
end
|
|
|
|
if location
|
|
user.user_profile.location = location
|
|
user.user_profile.save!
|
|
end
|
|
|
|
unless admin.nil? && moderator.nil?
|
|
Group.refresh_automatic_groups!(:admins, :moderators, :staff)
|
|
end
|
|
|
|
sso_record.save!
|
|
|
|
apply_group_rules(sso_record.user) if sso_record.user
|
|
|
|
sso_record && sso_record.user
|
|
end
|
|
|
|
def synchronize_groups(user)
|
|
names = (groups || "").split(",").map(&:downcase)
|
|
|
|
current_groups = user.groups.where(automatic: false)
|
|
desired_groups = Group.where("LOWER(NAME) in (?) AND NOT automatic", names)
|
|
|
|
to_be_added = desired_groups
|
|
if current_groups.present?
|
|
to_be_added = to_be_added.where("groups.id NOT IN (?)", current_groups.map(&:id))
|
|
end
|
|
|
|
to_be_removed = current_groups
|
|
if desired_groups.present?
|
|
to_be_removed = to_be_removed.where("groups.id NOT IN (?)", desired_groups.map(&:id))
|
|
end
|
|
|
|
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
|
|
end
|
|
end
|
|
|
|
def apply_group_rules(user)
|
|
if SiteSetting.discourse_connect_overrides_groups
|
|
synchronize_groups(user)
|
|
return
|
|
end
|
|
|
|
to_be_added = nil
|
|
if add_groups
|
|
split = add_groups.split(",").map(&:downcase)
|
|
if split.length > 0
|
|
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)
|
|
end
|
|
end
|
|
end
|
|
|
|
to_be_removed = nil
|
|
if remove_groups
|
|
split = remove_groups.split(",").map(&:downcase)
|
|
if split.length > 0
|
|
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
|
|
end
|
|
end
|
|
end
|
|
|
|
def match_email_or_create_user(ip_address)
|
|
# Use a mutex here to counter SSO requests that are sent at the same time with
|
|
# the same email payload
|
|
DistributedMutex.synchronize("discourse_single_sign_on_#{email}") do
|
|
user = User.find_by_email(email) if !require_activation
|
|
|
|
if !user
|
|
user_params = {
|
|
primary_email: UserEmail.new(email: email, primary: true),
|
|
name: resolve_name,
|
|
username: resolve_username,
|
|
ip_address: ip_address,
|
|
registration_ip_address: ip_address,
|
|
}
|
|
|
|
if SiteSetting.allow_user_locale && locale && LocaleSiteSetting.valid_value?(locale)
|
|
user_params[:locale] = locale
|
|
end
|
|
|
|
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
|
|
|
|
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
|
|
|
|
if SiteSetting.verbose_discourse_connect_logging
|
|
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
|
|
end
|
|
|
|
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,
|
|
override_gravatar: SiteSetting.discourse_connect_overrides_avatar,
|
|
)
|
|
end
|
|
|
|
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
|
|
|
|
user.create_single_sign_on_record!(
|
|
last_payload: unsigned_payload,
|
|
external_id: external_id,
|
|
external_username: username,
|
|
external_email: email,
|
|
external_name: name,
|
|
external_avatar_url: avatar_url,
|
|
external_profile_background_url: profile_background_url,
|
|
external_card_background_url: card_background_url,
|
|
)
|
|
end
|
|
end
|
|
|
|
user
|
|
end
|
|
end
|
|
|
|
def change_external_attributes_and_override(sso_record, user)
|
|
@email_changed = false
|
|
|
|
if SiteSetting.auth_overrides_email && email.present? && user.email != Email.downcase(email)
|
|
user.email = email
|
|
user.active = false if require_activation
|
|
@email_changed = true
|
|
end
|
|
|
|
if SiteSetting.auth_overrides_username? && username.present?
|
|
UsernameChanger.override(user, username)
|
|
end
|
|
|
|
if SiteSetting.auth_overrides_name && user.name != name && name.present?
|
|
user.name = name || User.suggest_name(username.blank? ? email : username)
|
|
end
|
|
|
|
if locale_force_update && SiteSetting.allow_user_locale && locale.present? &&
|
|
LocaleSiteSetting.valid_value?(locale)
|
|
user.locale = locale
|
|
end
|
|
|
|
avatar_missing = user.uploaded_avatar_id.nil? || !Upload.exists?(user.uploaded_avatar_id)
|
|
|
|
if (avatar_missing || avatar_force_update || SiteSetting.discourse_connect_overrides_avatar) &&
|
|
avatar_url.present?
|
|
avatar_changed = sso_record.external_avatar_url != avatar_url
|
|
|
|
if avatar_force_update || avatar_changed || avatar_missing
|
|
Jobs.enqueue(
|
|
:download_avatar_from_url,
|
|
url: avatar_url,
|
|
user_id: user.id,
|
|
override_gravatar: SiteSetting.discourse_connect_overrides_avatar,
|
|
)
|
|
end
|
|
end
|
|
|
|
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?
|
|
|
|
if profile_background_missing || SiteSetting.discourse_connect_overrides_profile_background
|
|
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
|
|
end
|
|
end
|
|
|
|
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?
|
|
if card_background_missing || SiteSetting.discourse_connect_overrides_card_background
|
|
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
|
|
end
|
|
end
|
|
|
|
# change external attributes for sso record
|
|
sso_record.external_username = username
|
|
sso_record.external_email = email
|
|
sso_record.external_name = name
|
|
sso_record.external_avatar_url = avatar_url
|
|
sso_record.external_profile_background_url = profile_background_url
|
|
sso_record.external_card_background_url = card_background_url
|
|
end
|
|
|
|
def resolve_username
|
|
suggester_input = [username]
|
|
suggester_input << name if SiteSetting.use_name_for_username_suggestions
|
|
suggester_input << email if SiteSetting.use_email_for_username_and_name_suggestions
|
|
UserNameSuggester.suggest(*suggester_input)
|
|
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
|
|
|
|
def add_user_to_groups(user, groups)
|
|
groups.each do |group|
|
|
GroupUser.create!(user_id: user.id, group_id: group.id)
|
|
GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user)
|
|
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
|
|
end
|