mirror of
https://github.com/bitwarden/server
synced 2026-01-14 06:23:46 +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>();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user