-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[PM-11516] Initial license file refactor #5002
Changes from all commits
02fd8b0
a473487
6fa8e5a
e5357d9
f650a44
d366592
8cd0406
d56de07
6b63cb7
73aa5fb
9fc82a8
9790d2c
056133e
51537ab
202f7e4
ee8763f
4547e12
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
๏ปฟusing System.Security.Claims; | ||
using Bit.Core.AdminConsole.Entities; | ||
using Bit.Core.Billing.Enums; | ||
using Bit.Core.Models.Business; | ||
|
||
namespace Bit.Core.Billing.Licenses.Extensions; | ||
|
||
public static class LicenseExtensions | ||
{ | ||
public static DateTime CalculateFreshExpirationDate(this Organization org, SubscriptionInfo subscriptionInfo) | ||
{ | ||
if (subscriptionInfo?.Subscription == null) | ||
{ | ||
if (org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue) | ||
{ | ||
return org.ExpirationDate.Value; | ||
} | ||
|
||
return DateTime.UtcNow.AddDays(7); | ||
} | ||
|
||
var subscription = subscriptionInfo.Subscription; | ||
|
||
if (subscription.TrialEndDate > DateTime.UtcNow) | ||
{ | ||
return subscription.TrialEndDate.Value; | ||
} | ||
|
||
if (org.ExpirationDate.HasValue && org.ExpirationDate.Value < DateTime.UtcNow) | ||
{ | ||
return org.ExpirationDate.Value; | ||
} | ||
|
||
if (subscription.PeriodEndDate.HasValue && subscription.PeriodDuration > TimeSpan.FromDays(180)) | ||
{ | ||
return subscription.PeriodEndDate | ||
.Value | ||
.AddDays(Bit.Core.Constants.OrganizationSelfHostSubscriptionGracePeriodDays); | ||
} | ||
|
||
return org.ExpirationDate?.AddMonths(11) ?? DateTime.UtcNow.AddYears(1); | ||
} | ||
|
||
public static DateTime CalculateFreshRefreshDate(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate) | ||
{ | ||
if (subscriptionInfo?.Subscription == null || | ||
subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow || | ||
org.ExpirationDate < DateTime.UtcNow) | ||
{ | ||
return expirationDate; | ||
} | ||
|
||
return subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180) || | ||
DateTime.UtcNow - expirationDate > TimeSpan.FromDays(30) | ||
? DateTime.UtcNow.AddDays(30) | ||
: expirationDate; | ||
} | ||
|
||
public static DateTime CalculateFreshExpirationDateWithoutGracePeriod(this Organization org, SubscriptionInfo subscriptionInfo, DateTime expirationDate) | ||
{ | ||
if (subscriptionInfo?.Subscription is null) | ||
{ | ||
return expirationDate; | ||
} | ||
|
||
var subscription = subscriptionInfo.Subscription; | ||
|
||
if (subscription.TrialEndDate <= DateTime.UtcNow && | ||
org.ExpirationDate >= DateTime.UtcNow && | ||
subscription.PeriodEndDate.HasValue && | ||
subscription.PeriodDuration > TimeSpan.FromDays(180)) | ||
{ | ||
return subscription.PeriodEndDate.Value; | ||
} | ||
|
||
return expirationDate; | ||
} | ||
|
||
public static T GetValue<T>(this ClaimsPrincipal principal, string claimType) | ||
{ | ||
var claim = principal.FindFirst(claimType); | ||
|
||
if (claim is null) | ||
{ | ||
return default; | ||
} | ||
|
||
// Handle Guid | ||
if (typeof(T) == typeof(Guid)) | ||
{ | ||
return Guid.TryParse(claim.Value, out var guid) | ||
? (T)(object)guid | ||
: default; | ||
} | ||
|
||
// Handle DateTime | ||
if (typeof(T) == typeof(DateTime)) | ||
{ | ||
return DateTime.TryParse(claim.Value, out var dateTime) | ||
? (T)(object)dateTime | ||
: default; | ||
} | ||
|
||
// Handle TimeSpan | ||
if (typeof(T) == typeof(TimeSpan)) | ||
{ | ||
return TimeSpan.TryParse(claim.Value, out var timeSpan) | ||
? (T)(object)timeSpan | ||
: default; | ||
} | ||
|
||
// Check for Nullable Types | ||
var underlyingType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); | ||
|
||
// Handle Enums | ||
if (underlyingType.IsEnum) | ||
{ | ||
if (Enum.TryParse(underlyingType, claim.Value, true, out var enumValue)) | ||
{ | ||
return (T)enumValue; // Cast back to T | ||
} | ||
|
||
return default; // Return default value for non-nullable enums or null for nullable enums | ||
} | ||
|
||
// Handle other Nullable Types (e.g., int?, bool?) | ||
if (underlyingType == typeof(int)) | ||
{ | ||
return int.TryParse(claim.Value, out var intValue) | ||
? (T)(object)intValue | ||
: default; | ||
} | ||
|
||
if (underlyingType == typeof(bool)) | ||
{ | ||
return bool.TryParse(claim.Value, out var boolValue) | ||
? (T)(object)boolValue | ||
: default; | ||
} | ||
|
||
if (underlyingType == typeof(double)) | ||
{ | ||
return double.TryParse(claim.Value, out var doubleValue) | ||
? (T)(object)doubleValue | ||
: default; | ||
} | ||
|
||
// Fallback to Convert.ChangeType for other types including strings | ||
return (T)Convert.ChangeType(claim.Value, underlyingType); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
๏ปฟusing Bit.Core.AdminConsole.Entities; | ||
using Bit.Core.Billing.Licenses.Services; | ||
using Bit.Core.Billing.Licenses.Services.Implementations; | ||
using Bit.Core.Entities; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Bit.Core.Billing.Licenses.Extensions; | ||
|
||
public static class LicenseServiceCollectionExtensions | ||
{ | ||
public static void AddLicenseServices(this IServiceCollection services) | ||
{ | ||
services.AddTransient<ILicenseClaimsFactory<Organization>, OrganizationLicenseClaimsFactory>(); | ||
services.AddTransient<ILicenseClaimsFactory<User>, UserLicenseClaimsFactory>(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
๏ปฟnamespace Bit.Core.Billing.Licenses; | ||
|
||
public static class OrganizationLicenseConstants | ||
{ | ||
public const string LicenseType = nameof(LicenseType); | ||
public const string LicenseKey = nameof(LicenseKey); | ||
public const string InstallationId = nameof(InstallationId); | ||
public const string Id = nameof(Id); | ||
public const string Name = nameof(Name); | ||
public const string BusinessName = nameof(BusinessName); | ||
public const string BillingEmail = nameof(BillingEmail); | ||
public const string Enabled = nameof(Enabled); | ||
public const string Plan = nameof(Plan); | ||
public const string PlanType = nameof(PlanType); | ||
public const string Seats = nameof(Seats); | ||
public const string MaxCollections = nameof(MaxCollections); | ||
public const string UsePolicies = nameof(UsePolicies); | ||
public const string UseSso = nameof(UseSso); | ||
public const string UseKeyConnector = nameof(UseKeyConnector); | ||
public const string UseScim = nameof(UseScim); | ||
public const string UseGroups = nameof(UseGroups); | ||
public const string UseEvents = nameof(UseEvents); | ||
public const string UseDirectory = nameof(UseDirectory); | ||
public const string UseTotp = nameof(UseTotp); | ||
public const string Use2fa = nameof(Use2fa); | ||
public const string UseApi = nameof(UseApi); | ||
public const string UseResetPassword = nameof(UseResetPassword); | ||
public const string MaxStorageGb = nameof(MaxStorageGb); | ||
public const string SelfHost = nameof(SelfHost); | ||
public const string UsersGetPremium = nameof(UsersGetPremium); | ||
public const string UseCustomPermissions = nameof(UseCustomPermissions); | ||
public const string Issued = nameof(Issued); | ||
public const string UsePasswordManager = nameof(UsePasswordManager); | ||
public const string UseSecretsManager = nameof(UseSecretsManager); | ||
public const string SmSeats = nameof(SmSeats); | ||
public const string SmServiceAccounts = nameof(SmServiceAccounts); | ||
public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion); | ||
public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems); | ||
public const string Expires = nameof(Expires); | ||
public const string Refresh = nameof(Refresh); | ||
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod); | ||
public const string Trial = nameof(Trial); | ||
} | ||
|
||
public static class UserLicenseConstants | ||
{ | ||
public const string LicenseType = nameof(LicenseType); | ||
public const string LicenseKey = nameof(LicenseKey); | ||
public const string Id = nameof(Id); | ||
public const string Name = nameof(Name); | ||
public const string Email = nameof(Email); | ||
public const string Premium = nameof(Premium); | ||
public const string MaxStorageGb = nameof(MaxStorageGb); | ||
public const string Issued = nameof(Issued); | ||
public const string Expires = nameof(Expires); | ||
public const string Refresh = nameof(Refresh); | ||
public const string Trial = nameof(Trial); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
๏ปฟ#nullable enable | ||
using Bit.Core.Models.Business; | ||
|
||
namespace Bit.Core.Billing.Licenses.Models; | ||
|
||
public class LicenseContext | ||
{ | ||
public Guid? InstallationId { get; init; } | ||
public required SubscriptionInfo SubscriptionInfo { get; init; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
๏ปฟusing System.Security.Claims; | ||
using Bit.Core.Billing.Licenses.Models; | ||
|
||
namespace Bit.Core.Billing.Licenses.Services; | ||
|
||
public interface ILicenseClaimsFactory<in T> | ||
{ | ||
Task<List<Claim>> GenerateClaims(T entity, LicenseContext licenseContext); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
๏ปฟusing System.Globalization; | ||
using System.Security.Claims; | ||
using Bit.Core.AdminConsole.Entities; | ||
using Bit.Core.Billing.Enums; | ||
using Bit.Core.Billing.Licenses.Extensions; | ||
using Bit.Core.Billing.Licenses.Models; | ||
using Bit.Core.Enums; | ||
using Bit.Core.Models.Business; | ||
|
||
namespace Bit.Core.Billing.Licenses.Services.Implementations; | ||
|
||
public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organization> | ||
{ | ||
public Task<List<Claim>> GenerateClaims(Organization entity, LicenseContext licenseContext) | ||
{ | ||
var subscriptionInfo = licenseContext.SubscriptionInfo; | ||
var expires = entity.CalculateFreshExpirationDate(subscriptionInfo); | ||
var refresh = entity.CalculateFreshRefreshDate(subscriptionInfo, expires); | ||
var expirationWithoutGracePeriod = entity.CalculateFreshExpirationDateWithoutGracePeriod(subscriptionInfo, expires); | ||
var trial = IsTrialing(entity, subscriptionInfo); | ||
|
||
var claims = new List<Claim> | ||
{ | ||
new(nameof(OrganizationLicenseConstants.LicenseType), LicenseType.Organization.ToString()), | ||
new Claim(nameof(OrganizationLicenseConstants.LicenseKey), entity.LicenseKey), | ||
new(nameof(OrganizationLicenseConstants.InstallationId), licenseContext.InstallationId.ToString()), | ||
new(nameof(OrganizationLicenseConstants.Id), entity.Id.ToString()), | ||
new(nameof(OrganizationLicenseConstants.Name), entity.Name), | ||
new(nameof(OrganizationLicenseConstants.BillingEmail), entity.BillingEmail), | ||
new(nameof(OrganizationLicenseConstants.Enabled), entity.Enabled.ToString()), | ||
new(nameof(OrganizationLicenseConstants.Plan), entity.Plan), | ||
new(nameof(OrganizationLicenseConstants.PlanType), entity.PlanType.ToString()), | ||
new(nameof(OrganizationLicenseConstants.Seats), entity.Seats.ToString()), | ||
new(nameof(OrganizationLicenseConstants.MaxCollections), entity.MaxCollections.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UsePolicies), entity.UsePolicies.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseSso), entity.UseSso.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseKeyConnector), entity.UseKeyConnector.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseScim), entity.UseScim.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseGroups), entity.UseGroups.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseEvents), entity.UseEvents.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseDirectory), entity.UseDirectory.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseTotp), entity.UseTotp.ToString()), | ||
new(nameof(OrganizationLicenseConstants.Use2fa), entity.Use2fa.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseApi), entity.UseApi.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseResetPassword), entity.UseResetPassword.ToString()), | ||
new(nameof(OrganizationLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()), | ||
new(nameof(OrganizationLicenseConstants.SelfHost), entity.SelfHost.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UsersGetPremium), entity.UsersGetPremium.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseCustomPermissions), entity.UseCustomPermissions.ToString()), | ||
new(nameof(OrganizationLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), | ||
new(nameof(OrganizationLicenseConstants.UsePasswordManager), entity.UsePasswordManager.ToString()), | ||
new(nameof(OrganizationLicenseConstants.UseSecretsManager), entity.UseSecretsManager.ToString()), | ||
new(nameof(OrganizationLicenseConstants.SmSeats), entity.SmSeats.ToString()), | ||
new(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()), | ||
new(nameof(OrganizationLicenseConstants.LimitCollectionCreationDeletion), entity.LimitCollectionCreationDeletion.ToString()), | ||
new(nameof(OrganizationLicenseConstants.AllowAdminAccessToAllCollectionItems), entity.AllowAdminAccessToAllCollectionItems.ToString()), | ||
new(nameof(OrganizationLicenseConstants.Expires), expires.ToString(CultureInfo.InvariantCulture)), | ||
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)), | ||
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)), | ||
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()), | ||
}; | ||
|
||
if (entity.BusinessName is not null) | ||
{ | ||
claims.Add(new Claim(nameof(OrganizationLicenseConstants.BusinessName), entity.BusinessName)); | ||
} | ||
|
||
return Task.FromResult(claims); | ||
} | ||
|
||
private static bool IsTrialing(Organization org, SubscriptionInfo subscriptionInfo) => | ||
subscriptionInfo?.Subscription is null | ||
? org.PlanType != PlanType.Custom || !org.ExpirationDate.HasValue | ||
: subscriptionInfo.Subscription.TrialEndDate > DateTime.UtcNow; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
๏ปฟusing System.Globalization; | ||
using System.Security.Claims; | ||
using Bit.Core.Billing.Licenses.Models; | ||
using Bit.Core.Entities; | ||
using Bit.Core.Enums; | ||
|
||
namespace Bit.Core.Billing.Licenses.Services.Implementations; | ||
|
||
public class UserLicenseClaimsFactory : ILicenseClaimsFactory<User> | ||
{ | ||
public Task<List<Claim>> GenerateClaims(User entity, LicenseContext licenseContext) | ||
{ | ||
var subscriptionInfo = licenseContext.SubscriptionInfo; | ||
|
||
var expires = subscriptionInfo.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7); | ||
var refresh = subscriptionInfo.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate; | ||
var trial = (subscriptionInfo.Subscription?.TrialEndDate.HasValue ?? false) && | ||
subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can probably leverage There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll note it for the next set of reactors in the next PR. In this one, I wanted to keep the logic as identical as possible to how it's working right now, just with the change of using a JWT |
||
|
||
var claims = new List<Claim> | ||
{ | ||
new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()), | ||
new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey), | ||
new(nameof(UserLicenseConstants.Id), entity.Id.ToString()), | ||
new(nameof(UserLicenseConstants.Name), entity.Name), | ||
new(nameof(UserLicenseConstants.Email), entity.Email), | ||
new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()), | ||
new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()), | ||
new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)), | ||
new(nameof(UserLicenseConstants.Expires), expires.ToString()), | ||
new(nameof(UserLicenseConstants.Refresh), refresh.ToString()), | ||
new(nameof(UserLicenseConstants.Trial), trial.ToString()), | ||
}; | ||
|
||
return Task.FromResult(claims); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
โ Out of curiosity, what's the purpose of this method returning a
Task
without doing anything asynchronously?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This came out of a discussion I had with platform on the approach. The idea was to have a single interface that could be used to generate claims for any object. If we needed to have some async IO in some speculative implementation to calculate the value for a claim, then this would allow for that.