diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs index 969b453ef7..d72182bd67 100644 --- a/util/DbSeederUtility/Program.cs +++ b/util/DbSeederUtility/Program.cs @@ -113,7 +113,7 @@ public class Program var stopwatch = Stopwatch.StartNew(); Console.WriteLine($"Seeding organization from preset '{args.Preset}'..."); - var result = recipe.Seed(args.Preset!); + var result = recipe.Seed(args.Preset!, args.Password); stopwatch.Stop(); PrintSeedResult(result, stopwatch.Elapsed); diff --git a/util/DbSeederUtility/README.md b/util/DbSeederUtility/README.md index e2f2b8f6b1..4ceb322921 100644 --- a/util/DbSeederUtility/README.md +++ b/util/DbSeederUtility/README.md @@ -11,7 +11,7 @@ dotnet build dotnet run -- [options] ``` -**Login Credentials:** All seeded users use password `asdfasdfasdf`. The owner email is `owner@`. +**Login Credentials:** All seeded users use password `asdfasdfasdf` by default (override with `--password`). The owner email is `owner@`. ## Commands @@ -29,6 +29,8 @@ dotnet run -- seed --preset dunder-mifflin-full --mangle # Large enterprise preset for performance testing dotnet run -- seed --preset large-enterprise + +dotnet run -- seed --preset dunder-mifflin-full --password "MyTestPassword1" --mangle ``` ### `organization` - Users Only (No Vault Data) @@ -65,4 +67,7 @@ dotnet run -- vault-organization -n ApacOrg -d apac.test -u 17 -c 600 -g 12 --re # With ID mangling for test isolation (prevents collisions with existing data) dotnet run -- vault-organization -n IsolatedOrg -d isolated.test -u 5 -c 25 -g 4 -o Spotify --mangle + +# With custom password for all accounts +dotnet run -- vault-organization -n CustomPwOrg -d custom-password-02.test -u 10 -c 100 -g 3 --password "MyTestPassword1" ``` diff --git a/util/DbSeederUtility/SeedArgs.cs b/util/DbSeederUtility/SeedArgs.cs index 699a065de4..183e9be4dc 100644 --- a/util/DbSeederUtility/SeedArgs.cs +++ b/util/DbSeederUtility/SeedArgs.cs @@ -8,7 +8,7 @@ namespace Bit.DbSeederUtility; /// public class SeedArgs : IArgumentModel { - [Option('p', "preset", Description = "Name of embedded preset to load (e.g., 'dunder-mifflin-full')")] + [Option("preset", Description = "Name of embedded preset to load")] public string? Preset { get; set; } [Option('l', "list", Description = "List all available presets and fixtures")] @@ -17,6 +17,9 @@ public class SeedArgs : IArgumentModel [Option("mangle", Description = "Enable mangling for test isolation")] public bool Mangle { get; set; } + [Option("password", Description = "Password for all seeded accounts (default: asdfasdfasdf)")] + public string? Password { get; set; } + public void Validate() { // List mode is standalone diff --git a/util/DbSeederUtility/VaultOrganizationArgs.cs b/util/DbSeederUtility/VaultOrganizationArgs.cs index 8aad49a03d..ae7d77bf2a 100644 --- a/util/DbSeederUtility/VaultOrganizationArgs.cs +++ b/util/DbSeederUtility/VaultOrganizationArgs.cs @@ -37,6 +37,9 @@ public class VaultOrganizationArgs : IArgumentModel [Option("mangle", Description = "Enable mangling for test isolation")] public bool Mangle { get; set; } = false; + [Option("password", Description = "Password for all seeded accounts (default: asdfasdfasdf)")] + public string? Password { get; set; } + public void Validate() { if (Users < 1) @@ -74,7 +77,8 @@ public class VaultOrganizationArgs : IArgumentModel Groups = Groups, RealisticStatusMix = MixStatuses, StructureModel = ParseOrgStructure(Structure), - Region = ParseGeographicRegion(Region) + Region = ParseGeographicRegion(Region), + Password = Password }; private static OrgStructureModel? ParseOrgStructure(string? structure) diff --git a/util/Seeder/CLAUDE.md b/util/Seeder/CLAUDE.md index dbdc13dc1f..d235313489 100644 --- a/util/Seeder/CLAUDE.md +++ b/util/Seeder/CLAUDE.md @@ -98,6 +98,6 @@ _seed = options.Seed ?? StableHash.ToInt32(options.Domain); ## Security Reminders -- Test password: `asdfasdfasdf` +- Default test password: `asdfasdfasdf` (overridable via `--password` CLI flag or `SeederSettings`) - Never commit database dumps with seeded data - Seeded keys are for testing only diff --git a/util/Seeder/Options/OrganizationVaultOptions.cs b/util/Seeder/Options/OrganizationVaultOptions.cs index 6afd13c1d9..9e46f2a7b2 100644 --- a/util/Seeder/Options/OrganizationVaultOptions.cs +++ b/util/Seeder/Options/OrganizationVaultOptions.cs @@ -83,4 +83,9 @@ public class OrganizationVaultOptions /// Seed for deterministic data generation. When null, derived from Domain hash. /// public int? Seed { get; init; } + + /// + /// Password for all seeded accounts. Defaults to "asdfasdfasdf" if not specified. + /// + public string? Password { get; init; } } diff --git a/util/Seeder/Pipeline/PresetExecutor.cs b/util/Seeder/Pipeline/PresetExecutor.cs index 83ee8fea1c..e4ee538957 100644 --- a/util/Seeder/Pipeline/PresetExecutor.cs +++ b/util/Seeder/Pipeline/PresetExecutor.cs @@ -18,11 +18,13 @@ internal sealed class PresetExecutor(DatabaseContext db, IMapper mapper) /// Name of the embedded preset (e.g., "dunder-mifflin-full") /// Password hasher for user creation /// Mangler service for test isolation + /// Optional password for all seeded accounts /// Execution result with organization ID and entity counts internal ExecutionResult Execute( string presetName, IPasswordHasher passwordHasher, - IManglerService manglerService) + IManglerService manglerService, + string? password = null) { var reader = new SeedReader(); @@ -30,6 +32,7 @@ internal sealed class PresetExecutor(DatabaseContext db, IMapper mapper) services.AddSingleton(passwordHasher); services.AddSingleton(manglerService); services.AddSingleton(reader); + services.AddSingleton(new SeederSettings(password)); services.AddSingleton(db); PresetLoader.RegisterRecipe(presetName, reader, services); diff --git a/util/Seeder/Pipeline/SeederContextExtensions.cs b/util/Seeder/Pipeline/SeederContextExtensions.cs index 8281dafb01..61ad50cecd 100644 --- a/util/Seeder/Pipeline/SeederContextExtensions.cs +++ b/util/Seeder/Pipeline/SeederContextExtensions.cs @@ -19,4 +19,15 @@ internal static class SeederContextExtensions internal static ISeedReader GetSeedReader(this SeederContext context) => context.Services.GetRequiredService(); + + internal static SeederSettings GetSettings(this SeederContext context) => + context.Services.GetRequiredService(); + + internal static string GetPassword(this SeederContext context) => + context.GetSettings().Password ?? Factories.UserSeeder.DefaultPassword; } + +/// +/// Runtime settings for a seeding operation, registered in DI. +/// +internal sealed record SeederSettings(string? Password = null); diff --git a/util/Seeder/Recipes/OrganizationFromPresetRecipe.cs b/util/Seeder/Recipes/OrganizationFromPresetRecipe.cs index 1a00c7307c..1670318fc6 100644 --- a/util/Seeder/Recipes/OrganizationFromPresetRecipe.cs +++ b/util/Seeder/Recipes/OrganizationFromPresetRecipe.cs @@ -27,10 +27,11 @@ public class OrganizationFromPresetRecipe( /// Seeds an organization from an embedded preset. /// /// Name of the embedded preset (e.g., "dunder-mifflin-full") + /// Optional password for all seeded accounts /// The organization ID and summary statistics. - public SeedResult Seed(string presetName) + public SeedResult Seed(string presetName, string? password = null) { - var result = _executor.Execute(presetName, passwordHasher, manglerService); + var result = _executor.Execute(presetName, passwordHasher, manglerService, password); return new SeedResult( result.OrganizationId, diff --git a/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs b/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs index 1003c5b0c2..d083a2864d 100644 --- a/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs +++ b/util/Seeder/Recipes/OrganizationWithVaultRecipe.cs @@ -54,6 +54,7 @@ public class OrganizationWithVaultRecipe( public Guid Seed(OrganizationVaultOptions options) { _ctx = GeneratorContext.FromOptions(options); + var password = options.Password ?? UserSeeder.DefaultPassword; var seats = Math.Max(options.Users + 1, _minimumOrgSeats); var orgKeys = RustSdkService.GenerateOrganizationKeys(); @@ -63,7 +64,11 @@ public class OrganizationWithVaultRecipe( options.Name, options.Domain, seats, orgKeys.PublicKey, orgKeys.PrivateKey); // Create owner user via factory - var ownerUser = UserSeeder.Create($"owner@{options.Domain}", passwordHasher, manglerService); + var ownerEmail = $"owner@{options.Domain}"; + var mangledOwnerEmail = manglerService.Mangle(ownerEmail); + var ownerKeys = RustSdkService.GenerateUserKeys(mangledOwnerEmail, password); + var ownerUser = UserSeeder.Create(mangledOwnerEmail, passwordHasher, manglerService, keys: ownerKeys, password: password); + var ownerOrgKey = RustSdkService.GenerateUserOrganizationKey(ownerUser.PublicKey!, orgKeys.Key); var ownerOrgUser = organization.CreateOrganizationUserWithKey( ownerUser, OrganizationUserType.Owner, OrganizationUserStatusType.Confirmed, ownerOrgKey); @@ -77,8 +82,8 @@ public class OrganizationWithVaultRecipe( { var email = $"user{i}@{options.Domain}"; var mangledEmail = manglerService.Mangle(email); - var userKeys = RustSdkService.GenerateUserKeys(mangledEmail, UserSeeder.DefaultPassword); - var memberUser = UserSeeder.Create(mangledEmail, passwordHasher, manglerService, keys: userKeys); + var userKeys = RustSdkService.GenerateUserKeys(mangledEmail, password); + var memberUser = UserSeeder.Create(mangledEmail, passwordHasher, manglerService, keys: userKeys, password: password); memberUsersWithKeys.Add(new UserWithKey(memberUser, userKeys.Key)); var status = useRealisticMix diff --git a/util/Seeder/Seeds/README.md b/util/Seeder/Seeds/README.md index 23a2c7a8f0..21f09a8586 100644 --- a/util/Seeder/Seeds/README.md +++ b/util/Seeder/Seeds/README.md @@ -35,73 +35,40 @@ Vault items - logins, cards, identities, secure notes. | `card` | `card` | Payment card details | | `identity` | `identity` | Personal identity info | | `secureNote` | — | Uses `notes` field only | +| `sshKey` | `sshKey` | SSH key credentials | -**Example** (`fixtures/ciphers/banking-logins.json`): - -```json -{ - "$schema": "../../schemas/cipher.schema.json", - "items": [ - { - "type": "login", - "name": "Chase Bank", - "login": { - "username": "myuser", - "password": "MyP@ssw0rd", - "uris": [{ "uri": "https://chase.com", "match": "domain" }] - } - } - ] -} -``` +**Schema**: `schemas/cipher.schema.json` ### Organizations -Organization definitions with name, domain, and seat count. +Organization identity definitions with name and domain. Plan type and seats are defined in presets, not org fixtures. -```json -{ - "$schema": "../../schemas/organization.schema.json", - "name": "Acme Corp", - "domain": "acme.com", - "seats": 100 -} -``` +**Required fields**: `name`, `domain` + +**Schema**: `schemas/organization.schema.json` ### Rosters Complete user/group/collection structures with permissions. User emails auto-generated as `firstName.lastName@domain`. **User roles**: `owner`, `admin`, `user`, `custom` - **Collection permissions**: `readOnly`, `hidePasswords`, `manage` - -See `rosters/dunder-mifflin.json` for a complete 58-user example. +**Schema**: `schemas/roster.schema.json` +**Example**: See `fixtures/rosters/dunder-mifflin.json` for a complete 58-user example ### Presets -Combine organization, roster, and ciphers into complete scenarios. +Combine organization, roster, and ciphers into complete scenarios. Presets can reference fixtures, generate data programmatically, or mix both approaches. -**From fixtures**: +**Key features**: -```json -{ - "$schema": "../../schemas/preset.schema.json", - "organization": { "fixture": "acme-corp" }, - "roster": { "fixture": "acme-roster" }, - "ciphers": { "fixture": "banking-logins" } -} -``` +- Reference existing fixtures by name +- Generate users, groups, collections, and ciphers with count parameters +- Add personal ciphers (user-owned, encrypted with user key, not in collections) +- Mix fixture references and generated data -**Mixed approach**: - -```json -{ - "organization": { "fixture": "acme-corp" }, - "users": { "count": 50 }, - "ciphers": { "count": 500 } -} -``` +**Schema**: `schemas/preset.schema.json` +**Examples**: See `fixtures/presets/` for complete examples including fixture-based, generated, and hybrid approaches ## Validation @@ -113,19 +80,6 @@ Build errors catch schema violations: dotnet build util/Seeder/Seeder.csproj ``` -## Testing - -Add integration test in `test/SeederApi.IntegrationTest/SeedReaderTests.cs`: - -```csharp -[Fact] -public void Read_YourFixture_Success() -{ - var result = _reader.Read("ciphers.your-fixture"); - Assert.NotEmpty(result.Items); -} -``` - ## Naming Conventions | Element | Pattern | Example | @@ -137,13 +91,7 @@ public void Read_YourFixture_Success() ## Security -- Test password: `asdfasdfasdf` +- Test password: See `UserSeeder.DefaultPassword` constant - Use fictional names/addresses - Never commit real passwords or PII - Never seed production databases - -## Examples - -- **Small org**: `presets/dunder-mifflin-full.json` (58 users, realistic structure) -- **Browser testing**: `ciphers/autofill-testing.json` (18 specialized items) -- **Real websites**: `ciphers/public-site-logins.json` (90+ website examples) diff --git a/util/Seeder/Steps/CreateOwnerStep.cs b/util/Seeder/Steps/CreateOwnerStep.cs index 329f4ffc65..856fc627f0 100644 --- a/util/Seeder/Steps/CreateOwnerStep.cs +++ b/util/Seeder/Steps/CreateOwnerStep.cs @@ -13,7 +13,11 @@ internal sealed class CreateOwnerStep : IStep public void Execute(SeederContext context) { var org = context.RequireOrganization(); - var owner = UserSeeder.Create($"owner@{context.RequireDomain()}", context.GetPasswordHasher(), context.GetMangler()); + var password = context.GetPassword(); + var ownerEmail = context.GetMangler().Mangle($"owner@{context.RequireDomain()}"); + var userKeys = RustSdkService.GenerateUserKeys(ownerEmail, password); + var owner = UserSeeder.Create(ownerEmail, context.GetPasswordHasher(), context.GetMangler(), keys: userKeys, password: password); + var ownerOrgKey = RustSdkService.GenerateUserOrganizationKey(owner.PublicKey!, context.RequireOrgKey()); var ownerOrgUser = org.CreateOrganizationUserWithKey( @@ -25,5 +29,6 @@ internal sealed class CreateOwnerStep : IStep context.Users.Add(owner); context.OrganizationUsers.Add(ownerOrgUser); context.Registry.HardenedOrgUserIds.Add(ownerOrgUser.Id); + context.Registry.UserDigests.Add(new EntityRegistry.UserDigest(owner.Id, ownerOrgUser.Id, userKeys.Key)); } } diff --git a/util/Seeder/Steps/CreateRosterStep.cs b/util/Seeder/Steps/CreateRosterStep.cs index a231769593..43f266a459 100644 --- a/util/Seeder/Steps/CreateRosterStep.cs +++ b/util/Seeder/Steps/CreateRosterStep.cs @@ -34,10 +34,12 @@ internal sealed class CreateRosterStep(string fixtureName) : IStep "Each user must have a unique FirstName.LastName combination."); } + var email = $"{emailPrefix}@{domain}"; var mangledEmail = context.GetMangler().Mangle(email); - var userKeys = RustSdkService.GenerateUserKeys(mangledEmail, UserSeeder.DefaultPassword); - var user = UserSeeder.Create(mangledEmail, context.GetPasswordHasher(), context.GetMangler(), keys: userKeys); + var password = context.GetPassword(); + var userKeys = RustSdkService.GenerateUserKeys(mangledEmail, password); + var user = UserSeeder.Create(mangledEmail, context.GetPasswordHasher(), context.GetMangler(), keys: userKeys, password: password); var userOrgKey = RustSdkService.GenerateUserOrganizationKey(user.PublicKey!, orgKey); var orgUserType = ParseRole(rosterUser.Role); var orgUser = org.CreateOrganizationUserWithKey( diff --git a/util/Seeder/Steps/CreateUsersStep.cs b/util/Seeder/Steps/CreateUsersStep.cs index 194840fbb3..4d20e044a1 100644 --- a/util/Seeder/Steps/CreateUsersStep.cs +++ b/util/Seeder/Steps/CreateUsersStep.cs @@ -28,13 +28,14 @@ internal sealed class CreateUsersStep(int count, bool realisticStatusMix = false var organizationUsers = new List(count); var hardenedOrgUserIds = new List(); var userDigests = new List(); + var password = context.GetPassword(); for (var i = 0; i < count; i++) { var email = $"user{i}@{domain}"; var mangledEmail = context.GetMangler().Mangle(email); - var userKeys = RustSdkService.GenerateUserKeys(mangledEmail, UserSeeder.DefaultPassword); - var user = UserSeeder.Create(mangledEmail, context.GetPasswordHasher(), context.GetMangler(), keys: userKeys); + var userKeys = RustSdkService.GenerateUserKeys(mangledEmail, password); + var user = UserSeeder.Create(mangledEmail, context.GetPasswordHasher(), context.GetMangler(), keys: userKeys, password: password); var status = statusDistribution.Select(i, count);