DEV: simplify username suggester (#14531)

This PR doesn't change any behavior, but just removes code that wasn't in use. This is a pretty dangerous place to change, since it gets called during user's registration. At the same time the refactoring is very straightforward, it's clear that this code wasn't doing any work (it still needs to be double-checked during review though). Also, the test coverage of UserNameSuggester is good.
This commit is contained in:
Andrei Prigorshnev 2021-10-27 14:41:24 +04:00 committed by GitHub
parent 69f0f48dc0
commit 19d95c64af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 52 additions and 62 deletions

View File

@ -323,8 +323,8 @@ class DiscourseSingleSignOn < SingleSignOn
if SiteSetting.auth_overrides_username? && username.present? if SiteSetting.auth_overrides_username? && username.present?
if user.username.downcase == username.downcase if user.username.downcase == username.downcase
user.username = username # there may be a change of case user.username = username # there may be a change of case
elsif user.username != username elsif user.username != UserNameSuggester.fix_username(username)
user.username = UserNameSuggester.suggest(username || name || email, user.username) user.username = UserNameSuggester.suggest(username)
end end
end end

View File

@ -77,8 +77,8 @@ class Auth::Result
def apply_user_attributes! def apply_user_attributes!
change_made = false change_made = false
if SiteSetting.auth_overrides_username? && username.present? && username != user.username if SiteSetting.auth_overrides_username? && username.present? && UserNameSuggester.fix_username(username) != user.username
user.username = UserNameSuggester.suggest(username_suggester_attributes, user.username) user.username = UserNameSuggester.suggest(username)
change_made = true change_made = true
end end

View File

@ -4,9 +4,9 @@ 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, allowed_username = nil) def self.suggest(name_or_email)
name = parse_name_from_email(name_or_email) name = parse_name_from_email(name_or_email)
find_available_username_based_on(name, allowed_username) find_available_username_based_on(name)
end end
def self.parse_name_from_email(name_or_email) def self.parse_name_from_email(name_or_email)
@ -20,22 +20,13 @@ module UserNameSuggester
name name
end end
def self.find_available_username_based_on(name, allowed_username = nil) def self.find_available_username_based_on(name)
name = fix_username(name) name = fix_username(name)
offset = nil offset = nil
i = 1 i = 1
attempt = name attempt = name
normalized_attempt = User.normalize_username(attempt) until User.username_available?(attempt) || i > 100
original_allowed_username = allowed_username
allowed_username = User.normalize_username(allowed_username) if allowed_username
until (
normalized_attempt == allowed_username ||
User.username_available?(attempt) ||
i > 100
)
if offset.nil? if offset.nil?
normalized = User.normalize_username(name) normalized = User.normalize_username(name)
@ -51,8 +42,7 @@ module UserNameSuggester
params = { params = {
count: count + 10, count: count + 10,
name: normalized, name: normalized
allowed_normalized: allowed_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
@ -60,11 +50,7 @@ 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 ( LEFT JOIN users ON (username_lower = :name || n::varchar)
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
@ -82,24 +68,17 @@ 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 normalized_attempt == allowed_username || User.username_available?(attempt) || i > 200 until 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
if allowed_username == normalized_attempt
original_allowed_username
else
attempt attempt
end end
end
def self.fix_username(name) def self.fix_username(name)
fixed_username = sanitize_username(name) fixed_username = sanitize_username(name)
if fixed_username.empty? if fixed_username.empty?

View File

@ -160,14 +160,6 @@ describe UserNameSuggester do
expect(UserNameSuggester.suggest('য়া')).to eq('য়া11') expect(UserNameSuggester.suggest('য়া')).to eq('য়া11')
end end
it "does not skip ove allowed names" do
Fabricate(:user, username: 'sam')
Fabricate(:user, username: 'saM1')
Fabricate(:user, username: 'sam2')
expect(UserNameSuggester.suggest('SaM', 'Sam1')).to eq('Sam1')
end
it "normalizes usernames" do it "normalizes usernames" do
actual = 'Löwe' # NFD, "Lo\u0308we" actual = 'Löwe' # NFD, "Lo\u0308we"
expected = 'Löwe' # NFC, "L\u00F6we" expected = 'Löwe' # NFC, "L\u00F6we"

View File

@ -265,27 +265,6 @@ describe DiscourseSingleSignOn do
expect(add_group4.usernames).to eq(user.username) expect(add_group4.usernames).to eq(user.username)
end end
it 'can override username properly when only the case changes' do
SiteSetting.auth_overrides_username = true
sso = new_discourse_sso
sso.username = "testuser"
sso.name = "test user"
sso.email = "test@test.com"
sso.external_id = "100"
sso.bio = "This **is** the bio"
sso.suppress_welcome_message = true
# create the original user
user = sso.lookup_or_create_user(ip_address)
expect(user.username).to eq "testuser"
# change the username case
sso.username = "TestUser"
user = sso.lookup_or_create_user(ip_address)
expect(user.username).to eq "TestUser"
end
it 'behaves properly when auth_overrides_username is set but username is missing or blank' do it 'behaves properly when auth_overrides_username is set but username is missing or blank' do
SiteSetting.auth_overrides_username = true SiteSetting.auth_overrides_username = true
@ -347,6 +326,46 @@ describe DiscourseSingleSignOn do
expect(admin.name).to eq "Louis C.K." expect(admin.name).to eq "Louis C.K."
end end
it 'can override username properly when only the case changes' do
SiteSetting.auth_overrides_username = true
sso = new_discourse_sso
sso.username = "testuser"
sso.name = "test user"
sso.email = "test@test.com"
sso.external_id = "100"
sso.bio = "This **is** the bio"
sso.suppress_welcome_message = true
# create the original user
user = sso.lookup_or_create_user(ip_address)
expect(user.username).to eq "testuser"
# change the username case
sso.username = "TestUser"
user = sso.lookup_or_create_user(ip_address)
expect(user.username).to eq "TestUser"
end
it 'do not override username when a new username after fixing is the same' do
SiteSetting.auth_overrides_username = true
sso = new_discourse_sso
sso.username = "testuser"
sso.name = "test user"
sso.email = "test@test.com"
sso.external_id = "100"
# create the original user
user = sso.lookup_or_create_user(ip_address)
expect(user.username).to eq "testuser"
# change the username case
sso.username = "testuserგამარჯობა"
user = sso.lookup_or_create_user(ip_address)
expect(user.username).to eq "testuser"
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"