mirror of
https://github.com/bitwarden/server
synced 2026-02-19 10:53:34 +00:00
Construct a folder seeder
This commit is contained in:
31
util/Seeder/Data/FolderNameGenerator.cs
Normal file
31
util/Seeder/Data/FolderNameGenerator.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Bogus;
|
||||
|
||||
namespace Bit.Seeder.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Generates deterministic folder names using Bogus Commerce.Department().
|
||||
/// Pre-generates a pool of business-themed names for consistent index-based access.
|
||||
/// </summary>
|
||||
internal sealed class FolderNameGenerator
|
||||
{
|
||||
private const int _namePoolSize = 50;
|
||||
|
||||
private readonly string[] _folderNames;
|
||||
|
||||
public FolderNameGenerator(int seed)
|
||||
{
|
||||
var faker = new Faker { Random = new Randomizer(seed) };
|
||||
|
||||
// Pre-generate business department names for determinism
|
||||
// Examples: "Automotive", "Home & Garden", "Sports", "Electronics", "Beauty"
|
||||
_folderNames = Enumerable.Range(0, _namePoolSize)
|
||||
.Select(_ => faker.Commerce.Department())
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a folder name by index, wrapping around if index exceeds pool size.
|
||||
/// </summary>
|
||||
public string GetFolderName(int index) => _folderNames[index % _folderNames.Length];
|
||||
}
|
||||
28
util/Seeder/Factories/FolderSeeder.cs
Normal file
28
util/Seeder/Factories/FolderSeeder.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.RustSDK;
|
||||
|
||||
namespace Bit.Seeder.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating Folder entities with encrypted names.
|
||||
/// Folders are per-user constructs encrypted with the user's symmetric key.
|
||||
/// </summary>
|
||||
internal sealed class FolderSeeder(RustSdkService sdkService)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a folder with an encrypted name.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user who owns this folder.</param>
|
||||
/// <param name="userKeyBase64">The user's symmetric key (not org key).</param>
|
||||
/// <param name="name">The plaintext folder name to encrypt.</param>
|
||||
public Folder CreateFolder(Guid userId, string userKeyBase64, string name)
|
||||
{
|
||||
return new Folder
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
UserId = userId,
|
||||
Name = sdkService.EncryptString(name, userKeyBase64)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,19 @@ public class UserSeeder(RustSdkService sdkService, IPasswordHasher<Bit.Core.Enti
|
||||
IPasswordHasher<User> passwordHasher)
|
||||
{
|
||||
var keys = sdkService.GenerateUserKeys(email, DefaultPassword);
|
||||
return CreateUserFromKeys(email, keys, passwordHasher);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user from pre-generated keys (no email mangling).
|
||||
/// Use this when you need to retain the user's symmetric key for subsequent operations
|
||||
/// (e.g., encrypting folders with the user's key).
|
||||
/// </summary>
|
||||
public static User CreateUserFromKeys(
|
||||
string email,
|
||||
UserKeys keys,
|
||||
IPasswordHasher<User> passwordHasher)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
|
||||
@@ -9,6 +9,7 @@ using Bit.Seeder.Factories;
|
||||
using Bit.Seeder.Options;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using EfFolder = Bit.Infrastructure.EntityFramework.Vault.Models.Folder;
|
||||
using EfOrganization = Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization;
|
||||
using EfOrganizationUser = Bit.Infrastructure.EntityFramework.Models.OrganizationUser;
|
||||
using EfUser = Bit.Infrastructure.EntityFramework.Models.User;
|
||||
@@ -31,6 +32,12 @@ public class OrganizationWithVaultRecipe(
|
||||
{
|
||||
private readonly CollectionSeeder _collectionSeeder = new(sdkService);
|
||||
private readonly CipherSeeder _cipherSeeder = new(sdkService);
|
||||
private readonly FolderSeeder _folderSeeder = new(sdkService);
|
||||
|
||||
/// <summary>
|
||||
/// Tracks a user with their symmetric key for folder encryption.
|
||||
/// </summary>
|
||||
private record UserWithKey(User User, string SymmetricKey);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds an organization with users, collections, groups, and encrypted ciphers.
|
||||
@@ -53,15 +60,17 @@ public class OrganizationWithVaultRecipe(
|
||||
var ownerOrgUser = organization.CreateOrganizationUserWithKey(
|
||||
ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed, ownerOrgKey);
|
||||
|
||||
// Create member users via factory
|
||||
var memberUsers = new List<User>();
|
||||
// Create member users via factory, retaining keys for folder encryption
|
||||
var memberUsersWithKeys = new List<UserWithKey>();
|
||||
var memberOrgUsers = new List<OrganizationUser>();
|
||||
var useRealisticMix = options.RealisticStatusMix && options.Users >= 10;
|
||||
|
||||
for (var i = 0; i < options.Users; i++)
|
||||
{
|
||||
var memberUser = UserSeeder.CreateUserWithSdkKeys($"user{i}@{options.Domain}", sdkService, passwordHasher);
|
||||
memberUsers.Add(memberUser);
|
||||
var email = $"user{i}@{options.Domain}";
|
||||
var userKeys = sdkService.GenerateUserKeys(email, UserSeeder.DefaultPassword);
|
||||
var memberUser = UserSeeder.CreateUserFromKeys(email, userKeys, passwordHasher);
|
||||
memberUsersWithKeys.Add(new UserWithKey(memberUser, userKeys.Key));
|
||||
|
||||
var status = useRealisticMix
|
||||
? GetRealisticStatus(i, options.Users)
|
||||
@@ -76,6 +85,8 @@ public class OrganizationWithVaultRecipe(
|
||||
memberUser, OrganizationUserType.User, status, memberOrgKey));
|
||||
}
|
||||
|
||||
var memberUsers = memberUsersWithKeys.Select(uwk => uwk.User).ToList();
|
||||
|
||||
// Persist organization and users
|
||||
db.Add(mapper.Map<EfOrganization>(organization));
|
||||
db.Add(mapper.Map<EfUser>(ownerUser));
|
||||
@@ -97,6 +108,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, options.Region);
|
||||
CreateFolders(memberUsersWithKeys);
|
||||
|
||||
return organization.Id;
|
||||
}
|
||||
@@ -253,4 +265,67 @@ public class OrganizationWithVaultRecipe(
|
||||
|
||||
return OrganizationUserStatusType.Revoked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates personal vault folders for users with realistic distribution.
|
||||
/// Folders are encrypted with each user's individual symmetric key.
|
||||
/// </summary>
|
||||
private void CreateFolders(List<UserWithKey> usersWithKeys)
|
||||
{
|
||||
if (usersWithKeys.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var seed = usersWithKeys[0].User.Id.GetHashCode();
|
||||
var random = new Random(seed);
|
||||
var folderNameGenerator = new FolderNameGenerator(seed);
|
||||
|
||||
var allFolders = usersWithKeys
|
||||
.SelectMany((uwk, userIndex) =>
|
||||
{
|
||||
var folderCount = GetFolderCountForUser(userIndex, usersWithKeys.Count, random);
|
||||
return Enumerable.Range(0, folderCount)
|
||||
.Select(folderIndex => _folderSeeder.CreateFolder(
|
||||
uwk.User.Id,
|
||||
uwk.SymmetricKey,
|
||||
folderNameGenerator.GetFolderName(userIndex * 15 + folderIndex)));
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (allFolders.Count > 0)
|
||||
{
|
||||
var efFolders = allFolders.Select(f => mapper.Map<EfFolder>(f)).ToList();
|
||||
db.BulkCopy(efFolders);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns folder count based on user index position in the distribution.
|
||||
/// Distribution: 35% Zero, 35% Few (1-3), 20% Some (4-7), 10% TooMany (10-15)
|
||||
/// </summary>
|
||||
private static int GetFolderCountForUser(int userIndex, int totalUsers, Random random)
|
||||
{
|
||||
var zeroCount = (int)(totalUsers * 0.35);
|
||||
var fewCount = (int)(totalUsers * 0.35);
|
||||
var someCount = (int)(totalUsers * 0.20);
|
||||
// TooMany gets the remainder
|
||||
|
||||
if (userIndex < zeroCount)
|
||||
{
|
||||
return 0; // Zero folders
|
||||
}
|
||||
|
||||
if (userIndex < zeroCount + fewCount)
|
||||
{
|
||||
return random.Next(1, 4); // Few: 1-3 folders
|
||||
}
|
||||
|
||||
if (userIndex < zeroCount + fewCount + someCount)
|
||||
{
|
||||
return random.Next(4, 8); // Some: 4-7 folders
|
||||
}
|
||||
|
||||
return random.Next(10, 16); // TooMany: 10-15 folders
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user