mirror of
https://github.com/discourse/discourse.git
synced 2025-03-21 07:35:47 +08:00
FIX: auth incorrectly handles duplicate usernames (#15197)
This commit is contained in:
parent
bf18145e70
commit
f3508065a3
@ -19,7 +19,7 @@ class UsernameChanger
|
|||||||
UsernameChanger.change(user, new_username, user)
|
UsernameChanger.change(user, new_username, user)
|
||||||
true
|
true
|
||||||
elsif user.username != UserNameSuggester.fix_username(new_username)
|
elsif user.username != UserNameSuggester.fix_username(new_username)
|
||||||
suggested_username = UserNameSuggester.suggest(new_username)
|
suggested_username = UserNameSuggester.suggest(new_username, user.username)
|
||||||
UsernameChanger.change(user, suggested_username, user)
|
UsernameChanger.change(user, suggested_username, user)
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
|
@ -4,9 +4,10 @@ module UserNameSuggester
|
|||||||
GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team']
|
GENERIC_NAMES = ['i', 'me', 'info', 'support', 'admin', 'webmaster', 'hello', 'mail', 'office', 'contact', 'team']
|
||||||
LAST_RESORT_USERNAME = "user"
|
LAST_RESORT_USERNAME = "user"
|
||||||
|
|
||||||
def self.suggest(name_or_email)
|
def self.suggest(name_or_email, current_username = nil)
|
||||||
name = parse_name_from_email(name_or_email)
|
name = parse_name_from_email(name_or_email)
|
||||||
find_available_username_based_on(name)
|
name = fix_username(name)
|
||||||
|
find_available_username_based_on(name, current_username)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.parse_name_from_email(name_or_email)
|
def self.parse_name_from_email(name_or_email)
|
||||||
@ -20,13 +21,21 @@ module UserNameSuggester
|
|||||||
name
|
name
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_available_username_based_on(name)
|
def self.find_available_username_based_on(name, current_username = nil)
|
||||||
name = fix_username(name)
|
|
||||||
offset = nil
|
offset = nil
|
||||||
i = 1
|
i = 1
|
||||||
|
|
||||||
attempt = name
|
attempt = name
|
||||||
until User.username_available?(attempt) || i > 100
|
normalized_attempt = User.normalize_username(attempt)
|
||||||
|
|
||||||
|
original_allowed_username = current_username
|
||||||
|
current_username = User.normalize_username(current_username) if current_username
|
||||||
|
|
||||||
|
until (
|
||||||
|
normalized_attempt == current_username ||
|
||||||
|
User.username_available?(attempt) ||
|
||||||
|
i > 100
|
||||||
|
)
|
||||||
|
|
||||||
if offset.nil?
|
if offset.nil?
|
||||||
normalized = User.normalize_username(name)
|
normalized = User.normalize_username(name)
|
||||||
@ -42,7 +51,8 @@ module UserNameSuggester
|
|||||||
|
|
||||||
params = {
|
params = {
|
||||||
count: count + 10,
|
count: count + 10,
|
||||||
name: normalized
|
name: normalized,
|
||||||
|
allowed_normalized: current_username || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
# increasing the search space a bit to allow for some extra noise
|
# increasing the search space a bit to allow for some extra noise
|
||||||
@ -50,7 +60,11 @@ module UserNameSuggester
|
|||||||
WITH numbers AS (SELECT generate_series(1, :count) AS n)
|
WITH numbers AS (SELECT generate_series(1, :count) AS n)
|
||||||
|
|
||||||
SELECT n FROM numbers
|
SELECT n FROM numbers
|
||||||
LEFT JOIN users ON (username_lower = :name || n::varchar)
|
LEFT JOIN users ON (
|
||||||
|
username_lower = :name || n::varchar
|
||||||
|
) AND (
|
||||||
|
username_lower <> :allowed_normalized
|
||||||
|
)
|
||||||
WHERE users.id IS NULL
|
WHERE users.id IS NULL
|
||||||
ORDER by n ASC
|
ORDER by n ASC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@ -68,15 +82,21 @@ module UserNameSuggester
|
|||||||
|
|
||||||
max_length = User.username_length.end - suffix.length
|
max_length = User.username_length.end - suffix.length
|
||||||
attempt = "#{truncate(name, max_length)}#{suffix}"
|
attempt = "#{truncate(name, max_length)}#{suffix}"
|
||||||
|
normalized_attempt = User.normalize_username(attempt)
|
||||||
i += 1
|
i += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
until User.username_available?(attempt) || i > 200
|
until normalized_attempt == current_username || User.username_available?(attempt) || i > 200
|
||||||
attempt = SecureRandom.hex[1..SiteSetting.max_username_length]
|
attempt = SecureRandom.hex[1..SiteSetting.max_username_length]
|
||||||
|
normalized_attempt = User.normalize_username(attempt)
|
||||||
i += 1
|
i += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
attempt
|
if current_username == normalized_attempt
|
||||||
|
original_allowed_username
|
||||||
|
else
|
||||||
|
attempt
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.fix_username(name)
|
def self.fix_username(name)
|
||||||
|
@ -118,6 +118,21 @@ describe UserNameSuggester do
|
|||||||
expect(UserNameSuggester.suggest('uuuuuuu_u')).to eq('uuuuuuu1')
|
expect(UserNameSuggester.suggest('uuuuuuu_u')).to eq('uuuuuuu1')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'preserves current username' do
|
||||||
|
# if several users have username "bill" on the external site,
|
||||||
|
# they will have usernames bill, bill1, bill2 etc in Discourse:
|
||||||
|
Fabricate(:user, username: "bill")
|
||||||
|
Fabricate(:user, username: "bill1")
|
||||||
|
Fabricate(:user, username: "bill2")
|
||||||
|
Fabricate(:user, username: "bill3")
|
||||||
|
Fabricate(:user, username: "bill4")
|
||||||
|
|
||||||
|
# the number should be preserved, bill3 should remain bill3
|
||||||
|
suggestion = UserNameSuggester.suggest("bill", "bill3")
|
||||||
|
|
||||||
|
expect(suggestion).to eq "bill3"
|
||||||
|
end
|
||||||
|
|
||||||
context "with Unicode usernames disabled" do
|
context "with Unicode usernames disabled" do
|
||||||
before { SiteSetting.unicode_usernames = false }
|
before { SiteSetting.unicode_usernames = false }
|
||||||
|
|
||||||
|
@ -366,6 +366,30 @@ describe DiscourseSingleSignOn do
|
|||||||
expect(user.username).to eq "testuser"
|
expect(user.username).to eq "testuser"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'should preserve username when several users login with the same username' do
|
||||||
|
SiteSetting.auth_overrides_username = true
|
||||||
|
|
||||||
|
# if several users have username "bill" on the external site,
|
||||||
|
# they will have usernames bill, bill1, bill2 etc in Discourse:
|
||||||
|
Fabricate(:user, username: "bill")
|
||||||
|
Fabricate(:user, username: "bill1")
|
||||||
|
Fabricate(:user, username: "bill2")
|
||||||
|
Fabricate(:user, username: "bill4")
|
||||||
|
|
||||||
|
# the number should be preserved during subsequent logins
|
||||||
|
# bill3 should remain bill3
|
||||||
|
sso = new_discourse_sso
|
||||||
|
sso.username = "bill3"
|
||||||
|
sso.email = "test@test.com"
|
||||||
|
sso.external_id = "100"
|
||||||
|
sso.lookup_or_create_user(ip_address)
|
||||||
|
|
||||||
|
sso.username = "bill"
|
||||||
|
user = sso.lookup_or_create_user(ip_address)
|
||||||
|
|
||||||
|
expect(user.username).to eq "bill3"
|
||||||
|
end
|
||||||
|
|
||||||
it "doesn't use email as a source for username suggestions by default" do
|
it "doesn't use email as a source for username suggestions by default" do
|
||||||
sso = new_discourse_sso
|
sso = new_discourse_sso
|
||||||
sso.external_id = "100"
|
sso.external_id = "100"
|
||||||
|
@ -467,6 +467,30 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||||||
expect(user.name).to eq('Some name')
|
expect(user.name).to eq('Some name')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should preserve username when several users login with the same username" do
|
||||||
|
SiteSetting.auth_overrides_username = true
|
||||||
|
|
||||||
|
# if several users have username "bill" on the external site,
|
||||||
|
# they will have usernames bill, bill1, bill2 etc in Discourse:
|
||||||
|
Fabricate(:user, username: "bill")
|
||||||
|
Fabricate(:user, username: "bill1")
|
||||||
|
Fabricate(:user, username: "bill2")
|
||||||
|
Fabricate(:user, username: "bill4")
|
||||||
|
|
||||||
|
# the number should be preserved during subsequent logins
|
||||||
|
# bill3 should remain bill3
|
||||||
|
user.update!(username: 'bill3')
|
||||||
|
|
||||||
|
uid = "12345"
|
||||||
|
UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user.id, provider_uid: uid)
|
||||||
|
mock_auth(user.email, "bill", uid)
|
||||||
|
|
||||||
|
get "/auth/google_oauth2/callback.json"
|
||||||
|
|
||||||
|
user.reload
|
||||||
|
expect(user.username).to eq('bill3')
|
||||||
|
end
|
||||||
|
|
||||||
it "will not update email if not verified" do
|
it "will not update email if not verified" do
|
||||||
SiteSetting.email_editable = false
|
SiteSetting.email_editable = false
|
||||||
SiteSetting.auth_overrides_email = true
|
SiteSetting.auth_overrides_email = true
|
||||||
@ -932,10 +956,10 @@ RSpec.describe Users::OmniauthCallbacksController do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mock_auth(email, nickname)
|
def mock_auth(email, nickname, uid = '12345')
|
||||||
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new(
|
||||||
provider: 'google_oauth2',
|
provider: 'google_oauth2',
|
||||||
uid: '123545',
|
uid: uid,
|
||||||
info: OmniAuth::AuthHash::InfoHash.new(
|
info: OmniAuth::AuthHash::InfoHash.new(
|
||||||
email: email,
|
email: email,
|
||||||
nickname: nickname
|
nickname: nickname
|
||||||
|
Loading…
x
Reference in New Issue
Block a user