Skip to content

Commit 9b3e01e

Browse files
authored
[PM-38224] Add claimed domain functionality to seeder (#7735)
1 parent c961545 commit 9b3e01e

11 files changed

Lines changed: 85 additions & 3 deletions

File tree

util/Seeder/Factories/OrganizationDomainSeeder.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using Bit.Core.Utilities;
2-
using Bit.Infrastructure.EntityFramework.Models;
1+
using Bit.Core.Entities;
2+
using Bit.Core.Utilities;
33

44
namespace Bit.Seeder.Factories;
55

util/Seeder/Models/SeedPreset.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal record SeedPresetOrganization
2525
public string? Fixture { get; init; }
2626
public string? Name { get; init; }
2727
public string? Domain { get; init; }
28+
public List<string>? ClaimedDomains { get; init; }
2829
public int? Seats { get; init; }
2930
public string? PlanType { get; init; }
3031
public bool? UseAutomaticUserConfirmation { get; init; }

util/Seeder/Options/OrganizationVaultOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public class OrganizationVaultOptions
2020
/// </summary>
2121
public required string Domain { get; init; }
2222

23+
/// <summary>
24+
/// Claimed (verified) domains to seed for the organization. Each becomes a verified <see cref="Bit.Core.Entities.OrganizationDomain"/> row.
25+
/// </summary>
26+
public IReadOnlyList<string> ClaimedDomains { get; init; } = [];
27+
2328
/// <summary>
2429
/// Number of member users to create.
2530
/// </summary>

util/Seeder/Pipeline/BulkCommitter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using EfGroupUser = Bit.Infrastructure.EntityFramework.Models.GroupUser;
1313
using EfOrganization = Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization;
1414
using EfOrganizationApiKey = Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey;
15+
using EfOrganizationDomain = Bit.Infrastructure.EntityFramework.Models.OrganizationDomain;
1516
using EfOrganizationUser = Bit.Infrastructure.EntityFramework.Models.OrganizationUser;
1617
using EfUser = Bit.Infrastructure.EntityFramework.Models.User;
1718

@@ -40,6 +41,8 @@ internal void Commit(SeederContext context)
4041
{
4142
MapCopyAndClear<Core.AdminConsole.Entities.Organization, EfOrganization>(context.Organizations);
4243

44+
MapCopyAndClear<Core.Entities.OrganizationDomain, EfOrganizationDomain>(context.OrganizationDomains);
45+
4346
MapAndCopy<Core.Entities.OrganizationApiKey, EfOrganizationApiKey>(context.OrganizationApiKey);
4447

4548
MapCopyAndClear<Core.Entities.User, EfUser>(context.Users);

util/Seeder/Pipeline/PresetLoader.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ private static void BuildIndividualRecipe(string presetName, SeedPreset preset,
8787
/// Builds a recipe from preset configuration, resolving fixtures and generation counts.
8888
/// </summary>
8989
/// <remarks>
90-
/// Resolution order: Org → OrgApiKey → Roster → Owner (if no roster owner) → Generator → Users → Groups → Collections → Folders → Ciphers → CipherCollections → CipherFolders → CipherFavorites → PersonalCiphers
90+
/// Resolution order: Org → OrgApiKey → ClaimedDomains → Roster → Owner (if no roster owner) → Generator → Users → Groups → Collections → Folders → Ciphers → CipherCollections → CipherFolders → CipherFavorites → PersonalCiphers
9191
/// </remarks>
9292
private static void BuildRecipe(string presetName, SeedPreset preset, ISeedReader reader, IServiceCollection services)
9393
{
@@ -117,6 +117,11 @@ private static void BuildRecipe(string presetName, SeedPreset preset, ISeedReade
117117

118118
builder.AddOrganizationApiKey();
119119

120+
if (org.ClaimedDomains is { Count: > 0 })
121+
{
122+
builder.WithOrganizationDomain(org.ClaimedDomains);
123+
}
124+
120125
if (preset.Roster?.Fixture is not null)
121126
{
122127
builder.UseRoster(preset.Roster.Fixture, reader);

util/Seeder/Pipeline/RecipeBuilderExtensions.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,39 @@ public static RecipeBuilder AddOrganizationApiKey(this RecipeBuilder builder)
7676
return builder;
7777
}
7878

79+
/// <summary>
80+
/// Seed one or more claimed (verified) organization domains.
81+
/// Domain names are trimmed; empty entries are dropped and duplicates removed case-insensitively.
82+
/// </summary>
83+
/// <param name="builder">The recipe builder</param>
84+
/// <param name="domainNames">Domain names to seed. Empty (or all-empty/whitespace) is a no-op.</param>
85+
/// <returns>The builder for fluent chaining</returns>
86+
/// <exception cref="InvalidOperationException">Thrown when no organization exists.</exception>
87+
public static RecipeBuilder WithOrganizationDomain(
88+
this RecipeBuilder builder,
89+
IReadOnlyList<string> domainNames)
90+
{
91+
if (!builder.HasOrg)
92+
{
93+
throw new InvalidOperationException(
94+
"Organization domains require an organization. Call UseOrganization() or CreateOrganization() first.");
95+
}
96+
97+
var unique = domainNames
98+
.Select(d => d.Trim())
99+
.Where(d => !string.IsNullOrWhiteSpace(d))
100+
.Distinct(StringComparer.OrdinalIgnoreCase)
101+
.ToList();
102+
103+
if (unique.Count == 0)
104+
{
105+
return builder;
106+
}
107+
108+
builder.AddStep(_ => new CreateOrganizationDomainsStep(unique));
109+
return builder;
110+
}
111+
79112
/// <summary>
80113
/// Add an organization owner user with admin privileges.
81114
/// </summary>

util/Seeder/Pipeline/RecipeOrchestrator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ internal PipelineExecutionResult Execute(OrganizationVaultOptions options)
6464

6565
builder.CreateOrganization(options.Name, options.Domain, options.Users + 1, options.PlanType, options.Overrides);
6666
builder.AddOrganizationApiKey();
67+
68+
if (options.ClaimedDomains.Count > 0)
69+
{
70+
builder.WithOrganizationDomain(options.ClaimedDomains);
71+
}
72+
6773
builder.AddOwner();
6874
builder.WithGenerator(options.Domain);
6975
builder.AddUsers(options.Users, options.RealisticStatusMix);

util/Seeder/Pipeline/SeederContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public sealed class SeederContext(IServiceProvider services)
5656

5757
internal List<Organization> Organizations { get; } = [];
5858

59+
internal List<OrganizationDomain> OrganizationDomains { get; } = [];
60+
5961
internal List<User> Users { get; } = [];
6062

6163
internal List<OrganizationUser> OrganizationUsers { get; } = [];

util/Seeder/Seeds/schemas/preset.schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
"type": "string",
3131
"description": "Organization domain (used with inline creation and generator seeding)."
3232
},
33+
"claimedDomains": {
34+
"type": "array",
35+
"items": { "type": "string", "minLength": 1 },
36+
"uniqueItems": true,
37+
"description": "Claimed (verified) organization domains to seed. Each becomes a verified OrganizationDomain row."
38+
},
3339
"seats": {
3440
"type": "integer",
3541
"minimum": 1,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Bit.Seeder.Factories;
2+
using Bit.Seeder.Pipeline;
3+
4+
namespace Bit.Seeder.Steps;
5+
6+
internal sealed class CreateOrganizationDomainsStep(IReadOnlyList<string> domainNames) : IStep
7+
{
8+
public void Execute(SeederContext context)
9+
{
10+
var orgId = context.RequireOrgId();
11+
12+
foreach (var name in domainNames)
13+
{
14+
context.OrganizationDomains.Add(OrganizationDomainSeeder.Create(orgId, name));
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)