Refactor to correctly implement statics and remove hardcoded organization keys (#6924)

This commit is contained in:
Mick Letofsky
2026-01-30 16:03:56 +01:00
committed by GitHub
parent bfc645e1c1
commit 5941e830d2
18 changed files with 169 additions and 147 deletions

View File

@@ -22,8 +22,6 @@ namespace Bit.Seeder.Factories;
/// </remarks>
public class CipherSeeder
{
private readonly RustSdkService _sdkService;
private static readonly JsonSerializerOptions SdkJsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -36,12 +34,7 @@ public class CipherSeeder
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public CipherSeeder(RustSdkService sdkService)
{
_sdkService = sdkService;
}
public Cipher CreateOrganizationLoginCipher(
public static Cipher CreateOrganizationLoginCipher(
Guid organizationId,
string orgKeyBase64,
string name,
@@ -67,7 +60,7 @@ public class CipherSeeder
return EncryptAndTransform(cipherView, orgKeyBase64, organizationId);
}
public Cipher CreateOrganizationLoginCipherWithFields(
public static Cipher CreateOrganizationLoginCipherWithFields(
Guid organizationId,
string orgKeyBase64,
string name,
@@ -98,10 +91,10 @@ public class CipherSeeder
return EncryptAndTransform(cipherView, orgKeyBase64, organizationId);
}
private Cipher EncryptAndTransform(CipherViewDto cipherView, string keyBase64, Guid organizationId)
private static Cipher EncryptAndTransform(CipherViewDto cipherView, string keyBase64, Guid organizationId)
{
var viewJson = JsonSerializer.Serialize(cipherView, SdkJsonOptions);
var encryptedJson = _sdkService.EncryptCipher(viewJson, keyBase64);
var encryptedJson = RustSdkService.EncryptCipher(viewJson, keyBase64);
var encryptedDto = JsonSerializer.Deserialize<EncryptedCipherDto>(encryptedJson, SdkJsonOptions)
?? throw new InvalidOperationException("Failed to parse encrypted cipher");

View File

@@ -3,15 +3,15 @@ using Bit.RustSDK;
namespace Bit.Seeder.Factories;
public class CollectionSeeder(RustSdkService sdkService)
public class CollectionSeeder
{
public Collection CreateCollection(Guid organizationId, string orgKey, string name)
public static Collection CreateCollection(Guid organizationId, string orgKey, string name)
{
return new Collection
{
Id = Guid.NewGuid(),
OrganizationId = organizationId,
Name = sdkService.EncryptString(name, orgKey),
Name = RustSdkService.EncryptString(name, orgKey),
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow
};

View File

@@ -8,7 +8,7 @@ namespace Bit.Seeder.Factories;
/// Factory for creating Folder entities with encrypted names.
/// Folders are per-user constructs encrypted with the user's symmetric key.
/// </summary>
internal sealed class FolderSeeder(RustSdkService sdkService)
internal sealed class FolderSeeder
{
/// <summary>
/// Creates a folder with an encrypted name.
@@ -16,13 +16,13 @@ internal sealed class FolderSeeder(RustSdkService sdkService)
/// <param name="userId">The user who owns this folder.</param>
/// <param name="userKeyBase64">The user's symmetric key (not org key).</param>
/// <param name="name">The plaintext folder name to encrypt.</param>
public Folder CreateFolder(Guid userId, string userKeyBase64, string name)
public static Folder CreateFolder(Guid userId, string userKeyBase64, string name)
{
return new Folder
{
Id = CoreHelpers.GenerateComb(),
UserId = userId,
Name = sdkService.EncryptString(name, userKeyBase64)
Name = RustSdkService.EncryptString(name, userKeyBase64)
};
}
}

View File

@@ -7,9 +7,6 @@ namespace Bit.Seeder.Factories;
public class OrganizationSeeder
{
private static readonly string _defaultPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmIJbGMk6eZqVE7UxhZ46Weu2jKciqOiOkSVYtGvs61rfe9AXxtLaaZEKN4d4DmkZcF6dna2eXNxZmb7U4pwlttye8ksqISe6IUAZQox7auBpjopdCEPhKRg3BD/u8ks9UxSxgWe+fpebjt6gd5hsl1/5HOObn7SeU6EEU04cp3/eH7a4OTdXxB8oN62HGV9kM/ubM1goILgjoSJDbihMK0eb7b8hPHwcA/YOgKKiu/N3FighccdSMD5Pk+HfjacsFNZQa2EsqW09IvvSZ+iL6HQeZ1vwc/6TO1J7EOfJZFQcjoEL9LVI693efYoMZSmrPEWziZ4PvwpOOGo6OObyMQIDAQAB";
private static readonly string _defaultPrivateKey = "2.6FggyKVyaKQsfohi5yqgbg==|UU2JeafOB41L5UscGmf4kq15JGDf3Bkf67KECiehTODzbWctVLTgyDk0Qco8/6CMN6nZGXjxR2A4r5ExhmwRNsNxd77G+MprkmiJz+7w33ROZ1ouQO5XjD3wbQ3ssqNiTKId6yAUPBvuAZRixVApauTuADc8QWGixqCQcqZzmU7YSBBIPf652/AEYr4Tk64YihoE39pHiK8MRbTLdRt3EF4LSMugPAPM24vCgUv3w1TD3Fj6sDg/6oi3flOV9SJZX4vCiUXbDNEuD/p2aQrEXVbaxweFOHjTe7F4iawjXw3nG3SO8rUBHcxbhDDVx5rjYactbW5QvHWiyla6uLb6o8WHBneg2EjTEwAHOZE/rBjcqmAJb2sVp1E0Kwq8ycGmL69vmqJPC1GqVTohAQvmEkaxIPpfq24Yb9ZPrADA7iEXBKuAQ1FphFUVgJBJGJbd60sOV1Rz1T+gUwS4wCNQ4l3LG1S22+wzUVlEku5DXFnT932tatqTyWEthqPqLCt6dL1+qa94XLpeHagXAx2VGe8n8IlcADtxqS+l8xQ4heT12WO9kC316vqvg1mnsI56faup9hb3eT9ZpKyxSBGYOphlTWfV1Y/v64f5PYvTo4aL0IYHyLY/9Qi72vFmOpPeHBYgD5t3j+H2CsiU1PkYsBggOmD7xW8FDuT6HWVvwhEJqeibVPK0Lhyj6tgvlSIAvFUaSMFPlmwFNmwfj/AHUhr9KuTfsBFTZ10yy9TZVgf+EofwnrxHBaWUgdD40aHoY1VjfG33iEuajb6buxG3pYFyPNhJNzeLZisUKIDRMQpUHrsE22EyrFFran3tZGdtcyIEK4Q1F0ULYzJ6T9iY25/ZgPy3pEAAMZCtqo3s+GjX295fWIHfMcnjMgNUHPjExjWBHa+ggK9iQXkFpBVyYB1ga/+0eiIhiek3PlgtvpDrqF7TsLK+ROiBw2GJ7uaO3EEXOj2GpNBuEJ5CdodhZkwzhwMcSatgDHkUuNVu0iVbF6/MxVdOxWXKO+jCYM6PZk/vAhLYqpPzu2T2Uyz4nkDs2Tiq61ez6FoCrzdHIiyIxVTzUQH8G9FgSmtaZ7GCbqlhnurYgcMciwPzxg0hpAQT+NZw1tVEii9vFSpJJbGJqNhORKfKh/Mu1P/9LOQq7Y0P2FIR3x/eUVEQ7CGv2jVtO5ryGSmKeq/P9Fr54wTPaNiqN2K+leACUznCdUWw8kZo/AsBcrOe4OkRX6k8LC3oeJXy06DEToatxEvPYemUauhxiXRw8nfNMqc4LyJq2bbT0zCgJHoqpozPdNg6AYWcoIobgAGu7ZQGq+oE1MT3GZxotMPe/NUJiAc5YE9Thb5Yf3gyno71pyqPTVl/6IQuh4SUz7rkgwF/aVHEnr4aUYNoc0PEzd2Me0jElsA3GAneq1I/wngutOWgTViTK4Nptr5uIzMVQs9H1rOMJNorP8b02t1NDu010rSsib9GaaJJq4r4iy46laQOxWoU0ex26arYnk+jw4833WSCTVBIprTgizZ+fKjoY0xwXvI2oOvGNEUCtGFvKFORTaQrlaXZIg1toa2BBVNicyONbwnI3KIu3MgGJ2SlCVXJn8oHFppVHFCdwgN1uDzGiKAhjvr0sZTUtXin2f2CszPTbbo=|fUhbVKrr8CSKE7TZJneXpDGraj5YhRrq9ESo206S+BY=";
public static Organization CreateEnterprise(string name, string domain, int seats, string? publicKey = null, string? privateKey = null)
{
return new Organization
@@ -43,36 +40,14 @@ public class OrganizationSeeder
SyncSeats = true,
Status = OrganizationStatusType.Created,
MaxStorageGb = 10,
PublicKey = publicKey ?? _defaultPublicKey,
PrivateKey = privateKey ?? _defaultPrivateKey,
PublicKey = publicKey,
PrivateKey = privateKey
};
}
}
public static class OrganizationExtensions
{
/// <summary>
/// Creates an OrganizationUser with fields populated based on status.
/// For Invited status, only user.Email is used. For other statuses, user.Id is used.
/// </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().

View File

@@ -11,7 +11,7 @@ public struct UserData
public string Email;
}
public class UserSeeder(RustSdkService sdkService, IPasswordHasher<Bit.Core.Entities.User> passwordHasher, MangleId mangleId)
public class UserSeeder(IPasswordHasher<Bit.Core.Entities.User> passwordHasher, MangleId mangleId)
{
private string MangleEmail(string email)
{
@@ -21,7 +21,7 @@ public class UserSeeder(RustSdkService sdkService, IPasswordHasher<Bit.Core.Enti
public User CreateUser(string email, bool emailVerified = false, bool premium = false)
{
email = MangleEmail(email);
var keys = sdkService.GenerateUserKeys(email, DefaultPassword);
var keys = RustSdkService.GenerateUserKeys(email, DefaultPassword);
var user = new User
{
@@ -76,10 +76,9 @@ public class UserSeeder(RustSdkService sdkService, IPasswordHasher<Bit.Core.Enti
/// </summary>
public static User CreateUserWithSdkKeys(
string email,
RustSdkService sdkService,
IPasswordHasher<User> passwordHasher)
{
var keys = sdkService.GenerateUserKeys(email, DefaultPassword);
var keys = RustSdkService.GenerateUserKeys(email, DefaultPassword);
return CreateUserFromKeys(email, keys, passwordHasher);
}

View File

@@ -14,7 +14,7 @@ public class CollectionsRecipe(DatabaseContext db)
/// <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)
public List<Guid> Seed(Guid organizationId, int collections, List<Guid> organizationUserIds, int maxUsersWithRelationships = 1000)
{
var collectionList = CreateAndSaveCollections(organizationId, collections);

View File

@@ -13,7 +13,7 @@ public class GroupsRecipe(DatabaseContext db)
/// <param name="groups">The number of groups 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 groups, List<Guid> organizationUserIds, int maxUsersWithRelationships = 1000)
public List<Guid> Seed(Guid organizationId, int groups, List<Guid> organizationUserIds, int maxUsersWithRelationships = 1000)
{
var groupList = CreateAndSaveGroups(organizationId, groups);

View File

@@ -5,7 +5,7 @@ namespace Bit.Seeder.Recipes;
public class OrganizationDomainRecipe(DatabaseContext db)
{
public void AddVerifiedDomainToOrganization(Guid organizationId, string domainName)
public void Seed(Guid organizationId, string domainName)
{
var domain = new OrganizationDomain
{

View File

@@ -1,39 +1,66 @@
using Bit.Core.Entities;
using AutoMapper;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.RustSDK;
using Bit.Seeder.Factories;
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;
public class OrganizationWithUsersRecipe(DatabaseContext db)
public class OrganizationWithUsersRecipe(DatabaseContext db, IMapper mapper, IPasswordHasher<User> passwordHasher)
{
public Guid Seed(string name, string domain, int users, OrganizationUserStatusType usersStatus = OrganizationUserStatusType.Confirmed)
{
var seats = Math.Max(users + 1, 1000);
var organization = OrganizationSeeder.CreateEnterprise(name, domain, seats);
var ownerUser = UserSeeder.CreateUserNoMangle($"owner@{domain}");
var ownerOrgUser = organization.CreateOrganizationUser(ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed);
// Generate organization keys
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var organization = OrganizationSeeder.CreateEnterprise(
name, domain, seats, orgKeys.PublicKey, orgKeys.PrivateKey);
// Create owner with SDK-generated keys
var ownerUser = UserSeeder.CreateUserWithSdkKeys($"owner@{domain}", passwordHasher);
var ownerOrgKey = RustSdkService.GenerateUserOrganizationKey(ownerUser.PublicKey!, orgKeys.Key);
var ownerOrgUser = organization.CreateOrganizationUserWithKey(
ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed, ownerOrgKey);
var additionalUsers = new List<User>();
var additionalOrgUsers = new List<OrganizationUser>();
for (var i = 0; i < users; i++)
{
var additionalUser = UserSeeder.CreateUserNoMangle($"user{i}@{domain}");
var additionalUser = UserSeeder.CreateUserWithSdkKeys($"user{i}@{domain}", passwordHasher);
additionalUsers.Add(additionalUser);
additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser, OrganizationUserType.User, usersStatus));
// Generate org key for confirmed/revoked users
var shouldHaveKey = usersStatus == OrganizationUserStatusType.Confirmed
|| usersStatus == OrganizationUserStatusType.Revoked;
var userOrgKey = shouldHaveKey
? RustSdkService.GenerateUserOrganizationKey(additionalUser.PublicKey!, orgKeys.Key)
: null;
additionalOrgUsers.Add(organization.CreateOrganizationUserWithKey(
additionalUser, OrganizationUserType.User, usersStatus, userOrgKey));
}
db.Add(organization);
db.Add(ownerUser);
db.Add(ownerOrgUser);
// Map Core entities to EF entities before adding to DbContext
db.Add(mapper.Map<EfOrganization>(organization));
db.Add(mapper.Map<EfUser>(ownerUser));
db.Add(mapper.Map<EfOrganizationUser>(ownerOrgUser));
// Map and BulkCopy additional users
var efAdditionalUsers = additionalUsers.Select(u => mapper.Map<EfUser>(u)).ToList();
var efAdditionalOrgUsers = additionalOrgUsers.Select(ou => mapper.Map<EfOrganizationUser>(ou)).ToList();
db.BulkCopy(efAdditionalUsers);
db.BulkCopy(efAdditionalOrgUsers);
db.SaveChanges();
// Use LinqToDB's BulkCopy for significant better performance
db.BulkCopy(additionalUsers);
db.BulkCopy(additionalOrgUsers);
return organization.Id;
}
}

View File

@@ -27,12 +27,8 @@ namespace Bit.Seeder.Recipes;
public class OrganizationWithVaultRecipe(
DatabaseContext db,
IMapper mapper,
RustSdkService sdkService,
IPasswordHasher<User> passwordHasher)
{
private readonly CollectionSeeder _collectionSeeder = new(sdkService);
private readonly CipherSeeder _cipherSeeder = new(sdkService);
private readonly FolderSeeder _folderSeeder = new(sdkService);
/// <summary>
/// Tracks a user with their symmetric key for folder encryption.
@@ -47,15 +43,15 @@ public class OrganizationWithVaultRecipe(
public Guid Seed(OrganizationVaultOptions options)
{
var seats = Math.Max(options.Users + 1, 1000);
var orgKeys = sdkService.GenerateOrganizationKeys();
var orgKeys = RustSdkService.GenerateOrganizationKeys();
// Create organization via factory
var organization = OrganizationSeeder.CreateEnterprise(
options.Name, options.Domain, seats, orgKeys.PublicKey, 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 ownerUser = UserSeeder.CreateUserWithSdkKeys($"owner@{options.Domain}", passwordHasher);
var ownerOrgKey = RustSdkService.GenerateUserOrganizationKey(ownerUser.PublicKey!, orgKeys.Key);
var ownerOrgUser = organization.CreateOrganizationUserWithKey(
ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed, ownerOrgKey);
@@ -67,7 +63,7 @@ public class OrganizationWithVaultRecipe(
for (var i = 0; i < options.Users; i++)
{
var email = $"user{i}@{options.Domain}";
var userKeys = sdkService.GenerateUserKeys(email, UserSeeder.DefaultPassword);
var userKeys = RustSdkService.GenerateUserKeys(email, UserSeeder.DefaultPassword);
var memberUser = UserSeeder.CreateUserFromKeys(email, userKeys, passwordHasher);
memberUsersWithKeys.Add(new UserWithKey(memberUser, userKeys.Key));
@@ -77,7 +73,7 @@ public class OrganizationWithVaultRecipe(
var memberOrgKey = (status == OrganizationUserStatusType.Confirmed ||
status == OrganizationUserStatusType.Revoked)
? sdkService.GenerateUserOrganizationKey(memberUser.PublicKey!, orgKeys.Key)
? RustSdkService.GenerateUserOrganizationKey(memberUser.PublicKey!, orgKeys.Key)
: null;
memberOrgUsers.Add(organization.CreateOrganizationUserWithKey(
@@ -124,12 +120,12 @@ public class OrganizationWithVaultRecipe(
{
var structure = OrgStructures.GetStructure(structureModel.Value);
collections = structure.Units
.Select(unit => _collectionSeeder.CreateCollection(organizationId, orgKeyBase64, unit.Name))
.Select(unit => CollectionSeeder.CreateCollection(organizationId, orgKeyBase64, unit.Name))
.ToList();
}
else
{
collections = [_collectionSeeder.CreateCollection(organizationId, orgKeyBase64, "Default Collection")];
collections = [CollectionSeeder.CreateCollection(organizationId, orgKeyBase64, "Default Collection")];
}
db.BulkCopy(collections);
@@ -191,7 +187,7 @@ public class OrganizationWithVaultRecipe(
.Select(i =>
{
var company = companies[i % companies.Length];
return _cipherSeeder.CreateOrganizationLoginCipher(
return CipherSeeder.CreateOrganizationLoginCipher(
organizationId,
orgKeyBase64,
name: $"{company.Name} ({company.Category})",
@@ -285,7 +281,7 @@ public class OrganizationWithVaultRecipe(
{
var folderCount = GetFolderCountForUser(userIndex, usersWithKeys.Count, random);
return Enumerable.Range(0, folderCount)
.Select(folderIndex => _folderSeeder.CreateFolder(
.Select(folderIndex => FolderSeeder.CreateFolder(
uwk.User.Id,
uwk.SymmetricKey,
folderNameGenerator.GetFolderName(userIndex * 15 + folderIndex)));