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()