From 03d42b23968b4aa748afa12e66bf92f68c7b3d29 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 31 Mar 2026 00:02:53 -0400 Subject: [PATCH] username randomness was not high enough --- .../src/username/generate_random_username.rs | 105 +++++++++++------- .../handle_new_sso_account_for_new_user.rs | 2 +- 2 files changed, 66 insertions(+), 41 deletions(-) diff --git a/crates/lib/users/src/username/generate_random_username.rs b/crates/lib/users/src/username/generate_random_username.rs index e8cb5755f5..fa8006cb90 100644 --- a/crates/lib/users/src/username/generate_random_username.rs +++ b/crates/lib/users/src/username/generate_random_username.rs @@ -76,63 +76,88 @@ fn generate_candidate_username() -> Option { UsernameFormat::ScreamingSnakeCase => format!("{}_{}", adjective.to_uppercase(), noun.to_uppercase()), }; - if let Some(digit) = maybe_random_safe_digit() { - candidate_username = match format { - UsernameFormat::CamelCase => format!("{}{}", candidate_username, digit), - UsernameFormat::KebabCase - | UsernameFormat::CamelKebabCase => format!("{}-{}", candidate_username, digit), - UsernameFormat::SnakeCase - | UsernameFormat::CamelSnakeCase - | UsernameFormat::ScreamingSnakeCase => format!("{}_{}", candidate_username, digit), - }; - } + let digit = random_digit_probably_safe(); + + candidate_username = match format { + UsernameFormat::CamelCase => format!("{}{}", candidate_username, digit), + UsernameFormat::KebabCase + | UsernameFormat::CamelKebabCase => format!("{}-{}", candidate_username, digit), + UsernameFormat::SnakeCase + | UsernameFormat::CamelSnakeCase + | UsernameFormat::ScreamingSnakeCase => format!("{}_{}", candidate_username, digit), + }; Some(candidate_username) } -fn maybe_random_safe_digit() -> Option { - fn maybe_random_digit() -> Option { - // Give uniform probability for the number of digits - let num_digits = rand::thread_rng().gen_range(0..5u8); - match num_digits { - 0 => None, - 1 => Some(rand::thread_rng().gen_range(0..10)), - 2 => Some(rand::thread_rng().gen_range(10..100)), - 3 => Some(rand::thread_rng().gen_range(100..1000)), - 4 => Some(rand::thread_rng().gen_range(1000..10000)), - _ => Some(rand::thread_rng().gen_range(10000..100000)), +fn random_digit_probably_safe() -> u32 { + for _ in 0..100 { + if let Some(digit) = maybe_safe_random_digit() { + return digit; } } + random_digit() +} - // Don't return potentially offensive numbers - match maybe_random_digit() { - None => None, - Some(69) => None, - Some(420) => None, - Some(666) => None, - Some(8008) => None, - Some(80085) => None, - Some(8008135) => None, - Some(digit) => Some(digit), +/// Don't return potentially offensive numbers +fn maybe_safe_random_digit() -> Option { + match random_digit() { + 69 => None, + 420 => None, + 666 => None, + 8008 => None, + 80085 => None, + 8008135 => None, + digit => Some(digit), + } +} + +fn random_digit() -> u32 { + // Non-uniform probability for the number of digits + let num_digits = rand::thread_rng().gen_range(0..100); + match num_digits { + 0..5 => rand::thread_rng().gen_range(0..10), // 5% + 5..25 => rand::thread_rng().gen_range(10..100), // 20% + 25..60 => rand::thread_rng().gen_range(100..1000), // 35% + 60..90 => rand::thread_rng().gen_range(1000..10000), // 30% + _ => rand::thread_rng().gen_range(10000..100000), // 10% } } #[cfg(test)] mod tests { use std::collections::HashSet; + use crate::username::generate_random_username::maybe_safe_random_digit; use crate::username::generate_random_username::generate_random_username; - #[test] - fn test_base_case_success() { - assert!(generate_random_username().len() > 0); + mod usernames { + use super::*; + + #[test] + fn test_base_case_success() { + assert!(generate_random_username().len() > 0); + } + + #[test] + fn generate_lots() { + let mut collection = HashSet::new(); + for _ in 0..100 { + collection.insert(generate_random_username()); + } + assert!(collection.len() > 95); // NB: Should be an easy bar to hit + } } - #[test] - fn generate_lots() { - let mut collection = HashSet::new(); - for _ in 0..100 { - collection.insert(generate_random_username()); + mod random_numbers { + use super::*; + + #[test] + fn generate_lots_of_numbers() { + let mut collection = HashSet::new(); + for _ in 0..10000 { + collection.insert(maybe_safe_random_digit()); + } + assert!(collection.len() > 1000); } - assert!(collection.len() > 50); // NB: Should be an easy bar to hit } } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso/handle_new_sso_account_for_new_user.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso/handle_new_sso_account_for_new_user.rs index 68edcb7d97..716e1c46bd 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso/handle_new_sso_account_for_new_user.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/users/google_sso/handle_new_sso_account_for_new_user.rs @@ -47,7 +47,7 @@ pub async fn handle_new_sso_account_for_new_user( let mut maybe_user_token = None; let mut maybe_user_display_name = None; - for _ in 0..3 { + for _ in 0..5 { // NB: We try a few times to make sure we don't hit a username collision. let display_name = generate_random_username(); let username = display_name.trim().to_lowercase();