mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
0902e56162
This is not recommended. But if you have other protections in place for CSRF mitigation, you may wish to disable Discourse's implementation. This site setting is not visible in the UI, and must be changed via the console.
366 lines
11 KiB
Ruby
366 lines
11 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class DiscourseSingleSignOn < SingleSignOn
|
|
|
|
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: SingleSignOn.nonce_expiry_time)
|
|
else
|
|
Discourse.cache.write(nonce_key, return_path, expires_in: SingleSignOn.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 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
|
|
end
|
|
end
|
|
|
|
def nonce_key
|
|
"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
|
|
|
|
if external_id.blank?
|
|
raise BlankExternalId
|
|
end
|
|
|
|
if BANNED_EXTERNAL_IDS.include?(external_id.downcase)
|
|
raise BannedExternalId, external_id
|
|
end
|
|
|
|
# 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 do |k, v|
|
|
user.custom_fields[k] = v
|
|
end
|
|
|
|
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!
|
|
|
|
if @email_changed && user.active
|
|
user.set_automatic_groups
|
|
end
|
|
|
|
# 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!
|
|
|
|
if sso_record.user
|
|
apply_group_rules(sso_record.user)
|
|
end
|
|
|
|
sso_record && sso_record.user
|
|
end
|
|
|
|
def synchronize_groups(user)
|
|
names = (groups || "").split(",").map(&:downcase)
|
|
ids = Group.where('LOWER(NAME) in (?) AND NOT automatic', names).pluck(:id)
|
|
|
|
group_users = GroupUser
|
|
.where('group_id IN (SELECT id FROM groups WHERE NOT automatic)')
|
|
.where(user_id: user.id)
|
|
|
|
delete_group_users = group_users
|
|
if ids.length > 0
|
|
delete_group_users = group_users.where('group_id NOT IN (?)', ids)
|
|
end
|
|
delete_group_users.destroy_all
|
|
|
|
ids -= group_users.where('group_id IN (?)', ids).pluck(:group_id)
|
|
|
|
ids.each do |group_id|
|
|
GroupUser.create(group_id: group_id, user_id: user.id)
|
|
end
|
|
end
|
|
|
|
def apply_group_rules(user)
|
|
if SiteSetting.discourse_connect_overrides_groups
|
|
synchronize_groups(user)
|
|
return
|
|
end
|
|
|
|
if add_groups
|
|
split = add_groups.split(",").map(&:downcase)
|
|
if split.length > 0
|
|
Group.where('LOWER(name) in (?) AND NOT automatic', split).pluck(:id).each do |id|
|
|
unless GroupUser.where(group_id: id, user_id: user.id).exists?
|
|
GroupUser.create(group_id: id, user_id: user.id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if remove_groups
|
|
split = remove_groups.split(",").map(&:downcase)
|
|
if split.length > 0
|
|
GroupUser
|
|
.where(user_id: user.id)
|
|
.where('group_id IN (SELECT id FROM groups WHERE LOWER(name) in (?))', split)
|
|
.destroy_all
|
|
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
|
|
try_name = name.presence
|
|
try_username = username.presence
|
|
|
|
user_params = {
|
|
primary_email: UserEmail.new(email: email, primary: true),
|
|
name: try_name || User.suggest_name(try_username || email),
|
|
username: UserNameSuggester.suggest(try_username || try_name || email),
|
|
ip_address: ip_address
|
|
}
|
|
|
|
if SiteSetting.allow_user_locale && locale && LocaleSiteSetting.valid_value?(locale)
|
|
user_params[:locale] = locale
|
|
end
|
|
|
|
user = User.create!(user_params)
|
|
|
|
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 && 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?
|
|
if user.username.downcase == username.downcase
|
|
user.username = username # there may be a change of case
|
|
elsif user.username != username
|
|
user.username = UserNameSuggester.suggest(username || name || email, user.username)
|
|
end
|
|
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 && 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_profile_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
|
|
end
|