1
0
mirror of https://github.com/bitwarden/server synced 2026-01-27 23:03:31 +00:00

feat(emergency-access) [PM-29584] Create Email for Emergency Access Removal (#6793)

* feat(emergency-access) [PM-29584]: Add email template.

* refactor(emergency-access) [PM-29584]: Move Emergency Access to Auth/UserFeatures.

* refactor(emergency-access) [PM-29584]: Move EmergencyAccess tests to UserFeatures space.

* feat(emergency-access) [PM-29584]: Add compiled EmergencyAccess templates.

* test(emergency-access) [PM-29584]: Add mailer-specific tests.

* refactor(emergency-access) [PM-29584]: Move mail to UserFeatures area.

* feat(emergency-access) [PM-29584]: Update link for help pages, not web vault.

* test(emergency-access) [PM-29584]: Update mail tests for new URL and single responsibility.

* refactor(emergency-access) [PM-29584]: Add comments for added test.
This commit is contained in:
Dave
2026-01-22 20:24:15 -05:00
committed by GitHub
parent 0cc72127d7
commit 93e2c971df
14 changed files with 802 additions and 100 deletions

View File

@@ -7,7 +7,7 @@ using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Api.Vault.Models.Response;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
using Bit.Core.Jobs;
using Quartz;

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
using Bit.Core.Jobs;
using Quartz;

View File

@@ -4,7 +4,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data;
@@ -19,7 +18,7 @@ using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Core.Vault.Services;
namespace Bit.Core.Auth.Services;
namespace Bit.Core.Auth.UserFeatures.EmergencyAccess;
public class EmergencyAccessService : IEmergencyAccessService
{
@@ -61,7 +60,7 @@ public class EmergencyAccessService : IEmergencyAccessService
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
public async Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime)
public async Task<Entities.EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime)
{
if (!await _userService.CanAccessPremium(grantorUser))
{
@@ -73,7 +72,7 @@ public class EmergencyAccessService : IEmergencyAccessService
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
}
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Entities.EmergencyAccess
{
GrantorId = grantorUser.Id,
Email = emergencyContactEmail.ToLowerInvariant(),
@@ -113,7 +112,7 @@ public class EmergencyAccessService : IEmergencyAccessService
await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));
}
public async Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService)
public async Task<Entities.EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
if (emergencyAccess == null)
@@ -175,7 +174,7 @@ public class EmergencyAccessService : IEmergencyAccessService
await _emergencyAccessRepository.DeleteAsync(emergencyAccess);
}
public async Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId)
public async Task<Entities.EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted ||
@@ -201,7 +200,7 @@ public class EmergencyAccessService : IEmergencyAccessService
return emergencyAccess;
}
public async Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser)
public async Task SaveAsync(Entities.EmergencyAccess emergencyAccess, User grantorUser)
{
if (!await _userService.CanAccessPremium(grantorUser))
{
@@ -311,7 +310,7 @@ public class EmergencyAccessService : IEmergencyAccessService
}
// TODO PM-21687: rename this to something like InitiateRecoveryTakeoverAsync
public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser)
public async Task<(Entities.EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
@@ -429,7 +428,7 @@ public class EmergencyAccessService : IEmergencyAccessService
return await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId);
}
private async Task SendInviteAsync(EmergencyAccess emergencyAccess, string invitingUsersName)
private async Task SendInviteAsync(Entities.EmergencyAccess emergencyAccess, string invitingUsersName)
{
var token = _dataProtectorTokenizer.Protect(new EmergencyAccessInviteTokenable(emergencyAccess, _globalSettings.OrganizationInviteExpirationHours));
await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);
@@ -449,7 +448,7 @@ public class EmergencyAccessService : IEmergencyAccessService
*/
//TODO PM-21687: this IsValidRequest() checks the validity based on the granteeUser. There should be a complementary method for the grantorUser
private static bool IsValidRequest(
EmergencyAccess availableAccess,
Entities.EmergencyAccess availableAccess,
User requestingUser,
EmergencyAccessType requestedAccessType)
{

View File

@@ -1,5 +1,4 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
@@ -7,7 +6,7 @@ using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Vault.Models.Data;
namespace Bit.Core.Auth.Services;
namespace Bit.Core.Auth.UserFeatures.EmergencyAccess;
public interface IEmergencyAccessService
{
@@ -20,7 +19,7 @@ public interface IEmergencyAccessService
/// <param name="accessType">Type of emergency access allowed to the emergency contact</param>
/// <param name="waitTime">The amount of time to pass before the invite is auto confirmed</param>
/// <returns>a new Emergency Access object</returns>
Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
Task<Entities.EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
/// <summary>
/// Sends an invite to the emergency contact associated with the emergency access id.
/// </summary>
@@ -37,7 +36,7 @@ public interface IEmergencyAccessService
/// <param name="token">the tokenable that was sent via email</param>
/// <param name="userService">service dependency</param>
/// <returns>void</returns>
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
Task<Entities.EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
/// <summary>
/// The creator of the emergency access request can delete the request.
/// </summary>
@@ -53,7 +52,7 @@ public interface IEmergencyAccessService
/// <param name="key">The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key)</param>
/// <param name="grantorId">Id of grantor user</param>
/// <returns>emergency access object associated with the Id passed in</returns>
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
Task<Entities.EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
/// <summary>
/// Fetches an emergency access object. The grantor user must own the object being fetched.
/// </summary>
@@ -67,7 +66,7 @@ public interface IEmergencyAccessService
/// <param name="emergencyAccess">emergency access entity being updated</param>
/// <param name="grantorUser">grantor user</param>
/// <returns>void</returns>
Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser);
Task SaveAsync(Entities.EmergencyAccess emergencyAccess, User grantorUser);
/// <summary>
/// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation.
/// </summary>
@@ -107,7 +106,7 @@ public interface IEmergencyAccessService
/// <param name="emergencyAccessId">Id of entity being accessed</param>
/// <param name="granteeUser">grantee user of the emergency access entity</param>
/// <returns>emergency access entity and the grantorUser</returns>
Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
Task<(Entities.EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
/// <summary>
/// Updates the grantor's password hash and updates the key for the EmergencyAccess entity.
/// </summary>

View File

@@ -0,0 +1,14 @@
using Bit.Core.Platform.Mail.Mailer;
namespace Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;
public class EmergencyAccessRemoveGranteesMailView : BaseMailView
{
public required IEnumerable<string> RemovedGranteeNames { get; set; }
public string EmergencyAccessHelpPageUrl => "https://bitwarden.com/help/emergency-access/";
}
public class EmergencyAccessRemoveGranteesMail : BaseMail<EmergencyAccessRemoveGranteesMailView>
{
public override string Subject { get; set; } = "Emergency contacts removed";
}

View File

@@ -0,0 +1,499 @@
<!doctype html>
<html lang="und" dir="auto" xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title></title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type="text/css">
#outlook a { padding:0; }
body { margin:0;padding:0;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%; }
table, td { border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt; }
img { border:0;height:auto;line-height:100%; outline:none;text-decoration:none;-ms-interpolation-mode:bicubic; }
p { display:block;margin:13px 0; }
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix { width:100% !important; }
</style>
<![endif]-->
<style type="text/css">
@media only screen and (min-width:480px) {
.mj-column-per-70 { width:70% !important; max-width: 70%; }
.mj-column-per-30 { width:30% !important; max-width: 30%; }
.mj-column-per-100 { width:100% !important; max-width: 100%; }
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-70 { width:70% !important; max-width: 70%; }
.moz-text-html .mj-column-per-30 { width:30% !important; max-width: 30%; }
.moz-text-html .mj-column-per-100 { width:100% !important; max-width: 100%; }
</style>
<style type="text/css">
@media only screen and (max-width:480px) {
.mj-bw-hero-responsive-img {
display: none !important;
}
}
@media only screen and (max-width:479px) {
table.mj-full-width-mobile { width: 100% !important; }
td.mj-full-width-mobile { width: auto !important; }
}
</style>
<style type="text/css">
.border-fix > table {
border-collapse: separate !important;
}
.border-fix > table > tbody > tr > td {
border-radius: 3px;
}
</style>
</head>
<body style="word-spacing:normal;background-color:#e6e9ef;">
<div class="border-fix" style="background-color:#e6e9ef;" lang="und" dir="auto">
<!-- Blue Header Section -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="border-fix-outlook" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div class="border-fix" style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 20px 0px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#175ddc;background-color:#175ddc;width:100%;border-radius:4px 4px 0px 0px;">
<tbody>
<tr>
<td>
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#175ddc" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;border-radius:4px 4px 0px 0px;max-width:620px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;border-radius:4px 4px 0px 0px;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:434px;" ><![endif]-->
<div class="mj-column-per-70 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:150px;">
<img alt src="https://bitwarden.com/images/logo-horizontal-white.png" style="border:0;display:block;outline:none;text-decoration:none;height:30px;width:100%;font-size:16px;" width="150" height="30">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td align="left" style="font-size:0px;padding:10px 25px;padding-top:0;padding-bottom:0;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:1;text-align:left;color:#ffffff;"><h1 style="font-weight: normal; font-size: 24px; line-height: 32px">
</h1></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td class="" style="vertical-align:bottom;width:186px;" ><![endif]-->
<div class="mj-column-per-30 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:bottom;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:bottom;" width="100%">
<tbody>
<tr>
<td align="center" class="mj-bw-hero-responsive-img" style="font-size:0px;padding:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:155px;">
<img alt src="undefined" style="border:0;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:16px;" width="155" height="auto">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Main Content -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px 20px 0px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" width="660px" ><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:620px;" width="620" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:620px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff;background-color:#ffffff;width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0px 10px 0px 10px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="left" style="font-size:0px;padding:10px 15px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:16px;line-height:24px;text-align:left;color:#1B2029;">The following emergency contacts have been removed from your account:
<ul>
{{#each RemovedGranteeNames}}
<li>{{this}}</li>
{{/each}}
</ul>
Learn more about <a href="{{EmergencyAccessHelpPageUrl}}">emergency access</a>.</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
<!-- Footer -->
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:660px;" width="660" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:660px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:5px 20px 10px 20px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:620px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0;word-break:break-word;">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" ><tr><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://x.com/bitwarden" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-x-twitter.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.reddit.com/r/Bitwarden/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-reddit.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://community.bitwarden.com/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-discourse.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://github.com/bitwarden" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-github.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.youtube.com/channel/UCId9a_jQqvJre0_dE2lE_Rw" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-youtube.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.linkedin.com/company/bitwarden1/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-linkedin.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td><td><![endif]-->
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="float:none;display:inline-table;">
<tbody>
<tr>
<td style="padding:8px;vertical-align:middle;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="border-radius:3px;width:24px;">
<tbody>
<tr>
<td style="font-size:0;height:24px;vertical-align:middle;width:24px;">
<a href="https://www.facebook.com/bitwarden/" target="_blank">
<img alt height="24" src="https://assets.bitwarden.com/email/v1/social-icons-facebook.png" style="border-radius:3px;display:block;" width="24">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
<tr>
<td align="center" style="font-size:0px;padding:10px 25px;word-break:break-word;">
<div style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:12px;line-height:16px;text-align:center;color:#5A6D91;"><p style="margin-bottom: 5px; margin-top: 5px">
© 2025 Bitwarden Inc. 1 N. Calle Cesar Chavez, Suite 102, Santa
Barbara, CA, USA
</p>
<p style="margin-top: 5px">
Always confirm you are on a trusted Bitwarden domain before logging
in:<br>
<a href="https://bitwarden.com/" style="text-decoration:none;color:#175ddc; font-weight:400">bitwarden.com</a> |
<a href="https://bitwarden.com/help/emails-from-bitwarden/" style="text-decoration:none; color:#175ddc; font-weight:400">Learn why we include this</a>
</p></div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@@ -0,0 +1,7 @@
The following emergency contacts have been removed from your account:
{{#each RemovedGranteeNames}}
{{this}}
{{/each}}
Learn more about emergency access at {{EmergencyAccessHelpPageUrl}}

View File

@@ -0,0 +1,31 @@
<mjml>
<mj-head>
<mj-include path="../../../../components/head.mjml" />
</mj-head>
<mj-body css-class="border-fix">
<!-- Blue Header Section -->
<mj-wrapper css-class="border-fix" padding="20px 20px 0px 20px">
<mj-bw-hero title=""/>
</mj-wrapper>
<!-- Main Content -->
<mj-wrapper padding="0px 20px 0px 20px">
<mj-section background-color="#fff" padding="0px 10px 0px 10px">
<mj-column>
<mj-text font-size="16px" line-height="24px" padding="10px 15px">
The following emergency contacts have been removed from your account:
<ul>
{{#each RemovedGranteeNames}}
<li>{{this}}</li>
{{/each}}
</ul>
Learn more about <a href="{{EmergencyAccessHelpPageUrl}}">emergency access</a>.
</mj-text>
</mj-column>
</mj-section>
</mj-wrapper>
<!-- Footer -->
<mj-include path="../../../../components/footer.mjml" />
</mj-body>
</mjml>

View File

@@ -22,6 +22,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.Services.Implementations;
using Bit.Core.Auth.UserFeatures;
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
using Bit.Core.Auth.UserFeatures.PasswordValidation;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;

View File

@@ -0,0 +1,153 @@
using Bit.Core.Auth.UserFeatures.EmergencyAccess.Mail;
using Bit.Core.Models.Mail;
using Bit.Core.Platform.Mail.Delivery;
using Bit.Core.Platform.Mail.Mailer;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute;
using Xunit;
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
namespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess;
[SutProviderCustomize]
public class EmergencyAccessMailTests
{
// Constant values for all Emergency Access emails
private const string _emergencyAccessHelpUrl = "https://bitwarden.com/help/emergency-access/";
private const string _emergencyAccessMailSubject = "Emergency contacts removed";
/// <summary>
/// Documents how to construct and send the emergency access removal email.
/// 1. Inject IMailer into their command/service
/// 2. Construct EmergencyAccessRemoveGranteesMail as shown below
/// 3. Call mailer.SendEmail(mail)
/// </summary>
[Theory, BitAutoData]
public async Task SendEmergencyAccessRemoveGranteesEmail_SingleGrantee_Success(
string grantorEmail,
string granteeName)
{
// Arrange
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
var globalSettings = new GlobalSettings { SelfHosted = false };
var deliveryService = Substitute.For<IMailDeliveryService>();
var mailer = new Mailer(
new HandlebarMailRenderer(logger, globalSettings),
deliveryService);
var mail = new EmergencyAccessRemoveGranteesMail
{
ToEmails = [grantorEmail],
View = new EmergencyAccessRemoveGranteesMailView
{
RemovedGranteeNames = [granteeName]
}
};
MailMessage sentMessage = null;
await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>
sentMessage = message
));
// Act
await mailer.SendEmail(mail);
// Assert
Assert.NotNull(sentMessage);
Assert.Contains(grantorEmail, sentMessage.ToEmails);
// Verify the content contains the grantee name
Assert.Contains(granteeName, sentMessage.TextContent);
Assert.Contains(granteeName, sentMessage.HtmlContent);
}
/// <summary>
/// Documents handling multiple removed grantees in a single email.
/// </summary>
[Theory, BitAutoData]
public async Task SendEmergencyAccessRemoveGranteesEmail_MultipleGrantees_RendersAllNames(
string grantorEmail)
{
// Arrange
var logger = Substitute.For<ILogger<HandlebarMailRenderer>>();
var globalSettings = new GlobalSettings { SelfHosted = false };
var deliveryService = Substitute.For<IMailDeliveryService>();
var mailer = new Mailer(
new HandlebarMailRenderer(logger, globalSettings),
deliveryService);
var granteeNames = new[] { "Alice", "Bob", "Carol" };
var mail = new EmergencyAccessRemoveGranteesMail
{
ToEmails = [grantorEmail],
View = new EmergencyAccessRemoveGranteesMailView
{
RemovedGranteeNames = granteeNames
}
};
MailMessage sentMessage = null;
await deliveryService.SendEmailAsync(Arg.Do<MailMessage>(message =>
sentMessage = message
));
// Act
await mailer.SendEmail(mail);
// Assert - All grantee names should appear in the email
Assert.NotNull(sentMessage);
foreach (var granteeName in granteeNames)
{
Assert.Contains(granteeName, sentMessage.TextContent);
Assert.Contains(granteeName, sentMessage.HtmlContent);
}
}
/// <summary>
/// Validates the required GranteeNames for the email view model.
/// </summary>
[Theory, BitAutoData]
public void EmergencyAccessRemoveGranteesMailView_GranteeNames_AreRequired(
string grantorEmail)
{
// Arrange - Shows the minimum required to construct the email
var mail = new EmergencyAccessRemoveGranteesMail
{
ToEmails = [grantorEmail], // Required: who to send to
View = new EmergencyAccessRemoveGranteesMailView
{
// Required: at least one removed grantee name
RemovedGranteeNames = ["Example Grantee"]
}
};
// Assert
Assert.NotNull(mail);
Assert.NotNull(mail.View);
Assert.NotEmpty(mail.View.RemovedGranteeNames);
}
/// <summary>
/// Ensure consistency with help pages link and email subject.
/// </summary>
/// <param name="grantorEmail"></param>
/// <param name="granteeName"></param>
[Theory, BitAutoData]
public void EmergencyAccessRemoveGranteesMailView_SubjectAndHelpLink_MatchesExpectedValues(string grantorEmail, string granteeName)
{
// Arrange
var mail = new EmergencyAccessRemoveGranteesMail
{
ToEmails = [grantorEmail],
View = new EmergencyAccessRemoveGranteesMailView { RemovedGranteeNames = [granteeName] }
};
// Assert
Assert.NotNull(mail);
Assert.NotNull(mail.View);
Assert.Equal(_emergencyAccessMailSubject, mail.Subject);
Assert.Equal(_emergencyAccessHelpUrl, mail.View.EmergencyAccessHelpPageUrl);
}
}

View File

@@ -1,11 +1,10 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.EmergencyAccess;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@@ -17,7 +16,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Auth.Services;
namespace Bit.Core.Test.Auth.UserFeatures.EmergencyAccess;
[SutProviderCustomize]
public class EmergencyAccessServiceTests
@@ -68,13 +67,13 @@ public class EmergencyAccessServiceTests
Assert.Equal(EmergencyAccessStatusType.Invited, result.Status);
await sutProvider.GetDependency<IEmergencyAccessRepository>()
.Received(1)
.CreateAsync(Arg.Any<EmergencyAccess>());
.CreateAsync(Arg.Any<Core.Auth.Entities.EmergencyAccess>());
sutProvider.GetDependency<IDataProtectorTokenFactory<EmergencyAccessInviteTokenable>>()
.Received(1)
.Protect(Arg.Any<EmergencyAccessInviteTokenable>());
await sutProvider.GetDependency<IMailService>()
.Received(1)
.SendEmergencyAccessInviteEmailAsync(Arg.Any<EmergencyAccess>(), Arg.Any<string>(), Arg.Any<string>());
.SendEmergencyAccessInviteEmailAsync(Arg.Any<Core.Auth.Entities.EmergencyAccess>(), Arg.Any<string>(), Arg.Any<string>());
}
[Theory, BitAutoData]
@@ -98,7 +97,7 @@ public class EmergencyAccessServiceTests
User invitingUser,
Guid emergencyAccessId)
{
EmergencyAccess emergencyAccess = null;
Core.Auth.Entities.EmergencyAccess emergencyAccess = null;
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
@@ -119,7 +118,7 @@ public class EmergencyAccessServiceTests
User invitingUser,
Guid emergencyAccessId)
{
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Status = EmergencyAccessStatusType.Invited,
GrantorId = Guid.NewGuid(),
@@ -148,7 +147,7 @@ public class EmergencyAccessServiceTests
User invitingUser,
Guid emergencyAccessId)
{
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Status = statusType,
GrantorId = invitingUser.Id,
@@ -172,7 +171,7 @@ public class EmergencyAccessServiceTests
User invitingUser,
Guid emergencyAccessId)
{
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Status = EmergencyAccessStatusType.Invited,
GrantorId = invitingUser.Id,
@@ -194,7 +193,7 @@ public class EmergencyAccessServiceTests
public async Task AcceptUserAsync_EmergencyAccessNull_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider, User acceptingUser, string token)
{
EmergencyAccess emergencyAccess = null;
Core.Auth.Entities.EmergencyAccess emergencyAccess = null;
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns(emergencyAccess);
@@ -209,7 +208,7 @@ public class EmergencyAccessServiceTests
public async Task AcceptUserAsync_CannotUnprotectToken_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User acceptingUser,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string token)
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
@@ -230,8 +229,8 @@ public class EmergencyAccessServiceTests
public async Task AcceptUserAsync_TokenDataInvalid_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User acceptingUser,
EmergencyAccess emergencyAccess,
EmergencyAccess wrongEmergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess wrongEmergencyAccess,
string token)
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
@@ -257,7 +256,7 @@ public class EmergencyAccessServiceTests
public async Task AcceptUserAsync_AcceptedStatus_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User acceptingUser,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string token)
{
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
@@ -284,7 +283,7 @@ public class EmergencyAccessServiceTests
public async Task AcceptUserAsync_NotInvitedStatus_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User acceptingUser,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string token)
{
emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;
@@ -311,7 +310,7 @@ public class EmergencyAccessServiceTests
public async Task AcceptUserAsync_EmergencyAccessEmailDoesNotMatch_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User acceptingUser,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string token)
{
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
@@ -339,7 +338,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider,
User acceptingUser,
User invitingUser,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string token)
{
emergencyAccess.Status = EmergencyAccessStatusType.Invited;
@@ -364,7 +363,7 @@ public class EmergencyAccessServiceTests
await sutProvider.GetDependency<IEmergencyAccessRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Accepted));
await sutProvider.GetDependency<IMailService>()
.Received(1)
@@ -375,11 +374,11 @@ public class EmergencyAccessServiceTests
public async Task DeleteAsync_EmergencyAccessNull_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User invitingUser,
EmergencyAccess emergencyAccess)
Core.Auth.Entities.EmergencyAccess emergencyAccess)
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id));
@@ -391,7 +390,7 @@ public class EmergencyAccessServiceTests
public async Task DeleteAsync_EmergencyAccessGrantorIdNotEqual_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User invitingUser,
EmergencyAccess emergencyAccess)
Core.Auth.Entities.EmergencyAccess emergencyAccess)
{
emergencyAccess.GrantorId = Guid.NewGuid();
sutProvider.GetDependency<IEmergencyAccessRepository>()
@@ -408,7 +407,7 @@ public class EmergencyAccessServiceTests
public async Task DeleteAsync_EmergencyAccessGranteeIdNotEqual_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
User invitingUser,
EmergencyAccess emergencyAccess)
Core.Auth.Entities.EmergencyAccess emergencyAccess)
{
emergencyAccess.GranteeId = Guid.NewGuid();
sutProvider.GetDependency<IEmergencyAccessRepository>()
@@ -425,7 +424,7 @@ public class EmergencyAccessServiceTests
public async Task DeleteAsync_EmergencyAccessIsDeleted_Success(
SutProvider<EmergencyAccessService> sutProvider,
User user,
EmergencyAccess emergencyAccess)
Core.Auth.Entities.EmergencyAccess emergencyAccess)
{
emergencyAccess.GranteeId = user.Id;
emergencyAccess.GrantorId = user.Id;
@@ -443,7 +442,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task ConfirmUserAsync_EmergencyAccessNull_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string key,
User grantorUser)
{
@@ -451,7 +450,7 @@ public class EmergencyAccessServiceTests
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id));
@@ -463,7 +462,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task ConfirmUserAsync_EmergencyAccessStatusIsNotAccepted_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string key,
User grantorUser)
{
@@ -484,7 +483,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task ConfirmUserAsync_EmergencyAccessGrantorIdNotEqualToConfirmingUserId_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string key,
User grantorUser)
{
@@ -505,7 +504,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider, User confirmingUser, string key)
{
confirmingUser.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Status = EmergencyAccessStatusType.Accepted,
GrantorId = confirmingUser.Id,
@@ -530,7 +529,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task ConfirmUserAsync_ConfirmsAndReplacesEmergencyAccess_Success(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
string key,
User grantorUser,
User granteeUser)
@@ -553,7 +552,7 @@ public class EmergencyAccessServiceTests
await sutProvider.GetDependency<IEmergencyAccessRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
await sutProvider.GetDependency<IMailService>()
.Received(1)
@@ -564,7 +563,7 @@ public class EmergencyAccessServiceTests
public async Task SaveAsync_PremiumCannotUpdate_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider, User savingUser)
{
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Type = EmergencyAccessType.Takeover,
GrantorId = savingUser.Id,
@@ -586,7 +585,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider, User savingUser)
{
savingUser.Premium = true;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Type = EmergencyAccessType.Takeover,
GrantorId = new Guid(),
@@ -611,7 +610,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider, User grantorUser)
{
grantorUser.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Type = EmergencyAccessType.Takeover,
GrantorId = grantorUser.Id,
@@ -633,7 +632,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider, User grantorUser)
{
grantorUser.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Type = EmergencyAccessType.View,
GrantorId = grantorUser.Id,
@@ -655,7 +654,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider, User grantorUser)
{
grantorUser.UsesKeyConnector = false;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Type = EmergencyAccessType.Takeover,
GrantorId = grantorUser.Id,
@@ -678,7 +677,7 @@ public class EmergencyAccessServiceTests
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser));
@@ -692,7 +691,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task InitiateAsync_EmergencyAccessGranteeIdNotEqual_ThrowBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User initiatingUser)
{
emergencyAccess.GranteeId = new Guid();
@@ -712,7 +711,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task InitiateAsync_EmergencyAccessStatusIsNotConfirmed_ThrowBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User initiatingUser)
{
emergencyAccess.GranteeId = initiatingUser.Id;
@@ -735,7 +734,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)
{
grantor.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Status = EmergencyAccessStatusType.Confirmed,
GranteeId = initiatingUser.Id,
@@ -764,7 +763,7 @@ public class EmergencyAccessServiceTests
SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)
{
grantor.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Status = EmergencyAccessStatusType.Confirmed,
GranteeId = initiatingUser.Id,
@@ -783,14 +782,14 @@ public class EmergencyAccessServiceTests
await sutProvider.GetDependency<IEmergencyAccessRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
}
[Theory, BitAutoData]
public async Task InitiateAsync_RequestIsCorrect_Success(
SutProvider<EmergencyAccessService> sutProvider, User initiatingUser, User grantor)
{
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
Status = EmergencyAccessStatusType.Confirmed,
GranteeId = initiatingUser.Id,
@@ -809,7 +808,7 @@ public class EmergencyAccessServiceTests
await sutProvider.GetDependency<IEmergencyAccessRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated));
}
[Theory, BitAutoData]
@@ -818,7 +817,7 @@ public class EmergencyAccessServiceTests
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ApproveAsync(new Guid(), null));
@@ -829,7 +828,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task ApproveAsync_EmergencyAccessGrantorIdNotEquatToApproving_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User grantorUser)
{
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;
@@ -851,7 +850,7 @@ public class EmergencyAccessServiceTests
public async Task ApproveAsync_EmergencyAccessStatusNotRecoveryInitiated_ThrowsBadRequest(
EmergencyAccessStatusType statusType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User grantorUser)
{
emergencyAccess.GrantorId = grantorUser.Id;
@@ -869,7 +868,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task ApproveAsync_Success(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User grantorUser,
User granteeUser)
{
@@ -885,20 +884,20 @@ public class EmergencyAccessServiceTests
await sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser);
await sutProvider.GetDependency<IEmergencyAccessRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryApproved));
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.RecoveryApproved));
}
[Theory, BitAutoData]
public async Task RejectAsync_EmergencyAccessIdNull_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User GrantorUser)
{
emergencyAccess.GrantorId = GrantorUser.Id;
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser));
@@ -909,7 +908,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task RejectAsync_EmergencyAccessGrantorIdNotEqualToRequestUser_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User GrantorUser)
{
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
@@ -930,7 +929,7 @@ public class EmergencyAccessServiceTests
public async Task RejectAsync_EmergencyAccessStatusNotValid_ThrowsBadRequest(
EmergencyAccessStatusType statusType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User GrantorUser)
{
emergencyAccess.GrantorId = GrantorUser.Id;
@@ -951,7 +950,7 @@ public class EmergencyAccessServiceTests
public async Task RejectAsync_Success(
EmergencyAccessStatusType statusType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User GrantorUser,
User GranteeUser)
{
@@ -968,7 +967,7 @@ public class EmergencyAccessServiceTests
await sutProvider.GetDependency<IEmergencyAccessRepository>()
.Received(1)
.ReplaceAsync(Arg.Is<EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
.ReplaceAsync(Arg.Is<Core.Auth.Entities.EmergencyAccess>(x => x.Status == EmergencyAccessStatusType.Confirmed));
}
[Theory, BitAutoData]
@@ -977,7 +976,7 @@ public class EmergencyAccessServiceTests
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.GetPoliciesAsync(default, default));
@@ -992,7 +991,7 @@ public class EmergencyAccessServiceTests
public async Task GetPoliciesAsync_RequestNotValidStatusType_ThrowsBadRequest(
EmergencyAccessStatusType statusType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;
@@ -1010,7 +1009,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task GetPoliciesAsync_RequestNotValidType_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;
@@ -1032,7 +1031,7 @@ public class EmergencyAccessServiceTests
public async Task GetPoliciesAsync_OrganizationUserTypeNotOwner_ReturnsNull(
OrganizationUserType userType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser,
User grantorUser,
OrganizationUser grantorOrganizationUser)
@@ -1062,7 +1061,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task GetPoliciesAsync_OrganizationUserEmpty_ReturnsNull(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser,
User grantorUser)
{
@@ -1090,7 +1089,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task GetPoliciesAsync_ReturnsNotNull(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser,
User grantorUser,
OrganizationUser grantorOrganizationUser)
@@ -1127,7 +1126,7 @@ public class EmergencyAccessServiceTests
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.TakeoverAsync(default, default));
@@ -1138,7 +1137,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task TakeoverAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;
@@ -1161,7 +1160,7 @@ public class EmergencyAccessServiceTests
public async Task TakeoverAsync_RequestNotValid_StatusType_ThrowsBadRequest(
EmergencyAccessStatusType statusType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;
@@ -1180,7 +1179,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task TakeoverAsync_RequestNotValid_TypeIsView_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;
@@ -1203,7 +1202,7 @@ public class EmergencyAccessServiceTests
User grantor)
{
grantor.UsesKeyConnector = true;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
GrantorId = grantor.Id,
GranteeId = granteeUser.Id,
@@ -1232,7 +1231,7 @@ public class EmergencyAccessServiceTests
User grantor)
{
grantor.UsesKeyConnector = false;
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
GrantorId = grantor.Id,
GranteeId = granteeUser.Id,
@@ -1260,7 +1259,7 @@ public class EmergencyAccessServiceTests
{
sutProvider.GetDependency<IEmergencyAccessRepository>()
.GetByIdAsync(Arg.Any<Guid>())
.Returns((EmergencyAccess)null);
.Returns((Core.Auth.Entities.EmergencyAccess)null);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PasswordAsync(default, default, default, default));
@@ -1271,7 +1270,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task PasswordAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;
@@ -1294,7 +1293,7 @@ public class EmergencyAccessServiceTests
public async Task PasswordAsync_RequestNotValid_StatusType_ThrowsBadRequest(
EmergencyAccessStatusType statusType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;
@@ -1313,7 +1312,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task PasswordAsync_RequestNotValid_TypeIsView_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;
@@ -1332,7 +1331,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task PasswordAsync_NonOrgUser_Success(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser,
User grantorUser,
string key,
@@ -1367,7 +1366,7 @@ public class EmergencyAccessServiceTests
public async Task PasswordAsync_OrgUser_NotOrganizationOwner_RemovedFromOrganization_Success(
OrganizationUserType userType,
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser,
User grantorUser,
OrganizationUser organizationUser,
@@ -1408,7 +1407,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task PasswordAsync_OrgUser_IsOrganizationOwner_NotRemovedFromOrganization_Success(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser,
User grantorUser,
OrganizationUser organizationUser,
@@ -1459,7 +1458,7 @@ public class EmergencyAccessServiceTests
Enabled = true
}
});
var emergencyAccess = new EmergencyAccess
var emergencyAccess = new Core.Auth.Entities.EmergencyAccess
{
GrantorId = grantor.Id,
GranteeId = requestingUser.Id,
@@ -1484,7 +1483,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task ViewAsync_EmergencyAccessTypeNotView_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;
@@ -1500,7 +1499,7 @@ public class EmergencyAccessServiceTests
[Theory, BitAutoData]
public async Task GetAttachmentDownloadAsync_EmergencyAccessTypeNotView_ThrowsBadRequest(
SutProvider<EmergencyAccessService> sutProvider,
EmergencyAccess emergencyAccess,
Core.Auth.Entities.EmergencyAccess emergencyAccess,
User granteeUser)
{
emergencyAccess.GranteeId = granteeUser.Id;

View File

@@ -2,7 +2,6 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
@@ -23,6 +22,7 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.WebUtilities;
using NSubstitute;
using Xunit;
using EmergencyAccessEntity = Bit.Core.Auth.Entities.EmergencyAccess;
namespace Bit.Core.Test.Auth.UserFeatures.Registration;
@@ -726,7 +726,7 @@ public class RegisterUserCommandTests
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_Succeeds(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
@@ -767,7 +767,7 @@ public class RegisterUserCommandTests
[Theory]
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_InvalidToken_ThrowsBadRequestException(SutProvider<RegisterUserCommand> sutProvider, User user,
string masterPasswordHash, EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
string masterPasswordHash, EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
user.Email = $"test+{Guid.NewGuid()}@example.com";
@@ -1112,7 +1112,7 @@ public class RegisterUserCommandTests
[BitAutoData]
public async Task RegisterUserViaAcceptEmergencyAccessInviteToken_BlockedDomain_ThrowsBadRequestException(
SutProvider<RegisterUserCommand> sutProvider, User user, string masterPasswordHash,
EmergencyAccess emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
EmergencyAccessEntity emergencyAccess, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
{
// Arrange
user.Email = "user@blocked-domain.com";