mirror of
https://github.com/bitwarden/server
synced 2026-01-12 21:44:13 +00:00
[PM-29611] Decouple License from Subscription Response (#6768)
* implement the ticket request * resolve the build lint error * Resolve the build lint error * Address review comments * Fixt the lint and failing unit test * Fix NSubstitute mock - use concrete ClaimsPrincipal instead of Arg.Any in Returns() * resolve InjectUser issues * Fix the failing testing * Fix the failing unit test
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Api.Billing.Models.Requests.Premium;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Licenses.Queries;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
@@ -21,6 +22,7 @@ public class AccountBillingVNextController(
|
||||
ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IGetUserLicenseQuery getUserLicenseQuery,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
|
||||
{
|
||||
[HttpGet("credit")]
|
||||
@@ -77,4 +79,13 @@ public class AccountBillingVNextController(
|
||||
user, paymentMethod, billingAddress, additionalStorageGb);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpGet("license")]
|
||||
[InjectUser]
|
||||
public async Task<IResult> GetLicenseAsync(
|
||||
[BindNever] User user)
|
||||
{
|
||||
var response = await getUserLicenseQuery.Run(user);
|
||||
return TypedResults.Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Caches.Implementations;
|
||||
using Bit.Core.Billing.Licenses;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Organizations.Commands;
|
||||
using Bit.Core.Billing.Organizations.Queries;
|
||||
@@ -28,6 +29,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
|
||||
services.AddTransient<ISubscriberService, SubscriberService>();
|
||||
services.AddLicenseServices();
|
||||
services.AddLicenseOperations();
|
||||
services.AddPricingClient();
|
||||
services.AddPaymentOperations();
|
||||
services.AddOrganizationLicenseCommandsQueries();
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Billing.Licenses.Extensions;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Models.Api.Response;
|
||||
|
||||
/// <summary>
|
||||
/// Response model containing user license information.
|
||||
/// Separated from subscription data to maintain separation of concerns.
|
||||
/// </summary>
|
||||
public class LicenseResponseModel : ResponseModel
|
||||
{
|
||||
public LicenseResponseModel(UserLicense license, ClaimsPrincipal? claimsPrincipal)
|
||||
: base("license")
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The user's license containing feature entitlements and metadata.
|
||||
/// </summary>
|
||||
public UserLicense License { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The license expiration date.
|
||||
/// Extracted from the cryptographically secured JWT token when available,
|
||||
/// otherwise falls back to the license file's expiration date.
|
||||
/// </summary>
|
||||
public DateTime? Expiration { get; set; }
|
||||
}
|
||||
23
src/Core/Billing/Licenses/Queries/GetUserLicenseQuery.cs
Normal file
23
src/Core/Billing/Licenses/Queries/GetUserLicenseQuery.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Bit.Core.Billing.Licenses.Models.Api.Response;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses.Queries;
|
||||
|
||||
public interface IGetUserLicenseQuery
|
||||
{
|
||||
Task<LicenseResponseModel> Run(User user);
|
||||
}
|
||||
|
||||
public class GetUserLicenseQuery(
|
||||
IUserService userService,
|
||||
ILicensingService licensingService) : IGetUserLicenseQuery
|
||||
{
|
||||
public async Task<LicenseResponseModel> Run(User user)
|
||||
{
|
||||
var license = await userService.GenerateLicenseAsync(user);
|
||||
var claimsPrincipal = licensingService.GetClaimsPrincipalFromLicense(license);
|
||||
return new LicenseResponseModel(license, claimsPrincipal);
|
||||
}
|
||||
}
|
||||
13
src/Core/Billing/Licenses/Registrations.cs
Normal file
13
src/Core/Billing/Licenses/Registrations.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Bit.Core.Billing.Licenses.Queries;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.Billing.Licenses;
|
||||
|
||||
public static class Registrations
|
||||
{
|
||||
public static void AddLicenseOperations(this IServiceCollection services)
|
||||
{
|
||||
// Queries
|
||||
services.AddTransient<IGetUserLicenseQuery, GetUserLicenseQuery>();
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -124,6 +125,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(false);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -161,6 +163,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -207,6 +210,7 @@ public class AccountsControllerTests : IDisposable
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GenerateLicenseAsync(user).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetSubscriptionAsync(_globalSettings, _paymentService);
|
||||
@@ -243,6 +247,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe; // User has payment gateway
|
||||
|
||||
@@ -293,6 +298,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
@@ -349,6 +355,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act & Assert - Feature flag ENABLED
|
||||
@@ -413,6 +420,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act - Step 4: Call AccountsController.GetSubscriptionAsync
|
||||
@@ -507,6 +515,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -558,6 +567,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -611,6 +621,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -658,6 +669,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act
|
||||
@@ -726,6 +738,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true);
|
||||
_paymentService.GetSubscriptionAsync(user).Returns(subscriptionInfo);
|
||||
_userService.GenerateLicenseAsync(user, subscriptionInfo).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
|
||||
// Act - Full pipeline: Stripe → SubscriptionInfo → SubscriptionResponseModel → API response
|
||||
@@ -791,6 +804,7 @@ public class AccountsControllerTests : IDisposable
|
||||
};
|
||||
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
_userService.GenerateLicenseAsync(user).Returns(license);
|
||||
_licensingService.GetClaimsPrincipalFromLicense(license).Returns(new ClaimsPrincipal());
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM23341_Milestone_2).Returns(true); // Flag enabled
|
||||
|
||||
// Act
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using Bit.Api.Billing.Controllers.VNext;
|
||||
using Bit.Core.Billing.Licenses.Queries;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.Billing.Controllers.VNext;
|
||||
|
||||
public class AccountBillingVNextControllerTests
|
||||
{
|
||||
private readonly ICreateBitPayInvoiceForCreditCommand _createBitPayInvoiceForCreditCommand;
|
||||
private readonly ICreatePremiumCloudHostedSubscriptionCommand _createPremiumCloudHostedSubscriptionCommand;
|
||||
private readonly IGetCreditQuery _getCreditQuery;
|
||||
private readonly IGetPaymentMethodQuery _getPaymentMethodQuery;
|
||||
private readonly IGetUserLicenseQuery _getUserLicenseQuery;
|
||||
private readonly IUpdatePaymentMethodCommand _updatePaymentMethodCommand;
|
||||
private readonly AccountBillingVNextController _sut;
|
||||
|
||||
public AccountBillingVNextControllerTests()
|
||||
{
|
||||
_createBitPayInvoiceForCreditCommand = Substitute.For<ICreateBitPayInvoiceForCreditCommand>();
|
||||
_createPremiumCloudHostedSubscriptionCommand = Substitute.For<ICreatePremiumCloudHostedSubscriptionCommand>();
|
||||
_getCreditQuery = Substitute.For<IGetCreditQuery>();
|
||||
_getPaymentMethodQuery = Substitute.For<IGetPaymentMethodQuery>();
|
||||
_getUserLicenseQuery = Substitute.For<IGetUserLicenseQuery>();
|
||||
_updatePaymentMethodCommand = Substitute.For<IUpdatePaymentMethodCommand>();
|
||||
|
||||
_sut = new AccountBillingVNextController(
|
||||
_createBitPayInvoiceForCreditCommand,
|
||||
_createPremiumCloudHostedSubscriptionCommand,
|
||||
_getCreditQuery,
|
||||
_getPaymentMethodQuery,
|
||||
_getUserLicenseQuery,
|
||||
_updatePaymentMethodCommand);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task GetLicenseAsync_ValidUser_ReturnsLicenseResponse(User user,
|
||||
Core.Billing.Licenses.Models.Api.Response.LicenseResponseModel licenseResponse)
|
||||
{
|
||||
// Arrange
|
||||
_getUserLicenseQuery.Run(user).Returns(licenseResponse);
|
||||
|
||||
// Act
|
||||
var result = await _sut.GetLicenseAsync(user);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<IResult>(result);
|
||||
await _getUserLicenseQuery.Received(1).Run(user);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user