diff --git a/util/DbSeederUtility/DbSeederUtility.csproj b/util/DbSeederUtility/DbSeederUtility.csproj new file mode 100644 index 0000000000..ab3ac933c6 --- /dev/null +++ b/util/DbSeederUtility/DbSeederUtility.csproj @@ -0,0 +1,29 @@ + + + + Exe + net8.0 + enable + enable + Bit.DbSeederUtility + DbSeeder + true + + + + + + + + + + + + + + + PreserveNewest + + + + \ No newline at end of file diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs new file mode 100644 index 0000000000..6f3552da19 --- /dev/null +++ b/util/DbSeederUtility/Program.cs @@ -0,0 +1,83 @@ +using CommandDotNet; +using Bit.Seeder.Commands; +using Bit.Seeder.Settings; + +namespace Bit.DbSeederUtility; + +public class Program +{ + private static int Main(string[] args) + { + // Ensure global settings are loaded + var globalSettings = GlobalSettingsFactory.GlobalSettings; + + // Set the current directory to the seeder directory for consistent seed paths + var seederDirectory = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "seeder")); + Directory.SetCurrentDirectory(seederDirectory); + + return new AppRunner() + .Run(args); + } + + [Command("generate", Description = "Generate seed data as JSON files")] + public int Generate( + [Option('u', "users", Description = "Number of users to generate")] + int users, + + [Option('c', "ciphers-per-user", Description = "Number of ciphers per user to generate")] + int ciphersPerUser, + + [Option('n', "seed-name", Description = "Name for the seed data files")] + string seedName + ) + { + // Execute the generate command + var generateCommand = new GenerateCommand(); + return generateCommand.Execute(users, ciphersPerUser, seedName, false) ? 0 : 1; + } + + [Command("load", Description = "Load seed data from JSON files into the database")] + public int Load( + [Option('n', "seed-name", Description = "Name of the seed data to load")] + string seedName, + + [Option('t', "timestamp", Description = "Specific timestamp of the seed data to load (defaults to most recent)")] + string? timestamp = null, + + [Option('d', "dry-run", Description = "Validate the seed data without actually loading it")] + bool dryRun = false + ) + { + // Execute the load command + var loadCommand = new LoadCommand(); + return loadCommand.Execute(seedName, timestamp, dryRun) ? 0 : 1; + } + + [Command("generate-direct-load", Description = "Generate seed data and load it directly into the database without creating JSON files")] + public int GenerateDirectLoad( + [Option('u', "users", Description = "Number of users to generate")] + int users, + + [Option('c', "ciphers-per-user", Description = "Number of ciphers per user to generate")] + int ciphersPerUser, + + [Option('n', "seed-name", Description = "Name identifier for this seed operation")] + string seedName + ) + { + // Execute the generate command with loadImmediately=true + var generateCommand = new GenerateCommand(); + return generateCommand.Execute(users, ciphersPerUser, seedName, true) ? 0 : 1; + } + + [Command("extract", Description = "Extract data from the database into seed files")] + public int Extract( + [Option('n', "seed-name", Description = "Name for the extracted seed")] + string seedName + ) + { + // Execute the extract command + var extractCommand = new ExtractCommand(); + return extractCommand.Execute(seedName) ? 0 : 1; + } +} \ No newline at end of file diff --git a/util/seeder/Commands/ExtractCommand.cs b/util/seeder/Commands/ExtractCommand.cs new file mode 100644 index 0000000000..7cf951d24e --- /dev/null +++ b/util/seeder/Commands/ExtractCommand.cs @@ -0,0 +1,119 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Bit.Seeder.Services; +using Bit.Seeder.Settings; +using Microsoft.AspNetCore.DataProtection; +using Bit.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.EntityFrameworkCore; +using Bit.Core.Enums; + +namespace Bit.Seeder.Commands +{ + public class ExtractCommand + { + public bool Execute(string seedName) + { + try + { + // Create service provider with necessary services + var services = new ServiceCollection(); + ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + // Get the necessary services + var seederService = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetRequiredService>(); + + logger.LogInformation($"Extracting data from database to seed: {seedName}"); + + // Execute the extract operation + seederService.ExtractSeedsAsync(seedName).GetAwaiter().GetResult(); + + logger.LogInformation("Seed extraction completed successfully"); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Error extracting seeds: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"Inner exception: {ex.InnerException.Message}"); + } + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + return false; + } + } + + private void ConfigureServices(ServiceCollection services) + { + // Add logging + services.AddLogging(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Information); + }); + + // Add JSON file configuration for other app settings not in GlobalSettings + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets("Bit.Seeder") + .Build(); + + services.AddSingleton(configuration); + + // Get global settings + var globalSettings = GlobalSettingsFactory.GlobalSettings; + services.AddSingleton(globalSettings); + + // Configure database provider + var provider = globalSettings.DatabaseProvider.ToLowerInvariant() switch + { + "postgres" or "postgresql" => SupportedDatabaseProviders.Postgres, + "mysql" or "mariadb" => SupportedDatabaseProviders.MySql, + "sqlite" => SupportedDatabaseProviders.Sqlite, + _ => SupportedDatabaseProviders.SqlServer + }; + + var connectionString = provider switch + { + SupportedDatabaseProviders.Postgres => globalSettings.PostgreSql?.ConnectionString, + SupportedDatabaseProviders.MySql => globalSettings.MySql?.ConnectionString, + SupportedDatabaseProviders.Sqlite => globalSettings.Sqlite?.ConnectionString, + _ => globalSettings.SqlServer?.ConnectionString + }; + + // Register database context + services.AddDbContext(options => + { + switch (provider) + { + case SupportedDatabaseProviders.Postgres: + options.UseNpgsql(connectionString); + break; + case SupportedDatabaseProviders.MySql: + options.UseMySql(connectionString!, ServerVersion.AutoDetect(connectionString!)); + break; + case SupportedDatabaseProviders.Sqlite: + options.UseSqlite(connectionString!); + break; + default: + options.UseSqlServer(connectionString!); + break; + } + }); + + // Add Data Protection services + services.AddDataProtection() + .SetApplicationName("Bitwarden"); + + // Register other services + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + } +} \ No newline at end of file diff --git a/util/seeder/Commands/GenerateCommand.cs b/util/seeder/Commands/GenerateCommand.cs new file mode 100644 index 0000000000..9c67efb202 --- /dev/null +++ b/util/seeder/Commands/GenerateCommand.cs @@ -0,0 +1,77 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Bit.Seeder.Services; +using Bit.Seeder.Settings; +using Microsoft.AspNetCore.DataProtection; +using Bit.Core; + +namespace Bit.Seeder.Commands +{ + public class GenerateCommand + { + public bool Execute(int users, int ciphersPerUser, string seedName, bool loadImmediately = false) + { + try + { + // Create service provider with necessary services + var services = new ServiceCollection(); + ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + // Get the seeder service + var seederService = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetRequiredService>(); + + logger.LogInformation($"Generating seeds for {users} users with {ciphersPerUser} ciphers per user"); + logger.LogInformation($"Seed name: {seedName}"); + + // Execute the appropriate action based on whether we need to load immediately + if (loadImmediately) + { + // Generate and load directly without creating intermediate files + seederService.GenerateAndLoadSeedsAsync(users, ciphersPerUser, seedName).GetAwaiter().GetResult(); + logger.LogInformation("Seeds generated and loaded directly to database"); + } + else + { + // Only generate seeds and save to files + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + seederService.GenerateSeedsAsync(users, ciphersPerUser, seedName).GetAwaiter().GetResult(); + logger.LogInformation("Seed generation completed successfully"); + } + + return true; + } + catch (Exception ex) + { + Console.WriteLine($"Error generating seeds: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"Inner exception: {ex.InnerException.Message}"); + } + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + return false; + } + } + + private 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"); + + // Register DatabaseContext and services + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddDbContext(); + } + } +} \ No newline at end of file diff --git a/util/seeder/Commands/LoadCommand.cs b/util/seeder/Commands/LoadCommand.cs new file mode 100644 index 0000000000..5664046de3 --- /dev/null +++ b/util/seeder/Commands/LoadCommand.cs @@ -0,0 +1,87 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Bit.Seeder.Services; +using Bit.Seeder.Settings; +using Microsoft.AspNetCore.DataProtection; +using Bit.Core; + +namespace Bit.Seeder.Commands +{ + public class LoadCommand + { + public bool Execute(string seedName, string? timestamp = null, bool dryRun = false) + { + try + { + // Create service provider with necessary services + var services = new ServiceCollection(); + ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + // Get the necessary services + var seederService = serviceProvider.GetRequiredService(); + var databaseService = serviceProvider.GetRequiredService(); + var logger = serviceProvider.GetRequiredService>(); + + logger.LogInformation($"Loading seeds named: {seedName}"); + if (!string.IsNullOrEmpty(timestamp)) + { + logger.LogInformation($"Using specific timestamp: {timestamp}"); + } + + if (dryRun) + { + logger.LogInformation("DRY RUN: No actual changes will be made to the database"); + // Perform validation here if needed + logger.LogInformation("Seed loading validation completed"); + return true; + } + + // Execute the load operation + seederService.LoadSeedsAsync(seedName, timestamp).GetAwaiter().GetResult(); + + logger.LogInformation("Seed loading completed successfully"); + return true; + } + catch (Exception ex) + { + // Display the full exception details + Console.WriteLine($"Error loading seeds: {ex.Message}"); + + if (ex.InnerException != null) + { + Console.WriteLine($"Inner exception: {ex.InnerException.Message}"); + + if (ex.InnerException.InnerException != null) + { + Console.WriteLine($"Inner inner exception: {ex.InnerException.InnerException.Message}"); + } + } + + Console.WriteLine($"Stack trace: {ex.StackTrace}"); + + return false; + } + } + + private 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"); + + // Register DatabaseContext and services + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddDbContext(); + } + } +} \ No newline at end of file diff --git a/util/seeder/DataProtection-Keys/key-c9676c44-16a7-4b38-b750-4ea5443f1b87.xml b/util/seeder/DataProtection-Keys/key-c9676c44-16a7-4b38-b750-4ea5443f1b87.xml new file mode 100644 index 0000000000..9d94531cbf --- /dev/null +++ b/util/seeder/DataProtection-Keys/key-c9676c44-16a7-4b38-b750-4ea5443f1b87.xml @@ -0,0 +1,16 @@ + + + 2025-03-13T17:13:50.8174933Z + 2025-03-13T17:13:50.8130955Z + 2025-06-11T17:13:50.8130955Z + + + + + + + VCxxS1xKEWQ9+XRXgMchHML7POXcwRkCHswj7JMjjN36RkERENo+ky/1mazB4RZ6BjjwXsAyjhSz2eGts/0BnQ== + + + + \ No newline at end of file diff --git a/util/seeder/README.md b/util/seeder/README.md new file mode 100644 index 0000000000..e6a9094d1e --- /dev/null +++ b/util/seeder/README.md @@ -0,0 +1,98 @@ +# Bitwarden Database Seeder + +The Bitwarden Database Seeder utility is a tool for generating and loading test data for Bitwarden databases. + +## Features + +- Generate random user accounts with associated ciphers (passwords, secure notes, etc.) +- Load generated seeds into the database +- Configure database connections using the same configuration approach as the Bitwarden Migrator + +## Usage + +The seeder utility can be run in two ways: + +1. Using `dotnet run` from the source: + ``` + cd util/DbSeederUtility + dotnet run -- generate-seeds --users 10 --ciphers 5 --output ./seeds + dotnet run -- load-seeds --path ./seeds + ``` + +2. As a standalone executable: + ``` + DbSeeder.exe generate-seeds --users 10 --ciphers 5 --output ./seeds + DbSeeder.exe load-seeds --path ./seeds + ``` + +## Commands + +### Generate Seeds + +Generates random seed data for users and ciphers. + +``` +DbSeeder.exe generate-seeds [options] + +Options: + -u, --users Number of users to generate (default: 10) + -c, --ciphers Number of ciphers per user (default: 5) + -o, --output Output directory for seed files (default: seeds) +``` + +### Load Seeds + +Loads generated seed data into the database. + +``` +DbSeeder.exe load-seeds [options] + +Options: + -p, --path Path to the directory containing seed data + --dry-run Preview the operation without making changes +``` + +## Configuration + +The utility uses the same configuration approach as other Bitwarden utilities: + +1. **User Secrets** - For local development +2. **appsettings.json** - For general settings +3. **Environment variables** - For deployment environments + +### Database Configuration + +Configure the database connection in user secrets or appsettings.json: + +```json +{ + "globalSettings": { + "databaseProvider": "postgresql", // or "sqlserver", "mysql", "sqlite" + "postgreSql": { + "connectionString": "Host=localhost;Port=5432;Database=vault_dev;Username=postgres;Password=YOURPASSWORD;Include Error Detail=true" + }, + "sqlServer": { + "connectionString": "Data Source=localhost;Initial Catalog=vault_dev;Integrated Security=SSPI;MultipleActiveResultSets=true" + } + } +} +``` + +## Building the Executable + +To build the standalone executable: + +``` +cd util +.\publish-seeder.ps1 +``` + +This will create the executable in the `util/Seeder/publish/win-x64` directory. You can also specify a different runtime: + +``` +.\publish-seeder.ps1 -runtime linux-x64 +``` + +## Integration with Bitwarden Server + +The seeder utility is designed to work seamlessly with Bitwarden Server. It uses the same database models and configuration approach as the server, ensuring compatibility with the core repository. \ No newline at end of file diff --git a/util/seeder/Seeder.csproj b/util/seeder/Seeder.csproj new file mode 100644 index 0000000000..a7acb3acf7 --- /dev/null +++ b/util/seeder/Seeder.csproj @@ -0,0 +1,43 @@ + + + + + net8.0 + enable + enable + Bit.Seeder + Bit.Seeder + Core library for generating and managing test data for Bitwarden + library + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/util/seeder/Services/DatabaseContext.cs b/util/seeder/Services/DatabaseContext.cs new file mode 100644 index 0000000000..b44304fecd --- /dev/null +++ b/util/seeder/Services/DatabaseContext.cs @@ -0,0 +1,76 @@ +using Microsoft.EntityFrameworkCore; +using Bit.Core.Entities; +using Bit.Core.Vault.Entities; +using Bit.Core.Enums; +using Bit.Seeder.Settings; + +namespace Bit.Seeder.Services; + +public class DatabaseContext : DbContext +{ + private readonly GlobalSettings _globalSettings; + + public DatabaseContext(GlobalSettings globalSettings) + { + _globalSettings = globalSettings; + } + + public DbSet Users { get; set; } + public DbSet Ciphers { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + var provider = _globalSettings.DatabaseProvider ?? string.Empty; + Console.WriteLine($"Database Provider: '{provider}'"); + + // Output all available connection strings for debugging + Console.WriteLine($"SqlServer ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.SqlServer?.ConnectionString)}"); + Console.WriteLine($"PostgreSql ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.PostgreSql?.ConnectionString)}"); + Console.WriteLine($"MySql ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.MySql?.ConnectionString)}"); + Console.WriteLine($"Sqlite ConnectionString available: {!string.IsNullOrEmpty(_globalSettings.Sqlite?.ConnectionString)}"); + + var connectionString = _globalSettings.DatabaseProvider switch + { + "postgres" => _globalSettings.PostgreSql?.ConnectionString, + "mysql" => _globalSettings.MySql?.ConnectionString, + "sqlite" => _globalSettings.Sqlite?.ConnectionString, + _ => _globalSettings.SqlServer?.ConnectionString + }; + + Console.WriteLine($"Using connection string: {connectionString}"); + + switch (_globalSettings.DatabaseProvider) + { + case "postgres": + optionsBuilder.UseNpgsql(connectionString); + break; + case "mysql": + optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)); + break; + case "sqlite": + optionsBuilder.UseSqlite(connectionString); + break; + default: + optionsBuilder.UseSqlServer(connectionString); + break; + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.ToTable("Cipher"); + entity.Property(e => e.Type).HasConversion(); + entity.Property(e => e.Reprompt).HasConversion(); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("User"); + entity.Property(e => e.Kdf).HasConversion(); + }); + } +} \ No newline at end of file diff --git a/util/seeder/Services/DatabaseService.cs b/util/seeder/Services/DatabaseService.cs new file mode 100644 index 0000000000..dccc6ab6bc --- /dev/null +++ b/util/seeder/Services/DatabaseService.cs @@ -0,0 +1,85 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Bit.Core.Entities; +using Bit.Core.Vault.Entities; + +namespace Bit.Seeder.Services; + +public class DatabaseService : IDatabaseService +{ + private readonly DatabaseContext _dbContext; + private readonly ILogger _logger; + + public DatabaseService(DatabaseContext dbContext, ILogger logger) + { + _dbContext = dbContext; + _logger = logger; + } + + public async Task ClearDatabaseAsync() + { + _logger.LogInformation("Clearing database..."); + + var ciphers = await _dbContext.Ciphers.ToListAsync(); + _dbContext.Ciphers.RemoveRange(ciphers); + + var users = await _dbContext.Users.ToListAsync(); + _dbContext.Users.RemoveRange(users); + + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation("Database cleared successfully."); + } + + public async Task SaveUsersAsync(IEnumerable users) + { + _logger.LogInformation("Saving users to database..."); + + foreach (var user in users) + { + await _dbContext.Users.AddAsync(user); + } + + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation($"Successfully saved {users.Count()} users to database."); + } + + public async Task SaveCiphersAsync(IEnumerable ciphers) + { + _logger.LogInformation("Saving ciphers to database..."); + + foreach (var cipher in ciphers) + { + await _dbContext.Ciphers.AddAsync(cipher); + } + + await _dbContext.SaveChangesAsync(); + + _logger.LogInformation($"Successfully saved {ciphers.Count()} ciphers to database."); + } + + public async Task> GetUsersAsync() + { + _logger.LogInformation("Retrieving all users from database..."); + var users = await _dbContext.Users.ToListAsync(); + _logger.LogInformation($"Successfully retrieved {users.Count} users from database."); + return users; + } + + public async Task> GetCiphersAsync() + { + _logger.LogInformation("Retrieving all ciphers from database..."); + var ciphers = await _dbContext.Ciphers.ToListAsync(); + _logger.LogInformation($"Successfully retrieved {ciphers.Count} ciphers from database."); + return ciphers; + } + + public async Task> GetCiphersByUserIdAsync(Guid userId) + { + _logger.LogInformation($"Retrieving ciphers for user {userId} from database..."); + var ciphers = await _dbContext.Ciphers.Where(c => c.UserId == userId).ToListAsync(); + _logger.LogInformation($"Successfully retrieved {ciphers.Count} ciphers for user {userId}."); + return ciphers; + } +} diff --git a/util/seeder/Services/EncryptionService.cs b/util/seeder/Services/EncryptionService.cs new file mode 100644 index 0000000000..9fc195de99 --- /dev/null +++ b/util/seeder/Services/EncryptionService.cs @@ -0,0 +1,66 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.DataProtection; +using Bit.Core; + +namespace Bit.Seeder.Services; + +public class EncryptionService : IEncryptionService +{ + private readonly ILogger _logger; + private readonly IDataProtector _dataProtector; + + public EncryptionService( + ILogger logger, + IDataProtectionProvider dataProtectionProvider) + { + _logger = logger; + _dataProtector = dataProtectionProvider.CreateProtector(Constants.DatabaseFieldProtectorPurpose); + } + + public string HashPassword(string password) + { + _logger.LogDebug("Hashing password using Data Protection"); + + // The real Bitwarden implementation uses BCrypt first and then protects that value + // For simplicity we're just protecting the raw password since this is only for seeding test data + var protectedPassword = _dataProtector.Protect(password); + + // Prefix with "P|" to match Bitwarden's password format + return string.Concat(Constants.DatabaseFieldProtectedPrefix, protectedPassword); + } + + public byte[] DeriveKey(string password, string salt) + { + _logger.LogDebug("Deriving key"); + + using var pbkdf2 = new Rfc2898DeriveBytes( + Encoding.UTF8.GetBytes(password), + Encoding.UTF8.GetBytes(salt), + 100000, + HashAlgorithmName.SHA256); + + return pbkdf2.GetBytes(32); + } + + public string EncryptString(string plaintext, byte[] key) + { + _logger.LogDebug("Encrypting string"); + + using var aes = Aes.Create(); + aes.Key = key; + aes.GenerateIV(); + + using var encryptor = aes.CreateEncryptor(); + var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); + + var cipherBytes = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length); + + var result = new byte[aes.IV.Length + cipherBytes.Length]; + Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length); + Buffer.BlockCopy(cipherBytes, 0, result, aes.IV.Length, cipherBytes.Length); + + return Convert.ToBase64String(result); + } +} \ No newline at end of file diff --git a/util/seeder/Services/IDatabaseService.cs b/util/seeder/Services/IDatabaseService.cs new file mode 100644 index 0000000000..4755bfad1b --- /dev/null +++ b/util/seeder/Services/IDatabaseService.cs @@ -0,0 +1,14 @@ +using Bit.Core.Entities; +using Bit.Core.Vault.Entities; + +namespace Bit.Seeder.Services; + +public interface IDatabaseService +{ + Task ClearDatabaseAsync(); + Task SaveUsersAsync(IEnumerable users); + Task SaveCiphersAsync(IEnumerable ciphers); + Task> GetUsersAsync(); + Task> GetCiphersAsync(); + Task> GetCiphersByUserIdAsync(Guid userId); +} diff --git a/util/seeder/Services/IDatabaseService.cs.bak b/util/seeder/Services/IDatabaseService.cs.bak new file mode 100644 index 0000000000..d312322442 --- /dev/null +++ b/util/seeder/Services/IDatabaseService.cs.bak @@ -0,0 +1,11 @@ +using Bit.Core.Entities; +using Bit.Core.Vault.Entities; + +namespace Bit.Seeder.Services; + +public interface IDatabaseService +{ + Task ClearDatabaseAsync(); + Task SaveUsersAsync(IEnumerable users); + Task SaveCiphersAsync(IEnumerable ciphers); +} \ No newline at end of file diff --git a/util/seeder/Services/IEncryptionService.cs b/util/seeder/Services/IEncryptionService.cs new file mode 100644 index 0000000000..8a29568569 --- /dev/null +++ b/util/seeder/Services/IEncryptionService.cs @@ -0,0 +1,8 @@ +namespace Bit.Seeder.Services; + +public interface IEncryptionService +{ + string HashPassword(string password); + byte[] DeriveKey(string password, string salt); + string EncryptString(string plaintext, byte[] key); +} \ No newline at end of file diff --git a/util/seeder/Services/ISeederService.cs b/util/seeder/Services/ISeederService.cs new file mode 100644 index 0000000000..1a8370c046 --- /dev/null +++ b/util/seeder/Services/ISeederService.cs @@ -0,0 +1,9 @@ +namespace Bit.Seeder.Services; + +public interface ISeederService +{ + Task GenerateSeedsAsync(int userCount, int ciphersPerUser, string seedName); + Task LoadSeedsAsync(string seedName, string? timestamp = null); + Task GenerateAndLoadSeedsAsync(int userCount, int ciphersPerUser, string seedName); + Task ExtractSeedsAsync(string seedName); +} diff --git a/util/seeder/Services/SeederService.cs b/util/seeder/Services/SeederService.cs new file mode 100644 index 0000000000..8efc10e44a --- /dev/null +++ b/util/seeder/Services/SeederService.cs @@ -0,0 +1,450 @@ +using System.Text.Json; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bogus; +using Microsoft.Extensions.Logging; + +namespace Bit.Seeder.Services; + +public class SeederService : ISeederService +{ + private readonly IEncryptionService _encryptionService; + private readonly IDatabaseService _databaseService; + private readonly ILogger _logger; + private readonly Faker _faker; + private readonly string _defaultPassword = "password"; + + public SeederService( + IEncryptionService encryptionService, + IDatabaseService databaseService, + ILogger logger) + { + _encryptionService = encryptionService; + _databaseService = databaseService; + _logger = logger; + _faker = new Faker(); + + // Set the random seed to ensure reproducible data + Randomizer.Seed = new Random(42); + } + + public async Task GenerateSeedsAsync(int userCount, int ciphersPerUser, string outputName) + { + _logger.LogInformation("Generating seeds: {UserCount} users with {CiphersPerUser} ciphers each", userCount, ciphersPerUser); + + // Create timestamped folder under a named folder in seeds directory + var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds"); + Directory.CreateDirectory(seedsBaseDir); + + var namedDir = Path.Combine(seedsBaseDir, outputName); + Directory.CreateDirectory(namedDir); + + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var outputDir = Path.Combine(namedDir, timestamp); + Directory.CreateDirectory(outputDir); + + // Create users and ciphers subdirectories + Directory.CreateDirectory(Path.Combine(outputDir, "users")); + Directory.CreateDirectory(Path.Combine(outputDir, "ciphers")); + + _logger.LogInformation("Seed output directory: {OutputDir}", outputDir); + + // Generate users + var users = GenerateUsers(userCount); + + // Generate ciphers for each user + var allCiphers = new List(); + foreach (var user in users) + { + var ciphers = GenerateCiphers(user, ciphersPerUser); + allCiphers.AddRange(ciphers); + + // Save each user's ciphers to a file + var cipherFilePath = Path.Combine(outputDir, "ciphers", $"{user.Id}.json"); + await File.WriteAllTextAsync(cipherFilePath, JsonSerializer.Serialize(ciphers, new JsonSerializerOptions + { + WriteIndented = true + })); + } + + // Save users to a file + var userFilePath = Path.Combine(outputDir, "users", "users.json"); + await File.WriteAllTextAsync(userFilePath, JsonSerializer.Serialize(users, new JsonSerializerOptions + { + WriteIndented = true + })); + + _logger.LogInformation("Successfully generated {UserCount} users and {CipherCount} ciphers", users.Count, allCiphers.Count); + _logger.LogInformation("Seed data saved to directory: {OutputDir}", outputDir); + } + + public async Task GenerateAndLoadSeedsAsync(int userCount, int ciphersPerUser, string seedName) + { + _logger.LogInformation("Generating and loading seeds directly: {UserCount} users with {CiphersPerUser} ciphers each", + userCount, ciphersPerUser); + + // Generate users directly without saving to files + var users = GenerateUsers(userCount); + + // Clear the database first + await _databaseService.ClearDatabaseAsync(); + + // Save users to database + await _databaseService.SaveUsersAsync(users); + _logger.LogInformation("Saved {UserCount} users directly to database", users.Count); + + // Generate and save ciphers for each user + int totalCiphers = 0; + foreach (var user in users) + { + var ciphers = GenerateCiphers(user, ciphersPerUser); + await _databaseService.SaveCiphersAsync(ciphers); + totalCiphers += ciphers.Count; + _logger.LogInformation("Saved {CipherCount} ciphers for user {UserId} directly to database", + ciphers.Count, user.Id); + } + + _logger.LogInformation("Successfully generated and loaded {UserCount} users and {CipherCount} ciphers directly to database", + users.Count, totalCiphers); + } + + public async Task LoadSeedsAsync(string seedName, string? timestamp = null) + { + // Construct path to seeds directory + var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds"); + var namedDir = Path.Combine(seedsBaseDir, seedName); + + if (!Directory.Exists(namedDir)) + { + _logger.LogError("Seed directory not found: {SeedDir}", namedDir); + return; + } + + string seedDir; + + // If timestamp is specified, use that exact directory + if (!string.IsNullOrEmpty(timestamp)) + { + seedDir = Path.Combine(namedDir, timestamp); + if (!Directory.Exists(seedDir)) + { + _logger.LogError("Timestamp directory not found: {TimestampDir}", seedDir); + return; + } + } + else + { + // Otherwise, find the most recent timestamped directory + var timestampDirs = Directory.GetDirectories(namedDir); + if (timestampDirs.Length == 0) + { + _logger.LogError("No seed data found in directory: {SeedDir}", namedDir); + return; + } + + // Sort by directory name (which is a timestamp) in descending order + Array.Sort(timestampDirs); + Array.Reverse(timestampDirs); + + // Use the most recent one + seedDir = timestampDirs[0]; + _logger.LogInformation("Using most recent seed data from: {SeedDir}", seedDir); + } + + _logger.LogInformation("Loading seeds from directory: {SeedDir}", seedDir); + + // Clear database first + await _databaseService.ClearDatabaseAsync(); + + // Load users + var userFilePath = Path.Combine(seedDir, "users", "users.json"); + if (!File.Exists(userFilePath)) + { + _logger.LogError("User file not found: {UserFilePath}", userFilePath); + return; + } + + var userJson = await File.ReadAllTextAsync(userFilePath); + var users = JsonSerializer.Deserialize>(userJson, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }) ?? new List(); + + if (users.Count == 0) + { + _logger.LogError("No users found in user file"); + return; + } + + // Save users to database + await _databaseService.SaveUsersAsync(users); + + // Load and save ciphers for each user + var cipherDir = Path.Combine(seedDir, "ciphers"); + if (!Directory.Exists(cipherDir)) + { + _logger.LogError("Cipher directory not found: {CipherDir}", cipherDir); + return; + } + + var cipherFiles = Directory.GetFiles(cipherDir, "*.json"); + foreach (var cipherFile in cipherFiles) + { + var cipherJson = await File.ReadAllTextAsync(cipherFile); + var ciphers = JsonSerializer.Deserialize>(cipherJson, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }) ?? new List(); + + if (ciphers.Count > 0) + { + await _databaseService.SaveCiphersAsync(ciphers); + } + } + + _logger.LogInformation("Successfully loaded seed data into database"); + } + + public async Task ExtractSeedsAsync(string seedName) + { + _logger.LogInformation("Extracting seed data from database for seed name: {SeedName}", seedName); + + // Create timestamped folder under a named folder in seeds directory + var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds"); + Directory.CreateDirectory(seedsBaseDir); + + var namedDir = Path.Combine(seedsBaseDir, seedName); + Directory.CreateDirectory(namedDir); + + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var outputDir = Path.Combine(namedDir, timestamp); + Directory.CreateDirectory(outputDir); + + // Create users and ciphers subdirectories + Directory.CreateDirectory(Path.Combine(outputDir, "users")); + Directory.CreateDirectory(Path.Combine(outputDir, "ciphers")); + + _logger.LogInformation("Seed output directory: {OutputDir}", outputDir); + + try + { + // Get all users from the database + var users = await _databaseService.GetUsersAsync(); + if (users == null || users.Count == 0) + { + _logger.LogWarning("No users found in the database"); + return; + } + + _logger.LogInformation("Extracted {Count} users from database", users.Count); + + // Save users to a file + var userFilePath = Path.Combine(outputDir, "users", "users.json"); + await File.WriteAllTextAsync(userFilePath, JsonSerializer.Serialize(users, new JsonSerializerOptions + { + WriteIndented = true + })); + + int totalCiphers = 0; + // Get ciphers for each user + foreach (var user in users) + { + var ciphers = await _databaseService.GetCiphersByUserIdAsync(user.Id); + if (ciphers != null && ciphers.Count > 0) + { + // Save ciphers to a file + var cipherFilePath = Path.Combine(outputDir, "ciphers", $"{user.Id}.json"); + await File.WriteAllTextAsync(cipherFilePath, JsonSerializer.Serialize(ciphers, new JsonSerializerOptions + { + WriteIndented = true + })); + totalCiphers += ciphers.Count; + } + } + + _logger.LogInformation("Successfully extracted {UserCount} users and {CipherCount} ciphers", users.Count, totalCiphers); + _logger.LogInformation("Seed data saved to directory: {OutputDir}", outputDir); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error extracting seed data: {Message}", ex.Message); + throw; + } + } + + private List GenerateUsers(int count) + { + _logger.LogInformation("Generating {Count} users", count); + + var users = new List(); + + for (int i = 0; i < count; i++) + { + var userId = Guid.NewGuid(); + var email = _faker.Internet.Email(provider: "example.com"); + var name = _faker.Name.FullName(); + var masterPassword = _encryptionService.HashPassword(_defaultPassword); + var masterPasswordHint = "It's the word 'password'"; + var key = _encryptionService.DeriveKey(_defaultPassword, email); + + var user = new User + { + Id = userId, + Email = email, + Name = name, + MasterPassword = masterPassword, + MasterPasswordHint = masterPasswordHint, + SecurityStamp = Guid.NewGuid().ToString(), + EmailVerified = true, + ApiKey = Guid.NewGuid().ToString("N").Substring(0, 30), + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 100000, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Key = _encryptionService.EncryptString(Convert.ToBase64String(key), key) + }; + + users.Add(user); + } + + return users; + } + + private List GenerateCiphers(User user, int count) + { + _logger.LogInformation("Generating {Count} ciphers for user {UserId}", count, user.Id); + + var ciphers = new List(); + var key = _encryptionService.DeriveKey(_defaultPassword, user.Email); + + for (int i = 0; i < count; i++) + { + var cipherId = Guid.NewGuid(); + CipherType type; + string name; + string? notes = null; + + var typeRandom = _faker.Random.Int(1, 4); + type = (CipherType)typeRandom; + + switch (type) + { + case CipherType.Login: + name = $"Login - {_faker.Internet.DomainName()}"; + var loginData = new + { + Name = name, + Notes = notes, + Username = _faker.Internet.UserName(), + Password = _faker.Internet.Password(), + Uris = new[] + { + new { Uri = $"https://{_faker.Internet.DomainName()}" } + } + }; + + var loginDataJson = JsonSerializer.Serialize(loginData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(loginDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + + case CipherType.SecureNote: + name = $"Note - {_faker.Lorem.Word()}"; + notes = _faker.Lorem.Paragraph(); + var secureNoteData = new + { + Name = name, + Notes = notes, + Type = 0 // Text + }; + + var secureNoteDataJson = JsonSerializer.Serialize(secureNoteData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(secureNoteDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + + case CipherType.Card: + name = $"Card - {_faker.Finance.CreditCardNumber().Substring(0, 4)}"; + var cardData = new + { + Name = name, + Notes = notes, + CardholderName = _faker.Name.FullName(), + Number = _faker.Finance.CreditCardNumber(), + ExpMonth = _faker.Random.Int(1, 12).ToString(), + ExpYear = _faker.Random.Int(DateTime.UtcNow.Year, DateTime.UtcNow.Year + 10).ToString(), + Code = _faker.Random.Int(100, 999).ToString() + }; + + var cardDataJson = JsonSerializer.Serialize(cardData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(cardDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + + case CipherType.Identity: + name = $"Identity - {_faker.Name.FullName()}"; + var identityData = new + { + Name = name, + Notes = notes, + Title = _faker.Name.Prefix(), + FirstName = _faker.Name.FirstName(), + MiddleName = _faker.Name.FirstName(), + LastName = _faker.Name.LastName(), + Email = _faker.Internet.Email(), + Phone = _faker.Phone.PhoneNumber(), + Address1 = _faker.Address.StreetAddress(), + City = _faker.Address.City(), + State = _faker.Address.State(), + PostalCode = _faker.Address.ZipCode(), + Country = _faker.Address.CountryCode() + }; + + var identityDataJson = JsonSerializer.Serialize(identityData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(identityDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + } + } + + return ciphers; + } +} \ No newline at end of file diff --git a/util/seeder/Services/SeederService.cs.bak b/util/seeder/Services/SeederService.cs.bak new file mode 100644 index 0000000000..1ac1a83278 --- /dev/null +++ b/util/seeder/Services/SeederService.cs.bak @@ -0,0 +1,383 @@ +using System.Text.Json; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bogus; +using Microsoft.Extensions.Logging; + +namespace Bit.Seeder.Services; + +public class SeederService : ISeederService +{ + private readonly IEncryptionService _encryptionService; + private readonly IDatabaseService _databaseService; + private readonly ILogger _logger; + private readonly Faker _faker; + private readonly string _defaultPassword = "password"; + + public SeederService( + IEncryptionService encryptionService, + IDatabaseService databaseService, + ILogger logger) + { + _encryptionService = encryptionService; + _databaseService = databaseService; + _logger = logger; + _faker = new Faker(); + + // Set the random seed to ensure reproducible data + Randomizer.Seed = new Random(42); + } + + public async Task GenerateSeedsAsync(int userCount, int ciphersPerUser, string outputName) + { + _logger.LogInformation("Generating seeds: {UserCount} users with {CiphersPerUser} ciphers each", userCount, ciphersPerUser); + + // Create timestamped folder under a named folder in seeds directory + var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds"); + Directory.CreateDirectory(seedsBaseDir); + + var namedDir = Path.Combine(seedsBaseDir, outputName); + Directory.CreateDirectory(namedDir); + + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + var outputDir = Path.Combine(namedDir, timestamp); + Directory.CreateDirectory(outputDir); + + // Create users and ciphers subdirectories + Directory.CreateDirectory(Path.Combine(outputDir, "users")); + Directory.CreateDirectory(Path.Combine(outputDir, "ciphers")); + + _logger.LogInformation("Seed output directory: {OutputDir}", outputDir); + + // Generate users + var users = GenerateUsers(userCount); + + // Generate ciphers for each user + var allCiphers = new List(); + foreach (var user in users) + { + var ciphers = GenerateCiphers(user, ciphersPerUser); + allCiphers.AddRange(ciphers); + + // Save each user's ciphers to a file + var cipherFilePath = Path.Combine(outputDir, "ciphers", $"{user.Id}.json"); + await File.WriteAllTextAsync(cipherFilePath, JsonSerializer.Serialize(ciphers, new JsonSerializerOptions + { + WriteIndented = true + })); + } + + // Save users to a file + var userFilePath = Path.Combine(outputDir, "users", "users.json"); + await File.WriteAllTextAsync(userFilePath, JsonSerializer.Serialize(users, new JsonSerializerOptions + { + WriteIndented = true + })); + + _logger.LogInformation("Successfully generated {UserCount} users and {CipherCount} ciphers", users.Count, allCiphers.Count); + _logger.LogInformation("Seed data saved to directory: {OutputDir}", outputDir); + } + + public async Task GenerateAndLoadSeedsAsync(int userCount, int ciphersPerUser, string seedName) + { + _logger.LogInformation("Generating and loading seeds directly: {UserCount} users with {CiphersPerUser} ciphers each", + userCount, ciphersPerUser); + + // Generate users directly without saving to files + var users = GenerateUsers(userCount); + + // Clear the database first + await _databaseService.ClearDatabaseAsync(); + + // Save users to database + await _databaseService.SaveUsersAsync(users); + _logger.LogInformation("Saved {UserCount} users directly to database", users.Count); + + // Generate and save ciphers for each user + int totalCiphers = 0; + foreach (var user in users) + { + var ciphers = GenerateCiphers(user, ciphersPerUser); + await _databaseService.SaveCiphersAsync(ciphers); + totalCiphers += ciphers.Count; + _logger.LogInformation("Saved {CipherCount} ciphers for user {UserId} directly to database", + ciphers.Count, user.Id); + } + + _logger.LogInformation("Successfully generated and loaded {UserCount} users and {CipherCount} ciphers directly to database", + users.Count, totalCiphers); + } + + public async Task LoadSeedsAsync(string seedName, string? timestamp = null) + { + // Construct path to seeds directory + var seedsBaseDir = Path.Combine(Directory.GetCurrentDirectory(), "seeds"); + var namedDir = Path.Combine(seedsBaseDir, seedName); + + if (!Directory.Exists(namedDir)) + { + _logger.LogError("Seed directory not found: {SeedDir}", namedDir); + return; + } + + string seedDir; + + // If timestamp is specified, use that exact directory + if (!string.IsNullOrEmpty(timestamp)) + { + seedDir = Path.Combine(namedDir, timestamp); + if (!Directory.Exists(seedDir)) + { + _logger.LogError("Timestamp directory not found: {TimestampDir}", seedDir); + return; + } + } + else + { + // Otherwise, find the most recent timestamped directory + var timestampDirs = Directory.GetDirectories(namedDir); + if (timestampDirs.Length == 0) + { + _logger.LogError("No seed data found in directory: {SeedDir}", namedDir); + return; + } + + // Sort by directory name (which is a timestamp) in descending order + Array.Sort(timestampDirs); + Array.Reverse(timestampDirs); + + // Use the most recent one + seedDir = timestampDirs[0]; + _logger.LogInformation("Using most recent seed data from: {SeedDir}", seedDir); + } + + _logger.LogInformation("Loading seeds from directory: {SeedDir}", seedDir); + + // Clear database first + await _databaseService.ClearDatabaseAsync(); + + // Load users + var userFilePath = Path.Combine(seedDir, "users", "users.json"); + if (!File.Exists(userFilePath)) + { + _logger.LogError("User file not found: {UserFilePath}", userFilePath); + return; + } + + var userJson = await File.ReadAllTextAsync(userFilePath); + var users = JsonSerializer.Deserialize>(userJson, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }) ?? new List(); + + if (users.Count == 0) + { + _logger.LogError("No users found in user file"); + return; + } + + // Save users to database + await _databaseService.SaveUsersAsync(users); + + // Load and save ciphers for each user + var cipherDir = Path.Combine(seedDir, "ciphers"); + if (!Directory.Exists(cipherDir)) + { + _logger.LogError("Cipher directory not found: {CipherDir}", cipherDir); + return; + } + + var cipherFiles = Directory.GetFiles(cipherDir, "*.json"); + foreach (var cipherFile in cipherFiles) + { + var cipherJson = await File.ReadAllTextAsync(cipherFile); + var ciphers = JsonSerializer.Deserialize>(cipherJson, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }) ?? new List(); + + if (ciphers.Count > 0) + { + await _databaseService.SaveCiphersAsync(ciphers); + } + } + + _logger.LogInformation("Successfully loaded seed data into database"); + } + + private List GenerateUsers(int count) + { + _logger.LogInformation("Generating {Count} users", count); + + var users = new List(); + + for (int i = 0; i < count; i++) + { + var userId = Guid.NewGuid(); + var email = _faker.Internet.Email(provider: "example.com"); + var name = _faker.Name.FullName(); + var masterPassword = _encryptionService.HashPassword(_defaultPassword); + var masterPasswordHint = "It's the word 'password'"; + var key = _encryptionService.DeriveKey(_defaultPassword, email); + + var user = new User + { + Id = userId, + Email = email, + Name = name, + MasterPassword = masterPassword, + MasterPasswordHint = masterPasswordHint, + SecurityStamp = Guid.NewGuid().ToString(), + EmailVerified = true, + ApiKey = Guid.NewGuid().ToString("N").Substring(0, 30), + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 100000, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Key = _encryptionService.EncryptString(Convert.ToBase64String(key), key) + }; + + users.Add(user); + } + + return users; + } + + private List GenerateCiphers(User user, int count) + { + _logger.LogInformation("Generating {Count} ciphers for user {UserId}", count, user.Id); + + var ciphers = new List(); + var key = _encryptionService.DeriveKey(_defaultPassword, user.Email); + + for (int i = 0; i < count; i++) + { + var cipherId = Guid.NewGuid(); + CipherType type; + string name; + string? notes = null; + + var typeRandom = _faker.Random.Int(1, 4); + type = (CipherType)typeRandom; + + switch (type) + { + case CipherType.Login: + name = $"Login - {_faker.Internet.DomainName()}"; + var loginData = new + { + Name = name, + Notes = notes, + Username = _faker.Internet.UserName(), + Password = _faker.Internet.Password(), + Uris = new[] + { + new { Uri = $"https://{_faker.Internet.DomainName()}" } + } + }; + + var loginDataJson = JsonSerializer.Serialize(loginData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(loginDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + + case CipherType.SecureNote: + name = $"Note - {_faker.Lorem.Word()}"; + notes = _faker.Lorem.Paragraph(); + var secureNoteData = new + { + Name = name, + Notes = notes, + Type = 0 // Text + }; + + var secureNoteDataJson = JsonSerializer.Serialize(secureNoteData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(secureNoteDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + + case CipherType.Card: + name = $"Card - {_faker.Finance.CreditCardNumber().Substring(0, 4)}"; + var cardData = new + { + Name = name, + Notes = notes, + CardholderName = _faker.Name.FullName(), + Number = _faker.Finance.CreditCardNumber(), + ExpMonth = _faker.Random.Int(1, 12).ToString(), + ExpYear = _faker.Random.Int(DateTime.UtcNow.Year, DateTime.UtcNow.Year + 10).ToString(), + Code = _faker.Random.Int(100, 999).ToString() + }; + + var cardDataJson = JsonSerializer.Serialize(cardData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(cardDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + + case CipherType.Identity: + name = $"Identity - {_faker.Name.FullName()}"; + var identityData = new + { + Name = name, + Notes = notes, + Title = _faker.Name.Prefix(), + FirstName = _faker.Name.FirstName(), + MiddleName = _faker.Name.FirstName(), + LastName = _faker.Name.LastName(), + Email = _faker.Internet.Email(), + Phone = _faker.Phone.PhoneNumber(), + Address1 = _faker.Address.StreetAddress(), + City = _faker.Address.City(), + State = _faker.Address.State(), + PostalCode = _faker.Address.ZipCode(), + Country = _faker.Address.CountryCode() + }; + + var identityDataJson = JsonSerializer.Serialize(identityData); + + ciphers.Add(new Cipher + { + Id = cipherId, + UserId = user.Id, + Type = type, + Data = _encryptionService.EncryptString(identityDataJson, key), + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Reprompt = CipherRepromptType.None + }); + break; + } + } + + return ciphers; + } +} \ No newline at end of file diff --git a/util/seeder/Settings/GlobalSettings.cs b/util/seeder/Settings/GlobalSettings.cs new file mode 100644 index 0000000000..895ccb8c1f --- /dev/null +++ b/util/seeder/Settings/GlobalSettings.cs @@ -0,0 +1,16 @@ +namespace Bit.Seeder.Settings; + +public class GlobalSettings +{ + public bool SelfHosted { get; set; } + public string DatabaseProvider { get; set; } = string.Empty; + public SqlSettings SqlServer { get; set; } = new SqlSettings(); + public SqlSettings PostgreSql { get; set; } = new SqlSettings(); + public SqlSettings MySql { get; set; } = new SqlSettings(); + public SqlSettings Sqlite { get; set; } = new SqlSettings(); + + public class SqlSettings + { + public string ConnectionString { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/util/seeder/Settings/GlobalSettingsFactory.cs b/util/seeder/Settings/GlobalSettingsFactory.cs new file mode 100644 index 0000000000..e7c6043cc7 --- /dev/null +++ b/util/seeder/Settings/GlobalSettingsFactory.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.Configuration; + +namespace Bit.Seeder.Settings; + +public static class GlobalSettingsFactory +{ + private static GlobalSettings? _globalSettings; + + public static GlobalSettings GlobalSettings + { + get + { + if (_globalSettings == null) + { + _globalSettings = LoadGlobalSettings(); + } + + return _globalSettings; + } + } + + 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"); + + // Debug: Print all settings from globalSettings section + foreach (var setting in globalSettingsSection.GetChildren()) + { + Console.WriteLine($"Found setting: {setting.Key}"); + foreach (var child in setting.GetChildren()) + { + Console.WriteLine($" - {setting.Key}.{child.Key}"); + } + } + + var settings = new GlobalSettings(); + globalSettingsSection.Bind(settings); + + // Output the loaded settings + Console.WriteLine($"Loaded DatabaseProvider: {settings.DatabaseProvider}"); + Console.WriteLine($"PostgreSql settings loaded: {settings.PostgreSql != null}"); + Console.WriteLine($"SqlServer settings loaded: {settings.SqlServer != null}"); + Console.WriteLine($"MySql settings loaded: {settings.MySql != null}"); + Console.WriteLine($"Sqlite settings loaded: {settings.Sqlite != null}"); + + // Check for case sensitivity issue with PostgreSql/postgresql keys + var postgresqlValue = globalSettingsSection.GetSection("postgresql")?.Value; + var postgreSqlValue = globalSettingsSection.GetSection("postgreSql")?.Value; + Console.WriteLine($"Raw check - postgresql setting exists: {postgresqlValue != null}"); + Console.WriteLine($"Raw check - postgreSql setting exists: {postgreSqlValue != null}"); + + return settings; + } +} + +// Non-static version that can accept command-line arguments +public class GlobalSettingsFactoryWithArgs +{ + public GlobalSettings GlobalSettings { get; } + + public GlobalSettingsFactoryWithArgs(string[] args) + { + GlobalSettings = new GlobalSettings(); + + var config = 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") + .AddCommandLine(args) + .AddEnvironmentVariables() + .Build(); + + config.GetSection("globalSettings").Bind(GlobalSettings); + } +} \ No newline at end of file diff --git a/util/seeder/appsettings.json b/util/seeder/appsettings.json new file mode 100644 index 0000000000..8976344175 --- /dev/null +++ b/util/seeder/appsettings.json @@ -0,0 +1,18 @@ +{ + "globalSettings": { + "selfHosted": true, + "databaseProvider": "postgres", + "sqlServer": { + "connectionString": "Server=localhost;Database=vault_dev;User Id=SA;Password=Th3P455word?;Encrypt=True;TrustServerCertificate=True" + }, + "postgreSql": { + "connectionString": "Host=localhost;Username=postgres;Password=Th3P455word?;Database=vault_dev;Include Error Detail=true" + }, + "mysql": { + "connectionString": "server=localhost;uid=root;pwd=Th3P455word?;database=vault_dev" + }, + "sqlite": { + "connectionString": "Data Source=./bitwarden_test.sqlite" + } + } +} \ No newline at end of file