mirror of
https://github.com/bitwarden/server
synced 2026-02-13 23:13:22 +00:00
Refactor into brand new recipe & proper organization per Seeder standards
This commit is contained in:
@@ -26,14 +26,13 @@ public class GroupsControllerPerformanceTests(ITestOutputHelper testOutputHelper
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount);
|
||||
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||
|
||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||
var collectionIds = collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
|
||||
var groupIds = groupsSeeder.AddToOrganization(orgId, 1, orgUserIds, 0);
|
||||
|
||||
@@ -28,14 +28,14 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, seats);
|
||||
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: seats);
|
||||
|
||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||
collectionsSeeder.AddToOrganization(orgId, 10, orgUserIds);
|
||||
groupsSeeder.AddToOrganization(orgId, 5, orgUserIds);
|
||||
@@ -64,14 +64,13 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, seats);
|
||||
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: seats);
|
||||
|
||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||
collectionsSeeder.AddToOrganization(orgId, 10, orgUserIds);
|
||||
groupsSeeder.AddToOrganization(orgId, 5, orgUserIds);
|
||||
@@ -99,12 +98,11 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, 1);
|
||||
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||
|
||||
var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();
|
||||
groupsSeeder.AddToOrganization(orgId, 2, [orgUserId]);
|
||||
@@ -132,10 +130,10 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, name: "Org", domain: domain, users: 1);
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||
|
||||
var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();
|
||||
|
||||
@@ -165,10 +163,14 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount, usersStatus: OrganizationUserStatusType.Accepted);
|
||||
var orgId = orgSeeder.Seed(
|
||||
name: "Org",
|
||||
domain: domain,
|
||||
users: userCount,
|
||||
usersStatus: OrganizationUserStatusType.Accepted);
|
||||
|
||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||
|
||||
@@ -209,10 +211,10 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, name: "Org", domain: domain, users: userCount);
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||
|
||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||
|
||||
@@ -249,10 +251,14 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount, usersStatus: OrganizationUserStatusType.Confirmed);
|
||||
var orgId = orgSeeder.Seed(
|
||||
name: "Org",
|
||||
domain: domain,
|
||||
users: userCount,
|
||||
usersStatus: OrganizationUserStatusType.Confirmed);
|
||||
|
||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||
|
||||
@@ -289,10 +295,14 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount, usersStatus: OrganizationUserStatusType.Revoked);
|
||||
var orgId = orgSeeder.Seed(
|
||||
name: "Org",
|
||||
domain: domain,
|
||||
users: userCount,
|
||||
usersStatus: OrganizationUserStatusType.Revoked);
|
||||
|
||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||
|
||||
@@ -329,11 +339,15 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var domainSeeder = new OrganizationDomainRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount,
|
||||
|
||||
var orgId = orgSeeder.Seed(
|
||||
name: "Org",
|
||||
domain: domain,
|
||||
users: userCount,
|
||||
usersStatus: OrganizationUserStatusType.Confirmed);
|
||||
|
||||
domainSeeder.AddVerifiedDomainToOrganization(orgId, domain);
|
||||
@@ -370,13 +384,13 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, 1);
|
||||
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||
|
||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||
var collectionIds = collectionsSeeder.AddToOrganization(orgId, 3, orgUserIds, 0);
|
||||
var groupIds = groupsSeeder.AddToOrganization(orgId, 2, orgUserIds, 0);
|
||||
@@ -420,10 +434,10 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, name: "Org", domain: domain, users: userCount);
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||
|
||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||
|
||||
@@ -457,11 +471,14 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var domainSeeder = new OrganizationDomainRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, 2,
|
||||
var orgId = orgSeeder.Seed(
|
||||
name: "Org",
|
||||
domain: domain,
|
||||
users: 2,
|
||||
usersStatus: OrganizationUserStatusType.Confirmed);
|
||||
|
||||
domainSeeder.AddVerifiedDomainToOrganization(orgId, domain);
|
||||
@@ -495,12 +512,12 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, 1);
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
|
||||
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||
var collectionIds = collectionsSeeder.AddToOrganization(orgId, 2, orgUserIds, 0);
|
||||
|
||||
@@ -543,10 +560,14 @@ public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testO
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount, usersStatus: OrganizationUserStatusType.Invited);
|
||||
var orgId = orgSeeder.Seed(
|
||||
name: "Org",
|
||||
domain: domain,
|
||||
users: userCount,
|
||||
usersStatus: OrganizationUserStatusType.Invited);
|
||||
|
||||
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount);
|
||||
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||
|
||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||
collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
|
||||
groupsSeeder.AddToOrganization(orgId, groupCount, orgUserIds, 0);
|
||||
@@ -77,13 +77,13 @@ public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutpu
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var db = factory.GetDatabaseContext();
|
||||
using var scope = factory.Services.CreateScope();
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, "Org", domain, userCount);
|
||||
|
||||
var orgSeeder = new OrganizationWithUsersRecipe(db);
|
||||
var collectionsSeeder = new CollectionsRecipe(db);
|
||||
var groupsSeeder = new GroupsRecipe(db);
|
||||
|
||||
var domain = OrganizationTestHelpers.GenerateRandomDomain();
|
||||
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
|
||||
|
||||
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
|
||||
collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
|
||||
groupsSeeder.AddToOrganization(orgId, groupCount, orgUserIds, 0);
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using AutoMapper;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.RustSDK;
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using Bit.Seeder.Options;
|
||||
using Bit.Seeder.Recipes;
|
||||
using CommandDotNet;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.DbSeederUtility;
|
||||
@@ -13,32 +19,91 @@ public class Program
|
||||
.Run(args);
|
||||
}
|
||||
|
||||
[Command("organization", Description = "Seed an organization with users and optional ciphers")]
|
||||
[Command("organization", Description = "Seed an organization and organization users")]
|
||||
public void Organization(
|
||||
[Option('n', "name", Description = "Name of organization")]
|
||||
[Option('n', "Name", Description = "Name of organization")]
|
||||
string name,
|
||||
[Option('u', "users", Description = "Number of users to generate")]
|
||||
int users,
|
||||
[Option('d', "domain", Description = "Email domain for users")]
|
||||
string domain
|
||||
)
|
||||
{
|
||||
// Create service provider with necessary services
|
||||
var services = new ServiceCollection();
|
||||
ServiceCollectionExtension.ConfigureServices(services);
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
// Get a scoped DB context
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var scopedServices = scope.ServiceProvider;
|
||||
var db = scopedServices.GetRequiredService<DatabaseContext>();
|
||||
|
||||
var recipe = new OrganizationWithUsersRecipe(db);
|
||||
recipe.Seed(name: name, domain: domain, users: users);
|
||||
}
|
||||
|
||||
[Command("vault-organization", Description = "Seed an organization with users and encrypted vault data (ciphers, collections, groups)")]
|
||||
public void VaultOrganization(
|
||||
[Option('n', "name", Description = "Name of organization")]
|
||||
string name,
|
||||
[Option('u', "users", Description = "Number of users to generate (minimum 1)")]
|
||||
int users,
|
||||
[Option('d', "domain", Description = "Email domain for users")]
|
||||
string domain,
|
||||
[Option('c', "ciphers", Description = "Number of login ciphers to create")]
|
||||
int ciphers = 0,
|
||||
[Option('c', "ciphers", Description = "Number of login ciphers to create (required, minimum 1)")]
|
||||
int ciphers,
|
||||
[Option('g', "groups", Description = "Number of groups to create (required, minimum 1)")]
|
||||
int groups,
|
||||
[Option('m', "mix-user-statuses", Description = "Use realistic status mix (85% confirmed, 5% each invited/accepted/revoked). Requires >= 10 users.")]
|
||||
bool mixStatuses = false,
|
||||
[Option('o', "org-structure", Description = "Org structure for collections: Traditional, Spotify, or Modern")]
|
||||
string? structure = null
|
||||
)
|
||||
{
|
||||
var structureModel = ParseStructureModel(structure);
|
||||
if (users < 1)
|
||||
{
|
||||
throw new ArgumentException("Users must be at least 1. Use another command for orgs without users.");
|
||||
}
|
||||
|
||||
if (ciphers < 1)
|
||||
{
|
||||
throw new ArgumentException("Ciphers must be at least 1. Use another command for orgs without vault data.");
|
||||
}
|
||||
|
||||
if (groups < 1)
|
||||
{
|
||||
throw new ArgumentException("Groups must be at least 1. Use another command for orgs without groups.");
|
||||
}
|
||||
|
||||
var structureModel = ParseOrgStructure(structure);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
ServiceCollectionExtension.ConfigureServices(services);
|
||||
var serviceProvider = services.BuildServiceProvider();
|
||||
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
OrganizationWithUsersRecipe.SeedFromServices(scope.ServiceProvider, name, domain, users, ciphers,
|
||||
structureModel: structureModel);
|
||||
var scopedServices = scope.ServiceProvider;
|
||||
|
||||
var recipe = new OrganizationWithVaultRecipe(
|
||||
scopedServices.GetRequiredService<DatabaseContext>(),
|
||||
scopedServices.GetRequiredService<IMapper>(),
|
||||
scopedServices.GetRequiredService<RustSdkService>(),
|
||||
scopedServices.GetRequiredService<IPasswordHasher<User>>());
|
||||
|
||||
recipe.Seed(new OrganizationVaultOptions
|
||||
{
|
||||
Name = name,
|
||||
Domain = domain,
|
||||
Users = users,
|
||||
Ciphers = ciphers,
|
||||
Groups = groups,
|
||||
RealisticStatusMix = mixStatuses,
|
||||
StructureModel = structureModel
|
||||
});
|
||||
}
|
||||
|
||||
private static OrgStructureModel? ParseStructureModel(string? structure)
|
||||
private static OrgStructureModel? ParseOrgStructure(string? structure)
|
||||
{
|
||||
if (string.IsNullOrEmpty(structure))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
namespace Bit.Seeder.Data.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Password strength levels for test data generation.
|
||||
/// Password strength levels aligned with zxcvbn scoring (0-4).
|
||||
/// </summary>
|
||||
public enum PasswordStrength { Weak, Medium, Strong, Mixed }
|
||||
public enum PasswordStrength
|
||||
{
|
||||
/// <summary>Score 0: Too guessable (< 10³ guesses)</summary>
|
||||
VeryWeak = 0,
|
||||
|
||||
/// <summary>Score 1: Very guessable (< 10⁶ guesses)</summary>
|
||||
Weak = 1,
|
||||
|
||||
/// <summary>Score 2: Somewhat guessable (< 10⁸ guesses)</summary>
|
||||
Fair = 2,
|
||||
|
||||
/// <summary>Score 3: Safely unguessable (< 10¹⁰ guesses)</summary>
|
||||
Strong = 3,
|
||||
|
||||
/// <summary>Score 4: Very unguessable (≥ 10¹⁰ guesses)</summary>
|
||||
VeryStrong = 4,
|
||||
|
||||
/// <summary>Realistic distribution based on breach data statistics.</summary>
|
||||
Realistic = 99
|
||||
}
|
||||
|
||||
@@ -3,64 +3,145 @@
|
||||
namespace Bit.Seeder.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Password collections by strength level for realistic test data.
|
||||
/// Password collections by zxcvbn strength level (0-4) for realistic test data.
|
||||
/// </summary>
|
||||
internal static class Passwords
|
||||
{
|
||||
/// <summary>
|
||||
/// Top breached passwords - use for security testing scenarios.
|
||||
/// Score 0 - Too guessable: keyboard walks, simple sequences, single words.
|
||||
/// </summary>
|
||||
public static readonly string[] VeryWeak =
|
||||
[
|
||||
"password", "123456", "qwerty", "abc123", "letmein",
|
||||
"admin", "welcome", "monkey", "dragon", "master",
|
||||
"111111", "baseball", "iloveyou", "trustno1", "sunshine",
|
||||
"princess", "football", "shadow", "superman", "michael",
|
||||
"password1", "123456789", "12345678", "1234567", "12345",
|
||||
"qwerty123", "1q2w3e4r", "123123", "000000", "654321"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Score 1 - Very guessable: common patterns with minor complexity.
|
||||
/// </summary>
|
||||
public static readonly string[] Weak =
|
||||
[
|
||||
"password", "123456", "qwerty", "abc123", "letmein", "welcome", "admin", "dragon", "sunshine", "princess",
|
||||
"football", "master", "shadow", "superman", "trustno1", "iloveyou", "passw0rd", "p@ssw0rd", "welcome1", "Password1",
|
||||
"qwerty123", "123qwe", "1q2w3e", "password123", "12345678", "111111", "1234567890", "monkey", "baseball", "access"
|
||||
"Password1", "Qwerty123", "Welcome1", "Admin123", "Letmein1",
|
||||
"Dragon123", "Master123", "Shadow123", "Michael1", "Jennifer1",
|
||||
"abc123!", "pass123!", "test1234", "hello123", "love1234",
|
||||
"money123", "secret1", "access1", "login123", "super123",
|
||||
"changeme", "temp1234", "guest123", "user1234", "pass1234",
|
||||
"default1", "sample12", "demo1234", "trial123", "secure1"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Meets basic complexity requirements but follows predictable patterns (season+year, name+numbers).
|
||||
/// Score 2 - Somewhat guessable: meets basic complexity but predictable patterns.
|
||||
/// </summary>
|
||||
public static readonly string[] Medium =
|
||||
public static readonly string[] Fair =
|
||||
[
|
||||
"Summer2024!", "Winter2023#", "Spring2024@", "Autumn2023$", "January2024!", "December2023#",
|
||||
"Welcome123!", "Company2024#", "Secure123!", "Access2024@", "Login123!", "Portal2024#",
|
||||
"Michael123!", "Jennifer2024@", "Robert456#", "Sarah789!",
|
||||
"Qwerty123!", "Asdfgh456@", "Zxcvbn789#",
|
||||
"Password123!", "Security2024@", "Admin123!", "User2024#", "Guest123!", "Test2024@",
|
||||
"Football123!", "Baseball2024@", "Soccer456#", "Hockey789!"
|
||||
"Summer2024!", "Winter2023#", "Spring2024@", "Autumn2023$", "January2024!",
|
||||
"Welcome123!", "Company2024#", "Secure123!", "Access2024@", "Login2024!",
|
||||
"Michael123!", "Jennifer2024@", "Robert456#", "Sarah789!", "David2024!",
|
||||
"Password123!", "Security2024@", "Admin2024!", "User2024#", "Guest123!",
|
||||
"Football123!", "Baseball2024@", "Soccer456#", "Hockey789!", "Tennis2024!",
|
||||
"NewYork2024!", "Chicago123@", "Boston2024#", "Seattle789!", "Denver2024$"
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// High-entropy passwords: random strings (password manager style) and diceware passphrases.
|
||||
/// Score 3 - Safely unguessable: good entropy, mixed character types.
|
||||
/// </summary>
|
||||
public static readonly string[] Strong =
|
||||
[
|
||||
"k#9Lm$vQ2@xR7nP!", "Yx8&mK3$pL5#wQ9@", "Nv4%jH7!bT2@sF6#", "Rm9#cX5$gW1@zK8!", "Qp3@hY6#nL9$tB2!",
|
||||
"Wz7!mF4@kS8#xC1$", "Jd2#pR9!vN5@bG7$", "Ht6@wL3#yK8!mQ4$", "Bf8$cM2@zT5#rX9!", "Lg1!nV7@sH4#pY6$",
|
||||
"Kx9#mL4$pQ7@wR2!vN5", "Yz3@hT8#bF1$cS6!nM9", "Wv5!rK2@jG9#tX4$mL7", "Qn7$sB3@yH6#pC1!zF8", "Tm2@xD5#kW9$vL4!rJ7",
|
||||
"correct-horse-battery-staple", "purple-monkey-dishwasher-lamp", "quantum-bicycle-elephant-storm",
|
||||
"velvet-thunder-crystal-forge", "neon-wizard-cosmic-river", "amber-phoenix-digital-maze",
|
||||
"silver-falcon-ancient-code", "lunar-garden-frozen-spark", "echo-prism-wandering-light", "rust-vapor-hidden-gate",
|
||||
"Brave.Tiger.Runs.Fast.42", "Blue.Ocean.Deep.Wave.17", "Swift.Eagle.Soars.High.93",
|
||||
"Calm.Forest.Green.Path.28", "Warm.Summer.Golden.Sun.61",
|
||||
"maple#stream#winter#glow", "ember@cloud@silent@peak", "frost$dawn$valley$mist", "coral!reef!azure!tide", "stone&moss&ancient&oak",
|
||||
"Kx9mL4pQ7wR2vN5hT8bF", "Yz3hT8bF1cS6nM9wK4pL", "Wv5rK2jG9tX4mL7nB3sH", "Qn7sB3yH6pC1zF8kW2xD", "Tm2xD5kW9vL4rJ7gN1cY"
|
||||
"k#9Lm$vQ2@xR7nP!", "Yx8&mK3$pL5#wQ9@", "Nv4%jH7!bT2@sF6#",
|
||||
"Rm9#cX5$gW1@zK8!", "Qp3@hY6#nL9$tB2!", "Wz7!mF4@kS8#xC1$",
|
||||
"Jd2#pR9!vN5@bG7$", "Ht6@wL3#yK8!mQ4$", "Bf8$cM2@zT5#rX9!",
|
||||
"Lg1!nV7@sH4#pY6$", "Xk5#tW8@jR2$mN9!", "Cv3@yB6#pF1$qL4!",
|
||||
"correct-horse-battery", "purple-monkey-dishwasher", "quantum-bicycle-elephant",
|
||||
"velvet-thunder-crystal", "neon-wizard-cosmic", "amber-phoenix-digital",
|
||||
"Brave.Tiger.Runs.42", "Blue.Ocean.Deep.17", "Swift.Eagle.Soars.93",
|
||||
"maple#stream#winter", "ember@cloud@silent", "frost$dawn$valley"
|
||||
];
|
||||
|
||||
/// <remarks>Must be declared after strength arrays (S3263).</remarks>
|
||||
public static readonly string[] All = [.. Weak, .. Medium, .. Strong];
|
||||
/// <summary>
|
||||
/// Score 4 - Very unguessable: high entropy, long passphrases, random strings.
|
||||
/// </summary>
|
||||
public static readonly string[] VeryStrong =
|
||||
[
|
||||
"Kx9#mL4$pQ7@wR2!vN5hT8", "Yz3@hT8#bF1$cS6!nM9wK4", "Wv5!rK2@jG9#tX4$mL7nB3",
|
||||
"Qn7$sB3@yH6#pC1!zF8kW2", "Tm2@xD5#kW9$vL4!rJ7gN1", "Pf4!nC8@bR3#yL6$hS9mV2",
|
||||
"correct-horse-battery-staple", "purple-monkey-dishwasher-lamp", "quantum-bicycle-elephant-storm",
|
||||
"velvet-thunder-crystal-forge", "neon-wizard-cosmic-river", "amber-phoenix-digital-maze",
|
||||
"silver-falcon-ancient-code", "lunar-garden-frozen-spark", "echo-prism-wandering-light",
|
||||
"Brave.Tiger.Runs.Fast.42!", "Blue.Ocean.Deep.Wave.17@", "Swift.Eagle.Soars.High.93#",
|
||||
"maple#stream#winter#glow#dawn", "ember@cloud@silent@peak@mist", "frost$dawn$valley$mist$glow",
|
||||
"7hK$mN2@pL9#xR4!wQ8vB5&jF", "3yT@nC7#bS1$kW6!mH9rL2%xD", "9pF!vK4@jR8#tN3$yB7mL1&wS"
|
||||
];
|
||||
|
||||
/// <summary>All passwords combined for mixed/random selection.</summary>
|
||||
public static readonly string[] All = [.. VeryWeak, .. Weak, .. Fair, .. Strong, .. VeryStrong];
|
||||
|
||||
/// <summary>
|
||||
/// Realistic distribution based on breach data and security research.
|
||||
/// Sources: NordPass annual reports, Have I Been Pwned analysis, academic studies.
|
||||
/// Distribution: 25% VeryWeak, 30% Weak, 25% Fair, 15% Strong, 5% VeryStrong
|
||||
/// </summary>
|
||||
private static readonly (PasswordStrength Strength, int CumulativePercent)[] RealisticDistribution =
|
||||
[
|
||||
(PasswordStrength.VeryWeak, 25), // 25% - most common breached passwords
|
||||
(PasswordStrength.Weak, 55), // 30% - simple patterns with numbers
|
||||
(PasswordStrength.Fair, 80), // 25% - meets basic requirements
|
||||
(PasswordStrength.Strong, 95), // 15% - good passwords
|
||||
(PasswordStrength.VeryStrong, 100) // 5% - password manager users
|
||||
];
|
||||
|
||||
public static string[] GetByStrength(PasswordStrength strength) => strength switch
|
||||
{
|
||||
PasswordStrength.VeryWeak => VeryWeak,
|
||||
PasswordStrength.Weak => Weak,
|
||||
PasswordStrength.Medium => Medium,
|
||||
PasswordStrength.Fair => Fair,
|
||||
PasswordStrength.Strong => Strong,
|
||||
PasswordStrength.Mixed => All,
|
||||
PasswordStrength.VeryStrong => VeryStrong,
|
||||
PasswordStrength.Realistic => All, // For direct array access, use All
|
||||
_ => Strong
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets a password with realistic strength distribution.
|
||||
/// Uses deterministic selection based on index for reproducible test data.
|
||||
/// </summary>
|
||||
public static string GetRealisticPassword(int index)
|
||||
{
|
||||
var strength = GetRealisticStrength(index);
|
||||
var passwords = GetByStrength(strength);
|
||||
return passwords[index % passwords.Length];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a password strength following realistic distribution.
|
||||
/// Deterministic based on index for reproducible results.
|
||||
/// </summary>
|
||||
public static PasswordStrength GetRealisticStrength(int index)
|
||||
{
|
||||
// Use modulo 100 for percentage-based bucket selection
|
||||
var bucket = index % 100;
|
||||
|
||||
foreach (var (strength, cumulativePercent) in RealisticDistribution)
|
||||
{
|
||||
if (bucket < cumulativePercent)
|
||||
{
|
||||
return strength;
|
||||
}
|
||||
}
|
||||
|
||||
return PasswordStrength.Strong; // Fallback
|
||||
}
|
||||
|
||||
public static string GetPassword(PasswordStrength strength, int index)
|
||||
{
|
||||
if (strength == PasswordStrength.Realistic)
|
||||
{
|
||||
return GetRealisticPassword(index);
|
||||
}
|
||||
|
||||
var passwords = GetByStrength(strength);
|
||||
return passwords[index % passwords.Length];
|
||||
}
|
||||
|
||||
41
util/Seeder/Factories/GroupSeeder.cs
Normal file
41
util/Seeder/Factories/GroupSeeder.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Seeder.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Creates groups and group-user relationships for seeding.
|
||||
/// </summary>
|
||||
public static class GroupSeeder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a group entity for an organization.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The organization ID.</param>
|
||||
/// <param name="name">The group name.</param>
|
||||
/// <returns>A new Group entity (not persisted).</returns>
|
||||
public static Group CreateGroup(Guid organizationId, string name)
|
||||
{
|
||||
return new Group
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
OrganizationId = organizationId,
|
||||
Name = name
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a group-user relationship entity.
|
||||
/// </summary>
|
||||
/// <param name="groupId">The group ID.</param>
|
||||
/// <param name="organizationUserId">The organization user ID.</param>
|
||||
/// <returns>A new GroupUser entity (not persisted).</returns>
|
||||
public static GroupUser CreateGroupUser(Guid groupId, Guid organizationUserId)
|
||||
{
|
||||
return new GroupUser
|
||||
{
|
||||
GroupId = groupId,
|
||||
OrganizationUserId = organizationUserId
|
||||
};
|
||||
}
|
||||
}
|
||||
32
util/Seeder/Factories/OrganizationDomainSeeder.cs
Normal file
32
util/Seeder/Factories/OrganizationDomainSeeder.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
|
||||
namespace Bit.Seeder.Factories;
|
||||
|
||||
/// <summary>
|
||||
/// Creates organization domain entities for seeding.
|
||||
/// </summary>
|
||||
public static class OrganizationDomainSeeder
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a verified organization domain entity.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The organization ID.</param>
|
||||
/// <param name="domainName">The domain name (e.g., "example.com").</param>
|
||||
/// <returns>A new verified OrganizationDomain entity (not persisted).</returns>
|
||||
public static OrganizationDomain CreateVerifiedDomain(Guid organizationId, string domainName)
|
||||
{
|
||||
var domain = new OrganizationDomain
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organizationId,
|
||||
DomainName = domainName,
|
||||
Txt = Guid.NewGuid().ToString("N"),
|
||||
CreationDate = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
domain.SetVerifiedDate();
|
||||
domain.SetLastCheckedDate();
|
||||
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,28 @@ public class OrganizationSeeder
|
||||
|
||||
public static class OrganizationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an OrganizationUser with hardcoded keys (no SDK calls).
|
||||
/// Used by OrganizationWithUsersRecipe for fast user creation without encryption needs.
|
||||
/// </summary>
|
||||
public static OrganizationUser CreateOrganizationUser(
|
||||
this Organization organization, User user, OrganizationUserType type, OrganizationUserStatusType status)
|
||||
{
|
||||
var isInvited = status == OrganizationUserStatusType.Invited;
|
||||
var isConfirmed = status == OrganizationUserStatusType.Confirmed || status == OrganizationUserStatusType.Revoked;
|
||||
|
||||
return new OrganizationUser
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organization.Id,
|
||||
UserId = isInvited ? null : user.Id,
|
||||
Email = isInvited ? user.Email : null,
|
||||
Key = isConfirmed ? "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==" : null,
|
||||
Type = type,
|
||||
Status = status
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an OrganizationUser with a dynamically provided encrypted org key.
|
||||
/// The encryptedOrgKey should be generated using sdkService.GenerateUserOrganizationKey().
|
||||
|
||||
@@ -57,6 +57,27 @@ public class UserSeeder(RustSdkService sdkService, IPasswordHasher<Bit.Core.Enti
|
||||
/// </summary>
|
||||
public const string DefaultPassword = "asdfasdfasdf";
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user with hardcoded keys (no email mangling, no SDK calls).
|
||||
/// Used by OrganizationWithUsersRecipe for fast user creation without encryption needs.
|
||||
/// </summary>
|
||||
public static User CreateUserNoMangle(string email)
|
||||
{
|
||||
return new User
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = email,
|
||||
MasterPassword = "AQAAAAIAAYagAAAAEBATmF66OHMpHuHKc1CsGZQ1ltHUHyhYK+7e4re3bVFi16SOpLpDfzdFswnvFQs2Rg==",
|
||||
SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609",
|
||||
Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=",
|
||||
PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB",
|
||||
PrivateKey = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=",
|
||||
ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR",
|
||||
Kdf = KdfType.PBKDF2_SHA256,
|
||||
KdfIterations = 600_000,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user with SDK-generated cryptographic keys (no email mangling).
|
||||
/// The user can log in with email and password = "asdfasdfasdf".
|
||||
|
||||
57
util/Seeder/Options/OrganizationVaultOptions.cs
Normal file
57
util/Seeder/Options/OrganizationVaultOptions.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Bit.Seeder.Data.Enums;
|
||||
|
||||
namespace Bit.Seeder.Options;
|
||||
|
||||
/// <summary>
|
||||
/// Options for seeding an organization with vault data.
|
||||
/// </summary>
|
||||
public class OrganizationVaultOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Organization name.
|
||||
/// </summary>
|
||||
public required string Name { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Domain for user emails (e.g., "example.com").
|
||||
/// </summary>
|
||||
public required string Domain { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of member users to create.
|
||||
/// </summary>
|
||||
public required int Users { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of login ciphers to create.
|
||||
/// </summary>
|
||||
public int Ciphers { get; init; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Number of groups to create.
|
||||
/// </summary>
|
||||
public int Groups { get; init; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// When true and Users >= 10, creates a realistic mix of user statuses:
|
||||
/// 85% Confirmed, 5% Invited, 5% Accepted, 5% Revoked.
|
||||
/// When false or Users < 10, all users are Confirmed.
|
||||
/// </summary>
|
||||
public bool RealisticStatusMix { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Org structure for realistic collection names.
|
||||
/// </summary>
|
||||
public OrgStructureModel? StructureModel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Username pattern for cipher logins.
|
||||
/// </summary>
|
||||
public UsernamePatternType UsernamePattern { get; init; } = UsernamePatternType.FirstDotLast;
|
||||
|
||||
/// <summary>
|
||||
/// Password strength for cipher logins. Defaults to Realistic distribution
|
||||
/// (25% VeryWeak, 30% Weak, 25% Fair, 15% Strong, 5% VeryStrong).
|
||||
/// </summary>
|
||||
public PasswordStrength PasswordStrength { get; init; } = PasswordStrength.Realistic;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.RustSDK;
|
||||
using Bit.Seeder.Data;
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using Bit.Seeder.Factories;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
|
||||
namespace Bit.Seeder.Recipes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates encrypted ciphers for seeding organization vaults.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Currently supports:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Login ciphers</description></item>
|
||||
/// </list>
|
||||
/// TODO: Add support for Card, Identity, and SecureNote cipher types.
|
||||
/// </remarks>
|
||||
public class CiphersRecipe(DatabaseContext db, RustSdkService sdkService)
|
||||
{
|
||||
private readonly CipherSeeder _cipherSeeder = new(sdkService);
|
||||
|
||||
public List<Guid> AddLoginCiphersToOrganization(
|
||||
Guid organizationId,
|
||||
string orgKeyBase64,
|
||||
List<Guid> collectionIds,
|
||||
int? count = null,
|
||||
bool useEnterpriseUrls = false)
|
||||
{
|
||||
// Delegate to the new system - Enterprise filter for enterprise URLs, Consumer for popular
|
||||
var companyType = useEnterpriseUrls ? CompanyType.Enterprise : CompanyType.Consumer;
|
||||
return AddLoginCiphersToOrganization(
|
||||
organizationId,
|
||||
orgKeyBase64,
|
||||
collectionIds,
|
||||
count,
|
||||
companyType,
|
||||
region: null,
|
||||
UsernamePatternType.FLast,
|
||||
PasswordStrength.Weak);
|
||||
}
|
||||
|
||||
public List<Guid> AddLoginCiphersToOrganization(
|
||||
Guid organizationId,
|
||||
string orgKeyBase64,
|
||||
List<Guid> collectionIds,
|
||||
int? count,
|
||||
CompanyType? companyType,
|
||||
GeographicRegion? region,
|
||||
UsernamePatternType usernamePattern = UsernamePatternType.FirstDotLast,
|
||||
PasswordStrength passwordStrength = PasswordStrength.Strong)
|
||||
{
|
||||
var companies = Companies.Filter(companyType, region);
|
||||
if (companies.Length == 0)
|
||||
{
|
||||
companies = Companies.All;
|
||||
}
|
||||
|
||||
var passwords = Passwords.GetByStrength(passwordStrength);
|
||||
var cipherCount = count ?? companies.Length;
|
||||
var usernameGenerator = new UsernameGenerator(organizationId.GetHashCode(), usernamePattern, region);
|
||||
|
||||
var ciphers = Enumerable.Range(0, cipherCount)
|
||||
.Select(i =>
|
||||
{
|
||||
var company = companies[i % companies.Length];
|
||||
return _cipherSeeder.CreateOrganizationLoginCipher(
|
||||
organizationId,
|
||||
orgKeyBase64,
|
||||
name: $"{company.Name} ({company.Category})",
|
||||
username: usernameGenerator.GenerateVaried(company, i),
|
||||
password: passwords[i % passwords.Length],
|
||||
uri: $"https://{company.Domain}");
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return SaveCiphersWithCollections(ciphers, collectionIds);
|
||||
}
|
||||
|
||||
private List<Guid> SaveCiphersWithCollections(List<Cipher> ciphers, List<Guid> collectionIds)
|
||||
{
|
||||
if (ciphers.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
db.BulkCopy(ciphers);
|
||||
|
||||
if (collectionIds.Count > 0)
|
||||
{
|
||||
var collectionCiphers = ciphers.SelectMany((cipher, i) =>
|
||||
{
|
||||
var primary = new CollectionCipher
|
||||
{
|
||||
CipherId = cipher.Id,
|
||||
CollectionId = collectionIds[i % collectionIds.Count]
|
||||
};
|
||||
|
||||
// Every 3rd cipher gets assigned to an additional collection
|
||||
if (i % 3 == 0 && collectionIds.Count > 1)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
primary,
|
||||
new CollectionCipher
|
||||
{
|
||||
CipherId = cipher.Id,
|
||||
CollectionId = collectionIds[(i + 1) % collectionIds.Count]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return [primary];
|
||||
}).ToList();
|
||||
|
||||
db.BulkCopy(collectionCiphers);
|
||||
}
|
||||
|
||||
return ciphers.Select(c => c.Id).ToList();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,38 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.RustSDK;
|
||||
using Bit.Seeder.Data;
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using Bit.Seeder.Factories;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
|
||||
namespace Bit.Seeder.Recipes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates collections for seeding organization vaults.
|
||||
/// </summary>
|
||||
public class CollectionsRecipe(DatabaseContext db, RustSdkService? sdkService = null)
|
||||
public class CollectionsRecipe(DatabaseContext db)
|
||||
{
|
||||
private readonly CollectionSeeder? _collectionSeeder = sdkService != null ? new(sdkService) : null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates collections from an organizational structure (e.g., Traditional departments, Spotify tribes).
|
||||
/// Collection names are properly encrypted.
|
||||
/// </summary>
|
||||
public List<Guid> AddFromStructure(
|
||||
Guid organizationId,
|
||||
string orgKeyBase64,
|
||||
OrgStructureModel model,
|
||||
List<Guid> organizationUserIds,
|
||||
int maxUsersWithRelationships = 1000)
|
||||
{
|
||||
var structure = OrgStructures.GetStructure(model);
|
||||
|
||||
var collections = structure.Units
|
||||
.Select(unit => _collectionSeeder!.CreateCollection(organizationId, orgKeyBase64, unit.Name))
|
||||
.ToList();
|
||||
|
||||
db.BulkCopy(collections);
|
||||
|
||||
if (collections.Count > 0 && organizationUserIds.Count > 0 && maxUsersWithRelationships > 0)
|
||||
{
|
||||
var collectionUsers = BuildCollectionUserRelationships(collections, organizationUserIds, maxUsersWithRelationships);
|
||||
db.BulkCopy(collectionUsers);
|
||||
}
|
||||
|
||||
return collections.Select(c => c.Id).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds generic numbered collections (unencrypted names - use AddFromStructure for realistic data).
|
||||
/// Adds collections to an organization and creates relationships between users and collections.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The ID of the organization to add collections to.</param>
|
||||
/// <param name="collections">The number of collections to add.</param>
|
||||
/// <param name="organizationUserIds">The IDs of the users to create relationships with.</param>
|
||||
/// <param name="maxUsersWithRelationships">The maximum number of users to create relationships with.</param>
|
||||
public List<Guid> AddToOrganization(Guid organizationId, int collections, List<Guid> organizationUserIds, int maxUsersWithRelationships = 1000)
|
||||
{
|
||||
var collectionList = Enumerable.Range(0, collections)
|
||||
.Select(i => new Core.Entities.Collection
|
||||
var collectionList = CreateAndSaveCollections(organizationId, collections);
|
||||
|
||||
if (collectionList.Any())
|
||||
{
|
||||
CreateAndSaveCollectionUserRelationships(collectionList, organizationUserIds, maxUsersWithRelationships);
|
||||
}
|
||||
|
||||
return collectionList.Select(c => c.Id).ToList();
|
||||
}
|
||||
|
||||
private List<Core.Entities.Collection> CreateAndSaveCollections(Guid organizationId, int count)
|
||||
{
|
||||
var collectionList = new List<Core.Entities.Collection>();
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
collectionList.Add(new Core.Entities.Collection
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
OrganizationId = organizationId,
|
||||
@@ -58,44 +40,83 @@ public class CollectionsRecipe(DatabaseContext db, RustSdkService? sdkService =
|
||||
Type = CollectionType.SharedCollection,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow
|
||||
})
|
||||
.ToList();
|
||||
|
||||
db.BulkCopy(collectionList);
|
||||
|
||||
if (collectionList.Count > 0 && organizationUserIds.Count > 0 && maxUsersWithRelationships > 0)
|
||||
{
|
||||
var collectionUsers = BuildCollectionUserRelationships(collectionList, organizationUserIds, maxUsersWithRelationships);
|
||||
db.BulkCopy(collectionUsers);
|
||||
});
|
||||
}
|
||||
|
||||
return collectionList.Select(c => c.Id).ToList();
|
||||
if (collectionList.Any())
|
||||
{
|
||||
db.BulkCopy(collectionList);
|
||||
}
|
||||
|
||||
return collectionList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates user-to-collection relationships with varied assignment patterns.
|
||||
/// Each user gets 1-3 collections (cycling). First collection has Manage rights.
|
||||
/// </summary>
|
||||
private static List<Core.Entities.CollectionUser> BuildCollectionUserRelationships(
|
||||
private void CreateAndSaveCollectionUserRelationships(
|
||||
List<Core.Entities.Collection> collections,
|
||||
List<Guid> organizationUserIds,
|
||||
int maxUsersWithRelationships)
|
||||
{
|
||||
return organizationUserIds
|
||||
.Take(maxUsersWithRelationships)
|
||||
.SelectMany((orgUserId, userIndex) =>
|
||||
if (!organizationUserIds.Any() || maxUsersWithRelationships <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var collectionUsers = BuildCollectionUserRelationships(collections, organizationUserIds, maxUsersWithRelationships);
|
||||
|
||||
if (collectionUsers.Any())
|
||||
{
|
||||
db.BulkCopy(collectionUsers);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates user-to-collection relationships with varied assignment patterns for realistic test data.
|
||||
/// Each user gets 1-3 collections based on a rotating pattern.
|
||||
/// </summary>
|
||||
private List<Core.Entities.CollectionUser> BuildCollectionUserRelationships(
|
||||
List<Core.Entities.Collection> collections,
|
||||
List<Guid> organizationUserIds,
|
||||
int maxUsersWithRelationships)
|
||||
{
|
||||
var maxRelationships = Math.Min(organizationUserIds.Count, maxUsersWithRelationships);
|
||||
var collectionUsers = new List<Core.Entities.CollectionUser>();
|
||||
|
||||
for (var i = 0; i < maxRelationships; i++)
|
||||
{
|
||||
var orgUserId = organizationUserIds[i];
|
||||
var userCollectionAssignments = CreateCollectionAssignmentsForUser(collections, orgUserId, i);
|
||||
collectionUsers.AddRange(userCollectionAssignments);
|
||||
}
|
||||
|
||||
return collectionUsers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns collections to a user with varying permissions.
|
||||
/// Pattern: 1-3 collections per user (cycles: 1, 2, 3, 1, 2, 3...).
|
||||
/// First collection has Manage rights, subsequent ones are ReadOnly.
|
||||
/// </summary>
|
||||
private List<Core.Entities.CollectionUser> CreateCollectionAssignmentsForUser(
|
||||
List<Core.Entities.Collection> collections,
|
||||
Guid organizationUserId,
|
||||
int userIndex)
|
||||
{
|
||||
var assignments = new List<Core.Entities.CollectionUser>();
|
||||
var userCollectionCount = (userIndex % 3) + 1; // Cycles through 1, 2, or 3 collections
|
||||
|
||||
for (var j = 0; j < userCollectionCount; j++)
|
||||
{
|
||||
var collectionIndex = (userIndex + j) % collections.Count; // Distribute across available collections
|
||||
assignments.Add(new Core.Entities.CollectionUser
|
||||
{
|
||||
var collectionCount = (userIndex % 3) + 1; // Cycles through 1, 2, or 3
|
||||
return Enumerable.Range(0, collectionCount)
|
||||
.Select(j => new Core.Entities.CollectionUser
|
||||
{
|
||||
CollectionId = collections[(userIndex + j) % collections.Count].Id,
|
||||
OrganizationUserId = orgUserId,
|
||||
ReadOnly = j > 0,
|
||||
HidePasswords = false,
|
||||
Manage = j == 0
|
||||
});
|
||||
})
|
||||
.ToList();
|
||||
CollectionId = collections[collectionIndex].Id,
|
||||
OrganizationUserId = organizationUserId,
|
||||
ReadOnly = j > 0, // First assignment gets write access
|
||||
HidePasswords = false,
|
||||
Manage = j == 0 // First assignment gets manage permissions
|
||||
});
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,30 +15,80 @@ public class GroupsRecipe(DatabaseContext db)
|
||||
/// <param name="maxUsersWithRelationships">The maximum number of users to create relationships with.</param>
|
||||
public List<Guid> AddToOrganization(Guid organizationId, int groups, List<Guid> organizationUserIds, int maxUsersWithRelationships = 1000)
|
||||
{
|
||||
var groupList = Enumerable.Range(0, groups)
|
||||
.Select(i => new Core.AdminConsole.Entities.Group
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
OrganizationId = organizationId,
|
||||
Name = $"Group {i + 1}"
|
||||
})
|
||||
.ToList();
|
||||
var groupList = CreateAndSaveGroups(organizationId, groups);
|
||||
|
||||
db.BulkCopy(groupList);
|
||||
|
||||
if (groupList.Count > 0 && organizationUserIds.Count > 0 && maxUsersWithRelationships > 0)
|
||||
if (groupList.Any())
|
||||
{
|
||||
var groupUsers = organizationUserIds
|
||||
.Take(maxUsersWithRelationships)
|
||||
.Select((orgUserId, i) => new Core.AdminConsole.Entities.GroupUser
|
||||
{
|
||||
GroupId = groupList[i % groupList.Count].Id,
|
||||
OrganizationUserId = orgUserId
|
||||
})
|
||||
.ToList();
|
||||
db.BulkCopy(groupUsers);
|
||||
CreateAndSaveGroupUserRelationships(groupList, organizationUserIds, maxUsersWithRelationships);
|
||||
}
|
||||
|
||||
return groupList.Select(g => g.Id).ToList();
|
||||
}
|
||||
|
||||
private List<Core.AdminConsole.Entities.Group> CreateAndSaveGroups(Guid organizationId, int count)
|
||||
{
|
||||
var groupList = new List<Core.AdminConsole.Entities.Group>();
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
groupList.Add(new Core.AdminConsole.Entities.Group
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb(),
|
||||
OrganizationId = organizationId,
|
||||
Name = $"Group {i + 1}"
|
||||
});
|
||||
}
|
||||
|
||||
if (groupList.Any())
|
||||
{
|
||||
db.BulkCopy(groupList);
|
||||
}
|
||||
|
||||
return groupList;
|
||||
}
|
||||
|
||||
private void CreateAndSaveGroupUserRelationships(
|
||||
List<Core.AdminConsole.Entities.Group> groups,
|
||||
List<Guid> organizationUserIds,
|
||||
int maxUsersWithRelationships)
|
||||
{
|
||||
if (!organizationUserIds.Any() || maxUsersWithRelationships <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var groupUsers = BuildGroupUserRelationships(groups, organizationUserIds, maxUsersWithRelationships);
|
||||
|
||||
if (groupUsers.Any())
|
||||
{
|
||||
db.BulkCopy(groupUsers);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates user-to-group relationships with distributed assignment patterns for realistic test data.
|
||||
/// Each user is assigned to one group, distributed evenly across available groups.
|
||||
/// </summary>
|
||||
private List<Core.AdminConsole.Entities.GroupUser> BuildGroupUserRelationships(
|
||||
List<Core.AdminConsole.Entities.Group> groups,
|
||||
List<Guid> organizationUserIds,
|
||||
int maxUsersWithRelationships)
|
||||
{
|
||||
var maxRelationships = Math.Min(organizationUserIds.Count, maxUsersWithRelationships);
|
||||
var groupUsers = new List<Core.AdminConsole.Entities.GroupUser>();
|
||||
|
||||
for (var i = 0; i < maxRelationships; i++)
|
||||
{
|
||||
var orgUserId = organizationUserIds[i];
|
||||
var groupIndex = i % groups.Count; // Round-robin distribution across groups
|
||||
|
||||
groupUsers.Add(new Core.AdminConsole.Entities.GroupUser
|
||||
{
|
||||
GroupId = groups[groupIndex].Id,
|
||||
OrganizationUserId = orgUserId
|
||||
});
|
||||
}
|
||||
|
||||
return groupUsers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,138 +1,38 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.RustSDK;
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using Bit.Seeder.Factories;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using EfOrganization = Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization;
|
||||
using EfOrganizationUser = Bit.Infrastructure.EntityFramework.Models.OrganizationUser;
|
||||
using EfUser = Bit.Infrastructure.EntityFramework.Models.User;
|
||||
|
||||
namespace Bit.Seeder.Recipes;
|
||||
|
||||
public class OrganizationWithUsersRecipe(
|
||||
DatabaseContext db,
|
||||
IMapper mapper,
|
||||
RustSdkService sdkService,
|
||||
IPasswordHasher<User> passwordHasher)
|
||||
public class OrganizationWithUsersRecipe(DatabaseContext db)
|
||||
{
|
||||
public static Guid SeedFromServices(
|
||||
IServiceProvider services,
|
||||
string name,
|
||||
string domain,
|
||||
int users,
|
||||
int ciphers = 0,
|
||||
OrganizationUserStatusType usersStatus = OrganizationUserStatusType.Confirmed,
|
||||
OrgStructureModel? structureModel = null)
|
||||
{
|
||||
var db = services.GetRequiredService<DatabaseContext>();
|
||||
var mapper = services.GetRequiredService<IMapper>();
|
||||
var sdkService = services.GetRequiredService<RustSdkService>();
|
||||
var passwordHasher = services.GetRequiredService<IPasswordHasher<User>>();
|
||||
|
||||
var recipe = new OrganizationWithUsersRecipe(db, mapper, sdkService, passwordHasher);
|
||||
return recipe.Seed(name, domain, users, ciphers, usersStatus, structureModel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds an organization with users and optionally encrypted ciphers.
|
||||
/// Users can log in with their email and password "asdfasdfasdf".
|
||||
/// Organization and user keys are generated dynamically for each run.
|
||||
/// </summary>
|
||||
/// <param name="structureModel">Optional org structure for realistic collection names (e.g., Traditional departments, Spotify tribes).</param>
|
||||
public Guid Seed(
|
||||
string name,
|
||||
string domain,
|
||||
int users,
|
||||
int ciphers = 0,
|
||||
OrganizationUserStatusType usersStatus = OrganizationUserStatusType.Confirmed,
|
||||
OrgStructureModel? structureModel = null)
|
||||
public Guid Seed(string name, string domain, int users, OrganizationUserStatusType usersStatus = OrganizationUserStatusType.Confirmed)
|
||||
{
|
||||
var seats = Math.Max(users + 1, 1000);
|
||||
var orgKeys = sdkService.GenerateOrganizationKeys();
|
||||
|
||||
var organization = OrganizationSeeder.CreateEnterprise(name, domain, seats);
|
||||
organization.PublicKey = orgKeys.PublicKey;
|
||||
organization.PrivateKey = orgKeys.PrivateKey;
|
||||
var ownerUser = UserSeeder.CreateUserNoMangle($"owner@{domain}");
|
||||
var ownerOrgUser = organization.CreateOrganizationUser(ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed);
|
||||
|
||||
var ownerUser = UserSeeder.CreateUserWithSdkKeys($"owner@{domain}", sdkService, passwordHasher);
|
||||
|
||||
var ownerOrgKey = sdkService.GenerateUserOrganizationKey(ownerUser.PublicKey!, orgKeys.Key);
|
||||
var ownerOrgUser = organization.CreateOrganizationUserWithKey(
|
||||
ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed, ownerOrgKey);
|
||||
|
||||
var memberUsers = new List<User>();
|
||||
var memberOrgUsers = new List<OrganizationUser>();
|
||||
var additionalUsers = new List<User>();
|
||||
var additionalOrgUsers = new List<OrganizationUser>();
|
||||
for (var i = 0; i < users; i++)
|
||||
{
|
||||
var memberUser = UserSeeder.CreateUserWithSdkKeys($"user{i}@{domain}", sdkService, passwordHasher);
|
||||
memberUsers.Add(memberUser);
|
||||
|
||||
var memberOrgKey = (usersStatus == OrganizationUserStatusType.Confirmed ||
|
||||
usersStatus == OrganizationUserStatusType.Revoked)
|
||||
? sdkService.GenerateUserOrganizationKey(memberUser.PublicKey!, orgKeys.Key)
|
||||
: null;
|
||||
|
||||
memberOrgUsers.Add(organization.CreateOrganizationUserWithKey(
|
||||
memberUser, OrganizationUserType.User, usersStatus, memberOrgKey));
|
||||
var additionalUser = UserSeeder.CreateUserNoMangle($"user{i}@{domain}");
|
||||
additionalUsers.Add(additionalUser);
|
||||
additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser, OrganizationUserType.User, usersStatus));
|
||||
}
|
||||
|
||||
db.Add(mapper.Map<EfOrganization>(organization));
|
||||
db.Add(mapper.Map<EfUser>(ownerUser));
|
||||
db.Add(mapper.Map<EfOrganizationUser>(ownerOrgUser));
|
||||
|
||||
// BulkCopy for performance with large user counts
|
||||
var efMemberUsers = memberUsers.Select(u => mapper.Map<EfUser>(u)).ToList();
|
||||
var efMemberOrgUsers = memberOrgUsers.Select(ou => mapper.Map<EfOrganizationUser>(ou)).ToList();
|
||||
db.BulkCopy(efMemberUsers);
|
||||
db.BulkCopy(efMemberOrgUsers);
|
||||
db.Add(organization);
|
||||
db.Add(ownerUser);
|
||||
db.Add(ownerOrgUser);
|
||||
|
||||
db.SaveChanges();
|
||||
|
||||
// Create collections - either from org structure or single default
|
||||
var allOrgUserIds = memberOrgUsers
|
||||
.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)
|
||||
.Select(ou => ou.Id)
|
||||
.Prepend(ownerOrgUser.Id)
|
||||
.ToList();
|
||||
|
||||
List<Guid> collectionIds;
|
||||
if (structureModel.HasValue)
|
||||
{
|
||||
var collectionsRecipe = new CollectionsRecipe(db, sdkService);
|
||||
collectionIds = collectionsRecipe.AddFromStructure(
|
||||
organization.Id,
|
||||
orgKeys.Key,
|
||||
structureModel.Value,
|
||||
allOrgUserIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
var defaultCollection = new CollectionSeeder(sdkService)
|
||||
.CreateCollection(organization.Id, orgKeys.Key, "Default Collection");
|
||||
db.BulkCopy(new[] { defaultCollection });
|
||||
|
||||
var collectionUsers = allOrgUserIds
|
||||
.Select((id, i) => CollectionSeeder.CreateCollectionUser(defaultCollection.Id, id, manage: i == 0))
|
||||
.ToList();
|
||||
db.BulkCopy(collectionUsers);
|
||||
|
||||
collectionIds = [defaultCollection.Id];
|
||||
}
|
||||
|
||||
if (ciphers > 0)
|
||||
{
|
||||
var cipherRecipe = new CiphersRecipe(db, sdkService);
|
||||
cipherRecipe.AddLoginCiphersToOrganization(
|
||||
organization.Id,
|
||||
orgKeys.Key,
|
||||
collectionIds,
|
||||
count: ciphers);
|
||||
}
|
||||
// Use LinqToDB's BulkCopy for significant better performance
|
||||
db.BulkCopy(additionalUsers);
|
||||
db.BulkCopy(additionalOrgUsers);
|
||||
|
||||
return organization.Id;
|
||||
}
|
||||
|
||||
255
util/Seeder/Recipes/OrganizationWithVaultRecipe.cs
Normal file
255
util/Seeder/Recipes/OrganizationWithVaultRecipe.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.RustSDK;
|
||||
using Bit.Seeder.Data;
|
||||
using Bit.Seeder.Data.Enums;
|
||||
using Bit.Seeder.Factories;
|
||||
using Bit.Seeder.Options;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using EfOrganization = Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization;
|
||||
using EfOrganizationUser = Bit.Infrastructure.EntityFramework.Models.OrganizationUser;
|
||||
using EfUser = Bit.Infrastructure.EntityFramework.Models.User;
|
||||
|
||||
namespace Bit.Seeder.Recipes;
|
||||
|
||||
/// <summary>
|
||||
/// Seeds an organization with users, collections, groups, and encrypted ciphers.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This recipe creates a complete organization with vault data in a single operation.
|
||||
/// All entity creation is delegated to factories. Users can log in with their email
|
||||
/// and password "asdfasdfasdf". Organization and user keys are generated dynamically.
|
||||
/// </remarks>
|
||||
public class OrganizationWithVaultRecipe(
|
||||
DatabaseContext db,
|
||||
IMapper mapper,
|
||||
RustSdkService sdkService,
|
||||
IPasswordHasher<User> passwordHasher)
|
||||
{
|
||||
private readonly CollectionSeeder _collectionSeeder = new(sdkService);
|
||||
private readonly CipherSeeder _cipherSeeder = new(sdkService);
|
||||
|
||||
/// <summary>
|
||||
/// Seeds an organization with users, collections, groups, and encrypted ciphers.
|
||||
/// </summary>
|
||||
/// <param name="options">Options specifying what to seed.</param>
|
||||
/// <returns>The organization ID.</returns>
|
||||
public Guid Seed(OrganizationVaultOptions options)
|
||||
{
|
||||
var seats = Math.Max(options.Users + 1, 1000);
|
||||
var orgKeys = sdkService.GenerateOrganizationKeys();
|
||||
|
||||
// Create organization via factory
|
||||
var organization = OrganizationSeeder.CreateEnterprise(options.Name, options.Domain, seats);
|
||||
organization.PublicKey = orgKeys.PublicKey;
|
||||
organization.PrivateKey = orgKeys.PrivateKey;
|
||||
|
||||
// Create owner user via factory
|
||||
var ownerUser = UserSeeder.CreateUserWithSdkKeys($"owner@{options.Domain}", sdkService, passwordHasher);
|
||||
var ownerOrgKey = sdkService.GenerateUserOrganizationKey(ownerUser.PublicKey!, orgKeys.Key);
|
||||
var ownerOrgUser = organization.CreateOrganizationUserWithKey(
|
||||
ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed, ownerOrgKey);
|
||||
|
||||
// Create member users via factory
|
||||
var memberUsers = new List<User>();
|
||||
var memberOrgUsers = new List<OrganizationUser>();
|
||||
var useRealisticMix = options.RealisticStatusMix && options.Users >= 10;
|
||||
|
||||
for (var i = 0; i < options.Users; i++)
|
||||
{
|
||||
var memberUser = UserSeeder.CreateUserWithSdkKeys($"user{i}@{options.Domain}", sdkService, passwordHasher);
|
||||
memberUsers.Add(memberUser);
|
||||
|
||||
var status = useRealisticMix
|
||||
? GetRealisticStatus(i, options.Users)
|
||||
: OrganizationUserStatusType.Confirmed;
|
||||
|
||||
var memberOrgKey = (status == OrganizationUserStatusType.Confirmed ||
|
||||
status == OrganizationUserStatusType.Revoked)
|
||||
? sdkService.GenerateUserOrganizationKey(memberUser.PublicKey!, orgKeys.Key)
|
||||
: null;
|
||||
|
||||
memberOrgUsers.Add(organization.CreateOrganizationUserWithKey(
|
||||
memberUser, OrganizationUserType.User, status, memberOrgKey));
|
||||
}
|
||||
|
||||
// Persist organization and users
|
||||
db.Add(mapper.Map<EfOrganization>(organization));
|
||||
db.Add(mapper.Map<EfUser>(ownerUser));
|
||||
db.Add(mapper.Map<EfOrganizationUser>(ownerOrgUser));
|
||||
|
||||
var efMemberUsers = memberUsers.Select(u => mapper.Map<EfUser>(u)).ToList();
|
||||
var efMemberOrgUsers = memberOrgUsers.Select(ou => mapper.Map<EfOrganizationUser>(ou)).ToList();
|
||||
db.BulkCopy(efMemberUsers);
|
||||
db.BulkCopy(efMemberOrgUsers);
|
||||
db.SaveChanges();
|
||||
|
||||
// Get confirmed user IDs for relationships
|
||||
var confirmedOrgUserIds = memberOrgUsers
|
||||
.Where(ou => ou.Status == OrganizationUserStatusType.Confirmed)
|
||||
.Select(ou => ou.Id)
|
||||
.Prepend(ownerOrgUser.Id)
|
||||
.ToList();
|
||||
|
||||
var collectionIds = CreateCollections(organization.Id, orgKeys.Key, options.StructureModel, confirmedOrgUserIds);
|
||||
CreateGroups(organization.Id, options.Groups, confirmedOrgUserIds);
|
||||
CreateCiphers(organization.Id, orgKeys.Key, collectionIds, options.Ciphers, options.UsernamePattern, options.PasswordStrength);
|
||||
|
||||
return organization.Id;
|
||||
}
|
||||
|
||||
private List<Guid> CreateCollections(
|
||||
Guid organizationId,
|
||||
string orgKeyBase64,
|
||||
OrgStructureModel? structureModel,
|
||||
List<Guid> orgUserIds)
|
||||
{
|
||||
List<Collection> collections;
|
||||
|
||||
if (structureModel.HasValue)
|
||||
{
|
||||
var structure = OrgStructures.GetStructure(structureModel.Value);
|
||||
collections = structure.Units
|
||||
.Select(unit => _collectionSeeder.CreateCollection(organizationId, orgKeyBase64, unit.Name))
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
collections = [_collectionSeeder.CreateCollection(organizationId, orgKeyBase64, "Default Collection")];
|
||||
}
|
||||
|
||||
db.BulkCopy(collections);
|
||||
|
||||
// Create collection-user relationships
|
||||
if (collections.Count > 0 && orgUserIds.Count > 0)
|
||||
{
|
||||
var collectionUsers = orgUserIds
|
||||
.SelectMany((orgUserId, userIndex) =>
|
||||
{
|
||||
var collectionCount = (userIndex % 3) + 1;
|
||||
return Enumerable.Range(0, collectionCount)
|
||||
.Select(j => CollectionSeeder.CreateCollectionUser(
|
||||
collections[(userIndex + j) % collections.Count].Id,
|
||||
orgUserId,
|
||||
readOnly: j > 0,
|
||||
manage: j == 0));
|
||||
})
|
||||
.ToList();
|
||||
db.BulkCopy(collectionUsers);
|
||||
}
|
||||
|
||||
return collections.Select(c => c.Id).ToList();
|
||||
}
|
||||
|
||||
private void CreateGroups(Guid organizationId, int groupCount, List<Guid> orgUserIds)
|
||||
{
|
||||
var groupList = Enumerable.Range(0, groupCount)
|
||||
.Select(i => GroupSeeder.CreateGroup(organizationId, $"Group {i + 1}"))
|
||||
.ToList();
|
||||
|
||||
db.BulkCopy(groupList);
|
||||
|
||||
// Create group-user relationships (round-robin assignment)
|
||||
if (groupList.Count > 0 && orgUserIds.Count > 0)
|
||||
{
|
||||
var groupUsers = orgUserIds
|
||||
.Select((orgUserId, i) => GroupSeeder.CreateGroupUser(
|
||||
groupList[i % groupList.Count].Id,
|
||||
orgUserId))
|
||||
.ToList();
|
||||
db.BulkCopy(groupUsers);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateCiphers(
|
||||
Guid organizationId,
|
||||
string orgKeyBase64,
|
||||
List<Guid> collectionIds,
|
||||
int cipherCount,
|
||||
UsernamePatternType usernamePattern,
|
||||
PasswordStrength passwordStrength)
|
||||
{
|
||||
var companies = Companies.All;
|
||||
var usernameGenerator = new UsernameGenerator(organizationId.GetHashCode(), usernamePattern);
|
||||
|
||||
var cipherList = Enumerable.Range(0, cipherCount)
|
||||
.Select(i =>
|
||||
{
|
||||
var company = companies[i % companies.Length];
|
||||
return _cipherSeeder.CreateOrganizationLoginCipher(
|
||||
organizationId,
|
||||
orgKeyBase64,
|
||||
name: $"{company.Name} ({company.Category})",
|
||||
username: usernameGenerator.GenerateVaried(company, i),
|
||||
password: Passwords.GetPassword(passwordStrength, i),
|
||||
uri: $"https://{company.Domain}");
|
||||
})
|
||||
.ToList();
|
||||
|
||||
db.BulkCopy(cipherList);
|
||||
|
||||
// Create cipher-collection relationships
|
||||
if (cipherList.Count > 0 && collectionIds.Count > 0)
|
||||
{
|
||||
var collectionCiphers = cipherList.SelectMany((cipher, i) =>
|
||||
{
|
||||
var primary = new CollectionCipher
|
||||
{
|
||||
CipherId = cipher.Id,
|
||||
CollectionId = collectionIds[i % collectionIds.Count]
|
||||
};
|
||||
|
||||
// Every 3rd cipher gets assigned to an additional collection
|
||||
if (i % 3 == 0 && collectionIds.Count > 1)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
primary,
|
||||
new CollectionCipher
|
||||
{
|
||||
CipherId = cipher.Id,
|
||||
CollectionId = collectionIds[(i + 1) % collectionIds.Count]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new[] { primary };
|
||||
}).ToList();
|
||||
|
||||
db.BulkCopy(collectionCiphers);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a realistic user status based on index position.
|
||||
/// Distribution: 85% Confirmed, 5% Invited, 5% Accepted, 5% Revoked.
|
||||
/// </summary>
|
||||
private static OrganizationUserStatusType GetRealisticStatus(int index, int totalUsers)
|
||||
{
|
||||
// Calculate bucket boundaries
|
||||
var confirmedCount = (int)(totalUsers * 0.85);
|
||||
var invitedCount = (int)(totalUsers * 0.05);
|
||||
var acceptedCount = (int)(totalUsers * 0.05);
|
||||
// Revoked gets the remainder
|
||||
|
||||
if (index < confirmedCount)
|
||||
{
|
||||
return OrganizationUserStatusType.Confirmed;
|
||||
}
|
||||
|
||||
if (index < confirmedCount + invitedCount)
|
||||
{
|
||||
return OrganizationUserStatusType.Invited;
|
||||
}
|
||||
|
||||
if (index < confirmedCount + invitedCount + acceptedCount)
|
||||
{
|
||||
return OrganizationUserStatusType.Accepted;
|
||||
}
|
||||
|
||||
return OrganizationUserStatusType.Revoked;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user