1
0
mirror of https://github.com/bitwarden/server synced 2026-02-25 00:52:57 +00:00

Seeder/resolve owner roster quirk (#7059)

This commit is contained in:
Mick Letofsky
2026-02-24 07:47:29 +01:00
committed by GitHub
parent 5c77ae9810
commit 60bbf00160
5 changed files with 72 additions and 14 deletions

View File

@@ -34,7 +34,7 @@ internal static class PresetLoader
/// Builds a recipe from preset configuration, resolving fixtures and generation counts.
/// </summary>
/// <remarks>
/// Resolution order: Org → Owner → Generator → Roster → Users → Groups → Collections → Folders → Ciphers → PersonalCiphers
/// Resolution order: Org → Roster → Owner (if no roster owner) → Generator → Users → Groups → Collections → Folders → Ciphers → PersonalCiphers
/// </remarks>
private static void BuildRecipe(string presetName, SeedPreset preset, ISeedReader reader, IServiceCollection services)
{
@@ -62,7 +62,15 @@ internal static class PresetLoader
domain = org.Domain;
}
builder.AddOwner();
if (preset.Roster?.Fixture is not null)
{
builder.UseRoster(preset.Roster.Fixture, reader);
}
if (!builder.HasRosterOwner)
{
builder.AddOwner();
}
// Generator requires a domain and is needed for generated ciphers, personal ciphers, or folders
if (domain is not null && (preset.Ciphers?.Count > 0 || preset.PersonalCiphers?.CountPerUser > 0 || preset.Folders == true))
@@ -70,11 +78,6 @@ internal static class PresetLoader
builder.WithGenerator(domain);
}
if (preset.Roster?.Fixture is not null)
{
builder.UseRoster(preset.Roster.Fixture);
}
if (preset.Users is not null)
{
builder.AddUsers(preset.Users.Count, preset.Users.RealisticStatusMix);

View File

@@ -9,7 +9,7 @@ namespace Bit.Seeder.Pipeline;
/// <remarks>
/// Wraps <see cref="IServiceCollection"/> and a recipe name, tracking step count for
/// deterministic ordering and validation flags for dependency rules.
/// <strong>Phase Order:</strong> Org → Owner → Generator → Roster → Users → Groups → Collections → Folders → Ciphers → PersonalCiphers
/// <strong>Phase Order:</strong> Org → Roster → Owner (if no roster owner) → Generator → Users → Groups → Collections → Folders → Ciphers → PersonalCiphers
/// </remarks>
public class RecipeBuilder
{
@@ -43,6 +43,8 @@ public class RecipeBuilder
internal bool HasCipherFolderAssignment { get; set; }
internal bool HasRosterOwner { get; set; }
internal bool HasPersonalCiphers { get; set; }
/// <summary>

View File

@@ -2,6 +2,8 @@
using Bit.Core.Vault.Enums;
using Bit.Seeder.Data.Distributions;
using Bit.Seeder.Data.Enums;
using Bit.Seeder.Models;
using Bit.Seeder.Services;
using Bit.Seeder.Steps;
namespace Bit.Seeder.Pipeline;
@@ -75,8 +77,9 @@ public static class RecipeBuilderExtensions
/// <param name="builder">The recipe builder</param>
/// <param name="fixture">Roster fixture name without extension</param>
/// <returns>The builder for fluent chaining</returns>
/// <param name="reader">Seed reader for peeking the roster fixture to detect owner declarations</param>
/// <exception cref="InvalidOperationException">Thrown when AddUsers() was already called</exception>
public static RecipeBuilder UseRoster(this RecipeBuilder builder, string fixture)
public static RecipeBuilder UseRoster(this RecipeBuilder builder, string fixture, ISeedReader reader)
{
if (builder.HasGeneratedUsers)
{
@@ -85,6 +88,13 @@ public static class RecipeBuilderExtensions
}
builder.HasRosterUsers = true;
var roster = reader.Read<SeedRoster>($"rosters.{fixture}");
if (roster.Users.Any(u => string.Equals(u.Role, "owner", StringComparison.OrdinalIgnoreCase)))
{
builder.HasRosterOwner = true;
}
builder.AddStep(_ => new CreateRosterStep(fixture));
return builder;
}
@@ -274,10 +284,10 @@ public static class RecipeBuilderExtensions
"Organization is required. Call UseOrganization() or CreateOrganization().");
}
if (!builder.HasOwner)
if (!builder.HasOwner && !builder.HasRosterOwner)
{
throw new InvalidOperationException(
"Owner is required. Call AddOwner().");
"Owner is required. Call AddOwner() or declare a user with role 'owner' in the roster.");
}
if (builder.HasGeneratedCiphers && !builder.HasGenerator)

View File

@@ -45,6 +45,13 @@ internal sealed class CreateRosterStep(string fixtureName) : IStep
var orgUser = org.CreateOrganizationUserWithKey(
user, orgUserType, OrganizationUserStatusType.Confirmed, userOrgKey);
// Promote the first owner-role user to pipeline owner
if (orgUserType == OrganizationUserType.Owner && context.Owner is null)
{
context.Owner = user;
context.OwnerOrgUser = orgUser;
}
userLookup[emailPrefix] = orgUser.Id;
context.Users.Add(user);