-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[PM-11516] Initial license file refactor (#5002)
* Added the ability to create a JWT on an organization license that contains all license properties as claims * Added the ability to create a JWT on a user license that contains all license properties as claims * Added ability to consume JWT licenses * Resolved generic type issues when getting claim value * Now validating the jwt signature, exp, and iat * Moved creation of ClaimsPrincipal outside of licenses given dependecy on cert * Ran dotnet format. Resolved identity error * Updated claim types to use string constants * Updated jwt expires to be one year * Fixed bug requiring email verification to be on the token * dotnet format * Patch build process --------- Co-authored-by: Matt Bishop <[email protected]>
- Loading branch information
1 parent
0e32dcc
commit 04cf513
Showing
23 changed files
with
847 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
151 changes: 151 additions & 0 deletions
151
src/Core/Billing/Licenses/Extensions/LicenseExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/Core/Billing/Licenses/Extensions/LicenseServiceCollectionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
75 changes: 75 additions & 0 deletions
75
src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
37 changes: 37 additions & 0 deletions
37
src/Core/Billing/Licenses/Services/Implementations/UserLicenseClaimsFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.