1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-10 12:24:50 -05:00

[PM-16684] Integrate Pricing Service behind FF (#5276)

* Remove gRPC and convert PricingClient to HttpClient wrapper

* Add PlanType.GetProductTier extension

Many instances of StaticStore use are just to get the ProductTierType of a PlanType, but this can be derived from the PlanType itself without having to fetch the entire plan.

* Remove invocations of the StaticStore in non-Test code

* Deprecate StaticStore entry points

* Run dotnet format

* Matt's feedback

* Run dotnet format

* Rui's feedback

* Run dotnet format

* Replacements since approval

* Run dotnet format
This commit is contained in:
Alex Morask
2025-02-27 07:55:46 -05:00
committed by GitHub
parent bd66f06bd9
commit a2e665cb96
78 changed files with 1178 additions and 712 deletions

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -55,6 +56,7 @@ public class OrganizationUsersController : Controller
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
private readonly IGetOrganizationUsersManagementStatusQuery _getOrganizationUsersManagementStatusQuery;
private readonly IFeatureService _featureService;
private readonly IPricingClient _pricingClient;
public OrganizationUsersController(
IOrganizationRepository organizationRepository,
@ -77,7 +79,8 @@ public class OrganizationUsersController : Controller
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand,
IGetOrganizationUsersManagementStatusQuery getOrganizationUsersManagementStatusQuery,
IFeatureService featureService)
IFeatureService featureService,
IPricingClient pricingClient)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -100,6 +103,7 @@ public class OrganizationUsersController : Controller
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
_getOrganizationUsersManagementStatusQuery = getOrganizationUsersManagementStatusQuery;
_featureService = featureService;
_pricingClient = pricingClient;
}
[HttpGet("{id}")]
@ -648,7 +652,9 @@ public class OrganizationUsersController : Controller
if (additionalSmSeatsRequired > 0)
{
var organization = await _organizationRepository.GetByIdAsync(orgId);
var update = new SecretsManagerSubscriptionUpdate(organization, true)
// TODO: https://bitwarden.atlassian.net/browse/PM-17000
var plan = await _pricingClient.GetPlanOrThrow(organization!.PlanType);
var update = new SecretsManagerSubscriptionUpdate(organization, plan, true)
.AdjustSeats(additionalSmSeatsRequired);
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
}

View File

@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
@ -60,6 +61,7 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPricingClient _pricingClient;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -81,7 +83,8 @@ public class OrganizationsController : Controller
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
IOrganizationDeleteCommand organizationDeleteCommand)
IOrganizationDeleteCommand organizationDeleteCommand,
IPricingClient pricingClient)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -103,6 +106,7 @@ public class OrganizationsController : Controller
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
_organizationDeleteCommand = organizationDeleteCommand;
_pricingClient = pricingClient;
}
[HttpGet("{id}")]
@ -120,7 +124,8 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
return new OrganizationResponseModel(organization);
var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
}
[HttpGet("")]
@ -181,7 +186,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization);
var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
return new OrganizationResponseModel(result.Organization, plan);
}
[HttpPost("create-without-payment")]
@ -196,7 +202,8 @@ public class OrganizationsController : Controller
var organizationSignup = model.ToOrganizationSignup(user);
var result = await _cloudOrganizationSignUpCommand.SignUpOrganizationAsync(organizationSignup);
return new OrganizationResponseModel(result.Organization);
var plan = await _pricingClient.GetPlanOrThrow(result.Organization.PlanType);
return new OrganizationResponseModel(result.Organization, plan);
}
[HttpPut("{id}")]
@ -224,7 +231,8 @@ public class OrganizationsController : Controller
}
await _organizationService.UpdateAsync(model.ToOrganization(organization, _globalSettings), updateBilling);
return new OrganizationResponseModel(organization);
var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
}
[HttpPost("{id}/storage")]
@ -358,8 +366,8 @@ public class OrganizationsController : Controller
if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim)
{
// Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types
var plan = StaticStore.GetPlan(organization.PlanType);
if (plan.ProductTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
var productTier = organization.PlanType.GetProductTier();
if (productTier is not ProductTierType.Enterprise and not ProductTierType.Teams)
{
throw new NotFoundException();
}
@ -542,7 +550,8 @@ public class OrganizationsController : Controller
}
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization);
var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
}
[HttpGet("{id}/plan-type")]

View File

@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Models.Business;
using Bit.Core.Models.StaticStore;
using Bit.Core.Utilities;
using Constants = Bit.Core.Constants;
@ -11,8 +12,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationResponseModel : ResponseModel
{
public OrganizationResponseModel(Organization organization, string obj = "organization")
: base(obj)
public OrganizationResponseModel(
Organization organization,
Plan plan,
string obj = "organization") : base(obj)
{
if (organization == null)
{
@ -28,7 +31,8 @@ public class OrganizationResponseModel : ResponseModel
BusinessCountry = organization.BusinessCountry;
BusinessTaxNumber = organization.BusinessTaxNumber;
BillingEmail = organization.BillingEmail;
Plan = new PlanResponseModel(StaticStore.GetPlan(organization.PlanType));
// Self-Host instances only require plan information that can be derived from the Organization record.
Plan = plan != null ? new PlanResponseModel(plan) : new PlanResponseModel(organization);
PlanType = organization.PlanType;
Seats = organization.Seats;
MaxAutoscaleSeats = organization.MaxAutoscaleSeats;
@ -110,7 +114,9 @@ public class OrganizationResponseModel : ResponseModel
public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
{
public OrganizationSubscriptionResponseModel(Organization organization) : base(organization, "organizationSubscription")
public OrganizationSubscriptionResponseModel(
Organization organization,
Plan plan) : base(organization, plan, "organizationSubscription")
{
Expiration = organization.ExpirationDate;
StorageName = organization.Storage.HasValue ?
@ -119,8 +125,11 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB
}
public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription, bool hideSensitiveData)
: this(organization)
public OrganizationSubscriptionResponseModel(
Organization organization,
SubscriptionInfo subscription,
Plan plan,
bool hideSensitiveData) : this(organization, plan)
{
Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
UpcomingInvoice = subscription.UpcomingInvoice != null ? new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
@ -142,7 +151,7 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
}
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) :
this(organization)
this(organization, (Plan)null)
{
if (license != null)
{

View File

@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
@ -37,7 +38,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
UsePasswordManager = organization.UsePasswordManager;
UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise;
UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
SelfHost = organization.SelfHost;
Seats = organization.Seats;
MaxCollections = organization.MaxCollections;
@ -60,7 +61,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null &&
StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise)
.UsersCanSponsor(organization);
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
ProductTierType = organization.PlanType.GetProductTier();
FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate;
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;

View File

@ -1,8 +1,8 @@
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.AdminConsole.Models.Response;
@ -26,7 +26,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
UseResetPassword = organization.UseResetPassword;
UsersGetPremium = organization.UsersGetPremium;
UseCustomPermissions = organization.UseCustomPermissions;
UseActivateAutofillPolicy = StaticStore.GetPlan(organization.PlanType).ProductTier == ProductTierType.Enterprise;
UseActivateAutofillPolicy = organization.PlanType.GetProductTier() == ProductTierType.Enterprise;
SelfHost = organization.SelfHost;
Seats = organization.Seats;
MaxCollections = organization.MaxCollections;
@ -44,7 +44,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProviderId = organization.ProviderId;
ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType;
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
ProductTierType = organization.PlanType.GetProductTier();
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;