mirror of
https://github.com/bitwarden/server.git
synced 2026-06-01 01:55:55 -05:00
173 lines
6.8 KiB
C#
173 lines
6.8 KiB
C#
using Bit.Seeder.Data.Distributions;
|
|
using Bit.Seeder.Data.Enums;
|
|
using Bit.Seeder.Factories;
|
|
using Bit.Seeder.Options;
|
|
using CommandDotNet;
|
|
|
|
namespace Bit.SeederUtility.Commands;
|
|
|
|
/// <summary>
|
|
/// CLI argument model for the organization command.
|
|
/// Maps to <see cref="OrganizationVaultOptions"/> for the Seeder library.
|
|
/// </summary>
|
|
public class OrganizationArgs : IArgumentModel
|
|
{
|
|
[Option('n', "name", Description = "Name of organization")]
|
|
public string Name { get; set; } = null!;
|
|
|
|
[Option('u', "users", Description = "Number of users to generate (minimum 1)")]
|
|
public int Users { get; set; }
|
|
|
|
[Option('d', "domain", Description = "Email domain for users")]
|
|
public string Domain { get; set; } = null!;
|
|
|
|
[Option('c', "ciphers", Description = "Number of ciphers to create (default: 0, no vault data)")]
|
|
public int? Ciphers { get; set; }
|
|
|
|
[Option('g', "groups", Description = "Number of groups to create (default: 0, no groups)")]
|
|
public int? Groups { get; set; }
|
|
|
|
[Option("collections", Description = "Number of collections to create (default: 0). Required for density profiles to be useful.")]
|
|
public int? Collections { get; set; }
|
|
|
|
[Option("density", Description = "Named density profile: balanced, highPerm, highCollection, broad, minimal, groupHeavy, or sparse")]
|
|
public string? Density { get; set; }
|
|
|
|
[Option('m', "mix-user-statuses", Description = "Use realistic status mix (85% confirmed, 5% each invited/accepted/revoked). Requires >= 10 users.")]
|
|
public bool MixStatuses { get; set; } = true;
|
|
|
|
[Option('o', "org-structure", Description = "Org structure for collections: Traditional, Spotify, Modern, Government, SchoolDistrict, Healthcare, or Startup")]
|
|
public string? Structure { get; set; }
|
|
|
|
[Option('r', "region", Description = "Geographic region for names: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global")]
|
|
public string? Region { get; set; }
|
|
|
|
[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; }
|
|
|
|
[Option("plan-type", Description = "Billing plan type: free, teams-monthly, teams-annually, enterprise-monthly, enterprise-annually, teams-starter, families-annually. Defaults to enterprise-annually.")]
|
|
public string PlanType { get; set; } = "enterprise-annually";
|
|
|
|
[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)
|
|
{
|
|
throw new ArgumentException("Users must be at least 1.");
|
|
}
|
|
|
|
if (!Domain.EndsWith(".example", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new ArgumentException("Domain must end with '.example' (RFC 2606). Example: myorg.example");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(Structure))
|
|
{
|
|
ParseOrgStructure(Structure);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(Region))
|
|
{
|
|
ParseGeographicRegion(Region);
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(Density))
|
|
{
|
|
DensityProfiles.Parse(Density);
|
|
}
|
|
|
|
PlanFeatures.Parse(PlanType);
|
|
|
|
if (KdfIterations < 5_000)
|
|
{
|
|
throw new ArgumentException("KDF iterations must be at least 5,000.");
|
|
}
|
|
}
|
|
|
|
public OrganizationVaultOptions ToOptions() => new()
|
|
{
|
|
Name = Name,
|
|
Domain = Domain,
|
|
Users = Users,
|
|
Ciphers = Ciphers ?? 0,
|
|
Groups = Groups ?? 0,
|
|
Collections = Collections ?? 0,
|
|
RealisticStatusMix = MixStatuses,
|
|
StructureModel = ParseOrgStructure(Structure),
|
|
Region = ParseGeographicRegion(Region),
|
|
Density = DensityProfiles.Parse(Density),
|
|
Password = Password,
|
|
PlanType = PlanFeatures.Parse(PlanType),
|
|
KdfIterations = KdfIterations,
|
|
Overrides = new()
|
|
{
|
|
UseAutomaticUserConfirmation = UseAutomaticUserConfirmation,
|
|
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
|
LimitItemDeletion = LimitItemDeletion,
|
|
LimitCollectionCreation = LimitCollectionCreation,
|
|
LimitCollectionDeletion = LimitCollectionDeletion,
|
|
},
|
|
};
|
|
|
|
private static OrgStructureModel? ParseOrgStructure(string? structure)
|
|
{
|
|
if (string.IsNullOrEmpty(structure))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return structure.ToLowerInvariant() switch
|
|
{
|
|
"traditional" => OrgStructureModel.Traditional,
|
|
"spotify" => OrgStructureModel.Spotify,
|
|
"modern" => OrgStructureModel.Modern,
|
|
"government" => OrgStructureModel.Government,
|
|
"schooldistrict" => OrgStructureModel.SchoolDistrict,
|
|
"healthcare" => OrgStructureModel.Healthcare,
|
|
"startup" => OrgStructureModel.Startup,
|
|
_ => throw new ArgumentException(
|
|
$"Unknown structure '{structure}'. Use: Traditional, Spotify, Modern, Government, SchoolDistrict, Healthcare, or Startup")
|
|
};
|
|
}
|
|
|
|
private static GeographicRegion? ParseGeographicRegion(string? region)
|
|
{
|
|
if (string.IsNullOrEmpty(region))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return region.ToLowerInvariant() switch
|
|
{
|
|
"northamerica" => GeographicRegion.NorthAmerica,
|
|
"europe" => GeographicRegion.Europe,
|
|
"asiapacific" => GeographicRegion.AsiaPacific,
|
|
"latinamerica" => GeographicRegion.LatinAmerica,
|
|
"middleeast" => GeographicRegion.MiddleEast,
|
|
"africa" => GeographicRegion.Africa,
|
|
"global" => GeographicRegion.Global,
|
|
_ => throw new ArgumentException($"Unknown region '{region}'. Use: NorthAmerica, Europe, AsiaPacific, LatinAmerica, MiddleEast, Africa, or Global")
|
|
};
|
|
}
|
|
}
|