1
0
mirror of https://github.com/bitwarden/server synced 2025-12-25 20:53:16 +00:00

[AC-1758] Implement RemoveOrganizationFromProviderCommand (#3515)

* Add RemovePaymentMethod to StripePaymentService

* Add SendProviderUpdatePaymentMethod to HandlebarsMailService

* Add RemoveOrganizationFromProviderCommand

* Use RemoveOrganizationFromProviderCommand in ProviderOrganizationController

* Remove RemoveOrganizationAsync from ProviderService

* Add RemoveOrganizationFromProviderCommandTests

* PR review feedback and refactoring

* Remove RemovePaymentMethod from StripePaymentService

* Review feedback

* Add Organization RisksSubscriptionFailure endpoint

* fix build error

* Review feedback

* [AC-1359] Bitwarden Portal Unlink Provider Buttons (#3588)

* Added ability to unlink organization from provider from provider edit page

* Refreshing provider edit page after removing an org

* Added button to organization to remove the org from the provider

* Updated based on product feedback

* Removed organization name from alert message

* Temporary logging

* Remove coupon from Stripe org after disconnected from MSP

* Updated test

* Change payment terms on org disconnect from MSP

* Set Stripe account email to new billing email

* Remove logging

---------

Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com>
Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com>
This commit is contained in:
Alex Morask
2024-01-12 10:38:47 -05:00
committed by GitHub
parent 505508a416
commit 95139def0f
35 changed files with 1168 additions and 119 deletions

View File

@@ -3,7 +3,9 @@ using Bit.Admin.Models;
using Bit.Admin.Services;
using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -48,6 +50,9 @@ public class OrganizationsController : Controller
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
public OrganizationsController(
IOrganizationService organizationService,
@@ -71,7 +76,10 @@ public class OrganizationsController : Controller
ICurrentContext currentContext,
ISecretRepository secretRepository,
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
IServiceAccountRepository serviceAccountRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IRemovePaymentMethodCommand removePaymentMethodCommand)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
@@ -95,6 +103,9 @@ public class OrganizationsController : Controller
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_removePaymentMethodCommand = removePaymentMethodCommand;
}
[RequirePermission(Permission.Org_List_View)]
@@ -286,6 +297,38 @@ public class OrganizationsController : Controller
return Json(null);
}
[HttpPost]
[RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> UnlinkOrganizationFromProviderAsync(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization is null)
{
return RedirectToAction("Index");
}
var provider = await _providerRepository.GetByOrganizationIdAsync(id);
if (provider is null)
{
return RedirectToAction("Edit", new { id });
}
var providerOrganization = await _providerOrganizationRepository.GetByOrganizationId(id);
if (providerOrganization is null)
{
return RedirectToAction("Edit", new { id });
}
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
provider,
providerOrganization,
organization);
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
return Json(null);
}
private async Task<Organization> GetOrganization(Guid id, OrganizationEditModel model)
{
var organization = await _organizationRepository.GetByIdAsync(id);

View File

@@ -0,0 +1,67 @@
using Bit.Admin.Enums;
using Bit.Admin.Utilities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Commands;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Admin.Controllers;
[Authorize]
[SelfHosted(NotSelfHostedOnly = true)]
public class ProviderOrganizationsController : Controller
{
private readonly IProviderRepository _providerRepository;
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand;
public ProviderOrganizationsController(IProviderRepository providerRepository,
IProviderOrganizationRepository providerOrganizationRepository,
IOrganizationRepository organizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IRemovePaymentMethodCommand removePaymentMethodCommand)
{
_providerRepository = providerRepository;
_providerOrganizationRepository = providerOrganizationRepository;
_organizationRepository = organizationRepository;
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_removePaymentMethodCommand = removePaymentMethodCommand;
}
[HttpPost]
[RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> DeleteAsync(Guid providerId, Guid id)
{
var provider = await _providerRepository.GetByIdAsync(providerId);
if (provider is null)
{
return RedirectToAction("Index", "Providers");
}
var providerOrganization = await _providerOrganizationRepository.GetByIdAsync(id);
if (providerOrganization is null)
{
return RedirectToAction("View", "Providers", new { id = providerId });
}
var organization = await _organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
if (organization == null)
{
return RedirectToAction("View", "Providers", new { id = providerId });
}
await _removeOrganizationFromProviderCommand.RemoveOrganizationFromProvider(
provider,
providerOrganization,
organization);
await _removePaymentMethodCommand.RemovePaymentMethod(organization);
return Json(null);
}
}

View File

@@ -9,6 +9,7 @@ using Stripe;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Admin.Services;
using Bit.Core.Billing.Extensions;
#if !OSS
using Bit.Commercial.Core.Utilities;
@@ -87,6 +88,7 @@ public class Startup
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddScoped<IAccessControlService, AccessControlService>();
services.AddBillingCommands();
#if OSS
services.AddOosServices();

View File

@@ -8,6 +8,7 @@
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
}
@section Scripts {
@@ -81,7 +82,7 @@
<div class="d-flex mt-4">
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
<div class="ml-auto d-flex">
@if (canInitiateTrial)
@if (canInitiateTrial && Model.Provider is null)
{
<button class="btn btn-secondary mr-2" type="button" id="teams-trial">
Teams Trial
@@ -90,6 +91,15 @@
Enterprise Trial
</button>
}
@if (canUnlinkFromProvider && Model.Provider is not null)
{
<button
class="btn btn-outline-danger mr-2"
onclick="return unlinkProvider('@Model.Organization.Id');"
>
Unlink provider
</button>
}
@if (canDelete)
{
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"

View File

@@ -1,7 +1,15 @@
@using Bit.Core.AdminConsole.Enums.Provider
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using Bit.Admin.Enums
@inject Bit.Admin.Services.IAccessControlService AccessControlService
@model ProviderViewModel
@{
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
}
@await Html.PartialAsync("_ProviderScripts")
@await Html.PartialAsync("_ProviderOrganizationScripts")
<h2>Provider Organizations</h2>
<div class="row">
@@ -32,26 +40,28 @@
}
else
{
@foreach (var org in Model.ProviderOrganizations)
@foreach (var providerOrganization in Model.ProviderOrganizations)
{
<tr>
<td class="align-middle">
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@org.OrganizationId">@org.OrganizationName</a>
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a>
</td>
<td>
@org.Status
@providerOrganization.Status
</td>
<td>
<div class="float-right">
@if (org.Status == OrganizationStatusType.Pending)
@if (canUnlinkFromProvider)
{
<a href="#" class="float-right" onclick="return resendOwnerInvite('@org.OrganizationId');">
<i class="fa fa-envelope-o fa-lg" title="Resend Setup Invite"></i>
<a href="#" class="text-danger float-right" onclick="return unlinkProvider('@Model.Provider.Id', '@providerOrganization.Id');">
Unlink provider
</a>
}
else
@if (providerOrganization.Status == OrganizationStatusType.Pending)
{
<i class="fa fa-envelope-o fa-lg text-secondary"></i>
<a href="#" class="float-right mr-3" onclick="return resendOwnerInvite('@providerOrganization.OrganizationId');">
Resend invitation
</a>
}
</div>
</td>

View File

@@ -0,0 +1,21 @@
<script>
function unlinkProvider(providerId, id) {
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
$.ajax({
type: "POST",
url: `@Url.Action("Delete", "ProviderOrganizations")?providerId=${providerId}&id=${id}`,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
alert("Successfully unlinked provider");
window.location.href = `@Url.Action("Edit", "Providers")?id=${providerId}`;
},
error: function (response) {
alert("Error!");
}
});
}
return false;
}
</script>

View File

@@ -113,6 +113,26 @@
}
}
function unlinkProvider(id) {
if (confirm('Are you sure you want to unlink this organization from its provider?')) {
$.ajax({
type: "POST",
url: `@Url.Action("UnlinkOrganizationFromProvider", "Organizations")?id=${id}`,
dataType: 'json',
contentType: false,
processData: false,
success: function (response) {
alert("Successfully unlinked provider");
window.location.href = `@Url.Action("Edit", "Organizations")?id=${id}`;
},
error: function (response) {
alert("Error!");
}
});
}
return false;
}
/***
* Set Secrets Manager values based on current usage (for migrating from SM beta or reinstating an old subscription)
*/