diff --git a/bitwarden-server.sln b/bitwarden-server.sln
index 154ffe36149a..b1e9b0d9be16 100644
--- a/bitwarden-server.sln
+++ b/bitwarden-server.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
@@ -124,6 +124,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsProcessor.Test", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\Notifications.Test\Notifications.Test.csproj", "{90D85D8F-5577-4570-A96E-5A2E185F0F6F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBSeeder", "util\DBSeeder\DBSeeder.csproj", "{29318B0D-E753-4A27-BFBC-F7566FE26E2F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFDBSeederUtility", "util\EFDBSeederUtility\EFDBSeederUtility.csproj", "{47F03C4D-C178-4F9A-99B1-C7E35342D8BC}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -308,6 +312,14 @@ Global
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -357,6 +369,8 @@ Global
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
+ {29318B0D-E753-4A27-BFBC-F7566FE26E2F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
+ {47F03C4D-C178-4F9A-99B1-C7E35342D8BC} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
diff --git a/util/DBSeeder/DBSeeder.csproj b/util/DBSeeder/DBSeeder.csproj
new file mode 100644
index 000000000000..0b14f1b3943b
--- /dev/null
+++ b/util/DBSeeder/DBSeeder.csproj
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/util/DBSeeder/EFDBSeeder.cs b/util/DBSeeder/EFDBSeeder.cs
new file mode 100644
index 000000000000..ecad691edc95
--- /dev/null
+++ b/util/DBSeeder/EFDBSeeder.cs
@@ -0,0 +1,185 @@
+using System.Text.Json;
+using Bit.Core.Vault.Enums; // Change this line
+using Bit.Infrastructure.EntityFramework.Repositories;
+using Bit.Infrastructure.EntityFramework.Vault.Models;
+using Bogus;
+using Microsoft.Extensions.Logging;
+
+
+
+namespace Bit.DBSeeder;
+
+public class EFDBSeeder
+{
+ private readonly string _connectionString;
+ private readonly string _databaseProvider;
+ private readonly ILogger _logger;
+
+ public EFDBSeeder(string connectionString, string databaseProvider)
+ {
+ _connectionString = connectionString;
+ _databaseProvider = databaseProvider;
+
+ }
+
+ public bool SeedDatabase()
+ {
+ //print connectionstring to console
+ Console.WriteLine(_connectionString);
+ Console.WriteLine(_databaseProvider);
+
+ try
+ {
+ var factory = new DatabaseContextFactory();
+ using (var context = factory.CreateDbContext(new[] { _connectionString }))
+ {
+ if (context.Database.CanConnect())
+ {
+ Console.WriteLine("Successfully connected to the database!");
+
+ // Seed the database
+ SeedUsers(context);
+ SeedCiphers(context);
+
+ Console.WriteLine("Database seeded successfully!");
+ }
+ else
+ {
+ Console.WriteLine("Failed to connect to the database.");
+ return false;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error adding users: {ex.Message}");
+ if (ex.InnerException != null)
+ {
+ Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
+ }
+ throw; // Re-throw the exception to stop the seeding process
+ }
+
+
+
+ return true;
+ }
+
+ private void SeedUsers(DatabaseContext context)
+ {
+ if (!context.Users.Any())
+ {
+ Console.WriteLine("Generating 5000 users...");
+
+ var faker = new Faker()
+ .RuleFor(u => u.Id, f => Guid.NewGuid())
+ .RuleFor(u => u.Name, f => f.Name.FullName())
+ .RuleFor(u => u.Email, (f, u) => f.Internet.Email(u.Name))
+ .RuleFor(u => u.EmailVerified, f => f.Random.Bool(0.9f))
+ .RuleFor(u => u.SecurityStamp, f => Guid.NewGuid().ToString())
+ .RuleFor(u => u.ApiKey, f => Guid.NewGuid().ToString("N").Substring(0, 30))
+ .RuleFor(u => u.CreationDate, f => f.Date.Past(2))
+ .RuleFor(u => u.RevisionDate, (f, u) => f.Date.Between(u.CreationDate, DateTime.UtcNow));
+
+ var users = faker.Generate(5000);
+
+ const int batchSize = 100;
+ for (int i = 0; i < users.Count; i += batchSize)
+ {
+ context.Users.AddRange(users.Skip(i).Take(batchSize));
+ context.SaveChanges();
+ Console.WriteLine($"Added {Math.Min(i + batchSize, users.Count)} users");
+ }
+
+ Console.WriteLine("5000 test users added to the database.");
+ }
+ else
+ {
+ Console.WriteLine("Users table is not empty. Skipping user seeding.");
+ }
+ }
+
+ private void SeedCiphers(DatabaseContext context)
+ {
+ if (!context.Ciphers.Any())
+ {
+ var users = context.Users.ToList();
+ if (!users.Any())
+ {
+ Console.WriteLine("No users found. Please seed users first.");
+ return;
+ }
+
+ Console.WriteLine($"Generating ciphers for {users.Count} users...");
+
+ var faker = new Faker()
+ .RuleFor(c => c.Id, f => Guid.NewGuid())
+ .RuleFor(c => c.Type, f => CipherType.Login)
+ .RuleFor(c => c.Data, f => JsonSerializer.Serialize(new
+ {
+ Name = f.Internet.DomainName(),
+ Notes = f.Lorem.Sentence(),
+ Login = new
+ {
+ Username = f.Internet.UserName(),
+ Password = f.Internet.Password(),
+ Uri = f.Internet.Url()
+ }
+ }))
+ .RuleFor(c => c.CreationDate, f => f.Date.Past(1))
+ .RuleFor(c => c.RevisionDate, (f, c) => f.Date.Between(c.CreationDate, DateTime.UtcNow))
+ .RuleFor(c => c.DeletedDate, f => null)
+ .RuleFor(c => c.Reprompt, f => CipherRepromptType.None);
+
+ const int batchSize = 100;
+ for (int i = 0; i < users.Count; i += batchSize)
+ {
+ var userBatch = users.Skip(i).Take(batchSize);
+ var ciphers = userBatch.Select(user =>
+ {
+ var cipher = faker.Generate();
+ cipher.UserId = user.Id;
+ return cipher;
+ }).ToList();
+
+ try
+ {
+ context.Ciphers.AddRange(ciphers);
+ context.SaveChanges();
+ Console.WriteLine($"Added ciphers for users {i + 1} to {Math.Min(i + batchSize, users.Count)}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error adding ciphers: {ex.Message}");
+ if (ex.InnerException != null)
+ {
+ Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
+ }
+ throw; // Re-throw the exception to stop the seeding process
+ }
+ }
+
+ Console.WriteLine($"Ciphers added for all {users.Count} users.");
+ }
+ else
+ {
+ Console.WriteLine("Ciphers table is not empty. Skipping cipher seeding.");
+ }
+ }
+
+ /* private ILogger CreateLogger()
+ {
+ var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder
+ .AddFilter("Microsoft", LogLevel.Warning)
+ .AddFilter("System", LogLevel.Warning)
+ .AddConsole();
+
+ builder.AddFilter("EFDBSeeder.EFDBSeeder", LogLevel.Information);
+ });
+
+ return loggerFactory.CreateLogger();
+ }
+ */
+}
diff --git a/util/DBSeeder/Factories.cs b/util/DBSeeder/Factories.cs
new file mode 100644
index 000000000000..64caebd01086
--- /dev/null
+++ b/util/DBSeeder/Factories.cs
@@ -0,0 +1,28 @@
+using Bit.Infrastructure.EntityFramework.Repositories;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Bit.DBSeeder;
+
+
+public class DatabaseContextFactory : IDesignTimeDbContextFactory
+{
+ public DatabaseContext CreateDbContext(string[] args)
+ {
+ if (args.Length == 0 || string.IsNullOrWhiteSpace(args[0]))
+ {
+ throw new ArgumentException("Connection string must be provided as the first argument.");
+ }
+
+ var connectionString = args[0];
+
+ var services = new ServiceCollection();
+ services.AddDataProtection();
+ services.AddDbContext(options =>
+ options.UseSqlServer(connectionString));
+ var serviceProvider = services.BuildServiceProvider();
+
+ return serviceProvider.GetRequiredService();
+ }
+}
diff --git a/util/EFDBSeederUtility/EFDBSeederUtility.csproj b/util/EFDBSeederUtility/EFDBSeederUtility.csproj
new file mode 100644
index 000000000000..90ad68b26711
--- /dev/null
+++ b/util/EFDBSeederUtility/EFDBSeederUtility.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/util/EFDBSeederUtility/Program.cs b/util/EFDBSeederUtility/Program.cs
new file mode 100644
index 000000000000..60c08f756396
--- /dev/null
+++ b/util/EFDBSeederUtility/Program.cs
@@ -0,0 +1,29 @@
+using Bit.DBSeeder;
+using CommandDotNet;
+
+internal class Program
+{
+ private static int Main(string[] args)
+ {
+ return new AppRunner().Run(args);
+ }
+
+ [DefaultCommand]
+ public void Execute(
+
+ [Operand(Description = "Database provider (mssql, mysql, postgres, sqlite).")] string databaseProvider,
+ [Operand(Description = "Database connection string.")] string ConnectionString
+
+ ) => SeedDatabase(ConnectionString, databaseProvider);
+
+ private static bool SeedDatabase(string databaseConnectionString,
+ string databaseProvider)
+ {
+ var seeder = new EFDBSeeder(databaseConnectionString, databaseProvider);
+ bool success;
+
+ success = seeder.SeedDatabase(); // Change this line
+
+ return success;
+ }
+}