diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs
index caf2651e16..dfa6984826 100644
--- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs
+++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs
@@ -57,8 +57,7 @@ public class ProviderClientsController(
Owner = user,
BillingEmail = provider.BillingEmail,
OwnerKey = requestBody.Key,
- PublicKey = requestBody.KeyPair.PublicKey,
- PrivateKey = requestBody.KeyPair.EncryptedPrivateKey,
+ 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 7754c44c8c..464ba0c2fd 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs
@@ -113,11 +113,10 @@ public class OrganizationCreateRequestModel : IValidatableObject
BillingAddressCountry = BillingAddressCountry,
},
InitiationPath = InitiationPath,
- SkipTrial = SkipTrial
+ SkipTrial = SkipTrial,
+ Keys = Keys?.ToPublicKeyEncryptionKeyPairData()
};
- 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 22b225a689..ef2fb0f07b 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationKeysRequestModel.cs
@@ -2,8 +2,7 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
-using Bit.Core.AdminConsole.Entities;
-using Bit.Core.Models.Business;
+using Bit.Core.KeyManagement.Models.Data;
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
@@ -14,48 +13,10 @@ public class OrganizationKeysRequestModel
[Required]
public string EncryptedPrivateKey { get; set; }
- public OrganizationSignup ToOrganizationSignup(OrganizationSignup existingSignup)
+ public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()
{
- if (string.IsNullOrWhiteSpace(existingSignup.PublicKey))
- {
- existingSignup.PublicKey = PublicKey;
- }
-
- if (string.IsNullOrWhiteSpace(existingSignup.PrivateKey))
- {
- existingSignup.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))
- {
- existingOrg.PublicKey = PublicKey;
- }
-
- if (string.IsNullOrWhiteSpace(existingOrg.PrivateKey))
- {
- existingOrg.PrivateKey = EncryptedPrivateKey;
- }
-
- return existingOrg;
+ 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 0c62b23518..81d7c413eb 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationNoPaymentCreateRequest.cs
@@ -110,10 +110,9 @@ public class OrganizationNoPaymentCreateRequest
BillingAddressCountry = BillingAddressCountry,
},
InitiationPath = InitiationPath,
+ Keys = Keys?.ToPublicKeyEncryptionKeyPairData()
};
- 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 6c3867fe09..a0b1247ae1 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpdateRequestModel.cs
@@ -22,7 +22,6 @@ public class OrganizationUpdateRequestModel
OrganizationId = organizationId,
Name = Name,
BillingEmail = BillingEmail,
- PublicKey = Keys?.PublicKey,
- EncryptedPrivateKey = Keys?.EncryptedPrivateKey
+ Keys = Keys?.ToPublicKeyEncryptionKeyPairData()
};
}
diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs
index a5dec192b9..7d5a9e56c7 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs
@@ -43,11 +43,10 @@ public class OrganizationUpgradeRequestModel
{
BillingAddressCountry = BillingAddressCountry,
BillingAddressPostalCode = BillingAddressPostalCode
- }
+ },
+ Keys = Keys?.ToPublicKeyEncryptionKeyPairData()
};
- Keys?.ToOrganizationUpgrade(orgUpgrade);
-
return orgUpgrade;
}
}
diff --git a/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs b/src/Api/Billing/Models/Requests/KeyPairRequestBody.cs
index 2fec3bd61d..9979141b6d 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.KeyManagement.Models.Data;
namespace Bit.Api.Billing.Models.Requests;
@@ -12,4 +13,11 @@ public class KeyPairRequestBody
public string PublicKey { get; set; }
[Required(ErrorMessage = "'encryptedPrivateKey' must be provided")]
public string EncryptedPrivateKey { get; set; }
+
+ public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()
+ {
+ 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 7f24c4acd7..2aa09a5250 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs
@@ -99,8 +99,8 @@ public class CloudOrganizationSignUpCommand(
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
- PublicKey = signup.PublicKey,
- PrivateKey = signup.PrivateKey,
+ PublicKey = signup.Keys?.PublicKey,
+ 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
new file mode 100644
index 0000000000..bb8f985495
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationExtensions.cs
@@ -0,0 +1,28 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.KeyManagement.Models.Data;
+
+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 perform certain actions,
+ /// e.g. change their details or upgrade their plan.
+ ///
+ 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.WrappedPrivateKey) ||
+ !string.IsNullOrWhiteSpace(organization.PublicKey) ||
+ !string.IsNullOrWhiteSpace(organization.PrivateKey))
+ {
+ return;
+ }
+
+ organization.PublicKey = keyPair.PublicKey;
+ organization.PrivateKey = keyPair.WrappedPrivateKey;
+ }
+}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs
index 4a8f08a4f7..c51ab2a5e0 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs
@@ -93,8 +93,8 @@ public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizati
ReferenceData = signup.Owner.ReferenceData,
Enabled = true,
LicenseKey = CoreHelpers.SecureRandomString(20),
- PublicKey = signup.PublicKey,
- PrivateKey = signup.PrivateKey,
+ PublicKey = signup.Keys?.PublicKey,
+ PrivateKey = signup.Keys?.WrappedPrivateKey,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
Status = OrganizationStatusType.Created,
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs
index 83318fd1e6..5cfd2191b3 100644
--- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateCommand.cs
@@ -39,8 +39,20 @@ public class OrganizationUpdateCommand(
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 @@ public class OrganizationUpdateCommand(
///
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 e90c39bc54..0000000000
--- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateExtensions.cs
+++ /dev/null
@@ -1,43 +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.
- ///
- public static void BackfillPublicPrivateKeys(this Organization organization, OrganizationUpdateRequest request)
- {
- if (!string.IsNullOrWhiteSpace(request.PublicKey) && string.IsNullOrWhiteSpace(organization.PublicKey))
- {
- organization.PublicKey = request.PublicKey;
- }
-
- if (!string.IsNullOrWhiteSpace(request.EncryptedPrivateKey) && string.IsNullOrWhiteSpace(organization.PrivateKey))
- {
- organization.PrivateKey = request.EncryptedPrivateKey;
- }
- }
-}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Update/OrganizationUpdateRequest.cs
index 21d4948678..4695ee0ba7 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).
@@ -22,12 +24,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 PublicKeyEncryptionKeyPairData? Keys { get; init; }
}
diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs
index 89b9a5e6f2..d165a96d0a 100644
--- a/src/Core/Models/Business/OrganizationUpgrade.cs
+++ b/src/Core/Models/Business/OrganizationUpgrade.cs
@@ -2,6 +2,7 @@
#nullable disable
using Bit.Core.Billing.Enums;
+using Bit.Core.KeyManagement.Models.Data;
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 PublicKeyEncryptionKeyPairData 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 092ee0f46e..4ad63bd8d7 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.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
@@ -256,27 +257,20 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
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;
organization.Enabled = success;
- organization.PublicKey = upgrade.PublicKey;
- organization.PrivateKey = upgrade.PrivateKey;
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 c7c749effd..259797dfb3 100644
--- a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs
@@ -66,8 +66,8 @@ public class ProviderClientsControllerTests
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.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 d547d80aed..997076e7ef 100644
--- a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationUpdateCommandTests.cs
@@ -3,6 +3,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
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;
@@ -162,8 +163,9 @@ public class OrganizationUpdateCommandTests
OrganizationId = organizationId,
Name = organization.Name,
BillingEmail = organization.BillingEmail,
- PublicKey = publicKey,
- EncryptedPrivateKey = encryptedPrivateKey
+ Keys = new PublicKeyEncryptionKeyPairData(
+ wrappedPrivateKey: encryptedPrivateKey,
+ publicKey: publicKey)
};
// Act
@@ -207,8 +209,9 @@ public class OrganizationUpdateCommandTests
OrganizationId = organizationId,
Name = organization.Name,
BillingEmail = organization.BillingEmail,
- PublicKey = newPublicKey,
- EncryptedPrivateKey = newEncryptedPrivateKey
+ Keys = new PublicKeyEncryptionKeyPairData(
+ wrappedPrivateKey: newEncryptedPrivateKey,
+ publicKey: newPublicKey)
};
// Act
@@ -394,8 +397,9 @@ public class OrganizationUpdateCommandTests
OrganizationId = organizationId,
Name = newName, // Should be ignored
BillingEmail = newBillingEmail, // Should be ignored
- PublicKey = publicKey,
- EncryptedPrivateKey = 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 8a00604bb0..223047ee07 100644
--- a/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs
+++ b/test/Core.Test/OrganizationFeatures/OrganizationSubscriptionUpdate/UpgradeOrganizationPlanCommandTests.cs
@@ -2,6 +2,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;
@@ -242,4 +243,134 @@ public class UpgradeOrganizationPlanCommandTests
await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAndUpdateCacheAsync(default);
}
+
+ [Theory]
+ [FreeOrganizationUpgradeCustomize, BitAutoData]
+ public async Task UpgradePlan_WhenOrganizationIsMissingPublicAndPrivateKeys_Backfills(
+ Organization organization,
+ OrganizationUpgrade upgrade,
+ string newPublicKey,
+ string newPrivateKey,
+ SutProvider sutProvider)
+ {
+ organization.PublicKey = null;
+ organization.PrivateKey = null;
+
+ upgrade.Plan = PlanType.TeamsAnnually;
+ upgrade.Keys = new PublicKeyEncryptionKeyPairData(
+ wrappedPrivateKey: newPrivateKey,
+ publicKey: newPublicKey);
+ 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_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotOverwriteWithNull(
+ Organization organization,
+ OrganizationUpgrade upgrade,
+ SutProvider sutProvider)
+ {
+ // Arrange
+ const string existingPublicKey = "existing-public-key";
+ const string existingPrivateKey = "existing-private-key";
+
+ 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_WhenOrganizationAlreadyHasPublicAndPrivateKeys_DoesNotBackfillWithNewKeys(
+ 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.PublicKey = existingPublicKey;
+ organization.PrivateKey = existingPrivateKey;
+
+ upgrade.Plan = PlanType.TeamsAnnually;
+ upgrade.Keys = new PublicKeyEncryptionKeyPairData(
+ wrappedPrivateKey: newPrivateKey,
+ publicKey: newPublicKey);
+ 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);
+ }
}