mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05:00
[PM-18770] Convert Organization to Business Unit (#5610)
* [NO LOGIC] Rename MultiOrganizationEnterprise to BusinessUnit * [Core] Add IMailService.SendBusinessUnitConversionInviteAsync * [Core] Add BusinessUnitConverter * [Admin] Add new permission * [Admin] Add BusinessUnitConverterController * [Admin] Add Convert to Business Unit button to Organization edit page * [Api] Add OrganizationBillingController.SetupBusinessUnitAsync action * [Multi] Propagate provider type to sync response * [Multi] Put updates behind feature flag * [Tests] BusinessUnitConverterTests * Run dotnet format * Fixing post-main merge compilation failure
This commit is contained in:
@ -8,6 +8,6 @@ public enum ProviderType : byte
|
||||
Msp = 0,
|
||||
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Creates Bitwarden Portal page for client organization billing management", Order = 1000)]
|
||||
Reseller = 1,
|
||||
[Display(ShortName = "MOE", Name = "Multi-organization Enterprises", Description = "Creates provider portal for multi-organization management", Order = 1)]
|
||||
MultiOrganizationEnterprise = 2,
|
||||
[Display(ShortName = "Business Unit", Name = "Business Unit", Description = "Creates provider portal for business unit management", Order = 1)]
|
||||
BusinessUnit = 2,
|
||||
}
|
||||
|
@ -17,4 +17,5 @@ public class ProviderUserProviderDetails
|
||||
public string Permissions { get; set; }
|
||||
public bool UseEvents { get; set; }
|
||||
public ProviderStatusType ProviderStatus { get; set; }
|
||||
public ProviderType ProviderType { get; set; }
|
||||
}
|
||||
|
@ -7,5 +7,5 @@ public interface ICreateProviderCommand
|
||||
{
|
||||
Task CreateMspAsync(Provider provider, string ownerEmail, int teamsMinimumSeats, int enterpriseMinimumSeats);
|
||||
Task CreateResellerAsync(Provider provider);
|
||||
Task CreateMultiOrganizationEnterpriseAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats);
|
||||
Task CreateBusinessUnitAsync(Provider provider, string ownerEmail, PlanType plan, int minimumSeats);
|
||||
}
|
||||
|
@ -25,19 +25,19 @@ public static class BillingExtensions
|
||||
public static bool IsBillable(this Provider provider) =>
|
||||
provider is
|
||||
{
|
||||
Type: ProviderType.Msp or ProviderType.MultiOrganizationEnterprise,
|
||||
Type: ProviderType.Msp or ProviderType.BusinessUnit,
|
||||
Status: ProviderStatusType.Billable
|
||||
};
|
||||
|
||||
public static bool IsBillable(this InviteOrganizationProvider inviteOrganizationProvider) =>
|
||||
inviteOrganizationProvider is
|
||||
{
|
||||
Type: ProviderType.Msp or ProviderType.MultiOrganizationEnterprise,
|
||||
Type: ProviderType.Msp or ProviderType.BusinessUnit,
|
||||
Status: ProviderStatusType.Billable
|
||||
};
|
||||
|
||||
public static bool SupportsConsolidatedBilling(this ProviderType providerType)
|
||||
=> providerType is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
||||
=> providerType is ProviderType.Msp or ProviderType.BusinessUnit;
|
||||
|
||||
public static bool IsValidClient(this Organization organization)
|
||||
=> organization is
|
||||
|
58
src/Core/Billing/Services/IBusinessUnitConverter.cs
Normal file
58
src/Core/Billing/Services/IBusinessUnitConverter.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using OneOf;
|
||||
|
||||
namespace Bit.Core.Billing.Services;
|
||||
|
||||
public interface IBusinessUnitConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Finalizes the process of converting the <paramref name="organization"/> to a <see cref="ProviderType.BusinessUnit"/> by
|
||||
/// saving all the necessary key provided by the client and updating the <paramref name="organization"/>'s subscription to a
|
||||
/// provider subscription.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to convert to a business unit.</param>
|
||||
/// <param name="userId">The ID of the organization member who will be the provider admin.</param>
|
||||
/// <param name="token">The token sent to the client as part of the <see cref="InitiateConversion"/> process.</param>
|
||||
/// <param name="providerKey">The encrypted provider key used to enable the <see cref="ProviderUser"/>.</param>
|
||||
/// <param name="organizationKey">The encrypted organization key used to enable the <see cref="ProviderOrganization"/>.</param>
|
||||
/// <returns>The provider ID</returns>
|
||||
Task<Guid> FinalizeConversion(
|
||||
Organization organization,
|
||||
Guid userId,
|
||||
string token,
|
||||
string providerKey,
|
||||
string organizationKey);
|
||||
|
||||
/// <summary>
|
||||
/// Begins the process of converting the <paramref name="organization"/> to a <see cref="ProviderType.BusinessUnit"/> by
|
||||
/// creating all the necessary database entities and sending a setup invitation to the <paramref name="providerAdminEmail"/>.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to convert to a business unit.</param>
|
||||
/// <param name="providerAdminEmail">The email address of the organization member who will be the provider admin.</param>
|
||||
/// <returns>Either the newly created provider ID or a list of validation failures.</returns>
|
||||
Task<OneOf<Guid, List<string>>> InitiateConversion(
|
||||
Organization organization,
|
||||
string providerAdminEmail);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the <paramref name="organization"/> has a business unit conversion in progress and, if it does, resends the
|
||||
/// setup invitation to the provider admin.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to convert to a business unit.</param>
|
||||
/// <param name="providerAdminEmail">The email address of the organization member who will be the provider admin.</param>
|
||||
Task ResendConversionInvite(
|
||||
Organization organization,
|
||||
string providerAdminEmail);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the <paramref name="organization"/> has a business unit conversion in progress and, if it does, resets that conversion
|
||||
/// by deleting all the database entities created as part of <see cref="InitiateConversion"/>.
|
||||
/// </summary>
|
||||
/// <param name="organization">The organization to convert to a business unit.</param>
|
||||
/// <param name="providerAdminEmail">The email address of the organization member who will be the provider admin.</param>
|
||||
Task ResetConversion(
|
||||
Organization organization,
|
||||
string providerAdminEmail);
|
||||
}
|
@ -147,6 +147,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
|
||||
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
|
||||
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
|
||||
|
||||
/* Key Management Team */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
|
@ -0,0 +1,19 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||
You have been invited to set up a new Business Unit Portal within Bitwarden.
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: center;" valign="top" align="center">
|
||||
<a href="{{{Url}}}" clicktracking=off target="_blank" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
Set Up Business Unit Portal Now
|
||||
</a>
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
You have been invited to set up a new Business Unit Portal within Bitwarden. To continue, click the following link:
|
||||
|
||||
{{{Url}}}
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Models.Mail.Billing;
|
||||
|
||||
public class BusinessUnitConversionInviteModel : BaseMailModel
|
||||
{
|
||||
public string OrganizationId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Token { get; set; }
|
||||
|
||||
public string Url =>
|
||||
$"{WebVaultUrl}/providers/setup-business-unit?organizationId={OrganizationId}&email={Email}&token={Token}";
|
||||
}
|
@ -70,6 +70,7 @@ public interface IMailService
|
||||
Task SendEnqueuedMailMessageAsync(IMailQueueMessage queueMessage);
|
||||
Task SendAdminResetPasswordEmailAsync(string email, string userName, string orgName);
|
||||
Task SendProviderSetupInviteEmailAsync(Provider provider, string token, string email);
|
||||
Task SendBusinessUnitConversionInviteAsync(Organization organization, string token, string email);
|
||||
Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email);
|
||||
Task SendProviderConfirmedEmailAsync(string providerName, string email);
|
||||
Task SendProviderUserRemoved(string providerName, string email);
|
||||
|
@ -11,6 +11,7 @@ using Bit.Core.Billing.Models.Mail;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Models.Mail;
|
||||
using Bit.Core.Models.Mail.Billing;
|
||||
using Bit.Core.Models.Mail.FamiliesForEnterprise;
|
||||
using Bit.Core.Models.Mail.Provider;
|
||||
using Bit.Core.SecretsManager.Models.Mail;
|
||||
@ -949,6 +950,22 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendBusinessUnitConversionInviteAsync(Organization organization, string token, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("Set Up Business Unit", email);
|
||||
var model = new BusinessUnitConversionInviteModel
|
||||
{
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
OrganizationId = organization.Id.ToString(),
|
||||
Email = WebUtility.UrlEncode(email),
|
||||
Token = WebUtility.UrlEncode(token)
|
||||
};
|
||||
await AddMessageContentAsync(message, "Billing.BusinessUnitConversionInvite", model);
|
||||
message.Category = "BusinessUnitConversionInvite";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"Join {providerName}", email);
|
||||
|
@ -212,6 +212,11 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendBusinessUnitConversionInviteAsync(Organization organization, string token, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendProviderInviteEmailAsync(string providerName, ProviderUser providerUser, string token, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
Reference in New Issue
Block a user