Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-7004] Org Admin Initiate Delete #3905

Merged
merged 28 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3f967e7
org delete
kspearrin Mar 15, 2024
877ad24
Merge branch 'main' into orgdelete
kspearrin Mar 15, 2024
c31a35c
move org id to URL path
kspearrin Mar 22, 2024
9ab2986
tweaks
kspearrin Mar 22, 2024
b86484f
Merge branch 'main' into orgdelete
kspearrin Mar 22, 2024
e636056
lint fixes
kspearrin Mar 22, 2024
1cc554b
Merge branch 'orgdelete' of github.com:bitwarden/server into orgdelete
kspearrin Mar 22, 2024
8bd450e
Update src/Core/Services/Implementations/HandlebarsMailService.cs
kspearrin Mar 29, 2024
fae1138
Update src/Core/Services/Implementations/HandlebarsMailService.cs
kspearrin Mar 29, 2024
fcc69a7
Apply suggestions from code review
kspearrin Mar 29, 2024
2817802
Apply suggestions from code review
kspearrin Apr 1, 2024
be95456
PR feedback
kspearrin Apr 1, 2024
2014bf9
fix id
kspearrin Apr 9, 2024
2f3e3cd
Merge branch 'main' into orgdelete
kspearrin Apr 9, 2024
03e6a03
[PM-7004] Move OrgDeleteTokenable to AdminConsole ownership
r-tome Apr 23, 2024
0a7ea40
Merge branch 'main' into orgdelete
r-tome Apr 23, 2024
5edecc4
[PM-7004] Add consolidated billing logic into organization delete req…
r-tome Apr 24, 2024
cb0eba4
[PM-7004] Delete unused IOrganizationService.DeleteAsync(Organization…
r-tome Apr 24, 2024
84c2782
[PM-7004] Fix unit tests
r-tome Apr 24, 2024
5c12bc1
[PM-7004] Update delete organization request email templates
r-tome Apr 25, 2024
404f306
Merge branch 'main' into orgdelete
r-tome May 8, 2024
aa7e748
Merge branch 'main' into orgdelete
r-tome May 16, 2024
95fe9c0
Add success message when initiating organization deletion
r-tome May 17, 2024
1a0ce74
Merge branch 'main' into orgdelete
r-tome May 17, 2024
e4a4da0
Refactor OrganizationsController request delete initiation action to …
r-tome May 17, 2024
74ac919
Merge branch 'main' into orgdelete
r-tome May 17, 2024
ff9ef05
Merge branch 'main' into orgdelete
r-tome May 22, 2024
460a1b0
Merge branch 'main' into orgdelete
r-tome May 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Admin/AdminConsole/Controllers/OrganizationsController.cs
Expand Up @@ -243,6 +243,20 @@
return RedirectToAction("Index");
}

[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Org_Delete)]
public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)
{
var organization = await _organizationRepository.GetByIdAsync(id);

Check warning on line 251 in src/Admin/AdminConsole/Controllers/OrganizationsController.cs

View check run for this annotation

Codecov / codecov/patch

src/Admin/AdminConsole/Controllers/OrganizationsController.cs#L250-L251

Added lines #L250 - L251 were not covered by tests
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
if (organization != null)
{
await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail);
}

Check warning on line 255 in src/Admin/AdminConsole/Controllers/OrganizationsController.cs

View check run for this annotation

Codecov / codecov/patch

src/Admin/AdminConsole/Controllers/OrganizationsController.cs#L253-L255

Added lines #L253 - L255 were not covered by tests

return RedirectToAction("Edit", new { id });
}

Check warning on line 258 in src/Admin/AdminConsole/Controllers/OrganizationsController.cs

View check run for this annotation

Codecov / codecov/patch

src/Admin/AdminConsole/Controllers/OrganizationsController.cs#L257-L258

Added lines #L257 - L258 were not covered by tests

public async Task<IActionResult> TriggerBillingSync(Guid id)
{
var organization = await _organizationRepository.GetByIdAsync(id);
Expand Down
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Admin.AdminConsole.Models;

public class OrganizationInitiateDeleteModel
{
[Required]
[Display(Name = "Admin Email")]
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
public string AdminEmail { get; set; }

Check warning on line 9 in src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs

View check run for this annotation

Codecov / codecov/patch

src/Admin/AdminConsole/Models/OrganizationInitiateDeleteModel.cs#L9

Added line #L9 was not covered by tests
}
27 changes: 19 additions & 8 deletions src/Admin/AdminConsole/Views/Organizations/Edit.cshtml
r-tome marked this conversation as resolved.
Show resolved Hide resolved
@@ -1,4 +1,4 @@
@using Bit.Admin.Enums;
@using Bit.Admin.Enums;
@using Bit.Admin.Models
@using Bit.Core.Enums
@inject Bit.Admin.Services.IAccessControlService AccessControlService
Expand Down Expand Up @@ -37,6 +37,15 @@
document.getElementById('@(nameof(Model.Plan))').value = 'Enterprise (Trial)';
});

document.getElementById('initiate-delete-form').addEventListener('submit', (e) => {
const email = prompt('Enter the email address of the owner/admin that your want to ' +
'initiate the organization delete verification process with.');
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
document.getElementById('AdminEmail').value = email;
if (email == null || email === '') {
e.preventDefault();
}
});

function setTrialDefaults(planType) {
// Plan
document.getElementById('@(nameof(Model.PlanType))').value = planType;
Expand Down Expand Up @@ -76,7 +85,7 @@
{
<h2>Billing Information</h2>
@await Html.PartialAsync("_BillingInformation",
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })
new BillingInformationModel { BillingInfo = Model.BillingInfo, OrganizationId = Model.Organization.Id, Entity = "Organization" })

Check warning on line 88 in src/Admin/AdminConsole/Views/Organizations/Edit.cshtml

View check run for this annotation

Codecov / codecov/patch

src/Admin/AdminConsole/Views/Organizations/Edit.cshtml#L88

Added line #L88 was not covered by tests
}

@await Html.PartialAsync("~/AdminConsole/Views/Shared/_OrganizationForm.cshtml", Model)
Expand All @@ -95,18 +104,20 @@
}
@if (canUnlinkFromProvider && Model.Provider is not null)
{
<button
class="btn btn-outline-danger mr-2"
onclick="return unlinkProvider('@Model.Organization.Id');"
>
<button class="btn btn-outline-danger mr-2"
onclick="return unlinkProvider('@Model.Organization.Id');">

Check warning on line 108 in src/Admin/AdminConsole/Views/Organizations/Edit.cshtml

View check run for this annotation

Codecov / codecov/patch

src/Admin/AdminConsole/Views/Organizations/Edit.cshtml#L108

Added line #L108 was not covered by tests
Unlink provider
</button>
}
@if (canDelete)
{
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
<input type="hidden" name="AdminEmail" id="AdminEmail" />
<button class="btn btn-danger mr-2" type="submit">Initiate Delete</button>
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
</form>
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
onsubmit="return confirm('Are you sure you want to delete this organization?')">
<button class="btn btn-danger" type="submit">Delete</button>
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
<button class="btn btn-danger" type="submit">Hard Delete</button>
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
</form>
}
</div>
Expand Down
12 changes: 12 additions & 0 deletions src/Api/AdminConsole/Controllers/OrganizationsController.cs
Expand Up @@ -579,6 +579,18 @@
}
}

[HttpPost("delete-recover-token")]
[AllowAnonymous]
public async Task PostDeleteRecoverToken([FromBody] OrganizationVerifyDeleteRecoverRequestModel model)

Check warning

Code scanning / Checkmarx One

Log Forging

Log Forging

Check warning

Code scanning / Checkmarx One

Log Forging

Log Forging

Check warning

Code scanning / Checkmarx One

Log Forging

Log Forging
{
var organization = await _organizationRepository.GetByIdAsync(new Guid(model.OrganizationId));

Check warning on line 586 in src/Api/AdminConsole/Controllers/OrganizationsController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Controllers/OrganizationsController.cs#L585-L586

Added lines #L585 - L586 were not covered by tests
if (organization == null)
{
throw new UnauthorizedAccessException();

Check warning on line 589 in src/Api/AdminConsole/Controllers/OrganizationsController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Controllers/OrganizationsController.cs#L588-L589

Added lines #L588 - L589 were not covered by tests
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
}
await _organizationService.DeleteAsync(organization, model.Token);
}

Check warning on line 592 in src/Api/AdminConsole/Controllers/OrganizationsController.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Controllers/OrganizationsController.cs#L591-L592

Added lines #L591 - L592 were not covered by tests

[HttpPost("{id}/import")]
public async Task Import(string id, [FromBody] ImportOrganizationUsersRequestModel model)
{
Expand Down
@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Api.AdminConsole.Models.Request.Organizations;

public class OrganizationVerifyDeleteRecoverRequestModel
{
[Required]
public string OrganizationId { get; set; }

Check warning on line 8 in src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs#L8

Added line #L8 was not covered by tests
[Required]
public string Token { get; set; }

Check warning on line 10 in src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs

View check run for this annotation

Codecov / codecov/patch

src/Api/AdminConsole/Models/Request/Organizations/OrganizationVerifyDeleteRecoverRequestModel.cs#L10

Added line #L10 was not covered by tests
}
2 changes: 2 additions & 0 deletions src/Core/AdminConsole/Services/IOrganizationService.cs
Expand Up @@ -32,6 +32,8 @@ public interface IOrganizationService
/// </summary>
Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner,
string ownerKey, string collectionName, string publicKey, string privateKey);
Task InitiateDeleteAsync(Organization organization, string orgAdminEmail);
Task DeleteAsync(Organization organization, string token);
Task DeleteAsync(Organization organization);
Task EnableAsync(Guid organizationId, DateTime? expirationDate);
Task DisableAsync(Guid organizationId, DateTime? expirationDate);
Expand Down
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
Expand Up @@ -60,6 +60,7 @@
private readonly IProviderUserRepository _providerUserRepository;
private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery;
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IProviderRepository _providerRepository;
private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory;
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
Expand Down Expand Up @@ -94,6 +95,7 @@
IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IProviderRepository providerRepository,
IFeatureService featureService)
{
Expand Down Expand Up @@ -123,6 +125,7 @@
_providerUserRepository = providerUserRepository;
_countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_providerRepository = providerRepository;
_orgUserInviteTokenableFactory = orgUserInviteTokenableFactory;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
Expand Down Expand Up @@ -728,6 +731,32 @@
}
}

public async Task InitiateDeleteAsync(Organization organization, string orgAdminEmail)
{
var orgAdmin = await _userRepository.GetByEmailAsync(orgAdminEmail);

Check warning on line 736 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L735-L736

Added lines #L735 - L736 were not covered by tests
if (orgAdmin == null)
{
throw new BadRequestException("Org admin not found.");

Check warning on line 739 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L738-L739

Added lines #L738 - L739 were not covered by tests
}
var orgAdminOrgUser = await _organizationUserRepository.GetDetailsByUserAsync(orgAdmin.Id, organization.Id);

Check warning on line 741 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L741

Added line #L741 was not covered by tests
if (orgAdminOrgUser == null || orgAdminOrgUser.Status != OrganizationUserStatusType.Confirmed ||
(orgAdminOrgUser.Type != OrganizationUserType.Admin && orgAdminOrgUser.Type != OrganizationUserType.Owner))
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
{
throw new BadRequestException("Org admin not found.");

Check warning on line 745 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L743-L745

Added lines #L743 - L745 were not covered by tests
}
var token = _orgDeleteTokenDataFactory.Protect(new OrgDeleteTokenable(organization, 1));
await _mailService.SendInitiateDeleteOrganzationEmailAsync(orgAdminEmail, organization, token);
}

Check warning on line 749 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L747-L749

Added lines #L747 - L749 were not covered by tests

public async Task DeleteAsync(Organization organization, string token)
{

Check warning on line 752 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L752

Added line #L752 was not covered by tests
if (!_orgDeleteTokenDataFactory.TryUnprotect(token, out var data) && data.IsValid(organization))
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
{
throw new BadRequestException("Invalid token.");

Check warning on line 755 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L754-L755

Added lines #L754 - L755 were not covered by tests
}
await DeleteAsync(organization);
}

Check warning on line 758 in src/Core/AdminConsole/Services/Implementations/OrganizationService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/AdminConsole/Services/Implementations/OrganizationService.cs#L757-L758

Added lines #L757 - L758 were not covered by tests

public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
Expand Down
32 changes: 32 additions & 0 deletions src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this belong in Auth? Feels like it might be more Admin Console

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, the clients code was already moved to AC ownership so I also moved this.

@@ -0,0 +1,32 @@
using System.Text.Json.Serialization;
using Bit.Core.AdminConsole.Entities;

namespace Bit.Core.Auth.Models.Business.Tokenables;

public class OrgDeleteTokenable : Tokens.ExpiringTokenable
{
public const string ClearTextPrefix = "";
public const string DataProtectorPurpose = "OrgDeleteDataProtector";
public const string TokenIdentifier = "OrgDelete";
public string Identifier { get; set; } = TokenIdentifier;
public Guid Id { get; set; }

Check warning on line 12 in src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs#L11-L12

Added lines #L11 - L12 were not covered by tests

[JsonConstructor]
public OrgDeleteTokenable(DateTime expirationDate)
{
ExpirationDate = expirationDate;
}

Check warning on line 18 in src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs#L15-L18

Added lines #L15 - L18 were not covered by tests

public OrgDeleteTokenable(Organization organization, int hoursTillExpiration)
{
Id = organization.Id;
ExpirationDate = DateTime.UtcNow.AddHours(hoursTillExpiration);
}

Check warning on line 24 in src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs#L20-L24

Added lines #L20 - L24 were not covered by tests

public bool IsValid(Organization organization)
{
return Id == organization.Id;
}

Check warning on line 29 in src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Auth/Models/Business/Tokenables/OrgDeleteTokenable.cs#L27-L29

Added lines #L27 - L29 were not covered by tests

protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default;
}
@@ -0,0 +1,39 @@
{{#>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 organization:
</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> {{OrganizationName}}<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> {{OrganizationId}}<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> {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{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;">Plan:</b> {{OrganizationPlan}}<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;">Number of seats:</b> {{OrganizationSeats}}<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> {{OrganizationBillingEmail}}
</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 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">
If you did not request this email to delete your Bitwarden organization, 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;">

Check warning

Code scanning / Checkmarx One

Unsafe Use Of Target blank Medium

Unsafe Use Of Target blank
Delete Your Organization
</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,17 @@
{{#>BasicTextLayout}}
We recently received your request to permanently delete the following Bitwarden organization:

- Name: {{OrganizationName}}
- ID: {{OrganizationId}}
- Created: {{OrganizationCreationDate}} at {{OrganizationCreationTime}} {{TimeZone}}
- Plan: {{OrganizationPlan}}
- Number of seats: {{OrganizationSeats}}
- Billing email address: {{OrganizationBillingEmail}}

Click the link below to complete the deletion of your organization.

If you did not request this email to delete your Bitwarden organization, you can safely ignore it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a nit, but if you didn't request this email you should probably be worried! Someone is trying to delete your organization. Maybe "If you did not request this email to delete your Bitwarden organization, please contact us." ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I am not opposed to changing that.


{{{Url}}}

{{/BasicTextLayout}}
21 changes: 21 additions & 0 deletions src/Core/Models/Mail/OrgInitiateDeleteModel.cs
@@ -0,0 +1,21 @@
using Bit.Core.Models.Mail;

namespace Bit.Core.Auth.Models.Mail;

public class OrgInitiateDeleteModel : BaseMailModel
{
public string Url => string.Format("{0}/verify-org-delete?orgId={1}&token={2}",
WebVaultUrl,
OrganizationId,
Token);

Check warning on line 10 in src/Core/Models/Mail/OrgInitiateDeleteModel.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Models/Mail/OrgInitiateDeleteModel.cs#L7-L10

Added lines #L7 - L10 were not covered by tests

public string Token { get; set; }
public Guid OrganizationId { get; set; }
public string OrganizationName { get; set; }
public string OrganizationPlan { get; set; }
public string OrganizationSeats { get; set; }
public string OrganizationBillingEmail { get; set; }
public string OrganizationCreationDate { get; set; }
public string OrganizationCreationTime { get; set; }
public string TimeZone { get; set; }

Check warning on line 20 in src/Core/Models/Mail/OrgInitiateDeleteModel.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Models/Mail/OrgInitiateDeleteModel.cs#L12-L20

Added lines #L12 - L20 were not covered by tests
}
1 change: 1 addition & 0 deletions src/Core/Services/IMailService.cs
Expand Up @@ -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 SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token);
}

23 changes: 23 additions & 0 deletions src/Core/Services/Implementations/HandlebarsMailService.cs
Expand Up @@ -977,6 +977,29 @@
await _mailDeliveryService.SendEmailAsync(message);
}

public async Task SendInitiateDeleteOrganzationEmailAsync(string email, Organization organization, string token)
{
var message = CreateDefaultMessage("Delete Your Organization", email);
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
var model = new OrgInitiateDeleteModel
{
Token = WebUtility.UrlEncode(token),
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
OrganizationId = organization.Id,
OrganizationName = organization.Name,
kspearrin marked this conversation as resolved.
Show resolved Hide resolved
OrganizationBillingEmail = organization.BillingEmail,
OrganizationPlan = organization.Plan,
OrganizationSeats = organization.Seats.ToString(),
OrganizationCreationDate = organization.CreationDate.ToLongDateString(),
OrganizationCreationTime = organization.CreationDate.ToShortTimeString(),
TimeZone = _utcTimeZoneDisplay,
};
await AddMessageContentAsync(message, "InitiateDeleteOrganzation", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "InitiateDeleteOrganzation";
await _mailDeliveryService.SendEmailAsync(message);
}

Check warning on line 1001 in src/Core/Services/Implementations/HandlebarsMailService.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Services/Implementations/HandlebarsMailService.cs#L981-L1001

Added lines #L981 - L1001 were not covered by tests

private static string GetUserIdentifier(string email, string userName)
{
return string.IsNullOrEmpty(userName) ? email : CoreHelpers.SanitizeForEmail(userName, false);
Expand Down