From 951201892e0413ee87e0d1e036708cdad3a5c578 Mon Sep 17 00:00:00 2001
From: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Date: Wed, 29 Nov 2023 23:13:46 +1000
Subject: [PATCH] [AC-1839] Add OrganizationLicense unit tests (#3474)
---
.../Models/Business/OrganizationLicense.cs | 28 ++---
.../OrganizationLicenseFileFixtures.cs | 110 ++++++++++++++++++
.../Business/OrganizationLicenseTests.cs | 68 +++++++++++
3 files changed, 193 insertions(+), 13 deletions(-)
create mode 100644 test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs
create mode 100644 test/Core.Test/Models/Business/OrganizationLicenseTests.cs
diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs
index a58f34e81..73605bd9b 100644
--- a/src/Core/Models/Business/OrganizationLicense.cs
+++ b/src/Core/Models/Business/OrganizationLicense.cs
@@ -13,12 +13,13 @@ namespace Bit.Core.Models.Business;
public class OrganizationLicense : ILicense
{
public OrganizationLicense()
- { }
+ {
+ }
public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId,
ILicensingService licenseService, int? version = null)
{
- Version = version.GetValueOrDefault(CURRENT_LICENSE_FILE_VERSION); // TODO: Remember to change the constant
+ Version = version.GetValueOrDefault(CurrentLicenseFileVersion); // TODO: Remember to change the constant
LicenseType = Enums.LicenseType.Organization;
LicenseKey = org.LicenseKey;
InstallationId = installationId;
@@ -66,7 +67,7 @@ public class OrganizationLicense : ILicense
}
}
else if (subscriptionInfo.Subscription.TrialEndDate.HasValue &&
- subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow)
+ subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow)
{
Expires = Refresh = subscriptionInfo.Subscription.TrialEndDate.Value;
Trial = true;
@@ -79,10 +80,11 @@ public class OrganizationLicense : ILicense
Expires = Refresh = org.ExpirationDate.Value;
}
else if (subscriptionInfo?.Subscription?.PeriodDuration != null &&
- subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180))
+ subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180))
{
Refresh = DateTime.UtcNow.AddDays(30);
- Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Constants.OrganizationSelfHostSubscriptionGracePeriodDays);
+ Expires = subscriptionInfo.Subscription.PeriodEndDate?.AddDays(Constants
+ .OrganizationSelfHostSubscriptionGracePeriodDays);
ExpirationWithoutGracePeriod = subscriptionInfo.Subscription.PeriodEndDate;
}
else
@@ -137,15 +139,15 @@ public class OrganizationLicense : ILicense
public LicenseType? LicenseType { get; set; }
public string Hash { get; set; }
public string Signature { get; set; }
- [JsonIgnore]
- public byte[] SignatureBytes => Convert.FromBase64String(Signature);
+ [JsonIgnore] public byte[] SignatureBytes => Convert.FromBase64String(Signature);
///
/// Represents the current version of the license format. Should be updated whenever new fields are added.
///
/// 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 = 12;
+ public const int CurrentLicenseFileVersion = 12;
+
private bool ValidLicenseVersion
{
get => Version is >= 1 and <= 13;
@@ -235,14 +237,14 @@ public class OrganizationLicense : ILicense
if (InstallationId != globalSettings.Installation.Id || !SelfHost)
{
exception = "Invalid license. Make sure your license allows for on-premise " +
- "hosting of organizations and that the installation id matches your current installation.";
+ "hosting of organizations and that the installation id matches your current installation.";
return false;
}
if (LicenseType != null && LicenseType != Enums.LicenseType.Organization)
{
exception = "Premium licenses cannot be applied to an organization. "
- + "Upload this license from your personal account settings page.";
+ + "Upload this license from your personal account settings page.";
return false;
}
@@ -331,9 +333,9 @@ public class OrganizationLicense : ILicense
if (valid && Version >= 13)
{
valid = organization.UseSecretsManager == UseSecretsManager &&
- organization.UsePasswordManager == UsePasswordManager &&
- organization.SmSeats == SmSeats &&
- organization.SmServiceAccounts == SmServiceAccounts;
+ organization.UsePasswordManager == UsePasswordManager &&
+ organization.SmSeats == SmSeats &&
+ organization.SmServiceAccounts == SmServiceAccounts;
}
return valid;
diff --git a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs
new file mode 100644
index 000000000..16ad92774
--- /dev/null
+++ b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs
@@ -0,0 +1,110 @@
+using System.Text.Json;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Models.Business;
+
+namespace Bit.Core.Test.Models.Business;
+
+///
+/// Contains test data for OrganizationLicense tests, including json strings for each OrganizationLicense version.
+/// If you increment the OrganizationLicense version (e.g. because you've added a property to it), you must add the
+/// json string for your new version to the LicenseVersions dictionary in this class.
+/// See OrganizationLicenseTests.GenerateLicenseFileJsonString to help you do this.
+///
+public static class OrganizationLicenseFileFixtures
+{
+ public const string InstallationId = "78900000-0000-0000-0000-000000000123";
+
+ private const string Version12 =
+ "{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 11,\n 'Issued': '2023-11-23T03:15:41.632267Z',\n 'Refresh': '2023-11-30T03:15:41.632267Z',\n 'Expires': '2023-11-30T03:15:41.632267Z',\n 'ExpirationWithoutGracePeriod': null,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': 'eMSljdMAlFiiVYP/DI8LwNtSZZy6cJaC\\u002BAdmYGd1RTs=',\n 'Signature': ''\n}";
+
+ private const string Version13 =
+ "{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 12,\n 'Issued': '2023-11-23T03:25:24.265409Z',\n 'Refresh': '2023-11-30T03:25:24.265409Z',\n 'Expires': '2023-11-30T03:25:24.265409Z',\n 'ExpirationWithoutGracePeriod': null,\n 'UsePasswordManager': true,\n 'UseSecretsManager': true,\n 'SmSeats': 5,\n 'SmServiceAccounts': 8,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': 'hZ4WcSX/7ooRZ6asDRMJ/t0K5hZkQdvkgEyy6wY\\u002BwQk=',\n 'Signature': ''\n}";
+
+ private static readonly Dictionary LicenseVersions = new() { { 12, Version12 }, { 13, Version13 } };
+
+ public static OrganizationLicense GetVersion(int licenseVersion)
+ {
+ if (!LicenseVersions.ContainsKey(licenseVersion))
+ {
+ throw new Exception(
+ $"Cannot find serialized license version {licenseVersion}. You must add this to OrganizationLicenseFileFixtures when adding a new license version.");
+ }
+
+ var json = LicenseVersions.GetValueOrDefault(licenseVersion).Replace("'", "\"");
+ var license = JsonSerializer.Deserialize(json);
+
+ if (license.Version != licenseVersion - 1)
+ {
+ // license.Version is 1 behind. e.g. if we requested version 13, then license.Version == 12. If not,
+ // the json string is probably for a different version and won't give us accurate test results.
+ throw new Exception(
+ $"License version {licenseVersion} in OrganizationLicenseFileFixtures did not match the expected version number. Make sure the json string is correct.");
+ }
+
+ return license;
+ }
+
+ ///
+ /// The organization used to generate the license file json strings in this class.
+ /// All its properties should be initialized with literal, non-default values.
+ /// If you add an Organization property value, please add a value here as well.
+ ///
+ public static Organization OrganizationFactory() =>
+ new()
+ {
+ Id = new Guid("12300000-0000-0000-0000-000000000456"),
+ Identifier = "myIdentifier",
+ Name = "myOrg",
+ BusinessName = "myBusinessName",
+ BusinessAddress1 = "myBusinessAddress1",
+ BusinessAddress2 = "myBusinessAddress2",
+ BusinessAddress3 = "myBusinessAddress3",
+ BusinessCountry = "myBusinessCountry",
+ BusinessTaxNumber = "myBusinessTaxNumber",
+ BillingEmail = "myBillingEmail",
+ Plan = "myPlan",
+ PlanType = PlanType.EnterpriseAnnually2020,
+ Seats = 10,
+ MaxCollections = 2,
+ UsePolicies = true,
+ UseSso = true,
+ UseKeyConnector = true,
+ UseScim = true,
+ UseGroups = true,
+ UseDirectory = true,
+ UseEvents = true,
+ UseTotp = true,
+ Use2fa = true,
+ UseApi = true,
+ UseResetPassword = true,
+ UseSecretsManager = true,
+ SelfHost = true,
+ UsersGetPremium = true,
+ UseCustomPermissions = true,
+ Storage = 100000,
+ MaxStorageGb = 100,
+ Gateway = GatewayType.Stripe,
+ GatewayCustomerId = "myGatewayCustomerId",
+ GatewaySubscriptionId = "myGatewaySubscriptionId",
+ ReferenceData = "myReferenceData",
+ Enabled = true,
+ LicenseKey = "myLicenseKey",
+ PublicKey = "myPublicKey",
+ PrivateKey = "myPrivateKey",
+ TwoFactorProviders = "myTwoFactorProviders",
+ ExpirationDate = new DateTime(2024, 12, 24),
+ CreationDate = new DateTime(2022, 10, 22),
+ RevisionDate = new DateTime(2023, 11, 23),
+ MaxAutoscaleSeats = 100,
+ OwnersNotifiedOfAutoscaling = new DateTime(2020, 5, 10),
+ Status = OrganizationStatusType.Created,
+ UsePasswordManager = true,
+ SmSeats = 5,
+ SmServiceAccounts = 8,
+ MaxAutoscaleSmSeats = 101,
+ MaxAutoscaleSmServiceAccounts = 102,
+ SecretsManagerBeta = true,
+ LimitCollectionCreationDeletion = true
+ };
+}
diff --git a/test/Core.Test/Models/Business/OrganizationLicenseTests.cs b/test/Core.Test/Models/Business/OrganizationLicenseTests.cs
new file mode 100644
index 000000000..d5067a2e5
--- /dev/null
+++ b/test/Core.Test/Models/Business/OrganizationLicenseTests.cs
@@ -0,0 +1,68 @@
+using System.Text.Json;
+using Bit.Core.Models.Business;
+using Bit.Core.Services;
+using Bit.Core.Settings;
+using Bit.Core.Utilities;
+using Bit.Test.Common.AutoFixture.Attributes;
+using NSubstitute;
+using Xunit;
+
+namespace Bit.Core.Test.Models.Business;
+
+public class OrganizationLicenseTests
+{
+ ///
+ /// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,
+ /// its hash does not change.
+ /// This guards against the risk that properties added in later versions are accidentally included in the hash,
+ /// or that a property is added without incrementing the version number.
+ ///
+ [Theory]
+ [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind)
+ [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version
+ public void OrganizationLicense_LoadFromDisk_HashDoesNotChange(int licenseVersion)
+ {
+ var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion);
+
+ // Compare the hash loaded from the json to the hash generated by the current class
+ Assert.Equal(Convert.FromBase64String(license.Hash), license.ComputeHash());
+ }
+
+ ///
+ /// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,
+ /// it matches the Organization it was generated for.
+ /// This guards against the risk that properties added in later versions are accidentally included in the validation
+ ///
+ [Theory]
+ [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind)
+ [BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version
+ public void OrganizationLicense_LoadedFromDisk_VerifyData_Passes(int licenseVersion)
+ {
+ var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion);
+ var organization = OrganizationLicenseFileFixtures.OrganizationFactory();
+ var globalSettings = Substitute.For();
+ globalSettings.Installation.Returns(new GlobalSettings.InstallationSettings
+ {
+ Id = new Guid(OrganizationLicenseFileFixtures.InstallationId)
+ });
+ Assert.True(license.VerifyData(organization, globalSettings));
+ }
+
+ ///
+ /// Helper used to generate a new json string to be added in OrganizationLicenseFileFixtures.
+ /// Uncomment [Fact], run the test and copy the value of the `result` variable into OrganizationLicenseFileFixtures,
+ /// following the instructions in that class.
+ ///
+ // [Fact]
+ private void GenerateLicenseFileJsonString()
+ {
+ var organization = OrganizationLicenseFileFixtures.OrganizationFactory();
+ var licensingService = Substitute.For();
+ var installationId = new Guid(OrganizationLicenseFileFixtures.InstallationId);
+
+ var license = new OrganizationLicense(organization, null, installationId, licensingService);
+
+ var result = JsonSerializer.Serialize(license, JsonHelpers.Indented).Replace("\"", "'");
+ // Put a break after this line, then copy and paste the value of `result` into OrganizationLicenseFileFixtures
+ }
+}