Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions util/Seeder/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@ Developer-facing documentation in `Seeds/docs/scenarios/`. Each file maps an eng
- `Seeds/docs/presets.md` β†’ what exists (the catalog) β†’ scenarios link back to it
- `Seeds/docs/scenarios/` β†’ why you'd use it (problem β†’ command)

## Collection Management Settings

**Collection management settings are not plan-gated.** `AllowAdminAccessToAllCollectionItems`, `LimitCollectionCreation`, `LimitCollectionDeletion`, and `LimitItemDeletion` apply identically across all plan types. They are org-level admin settings, not billing-plan features.

**These settings alter access control behavior.** When seeding scenarios that test member vs. admin permissions, collection creation/deletion policies, or item-level access, set them explicitly in the preset rather than relying on defaults.

**Configurable in presets and CLI.** Use the JSON preset `organization` block (e.g. `"limitCollectionCreation": true`) or the CLI flags: `--limit-collection-creation`, `--limit-collection-deletion`, `--limit-item-deletion`, `--allow-admin-collection-access`.

## Security Reminders

- Default test password: `asdfasdfasdf` (overridable via `--password` CLI flag or `SeederSettings`)
Expand Down
26 changes: 19 additions & 7 deletions util/Seeder/Factories/PlanFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Text;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Seeder.Options;

namespace Bit.Seeder.Factories;

Expand All @@ -13,13 +14,6 @@ public static class PlanFeatures
{
internal static void Apply(Organization org, PlanType planType)
{
// Org-level admin settings β€” not plan-gated, safe defaults for seeding
org.UseAutomaticUserConfirmation = true;
org.AllowAdminAccessToAllCollectionItems = true;
org.LimitCollectionCreation = true;
org.LimitCollectionDeletion = true;
org.LimitItemDeletion = true;
Comment thread
theMickster marked this conversation as resolved.

switch (planType)
{
case PlanType.Free:
Expand Down Expand Up @@ -78,6 +72,24 @@ internal static void Apply(Organization org, PlanType planType)
}
}

/// <summary>
/// Applies overrides on top of the organization's initial values.
/// Only non-null properties are applied; null means "leave the value unchanged from <see cref="OrganizationSeeder.Create"/>".
/// </summary>
internal static void ApplyOrganizationOverrides(Organization org, OrganizationOverrides? overrides)
{
if (overrides is null)
{
return;
}

org.UseAutomaticUserConfirmation = overrides.UseAutomaticUserConfirmation ?? org.UseAutomaticUserConfirmation;
org.AllowAdminAccessToAllCollectionItems = overrides.AllowAdminAccessToAllCollectionItems ?? org.AllowAdminAccessToAllCollectionItems;
org.LimitItemDeletion = overrides.LimitItemDeletion ?? org.LimitItemDeletion;
org.LimitCollectionCreation = overrides.LimitCollectionCreation ?? org.LimitCollectionCreation;
org.LimitCollectionDeletion = overrides.LimitCollectionDeletion ?? org.LimitCollectionDeletion;
}

public static PlanType Parse(string? planTypeString)
{
if (string.IsNullOrEmpty(planTypeString))
Expand Down
5 changes: 5 additions & 0 deletions util/Seeder/Models/SeedPreset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ internal record SeedPresetOrganization
public string? Domain { get; init; }
public int? Seats { get; init; }
public string? PlanType { get; init; }
public bool? UseAutomaticUserConfirmation { get; init; }
public bool? AllowAdminAccessToAllCollectionItems { get; init; }
public bool? LimitItemDeletion { get; init; }
public bool? LimitCollectionCreation { get; init; }
public bool? LimitCollectionDeletion { get; init; }
}

internal record SeedPresetRoster
Expand Down
14 changes: 14 additions & 0 deletions util/Seeder/Options/OrganizationOverrides.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ο»Ώnamespace Bit.Seeder.Options;

/// <summary>
/// Optional overrides applied on top of the organization's initial values.
/// Null properties mean "leave the value unchanged from <see cref="Bit.Seeder.Factories.OrganizationSeeder.Create"/>".
/// </summary>
public sealed record OrganizationOverrides
{
public bool? UseAutomaticUserConfirmation { get; init; }
public bool? AllowAdminAccessToAllCollectionItems { get; init; }
public bool? LimitItemDeletion { get; init; }
public bool? LimitCollectionCreation { get; init; }
public bool? LimitCollectionDeletion { get; init; }
}
6 changes: 6 additions & 0 deletions util/Seeder/Options/OrganizationVaultOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,10 @@ public class OrganizationVaultOptions
/// Use 600,000 for production-realistic e2e testing.
/// </summary>
public int KdfIterations { get; init; } = 5_000;

/// <summary>
/// Optional overrides for collection management settings applied on top of the organization's initial values.
/// Null means "leave all collection management settings unchanged from <see cref="Bit.Seeder.Factories.OrganizationSeeder.Create"/>".
/// </summary>
public OrganizationOverrides? Overrides { get; init; }
}
13 changes: 11 additions & 2 deletions util/Seeder/Pipeline/PresetLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private static void BuildRecipe(string presetName, SeedPreset preset, ISeedReade

if (org.Fixture is not null)
{
builder.UseOrganization(org.Fixture, org.PlanType, org.Seats);
builder.UseOrganization(org.Fixture, org.PlanType, org.Seats, ToOverrides(org));

// If using a fixture and domain not explicitly provided, read it from the fixture
if (domain is null)
Expand All @@ -111,7 +111,7 @@ private static void BuildRecipe(string presetName, SeedPreset preset, ISeedReade
else if (org.Name is not null && org.Domain is not null)
{
var planType = PlanFeatures.Parse(org.PlanType);
builder.CreateOrganization(org.Name, org.Domain, org.Seats, planType);
builder.CreateOrganization(org.Name, org.Domain, org.Seats, planType, ToOverrides(org));
domain = org.Domain;
}

Expand Down Expand Up @@ -198,6 +198,15 @@ private static void BuildRecipe(string presetName, SeedPreset preset, ISeedReade
builder.Validate();
}

private static OrganizationOverrides ToOverrides(SeedPresetOrganization org) => new()
{
UseAutomaticUserConfirmation = org.UseAutomaticUserConfirmation,
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems,
LimitItemDeletion = org.LimitItemDeletion,
LimitCollectionCreation = org.LimitCollectionCreation,
LimitCollectionDeletion = org.LimitCollectionDeletion,
};

private static DensityProfile? ParseDensity(SeedPresetDensity? preset)
{
if (preset is null)
Expand Down
23 changes: 18 additions & 5 deletions util/Seeder/Pipeline/RecipeBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,23 @@ namespace Bit.Seeder.Pipeline;
public static class RecipeBuilderExtensions
{
/// <summary>
/// Use an organization from embedded fixtures with optional plan/seats overrides from the preset.
/// Use an organization from embedded fixtures with optional plan/seats/overrides from the preset.
/// </summary>
/// <param name="builder">The recipe builder</param>
/// <param name="fixture">Organization fixture name without extension</param>
/// <param name="planType">Optional plan type override (from preset)</param>
/// <param name="seats">Optional seats override (from preset)</param>
/// <param name="overrides">Optional org-level overrides applied on top of plan defaults. Null keeps all plan defaults.</param>
/// <returns>The builder for fluent chaining</returns>
public static RecipeBuilder UseOrganization(this RecipeBuilder builder, string fixture, string? planType = null, int? seats = null)
public static RecipeBuilder UseOrganization(
this RecipeBuilder builder,
string fixture,
string? planType = null,
int? seats = null,
OrganizationOverrides? overrides = null)
{
builder.HasOrg = true;
builder.AddStep(_ => CreateOrganizationStep.FromFixture(fixture, planType, seats));
builder.AddStep(_ => CreateOrganizationStep.FromFixture(fixture, planType, seats, overrides));
return builder;
}

Expand All @@ -38,11 +44,18 @@ public static RecipeBuilder UseOrganization(this RecipeBuilder builder, string f
/// <param name="domain">Organization domain (used for email generation)</param>
/// <param name="seats">Number of user seats</param>
/// <param name="planType">Billing plan type (defaults to EnterpriseAnnually)</param>
/// <param name="overrides">Optional org-level overrides applied on top of plan defaults. Null keeps all plan defaults.</param>
/// <returns>The builder for fluent chaining</returns>
public static RecipeBuilder CreateOrganization(this RecipeBuilder builder, string name, string domain, int? seats = null, PlanType planType = PlanType.EnterpriseAnnually)
public static RecipeBuilder CreateOrganization(
this RecipeBuilder builder,
string name,
string domain,
int? seats = null,
PlanType planType = PlanType.EnterpriseAnnually,
OrganizationOverrides? overrides = null)
{
builder.HasOrg = true;
builder.AddStep(_ => CreateOrganizationStep.FromParams(name, domain, seats, planType));
builder.AddStep(_ => CreateOrganizationStep.FromParams(name, domain, seats, planType, overrides));
return builder;
}

Expand Down
2 changes: 1 addition & 1 deletion util/Seeder/Pipeline/RecipeOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal PipelineExecutionResult Execute(OrganizationVaultOptions options)
var recipeName = "from-options";
var builder = services.AddRecipe(recipeName);

builder.CreateOrganization(options.Name, options.Domain, options.Users + 1, options.PlanType);
builder.CreateOrganization(options.Name, options.Domain, options.Users + 1, options.PlanType, options.Overrides);
builder.AddOrganizationApiKey();
builder.AddOwner();
builder.WithGenerator(options.Domain);
Expand Down
20 changes: 20 additions & 0 deletions util/Seeder/Seeds/schemas/preset.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@
"families-annually"
],
"description": "Billing plan type. Defaults to enterprise-annually if omitted."
},
"useAutomaticUserConfirmation": {
"type": "boolean",
"description": "When true, invited users are automatically confirmed without manual admin approval."
},
"allowAdminAccessToAllCollectionItems": {
"type": "boolean",
"description": "When true, admins, owners, and some custom users can read/write all collections and items."
},
"limitItemDeletion": {
"type": "boolean",
"description": "When true, members can only delete items when they have Can Manage permission."
},
"limitCollectionCreation": {
"type": "boolean",
"description": "When true, only owners, admins, and some custom users can create collections."
},
"limitCollectionDeletion": {
"type": "boolean",
"description": "When true, only owners, admins, and some custom users can delete collections."
}
Comment thread
eliykat marked this conversation as resolved.
}
},
Expand Down
30 changes: 25 additions & 5 deletions util/Seeder/Steps/CreateOrganizationStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Bit.RustSDK;
using Bit.Seeder.Factories;
using Bit.Seeder.Models;
using Bit.Seeder.Options;
using Bit.Seeder.Pipeline;

namespace Bit.Seeder.Steps;
Expand All @@ -16,8 +17,15 @@ internal sealed class CreateOrganizationStep : IStep
private readonly string? _domain;
private readonly int? _seats;
private readonly PlanType _planType;
private readonly OrganizationOverrides? _overrides;

private CreateOrganizationStep(string? fixtureName, string? name, string? domain, int? seats, PlanType planType)
private CreateOrganizationStep(
string? fixtureName,
string? name,
string? domain,
int? seats,
PlanType planType,
OrganizationOverrides? overrides)
{
if (fixtureName is null && (name is null || domain is null))
{
Expand All @@ -30,13 +38,23 @@ private CreateOrganizationStep(string? fixtureName, string? name, string? domain
_domain = domain;
_seats = seats;
_planType = planType;
_overrides = overrides;
}

internal static CreateOrganizationStep FromFixture(string fixtureName, string? planType = null, int? seats = null) =>
new(fixtureName, null, null, seats, PlanFeatures.Parse(planType));
internal static CreateOrganizationStep FromFixture(
string fixtureName,
string? planType = null,
int? seats = null,
OrganizationOverrides? overrides = null) =>
new(fixtureName, null, null, seats, PlanFeatures.Parse(planType), overrides);

internal static CreateOrganizationStep FromParams(string name, string domain, int? seats = null, PlanType planType = PlanType.EnterpriseAnnually) =>
new(null, name, domain, seats, planType);
internal static CreateOrganizationStep FromParams(
string name,
string domain,
int? seats = null,
PlanType planType = PlanType.EnterpriseAnnually,
OrganizationOverrides? overrides = null) =>
new(null, name, domain, seats, planType, overrides);

public void Execute(SeederContext context)
{
Expand All @@ -58,6 +76,8 @@ public void Execute(SeederContext context)
var orgKeys = RustSdkService.GenerateOrganizationKeys();
var organization = OrganizationSeeder.Create(name, domain, seats, context.GetMangler(), orgKeys.PublicKey, orgKeys.PrivateKey, _planType);

PlanFeatures.ApplyOrganizationOverrides(organization, _overrides);

context.Organization = organization;
context.OrgKeys = orgKeys;
context.Domain = domain;
Expand Down
25 changes: 24 additions & 1 deletion util/SeederUtility/Commands/OrganizationArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ public class OrganizationArgs : IArgumentModel
[Option("kdf-iterations", Description = "KDF iteration count for all seeded users (default: 5000). Use 600000 for production-realistic e2e testing.")]
public int KdfIterations { get; set; } = 5_000;

[Option("auto-confirm-users", Description = "Automatically confirm invited users without manual approval")]
public bool? UseAutomaticUserConfirmation { get; set; }

[Option("allow-admin-collection-access", Description = "Allow admins/owners to access all collection items")]
public bool? AllowAdminAccessToAllCollectionItems { get; set; }

[Option("limit-item-deletion", Description = "Restrict item deletion to members with Can Manage permission")]
public bool? LimitItemDeletion { get; set; }

[Option("limit-collection-creation", Description = "Restrict collection creation to admins/owners")]
public bool? LimitCollectionCreation { get; set; }

[Option("limit-collection-deletion", Description = "Restrict collection deletion to admins/owners")]
public bool? LimitCollectionDeletion { get; set; }

public void Validate()
{
if (Users < 1)
Expand Down Expand Up @@ -103,7 +118,15 @@ public void Validate()
Density = DensityProfiles.Parse(Density),
Password = Password,
PlanType = PlanFeatures.Parse(PlanType),
KdfIterations = KdfIterations
KdfIterations = KdfIterations,
Overrides = new()
{
UseAutomaticUserConfirmation = UseAutomaticUserConfirmation,
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
LimitItemDeletion = LimitItemDeletion,
LimitCollectionCreation = LimitCollectionCreation,
LimitCollectionDeletion = LimitCollectionDeletion,
},
};

private static OrgStructureModel? ParseOrgStructure(string? structure)
Expand Down
Loading