1
0
mirror of https://github.com/bitwarden/server synced 2026-02-20 19:33:32 +00:00
Files
server/util/Seeder/Factories/PlanFeatures.cs
Mick Letofsky 10044397c1 Implement plan types, personal ciphers and fix folder assignment (#7030)
* Implement plan types, personal ciphers and fix folder assignment
2026-02-19 15:47:37 +01:00

220 lines
7.4 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
namespace Bit.Seeder.Factories;
/// <summary>
/// Maps PlanType to organization feature flags.
/// Values sourced from MockPlans in test/Core.Test/Billing/Mocks/Plans/.
/// </summary>
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;
switch (planType)
{
case PlanType.Free:
org.Plan = "Free";
org.PlanType = PlanType.Free;
org.MaxCollections = 2;
org.MaxStorageGb = null;
ApplyMinimalFeatures(org);
break;
case PlanType.TeamsMonthly:
org.Plan = "Teams (Monthly)";
org.PlanType = PlanType.TeamsMonthly;
ApplyTeamsFeatures(org);
break;
case PlanType.TeamsAnnually:
org.Plan = "Teams (Annually)";
org.PlanType = PlanType.TeamsAnnually;
ApplyTeamsFeatures(org);
break;
case PlanType.TeamsStarter:
org.Plan = "Teams Starter";
org.PlanType = PlanType.TeamsStarter;
ApplyTeamsFeatures(org);
break;
case PlanType.EnterpriseMonthly:
org.Plan = "Enterprise (Monthly)";
org.PlanType = PlanType.EnterpriseMonthly;
ApplyEnterpriseFeatures(org);
break;
case PlanType.EnterpriseAnnually:
org.Plan = "Enterprise (Annually)";
org.PlanType = PlanType.EnterpriseAnnually;
ApplyEnterpriseFeatures(org);
break;
case PlanType.FamiliesAnnually:
org.Plan = "Families";
org.PlanType = PlanType.FamiliesAnnually;
org.MaxCollections = null;
org.MaxStorageGb = 1;
ApplyMinimalFeatures(org);
org.UseTotp = true;
org.Use2fa = true;
org.UsersGetPremium = true;
break;
default:
throw new ArgumentException(
$"Unsupported PlanType '{planType}'. Supported types: Free, TeamsMonthly, TeamsAnnually, " +
"TeamsStarter, EnterpriseMonthly, EnterpriseAnnually, FamiliesAnnually.");
}
}
public static PlanType Parse(string? planTypeString)
{
if (string.IsNullOrEmpty(planTypeString))
{
return PlanType.EnterpriseAnnually;
}
return planTypeString switch
{
"free" => PlanType.Free,
"teams-monthly" => PlanType.TeamsMonthly,
"teams-annually" => PlanType.TeamsAnnually,
"teams-starter" => PlanType.TeamsStarter,
"enterprise-monthly" => PlanType.EnterpriseMonthly,
"enterprise-annually" => PlanType.EnterpriseAnnually,
"families-annually" => PlanType.FamiliesAnnually,
_ => throw new ArgumentException(
$"Invalid planType string '{planTypeString}'. Valid values: free, teams-monthly, " +
"teams-annually, teams-starter, enterprise-monthly, enterprise-annually, families-annually.")
};
}
/// <summary>
/// Deterministic seat count from a log-normal distribution seeded by domain.
/// Ranges sourced from our production data.
/// </summary>
internal static int GenerateRealisticSeatCount(PlanType planType, string domain)
{
var (min, max, avg) = planType switch
{
PlanType.Free => (1, 2, 2),
PlanType.FamiliesAnnually => (6, 6, 6),
PlanType.TeamsMonthly => (1, 300, 15),
PlanType.TeamsAnnually => (1, 100, 7),
PlanType.TeamsStarter => (10, 10, 10),
PlanType.EnterpriseMonthly => (1, 185, 17),
PlanType.EnterpriseAnnually => (1, 12000, 60),
_ => (1, 100, 10)
};
if (min == max)
{
return min;
}
var logAvg = Math.Log(avg);
var logMax = Math.Log(max);
var sigma = (logMax - logAvg) / 2.0;
var mu = logAvg - (sigma * sigma / 2.0);
var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(domain));
var random = new Random(BitConverter.ToInt32(hashBytes, 0));
var u1 = 1.0 - random.NextDouble();
var u2 = random.NextDouble();
var stdNormal = Math.Sqrt(-2.0 * Math.Log(u1)) * Math.Cos(2.0 * Math.PI * u2);
return Math.Clamp((int)Math.Round(Math.Exp(mu + sigma * stdNormal)), min, max);
}
/// <summary>
/// Baseline: all plan-gated features off. Free and Families start here then enable selectively.
/// </summary>
private static void ApplyMinimalFeatures(Organization org)
{
org.UseGroups = false;
org.UseDirectory = false;
org.UseEvents = false;
org.UseTotp = false;
org.Use2fa = false;
org.UseApi = false;
org.UseScim = false;
org.UseSso = false;
org.UsePolicies = false;
org.UseKeyConnector = false;
org.UseResetPassword = false;
org.UseCustomPermissions = false;
org.UseOrganizationDomains = false;
org.UsersGetPremium = false;
org.SelfHost = false;
org.UsePasswordManager = true;
org.UseSecretsManager = false;
org.UseRiskInsights = false;
org.UseAdminSponsoredFamilies = false;
org.SyncSeats = false;
}
private static void ApplyTeamsFeatures(Organization org)
{
org.MaxCollections = null;
org.MaxStorageGb = 1;
org.UseGroups = true;
org.UseDirectory = true;
org.UseEvents = true;
org.UseTotp = true;
org.Use2fa = true;
org.UseApi = true;
org.UseScim = true;
org.UseSso = false;
org.UsePolicies = false;
org.UseKeyConnector = false;
org.UseResetPassword = false;
org.UseCustomPermissions = false;
org.UseOrganizationDomains = false;
org.UsersGetPremium = true;
org.SelfHost = false;
org.UsePasswordManager = true;
org.UseSecretsManager = true;
org.UseRiskInsights = false;
org.UseAdminSponsoredFamilies = false;
org.SyncSeats = true;
}
private static void ApplyEnterpriseFeatures(Organization org)
{
org.MaxCollections = null;
org.MaxStorageGb = 1;
org.UseGroups = true;
org.UseDirectory = true;
org.UseEvents = true;
org.UseTotp = true;
org.Use2fa = true;
org.UseApi = true;
org.UseScim = true;
org.UseSso = true;
org.UsePolicies = true;
org.UseKeyConnector = true;
org.UseResetPassword = true;
org.UseCustomPermissions = true;
org.UseOrganizationDomains = true;
org.UsersGetPremium = true;
org.SelfHost = true;
org.UsePasswordManager = true;
org.UseSecretsManager = true;
org.UseRiskInsights = true;
org.UseAdminSponsoredFamilies = true;
org.SyncSeats = true;
}
}