1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-20 19:14:32 -05:00

[AC-1650] [AC-1578] (#3320)

* Upgraded old 2019 plans to have the same features as 2020 and beyond

* Removed redundant test and moved additional test cases to GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull

* Fixed issue where feature flag wasn't returning correct plans

* Resolved issue where getting plans would return a value that LINQ previously cached when feature flag was in a different state

---------

Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
This commit is contained in:
Conner Turnbull 2023-11-01 08:43:35 -04:00 committed by GitHub
parent da4a86c643
commit f9fc43dbb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 485 additions and 99 deletions

View File

@ -30,29 +30,36 @@ public class MaxProjectsQueryTests
[Theory] [Theory]
[BitAutoData(PlanType.FamiliesAnnually2019)] [BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.Custom)] [BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)] [BitAutoData(PlanType.FamiliesAnnually)]
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType, public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization) SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{ {
organization.PlanType = planType; organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization); sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
await Assert.ThrowsAsync<BadRequestException>( await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1)); async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IProjectRepository>()
.DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id); .GetProjectCountByOrganizationIdAsync(organization.Id);
} }
[Theory] [Theory]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)] [BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)] [BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)] [BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)] [BitAutoData(PlanType.EnterpriseAnnually)]
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType, public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization) SutProvider<MaxProjectsQuery> sutProvider, Organization organization)

View File

@ -68,6 +68,10 @@
function togglePlanFeatures(planType) { function togglePlanFeatures(planType) {
switch(planType) { switch(planType) {
case '@((byte)PlanType.TeamsMonthly2019)':
case '@((byte)PlanType.TeamsAnnually2019)':
case '@((byte)PlanType.TeamsMonthly2020)':
case '@((byte)PlanType.TeamsAnnually2020)':
case '@((byte)PlanType.TeamsMonthly)': case '@((byte)PlanType.TeamsMonthly)':
case '@((byte)PlanType.TeamsAnnually)': case '@((byte)PlanType.TeamsAnnually)':
document.getElementById('@(nameof(Model.UsePolicies))').checked = false; document.getElementById('@(nameof(Model.UsePolicies))').checked = false;
@ -85,6 +89,10 @@
document.getElementById('@(nameof(Model.UseScim))').checked = false; document.getElementById('@(nameof(Model.UseScim))').checked = false;
break; break;
case '@((byte)PlanType.EnterpriseMonthly2019)':
case '@((byte)PlanType.EnterpriseAnnually2019)':
case '@((byte)PlanType.EnterpriseMonthly2020)':
case '@((byte)PlanType.EnterpriseAnnually2020)':
case '@((byte)PlanType.EnterpriseMonthly)': case '@((byte)PlanType.EnterpriseMonthly)':
case '@((byte)PlanType.EnterpriseAnnually)': case '@((byte)PlanType.EnterpriseAnnually)':
document.getElementById('@(nameof(Model.UsePolicies))').checked = true; document.getElementById('@(nameof(Model.UsePolicies))').checked = true;

View File

@ -32,8 +32,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
UsePasswordManager = organization.UsePasswordManager; UsePasswordManager = organization.UsePasswordManager;
UsersGetPremium = organization.UsersGetPremium; UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions; UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = organization.PlanType == PlanType.EnterpriseAnnually || UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).Product == ProductType.Enterprise;
organization.PlanType == PlanType.EnterpriseMonthly;
SelfHost = organization.SelfHost; SelfHost = organization.SelfHost;
Seats = organization.Seats; Seats = organization.Seats;
MaxCollections = organization.MaxCollections; MaxCollections = organization.MaxCollections;

View File

@ -25,8 +25,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
UseResetPassword = organization.UseResetPassword; UseResetPassword = organization.UseResetPassword;
UsersGetPremium = organization.UsersGetPremium; UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions; UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = organization.PlanType == PlanType.EnterpriseAnnually || UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).Product == ProductType.Enterprise;
organization.PlanType == PlanType.EnterpriseMonthly;
SelfHost = organization.SelfHost; SelfHost = organization.SelfHost;
Seats = organization.Seats; Seats = organization.Seats;
MaxCollections = organization.MaxCollections; MaxCollections = organization.MaxCollections;

View File

@ -1,5 +1,9 @@
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -11,17 +15,39 @@ namespace Bit.Api.Controllers;
public class PlansController : Controller public class PlansController : Controller
{ {
private readonly ITaxRateRepository _taxRateRepository; private readonly ITaxRateRepository _taxRateRepository;
public PlansController(ITaxRateRepository taxRateRepository) private readonly IFeatureService _featureService;
private readonly ICurrentContext _currentContext;
public PlansController(
ITaxRateRepository taxRateRepository,
IFeatureService featureService,
ICurrentContext currentContext)
{ {
_taxRateRepository = taxRateRepository; _taxRateRepository = taxRateRepository;
_featureService = featureService;
_currentContext = currentContext;
} }
[HttpGet("")] [HttpGet("")]
[AllowAnonymous] [AllowAnonymous]
public ListResponseModel<PlanResponseModel> Get() public ListResponseModel<PlanResponseModel> Get()
{ {
var plansUpgradeIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.BillingPlansUpgrade, _currentContext);
var data = StaticStore.Plans; var data = StaticStore.Plans;
var responses = data.Select(plan => new PlanResponseModel(plan)); var responses = data
.Where(plan => plansUpgradeIsEnabled || plan.Type <= PlanType.EnterpriseAnnually2020)
.Select(plan =>
{
if (!plansUpgradeIsEnabled && plan.Type is <= PlanType.EnterpriseAnnually2020 and >= PlanType.TeamsMonthly2020)
{
plan.LegacyYear = null;
}
else if (plansUpgradeIsEnabled && plan.Type is <= PlanType.EnterpriseAnnually2020 and >= PlanType.TeamsMonthly2020)
{
plan.LegacyYear = 2023;
}
return new PlanResponseModel(plan);
});
return new ListResponseModel<PlanResponseModel>(responses); return new ListResponseModel<PlanResponseModel>(responses);
} }

View File

@ -159,14 +159,18 @@ public class FreshsalesController : Controller
planName = "Families"; planName = "Families";
return true; return true;
case PlanType.TeamsAnnually: case PlanType.TeamsAnnually:
case PlanType.TeamsAnnually2020:
case PlanType.TeamsAnnually2019: case PlanType.TeamsAnnually2019:
case PlanType.TeamsMonthly: case PlanType.TeamsMonthly:
case PlanType.TeamsMonthly2020:
case PlanType.TeamsMonthly2019: case PlanType.TeamsMonthly2019:
planName = "Teams"; planName = "Teams";
return true; return true;
case PlanType.EnterpriseAnnually: case PlanType.EnterpriseAnnually:
case PlanType.EnterpriseAnnually2020:
case PlanType.EnterpriseAnnually2019: case PlanType.EnterpriseAnnually2019:
case PlanType.EnterpriseMonthly: case PlanType.EnterpriseMonthly:
case PlanType.EnterpriseMonthly2020:
case PlanType.EnterpriseMonthly2019: case PlanType.EnterpriseMonthly2019:
planName = "Enterprise"; planName = "Enterprise";
return true; return true;

View File

@ -668,18 +668,7 @@ public class StripeController : Controller
return new Tuple<Guid?, Guid?>(orgId, userId); return new Tuple<Guid?, Guid?>(orgId, userId);
} }
private bool OrgPlanForInvoiceNotifications(Organization org) private static bool OrgPlanForInvoiceNotifications(Organization org) => StaticStore.GetPlan(org.PlanType).IsAnnual;
{
switch (org.PlanType)
{
case PlanType.FamiliesAnnually:
case PlanType.TeamsAnnually:
case PlanType.EnterpriseAnnually:
return true;
default:
return false;
}
}
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false) private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)
{ {

View File

@ -49,6 +49,7 @@ public static class FeatureFlagKeys
public const string BulkCollectionAccess = "bulk-collection-access"; public const string BulkCollectionAccess = "bulk-collection-access";
public const string AutofillOverlay = "autofill-overlay"; public const string AutofillOverlay = "autofill-overlay";
public const string ItemShare = "item-share"; public const string ItemShare = "item-share";
public const string BillingPlansUpgrade = "billing-plans-upgrade";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -20,12 +20,20 @@ public enum PlanType : byte
Custom = 6, Custom = 6,
[Display(Name = "Families")] [Display(Name = "Families")]
FamiliesAnnually = 7, FamiliesAnnually = 7,
[Display(Name = "Teams (Monthly) 2020")]
TeamsMonthly2020 = 8,
[Display(Name = "Teams (Annually) 2020")]
TeamsAnnually2020 = 9,
[Display(Name = "Enterprise (Monthly) 2020")]
EnterpriseMonthly2020 = 10,
[Display(Name = "Enterprise (Annually) 2020")]
EnterpriseAnnually2020 = 11,
[Display(Name = "Teams (Monthly)")] [Display(Name = "Teams (Monthly)")]
TeamsMonthly = 8, TeamsMonthly = 12,
[Display(Name = "Teams (Annually)")] [Display(Name = "Teams (Annually)")]
TeamsAnnually = 9, TeamsAnnually = 13,
[Display(Name = "Enterprise (Monthly)")] [Display(Name = "Enterprise (Monthly)")]
EnterpriseMonthly = 10, EnterpriseMonthly = 14,
[Display(Name = "Enterprise (Annually)")] [Display(Name = "Enterprise (Annually)")]
EnterpriseAnnually = 11, EnterpriseAnnually = 15,
} }

View File

@ -28,7 +28,7 @@ public abstract record Plan
public bool HasCustomPermissions { get; protected init; } public bool HasCustomPermissions { get; protected init; }
public int UpgradeSortOrder { get; protected init; } public int UpgradeSortOrder { get; protected init; }
public int DisplaySortOrder { get; protected init; } public int DisplaySortOrder { get; protected init; }
public int? LegacyYear { get; protected init; } public int? LegacyYear { get; set; }
public bool Disabled { get; protected init; } public bool Disabled { get; protected init; }
public PasswordManagerPlanFeatures PasswordManager { get; protected init; } public PasswordManagerPlanFeatures PasswordManager { get; protected init; }
public SecretsManagerPlanFeatures SecretsManager { get; protected init; } public SecretsManagerPlanFeatures SecretsManager { get; protected init; }

View File

@ -24,6 +24,10 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
HasTotp = true; HasTotp = true;
Has2fa = true; Has2fa = true;
HasApi = true; HasApi = true;
HasSso = true;
HasKeyConnector = true;
HasScim = true;
HasResetPassword = true;
UsersGetPremium = true; UsersGetPremium = true;
HasCustomPermissions = true; HasCustomPermissions = true;
@ -31,9 +35,41 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
DisplaySortOrder = 3; DisplaySortOrder = 3;
LegacyYear = 2020; LegacyYear = 2020;
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual); PasswordManager = new Enterprise2019PasswordManagerFeatures(isAnnual);
} }
private record Enterprise2019SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Enterprise2019SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 200;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 144;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 13;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures private record Enterprise2019PasswordManagerFeatures : PasswordManagerPlanFeatures
{ {
public Enterprise2019PasswordManagerFeatures(bool isAnnual) public Enterprise2019PasswordManagerFeatures(bool isAnnual)

View File

@ -0,0 +1,101 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record Enterprise2020Plan : Models.StaticStore.Plan
{
public Enterprise2020Plan(bool isAnnual)
{
Type = isAnnual ? PlanType.EnterpriseAnnually2020 : PlanType.EnterpriseMonthly2020;
Product = ProductType.Enterprise;
Name = isAnnual ? "Enterprise (Annually) 2020" : "Enterprise (Monthly) 2020";
IsAnnual = isAnnual;
NameLocalizationKey = "planNameEnterprise";
DescriptionLocalizationKey = "planDescEnterprise";
CanBeUsedByBusiness = true;
TrialPeriodDays = 7;
HasPolicies = true;
HasSelfHost = true;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
HasSso = true;
HasKeyConnector = true;
HasScim = true;
HasResetPassword = true;
UsersGetPremium = true;
HasCustomPermissions = true;
UpgradeSortOrder = 3;
DisplaySortOrder = 3;
LegacyYear = 2023;
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
SecretsManager = new Enterprise2020SecretsManagerFeatures(isAnnual);
}
private record Enterprise2020SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Enterprise2020SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 200;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 144;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-enterprise-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 13;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Enterprise2020PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public Enterprise2020PasswordManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BaseStorageGb = 1;
HasAdditionalStorageOption = true;
HasAdditionalSeatsOption = true;
AllowSeatAutoscale = true;
if (isAnnual)
{
AdditionalStoragePricePerGb = 4;
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-enterprise-org-seat-annually";
SeatPrice = 60;
}
else
{
StripeSeatPlanId = "2020-enterprise-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 6;
AdditionalStoragePricePerGb = 0.5M;
}
}
}
}

View File

@ -85,14 +85,14 @@ public record EnterprisePlan : Models.StaticStore.Plan
{ {
AdditionalStoragePricePerGb = 4; AdditionalStoragePricePerGb = 4;
StripeStoragePlanId = "storage-gb-annually"; StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-enterprise-org-seat-annually"; StripeSeatPlanId = "2023-enterprise-org-seat-annually";
SeatPrice = 60; SeatPrice = 72;
} }
else else
{ {
StripeSeatPlanId = "2020-enterprise-seat-monthly"; StripeSeatPlanId = "2023-enterprise-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly"; StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 6; SeatPrice = 7;
AdditionalStoragePricePerGb = 0.5M; AdditionalStoragePricePerGb = 0.5M;
} }
} }

View File

@ -17,6 +17,7 @@ public record Families2019Plan : Models.StaticStore.Plan
HasSelfHost = true; HasSelfHost = true;
HasTotp = true; HasTotp = true;
UsersGetPremium = true;
UpgradeSortOrder = 1; UpgradeSortOrder = 1;
DisplaySortOrder = 1; DisplaySortOrder = 1;

View File

@ -16,15 +16,53 @@ public record Teams2019Plan : Models.StaticStore.Plan
TrialPeriodDays = 7; TrialPeriodDays = 7;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true; HasTotp = true;
Has2fa = true;
HasApi = true;
UsersGetPremium = true;
UpgradeSortOrder = 2; UpgradeSortOrder = 2;
DisplaySortOrder = 2; DisplaySortOrder = 2;
LegacyYear = 2020; LegacyYear = 2020;
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual); PasswordManager = new Teams2019PasswordManagerFeatures(isAnnual);
} }
private record Teams2019SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Teams2019SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 50;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 72;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 7;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures private record Teams2019PasswordManagerFeatures : PasswordManagerPlanFeatures
{ {
public Teams2019PasswordManagerFeatures(bool isAnnual) public Teams2019PasswordManagerFeatures(bool isAnnual)

View File

@ -0,0 +1,95 @@
using Bit.Core.Enums;
namespace Bit.Core.Models.StaticStore.Plans;
public record Teams2020Plan : Models.StaticStore.Plan
{
public Teams2020Plan(bool isAnnual)
{
Type = isAnnual ? PlanType.TeamsAnnually2020 : PlanType.TeamsMonthly2020;
Product = ProductType.Teams;
Name = isAnnual ? "Teams (Annually) 2020" : "Teams (Monthly) 2020";
IsAnnual = isAnnual;
NameLocalizationKey = "planNameTeams";
DescriptionLocalizationKey = "planDescTeams";
CanBeUsedByBusiness = true;
TrialPeriodDays = 7;
HasGroups = true;
HasDirectory = true;
HasEvents = true;
HasTotp = true;
Has2fa = true;
HasApi = true;
UsersGetPremium = true;
UpgradeSortOrder = 2;
DisplaySortOrder = 2;
LegacyYear = 2023;
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
SecretsManager = new Teams2020SecretsManagerFeatures(isAnnual);
}
private record Teams2020SecretsManagerFeatures : SecretsManagerPlanFeatures
{
public Teams2020SecretsManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BasePrice = 0;
BaseServiceAccount = 50;
HasAdditionalSeatsOption = true;
HasAdditionalServiceAccountOption = true;
AllowSeatAutoscale = true;
AllowServiceAccountsAutoscale = true;
if (isAnnual)
{
StripeSeatPlanId = "secrets-manager-teams-seat-annually";
StripeServiceAccountPlanId = "secrets-manager-service-account-annually";
SeatPrice = 72;
AdditionalPricePerServiceAccount = 6;
}
else
{
StripeSeatPlanId = "secrets-manager-teams-seat-monthly";
StripeServiceAccountPlanId = "secrets-manager-service-account-monthly";
SeatPrice = 7;
AdditionalPricePerServiceAccount = 0.5M;
}
}
}
private record Teams2020PasswordManagerFeatures : PasswordManagerPlanFeatures
{
public Teams2020PasswordManagerFeatures(bool isAnnual)
{
BaseSeats = 0;
BaseStorageGb = 1;
BasePrice = 0;
HasAdditionalStorageOption = true;
HasAdditionalSeatsOption = true;
AllowSeatAutoscale = true;
if (isAnnual)
{
StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-teams-org-seat-annually";
SeatPrice = 36;
AdditionalStoragePricePerGb = 4;
}
else
{
StripeSeatPlanId = "2020-teams-org-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 4;
AdditionalStoragePricePerGb = 0.5M;
}
}
}
}

View File

@ -78,15 +78,15 @@ public record TeamsPlan : Models.StaticStore.Plan
if (isAnnual) if (isAnnual)
{ {
StripeStoragePlanId = "storage-gb-annually"; StripeStoragePlanId = "storage-gb-annually";
StripeSeatPlanId = "2020-teams-org-seat-annually"; StripeSeatPlanId = "2023-teams-org-seat-annually";
SeatPrice = 36; SeatPrice = 48;
AdditionalStoragePricePerGb = 4; AdditionalStoragePricePerGb = 4;
} }
else else
{ {
StripeSeatPlanId = "2020-teams-org-seat-monthly"; StripeSeatPlanId = "2023-teams-org-seat-monthly";
StripeStoragePlanId = "storage-gb-monthly"; StripeStoragePlanId = "storage-gb-monthly";
SeatPrice = 4; SeatPrice = 5;
AdditionalStoragePricePerGb = 0.5M; AdditionalStoragePricePerGb = 0.5M;
} }
} }

View File

@ -98,14 +98,6 @@ public class PolicyService : IPolicyService
await DependsOnSingleOrgAsync(org); await DependsOnSingleOrgAsync(org);
} }
break; break;
// Activate Autofill is only available to Enterprise 2020-current plans
case PolicyType.ActivateAutofill:
if (policy.Enabled)
{
LockedTo2020Plan(org);
}
break;
} }
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -274,14 +266,6 @@ public class PolicyService : IPolicyService
} }
} }
private void LockedTo2020Plan(Organization org)
{
if (org.PlanType != PlanType.EnterpriseAnnually && org.PlanType != PlanType.EnterpriseMonthly)
{
throw new BadRequestException("This policy is only available to 2020 Enterprise plans.");
}
}
private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org) private async Task RequiredBySsoTrustedDeviceEncryptionAsync(Organization org)
{ {
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id); var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);

View File

@ -6,7 +6,7 @@ using Bit.Core.Models.StaticStore.Plans;
namespace Bit.Core.Utilities; namespace Bit.Core.Utilities;
public class StaticStore public static class StaticStore
{ {
static StaticStore() static StaticStore()
{ {
@ -112,6 +112,11 @@ public class StaticStore
new EnterprisePlan(false), new EnterprisePlan(false),
new TeamsPlan(true), new TeamsPlan(true),
new TeamsPlan(false), new TeamsPlan(false),
new Enterprise2020Plan(true),
new Enterprise2020Plan(false),
new Teams2020Plan(true),
new Teams2020Plan(false),
new FamiliesPlan(), new FamiliesPlan(),
new FreePlan(), new FreePlan(),
new CustomPlan(), new CustomPlan(),
@ -139,8 +144,7 @@ public class StaticStore
} }
}; };
public static Models.StaticStore.Plan GetPlan(PlanType planType) => public static Plan GetPlan(PlanType planType) => Plans.SingleOrDefault(p => p.Type == planType);
Plans.SingleOrDefault(p => p.Type == planType);
public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) =>

View File

@ -95,41 +95,44 @@ public class OrganizationRepository : Repository<Core.Entities.Organization, Org
public async Task<ICollection<Core.Entities.Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take) public async Task<ICollection<Core.Entities.Organization>> SearchUnassignedToProviderAsync(string name, string ownerEmail, int skip, int take)
{ {
using (var scope = ServiceScopeFactory.CreateScope()) using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
var query = from o in dbContext.Organizations
where o.PlanType >= PlanType.TeamsMonthly2020 && o.PlanType <= PlanType.EnterpriseAnnually &&
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) &&
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
select o;
if (string.IsNullOrWhiteSpace(ownerEmail))
{ {
var dbContext = GetDatabaseContext(scope); return await query.OrderByDescending(o => o.CreationDate)
var query = from o in dbContext.Organizations .Skip(skip)
where o.PlanType >= PlanType.TeamsMonthly && o.PlanType <= PlanType.EnterpriseAnnually && .Take(take)
!dbContext.ProviderOrganizations.Any(po => po.OrganizationId == o.Id) && .ToArrayAsync();
(string.IsNullOrWhiteSpace(name) || EF.Functions.Like(o.Name, $"%{name}%"))
select o;
if (!string.IsNullOrWhiteSpace(ownerEmail))
{
if (dbContext.Database.IsNpgsql())
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.ILike(EF.Functions.Collate(u.Email, "default"), $"{ownerEmail}%")
select o;
}
else
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $"{ownerEmail}%")
select o;
}
}
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
} }
if (dbContext.Database.IsNpgsql())
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.ILike(EF.Functions.Collate(u.Email, "default"), $"{ownerEmail}%")
select o;
}
else
{
query = from o in query
join ou in dbContext.OrganizationUsers
on o.Id equals ou.OrganizationId
join u in dbContext.Users
on ou.UserId equals u.Id
where ou.Type == OrganizationUserType.Owner && EF.Functions.Like(u.Email, $"{ownerEmail}%")
select o;
}
return await query.OrderByDescending(o => o.CreationDate).Skip(skip).Take(take).ToArrayAsync();
} }
public async Task UpdateStorageAsync(Guid id) public async Task UpdateStorageAsync(Guid id)

View File

@ -133,8 +133,8 @@ public class SecretsManagerOrganizationCustomization : ICustomization
{ {
public void Customize(IFixture fixture) public void Customize(IFixture fixture)
{ {
const PlanType planType = PlanType.EnterpriseAnnually;
var organizationId = Guid.NewGuid(); var organizationId = Guid.NewGuid();
var planType = PlanType.EnterpriseAnnually;
fixture.Customize<Organization>(composer => composer fixture.Customize<Organization>(composer => composer
.With(o => o.Id, organizationId) .With(o => o.Id, organizationId)
@ -143,8 +143,7 @@ public class SecretsManagerOrganizationCustomization : ICustomization
.With(o => o.PlanType, planType) .With(o => o.PlanType, planType)
.With(o => o.Plan, StaticStore.GetPlan(planType).Name) .With(o => o.Plan, StaticStore.GetPlan(planType).Name)
.With(o => o.MaxAutoscaleSmSeats, (int?)null) .With(o => o.MaxAutoscaleSmSeats, (int?)null)
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null) .With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null));
);
} }
} }

View File

@ -15,17 +15,45 @@ public class SecretsManagerSubscriptionUpdateTests
[BitAutoData(PlanType.Custom)] [BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)] [BitAutoData(PlanType.FamiliesAnnually)]
[BitAutoData(PlanType.FamiliesAnnually2019)] [BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.EnterpriseMonthly2019)] public Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsAnnually2019)]
public async Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
PlanType planType, PlanType planType,
Organization organization) Organization organization)
{ {
// Arrange
organization.PlanType = planType; organization.PlanType = planType;
// Act
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false)); var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false));
// Assert
Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase); Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase);
return Task.CompletedTask;
}
[Theory]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseMonthly2020)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.EnterpriseAnnually2020)]
[BitAutoData(PlanType.EnterpriseAnnually)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsMonthly2020)]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.TeamsAnnually2020)]
[BitAutoData(PlanType.TeamsAnnually)]
public void UpdateSubscription_WithNonSecretsManagerPlanType_DoesNotThrowException(
PlanType planType,
Organization organization)
{
// Arrange
organization.PlanType = planType;
// Act
var ex = Record.Exception(() => new SecretsManagerSubscriptionUpdate(organization, false));
// Assert
Assert.Null(ex);
} }
} }

View File

@ -10,10 +10,10 @@ public class StaticStoreTests
[Fact] [Fact]
public void StaticStore_Initialization_Success() public void StaticStore_Initialization_Success()
{ {
var plans = StaticStore.Plans; var plans = StaticStore.Plans.ToList();
Assert.NotNull(plans); Assert.NotNull(plans);
Assert.NotEmpty(plans); Assert.NotEmpty(plans);
Assert.Equal(12, plans.Count()); Assert.Equal(16, plans.Count);
} }
[Theory] [Theory]

View File

@ -0,0 +1,21 @@
BEGIN TRY
BEGIN TRANSACTION;
UPDATE
[dbo].[Organization]
SET
[Use2fa] = 1,
[UseApi] = 1,
[UseDirectory] = 1,
[UseEvents] = 1,
[UseGroups] = 1,
[UsersGetPremium] = 1
WHERE
[PlanType] IN (2, 3); -- Teams 2019
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW;
END CATCH

View File

@ -0,0 +1,19 @@
BEGIN TRY
BEGIN TRANSACTION;
UPDATE
[dbo].[Organization]
SET
[UseSso] = 1,
[UseKeyConnector] = 1,
[UseScim] = 1,
[UseResetPassword] = 1
WHERE
[PlanType] IN (4, 5) -- Enterprise 2019
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW;
END CATCH

View File

@ -0,0 +1,16 @@
BEGIN TRY
BEGIN TRANSACTION;
UPDATE
[dbo].[Organization]
SET
[UsersGetPremium] = 1
WHERE
[PlanType] = 1 -- Families 2019 Annual
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
THROW;
END CATCH