1
0
mirror of https://github.com/bitwarden/server synced 2026-01-28 23:36:12 +00:00

Implement Bogus for name generation & Region specific usernames for vault items

This commit is contained in:
Mick Letofsky
2026-01-27 18:08:17 +01:00
parent ef748b428f
commit 30bc4c76c3
7 changed files with 128 additions and 93 deletions

View File

@@ -58,7 +58,9 @@ public class Program
[Option('m', "mix-user-statuses", Description = "Use realistic status mix (85% confirmed, 5% each invited/accepted/revoked). Requires >= 10 users.")]
bool mixStatuses = true,
[Option('o', "org-structure", Description = "Org structure for collections: Traditional, Spotify, or Modern")]
string? structure = null
string? structure = null,
[Option('r', "region", Description = "Geographic region for names: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global")]
string? region = null
)
{
if (users < 1)
@@ -77,6 +79,7 @@ public class Program
}
var structureModel = ParseOrgStructure(structure);
var geographicRegion = ParseGeographicRegion(region);
var services = new ServiceCollection();
ServiceCollectionExtension.ConfigureServices(services);
@@ -99,7 +102,8 @@ public class Program
Ciphers = ciphers,
Groups = groups,
RealisticStatusMix = mixStatuses,
StructureModel = structureModel
StructureModel = structureModel,
Region = geographicRegion
});
}
@@ -118,4 +122,24 @@ public class Program
_ => throw new ArgumentException($"Unknown structure '{structure}'. Use: Traditional, Spotify, or Modern")
};
}
private static GeographicRegion? ParseGeographicRegion(string? region)
{
if (string.IsNullOrEmpty(region))
{
return null;
}
return region.ToLowerInvariant() switch
{
"northamerica" => GeographicRegion.NorthAmerica,
"europe" => GeographicRegion.Europe,
"asiapacific" => GeographicRegion.AsiaPacific,
"latinamerica" => GeographicRegion.LatinAmerica,
"middleeast" => GeographicRegion.MiddleEast,
"africa" => GeographicRegion.Africa,
"global" => GeographicRegion.Global,
_ => throw new ArgumentException($"Unknown region '{region}'. Use: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global")
};
}
}

View File

@@ -0,0 +1,78 @@
using Bit.Seeder.Data.Enums;
using Bogus;
using Bogus.DataSets;
namespace Bit.Seeder.Data;
/// <summary>
/// Provides locale-aware name generation using the Bogus library.
/// Maps GeographicRegion to appropriate Bogus locales for culturally-appropriate names.
/// </summary>
internal sealed class BogusNameProvider
{
private readonly Faker _faker;
public BogusNameProvider(GeographicRegion region, int? seed = null)
{
var locale = MapRegionToLocale(region, seed);
_faker = seed.HasValue
? new Faker(locale) { Random = new Randomizer(seed.Value) }
: new Faker(locale);
}
public string FirstName() => _faker.Name.FirstName();
public string FirstName(Name.Gender gender) => _faker.Name.FirstName(gender);
public string LastName() => _faker.Name.LastName();
private static string MapRegionToLocale(GeographicRegion region, int? seed) => region switch
{
GeographicRegion.NorthAmerica => "en_US",
GeographicRegion.Europe => GetRandomEuropeanLocale(seed),
GeographicRegion.AsiaPacific => GetRandomAsianLocale(seed),
GeographicRegion.LatinAmerica => GetRandomLatinAmericanLocale(seed),
GeographicRegion.MiddleEast => GetRandomMiddleEastLocale(seed),
GeographicRegion.Africa => GetRandomAfricanLocale(seed),
GeographicRegion.Global => "en",
_ => "en"
};
private static string GetRandomEuropeanLocale(int? seed)
{
var locales = new[] { "en_GB", "de", "fr", "es", "it", "nl", "pl", "pt_PT", "sv" };
return PickLocale(locales, seed);
}
private static string GetRandomAsianLocale(int? seed)
{
var locales = new[] { "ja", "ko", "zh_CN", "zh_TW", "vi" };
return PickLocale(locales, seed);
}
private static string GetRandomLatinAmericanLocale(int? seed)
{
var locales = new[] { "es_MX", "pt_BR", "es" };
return PickLocale(locales, seed);
}
private static string GetRandomMiddleEastLocale(int? seed)
{
// Bogus has limited Middle East support; use available Arabic/Turkish locales
var locales = new[] { "ar", "tr", "fa" };
return PickLocale(locales, seed);
}
private static string GetRandomAfricanLocale(int? seed)
{
// Bogus has limited African support; use South African English and French (West Africa)
var locales = new[] { "en_ZA", "fr" };
return PickLocale(locales, seed);
}
private static string PickLocale(string[] locales, int? seed)
{
var random = seed.HasValue ? new Random(seed.Value) : Random.Shared;
return locales[random.Next(locales.Length)];
}
}

View File

@@ -4,9 +4,13 @@ namespace Bit.Seeder.Data;
/// <summary>
/// Generates deterministic usernames for companies using configurable patterns.
/// Uses Bogus library for locale-aware name generation while maintaining determinism
/// through pre-generated arrays indexed by a seed.
/// </summary>
internal sealed class UsernameGenerator
internal sealed class CipherUsernameGenerator
{
private const int _namePoolSize = 1500;
private readonly Random _random;
private readonly UsernamePattern _pattern;
@@ -15,7 +19,7 @@ internal sealed class UsernameGenerator
private readonly string[] _lastNames;
public UsernameGenerator(
public CipherUsernameGenerator(
int seed,
UsernamePatternType patternType = UsernamePatternType.FirstDotLast,
GeographicRegion? region = null)
@@ -23,12 +27,10 @@ internal sealed class UsernameGenerator
_random = new Random(seed);
_pattern = UsernamePatterns.GetPattern(patternType);
(_firstNames, _lastNames) = region switch
{
GeographicRegion.NorthAmerica => (Names.UsFirstNames, Names.UsLastNames),
GeographicRegion.Europe => (Names.EuropeanFirstNames, Names.EuropeanLastNames),
_ => (Names.AllFirstNames, Names.AllLastNames)
};
// Pre-generate arrays from Bogus for deterministic index-based access
var provider = new BogusNameProvider(region ?? GeographicRegion.Global, seed);
_firstNames = Enumerable.Range(0, _namePoolSize).Select(_ => provider.FirstName()).ToArray();
_lastNames = Enumerable.Range(0, _namePoolSize).Select(_ => provider.LastName()).ToArray();
}
public string Generate(Company company)

View File

@@ -1,80 +0,0 @@
namespace Bit.Seeder.Data;
/// <summary>
/// First and last names organized by region for username generation.
/// Add new regions by creating arrays and including them in the All* properties.
/// </summary>
internal static class Names
{
public static readonly string[] UsFirstNames =
[
// Male
"James", "Robert", "John", "Michael", "David", "William", "Richard", "Joseph", "Thomas", "Christopher",
"Charles", "Daniel", "Matthew", "Anthony", "Mark", "Donald", "Steven", "Paul", "Andrew", "Joshua",
"Kenneth", "Kevin", "Brian", "George", "Timothy", "Ronald", "Edward", "Jason", "Jeffrey", "Ryan",
"Jacob", "Gary", "Nicholas", "Eric", "Jonathan", "Stephen", "Larry", "Justin", "Scott", "Brandon",
"Benjamin", "Samuel", "Raymond", "Gregory", "Frank", "Alexander", "Patrick", "Jack", "Dennis", "Jerry",
// Female
"Mary", "Patricia", "Jennifer", "Linda", "Barbara", "Elizabeth", "Susan", "Jessica", "Sarah", "Karen",
"Lisa", "Nancy", "Betty", "Margaret", "Sandra", "Ashley", "Kimberly", "Emily", "Donna", "Michelle",
"Dorothy", "Carol", "Amanda", "Melissa", "Deborah", "Stephanie", "Rebecca", "Sharon", "Laura", "Cynthia",
"Kathleen", "Amy", "Angela", "Shirley", "Anna", "Brenda", "Pamela", "Emma", "Nicole", "Helen",
"Samantha", "Katherine", "Christine", "Debra", "Rachel", "Carolyn", "Janet", "Catherine", "Maria", "Heather"
];
public static readonly string[] UsLastNames =
[
"Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller", "Davis", "Rodriguez", "Martinez",
"Hernandez", "Lopez", "Gonzalez", "Wilson", "Anderson", "Thomas", "Taylor", "Moore", "Jackson", "Martin",
"Lee", "Perez", "Thompson", "White", "Harris", "Sanchez", "Clark", "Ramirez", "Lewis", "Robinson",
"Walker", "Young", "Allen", "King", "Wright", "Scott", "Torres", "Nguyen", "Hill", "Flores",
"Green", "Adams", "Nelson", "Baker", "Hall", "Rivera", "Campbell", "Mitchell", "Carter", "Roberts",
"Gomez", "Phillips", "Evans", "Turner", "Diaz", "Parker", "Cruz", "Edwards", "Collins", "Reyes",
"Stewart", "Morris", "Morales", "Murphy", "Cook", "Rogers", "Gutierrez", "Ortiz", "Morgan", "Cooper",
"Peterson", "Bailey", "Reed", "Kelly", "Howard", "Ramos", "Kim", "Cox", "Ward", "Richardson",
"Watson", "Brooks", "Chavez", "Wood", "James", "Bennett", "Gray", "Mendoza", "Ruiz", "Hughes",
"Price", "Alvarez", "Castillo", "Sanders", "Patel", "Myers", "Long", "Ross", "Foster", "Jimenez"
];
public static readonly string[] EuropeanFirstNames =
[
// British
"Oliver", "George", "Harry", "Jack", "Charlie", "Thomas", "Oscar", "William", "James", "Henry",
"Olivia", "Amelia", "Isla", "Ava", "Emily", "Sophie", "Grace", "Mia", "Poppy", "Ella",
// German
"Maximilian", "Alexander", "Paul", "Leon", "Lukas", "Felix", "Noah", "Elias", "Ben", "Finn",
"Emma", "Hannah", "Mia", "Sofia", "Anna", "Emilia", "Lena", "Marie", "Lea", "Clara",
// French
"Gabriel", "Raphael", "Leo", "Louis", "Lucas", "Adam", "Hugo", "Jules", "Arthur", "Nathan",
"Louise", "Alice", "Chloe", "Ines", "Lea", "Manon", "Rose", "Anna", "Lina", "Mila",
// Spanish
"Hugo", "Martin", "Lucas", "Daniel", "Pablo", "Alejandro", "Adrian", "Alvaro", "David", "Mario",
"Lucia", "Sofia", "Maria", "Martina", "Paula", "Julia", "Daniela", "Valeria", "Alba", "Emma",
// Italian
"Leonardo", "Francesco", "Alessandro", "Lorenzo", "Mattia", "Andrea", "Gabriele", "Riccardo", "Tommaso", "Edoardo",
"Sofia", "Giulia", "Aurora", "Alice", "Ginevra", "Emma", "Giorgia", "Greta", "Beatrice", "Anna"
];
public static readonly string[] EuropeanLastNames =
[
// British
"Smith", "Jones", "Williams", "Brown", "Taylor", "Davies", "Wilson", "Evans", "Thomas", "Johnson",
"Roberts", "Walker", "Wright", "Robinson", "Thompson", "White", "Hughes", "Edwards", "Green", "Hall",
// German
"Mueller", "Schmidt", "Schneider", "Fischer", "Weber", "Meyer", "Wagner", "Becker", "Schulz", "Hoffmann",
"Schaefer", "Koch", "Bauer", "Richter", "Klein", "Wolf", "Schroeder", "Neumann", "Schwarz", "Zimmermann",
// French
"Martin", "Bernard", "Dubois", "Thomas", "Robert", "Richard", "Petit", "Durand", "Leroy", "Moreau",
"Simon", "Laurent", "Lefebvre", "Michel", "Garcia", "David", "Bertrand", "Roux", "Vincent", "Fournier",
// Spanish
"Garcia", "Rodriguez", "Martinez", "Lopez", "Sanchez", "Gonzalez", "Perez", "Martin", "Gomez", "Ruiz",
"Hernandez", "Jimenez", "Diaz", "Moreno", "Alvarez", "Munoz", "Romero", "Alonso", "Gutierrez", "Navarro",
// Italian
"Rossi", "Russo", "Ferrari", "Esposito", "Bianchi", "Romano", "Colombo", "Ricci", "Marino", "Greco",
"Bruno", "Gallo", "Conti", "DeLuca", "Costa", "Giordano", "Mancini", "Rizzo", "Lombardi", "Moretti"
];
public static readonly string[] AllFirstNames = [.. UsFirstNames, .. EuropeanFirstNames];
public static readonly string[] AllLastNames = [.. UsLastNames, .. EuropeanLastNames];
}

View File

@@ -54,4 +54,10 @@ public class OrganizationVaultOptions
/// (25% VeryWeak, 30% Weak, 25% Fair, 15% Strong, 5% VeryStrong).
/// </summary>
public PasswordStrength PasswordStrength { get; init; } = PasswordStrength.Realistic;
/// <summary>
/// Geographic region for culturally-appropriate name generation in cipher usernames.
/// Defaults to Global (mixed locales from all regions).
/// </summary>
public GeographicRegion? Region { get; init; }
}

View File

@@ -96,7 +96,7 @@ public class OrganizationWithVaultRecipe(
var collectionIds = CreateCollections(organization.Id, orgKeys.Key, options.StructureModel, confirmedOrgUserIds);
CreateGroups(organization.Id, options.Groups, confirmedOrgUserIds);
CreateCiphers(organization.Id, orgKeys.Key, collectionIds, options.Ciphers, options.UsernamePattern, options.PasswordStrength);
CreateCiphers(organization.Id, orgKeys.Key, collectionIds, options.Ciphers, options.UsernamePattern, options.PasswordStrength, options.Region);
return organization.Id;
}
@@ -170,10 +170,11 @@ public class OrganizationWithVaultRecipe(
List<Guid> collectionIds,
int cipherCount,
UsernamePatternType usernamePattern,
PasswordStrength passwordStrength)
PasswordStrength passwordStrength,
GeographicRegion? region)
{
var companies = Companies.All;
var usernameGenerator = new UsernameGenerator(organizationId.GetHashCode(), usernamePattern);
var usernameGenerator = new CipherUsernameGenerator(organizationId.GetHashCode(), usernamePattern, region);
var cipherList = Enumerable.Range(0, cipherCount)
.Select(i =>

View File

@@ -19,6 +19,10 @@
<ProjectReference Include="..\RustSdk\RustSdk.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Bogus" Version="35.*" />
</ItemGroup>
<ItemGroup>
<Compile Remove="..\..\Program.cs" />
</ItemGroup>