From 2acbb9b0c1fad7da1bc9532b5bb4f10496870e48 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 16 Dec 2025 14:46:43 +1000 Subject: [PATCH 1/9] Initial fix --- .../Controllers/ProviderClientsController.cs | 8 +- .../OrganizationKeysRequestModel.cs | 29 +-- .../OrganizationUpdateRequestModel.cs | 8 +- .../OrganizationUpgradeRequestModel.cs | 10 +- .../CloudOrganizationSignUpCommand.cs | 4 +- .../Organizations/OrganizationKeyPair.cs | 11 ++ ...ProviderClientOrganizationSignUpCommand.cs | 4 +- .../Update/OrganizationUpdateExtensions.cs | 27 ++- .../Update/OrganizationUpdateRequest.cs | 9 +- .../Models/Business/OrganizationUpgrade.cs | 4 +- .../UpgradeOrganizationPlanCommand.cs | 4 +- .../OrganizationUpdateCommandTests.cs | 22 ++- .../UpgradeOrganizationPlanCommandTests.cs | 186 +++++++++++++++++- 13 files changed, 269 insertions(+), 57 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs index caf2651e160f..a560bc59b79f 100644 --- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -3,6 +3,7 @@ using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Providers.Services; @@ -57,8 +58,11 @@ public async Task CreateAsync( Owner = user, BillingEmail = provider.BillingEmail, OwnerKey = requestBody.Key, - PublicKey = requestBody.KeyPair.PublicKey, - PrivateKey = requestBody.KeyPair.EncryptedPrivateKey, + Keys = new OrganizationKeyPair + { + PublicKey = requestBody.KeyPair.PublicKey, + PrivateKey = requestBody.KeyPair.EncryptedPrivateKey + }, CollectionName = requestBody.CollectionName, IsFromProvider = true }; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs index 22b225a689b8..5a6d52059ae2 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Models.Business; namespace Bit.Api.AdminConsole.Models.Request.Organizations; @@ -16,34 +17,18 @@ public class OrganizationKeysRequestModel public OrganizationSignup ToOrganizationSignup(OrganizationSignup existingSignup) { - if (string.IsNullOrWhiteSpace(existingSignup.PublicKey)) + if (existingSignup.Keys == null || (string.IsNullOrWhiteSpace(existingSignup.Keys.PublicKey) && string.IsNullOrWhiteSpace(existingSignup.Keys.PrivateKey))) { - existingSignup.PublicKey = PublicKey; - } - - if (string.IsNullOrWhiteSpace(existingSignup.PrivateKey)) - { - existingSignup.PrivateKey = EncryptedPrivateKey; + existingSignup.Keys = new OrganizationKeyPair + { + PublicKey = PublicKey, + PrivateKey = EncryptedPrivateKey + }; } return existingSignup; } - public OrganizationUpgrade ToOrganizationUpgrade(OrganizationUpgrade existingUpgrade) - { - if (string.IsNullOrWhiteSpace(existingUpgrade.PublicKey)) - { - existingUpgrade.PublicKey = PublicKey; - } - - if (string.IsNullOrWhiteSpace(existingUpgrade.PrivateKey)) - { - existingUpgrade.PrivateKey = EncryptedPrivateKey; - } - - return existingUpgrade; - } - public Organization ToOrganization(Organization existingOrg) { if (string.IsNullOrWhiteSpace(existingOrg.PublicKey)) diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs index 6c3867fe0990..ae37af654eef 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.Utilities; @@ -22,7 +23,10 @@ public class OrganizationUpdateRequestModel OrganizationId = organizationId, Name = Name, BillingEmail = BillingEmail, - PublicKey = Keys?.PublicKey, - EncryptedPrivateKey = Keys?.EncryptedPrivateKey + Keys = Keys != null ? new OrganizationKeyPair + { + PublicKey = Keys.PublicKey, + PrivateKey = Keys.EncryptedPrivateKey + } : null }; } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs index a5dec192b978..91e079e61723 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; @@ -43,11 +44,14 @@ public OrganizationUpgrade ToOrganizationUpgrade() { BillingAddressCountry = BillingAddressCountry, BillingAddressPostalCode = BillingAddressPostalCode - } + }, + Keys = Keys != null ? new OrganizationKeyPair + { + PublicKey = Keys.PublicKey, + PrivateKey = Keys.EncryptedPrivateKey + } : null }; - Keys?.ToOrganizationUpgrade(orgUpgrade); - return orgUpgrade; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 7f24c4acd728..f6bf4fe8296f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -99,8 +99,8 @@ public async Task SignUpOrganizationAsync(Organizati ReferenceData = signup.Owner.ReferenceData, Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, + PublicKey = signup.Keys?.PublicKey, + PrivateKey = signup.Keys?.PrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs new file mode 100644 index 000000000000..8222f3fc29a3 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +/// +/// Data transfer object for organization public/private key pairs. +/// Used to normalize key handling across different request models and commands. +/// +public record OrganizationKeyPair +{ + public string? PublicKey { get; init; } + public string? PrivateKey { get; init; } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs index 4a8f08a4f7c6..324fc7f11af4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -93,8 +93,8 @@ public async Task SignUpClientOrganiza ReferenceData = signup.Owner.ReferenceData, Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, + PublicKey = signup.Keys?.PublicKey, + PrivateKey = signup.Keys?.PrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs index e90c39bc54cf..6637e825fd5c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs @@ -26,18 +26,33 @@ public static void UpdateDetails(this Organization organization, OrganizationUpd /// /// Updates the organization public and private keys if provided and not already set. /// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft - /// migration that will silently migrate organizations when they change their details. + /// migration that will silently migrate organizations when they change their details or upgrade their plan. /// - public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationUpdateRequest request) + public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationKeyPair? keyPair) { - if (!string.IsNullOrWhiteSpace(request.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey)) + if (keyPair == null) { - organization.PublicKey = request.PublicKey; + return; } - if (!string.IsNullOrWhiteSpace(request.EncryptedPrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey)) + if (!string.IsNullOrWhiteSpace(keyPair.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey)) { - organization.PrivateKey = request.EncryptedPrivateKey; + organization.PublicKey = keyPair.PublicKey; } + + if (!string.IsNullOrWhiteSpace(keyPair.PrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey)) + { + organization.PrivateKey = keyPair.PrivateKey; + } + } + + /// + /// Updates the organization public and private keys if provided and not already set. + /// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft + /// migration that will silently migrate organizations when they change their details. + /// + public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationUpdateRequest request) + { + organization.BackfillPublicPrivateKeys(request.Keys); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs index 21d49486781c..622420cc6fcf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs @@ -22,12 +22,7 @@ public record OrganizationUpdateRequest public string? BillingEmail { get; init; } /// - /// The organization's public key to set (optional, only set if not already present on the organization). + /// The organization's public/private key pair to set (optional, only set if not already present on the organization). /// - public string? PublicKey { get; init; } - - /// - /// The organization's encrypted private key to set (optional, only set if not already present on the organization). - /// - public string? EncryptedPrivateKey { get; init; } + public OrganizationKeyPair? Keys { get; init; } } diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs index 89b9a5e6f246..c342656d87a8 100644 --- a/src/Core/Models/Business/OrganizationUpgrade.cs +++ b/src/Core/Models/Business/OrganizationUpgrade.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; namespace Bit.Core.Models.Business; @@ -13,8 +14,7 @@ public class OrganizationUpgrade public short AdditionalStorageGb { get; set; } public bool PremiumAccessAddon { get; set; } public TaxInfo TaxInfo { get; set; } - public string PublicKey { get; set; } - public string PrivateKey { get; set; } + public OrganizationKeyPair Keys { get; set; } public int? AdditionalSmSeats { get; set; } public int? AdditionalServiceAccounts { get; set; } public bool UseSecretsManager { get; set; } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 092ee0f46e26..5161edbde717 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; @@ -272,8 +273,7 @@ await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, organization.UseCustomPermissions = newPlan.HasCustomPermissions; organization.Plan = newPlan.Name; organization.Enabled = success; - organization.PublicKey = upgrade.PublicKey; - organization.PrivateKey = upgrade.PrivateKey; + organization.BackfillPublicPrivateKeys(upgrade.Keys); organization.UsePasswordManager = true; organization.UseSecretsManager = upgrade.UseSecretsManager; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs index 3a60a6ffd2ca..5810e9057485 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Enums; @@ -162,8 +163,11 @@ public async Task UpdateAsync_WhenKeysProvided_AndNotAlreadySet_SetsKeys( OrganizationId = organizationId, Name = organization.Name, BillingEmail = organization.BillingEmail, - PublicKey = publicKey, - EncryptedPrivateKey = encryptedPrivateKey + Keys = new OrganizationKeyPair + { + PublicKey = publicKey, + PrivateKey = encryptedPrivateKey + } }; // Act @@ -207,8 +211,11 @@ public async Task UpdateAsync_WhenKeysProvided_AndAlreadySet_DoesNotOverwriteKey OrganizationId = organizationId, Name = organization.Name, BillingEmail = organization.BillingEmail, - PublicKey = newPublicKey, - EncryptedPrivateKey = newEncryptedPrivateKey + Keys = new OrganizationKeyPair + { + PublicKey = newPublicKey, + PrivateKey = newEncryptedPrivateKey + } }; // Act @@ -394,8 +401,11 @@ public async Task UpdateAsync_SelfHosted_OnlyUpdatesKeysNotOrganizationDetails( OrganizationId = organizationId, Name = newName, // Should be ignored BillingEmail = newBillingEmail, // Should be ignored - PublicKey = publicKey, - EncryptedPrivateKey = encryptedPrivateKey + Keys = new OrganizationKeyPair + { + PublicKey = publicKey, + PrivateKey = encryptedPrivateKey + } }; // Act diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 8a00604bb01e..0a1079d9d002 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Billing.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Exceptions; @@ -242,4 +243,187 @@ public async Task UpgradePlan_SM_NotEnoughServiceAccounts_Throws(PlanType planTy await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default); } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_OrgHasNoKeysAndUpgradeHasKeys_SetsKeys( + Organization organization, + OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + // Arrange + const string newPublicKey = "new-public-key"; + const string newPrivateKey = "new-private-key"; + + organization.GatewayCustomerId = "customer-id"; + organization.GatewaySubscriptionId = "subscription-id"; + organization.PublicKey = null; + organization.PrivateKey = null; + + upgrade.Plan = PlanType.TeamsAnnually; + upgrade.Keys = new OrganizationKeyPair + { + PublicKey = newPublicKey, + PrivateKey = newPrivateKey + }; + upgrade.AdditionalSeats = 10; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(MockPlans.Get(organization.PlanType)); + sutProvider.GetDependency() + .GetPlanOrThrow(upgrade.Plan) + .Returns(MockPlans.Get(upgrade.Plan)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); + + // Act + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + + // Assert + Assert.Equal(newPublicKey, organization.PublicKey); + Assert.Equal(newPrivateKey, organization.PrivateKey); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(organization); + } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_OrgHasKeysAndUpgradeHasNullKeys_PreservesExistingKeys( + Organization organization, + OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + // Arrange + const string existingPublicKey = "existing-public-key"; + const string existingPrivateKey = "existing-private-key"; + + organization.GatewayCustomerId = "customer-id"; + organization.GatewaySubscriptionId = "subscription-id"; + organization.PublicKey = existingPublicKey; + organization.PrivateKey = existingPrivateKey; + + upgrade.Plan = PlanType.TeamsAnnually; + upgrade.Keys = null; + upgrade.AdditionalSeats = 10; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(MockPlans.Get(organization.PlanType)); + sutProvider.GetDependency() + .GetPlanOrThrow(upgrade.Plan) + .Returns(MockPlans.Get(upgrade.Plan)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); + + // Act + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + + // Assert + Assert.Equal(existingPublicKey, organization.PublicKey); + Assert.Equal(existingPrivateKey, organization.PrivateKey); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(organization); + } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_OrgHasKeysAndUpgradeHasKeys_PreservesExistingKeys( + Organization organization, + OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + // Arrange + const string existingPublicKey = "existing-public-key"; + const string existingPrivateKey = "existing-private-key"; + const string newPublicKey = "new-public-key"; + const string newPrivateKey = "new-private-key"; + + organization.GatewayCustomerId = "customer-id"; + organization.GatewaySubscriptionId = "subscription-id"; + organization.PublicKey = existingPublicKey; + organization.PrivateKey = existingPrivateKey; + + upgrade.Plan = PlanType.TeamsAnnually; + upgrade.Keys = new OrganizationKeyPair + { + PublicKey = newPublicKey, + PrivateKey = newPrivateKey + }; + upgrade.AdditionalSeats = 10; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(MockPlans.Get(organization.PlanType)); + sutProvider.GetDependency() + .GetPlanOrThrow(upgrade.Plan) + .Returns(MockPlans.Get(upgrade.Plan)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); + + // Act + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + + // Assert + Assert.Equal(existingPublicKey, organization.PublicKey); + Assert.Equal(existingPrivateKey, organization.PrivateKey); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(organization); + } + + [Theory] + [FreeOrganizationUpgradeCustomize, BitAutoData] + public async Task UpgradePlan_OrgHasNoKeysAndUpgradeHasNoKeys_LeavesKeysNull( + Organization organization, + OrganizationUpgrade upgrade, + SutProvider sutProvider) + { + // Arrange + organization.GatewayCustomerId = "customer-id"; + organization.GatewaySubscriptionId = "subscription-id"; + organization.PublicKey = null; + organization.PrivateKey = null; + + upgrade.Plan = PlanType.TeamsAnnually; + upgrade.Keys = null; + upgrade.AdditionalSeats = 10; + + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .GetPlanOrThrow(organization.PlanType) + .Returns(MockPlans.Get(organization.PlanType)); + sutProvider.GetDependency() + .GetPlanOrThrow(upgrade.Plan) + .Returns(MockPlans.Get(upgrade.Plan)); + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) + .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); + + // Act + await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); + + // Assert + Assert.Null(organization.PublicKey); + Assert.Null(organization.PrivateKey); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAndUpdateCacheAsync(organization); + } } From 8f46e7266a4f13d60ee2fd660fcc6bb6e1e29372 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 16 Dec 2025 14:59:35 +1000 Subject: [PATCH 2/9] remove duplicate assignments --- .../UpgradeOrganizationPlanCommand.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 5161edbde717..5c1c6007d4fd 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -257,18 +257,11 @@ await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, organization.SelfHost = newPlan.HasSelfHost; organization.UsePolicies = newPlan.HasPolicies; organization.MaxStorageGb = (short)(newPlan.PasswordManager.BaseStorageGb + upgrade.AdditionalStorageGb); - organization.UseGroups = newPlan.HasGroups; - organization.UseDirectory = newPlan.HasDirectory; - organization.UseEvents = newPlan.HasEvents; - organization.UseTotp = newPlan.HasTotp; - organization.Use2fa = newPlan.Has2fa; - organization.UseApi = newPlan.HasApi; organization.UseSso = newPlan.HasSso; organization.UseOrganizationDomains = newPlan.HasOrganizationDomains; organization.UseKeyConnector = newPlan.HasKeyConnector ? organization.UseKeyConnector : false; organization.UseScim = newPlan.HasScim; organization.UseResetPassword = newPlan.HasResetPassword; - organization.SelfHost = newPlan.HasSelfHost; organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon; organization.UseCustomPermissions = newPlan.HasCustomPermissions; organization.Plan = newPlan.Name; From 3c16b3eb3cb0854efd18bc0225a4708c1fc82db6 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 16 Dec 2025 15:56:14 +1000 Subject: [PATCH 3/9] Misc tweaks - make both keys required in new DTO model - move extensions class to higher level for reuse - fix mistakes --- .../Organizations/OrganizationExtensions.cs | 29 ++++++++++ .../Organizations/OrganizationKeyPair.cs | 4 +- .../Update/OrganizationUpdateCommand.cs | 18 +++++- .../Update/OrganizationUpdateExtensions.cs | 58 ------------------- .../UpgradeOrganizationPlanCommand.cs | 4 +- .../ProviderClientsControllerTests.cs | 4 +- 6 files changed, 51 insertions(+), 66 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs new file mode 100644 index 000000000000..f49e04762155 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs @@ -0,0 +1,29 @@ +using Bit.Core.AdminConsole.Entities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public static class OrganizationExtensions +{ + /// + /// Updates the organization public and private keys if provided and not already set. + /// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft + /// migration that will silently migrate organizations when they change their details or upgrade their plan. + /// + public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationKeyPair? keyPair) + { + if (keyPair == null) + { + return; + } + + if (!string.IsNullOrWhiteSpace(keyPair.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey)) + { + organization.PublicKey = keyPair.PublicKey; + } + + if (!string.IsNullOrWhiteSpace(keyPair.PrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey)) + { + organization.PrivateKey = keyPair.PrivateKey; + } + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs index 8222f3fc29a3..e44c053a35c2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs @@ -6,6 +6,6 @@ /// public record OrganizationKeyPair { - public string? PublicKey { get; init; } - public string? PrivateKey { get; init; } + public required string PublicKey { get; init; } + public required string PrivateKey { get; init; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs index 64358f3048e3..8285f27a65ce 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs @@ -39,8 +39,20 @@ private async Task UpdateCloudAsync(Organization organization, Org var originalBillingEmail = organization.BillingEmail; // Apply updates to organization - organization.UpdateDetails(request); - organization.BackfillPublicPrivateKeys(request); + // These values may or may not be sent by the client depending on the operation being performed. + // Skip any values not provided. + if (request.Name is not null) + { + organization.Name = request.Name; + } + + if (request.BillingEmail is not null) + { + organization.BillingEmail = request.BillingEmail.ToLowerInvariant().Trim(); + } + + organization.BackfillPublicPrivateKeys(request.Keys); + await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated); // Update billing information in Stripe if required @@ -56,7 +68,7 @@ private async Task UpdateCloudAsync(Organization organization, Org /// private async Task UpdateSelfHostedAsync(Organization organization, OrganizationUpdateRequest request) { - organization.BackfillPublicPrivateKeys(request); + organization.BackfillPublicPrivateKeys(request.Keys); await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated); return organization; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs deleted file mode 100644 index 6637e825fd5c..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Bit.Core.AdminConsole.Entities; - -namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; - -public static class OrganizationUpdateExtensions -{ - /// - /// Updates the organization name and/or billing email. - /// Any null property on the request object will be skipped. - /// - public static void UpdateDetails(this Organization organization, OrganizationUpdateRequest request) - { - // These values may or may not be sent by the client depending on the operation being performed. - // Skip any values not provided. - if (request.Name is not null) - { - organization.Name = request.Name; - } - - if (request.BillingEmail is not null) - { - organization.BillingEmail = request.BillingEmail.ToLowerInvariant().Trim(); - } - } - - /// - /// Updates the organization public and private keys if provided and not already set. - /// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft - /// migration that will silently migrate organizations when they change their details or upgrade their plan. - /// - public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationKeyPair? keyPair) - { - if (keyPair == null) - { - return; - } - - if (!string.IsNullOrWhiteSpace(keyPair.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey)) - { - organization.PublicKey = keyPair.PublicKey; - } - - if (!string.IsNullOrWhiteSpace(keyPair.PrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey)) - { - organization.PrivateKey = keyPair.PrivateKey; - } - } - - /// - /// Updates the organization public and private keys if provided and not already set. - /// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft - /// migration that will silently migrate organizations when they change their details. - /// - public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationUpdateRequest request) - { - organization.BackfillPublicPrivateKeys(request.Keys); - } -} diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 5c1c6007d4fd..8bdd964f63b8 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; @@ -266,10 +267,11 @@ await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, organization.UseCustomPermissions = newPlan.HasCustomPermissions; organization.Plan = newPlan.Name; organization.Enabled = success; - organization.BackfillPublicPrivateKeys(upgrade.Keys); organization.UsePasswordManager = true; organization.UseSecretsManager = upgrade.UseSecretsManager; + organization.BackfillPublicPrivateKeys(upgrade.Keys); + if (upgrade.UseSecretsManager) { organization.SmSeats = newPlan.SecretsManager.BaseSeats + upgrade.AdditionalSmSeats.GetValueOrDefault(); diff --git a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs index c7c749effd2d..6b6da859f3b9 100644 --- a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs @@ -66,8 +66,8 @@ public async Task CreateAsync_OK( signup.Plan == requestBody.PlanType && signup.AdditionalSeats == requestBody.Seats && signup.OwnerKey == requestBody.Key && - signup.PublicKey == requestBody.KeyPair.PublicKey && - signup.PrivateKey == requestBody.KeyPair.EncryptedPrivateKey && + signup.Keys.PublicKey == requestBody.KeyPair.PublicKey && + signup.Keys.PrivateKey == requestBody.KeyPair.EncryptedPrivateKey && signup.CollectionName == requestBody.CollectionName), requestBody.OwnerEmail, user) From 261c40ad88a3c31541b81cfebb37021515455a22 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 16 Dec 2025 17:15:16 +1000 Subject: [PATCH 4/9] Use method to map between api and core --- .../Controllers/ProviderClientsController.cs | 7 +--- .../OrganizationCreateRequestModel.cs | 5 ++- .../OrganizationKeysRequestModel.cs | 32 +++---------------- .../OrganizationNoPaymentCreateRequest.cs | 3 +- .../OrganizationUpdateRequestModel.cs | 7 +--- .../OrganizationUpgradeRequestModel.cs | 7 +--- .../Models/Requests/KeyPairRequestBody.cs | 10 ++++++ .../UpgradeOrganizationPlanCommand.cs | 1 - 8 files changed, 21 insertions(+), 51 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs index a560bc59b79f..b98d350ae2f9 100644 --- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -3,7 +3,6 @@ using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Providers.Services; @@ -58,11 +57,7 @@ public async Task CreateAsync( Owner = user, BillingEmail = provider.BillingEmail, OwnerKey = requestBody.Key, - Keys = new OrganizationKeyPair - { - PublicKey = requestBody.KeyPair.PublicKey, - PrivateKey = requestBody.KeyPair.EncryptedPrivateKey - }, + Keys = requestBody.KeyPair.ToOrganizationKeyPair(), CollectionName = requestBody.CollectionName, IsFromProvider = true }; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 7754c44c8c4d..50803d29b013 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -113,11 +113,10 @@ public virtual OrganizationSignup ToOrganizationSignup(User user) BillingAddressCountry = BillingAddressCountry, }, InitiationPath = InitiationPath, - SkipTrial = SkipTrial + SkipTrial = SkipTrial, + Keys = Keys?.ToOrganizationKeyPair() }; - Keys?.ToOrganizationSignup(orgSignup); - return orgSignup; } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs index 5a6d52059ae2..e468edbfec81 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs @@ -2,9 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; -using Bit.Core.Models.Business; namespace Bit.Api.AdminConsole.Models.Request.Organizations; @@ -15,32 +13,12 @@ public class OrganizationKeysRequestModel [Required] public string EncryptedPrivateKey { get; set; } - public OrganizationSignup ToOrganizationSignup(OrganizationSignup existingSignup) + public OrganizationKeyPair ToOrganizationKeyPair() { - if (existingSignup.Keys == null || (string.IsNullOrWhiteSpace(existingSignup.Keys.PublicKey) && string.IsNullOrWhiteSpace(existingSignup.Keys.PrivateKey))) + return new OrganizationKeyPair { - existingSignup.Keys = new OrganizationKeyPair - { - PublicKey = PublicKey, - PrivateKey = EncryptedPrivateKey - }; - } - - return existingSignup; - } - - public Organization ToOrganization(Organization existingOrg) - { - if (string.IsNullOrWhiteSpace(existingOrg.PublicKey)) - { - existingOrg.PublicKey = PublicKey; - } - - if (string.IsNullOrWhiteSpace(existingOrg.PrivateKey)) - { - existingOrg.PrivateKey = EncryptedPrivateKey; - } - - return existingOrg; + PublicKey = PublicKey, + PrivateKey = EncryptedPrivateKey + }; } } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs index 0c62b235189a..a055ba078edf 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs @@ -110,10 +110,9 @@ public virtual OrganizationSignup ToOrganizationSignup(User user) BillingAddressCountry = BillingAddressCountry, }, InitiationPath = InitiationPath, + Keys = Keys?.ToOrganizationKeyPair() }; - Keys?.ToOrganizationSignup(orgSignup); - return orgSignup; } } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs index ae37af654eef..aab4d54908b7 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -1,6 +1,5 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.Utilities; @@ -23,10 +22,6 @@ public class OrganizationUpdateRequestModel OrganizationId = organizationId, Name = Name, BillingEmail = BillingEmail, - Keys = Keys != null ? new OrganizationKeyPair - { - PublicKey = Keys.PublicKey, - PrivateKey = Keys.EncryptedPrivateKey - } : null + Keys = Keys?.ToOrganizationKeyPair() }; } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs index 91e079e61723..6322bf5cfbc4 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -2,7 +2,6 @@ #nullable disable using System.ComponentModel.DataAnnotations; -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; using Bit.Core.Models.Business; @@ -45,11 +44,7 @@ public OrganizationUpgrade ToOrganizationUpgrade() BillingAddressCountry = BillingAddressCountry, BillingAddressPostalCode = BillingAddressPostalCode }, - Keys = Keys != null ? new OrganizationKeyPair - { - PublicKey = Keys.PublicKey, - PrivateKey = Keys.EncryptedPrivateKey - } : null + Keys = Keys?.ToOrganizationKeyPair() }; return orgUpgrade; diff --git a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs index 2fec3bd61d3a..b994c78ec553 100644 --- a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs +++ b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs @@ -2,6 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; namespace Bit.Api.Billing.Models.Requests; @@ -12,4 +13,13 @@ public class KeyPairRequestBody public string PublicKey { get; set; } [Required(ErrorMessage = "'encryptedPrivateKey' must be provided")] public string EncryptedPrivateKey { get; set; } + + public OrganizationKeyPair ToOrganizationKeyPair() + { + return new OrganizationKeyPair + { + PublicKey = PublicKey, + PrivateKey = EncryptedPrivateKey + }; + } } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 8bdd964f63b8..4ad63bd8d77a 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; From 4adb96b2662ef0f823f72a9841f7ab90327a308b Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 16 Dec 2025 17:24:51 +1000 Subject: [PATCH 5/9] Ensure keys are set together or not at all --- .../Organizations/OrganizationExtensions.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs index f49e04762155..385b9be07f7a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs @@ -11,19 +11,16 @@ public static class OrganizationExtensions /// public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationKeyPair? keyPair) { - if (keyPair == null) + // Only backfill if both new keys are provided and both old keys are missing. + if (string.IsNullOrWhiteSpace(keyPair?.PublicKey) || + string.IsNullOrWhiteSpace(keyPair.PrivateKey) || + !string.IsNullOrWhiteSpace(organization.PublicKey) || + !string.IsNullOrWhiteSpace(organization.PrivateKey)) { return; } - if (!string.IsNullOrWhiteSpace(keyPair.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey)) - { - organization.PublicKey = keyPair.PublicKey; - } - - if (!string.IsNullOrWhiteSpace(keyPair.PrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey)) - { - organization.PrivateKey = keyPair.PrivateKey; - } + organization.PublicKey = keyPair.PublicKey; + organization.PrivateKey = keyPair.PrivateKey; } } From 78fe901d63a5f484131bb6e7935db2bba410f14f Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Tue, 16 Dec 2025 17:28:19 +1000 Subject: [PATCH 6/9] Tweak comment --- .../Organizations/OrganizationExtensions.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs index 385b9be07f7a..0a284c0b0053 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs @@ -6,8 +6,9 @@ public static class OrganizationExtensions { /// /// Updates the organization public and private keys if provided and not already set. - /// This is legacy code for old organizations that were not created with a public/private keypair. It is a soft - /// migration that will silently migrate organizations when they change their details or upgrade their plan. + /// This is legacy code for old organizations that were not created with a public/private keypair. + /// It is a soft migration that will silently migrate organizations when they perform certain actions, + /// e.g. change their details or upgrade their plan. /// public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationKeyPair? keyPair) { From 184cbb79d59653ee78e62d846b6cc7e0df1a4157 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 17 Dec 2025 13:35:39 +1000 Subject: [PATCH 7/9] Use KM Team keypair model --- .../Controllers/ProviderClientsController.cs | 2 +- .../OrganizationCreateRequestModel.cs | 2 +- .../OrganizationKeysRequestModel.cs | 12 ++++----- .../OrganizationNoPaymentCreateRequest.cs | 2 +- .../OrganizationUpdateRequestModel.cs | 2 +- .../OrganizationUpgradeRequestModel.cs | 2 +- .../Models/Requests/KeyPairRequestBody.cs | 12 ++++----- .../CloudOrganizationSignUpCommand.cs | 2 +- .../Organizations/OrganizationExtensions.cs | 7 +++--- .../Organizations/OrganizationKeyPair.cs | 11 -------- ...ProviderClientOrganizationSignUpCommand.cs | 2 +- .../Update/OrganizationUpdateRequest.cs | 6 +++-- .../Models/Business/OrganizationUpgrade.cs | 4 +-- .../ProviderClientsControllerTests.cs | 2 +- .../OrganizationUpdateCommandTests.cs | 25 ++++++++----------- .../UpgradeOrganizationPlanCommandTests.cs | 17 ++++++------- 16 files changed, 45 insertions(+), 65 deletions(-) delete mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs index b98d350ae2f9..dfa698482679 100644 --- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -57,7 +57,7 @@ public async Task CreateAsync( Owner = user, BillingEmail = provider.BillingEmail, OwnerKey = requestBody.Key, - Keys = requestBody.KeyPair.ToOrganizationKeyPair(), + Keys = requestBody.KeyPair.ToPublicKeyEncryptionKeyPairData(), CollectionName = requestBody.CollectionName, IsFromProvider = true }; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 50803d29b013..464ba0c2fdee 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -114,7 +114,7 @@ public virtual OrganizationSignup ToOrganizationSignup(User user) }, InitiationPath = InitiationPath, SkipTrial = SkipTrial, - Keys = Keys?.ToOrganizationKeyPair() + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; return orgSignup; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs index e468edbfec81..ef2fb0f07be7 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs @@ -2,7 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Api.AdminConsole.Models.Request.Organizations; @@ -13,12 +13,10 @@ public class OrganizationKeysRequestModel [Required] public string EncryptedPrivateKey { get; set; } - public OrganizationKeyPair ToOrganizationKeyPair() + public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData() { - return new OrganizationKeyPair - { - PublicKey = PublicKey, - PrivateKey = EncryptedPrivateKey - }; + return new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: EncryptedPrivateKey, + publicKey: PublicKey); } } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs index a055ba078edf..81d7c413ebb0 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs @@ -110,7 +110,7 @@ public virtual OrganizationSignup ToOrganizationSignup(User user) BillingAddressCountry = BillingAddressCountry, }, InitiationPath = InitiationPath, - Keys = Keys?.ToOrganizationKeyPair() + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; return orgSignup; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs index aab4d54908b7..a0b1247ae181 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -22,6 +22,6 @@ public class OrganizationUpdateRequestModel OrganizationId = organizationId, Name = Name, BillingEmail = BillingEmail, - Keys = Keys?.ToOrganizationKeyPair() + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; } diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs index 6322bf5cfbc4..7d5a9e56c740 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -44,7 +44,7 @@ public OrganizationUpgrade ToOrganizationUpgrade() BillingAddressCountry = BillingAddressCountry, BillingAddressPostalCode = BillingAddressPostalCode }, - Keys = Keys?.ToOrganizationKeyPair() + Keys = Keys?.ToPublicKeyEncryptionKeyPairData() }; return orgUpgrade; diff --git a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs index b994c78ec553..9979141b6ddb 100644 --- a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs +++ b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs @@ -2,7 +2,7 @@ #nullable disable using System.ComponentModel.DataAnnotations; -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Api.Billing.Models.Requests; @@ -14,12 +14,10 @@ public class KeyPairRequestBody [Required(ErrorMessage = "'encryptedPrivateKey' must be provided")] public string EncryptedPrivateKey { get; set; } - public OrganizationKeyPair ToOrganizationKeyPair() + public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData() { - return new OrganizationKeyPair - { - PublicKey = PublicKey, - PrivateKey = EncryptedPrivateKey - }; + return new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: EncryptedPrivateKey, + publicKey: PublicKey); } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index f6bf4fe8296f..2aa09a5250d2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -100,7 +100,7 @@ public async Task SignUpOrganizationAsync(Organizati Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), PublicKey = signup.Keys?.PublicKey, - PrivateKey = signup.Keys?.PrivateKey, + PrivateKey = signup.Keys?.WrappedPrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs index 0a284c0b0053..bb8f98549519 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs @@ -1,4 +1,5 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; @@ -10,11 +11,11 @@ public static class OrganizationExtensions /// It is a soft migration that will silently migrate organizations when they perform certain actions, /// e.g. change their details or upgrade their plan. /// - public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationKeyPair? keyPair) + public static void BackfillPublicPrivateKeys(this Organization organization, PublicKeyEncryptionKeyPairData? keyPair) { // Only backfill if both new keys are provided and both old keys are missing. if (string.IsNullOrWhiteSpace(keyPair?.PublicKey) || - string.IsNullOrWhiteSpace(keyPair.PrivateKey) || + string.IsNullOrWhiteSpace(keyPair.WrappedPrivateKey) || !string.IsNullOrWhiteSpace(organization.PublicKey) || !string.IsNullOrWhiteSpace(organization.PrivateKey)) { @@ -22,6 +23,6 @@ public static void BackfillPublicPrivateKeys(this Organization organization, Org } organization.PublicKey = keyPair.PublicKey; - organization.PrivateKey = keyPair.PrivateKey; + organization.PrivateKey = keyPair.WrappedPrivateKey; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs deleted file mode 100644 index e44c053a35c2..000000000000 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationKeyPair.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; - -/// -/// Data transfer object for organization public/private key pairs. -/// Used to normalize key handling across different request models and commands. -/// -public record OrganizationKeyPair -{ - public required string PublicKey { get; init; } - public required string PrivateKey { get; init; } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs index 324fc7f11af4..c51ab2a5e03f 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -94,7 +94,7 @@ public async Task SignUpClientOrganiza Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), PublicKey = signup.Keys?.PublicKey, - PrivateKey = signup.Keys?.PrivateKey, + PrivateKey = signup.Keys?.WrappedPrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs index 622420cc6fcf..4695ee0ba7d0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; /// /// Request model for updating the name, billing email, and/or public-private keys for an organization (legacy migration code). @@ -24,5 +26,5 @@ public record OrganizationUpdateRequest /// /// The organization's public/private key pair to set (optional, only set if not already present on the organization). /// - public OrganizationKeyPair? Keys { get; init; } + public PublicKeyEncryptionKeyPairData? Keys { get; init; } } diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs index c342656d87a8..d165a96d0a39 100644 --- a/src/Core/Models/Business/OrganizationUpgrade.cs +++ b/src/Core/Models/Business/OrganizationUpgrade.cs @@ -1,8 +1,8 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.Billing.Enums; +using Bit.Core.KeyManagement.Models.Data; namespace Bit.Core.Models.Business; @@ -14,7 +14,7 @@ public class OrganizationUpgrade public short AdditionalStorageGb { get; set; } public bool PremiumAccessAddon { get; set; } public TaxInfo TaxInfo { get; set; } - public OrganizationKeyPair Keys { get; set; } + public PublicKeyEncryptionKeyPairData Keys { get; set; } public int? AdditionalSmSeats { get; set; } public int? AdditionalServiceAccounts { get; set; } public bool UseSecretsManager { get; set; } diff --git a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs index 6b6da859f3b9..259797dfb32b 100644 --- a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs @@ -67,7 +67,7 @@ public async Task CreateAsync_OK( signup.AdditionalSeats == requestBody.Seats && signup.OwnerKey == requestBody.Key && signup.Keys.PublicKey == requestBody.KeyPair.PublicKey && - signup.Keys.PrivateKey == requestBody.KeyPair.EncryptedPrivateKey && + signup.Keys.WrappedPrivateKey == requestBody.KeyPair.EncryptedPrivateKey && signup.CollectionName == requestBody.CollectionName), requestBody.OwnerEmail, user) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs index 5810e9057485..13c51d42d28b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs @@ -4,6 +4,7 @@ using Bit.Core.Billing.Organizations.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -163,11 +164,9 @@ public async Task UpdateAsync_WhenKeysProvided_AndNotAlreadySet_SetsKeys( OrganizationId = organizationId, Name = organization.Name, BillingEmail = organization.BillingEmail, - Keys = new OrganizationKeyPair - { - PublicKey = publicKey, - PrivateKey = encryptedPrivateKey - } + Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: encryptedPrivateKey, + publicKey: publicKey) }; // Act @@ -211,11 +210,9 @@ public async Task UpdateAsync_WhenKeysProvided_AndAlreadySet_DoesNotOverwriteKey OrganizationId = organizationId, Name = organization.Name, BillingEmail = organization.BillingEmail, - Keys = new OrganizationKeyPair - { - PublicKey = newPublicKey, - PrivateKey = newEncryptedPrivateKey - } + Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: newEncryptedPrivateKey, + publicKey: newPublicKey) }; // Act @@ -401,11 +398,9 @@ public async Task UpdateAsync_SelfHosted_OnlyUpdatesKeysNotOrganizationDetails( OrganizationId = organizationId, Name = newName, // Should be ignored BillingEmail = newBillingEmail, // Should be ignored - Keys = new OrganizationKeyPair - { - PublicKey = publicKey, - PrivateKey = encryptedPrivateKey - } + Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: encryptedPrivateKey, + publicKey: publicKey) }; // Act diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 0a1079d9d002..9a6207d8879a 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -3,6 +3,7 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; @@ -261,11 +262,9 @@ public async Task UpgradePlan_OrgHasNoKeysAndUpgradeHasKeys_SetsKeys( organization.PrivateKey = null; upgrade.Plan = PlanType.TeamsAnnually; - upgrade.Keys = new OrganizationKeyPair - { - PublicKey = newPublicKey, - PrivateKey = newPrivateKey - }; + upgrade.Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: newPrivateKey, + publicKey: newPublicKey); upgrade.AdditionalSeats = 10; sutProvider.GetDependency() @@ -355,11 +354,9 @@ public async Task UpgradePlan_OrgHasKeysAndUpgradeHasKeys_PreservesExistingKeys( organization.PrivateKey = existingPrivateKey; upgrade.Plan = PlanType.TeamsAnnually; - upgrade.Keys = new OrganizationKeyPair - { - PublicKey = newPublicKey, - PrivateKey = newPrivateKey - }; + upgrade.Keys = new PublicKeyEncryptionKeyPairData( + wrappedPrivateKey: newPrivateKey, + publicKey: newPublicKey); upgrade.AdditionalSeats = 10; sutProvider.GetDependency() From 823bf6a25446947c5145f79e8a1db1902df47453 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 17 Dec 2025 13:42:43 +1000 Subject: [PATCH 8/9] Tweak tests --- .../UpgradeOrganizationPlanCommandTests.cs | 59 ++----------------- 1 file changed, 5 insertions(+), 54 deletions(-) diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 9a6207d8879a..7a77d6c78a25 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -247,17 +247,13 @@ public async Task UpgradePlan_SM_NotEnoughServiceAccounts_Throws(PlanType planTy [Theory] [FreeOrganizationUpgradeCustomize, BitAutoData] - public async Task UpgradePlan_OrgHasNoKeysAndUpgradeHasKeys_SetsKeys( + public async Task UpgradePlan_WhenOrganizationIsMissingPublicAndPrivateKeys_Backfills( Organization organization, OrganizationUpgrade upgrade, + string newPublicKey, + string newPrivateKey, SutProvider sutProvider) { - // Arrange - const string newPublicKey = "new-public-key"; - const string newPrivateKey = "new-private-key"; - - organization.GatewayCustomerId = "customer-id"; - organization.GatewaySubscriptionId = "subscription-id"; organization.PublicKey = null; organization.PrivateKey = null; @@ -293,7 +289,7 @@ await sutProvider.GetDependency() [Theory] [FreeOrganizationUpgradeCustomize, BitAutoData] - public async Task UpgradePlan_OrgHasKeysAndUpgradeHasNullKeys_PreservesExistingKeys( + public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull( Organization organization, OrganizationUpgrade upgrade, SutProvider sutProvider) @@ -302,8 +298,6 @@ public async Task UpgradePlan_OrgHasKeysAndUpgradeHasNullKeys_PreservesExistingK const string existingPublicKey = "existing-public-key"; const string existingPrivateKey = "existing-private-key"; - organization.GatewayCustomerId = "customer-id"; - organization.GatewaySubscriptionId = "subscription-id"; organization.PublicKey = existingPublicKey; organization.PrivateKey = existingPrivateKey; @@ -337,7 +331,7 @@ await sutProvider.GetDependency() [Theory] [FreeOrganizationUpgradeCustomize, BitAutoData] - public async Task UpgradePlan_OrgHasKeysAndUpgradeHasKeys_PreservesExistingKeys( + public async Task UpgradePlan_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys( Organization organization, OrganizationUpgrade upgrade, SutProvider sutProvider) @@ -348,8 +342,6 @@ public async Task UpgradePlan_OrgHasKeysAndUpgradeHasKeys_PreservesExistingKeys( const string newPublicKey = "new-public-key"; const string newPrivateKey = "new-private-key"; - organization.GatewayCustomerId = "customer-id"; - organization.GatewaySubscriptionId = "subscription-id"; organization.PublicKey = existingPublicKey; organization.PrivateKey = existingPrivateKey; @@ -382,45 +374,4 @@ await sutProvider.GetDependency() .Received(1) .ReplaceAndUpdateCacheAsync(organization); } - - [Theory] - [FreeOrganizationUpgradeCustomize, BitAutoData] - public async Task UpgradePlan_OrgHasNoKeysAndUpgradeHasNoKeys_LeavesKeysNull( - Organization organization, - OrganizationUpgrade upgrade, - SutProvider sutProvider) - { - // Arrange - organization.GatewayCustomerId = "customer-id"; - organization.GatewaySubscriptionId = "subscription-id"; - organization.PublicKey = null; - organization.PrivateKey = null; - - upgrade.Plan = PlanType.TeamsAnnually; - upgrade.Keys = null; - upgrade.AdditionalSeats = 10; - - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - sutProvider.GetDependency() - .GetPlanOrThrow(organization.PlanType) - .Returns(MockPlans.Get(organization.PlanType)); - sutProvider.GetDependency() - .GetPlanOrThrow(upgrade.Plan) - .Returns(MockPlans.Get(upgrade.Plan)); - sutProvider.GetDependency() - .GetOccupiedSeatCountByOrganizationIdAsync(organization.Id) - .Returns(new OrganizationSeatCounts { Sponsored = 0, Users = 1 }); - - // Act - await sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade); - - // Assert - Assert.Null(organization.PublicKey); - Assert.Null(organization.PrivateKey); - await sutProvider.GetDependency() - .Received(1) - .ReplaceAndUpdateCacheAsync(organization); - } } From 449d4d18ea71507a4ef733390f87f12b74f0724c Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Wed, 17 Dec 2025 14:05:13 +1000 Subject: [PATCH 9/9] dotnet format --- .../Organizations/OrganizationUpdateCommandTests.cs | 1 - .../UpgradeOrganizationPlanCommandTests.cs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs index b3c15d028204..997076e7ef33 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs @@ -1,5 +1,4 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update; using Bit.Core.Billing.Organizations.Services; using Bit.Core.Enums; diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs index 7a77d6c78a25..223047ee07f2 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs @@ -1,5 +1,4 @@ -using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; -using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Exceptions;