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; + } +}