1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-01 13:43:23 +01:00

[PM-11406] Account Management: Prevent a verified user from deleting their account (#4878)

* Add check for managed user before purging account

* Rename IOrganizationRepository.GetByClaimedUserDomainAsync to GetByVerifiedUserEmailDomainAsync and refactor to return a list. Remove ManagedByOrganizationId from ProfileResponseMode. Add ManagesActiveUser to ProfileOrganizationResponseModel

* Rename the property ManagesActiveUser to UserIsManagedByOrganization

* Remove whole class #nullable enable and add it to specific places

* [PM-11405] Account Deprovisioning: Prevent a verified user from changing their email address

* Remove unnecessary .ToList()

* Refactor IUserService methods GetOrganizationsManagingUserAsync and IsManagedByAnyOrganizationAsync to not return nullable objects. Update ProfileOrganizationResponseModel.UserIsManagedByOrganization to not be nullable

* Prevent deletion of accounts managed by an organization when Account Deprovisioning is enabled

* Add CannotDeleteManagedAccountViewModel and email templates

- Added CannotDeleteManagedAccountViewModel class to handle emails related to preventing deletion of accounts managed by an organization.
- Added HTML and text email templates for sending notifications about the inability to delete an account owned by an organization.
- Updated IMailService interface with a new method to send the cannot delete managed account email.
- Implemented the SendCannotDeleteManagedAccountEmailAsync method in HandlebarsMailService.
- Added a check in UserService to send the cannot delete managed account email if the user is managed by any organization.
- Added a no-op implementation for SendCannotDeleteManagedAccountEmailAsync in NoopMailService.

* Update error message when unable to purge vault for managed account

* Update error message when unable to change email for managed account

* Update error message when unable to delete account when managed by organization

* Update error message in test for deleting organization-owned accounts
This commit is contained in:
Rui Tomé 2024-11-04 16:37:21 +00:00 committed by Cy Okeke
parent e0e24db445
commit 0a1238f887
No known key found for this signature in database
GPG Key ID: 88B341B55C84B45C
9 changed files with 88 additions and 0 deletions

View File

@ -580,6 +580,13 @@ public class AccountsController : Controller
}
else
{
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
}
var result = await _userService.DeleteAsync(user);
if (result.Succeeded)
{

View File

@ -0,0 +1,7 @@
using Bit.Core.Models.Mail;
namespace Bit.Core.Auth.Models.Mail;
public class CannotDeleteManagedAccountViewModel : BaseMailModel
{
}

View File

@ -0,0 +1,15 @@
{{#>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">
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
</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">
Please contact your organization administrator for additional details.
<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}}

View File

@ -0,0 +1,6 @@
{{#>BasicTextLayout}}
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
Please contact your organization administrator for additional details.
{{/BasicTextLayout}}

View File

@ -18,6 +18,7 @@ public interface IMailService
ProductTierType productTier,
IEnumerable<ProductType> products);
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
Task SendCannotDeleteManagedAccountEmailAsync(string email);
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
Task SendTwoFactorEmailAsync(string email, string token);

View File

@ -112,6 +112,19 @@ public class HandlebarsMailService : IMailService
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendCannotDeleteManagedAccountEmailAsync(string email)
{
var message = CreateDefaultMessage("Delete Your Account", email);
var model = new CannotDeleteManagedAccountViewModel
{
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};
await AddMessageContentAsync(message, "AdminConsole.CannotDeleteManagedAccount", model);
message.Category = "CannotDeleteManagedAccount";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
{
var message = CreateDefaultMessage("Your Email Change", toEmail);

View File

@ -297,6 +297,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
return;
}
if (await IsManagedByAnyOrganizationAsync(user.Id))
{
await _mailService.SendCannotDeleteManagedAccountEmailAsync(user.Email);
return;
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
}

View File

@ -94,6 +94,11 @@ public class NoopMailService : IMailService
return Task.FromResult(0);
}
public Task SendCannotDeleteManagedAccountEmailAsync(string email)
{
return Task.FromResult(0);
}
public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
{
return Task.FromResult(0);

View File

@ -534,6 +534,34 @@ public class AccountsControllerTests : IDisposable
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));
}
[Fact]
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
Assert.Equal("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
}
[Fact]
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed()
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
ConfigureUserServiceToAcceptPasswordFor(user);
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
_userService.DeleteAsync(user).Returns(IdentityResult.Success);
await _sut.Delete(new SecretVerificationRequestModel());
await _userService.Received(1).DeleteAsync(user);
}
// Below are helper functions that currently belong to this
// test class, but ultimately may need to be split out into