diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 892d2f4255..2ec8d86e0e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -325,6 +329,14 @@ Global {3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -377,6 +389,8 @@ Global {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs new file mode 100644 index 0000000000..94432b05a0 --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Http.Headers; +using Bit.Api.IntegrationTest.Factories; +using Bit.Seeder.Recipes; +using Xunit; +using Xunit.Abstractions; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) +{ + [Theory(Skip = "Performance test")] + [InlineData(100)] + [InlineData(60000)] + public async Task GetAsync(int seats) + { + await using var factory = new ApiApplicationFactory(); + var client = factory.CreateClient(); + + var db = factory.GetDatabaseContext(); + var seeder = new OrganizationWithUsersRecipe(db); + + var orgId = seeder.Seed("Org", seats, "large.test"); + + var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs="); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + var response = await client.GetAsync($"/organizations/{orgId}/users?includeCollections=true"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = await response.Content.ReadAsStringAsync(); + Assert.NotEmpty(result); + + stopwatch.Stop(); + testOutputHelper.WriteLine($"Seed: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms"); + } +} diff --git a/test/Api.IntegrationTest/Api.IntegrationTest.csproj b/test/Api.IntegrationTest/Api.IntegrationTest.csproj index 8fa74f98d4..a9d7fd502e 100644 --- a/test/Api.IntegrationTest/Api.IntegrationTest.csproj +++ b/test/Api.IntegrationTest/Api.IntegrationTest.csproj @@ -18,6 +18,7 @@ + diff --git a/util/DbSeederUtility/DbSeederUtility.csproj b/util/DbSeederUtility/DbSeederUtility.csproj new file mode 100644 index 0000000000..90ac7f22b4 --- /dev/null +++ b/util/DbSeederUtility/DbSeederUtility.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + Bit.DbSeederUtility + DbSeeder + true + 2294c6ba-7cd0-4293-a797-3882e41c61cb + + + + + + + + + + + diff --git a/util/DbSeederUtility/GlobalSettingsFactory.cs b/util/DbSeederUtility/GlobalSettingsFactory.cs new file mode 100644 index 0000000000..e4ad275a0e --- /dev/null +++ b/util/DbSeederUtility/GlobalSettingsFactory.cs @@ -0,0 +1,34 @@ +using Bit.Core.Settings; +using Microsoft.Extensions.Configuration; + +namespace Bit.DbSeederUtility; + +public static class GlobalSettingsFactory +{ + private static GlobalSettings? _globalSettings; + + public static GlobalSettings GlobalSettings + { + get { return _globalSettings ??= LoadGlobalSettings(); } + } + + private static GlobalSettings LoadGlobalSettings() + { + Console.WriteLine("Loading global settings..."); + + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true) + .AddUserSecrets("bitwarden-Api") // Load user secrets from the API project + .AddEnvironmentVariables(); + + var configuration = configBuilder.Build(); + var globalSettingsSection = configuration.GetSection("globalSettings"); + + var settings = new GlobalSettings(); + globalSettingsSection.Bind(settings); + + return settings; + } +} diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs new file mode 100644 index 0000000000..2d75b31934 --- /dev/null +++ b/util/DbSeederUtility/Program.cs @@ -0,0 +1,39 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Recipes; +using CommandDotNet; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.DbSeederUtility; + +public class Program +{ + private static int Main(string[] args) + { + return new AppRunner() + .Run(args); + } + + [Command("organization", Description = "Seed an organization and organization users")] + public void Organization( + [Option('n', "Name", Description = "Name of organization")] + string name, + [Option('u', "users", Description = "Number of users to generate")] + int users, + [Option('d', "domain", Description = "Email domain for users")] + string domain + ) + { + // Create service provider with necessary services + var services = new ServiceCollection(); + ServiceCollectionExtension.ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + // Get a scoped DB context + using var scope = serviceProvider.CreateScope(); + var scopedServices = scope.ServiceProvider; + var db = scopedServices.GetRequiredService(); + + var recipe = new OrganizationWithUsersRecipe(db); + recipe.Seed(name, users, domain); + } +} diff --git a/util/DbSeederUtility/README.md b/util/DbSeederUtility/README.md new file mode 100644 index 0000000000..0eb21ae6c5 --- /dev/null +++ b/util/DbSeederUtility/README.md @@ -0,0 +1,40 @@ +# Bitwarden Database Seeder Utility + +A command-line utility for generating and managing test data for Bitwarden databases. + +## Overview + +DbSeederUtility is an executable wrapper around the Seeder class library that provides a convenient command-line +interface for executing seed-recipes in your local environment. + +## Installation + +The utility can be built and run as a .NET 8 application: + +``` +dotnet build +dotnet run -- [options] +``` + +Or directly using the compiled executable: + +``` +DbSeeder.exe [options] +``` + +## Examples + +### Generate and load test organization + +```bash +# Generate an organization called "seeded" with 10000 users using the @large.test email domain. +# Login using "admin@large.test" with password "asdfasdfasdf" +DbSeeder.exe organization -n seeded -u 10000 -d large.test +``` + +## Dependencies + +This utility depends on: +- The Seeder class library +- CommandDotNet for command-line parsing +- .NET 8.0 runtime diff --git a/util/DbSeederUtility/ServiceCollectionExtension.cs b/util/DbSeederUtility/ServiceCollectionExtension.cs new file mode 100644 index 0000000000..0653bb1801 --- /dev/null +++ b/util/DbSeederUtility/ServiceCollectionExtension.cs @@ -0,0 +1,25 @@ +using Bit.SharedWeb.Utilities; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.DbSeederUtility; + +public static class ServiceCollectionExtension +{ + public static void ConfigureServices(ServiceCollection services) + { + // Load configuration using the GlobalSettingsFactory + var globalSettings = GlobalSettingsFactory.GlobalSettings; + + // Register services + services.AddLogging(builder => builder.AddConsole()); + services.AddSingleton(globalSettings); + + // Add Data Protection services + services.AddDataProtection() + .SetApplicationName("Bitwarden"); + + services.AddDatabaseRepositories(globalSettings); + } +} diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs new file mode 100644 index 0000000000..5e5cb17419 --- /dev/null +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -0,0 +1,44 @@ +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class OrganizationSeeder +{ + public static Organization CreateEnterprise(string name, string domain, int seats) + { + return new Organization + { + Id = Guid.NewGuid(), + Name = name, + BillingEmail = $"billing@{domain}", + Plan = "Enterprise (Annually)", + PlanType = PlanType.EnterpriseAnnually, + Seats = seats, + + // Currently hardcoded to the values from https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-core/src/client/test_accounts.rs. + // TODO: These should be dynamically generated by the SDK. + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmIJbGMk6eZqVE7UxhZ46Weu2jKciqOiOkSVYtGvs61rfe9AXxtLaaZEKN4d4DmkZcF6dna2eXNxZmb7U4pwlttye8ksqISe6IUAZQox7auBpjopdCEPhKRg3BD/u8ks9UxSxgWe+fpebjt6gd5hsl1/5HOObn7SeU6EEU04cp3/eH7a4OTdXxB8oN62HGV9kM/ubM1goILgjoSJDbihMK0eb7b8hPHwcA/YOgKKiu/N3FighccdSMD5Pk+HfjacsFNZQa2EsqW09IvvSZ+iL6HQeZ1vwc/6TO1J7EOfJZFQcjoEL9LVI693efYoMZSmrPEWziZ4PvwpOOGo6OObyMQIDAQAB", + PrivateKey = "2.6FggyKVyaKQsfohi5yqgbg==|UU2JeafOB41L5UscGmf4kq15JGDf3Bkf67KECiehTODzbWctVLTgyDk0Qco8/6CMN6nZGXjxR2A4r5ExhmwRNsNxd77G+MprkmiJz+7w33ROZ1ouQO5XjD3wbQ3ssqNiTKId6yAUPBvuAZRixVApauTuADc8QWGixqCQcqZzmU7YSBBIPf652/AEYr4Tk64YihoE39pHiK8MRbTLdRt3EF4LSMugPAPM24vCgUv3w1TD3Fj6sDg/6oi3flOV9SJZX4vCiUXbDNEuD/p2aQrEXVbaxweFOHjTe7F4iawjXw3nG3SO8rUBHcxbhDDVx5rjYactbW5QvHWiyla6uLb6o8WHBneg2EjTEwAHOZE/rBjcqmAJb2sVp1E0Kwq8ycGmL69vmqJPC1GqVTohAQvmEkaxIPpfq24Yb9ZPrADA7iEXBKuAQ1FphFUVgJBJGJbd60sOV1Rz1T+gUwS4wCNQ4l3LG1S22+wzUVlEku5DXFnT932tatqTyWEthqPqLCt6dL1+qa94XLpeHagXAx2VGe8n8IlcADtxqS+l8xQ4heT12WO9kC316vqvg1mnsI56faup9hb3eT9ZpKyxSBGYOphlTWfV1Y/v64f5PYvTo4aL0IYHyLY/9Qi72vFmOpPeHBYgD5t3j+H2CsiU1PkYsBggOmD7xW8FDuT6HWVvwhEJqeibVPK0Lhyj6tgvlSIAvFUaSMFPlmwFNmwfj/AHUhr9KuTfsBFTZ10yy9TZVgf+EofwnrxHBaWUgdD40aHoY1VjfG33iEuajb6buxG3pYFyPNhJNzeLZisUKIDRMQpUHrsE22EyrFFran3tZGdtcyIEK4Q1F0ULYzJ6T9iY25/ZgPy3pEAAMZCtqo3s+GjX295fWIHfMcnjMgNUHPjExjWBHa+ggK9iQXkFpBVyYB1ga/+0eiIhiek3PlgtvpDrqF7TsLK+ROiBw2GJ7uaO3EEXOj2GpNBuEJ5CdodhZkwzhwMcSatgDHkUuNVu0iVbF6/MxVdOxWXKO+jCYM6PZk/vAhLYqpPzu2T2Uyz4nkDs2Tiq61ez6FoCrzdHIiyIxVTzUQH8G9FgSmtaZ7GCbqlhnurYgcMciwPzxg0hpAQT+NZw1tVEii9vFSpJJbGJqNhORKfKh/Mu1P/9LOQq7Y0P2FIR3x/eUVEQ7CGv2jVtO5ryGSmKeq/P9Fr54wTPaNiqN2K+leACUznCdUWw8kZo/AsBcrOe4OkRX6k8LC3oeJXy06DEToatxEvPYemUauhxiXRw8nfNMqc4LyJq2bbT0zCgJHoqpozPdNg6AYWcoIobgAGu7ZQGq+oE1MT3GZxotMPe/NUJiAc5YE9Thb5Yf3gyno71pyqPTVl/6IQuh4SUz7rkgwF/aVHEnr4aUYNoc0PEzd2Me0jElsA3GAneq1I/wngutOWgTViTK4Nptr5uIzMVQs9H1rOMJNorP8b02t1NDu010rSsib9GaaJJq4r4iy46laQOxWoU0ex26arYnk+jw4833WSCTVBIprTgizZ+fKjoY0xwXvI2oOvGNEUCtGFvKFORTaQrlaXZIg1toa2BBVNicyONbwnI3KIu3MgGJ2SlCVXJn8oHFppVHFCdwgN1uDzGiKAhjvr0sZTUtXin2f2CszPTbbo=|fUhbVKrr8CSKE7TZJneXpDGraj5YhRrq9ESo206S+BY=", + }; + } +} + +public static class OrgnaizationExtensions +{ + public static OrganizationUser CreateOrganizationUser(this Organization organization, User user) + { + return new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + UserId = user.Id, + + Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", + Type = OrganizationUserType.Admin, + Status = OrganizationUserStatusType.Confirmed + }; + } +} diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs new file mode 100644 index 0000000000..90cadf0b78 --- /dev/null +++ b/util/Seeder/Factories/UserSeeder.cs @@ -0,0 +1,25 @@ +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class UserSeeder +{ + public static User CreateUser(string email) + { + return new User + { + Id = Guid.NewGuid(), + Email = email, + MasterPassword = "AQAAAAIAAYagAAAAEBATmF66OHMpHuHKc1CsGZQ1ltHUHyhYK+7e4re3bVFi16SOpLpDfzdFswnvFQs2Rg==", + SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", + Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", + PrivateKey = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=", + ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", + + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 600_000, + }; + } +} diff --git a/util/Seeder/README.md b/util/Seeder/README.md new file mode 100644 index 0000000000..8597ad6e39 --- /dev/null +++ b/util/Seeder/README.md @@ -0,0 +1,18 @@ +# Bitwarden Database Seeder + +A class library for generating and inserting test data. + +## Project Structure + +The project is organized into these main components: + +### Factories + +Factories are helper classes for creating domain entities and populating them with realistic data. This assist in +decreasing the amount of boilerplate code needed to create test data in recipes. + +### Recipes + +Recipes are pre-defined data sets which can be run to generate and load data into the database. They often allow a allow +for a few arguments to customize the data slightly. Recipes should be kept simple and focused on a single task. Default +to creating more recipes rather than adding complexity to existing ones. diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs new file mode 100644 index 0000000000..fb06c091ae --- /dev/null +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -0,0 +1,37 @@ +using Bit.Infrastructure.EntityFramework.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Factories; +using LinqToDB.EntityFrameworkCore; + +namespace Bit.Seeder.Recipes; + +public class OrganizationWithUsersRecipe(DatabaseContext db) +{ + public Guid Seed(string name, int users, string domain) + { + var organization = OrganizationSeeder.CreateEnterprise(name, domain, users); + var user = UserSeeder.CreateUser($"admin@{domain}"); + var orgUser = organization.CreateOrganizationUser(user); + + var additionalUsers = new List(); + var additionalOrgUsers = new List(); + for (var i = 0; i < users; i++) + { + var additionalUser = UserSeeder.CreateUser($"user{i}@{domain}"); + additionalUsers.Add(additionalUser); + additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); + } + + db.Add(organization); + db.Add(user); + db.Add(orgUser); + + db.SaveChanges(); + + // Use LinqToDB's BulkCopy for significant better performance + db.BulkCopy(additionalUsers); + db.BulkCopy(additionalOrgUsers); + + return organization.Id; + } +} diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj new file mode 100644 index 0000000000..392f6434cc --- /dev/null +++ b/util/Seeder/Seeder.csproj @@ -0,0 +1,29 @@ + + + + + net8.0 + enable + enable + Bit.Seeder + Bit.Seeder + Core library for generating and managing test data for Bitwarden + library + false + + + + + + + + + + + + + + + + +