Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial commit for EF db seeder #53

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion bitwarden-server.sln
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@

๏ปฟ
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
Expand Down Expand Up @@ -124,6 +124,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventsProcessor.Test", "tes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\Notifications.Test\Notifications.Test.csproj", "{90D85D8F-5577-4570-A96E-5A2E185F0F6F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DBSeeder", "util\DBSeeder\DBSeeder.csproj", "{29318B0D-E753-4A27-BFBC-F7566FE26E2F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFDBSeederUtility", "util\EFDBSeederUtility\EFDBSeederUtility.csproj", "{47F03C4D-C178-4F9A-99B1-C7E35342D8BC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -308,6 +312,14 @@ Global
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90D85D8F-5577-4570-A96E-5A2E185F0F6F}.Release|Any CPU.Build.0 = Release|Any CPU
{29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{29318B0D-E753-4A27-BFBC-F7566FE26E2F}.Release|Any CPU.Build.0 = Release|Any CPU
{47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47F03C4D-C178-4F9A-99B1-C7E35342D8BC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -357,6 +369,8 @@ Global
{916AFD8C-30AF-49B6-A5C9-28CA1B5D9298} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{29318B0D-E753-4A27-BFBC-F7566FE26E2F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
{47F03C4D-C178-4F9A-99B1-C7E35342D8BC} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
Expand Down
14 changes: 14 additions & 0 deletions util/DBSeeder/DBSeeder.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
๏ปฟ<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="Bogus" Version="35.5.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Core\Core.csproj" />
<ProjectReference Include="..\..\src\Api\Api.csproj" />
<ProjectReference Include="..\..\src\Infrastructure.EntityFramework\Infrastructure.EntityFramework.csproj" />
</ItemGroup>

</Project>
185 changes: 185 additions & 0 deletions util/DBSeeder/EFDBSeeder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
๏ปฟusing System.Text.Json;
using Bit.Core.Vault.Enums; // Change this line
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Vault.Models;
using Bogus;
using Microsoft.Extensions.Logging;



namespace Bit.DBSeeder;

public class EFDBSeeder
{
private readonly string _connectionString;
private readonly string _databaseProvider;
private readonly ILogger<EFDBSeeder> _logger;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: ILogger is declared but never used


public EFDBSeeder(string connectionString, string databaseProvider)
{
_connectionString = connectionString;
_databaseProvider = databaseProvider;

}

public bool SeedDatabase()
{
//print connectionstring to console
Console.WriteLine(_connectionString);
Console.WriteLine(_databaseProvider);

try
{
var factory = new DatabaseContextFactory();
using (var context = factory.CreateDbContext(new[] { _connectionString }))
Comment on lines +33 to +34
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: DatabaseContextFactory is not using the _databaseProvider. Ensure it's correctly configured for different database types

{
if (context.Database.CanConnect())
{
Console.WriteLine("Successfully connected to the database!");

// Seed the database
SeedUsers(context);
SeedCiphers(context);

Console.WriteLine("Database seeded successfully!");
}
else
{
Console.WriteLine("Failed to connect to the database.");
return false;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error adding users: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
throw; // Re-throw the exception to stop the seeding process
}



return true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: This always returns true, even if an exception is thrown. Consider returning false on exceptions

}

private void SeedUsers(DatabaseContext context)
{
if (!context.Users.Any())
{
Console.WriteLine("Generating 5000 users...");

var faker = new Faker<Bit.Infrastructure.EntityFramework.Models.User>()
.RuleFor(u => u.Id, f => Guid.NewGuid())
.RuleFor(u => u.Name, f => f.Name.FullName())
.RuleFor(u => u.Email, (f, u) => f.Internet.Email(u.Name))
.RuleFor(u => u.EmailVerified, f => f.Random.Bool(0.9f))
.RuleFor(u => u.SecurityStamp, f => Guid.NewGuid().ToString())
.RuleFor(u => u.ApiKey, f => Guid.NewGuid().ToString("N").Substring(0, 30))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: ApiKey generation might not be secure. Consider using a cryptographically secure method

.RuleFor(u => u.CreationDate, f => f.Date.Past(2))
.RuleFor(u => u.RevisionDate, (f, u) => f.Date.Between(u.CreationDate, DateTime.UtcNow));

var users = faker.Generate(5000);

const int batchSize = 100;
for (int i = 0; i < users.Count; i += batchSize)
{
context.Users.AddRange(users.Skip(i).Take(batchSize));
context.SaveChanges();
Console.WriteLine($"Added {Math.Min(i + batchSize, users.Count)} users");
}

Console.WriteLine("5000 test users added to the database.");
}
else
{
Console.WriteLine("Users table is not empty. Skipping user seeding.");
}
}

private void SeedCiphers(DatabaseContext context)
{
if (!context.Ciphers.Any())
{
var users = context.Users.ToList();
if (!users.Any())
{
Console.WriteLine("No users found. Please seed users first.");
return;
}

Console.WriteLine($"Generating ciphers for {users.Count} users...");

var faker = new Faker<Cipher>()
.RuleFor(c => c.Id, f => Guid.NewGuid())
.RuleFor(c => c.Type, f => CipherType.Login)
.RuleFor(c => c.Data, f => JsonSerializer.Serialize(new
{
Name = f.Internet.DomainName(),
Notes = f.Lorem.Sentence(),
Login = new
{
Username = f.Internet.UserName(),
Password = f.Internet.Password(),
Uri = f.Internet.Url()
}
}))
.RuleFor(c => c.CreationDate, f => f.Date.Past(1))
.RuleFor(c => c.RevisionDate, (f, c) => f.Date.Between(c.CreationDate, DateTime.UtcNow))
.RuleFor(c => c.DeletedDate, f => null)
.RuleFor(c => c.Reprompt, f => CipherRepromptType.None);

const int batchSize = 100;
for (int i = 0; i < users.Count; i += batchSize)
{
var userBatch = users.Skip(i).Take(batchSize);
var ciphers = userBatch.Select(user =>
{
var cipher = faker.Generate();
cipher.UserId = user.Id;
return cipher;
}).ToList();

try
{
context.Ciphers.AddRange(ciphers);
context.SaveChanges();
Console.WriteLine($"Added ciphers for users {i + 1} to {Math.Min(i + batchSize, users.Count)}");
}
catch (Exception ex)
{
Console.WriteLine($"Error adding ciphers: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
throw; // Re-throw the exception to stop the seeding process
}
}

Console.WriteLine($"Ciphers added for all {users.Count} users.");
}
else
{
Console.WriteLine("Ciphers table is not empty. Skipping cipher seeding.");
}
}

/* private ILogger<EFDBSeeder> CreateLogger()
{
var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning)
.AddConsole();

builder.AddFilter("EFDBSeeder.EFDBSeeder", LogLevel.Information);
});

return loggerFactory.CreateLogger<EFDBSeeder>();
}
*/
}
28 changes: 28 additions & 0 deletions util/DBSeeder/Factories.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
๏ปฟusing Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.DBSeeder;


public class DatabaseContextFactory : IDesignTimeDbContextFactory<DatabaseContext>
{
public DatabaseContext CreateDbContext(string[] args)
{
if (args.Length == 0 || string.IsNullOrWhiteSpace(args[0]))
{
throw new ArgumentException("Connection string must be provided as the first argument.");
}

var connectionString = args[0];

var services = new ServiceCollection();
services.AddDataProtection();
services.AddDbContext<DatabaseContext>(options =>
options.UseSqlServer(connectionString));
Comment on lines +22 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: This hardcodes SQL Server. Consider using a provider-agnostic approach to support multiple database types.

var serviceProvider = services.BuildServiceProvider();
Comment on lines +20 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: ServiceCollection is created and immediately disposed. Consider passing IServiceProvider as a parameter for better testability and reusability.


return serviceProvider.GetRequiredService<DatabaseContext>();
}
}
18 changes: 18 additions & 0 deletions util/EFDBSeederUtility/EFDBSeederUtility.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
๏ปฟ<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\DBSeeder\DBSeeder.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="CommandDotNet" Version="7.0.4" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions util/EFDBSeederUtility/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
๏ปฟusing Bit.DBSeeder;
using CommandDotNet;

internal class Program
{
private static int Main(string[] args)
{
return new AppRunner<Program>().Run(args);
}

[DefaultCommand]
public void Execute(

[Operand(Description = "Database provider (mssql, mysql, postgres, sqlite).")] string databaseProvider,
[Operand(Description = "Database connection string.")] string ConnectionString
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: ConnectionString parameter name should be camelCase for consistency


) => SeedDatabase(ConnectionString, databaseProvider);

private static bool SeedDatabase(string databaseConnectionString,
string databaseProvider)
{
var seeder = new EFDBSeeder(databaseConnectionString, databaseProvider);
bool success;

success = seeder.SeedDatabase(); // Change this line
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: Remove the comment '// Change this line' as it's no longer relevant


return success;
}
}