1
0
mirror of https://github.com/bitwarden/server synced 2026-01-28 15:23:38 +00:00
Files
server/src/Admin/Controllers/ToolsController.cs
Kyle Denney 99e1326039 [PM-24616] refactor stripe adapter (#6527)
* move billing services+tests to billing namespaces

* reorganized methods in file and added comment headers

* renamed StripeAdapter methods for better clarity

* clean up redundant qualifiers

* Upgrade Stripe.net to v48.4.0

* Update PreviewTaxAmountCommand

* Remove unused UpcomingInvoiceOptionExtensions

* Added SubscriptionExtensions with GetCurrentPeriodEnd

* Update PremiumUserBillingService

* Update OrganizationBillingService

* Update GetOrganizationWarningsQuery

* Update BillingHistoryInfo

* Update SubscriptionInfo

* Remove unused Sql Billing folder

* Update StripeAdapter

* Update StripePaymentService

* Update InvoiceCreatedHandler

* Update PaymentFailedHandler

* Update PaymentSucceededHandler

* Update ProviderEventService

* Update StripeEventUtilityService

* Update SubscriptionDeletedHandler

* Update SubscriptionUpdatedHandler

* Update UpcomingInvoiceHandler

* Update ProviderSubscriptionResponse

* Remove unused Stripe Subscriptions Admin Tool

* Update RemoveOrganizationFromProviderCommand

* Update ProviderBillingService

* Update RemoveOrganizatinoFromProviderCommandTests

* Update PreviewTaxAmountCommandTests

* Update GetCloudOrganizationLicenseQueryTests

* Update GetOrganizationWarningsQueryTests

* Update StripePaymentServiceTests

* Update ProviderBillingControllerTests

* Update ProviderEventServiceTests

* Update SubscriptionDeletedHandlerTests

* Update SubscriptionUpdatedHandlerTests

* Resolve Billing test failures

I completely removed tests for the StripeEventService as they were using a system I setup a while back that read JSON files of the Stripe event structure. I did not anticipate how frequently these structures would change with each API version and the cost of trying to update these specific JSON files to test a very static data retrieval service far outweigh the benefit.

* Resolve Core test failures

* Run dotnet format

* Remove unused provider migration

* Fixed failing tests

* Run dotnet format

* Replace the old webhook secret key with new one (#6223)

* Fix compilation failures in additions

* Run dotnet format

* Bump Stripe API version

* Fix recent addition: CreatePremiumCloudHostedSubscriptionCommand

* Fix new code in main according to Stripe update

* Fix InvoiceExtensions

* Bump SDK version to match API Version

* cleanup

* fixing items missed after the merge

* use expression body for all simple returns

* forgot fixes, format, and pr feedback

* claude pr feedback

* pr feedback and cleanup

* more claude feedback

---------

Co-authored-by: Alex Morask <amorask@bitwarden.com>
Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com>
2025-12-12 15:32:43 -06:00

341 lines
12 KiB
C#

// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json;
using Bit.Admin.Enums;
using Bit.Admin.Models;
using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Entities;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Admin.Controllers;
[Authorize]
[SelfHosted(NotSelfHostedOnly = true)]
public class ToolsController : Controller
{
private readonly GlobalSettings _globalSettings;
private readonly IOrganizationRepository _organizationRepository;
private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;
private readonly IUserService _userService;
private readonly ITransactionRepository _transactionRepository;
private readonly IInstallationRepository _installationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IProviderUserRepository _providerUserRepository;
private readonly IStripeAdapter _stripeAdapter;
private readonly IWebHostEnvironment _environment;
public ToolsController(
GlobalSettings globalSettings,
IOrganizationRepository organizationRepository,
IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,
IUserService userService,
ITransactionRepository transactionRepository,
IInstallationRepository installationRepository,
IOrganizationUserRepository organizationUserRepository,
IProviderUserRepository providerUserRepository,
IStripeAdapter stripeAdapter,
IWebHostEnvironment environment)
{
_globalSettings = globalSettings;
_organizationRepository = organizationRepository;
_getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery;
_userService = userService;
_transactionRepository = transactionRepository;
_installationRepository = installationRepository;
_organizationUserRepository = organizationUserRepository;
_providerUserRepository = providerUserRepository;
_stripeAdapter = stripeAdapter;
_environment = environment;
}
[RequirePermission(Permission.Tools_ChargeBrainTreeCustomer)]
public IActionResult ChargeBraintree()
{
return View(new ChargeBraintreeModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_ChargeBrainTreeCustomer)]
public async Task<IActionResult> ChargeBraintree(ChargeBraintreeModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var btGateway = new Braintree.BraintreeGateway
{
Environment = _globalSettings.Braintree.Production ?
Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX,
MerchantId = _globalSettings.Braintree.MerchantId,
PublicKey = _globalSettings.Braintree.PublicKey,
PrivateKey = _globalSettings.Braintree.PrivateKey
};
var btObjIdField = model.Id[0] == 'o' ? "organization_id" : "user_id";
var btObjId = new Guid(model.Id.Substring(1, 32));
var transactionResult = await btGateway.Transaction.SaleAsync(
new Braintree.TransactionRequest
{
Amount = model.Amount.Value,
CustomerId = model.Id,
Options = new Braintree.TransactionOptionsRequest
{
SubmitForSettlement = true,
PayPal = new Braintree.TransactionOptionsPayPalRequest
{
CustomField = $"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
}
},
CustomFields = new Dictionary<string, string>
{
[btObjIdField] = btObjId.ToString(),
["region"] = _globalSettings.BaseServiceUri.CloudRegion
}
});
if (!transactionResult.IsSuccess())
{
ModelState.AddModelError(string.Empty, "Charge failed. " +
"Refer to Braintree admin portal for more information.");
}
else
{
model.TransactionId = transactionResult.Target.Id;
model.PayPalTransactionId = transactionResult.Target?.PayPalDetails?.CaptureId;
}
return View(model);
}
[RequirePermission(Permission.Tools_CreateEditTransaction)]
public IActionResult CreateTransaction(Guid? organizationId = null, Guid? userId = null)
{
return View("CreateUpdateTransaction", new CreateUpdateTransactionModel
{
OrganizationId = organizationId,
UserId = userId
});
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_CreateEditTransaction)]
public async Task<IActionResult> CreateTransaction(CreateUpdateTransactionModel model)
{
if (!ModelState.IsValid)
{
return View("CreateUpdateTransaction", model);
}
await _transactionRepository.CreateAsync(model.ToTransaction());
if (model.UserId.HasValue)
{
return RedirectToAction("Edit", "Users", new { id = model.UserId });
}
else
{
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId });
}
}
[RequirePermission(Permission.Tools_CreateEditTransaction)]
public async Task<IActionResult> EditTransaction(Guid id)
{
var transaction = await _transactionRepository.GetByIdAsync(id);
if (transaction == null)
{
return RedirectToAction("Index", "Home");
}
return View("CreateUpdateTransaction", new CreateUpdateTransactionModel(transaction));
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_CreateEditTransaction)]
public async Task<IActionResult> EditTransaction(Guid id, CreateUpdateTransactionModel model)
{
if (!ModelState.IsValid)
{
return View("CreateUpdateTransaction", model);
}
await _transactionRepository.ReplaceAsync(model.ToTransaction(id));
if (model.UserId.HasValue)
{
return RedirectToAction("Edit", "Users", new { id = model.UserId });
}
else
{
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId });
}
}
[RequirePermission(Permission.Tools_PromoteAdmin)]
public IActionResult PromoteAdmin()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_PromoteAdmin)]
public async Task<IActionResult> PromoteAdmin(PromoteAdminModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var orgUsers = await _organizationUserRepository.GetManyByOrganizationAsync(
model.OrganizationId.Value, null);
var user = orgUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);
if (user == null)
{
ModelState.AddModelError(nameof(model.UserId), "User Id not found in this organization.");
}
else if (user.Type != Core.Enums.OrganizationUserType.Admin)
{
ModelState.AddModelError(nameof(model.UserId), "User is not an admin of this organization.");
}
if (!ModelState.IsValid)
{
return View(model);
}
user.Type = Core.Enums.OrganizationUserType.Owner;
await _organizationUserRepository.ReplaceAsync(user);
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value });
}
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
public IActionResult PromoteProviderServiceUser()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
public async Task<IActionResult> PromoteProviderServiceUser(PromoteProviderServiceUserModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var providerUsers = await _providerUserRepository.GetManyByProviderAsync(
model.ProviderId.Value, null);
var serviceUser = providerUsers.FirstOrDefault(u => u.UserId == model.UserId.Value);
if (serviceUser == null)
{
ModelState.AddModelError(nameof(model.UserId), "Service User Id not found in this provider.");
}
else if (serviceUser.Type != Core.AdminConsole.Enums.Provider.ProviderUserType.ServiceUser)
{
ModelState.AddModelError(nameof(model.UserId), "User is not a service user of this provider.");
}
if (!ModelState.IsValid)
{
return View(model);
}
serviceUser.Type = Core.AdminConsole.Enums.Provider.ProviderUserType.ProviderAdmin;
await _providerUserRepository.ReplaceAsync(serviceUser);
return RedirectToAction("Edit", "Providers", new { id = model.ProviderId.Value });
}
[RequirePermission(Permission.Tools_GenerateLicenseFile)]
public IActionResult GenerateLicense()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Tools_GenerateLicenseFile)]
public async Task<IActionResult> GenerateLicense(LicenseModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
User user = null;
Organization organization = null;
if (model.UserId.HasValue)
{
user = await _userService.GetUserByIdAsync(model.UserId.Value);
if (user == null)
{
ModelState.AddModelError(nameof(model.UserId), "User Id not found.");
}
}
else if (model.OrganizationId.HasValue)
{
organization = await _organizationRepository.GetByIdAsync(model.OrganizationId.Value);
if (organization == null)
{
ModelState.AddModelError(nameof(model.OrganizationId), "Organization not found.");
}
else if (!organization.Enabled)
{
ModelState.AddModelError(nameof(model.OrganizationId), "Organization is disabled.");
}
}
if (model.InstallationId.HasValue)
{
var installation = await _installationRepository.GetByIdAsync(model.InstallationId.Value);
if (installation == null)
{
ModelState.AddModelError(nameof(model.InstallationId), "Installation not found.");
}
else if (!installation.Enabled)
{
ModelState.AddModelError(nameof(model.OrganizationId), "Installation is disabled.");
}
}
if (!ModelState.IsValid)
{
return View(model);
}
if (organization != null)
{
var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization,
model.InstallationId.Value, model.Version);
var ms = new MemoryStream();
await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);
ms.Seek(0, SeekOrigin.Begin);
return File(ms, "text/plain", "bitwarden_organization_license.json");
}
else if (user != null)
{
var license = await _userService.GenerateLicenseAsync(user, null, model.Version);
var ms = new MemoryStream();
ms.Seek(0, SeekOrigin.Begin);
await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);
ms.Seek(0, SeekOrigin.Begin);
return File(ms, "text/plain", "bitwarden_premium_license.json");
}
else
{
throw new Exception("No license to generate.");
}
}
}