mirror of
https://github.com/bitwarden/server.git
synced 2025-12-10 00:42:07 -06:00
[PM-28508] Fix No validation occurs for Expiration date on Self Host licenses (#6655)
* Fix the license validation bug * resolve the failing test * fix the failing test * Revert changes and Add the ui display fix * remove empty spaces * revert the changes on licensing file * revert changes to the test signup * Revert the org license file changes * revert the empty spaces * revert the empty spaces changes * remove the empty spaces * revert * Remove the duplicate code * Add the expire date fix for premium * Fix the failing test * Fix the lint error
This commit is contained in:
parent
655054aa56
commit
d619a49998
@ -1,10 +1,13 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Licenses;
|
||||||
|
using Bit.Core.Billing.Licenses.Extensions;
|
||||||
using Bit.Core.Billing.Organizations.Models;
|
using Bit.Core.Billing.Organizations.Models;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
@ -177,6 +180,30 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license, ClaimsPrincipal claimsPrincipal) :
|
||||||
|
this(organization, (Plan)null)
|
||||||
|
{
|
||||||
|
if (license != null)
|
||||||
|
{
|
||||||
|
// CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim
|
||||||
|
// The token's expiration is cryptographically secured and cannot be tampered with
|
||||||
|
// The file's Expires property can be manually edited and should NOT be trusted for display
|
||||||
|
if (claimsPrincipal != null)
|
||||||
|
{
|
||||||
|
Expiration = claimsPrincipal.GetValue<DateTime>(OrganizationLicenseConstants.Expires);
|
||||||
|
ExpirationWithoutGracePeriod = claimsPrincipal.GetValue<DateTime?>(OrganizationLicenseConstants.ExpirationWithoutGracePeriod);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No token - use the license file expiration (for older licenses without tokens)
|
||||||
|
Expiration = license.Expires;
|
||||||
|
ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? (license.Trial
|
||||||
|
? license.Expires
|
||||||
|
: license.Expires?.AddDays(-Constants.OrganizationSelfHostSubscriptionGracePeriodDays));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string StorageName { get; set; }
|
public string StorageName { get; set; }
|
||||||
public double? StorageGb { get; set; }
|
public double? StorageGb { get; set; }
|
||||||
public BillingCustomerDiscount CustomerDiscount { get; set; }
|
public BillingCustomerDiscount CustomerDiscount { get; set; }
|
||||||
|
|||||||
@ -26,7 +26,8 @@ public class AccountsController(
|
|||||||
IUserService userService,
|
IUserService userService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IUserAccountKeysQuery userAccountKeysQuery,
|
IUserAccountKeysQuery userAccountKeysQuery,
|
||||||
IFeatureService featureService) : Controller
|
IFeatureService featureService,
|
||||||
|
ILicensingService licensingService) : Controller
|
||||||
{
|
{
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||||
@ -97,12 +98,14 @@ public class AccountsController(
|
|||||||
var includeMilestone2Discount = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
|
var includeMilestone2Discount = featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2);
|
||||||
var subscriptionInfo = await paymentService.GetSubscriptionAsync(user);
|
var subscriptionInfo = await paymentService.GetSubscriptionAsync(user);
|
||||||
var license = await userService.GenerateLicenseAsync(user, subscriptionInfo);
|
var license = await userService.GenerateLicenseAsync(user, subscriptionInfo);
|
||||||
return new SubscriptionResponseModel(user, subscriptionInfo, license, includeMilestone2Discount);
|
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
|
||||||
|
return new SubscriptionResponseModel(user, subscriptionInfo, license, claimsPrincipal, includeMilestone2Discount);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var license = await userService.GenerateLicenseAsync(user);
|
var license = await userService.GenerateLicenseAsync(user);
|
||||||
return new SubscriptionResponseModel(user, license);
|
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
|
||||||
|
return new SubscriptionResponseModel(user, null, license, claimsPrincipal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@ -67,7 +67,8 @@ public class OrganizationsController(
|
|||||||
if (globalSettings.SelfHosted)
|
if (globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
|
var orgLicense = await licensingService.ReadOrganizationLicenseAsync(organization);
|
||||||
return new OrganizationSubscriptionResponseModel(organization, orgLicense);
|
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(orgLicense);
|
||||||
|
return new OrganizationSubscriptionResponseModel(organization, orgLicense, claimsPrincipal);
|
||||||
}
|
}
|
||||||
|
|
||||||
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
var plan = await pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||||
|
|||||||
@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.Billing.Constants;
|
using System.Security.Claims;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
|
using Bit.Core.Billing.Licenses;
|
||||||
|
using Bit.Core.Billing.Licenses.Extensions;
|
||||||
using Bit.Core.Billing.Models.Business;
|
using Bit.Core.Billing.Models.Business;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
@ -37,6 +40,46 @@ public class SubscriptionResponseModel : ResponseModel
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <param name="user">The user entity containing storage and premium subscription information</param>
|
||||||
|
/// <param name="subscription">Subscription information retrieved from the payment provider (Stripe/Braintree)</param>
|
||||||
|
/// <param name="license">The user's license containing expiration and feature entitlements</param>
|
||||||
|
/// <param name="claimsPrincipal">The claims principal containing cryptographically secure token claims</param>
|
||||||
|
/// <param name="includeMilestone2Discount">
|
||||||
|
/// Whether to include discount information in the response.
|
||||||
|
/// Set to true when the PM23341_Milestone_2 feature flag is enabled AND
|
||||||
|
/// you want to expose Milestone 2 discount information to the client.
|
||||||
|
/// The discount will only be included if it matches the specific Milestone 2 coupon ID.
|
||||||
|
/// </param>
|
||||||
|
public SubscriptionResponseModel(User user, SubscriptionInfo? subscription, UserLicense license, ClaimsPrincipal? claimsPrincipal, bool includeMilestone2Discount = false)
|
||||||
|
: base("subscription")
|
||||||
|
{
|
||||||
|
Subscription = subscription?.Subscription != null ? new BillingSubscription(subscription.Subscription) : null;
|
||||||
|
UpcomingInvoice = subscription?.UpcomingInvoice != null ?
|
||||||
|
new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null;
|
||||||
|
StorageName = user.Storage.HasValue ? CoreHelpers.ReadableBytesSize(user.Storage.Value) : null;
|
||||||
|
StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB
|
||||||
|
MaxStorageGb = user.MaxStorageGb;
|
||||||
|
License = license;
|
||||||
|
|
||||||
|
// CRITICAL: When a license has a Token (JWT), ALWAYS use the expiration from the token claim
|
||||||
|
// The token's expiration is cryptographically secured and cannot be tampered with
|
||||||
|
// The file's Expires property can be manually edited and should NOT be trusted for display
|
||||||
|
if (claimsPrincipal != null)
|
||||||
|
{
|
||||||
|
Expiration = claimsPrincipal.GetValue<DateTime?>(UserLicenseConstants.Expires);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No token - use the license file expiration (for older licenses without tokens)
|
||||||
|
Expiration = License.Expires;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only display the Milestone 2 subscription discount on the subscription page.
|
||||||
|
CustomerDiscount = ShouldIncludeMilestone2Discount(includeMilestone2Discount, subscription?.CustomerDiscount)
|
||||||
|
? new BillingCustomerDiscount(subscription!.CustomerDiscount!)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
public SubscriptionResponseModel(User user, UserLicense? license = null)
|
public SubscriptionResponseModel(User user, UserLicense? license = null)
|
||||||
: base("subscription")
|
: base("subscription")
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Models.Business;
|
using Bit.Core.Billing.Models.Business;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||||
@ -30,6 +31,7 @@ public class AccountsControllerTests : IDisposable
|
|||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||||
|
private readonly ILicensingService _licensingService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly AccountsController _sut;
|
private readonly AccountsController _sut;
|
||||||
|
|
||||||
@ -40,13 +42,15 @@ public class AccountsControllerTests : IDisposable
|
|||||||
_paymentService = Substitute.For<IPaymentService>();
|
_paymentService = Substitute.For<IPaymentService>();
|
||||||
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
|
||||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||||
|
_licensingService = Substitute.For<ILicensingService>();
|
||||||
_globalSettings = new GlobalSettings { SelfHosted = false };
|
_globalSettings = new GlobalSettings { SelfHosted = false };
|
||||||
|
|
||||||
_sut = new AccountsController(
|
_sut = new AccountsController(
|
||||||
_userService,
|
_userService,
|
||||||
_twoFactorIsEnabledQuery,
|
_twoFactorIsEnabledQuery,
|
||||||
_userAccountKeysQuery,
|
_userAccountKeysQuery,
|
||||||
_featureService
|
_featureService,
|
||||||
|
_licensingService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user