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 + } +}