mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[AC-1218] Add ability to delete Provider Portals (#3973)
* add new classes * initial commit * revert the changes on this files Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * revert unnecessary changes * Add a model * add the delete token endpoint * add a unit test for delete provider Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * add the delete provider method Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve the delete request redirect issue Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * changes to correct the json issue Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve errors Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve pr comment Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * move ProviderDeleteTokenable to the adminConsole Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * Add feature flag * resolve pr comments Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * add some unit test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve the failing test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * resolve test Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * add the remove feature flag Signed-off-by: Cy Okeke <cokeke@bitwarden.com> * [AC-2378] Added `ProviderId` to PayPal transaction model (#3995) * Added ProviderId to PayPal transaction model * Fixed issue with parsing provider id * [AC-1923] Add endpoint to create client organization (#3977) * Add new endpoint for creating client organizations in consolidated billing * Create empty org and then assign seats for code re-use * Fixes made from debugging client side * few more small fixes * Vincent's feedback * Bumped version to 2024.4.1 (#3997) * [AC-1923] Add endpoint to create client organization (#3977) * Add new endpoint for creating client organizations in consolidated billing * Create empty org and then assign seats for code re-use * Fixes made from debugging client side * few more small fixes * Vincent's feedback * [AC-1923] Add endpoint to create client organization (#3977) * Add new endpoint for creating client organizations in consolidated billing * Create empty org and then assign seats for code re-use * Fixes made from debugging client side * few more small fixes * Vincent's feedback * add changes after merge conflict Signed-off-by: Cy Okeke <cokeke@bitwarden.com> --------- Signed-off-by: Cy Okeke <cokeke@bitwarden.com> Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Co-authored-by: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Co-authored-by: Bitwarden DevOps <106330231+bitwarden-devops-bot@users.noreply.github.com>
This commit is contained in:
parent
ddbb031bcb
commit
6672019122
@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
@ -15,6 +16,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Stripe;
|
||||
@ -39,13 +41,17 @@ public class ProviderService : IProviderService
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IDataProtectorTokenFactory<ProviderDeleteTokenable> _providerDeleteTokenDataFactory;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
|
||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||
IUserService userService, IOrganizationService organizationService, IMailService mailService,
|
||||
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
|
||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService)
|
||||
ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService,
|
||||
IDataProtectorTokenFactory<ProviderDeleteTokenable> providerDeleteTokenDataFactory,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
{
|
||||
_providerRepository = providerRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
@ -61,6 +67,8 @@ public class ProviderService : IProviderService
|
||||
_currentContext = currentContext;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_featureService = featureService;
|
||||
_providerDeleteTokenDataFactory = providerDeleteTokenDataFactory;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
}
|
||||
|
||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
||||
@ -601,6 +609,44 @@ public class ProviderService : IProviderService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InitiateDeleteAsync(Provider provider, string providerAdminEmail)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(provider.Name))
|
||||
{
|
||||
throw new BadRequestException("Provider name not found.");
|
||||
}
|
||||
var providerAdmin = await _userRepository.GetByEmailAsync(providerAdminEmail);
|
||||
if (providerAdmin == null)
|
||||
{
|
||||
throw new BadRequestException("Provider admin not found.");
|
||||
}
|
||||
|
||||
var providerAdminOrgUser = await _providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id);
|
||||
if (providerAdminOrgUser == null || providerAdminOrgUser.Status != ProviderUserStatusType.Confirmed ||
|
||||
providerAdminOrgUser.Type != ProviderUserType.ProviderAdmin)
|
||||
{
|
||||
throw new BadRequestException("Org admin not found.");
|
||||
}
|
||||
|
||||
var token = _providerDeleteTokenDataFactory.Protect(new ProviderDeleteTokenable(provider, 1));
|
||||
await _mailService.SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, token);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Provider provider, string token)
|
||||
{
|
||||
if (!_providerDeleteTokenDataFactory.TryUnprotect(token, out var data) || !data.IsValid(provider))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
await DeleteAsync(provider);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Provider provider)
|
||||
{
|
||||
await _providerRepository.DeleteAsync(provider);
|
||||
await _applicationCacheService.DeleteProviderAbilityAsync(provider.Id);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(ProviderUser providerUser, Provider provider)
|
||||
{
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
@ -14,6 +15,7 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -745,6 +747,95 @@ public class ProviderServiceTests
|
||||
t.First().Item2 == null));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Delete_Success(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(provider);
|
||||
|
||||
await providerRepository.Received().DeleteAsync(provider);
|
||||
await applicationCacheService.Received().DeleteProviderAbilityAsync(provider.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderNameIsEmpty(string providerAdminEmail, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var provider = new Provider { Name = "" };
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminNotFound(Provider provider, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerAdminEmail = "nonexistent@example.com";
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(null));
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateDeleteAsync_ThrowsBadRequestException_WhenProviderAdminStatusIsNotConfirmed(
|
||||
Provider provider
|
||||
, User providerAdmin
|
||||
, ProviderUser providerUser
|
||||
, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerAdminEmail = "nonexistent@example.com";
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
providerUser.Type = ProviderUserType.ServiceUser;
|
||||
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail));
|
||||
Assert.Contains("Org admin not found.", exception.Message);
|
||||
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task InitiateDeleteAsync_SendsInitiateDeleteProviderEmail(Provider provider, User providerAdmin
|
||||
, ProviderUser providerUser, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerAdminEmail = providerAdmin.Email;
|
||||
providerUser.Status = ProviderUserStatusType.Confirmed;
|
||||
providerUser.Type = ProviderUserType.ProviderAdmin;
|
||||
|
||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||
userRepository.GetByEmailAsync(providerAdminEmail).Returns(Task.FromResult<User>(providerAdmin));
|
||||
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
|
||||
providerUserRepository.GetByProviderUserAsync(provider.Id, providerAdmin.Id).Returns(providerUser);
|
||||
var mailService = sutProvider.GetDependency<IMailService>();
|
||||
|
||||
await sutProvider.Sut.InitiateDeleteAsync(provider, providerAdminEmail);
|
||||
await mailService.Received().SendInitiateDeletProviderEmailAsync(providerAdminEmail, provider, Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidToken(Provider provider, string invalidToken
|
||||
, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
|
||||
providerDeleteTokenDataFactory.TryUnprotect(invalidToken, out Arg.Any<ProviderDeleteTokenable>()).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, invalidToken));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_ThrowsBadRequestException_WhenInvalidTokenData(Provider provider, string validToken
|
||||
, SutProvider<ProviderService> sutProvider)
|
||||
{
|
||||
var validTokenData = new ProviderDeleteTokenable();
|
||||
var providerDeleteTokenDataFactory = sutProvider.GetDependency<IDataProtectorTokenFactory<ProviderDeleteTokenable>>();
|
||||
providerDeleteTokenDataFactory.TryUnprotect(validToken, out validTokenData).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAsync(provider, validToken));
|
||||
}
|
||||
|
||||
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
||||
new()
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Utilities;
|
||||
@ -10,6 +11,7 @@ using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -275,4 +277,64 @@ public class ProvidersController : Controller
|
||||
|
||||
return RedirectToAction("Edit", "Providers", new { id = providerId });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> Delete(Guid id, string providerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerName))
|
||||
{
|
||||
return BadRequest("Invalid provider name");
|
||||
}
|
||||
|
||||
var providerOrganizations = await _providerOrganizationRepository.GetManyDetailsByProviderAsync(id);
|
||||
|
||||
if (providerOrganizations.Count > 0)
|
||||
{
|
||||
return BadRequest("You must unlink all clients before you can delete a provider");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
|
||||
if (provider is null)
|
||||
{
|
||||
return BadRequest("Provider does not exist");
|
||||
}
|
||||
|
||||
if (!string.Equals(providerName.Trim(), provider.Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest("Invalid provider name");
|
||||
}
|
||||
|
||||
await _providerService.DeleteAsync(provider);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
public async Task<IActionResult> DeleteInitiation(Guid id, string providerEmail)
|
||||
{
|
||||
var emailAttribute = new EmailAddressAttribute();
|
||||
if (!emailAttribute.IsValid(providerEmail))
|
||||
{
|
||||
return BadRequest("Invalid provider admin email");
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _providerService.InitiateDeleteAsync(provider, providerEmail);
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
|
||||
|
||||
var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
|
||||
@ -62,10 +61,89 @@
|
||||
}
|
||||
</form>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
@if (canEdit)
|
||||
{
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
</div>
|
||||
}
|
||||
@if (canEdit)
|
||||
{
|
||||
<!-- Modals -->
|
||||
<div class="modal fade rounded" id="requestDeletionModal" tabindex="-1" aria-labelledby="requestDeletionModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content rounded">
|
||||
<div class="p-3">
|
||||
<h3 class="font-weight-bolder" id="exampleModalLabel">Request provider deletion</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="font-weight-light">
|
||||
Enter the email of the provider admin that will receive the request to delete the provider portal.
|
||||
</span>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="provider-email" class="col-form-label">Provider email</label>
|
||||
<input type="email" class="form-control" id="provider-email">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-pill" onclick="initiateDeleteProvider('@Model.Provider.Id')">Send email request</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="DeleteModal" tabindex="-1" aria-labelledby="DeleteModal" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content rounded">
|
||||
<div class="p-3">
|
||||
<h3 class="font-weight-bolder" id="exampleModalLabel">Delete provider</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="font-weight-light">
|
||||
This action is permanent and irreversible. Enter the provide name to complete deletion of the provider and associated data.
|
||||
</span>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="provider-name" class="col-form-label">Provider name</label>
|
||||
<input type="text" class="form-control" id="provider-name">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="linkedWarningModal" tabindex="-1" role="dialog" aria-labelledby="linkedWarningModal" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content rounded">
|
||||
<div class="modal-body">
|
||||
<h4 class="font-weight-bolder">Cannot Delete @Model.Name</h4>
|
||||
<p class="font-weight-lighter">you must unlink all clients before deleting @Model.Name</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-primary btn-pill" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary btn-pill" data-dismiss="modal">Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- End of Modal Section -->
|
||||
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider))
|
||||
{
|
||||
<div class="ml-auto d-flex">
|
||||
<button class="btn btn-danger" onclick="openRequestDeleteModal(@Model.ProviderOrganizations.Count())">Request Delete</button>
|
||||
<button id="requestDeletionBtn" hidden="hidden" data-toggle="modal" data-target="#requestDeletionModal"></button>
|
||||
|
||||
<button class="btn btn-outline-danger ml-2" onclick="openDeleteModal(@Model.ProviderOrganizations.Count())">Delete</button>
|
||||
<button id="deleteBtn" hidden="hidden" data-toggle="modal" data-target="#DeleteModal"></button>
|
||||
|
||||
<button id="linkAccWarningBtn" hidden="hidden" data-toggle="modal" data-target="#linkedWarningModal"></button>
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
|
@ -17,4 +17,60 @@
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteProvider(id) {
|
||||
const providerName = $('#DeleteModal input#provider-name').val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${providerName}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function () {
|
||||
$('#DeleteModal').modal('hide');
|
||||
window.location.href = `@Url.Action("Index", "Providers")`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!: " + response.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initiateDeleteProvider(id) {
|
||||
const email = $('#requestDeletionModal input#provider-email').val();
|
||||
const providerEmail = encodeURIComponent(email);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `@Url.Action("DeleteInitiation", "Providers")?id=${id}&providerEmail=${providerEmail}`,
|
||||
dataType: 'json',
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function () {
|
||||
$('#requestDeletionModal').modal('hide');
|
||||
window.location.href = `@Url.Action("Index", "Providers")`;
|
||||
},
|
||||
error: function (response) {
|
||||
alert("Error!: " + response.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openDeleteModal(providerOrganizations) {
|
||||
|
||||
if (providerOrganizations > 0){
|
||||
$('#linkAccWarningBtn').click()
|
||||
} else {
|
||||
$('#deleteBtn').click()
|
||||
}
|
||||
}
|
||||
|
||||
function openRequestDeleteModal(providerOrganizations) {
|
||||
|
||||
if (providerOrganizations > 0){
|
||||
$('#linkAccWarningBtn').click()
|
||||
} else {
|
||||
$('#requestDeletionBtn').click()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -123,4 +123,40 @@ public class ProvidersController : Controller
|
||||
|
||||
return new ProviderResponseModel(response);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete-recover-token")]
|
||||
[AllowAnonymous]
|
||||
public async Task PostDeleteRecoverToken(Guid id, [FromBody] ProviderVerifyDeleteRecoverRequestModel model)
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await _providerService.DeleteAsync(provider, model.Token);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid id)
|
||||
{
|
||||
if (!_currentContext.ProviderProviderAdmin(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByIdAsync(id);
|
||||
if (provider == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
await _providerService.DeleteAsync(provider);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
|
||||
public class ProviderVerifyDeleteRecoverRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Token { get; set; }
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
|
||||
public class ProviderDeleteTokenable : Tokens.ExpiringTokenable
|
||||
{
|
||||
public const string ClearTextPrefix = "BwProviderId";
|
||||
public const string DataProtectorPurpose = "ProviderDeleteDataProtector";
|
||||
public const string TokenIdentifier = "ProviderDelete";
|
||||
public string Identifier { get; set; } = TokenIdentifier;
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
public ProviderDeleteTokenable()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public ProviderDeleteTokenable(DateTime expirationDate)
|
||||
{
|
||||
ExpirationDate = expirationDate;
|
||||
}
|
||||
|
||||
public ProviderDeleteTokenable(Entities.Provider.Provider provider, int hoursTillExpiration)
|
||||
{
|
||||
Id = provider.Id;
|
||||
ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration);
|
||||
}
|
||||
|
||||
public bool IsValid(Entities.Provider.Provider provider)
|
||||
{
|
||||
return Id == provider.Id;
|
||||
}
|
||||
|
||||
protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default;
|
||||
}
|
@ -26,5 +26,8 @@ public interface IProviderService
|
||||
Task LogProviderAccessToOrganizationAsync(Guid organizationId);
|
||||
Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid ownerId);
|
||||
Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail);
|
||||
Task InitiateDeleteAsync(Provider provider, string providerAdminEmail);
|
||||
Task DeleteAsync(Provider provider, string token);
|
||||
Task DeleteAsync(Provider provider);
|
||||
}
|
||||
|
||||
|
@ -35,4 +35,7 @@ public class NoopProviderService : IProviderService
|
||||
|
||||
public Task ResendProviderSetupInviteEmailAsync(Guid providerId, Guid userId) => throw new NotImplementedException();
|
||||
public Task SendProviderSetupInviteEmailAsync(Provider provider, string ownerEmail) => throw new NotImplementedException();
|
||||
public Task InitiateDeleteAsync(Provider provider, string providerAdminEmail) => throw new NotImplementedException();
|
||||
public Task DeleteAsync(Provider provider, string token) => throw new NotImplementedException();
|
||||
public Task DeleteAsync(Provider provider) => throw new NotImplementedException();
|
||||
}
|
||||
|
@ -128,11 +128,12 @@ public static class FeatureFlagKeys
|
||||
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
|
||||
public const string PM5766AutomaticTax = "PM-5766-automatic-tax";
|
||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||
public const string UnassignedItemsBanner = "unassigned-items-banner";
|
||||
public const string EnableDeleteProvider = "AC-1218-delete-provider";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -3,5 +3,6 @@
|
||||
public enum ApplicationCacheMessageType : byte
|
||||
{
|
||||
UpsertOrganizationAbility = 0,
|
||||
DeleteOrganizationAbility = 1
|
||||
DeleteOrganizationAbility = 1,
|
||||
DeleteProviderAbility = 2,
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
{{#>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">
|
||||
We recently received your request to permanently delete the following Bitwarden provider:
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; 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;" valign="top">
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Name:</b> {{ProviderName}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">ID:</b> {{ProviderId}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Created:</b> {{ProviderCreationDate}} at {{ProviderCreationTime}} {{TimeZone}}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
<b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">Billing email address:</b> {{ProviderBillingEmail}}
|
||||
</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: left;" valign="top" align="center">
|
||||
Click the link below to delete your Bitwarden provider.
|
||||
</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 last" 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; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||
If you did not request this email to delete your Bitwarden provider, you can safely ignore it.
|
||||
<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;">
|
||||
Delete Your Provider
|
||||
</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,15 @@
|
||||
{{#>BasicTextLayout}}
|
||||
We recently received your request to permanently delete the following Bitwarden provider:
|
||||
|
||||
- Name: {{ProviderName}}
|
||||
- ID: {{ProviderId}}
|
||||
- Created: {{ProviderCreationDate}} at {{ProviderCreationTime}} {{TimeZone}}
|
||||
- Billing email address: {{ProviderBillingEmail}}
|
||||
|
||||
Click the link below to complete the deletion of your provider.
|
||||
|
||||
If you did not request this email to delete your Bitwarden provider, you can safely ignore it.
|
||||
|
||||
{{{Url}}}
|
||||
|
||||
{{/BasicTextLayout}}
|
21
src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs
Normal file
21
src/Core/Models/Mail/Provider/ProviderInitiateDeleteModel.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace Bit.Core.Models.Mail.Provider;
|
||||
|
||||
public class ProviderInitiateDeleteModel : BaseMailModel
|
||||
{
|
||||
public string Url => string.Format("{0}/verify-recover-delete-provider?providerId={1}&token={2}&name={3}",
|
||||
WebVaultUrl,
|
||||
ProviderId,
|
||||
Token,
|
||||
ProviderNameUrlEncoded);
|
||||
|
||||
public string WebVaultUrl { get; set; }
|
||||
public string Token { get; set; }
|
||||
public Guid ProviderId { get; set; }
|
||||
public string SiteName { get; set; }
|
||||
public string ProviderName { get; set; }
|
||||
public string ProviderNameUrlEncoded { get; set; }
|
||||
public string ProviderBillingEmail { get; set; }
|
||||
public string ProviderCreationDate { get; set; }
|
||||
public string ProviderCreationTime { get; set; }
|
||||
public string TimeZone { get; set; }
|
||||
}
|
@ -15,4 +15,5 @@ public interface IApplicationCacheService
|
||||
Task UpsertOrganizationAbilityAsync(Organization organization);
|
||||
Task UpsertProviderAbilityAsync(Provider provider);
|
||||
Task DeleteOrganizationAbilityAsync(Guid organizationId);
|
||||
Task DeleteProviderAbilityAsync(Guid providerId);
|
||||
}
|
||||
|
@ -78,5 +78,6 @@ public interface IMailService
|
||||
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
|
||||
Task SendTrialInitiationEmailAsync(string email);
|
||||
Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token);
|
||||
}
|
||||
|
||||
|
@ -267,6 +267,28 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token)
|
||||
{
|
||||
var message = CreateDefaultMessage("Request to Delete Your Provider", email);
|
||||
var model = new ProviderInitiateDeleteModel
|
||||
{
|
||||
Token = WebUtility.UrlEncode(token),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
ProviderId = provider.Id,
|
||||
ProviderName = CoreHelpers.SanitizeForEmail(provider.DisplayName(), false),
|
||||
ProviderNameUrlEncoded = WebUtility.UrlEncode(provider.Name),
|
||||
ProviderBillingEmail = provider.BillingEmail,
|
||||
ProviderCreationDate = provider.CreationDate.ToLongDateString(),
|
||||
ProviderCreationTime = provider.CreationDate.ToShortTimeString(),
|
||||
TimeZone = _utcTimeZoneDisplay,
|
||||
};
|
||||
await AddMessageContentAsync(message, "Provider.InitiateDeleteProvider", model);
|
||||
message.MetaData.Add("SendGridBypassListManagement", true);
|
||||
message.Category = "InitiateDeleteProvider";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("[Admin] Continue Logging In", email);
|
||||
|
@ -85,6 +85,16 @@ public class InMemoryApplicationCacheService : IApplicationCacheService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public virtual Task DeleteProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
if (_providerAbilities != null && _providerAbilities.ContainsKey(providerId))
|
||||
{
|
||||
_providerAbilities.Remove(providerId);
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
private async Task InitOrganizationAbilitiesAsync()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
@ -64,4 +64,19 @@ public class InMemoryServiceBusApplicationCacheService : InMemoryApplicationCach
|
||||
{
|
||||
await base.DeleteOrganizationAbilityAsync(organizationId);
|
||||
}
|
||||
|
||||
public override async Task DeleteProviderAbilityAsync(Guid providerId)
|
||||
{
|
||||
await base.DeleteProviderAbilityAsync(providerId);
|
||||
var message = new ServiceBusMessage
|
||||
{
|
||||
Subject = _subName,
|
||||
ApplicationProperties =
|
||||
{
|
||||
{ "type", (byte)ApplicationCacheMessageType.DeleteProviderAbility },
|
||||
{ "id", providerId },
|
||||
}
|
||||
};
|
||||
var task = _topicMessageSender.SendMessageAsync(message);
|
||||
}
|
||||
}
|
||||
|
@ -268,5 +268,7 @@ public class NoopMailService : IMailService
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendInitiateDeletProviderEmailAsync(string email, Provider provider, string token) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
@ -203,6 +204,14 @@ public static class ServiceCollectionExtensions
|
||||
DuoUserStateTokenable.DataProtectorPurpose,
|
||||
serviceProvider.GetDataProtectionProvider(),
|
||||
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<DuoUserStateTokenable>>>()));
|
||||
|
||||
services.AddSingleton<IDataProtectorTokenFactory<ProviderDeleteTokenable>>(serviceProvider =>
|
||||
new DataProtectorTokenFactory<ProviderDeleteTokenable>(
|
||||
ProviderDeleteTokenable.ClearTextPrefix,
|
||||
ProviderDeleteTokenable.DataProtectorPurpose,
|
||||
serviceProvider.GetDataProtectionProvider(),
|
||||
serviceProvider.GetRequiredService<ILogger<DataProtectorTokenFactory<ProviderDeleteTokenable>>>())
|
||||
);
|
||||
}
|
||||
|
||||
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
|
Loading…
Reference in New Issue
Block a user