1
0
mirror of https://github.com/bitwarden/server synced 2025-12-30 15:14:02 +00:00

Merge branch 'main' into jmccannon/ac/pm-27131-auto-confirm-req

This commit is contained in:
jrmccannon
2025-11-26 09:06:52 -06:00
61 changed files with 4751 additions and 813 deletions

View File

@@ -0,0 +1,15 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
public interface IOrganizationUpdateCommand
{
/// <summary>
/// Updates an organization's information in the Bitwarden database and Stripe (if required).
/// Also optionally updates an organization's public-private keypair if it was not created with one.
/// On self-host, only the public-private keys will be updated because all other properties are fixed by the license file.
/// </summary>
/// <param name="request">The update request containing the details to be updated.</param>
Task<Organization> UpdateAsync(OrganizationUpdateRequest request);
}

View File

@@ -0,0 +1,77 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
public class OrganizationUpdateCommand(
IOrganizationService organizationService,
IOrganizationRepository organizationRepository,
IGlobalSettings globalSettings,
IOrganizationBillingService organizationBillingService
) : IOrganizationUpdateCommand
{
public async Task<Organization> UpdateAsync(OrganizationUpdateRequest request)
{
var organization = await organizationRepository.GetByIdAsync(request.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
if (globalSettings.SelfHosted)
{
return await UpdateSelfHostedAsync(organization, request);
}
return await UpdateCloudAsync(organization, request);
}
private async Task<Organization> UpdateCloudAsync(Organization organization, OrganizationUpdateRequest request)
{
// Store original values for comparison
var originalName = organization.Name;
var originalBillingEmail = organization.BillingEmail;
// Apply updates to organization
organization.UpdateDetails(request);
organization.BackfillPublicPrivateKeys(request);
await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);
// Update billing information in Stripe if required
await UpdateBillingAsync(organization, originalName, originalBillingEmail);
return organization;
}
/// <summary>
/// Self-host cannot update the organization details because they are set by the license file.
/// However, this command does offer a soft migration pathway for organizations without public and private keys.
/// If we remove this migration code in the future, this command and endpoint can become cloud only.
/// </summary>
private async Task<Organization> UpdateSelfHostedAsync(Organization organization, OrganizationUpdateRequest request)
{
organization.BackfillPublicPrivateKeys(request);
await organizationService.ReplaceAndUpdateCacheAsync(organization, EventType.Organization_Updated);
return organization;
}
private async Task UpdateBillingAsync(Organization organization, string originalName, string? originalBillingEmail)
{
// Update Stripe if name or billing email changed
var shouldUpdateBilling = originalName != organization.Name ||
originalBillingEmail != organization.BillingEmail;
if (!shouldUpdateBilling || string.IsNullOrWhiteSpace(organization.GatewayCustomerId))
{
return;
}
await organizationBillingService.UpdateOrganizationNameAndEmail(organization);
}
}

View File

@@ -0,0 +1,43 @@
using Bit.Core.AdminConsole.Entities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
public static class OrganizationUpdateExtensions
{
/// <summary>
/// Updates the organization name and/or billing email.
/// Any null property on the request object will be skipped.
/// </summary>
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();
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
}
}

View File

@@ -0,0 +1,33 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Update;
/// <summary>
/// Request model for updating the name, billing email, and/or public-private keys for an organization (legacy migration code).
/// Any combination of these properties can be updated, so they are optional. If none are specified it will not update anything.
/// </summary>
public record OrganizationUpdateRequest
{
/// <summary>
/// The ID of the organization to update.
/// </summary>
public required Guid OrganizationId { get; init; }
/// <summary>
/// The new organization name to apply (optional, this is skipped if not provided).
/// </summary>
public string? Name { get; init; }
/// <summary>
/// The new billing email address to apply (optional, this is skipped if not provided).
/// </summary>
public string? BillingEmail { get; init; }
/// <summary>
/// The organization's public key to set (optional, only set if not already present on the organization).
/// </summary>
public string? PublicKey { get; init; }
/// <summary>
/// The organization's encrypted private key to set (optional, only set if not already present on the organization).
/// </summary>
public string? EncryptedPrivateKey { get; init; }
}