mirror of
https://github.com/bitwarden/server.git
synced 2025-05-23 04:21:05 -05:00
Simple data seed generator (users and ciphers), loader, and extracter that can load from json or directly from seed generation - cannot be used to login as encryption methods are non functional
This commit is contained in:
parent
2df4076a6b
commit
4737d9d95c
29
util/DbSeederUtility/DbSeederUtility.csproj
Normal file
29
util/DbSeederUtility/DbSeederUtility.csproj
Normal file
@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Bit.DbSeederUtility</RootNamespace>
|
||||
<AssemblyName>DbSeeder</AssemblyName>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Seeder\Seeder.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandDotNet" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\Seeder\appsettings.json" Link="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
83
util/DbSeederUtility/Program.cs
Normal file
83
util/DbSeederUtility/Program.cs
Normal file
@ -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<Program>()
|
||||
.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;
|
||||
}
|
||||
}
|
119
util/seeder/Commands/ExtractCommand.cs
Normal file
119
util/seeder/Commands/ExtractCommand.cs
Normal file
@ -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<ISeederService>();
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<ExtractCommand>>();
|
||||
|
||||
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<IConfiguration>(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<DatabaseContext>(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<ISeederService, SeederService>();
|
||||
services.AddTransient<IDatabaseService, DatabaseService>();
|
||||
services.AddTransient<IEncryptionService, EncryptionService>();
|
||||
}
|
||||
}
|
||||
}
|
77
util/seeder/Commands/GenerateCommand.cs
Normal file
77
util/seeder/Commands/GenerateCommand.cs
Normal file
@ -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<ISeederService>();
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<GenerateCommand>>();
|
||||
|
||||
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<ISeederService, SeederService>();
|
||||
services.AddTransient<IDatabaseService, DatabaseService>();
|
||||
services.AddTransient<IEncryptionService, EncryptionService>();
|
||||
services.AddDbContext<DatabaseContext>();
|
||||
}
|
||||
}
|
||||
}
|
87
util/seeder/Commands/LoadCommand.cs
Normal file
87
util/seeder/Commands/LoadCommand.cs
Normal file
@ -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<ISeederService>();
|
||||
var databaseService = serviceProvider.GetRequiredService<IDatabaseService>();
|
||||
var logger = serviceProvider.GetRequiredService<ILogger<LoadCommand>>();
|
||||
|
||||
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<ISeederService, SeederService>();
|
||||
services.AddTransient<IDatabaseService, DatabaseService>();
|
||||
services.AddTransient<IEncryptionService, EncryptionService>();
|
||||
services.AddDbContext<DatabaseContext>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<key id="c9676c44-16a7-4b38-b750-4ea5443f1b87" version="1">
|
||||
<creationDate>2025-03-13T17:13:50.8174933Z</creationDate>
|
||||
<activationDate>2025-03-13T17:13:50.8130955Z</activationDate>
|
||||
<expirationDate>2025-06-11T17:13:50.8130955Z</expirationDate>
|
||||
<descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
||||
<descriptor>
|
||||
<encryption algorithm="AES_256_CBC" />
|
||||
<validation algorithm="HMACSHA256" />
|
||||
<masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
|
||||
<!-- Warning: the key below is in an unencrypted form. -->
|
||||
<value>VCxxS1xKEWQ9+XRXgMchHML7POXcwRkCHswj7JMjjN36RkERENo+ky/1mazB4RZ6BjjwXsAyjhSz2eGts/0BnQ==</value>
|
||||
</masterKey>
|
||||
</descriptor>
|
||||
</descriptor>
|
||||
</key>
|
98
util/seeder/README.md
Normal file
98
util/seeder/README.md
Normal file
@ -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> Number of users to generate (default: 10)
|
||||
-c, --ciphers <NUMBER> Number of ciphers per user (default: 5)
|
||||
-o, --output <DIRECTORY> Output directory for seed files (default: seeds)
|
||||
```
|
||||
|
||||
### Load Seeds
|
||||
|
||||
Loads generated seed data into the database.
|
||||
|
||||
```
|
||||
DbSeeder.exe load-seeds [options]
|
||||
|
||||
Options:
|
||||
-p, --path <DIRECTORY> 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.
|
43
util/seeder/Seeder.csproj
Normal file
43
util/seeder/Seeder.csproj
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Bit.Seeder</RootNamespace>
|
||||
<UserSecretsId>Bit.Seeder</UserSecretsId>
|
||||
<Description>Core library for generating and managing test data for Bitwarden</Description>
|
||||
<OutputType>library</OutputType>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="Bogus" Version="35.4.0" />
|
||||
<PackageReference Include="CommandDotNet" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="..\..\Program.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
76
util/seeder/Services/DatabaseContext.cs
Normal file
76
util/seeder/Services/DatabaseContext.cs
Normal file
@ -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<User> Users { get; set; }
|
||||
public DbSet<Cipher> 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<Cipher>(entity =>
|
||||
{
|
||||
entity.ToTable("Cipher");
|
||||
entity.Property(e => e.Type).HasConversion<int>();
|
||||
entity.Property(e => e.Reprompt).HasConversion<int>();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<User>(entity =>
|
||||
{
|
||||
entity.ToTable("User");
|
||||
entity.Property(e => e.Kdf).HasConversion<int>();
|
||||
});
|
||||
}
|
||||
}
|
85
util/seeder/Services/DatabaseService.cs
Normal file
85
util/seeder/Services/DatabaseService.cs
Normal file
@ -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<DatabaseService> _logger;
|
||||
|
||||
public DatabaseService(DatabaseContext dbContext, ILogger<DatabaseService> 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<User> 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<Cipher> 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<List<User>> 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<List<Cipher>> 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<List<Cipher>> 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;
|
||||
}
|
||||
}
|
66
util/seeder/Services/EncryptionService.cs
Normal file
66
util/seeder/Services/EncryptionService.cs
Normal file
@ -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<EncryptionService> _logger;
|
||||
private readonly IDataProtector _dataProtector;
|
||||
|
||||
public EncryptionService(
|
||||
ILogger<EncryptionService> 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);
|
||||
}
|
||||
}
|
14
util/seeder/Services/IDatabaseService.cs
Normal file
14
util/seeder/Services/IDatabaseService.cs
Normal file
@ -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<User> users);
|
||||
Task SaveCiphersAsync(IEnumerable<Cipher> ciphers);
|
||||
Task<List<User>> GetUsersAsync();
|
||||
Task<List<Cipher>> GetCiphersAsync();
|
||||
Task<List<Cipher>> GetCiphersByUserIdAsync(Guid userId);
|
||||
}
|
11
util/seeder/Services/IDatabaseService.cs.bak
Normal file
11
util/seeder/Services/IDatabaseService.cs.bak
Normal file
@ -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<User> users);
|
||||
Task SaveCiphersAsync(IEnumerable<Cipher> ciphers);
|
||||
}
|
8
util/seeder/Services/IEncryptionService.cs
Normal file
8
util/seeder/Services/IEncryptionService.cs
Normal file
@ -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);
|
||||
}
|
9
util/seeder/Services/ISeederService.cs
Normal file
9
util/seeder/Services/ISeederService.cs
Normal file
@ -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);
|
||||
}
|
450
util/seeder/Services/SeederService.cs
Normal file
450
util/seeder/Services/SeederService.cs
Normal file
@ -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<SeederService> _logger;
|
||||
private readonly Faker _faker;
|
||||
private readonly string _defaultPassword = "password";
|
||||
|
||||
public SeederService(
|
||||
IEncryptionService encryptionService,
|
||||
IDatabaseService databaseService,
|
||||
ILogger<SeederService> 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<Cipher>();
|
||||
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<List<User>>(userJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new List<User>();
|
||||
|
||||
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<List<Cipher>>(cipherJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new List<Cipher>();
|
||||
|
||||
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<User> GenerateUsers(int count)
|
||||
{
|
||||
_logger.LogInformation("Generating {Count} users", count);
|
||||
|
||||
var users = new List<User>();
|
||||
|
||||
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<Cipher> GenerateCiphers(User user, int count)
|
||||
{
|
||||
_logger.LogInformation("Generating {Count} ciphers for user {UserId}", count, user.Id);
|
||||
|
||||
var ciphers = new List<Cipher>();
|
||||
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;
|
||||
}
|
||||
}
|
383
util/seeder/Services/SeederService.cs.bak
Normal file
383
util/seeder/Services/SeederService.cs.bak
Normal file
@ -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<SeederService> _logger;
|
||||
private readonly Faker _faker;
|
||||
private readonly string _defaultPassword = "password";
|
||||
|
||||
public SeederService(
|
||||
IEncryptionService encryptionService,
|
||||
IDatabaseService databaseService,
|
||||
ILogger<SeederService> 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<Cipher>();
|
||||
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<List<User>>(userJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new List<User>();
|
||||
|
||||
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<List<Cipher>>(cipherJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new List<Cipher>();
|
||||
|
||||
if (ciphers.Count > 0)
|
||||
{
|
||||
await _databaseService.SaveCiphersAsync(ciphers);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Successfully loaded seed data into database");
|
||||
}
|
||||
|
||||
private List<User> GenerateUsers(int count)
|
||||
{
|
||||
_logger.LogInformation("Generating {Count} users", count);
|
||||
|
||||
var users = new List<User>();
|
||||
|
||||
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<Cipher> GenerateCiphers(User user, int count)
|
||||
{
|
||||
_logger.LogInformation("Generating {Count} ciphers for user {UserId}", count, user.Id);
|
||||
|
||||
var ciphers = new List<Cipher>();
|
||||
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;
|
||||
}
|
||||
}
|
16
util/seeder/Settings/GlobalSettings.cs
Normal file
16
util/seeder/Settings/GlobalSettings.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
86
util/seeder/Settings/GlobalSettingsFactory.cs
Normal file
86
util/seeder/Settings/GlobalSettingsFactory.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
18
util/seeder/appsettings.json
Normal file
18
util/seeder/appsettings.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user