From bfd3f85bb0ae1ad58414375cc6cc2b4dafc5ad4b Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 15 May 2023 07:38:41 -0700 Subject: [PATCH] [AC-358] Server changes for self host subscription page changes (#2826) * [AC-358] Add constant for grace period length * [AC-358] Add SubscriptionExpiration to OrganizationLicense.cs and increment Current_License_File_Version * [AC-358] Update org subscription response model - Add new SelfHostSubscriptionExpiration field that does not include a grace period - Add optional License argument to constructor for self host responses - Use the License, if available, to populate the expiration/subscription expiration fields - Maintain backwards compatability by falling back to organization expiration date * [AC-358] Read organization license file for self hosted subscription response * [AC-358] Decrement current license file version and add comment documenting why * [AC-358] Clarify name for new expiration without grace period field --- .../Controllers/OrganizationsController.cs | 13 +++++++--- .../OrganizationResponseModel.cs | 24 +++++++++++++++++++ src/Core/Constants.cs | 6 +++++ .../Models/Business/OrganizationLicense.cs | 12 +++++++--- .../OrganizationsControllerTests.cs | 4 +++- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 36fdd2a99..ace46d438 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -50,6 +50,7 @@ public class OrganizationsController : Controller private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery; private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; + private readonly ILicensingService _licensingService; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -69,7 +70,8 @@ public class OrganizationsController : Controller IUpdateOrganizationLicenseCommand updateOrganizationLicenseCommand, ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, IFeatureService featureService, - GlobalSettings globalSettings) + GlobalSettings globalSettings, + ILicensingService licensingService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -89,6 +91,7 @@ public class OrganizationsController : Controller _cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery; _featureService = featureService; _globalSettings = globalSettings; + _licensingService = licensingService; } [HttpGet("{id}")] @@ -156,10 +159,14 @@ public class OrganizationsController : Controller return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo, hideSensitiveData); } - else + + if (_globalSettings.SelfHosted) { - return new OrganizationSubscriptionResponseModel(organization); + var orgLicense = await _licensingService.ReadOrganizationLicenseAsync(organization); + return new OrganizationSubscriptionResponseModel(organization, orgLicense); } + + return new OrganizationSubscriptionResponseModel(organization); } [HttpGet("{id}/license")] diff --git a/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs index d2fdc1e0b..8b9d94d88 100644 --- a/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs @@ -3,6 +3,7 @@ using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Business; using Bit.Core.Utilities; +using Constants = Bit.Core.Constants; namespace Bit.Api.Models.Response.Organizations; @@ -108,9 +109,32 @@ public class OrganizationSubscriptionResponseModel : OrganizationResponseModel } } + public OrganizationSubscriptionResponseModel(Organization organization, OrganizationLicense license) : + this(organization) + { + if (license != null) + { + // License expiration should always include grace period - See OrganizationLicense.cs + Expiration = license.Expires; + // Use license.ExpirationWithoutGracePeriod if available, otherwise assume license expiration minus grace period + ExpirationWithoutGracePeriod = license.ExpirationWithoutGracePeriod ?? + license.Expires?.AddDays(-Constants + .OrganizationSelfHostSubscriptionGracePeriodDays); + } + } + public string StorageName { get; set; } public double? StorageGb { get; set; } public BillingSubscription Subscription { get; set; } public BillingSubscriptionUpcomingInvoice UpcomingInvoice { get; set; } + + /// + /// Date when a self-hosted organization's subscription expires, without any grace period. + /// + public DateTime? ExpirationWithoutGracePeriod { get; set; } + + /// + /// Date when a self-hosted organization expires (includes grace period). + /// public DateTime? Expiration { get; set; } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8ae79084d..8ff378d00 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -13,6 +13,12 @@ public static class Constants public const long FileSize501mb = 501L * 1024L * 1024L; public const string DatabaseFieldProtectorPurpose = "DatabaseFieldProtection"; public const string DatabaseFieldProtectedPrefix = "P|"; + + /// + /// Default number of days an organization has to apply an updated license to their self-hosted installation after + /// their subscription has expired. + /// + public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60; } public static class TokenPurposes diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 58cce4cff..c72430d94 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -78,7 +78,8 @@ public class OrganizationLicense : ILicense subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) { Refresh = DateTime.UtcNow.AddDays(30); - Expires = subscriptionInfo?.Subscription.PeriodEndDate.Value.AddDays(60); + Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Constants.OrganizationSelfHostSubscriptionGracePeriodDays); + ExpirationWithoutGracePeriod = subscriptionInfo.Subscription.PeriodEndDate; } else { @@ -123,6 +124,7 @@ public class OrganizationLicense : ILicense public DateTime Issued { get; set; } public DateTime? Refresh { get; set; } public DateTime? Expires { get; set; } + public DateTime? ExpirationWithoutGracePeriod { get; set; } public bool Trial { get; set; } public LicenseType? LicenseType { get; set; } public string Hash { get; set; } @@ -133,10 +135,12 @@ public class OrganizationLicense : ILicense /// /// Represents the current version of the license format. Should be updated whenever new fields are added. /// - private const int CURRENT_LICENSE_FILE_VERSION = 10; + /// Intentionally set one version behind to allow self hosted users some time to update before + /// getting out of date license errors + private const int CURRENT_LICENSE_FILE_VERSION = 11; private bool ValidLicenseVersion { - get => Version is >= 1 and <= 11; + get => Version is >= 1 and <= 12; } public byte[] GetDataBytes(bool forHash = false) @@ -170,6 +174,8 @@ public class OrganizationLicense : ILicense (Version >= 10 || !p.Name.Equals(nameof(UseScim))) && // UseCustomPermissions was added in Version 11 (Version >= 11 || !p.Name.Equals(nameof(UseCustomPermissions))) && + // ExpirationWithoutGracePeriod was added in Version 12 + (Version >= 12 || !p.Name.Equals(nameof(ExpirationWithoutGracePeriod))) && ( !forHash || ( diff --git a/test/Api.Test/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Controllers/OrganizationsControllerTests.cs index aeadbb104..069b4bb27 100644 --- a/test/Api.Test/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationsControllerTests.cs @@ -40,6 +40,7 @@ public class OrganizationsControllerTests : IDisposable private readonly IUpdateOrganizationLicenseCommand _updateOrganizationLicenseCommand; private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IFeatureService _featureService; + private readonly ILicensingService _licensingService; private readonly OrganizationsController _sut; @@ -63,12 +64,13 @@ public class OrganizationsControllerTests : IDisposable _createOrganizationApiKeyCommand = Substitute.For(); _updateOrganizationLicenseCommand = Substitute.For(); _featureService = Substitute.For(); + _licensingService = Substitute.For(); _sut = new OrganizationsController(_organizationRepository, _organizationUserRepository, _policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext, _ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand, _createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand, - _cloudGetOrganizationLicenseQuery, _featureService, _globalSettings); + _cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService); } public void Dispose()