mirror of
https://github.com/bitwarden/server
synced 2026-02-19 19:03:30 +00:00
Enhance seeder allowing for a user-defined password (#7021)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -11,7 +11,7 @@ dotnet build
|
||||
dotnet run -- <command> [options]
|
||||
```
|
||||
|
||||
**Login Credentials:** All seeded users use password `asdfasdfasdf`. The owner email is `owner@<domain>`.
|
||||
**Login Credentials:** All seeded users use password `asdfasdfasdf` by default (override with `--password`). The owner email is `owner@<domain>`.
|
||||
|
||||
## 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"
|
||||
```
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Bit.DbSeederUtility;
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -83,4 +83,9 @@ public class OrganizationVaultOptions
|
||||
/// Seed for deterministic data generation. When null, derived from Domain hash.
|
||||
/// </summary>
|
||||
public int? Seed { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Password for all seeded accounts. Defaults to "asdfasdfasdf" if not specified.
|
||||
/// </summary>
|
||||
public string? Password { get; init; }
|
||||
}
|
||||
|
||||
@@ -18,11 +18,13 @@ internal sealed class PresetExecutor(DatabaseContext db, IMapper mapper)
|
||||
/// <param name="presetName">Name of the embedded preset (e.g., "dunder-mifflin-full")</param>
|
||||
/// <param name="passwordHasher">Password hasher for user creation</param>
|
||||
/// <param name="manglerService">Mangler service for test isolation</param>
|
||||
/// <param name="password">Optional password for all seeded accounts</param>
|
||||
/// <returns>Execution result with organization ID and entity counts</returns>
|
||||
internal ExecutionResult Execute(
|
||||
string presetName,
|
||||
IPasswordHasher<User> 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<ISeedReader>(reader);
|
||||
services.AddSingleton(new SeederSettings(password));
|
||||
services.AddSingleton(db);
|
||||
|
||||
PresetLoader.RegisterRecipe(presetName, reader, services);
|
||||
|
||||
@@ -19,4 +19,15 @@ internal static class SeederContextExtensions
|
||||
|
||||
internal static ISeedReader GetSeedReader(this SeederContext context) =>
|
||||
context.Services.GetRequiredService<ISeedReader>();
|
||||
|
||||
internal static SeederSettings GetSettings(this SeederContext context) =>
|
||||
context.Services.GetRequiredService<SeederSettings>();
|
||||
|
||||
internal static string GetPassword(this SeederContext context) =>
|
||||
context.GetSettings().Password ?? Factories.UserSeeder.DefaultPassword;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime settings for a seeding operation, registered in DI.
|
||||
/// </summary>
|
||||
internal sealed record SeederSettings(string? Password = null);
|
||||
|
||||
@@ -27,10 +27,11 @@ public class OrganizationFromPresetRecipe(
|
||||
/// Seeds an organization from an embedded preset.
|
||||
/// </summary>
|
||||
/// <param name="presetName">Name of the embedded preset (e.g., "dunder-mifflin-full")</param>
|
||||
/// <param name="password">Optional password for all seeded accounts</param>
|
||||
/// <returns>The organization ID and summary statistics.</returns>
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SeedFile>("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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -28,13 +28,14 @@ internal sealed class CreateUsersStep(int count, bool realisticStatusMix = false
|
||||
var organizationUsers = new List<OrganizationUser>(count);
|
||||
var hardenedOrgUserIds = new List<Guid>();
|
||||
var userDigests = new List<EntityRegistry.UserDigest>();
|
||||
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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user