diff --git a/src/Api/Billing/Public/Controllers/OrganizationController.cs b/src/Api/Billing/Public/Controllers/OrganizationController.cs new file mode 100644 index 0000000000..294165a7e8 --- /dev/null +++ b/src/Api/Billing/Public/Controllers/OrganizationController.cs @@ -0,0 +1,81 @@ +using System.Net; +using Bit.Api.Models.Public.Response; +using Bit.Core.Context; +using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using OrganizationSubscriptionUpdateRequestModel = Bit.Api.Billing.Public.Models.OrganizationSubscriptionUpdateRequestModel; + +namespace Bit.Api.Billing.Public.Controllers; + +[Route("public/organization")] +[Authorize("Organization")] +public class OrganizationController : Controller +{ + private readonly IOrganizationService _organizationService; + private readonly ICurrentContext _currentContext; + private readonly IOrganizationRepository _organizationRepository; + private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; + + public OrganizationController( + IOrganizationService organizationService, + ICurrentContext currentContext, + IOrganizationRepository organizationRepository, + IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand) + { + _organizationService = organizationService; + _currentContext = currentContext; + _organizationRepository = organizationRepository; + _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; + } + + /// + /// Update the organization's current subscription for Password Manager and/or Secrets Manager. + /// + /// The request model containing the updated subscription information. + [HttpPut("subscription")] + [SelfHosted(NotSelfHostedOnly = true)] + [ProducesResponseType((int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)] + [ProducesResponseType((int)HttpStatusCode.NotFound)] + public async Task PostSubscriptionAsync([FromBody] OrganizationSubscriptionUpdateRequestModel model) + { + + await UpdatePasswordManagerAsync(model, _currentContext.OrganizationId.Value); + + await UpdateSecretsManagerAsync(model, _currentContext.OrganizationId.Value); + + return new OkResult(); + } + + private async Task UpdatePasswordManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId) + { + if (model.PasswordManager != null) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + + model.PasswordManager.ToPasswordManagerSubscriptionUpdate(organization); + await _organizationService.UpdateSubscription(organization.Id, (int)model.PasswordManager.Seats, + model.PasswordManager.MaxAutoScaleSeats); + if (model.PasswordManager.Storage.HasValue) + { + await _organizationService.AdjustStorageAsync(organization.Id, (short)model.PasswordManager.Storage); + } + } + } + + private async Task UpdateSecretsManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId) + { + if (model.SecretsManager != null) + { + var organization = + await _organizationRepository.GetByIdAsync(organizationId); + + var organizationUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization); + await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(organizationUpdate); + } + } +} diff --git a/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs b/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs new file mode 100644 index 0000000000..781ad3ca53 --- /dev/null +++ b/src/Api/Billing/Public/Models/OrganizationSubscriptionUpdateRequestModel.cs @@ -0,0 +1,138 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Models.Business; + +namespace Bit.Api.Billing.Public.Models; + +public class OrganizationSubscriptionUpdateRequestModel : IValidatableObject +{ + public PasswordManagerSubscriptionUpdateModel PasswordManager { get; set; } + public SecretsManagerSubscriptionUpdateModel SecretsManager { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if (PasswordManager == null && SecretsManager == null) + { + yield return new ValidationResult("At least one of PasswordManager or SecretsManager must be provided."); + } + + yield return ValidationResult.Success; + } +} + +public class PasswordManagerSubscriptionUpdateModel +{ + public int? Seats { get; set; } + public int? Storage { get; set; } + private int? _maxAutoScaleSeats; + public int? MaxAutoScaleSeats + { + get { return _maxAutoScaleSeats; } + set { _maxAutoScaleSeats = value < 0 ? null : value; } + } + + public virtual void ToPasswordManagerSubscriptionUpdate(Organization organization) + { + UpdateMaxAutoScaleSeats(organization); + + UpdateSeats(organization); + + UpdateStorage(organization); + } + + private void UpdateMaxAutoScaleSeats(Organization organization) + { + MaxAutoScaleSeats ??= organization.MaxAutoscaleSeats; + } + + private void UpdateSeats(Organization organization) + { + if (Seats is > 0) + { + if (organization.Seats.HasValue) + { + Seats = Seats.Value - organization.Seats.Value; + } + } + else + { + Seats = 0; + } + } + + private void UpdateStorage(Organization organization) + { + if (Storage is > 0) + { + if (organization.MaxStorageGb.HasValue) + { + Storage = (short?)(Storage - organization.MaxStorageGb.Value); + } + } + else + { + Storage = null; + } + } +} + +public class SecretsManagerSubscriptionUpdateModel +{ + public int? Seats { get; set; } + private int? _maxAutoScaleSeats; + public int? MaxAutoScaleSeats + { + get { return _maxAutoScaleSeats; } + set { _maxAutoScaleSeats = value < 0 ? null : value; } + } + public int? ServiceAccounts { get; set; } + private int? _maxAutoScaleServiceAccounts; + public int? MaxAutoScaleServiceAccounts + { + get { return _maxAutoScaleServiceAccounts; } + set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; } + } + + public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization) + { + var update = UpdateUpdateMaxAutoScale(organization); + UpdateSeats(organization, update); + UpdateServiceAccounts(organization, update); + return update; + } + + private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization) + { + var update = new SecretsManagerSubscriptionUpdate(organization, false) + { + MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats, + MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts + }; + return update; + } + + private void UpdateSeats(Organization organization, SecretsManagerSubscriptionUpdate update) + { + if (Seats is > 0) + { + if (organization.SmSeats.HasValue) + { + Seats = Seats.Value - organization.SmSeats.Value; + + } + update.AdjustSeats(Seats.Value); + } + } + + private void UpdateServiceAccounts(Organization organization, SecretsManagerSubscriptionUpdate update) + { + if (ServiceAccounts is > 0) + { + if (organization.SmServiceAccounts.HasValue) + { + ServiceAccounts = ServiceAccounts.Value - organization.SmServiceAccounts.Value; + } + update.AdjustServiceAccounts(ServiceAccounts.Value); + } + } +}