mirror of
https://github.com/bitwarden/server.git
synced 2025-02-16 01:51:21 +01:00
Add support for Emergency Access (#1000)
* Add support for Emergency Access * Add migration script * Review comments * Ensure grantor has premium when inviting new grantees. * Resolve review comments * Remove two factor references
This commit is contained in:
parent
9bb63b86f0
commit
0f1af2333e
157
src/Api/Controllers/EmergencyAccessController.cs
Normal file
157
src/Api/Controllers/EmergencyAccessController.cs
Normal file
@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Api.Request;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers
|
||||
{
|
||||
[Route("emergency-access")]
|
||||
[Authorize("Application")]
|
||||
public class EmergencyAccessController : Controller
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
private readonly IEmergencyAccessService _emergencyAccessService;
|
||||
|
||||
public EmergencyAccessController(
|
||||
IUserService userService,
|
||||
IEmergencyAccessRepository emergencyAccessRepository,
|
||||
IEmergencyAccessService emergencyAccessService)
|
||||
{
|
||||
_userService = userService;
|
||||
_emergencyAccessRepository = emergencyAccessRepository;
|
||||
_emergencyAccessService = emergencyAccessService;
|
||||
}
|
||||
|
||||
[HttpGet("trusted")]
|
||||
public async Task<ListResponseModel<EmergencyAccessGranteeDetailsResponseModel>> GetContacts()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var granteeDetails = await _emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(userId.Value);
|
||||
|
||||
var responses = granteeDetails.Select(d =>
|
||||
new EmergencyAccessGranteeDetailsResponseModel(d));
|
||||
|
||||
return new ListResponseModel<EmergencyAccessGranteeDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("granted")]
|
||||
public async Task<ListResponseModel<EmergencyAccessGrantorDetailsResponseModel>> GetGrantees()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var granteeDetails = await _emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(userId.Value);
|
||||
|
||||
var responses = granteeDetails.Select(d => new EmergencyAccessGrantorDetailsResponseModel(d));
|
||||
|
||||
return new ListResponseModel<EmergencyAccessGrantorDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<EmergencyAccessGranteeDetailsResponseModel> Get(string id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
var result = await _emergencyAccessService.GetAsync(new Guid(id), userId.Value);
|
||||
return new EmergencyAccessGranteeDetailsResponseModel(result);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task Put(string id, [FromBody]EmergencyAccessUpdateRequestModel model)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(new Guid(id));
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _emergencyAccessService.SaveAsync(model.ToEmergencyAccess(emergencyAccess), userId.Value);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(string id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _emergencyAccessService.DeleteAsync(new Guid(id), userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("invite")]
|
||||
public async Task Invite([FromBody] EmergencyAccessInviteRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.InviteAsync(user, user.Name, model.Email, model.Type.Value, model.WaitTimeDays);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/reinvite")]
|
||||
public async Task Reinvite(string id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.ResendInviteAsync(user.Id, new Guid(id), user.Name);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/accept")]
|
||||
public async Task Accept(string id, [FromBody] OrganizationUserAcceptRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.AcceptUserAsync(new Guid(id), user, model.Token, _userService);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/confirm")]
|
||||
public async Task Confirm(string id, [FromBody] OrganizationUserConfirmRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _emergencyAccessService.ConfirmUserAsync(new Guid(id), model.Key, userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/initiate")]
|
||||
public async Task Initiate(string id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.InitiateAsync(new Guid(id), user);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/approve")]
|
||||
public async Task Accept(string id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.ApproveAsync(new Guid(id), user);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/reject")]
|
||||
public async Task Reject(string id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.RejectAsync(new Guid(id), user);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/takeover")]
|
||||
public async Task<EmergencyAccessTakeoverResponseModel> Takeover(string id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var (result, grantor) = await _emergencyAccessService.TakeoverAsync(new Guid(id), user);
|
||||
return new EmergencyAccessTakeoverResponseModel(result, grantor);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/password")]
|
||||
public async Task Password(string id, [FromBody] EmergencyAccessPasswordRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.PasswordAsync(new Guid(id), user, model.NewMasterPasswordHash, model.Key);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/view")]
|
||||
public async Task<EmergencyAccessViewResponseModel> ViewCiphers(string id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
return await _emergencyAccessService.ViewAsync(new Guid(id), user);
|
||||
}
|
||||
}
|
||||
}
|
27
src/Api/Jobs/EmergencyAccessNotificationJob.cs
Normal file
27
src/Api/Jobs/EmergencyAccessNotificationJob.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Jobs;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Quartz;
|
||||
|
||||
namespace Bit.Api.Jobs
|
||||
{
|
||||
public class EmergencyAccessNotificationJob : BaseJob
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public EmergencyAccessNotificationJob(IServiceScopeFactory serviceScopeFactory, ILogger<EmergencyAccessNotificationJob> logger)
|
||||
: base(logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var emergencyAccessService = scope.ServiceProvider.GetService(typeof(IEmergencyAccessService)) as IEmergencyAccessService;
|
||||
await emergencyAccessService.SendNotificationsAsync();
|
||||
}
|
||||
}
|
||||
}
|
27
src/Api/Jobs/EmergencyAccessTimeoutJob.cs
Normal file
27
src/Api/Jobs/EmergencyAccessTimeoutJob.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Jobs;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Quartz;
|
||||
|
||||
namespace Bit.Api.Jobs
|
||||
{
|
||||
public class EmergencyAccessTimeoutJob : BaseJob
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public EmergencyAccessTimeoutJob(IServiceScopeFactory serviceScopeFactory, ILogger<EmergencyAccessNotificationJob> logger)
|
||||
: base(logger)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
|
||||
{
|
||||
using var scope = _serviceScopeFactory.CreateScope();
|
||||
var emergencyAccessService = scope.ServiceProvider.GetService(typeof(IEmergencyAccessService)) as IEmergencyAccessService;
|
||||
await emergencyAccessService.HandleTimedOutRequestsAsync();
|
||||
}
|
||||
}
|
||||
}
|
@ -23,6 +23,14 @@ namespace Bit.Api.Jobs
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 0 * * * ?")
|
||||
.Build();
|
||||
var emergencyAccessNotificationTrigger = TriggerBuilder.Create()
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 * * * * ?")
|
||||
.Build();
|
||||
var emergencyAccessTimeoutTrigger = TriggerBuilder.Create()
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 * * * * ?")
|
||||
.Build();
|
||||
var everyTopOfTheSixthHourTrigger = TriggerBuilder.Create()
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 0 */6 * * ?")
|
||||
@ -35,6 +43,8 @@ namespace Bit.Api.Jobs
|
||||
Jobs = new List<Tuple<Type, ITrigger>>
|
||||
{
|
||||
new Tuple<Type, ITrigger>(typeof(AliveJob), everyTopOfTheHourTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(EmergencyAccessNotificationJob), emergencyAccessNotificationTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(EmergencyAccessTimeoutJob), emergencyAccessTimeoutTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger)
|
||||
};
|
||||
@ -45,6 +55,8 @@ namespace Bit.Api.Jobs
|
||||
public static void AddJobsServices(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<AliveJob>();
|
||||
services.AddTransient<EmergencyAccessNotificationJob>();
|
||||
services.AddTransient<EmergencyAccessTimeoutJob>();
|
||||
services.AddTransient<ValidateUsersJob>();
|
||||
services.AddTransient<ValidateOrganizationsJob>();
|
||||
}
|
||||
|
@ -126,6 +126,8 @@ namespace Bit.Api
|
||||
});
|
||||
|
||||
services.AddSwagger(globalSettings);
|
||||
Jobs.JobsHostedService.AddJobsServices(services);
|
||||
services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
|
11
src/Core/Enums/EmergencyAccessStatusType.cs
Normal file
11
src/Core/Enums/EmergencyAccessStatusType.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum EmergencyAccessStatusType : byte
|
||||
{
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2,
|
||||
RecoveryInitiated = 3,
|
||||
RecoveryApproved = 4,
|
||||
}
|
||||
}
|
8
src/Core/Enums/EmergencyAccessType.cs
Normal file
8
src/Core/Enums/EmergencyAccessType.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Enums
|
||||
{
|
||||
public enum EmergencyAccessType : byte
|
||||
{
|
||||
View = 0,
|
||||
Takeover = 1,
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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">
|
||||
This email is to notify you that {{GranteeEmail}} has accepted your invitation to become an emergency access contact.
|
||||
</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">
|
||||
To confirm this user, log into the Bitwarden web vault, go to settings and confirm the user.
|
||||
</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 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;" valign="top">
|
||||
If you do not wish to confirm this user, you can also remove them on the same page.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,7 @@
|
||||
{{#>BasicTextLayout}}
|
||||
This email is to notify you that {{GranteeEmail}} has accepted your invitation to become an emergency access contact.
|
||||
|
||||
To confirm this user, log into the Bitwarden web vault, go to settings and confirm the user.
|
||||
|
||||
If you do not wish to confirm this user, you can also remove them on the same page.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,9 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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; text-align: center;" valign="top" align="center">
|
||||
{{Name}} has approved your emergency request. You may now login on the web vault and access their account.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,3 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{Name}} has approved your emergency request. You may now login on the web vault and access their account.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,14 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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">
|
||||
This email is to notify you that you have been confirmed as an emergency access contect for <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;"></b>{{Name}}</b>.
|
||||
</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 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;" valign="top">
|
||||
You can now initiate emergency access requests from the web vault.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
This email is to notify you that you have been confirmed as an emergency access contect for {{Name}}.
|
||||
|
||||
You can now initiate emergency access requests from the web vault.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,21 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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; text-align: center;" valign="top" align="center">
|
||||
You have been invited to become an emergency contact for <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>.
|
||||
</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; 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;">
|
||||
Become emergency contact
|
||||
</a>
|
||||
</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 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: center;" valign="top" align="center">
|
||||
If you do not wish to become an emergency contact for {{Name}}, you can safely ignore this email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,7 @@
|
||||
{{#>BasicTextLayout}}
|
||||
You have been invited to become an emergency contact for {{Name}}. To accept this invite, click the following link:
|
||||
|
||||
{{{Url}}}
|
||||
|
||||
If you do not wish to become an emergency contact for {{Name}}, you can safely ignore this email.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,14 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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; text-align: center;" valign="top" align="center">
|
||||
{{Name}} has initiated an emergency request to <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;">{{Action}}</b> your account. You may login on the web vault and manually approve or reject this request.
|
||||
</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 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: center;" valign="top" align="center">
|
||||
If you do nothing, the request will be automatically approved after <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;">{{DaysLeft}} day(s)</b>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{Name}} has initiated an emergency request to {{Action}} your account. You may login on the web vault and manually approve or reject this request.
|
||||
|
||||
If you do nothing, the request will automatically be approved after {{DaysLeft}} day(s).
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,14 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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; text-align: center;" valign="top" align="center">
|
||||
{{Name}} has a pending emergency request to <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;">{{Action}}</b> your account. You may login on the web vault and manually approve or reject this request.
|
||||
</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 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: center;" valign="top" align="center">
|
||||
If you do nothing, the request will be automatically approved after <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;">{{DaysLeft}} day(s)</b>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,5 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{Name}} has a pending emergency request to {{Action}} your account. You may login on the web vault and manually approve or reject this request.
|
||||
|
||||
If you do nothing, the request will automatically be approved after {{DaysLeft}} day(s).
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,9 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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; text-align: center;" valign="top" align="center">
|
||||
{{Name}} has been granted emergency request to <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;">{{Action}}</b> your account. You may login on the web vault and manually revoke this request.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,3 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{Name}} has been granted emergency request to {{Action}} your account. You may login on the web vault and manually revoke this request.
|
||||
{{/BasicTextLayout}}
|
@ -0,0 +1,9 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" 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;">
|
||||
<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; text-align: center;" valign="top" align="center">
|
||||
{{Name}} has rejected your emergency request.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,3 @@
|
||||
{{#>BasicTextLayout}}
|
||||
{{Name}} has rejected your emergency request.
|
||||
{{/BasicTextLayout}}
|
47
src/Core/Models/Api/Request/EmergencyAccessRequstModels.cs
Normal file
47
src/Core/Models/Api/Request/EmergencyAccessRequstModels.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Models.Table;
|
||||
|
||||
namespace Bit.Core.Models.Api.Request
|
||||
{
|
||||
public class EmergencyAccessInviteRequestModel
|
||||
{
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[StringLength(50)]
|
||||
public string Email { get; set; }
|
||||
[Required]
|
||||
public Enums.EmergencyAccessType? Type { get; set; }
|
||||
[Required]
|
||||
public int WaitTimeDays { get; set; }
|
||||
}
|
||||
|
||||
public class EmergencyAccessUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
public Enums.EmergencyAccessType Type { get; set; }
|
||||
[Required]
|
||||
public int WaitTimeDays { get; set; }
|
||||
public string KeyEncrypted { get; set; }
|
||||
|
||||
public EmergencyAccess ToEmergencyAccess(EmergencyAccess existingEmergencyAccess)
|
||||
{
|
||||
// Ensure we only set keys for a confirmed emergency access.
|
||||
if (!string.IsNullOrWhiteSpace(existingEmergencyAccess.KeyEncrypted) && !string.IsNullOrWhiteSpace(KeyEncrypted))
|
||||
{
|
||||
existingEmergencyAccess.KeyEncrypted = KeyEncrypted;
|
||||
}
|
||||
existingEmergencyAccess.Type = Type;
|
||||
existingEmergencyAccess.WaitTimeDays = WaitTimeDays;
|
||||
return existingEmergencyAccess;
|
||||
}
|
||||
}
|
||||
|
||||
public class EmergencyAccessPasswordRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string NewMasterPasswordHash { get; set; }
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
}
|
||||
}
|
119
src/Core/Models/Api/Response/EmergencyAccessResponseModel.cs
Normal file
119
src/Core/Models/Api/Response/EmergencyAccessResponseModel.cs
Normal file
@ -0,0 +1,119 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
using Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.Models.Api.Response
|
||||
{
|
||||
public class EmergencyAccessResponseModel : ResponseModel
|
||||
{
|
||||
public EmergencyAccessResponseModel(EmergencyAccess emergencyAccess, string obj = "emergencyAccess") : base(obj)
|
||||
{
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(emergencyAccess));
|
||||
}
|
||||
|
||||
Id = emergencyAccess.Id.ToString();
|
||||
Status = emergencyAccess.Status;
|
||||
Type = emergencyAccess.Type;
|
||||
WaitTimeDays = emergencyAccess.WaitTimeDays;
|
||||
}
|
||||
|
||||
public EmergencyAccessResponseModel(EmergencyAccessDetails emergencyAccess, string obj = "emergencyAccess") : base(obj)
|
||||
{
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(emergencyAccess));
|
||||
}
|
||||
|
||||
Id = emergencyAccess.Id.ToString();
|
||||
Status = emergencyAccess.Status;
|
||||
Type = emergencyAccess.Type;
|
||||
WaitTimeDays = emergencyAccess.WaitTimeDays;
|
||||
}
|
||||
|
||||
public string Id { get; private set; }
|
||||
public EmergencyAccessStatusType Status { get; private set; }
|
||||
public EmergencyAccessType Type { get; private set; }
|
||||
public int WaitTimeDays { get; private set; }
|
||||
}
|
||||
|
||||
public class EmergencyAccessGranteeDetailsResponseModel : EmergencyAccessResponseModel
|
||||
{
|
||||
public EmergencyAccessGranteeDetailsResponseModel(EmergencyAccessDetails emergencyAccess)
|
||||
: base(emergencyAccess, "emergencyAccessGranteeDetails")
|
||||
{
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(emergencyAccess));
|
||||
}
|
||||
|
||||
GranteeId = emergencyAccess.GranteeId.ToString();
|
||||
Email = emergencyAccess.GranteeEmail;
|
||||
Name = emergencyAccess.GranteeName;
|
||||
}
|
||||
|
||||
public string GranteeId { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Email { get; private set; }
|
||||
}
|
||||
|
||||
public class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessResponseModel
|
||||
{
|
||||
public EmergencyAccessGrantorDetailsResponseModel(EmergencyAccessDetails emergencyAccess)
|
||||
: base(emergencyAccess, "emergencyAccessGrantorDetails")
|
||||
{
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(emergencyAccess));
|
||||
}
|
||||
|
||||
GrantorId = emergencyAccess.GrantorId.ToString();
|
||||
Email = emergencyAccess.GrantorEmail;
|
||||
Name = emergencyAccess.GrantorName;
|
||||
}
|
||||
|
||||
public string GrantorId { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public string Email { get; private set; }
|
||||
}
|
||||
|
||||
public class EmergencyAccessTakeoverResponseModel : ResponseModel
|
||||
{
|
||||
public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj)
|
||||
{
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(emergencyAccess));
|
||||
}
|
||||
|
||||
KeyEncrypted = emergencyAccess.KeyEncrypted;
|
||||
Kdf = grantor.Kdf;
|
||||
KdfIterations = grantor.KdfIterations;
|
||||
}
|
||||
|
||||
public int KdfIterations { get; private set; }
|
||||
public KdfType Kdf { get; private set; }
|
||||
public string KeyEncrypted { get; private set; }
|
||||
}
|
||||
|
||||
public class EmergencyAccessViewResponseModel : ResponseModel
|
||||
{
|
||||
public EmergencyAccessViewResponseModel(
|
||||
GlobalSettings globalSettings,
|
||||
EmergencyAccess emergencyAccess,
|
||||
IEnumerable<CipherDetails> ciphers)
|
||||
: base("emergencyAccessView")
|
||||
{
|
||||
KeyEncrypted = emergencyAccess.KeyEncrypted;
|
||||
Ciphers = ciphers.Select(c => new CipherResponseModel(c, globalSettings));
|
||||
}
|
||||
|
||||
public string KeyEncrypted { get; set; }
|
||||
public IEnumerable<CipherResponseModel> Ciphers { get; set; }
|
||||
}
|
||||
}
|
12
src/Core/Models/Data/EmergencyAccessDetails.cs
Normal file
12
src/Core/Models/Data/EmergencyAccessDetails.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Bit.Core.Models.Table;
|
||||
|
||||
namespace Bit.Core.Models.Data
|
||||
{
|
||||
public class EmergencyAccessDetails : EmergencyAccess
|
||||
{
|
||||
public string GranteeName { get; set; }
|
||||
public string GranteeEmail { get; set; }
|
||||
public string GrantorName { get; set; }
|
||||
public string GrantorEmail { get; set; }
|
||||
}
|
||||
}
|
14
src/Core/Models/Data/EmergencyAccessNotify.cs
Normal file
14
src/Core/Models/Data/EmergencyAccessNotify.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Table;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Bit.Core.Models.Data
|
||||
{
|
||||
public class EmergencyAccessNotify : EmergencyAccess
|
||||
{
|
||||
public string GrantorEmail { get; set; }
|
||||
public string GranteeName { get; set; }
|
||||
}
|
||||
}
|
7
src/Core/Models/Mail/EmergencyAccessAcceptedViewModel.cs
Normal file
7
src/Core/Models/Mail/EmergencyAccessAcceptedViewModel.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class EmergencyAccessAcceptedViewModel : BaseMailModel
|
||||
{
|
||||
public string GranteeEmail { get; set; }
|
||||
}
|
||||
}
|
7
src/Core/Models/Mail/EmergencyAccessApprovedViewModel.cs
Normal file
7
src/Core/Models/Mail/EmergencyAccessApprovedViewModel.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class EmergencyAccessApprovedViewModel : BaseMailModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class EmergencyAccessConfirmedViewModel : BaseMailModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
11
src/Core/Models/Mail/EmergencyAccessInvitedViewModel.cs
Normal file
11
src/Core/Models/Mail/EmergencyAccessInvitedViewModel.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class EmergencyAccessInvitedViewModel : BaseMailModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string Url => $"{WebVaultUrl}/accept-emergency?id={Id}&name={Name}&email={Email}&token={Token}";
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class EmergencyAccessRecoveryTimedOutViewModel : BaseMailModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Action { get; set; }
|
||||
}
|
||||
}
|
9
src/Core/Models/Mail/EmergencyAccessRecoveryViewModel.cs
Normal file
9
src/Core/Models/Mail/EmergencyAccessRecoveryViewModel.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class EmergencyAccessRecoveryViewModel : BaseMailModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Action { get; set; }
|
||||
public int DaysLeft { get; set; }
|
||||
}
|
||||
}
|
7
src/Core/Models/Mail/EmergencyAccessRejectedViewModel.cs
Normal file
7
src/Core/Models/Mail/EmergencyAccessRejectedViewModel.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Mail
|
||||
{
|
||||
public class EmergencyAccessRejectedViewModel : BaseMailModel
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
46
src/Core/Models/Table/EmergencyAccess.cs
Normal file
46
src/Core/Models/Table/EmergencyAccess.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Models.Table
|
||||
{
|
||||
public class EmergencyAccess : ITableObject<Guid>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid GrantorId { get; set; }
|
||||
public Guid? GranteeId { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string KeyEncrypted { get; set; }
|
||||
public EmergencyAccessType Type { get; set; }
|
||||
public EmergencyAccessStatusType Status { get; set; }
|
||||
public int WaitTimeDays { get; set; }
|
||||
public DateTime? RecoveryInitiatedDate { get; internal set; }
|
||||
public DateTime? LastNotificationDate { get; internal set; }
|
||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public EmergencyAccess ToEmergencyAccess()
|
||||
{
|
||||
return new EmergencyAccess
|
||||
{
|
||||
Id = Id,
|
||||
GrantorId = GrantorId,
|
||||
GranteeId = GranteeId,
|
||||
Email = Email,
|
||||
KeyEncrypted = KeyEncrypted,
|
||||
Type = Type,
|
||||
Status = Status,
|
||||
WaitTimeDays = WaitTimeDays,
|
||||
RecoveryInitiatedDate = RecoveryInitiatedDate,
|
||||
LastNotificationDate = LastNotificationDate,
|
||||
CreationDate = CreationDate,
|
||||
RevisionDate = RevisionDate,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
18
src/Core/Repositories/IEmergencyAccessRepository.cs
Normal file
18
src/Core/Repositories/IEmergencyAccessRepository.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
|
||||
namespace Bit.Core.Repositories
|
||||
{
|
||||
public interface IEmergencyAccessRepository : IRepository<EmergencyAccess, Guid>
|
||||
{
|
||||
Task<int> GetCountByGrantorIdEmailAsync(Guid grantorId, string email, bool onlyRegisteredUsers);
|
||||
Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGrantorIdAsync(Guid grantorId);
|
||||
Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGranteeIdAsync(Guid granteeId);
|
||||
Task<EmergencyAccessDetails> GetDetailsByIdGrantorIdAsync(Guid id, Guid grantorId);
|
||||
Task<ICollection<EmergencyAccessNotify>> GetManyToNotifyAsync();
|
||||
Task<ICollection<EmergencyAccessDetails>> GetExpiredRecoveriesAsync();
|
||||
}
|
||||
}
|
99
src/Core/Repositories/SqlServer/EmergencyAccessRepository.cs
Normal file
99
src/Core/Repositories/SqlServer/EmergencyAccessRepository.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using Bit.Core.Models.Table;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Core.Repositories.SqlServer
|
||||
{
|
||||
public class EmergencyAccessRepository : Repository<EmergencyAccess, Guid>, IEmergencyAccessRepository
|
||||
{
|
||||
public EmergencyAccessRepository(GlobalSettings globalSettings)
|
||||
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public EmergencyAccessRepository(string connectionString, string readOnlyConnectionString)
|
||||
: base(connectionString, readOnlyConnectionString)
|
||||
{ }
|
||||
|
||||
public async Task<int> GetCountByGrantorIdEmailAsync(Guid grantorId, string email, bool onlyRegisteredUsers)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.ExecuteScalarAsync<int>(
|
||||
"[dbo].[EmergencyAccess_ReadCountByGrantorIdEmail]",
|
||||
new { GrantorId = grantorId, Email = email, OnlyUsers = onlyRegisteredUsers },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGrantorIdAsync(Guid grantorId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<EmergencyAccessDetails>(
|
||||
"[dbo].[EmergencyAccessDetails_ReadByGrantorId]",
|
||||
new { GrantorId = grantorId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<EmergencyAccessDetails>> GetManyDetailsByGranteeIdAsync(Guid granteeId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<EmergencyAccessDetails>(
|
||||
"[dbo].[EmergencyAccessDetails_ReadByGranteeId]",
|
||||
new { GranteeId = granteeId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccessDetails> GetDetailsByIdGrantorIdAsync(Guid id, Guid grantorId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<EmergencyAccessDetails>(
|
||||
"[dbo].[EmergencyAccessDetails_ReadByIdGrantorId]",
|
||||
new { Id = id, GrantorId = grantorId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<EmergencyAccessNotify>> GetManyToNotifyAsync()
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<EmergencyAccessNotify>(
|
||||
"[dbo].[EmergencyAccess_ReadToNotify]",
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<EmergencyAccessDetails>> GetExpiredRecoveriesAsync()
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<EmergencyAccessDetails>(
|
||||
"[dbo].[EmergencyAccessDetails_ReadExpiredRecoveries]",
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
28
src/Core/Services/IEmergencyAccessService.cs
Normal file
28
src/Core/Services/IEmergencyAccessService.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public interface IEmergencyAccessService
|
||||
{
|
||||
Task<EmergencyAccess> InviteAsync(User invitingUser, string invitingUsersName, string email, EmergencyAccessType type, int waitTime);
|
||||
Task ResendInviteAsync(Guid invitingUserId, Guid emergencyAccessId, string invitingUsersName);
|
||||
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService);
|
||||
Task DeleteAsync(Guid emergencyAccessId, Guid grantorId);
|
||||
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
|
||||
Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid userId);
|
||||
Task SaveAsync(EmergencyAccess emergencyAccess, Guid savingUserId);
|
||||
Task InitiateAsync(Guid id, User initiatingUser);
|
||||
Task ApproveAsync(Guid id, User approvingUser);
|
||||
Task RejectAsync(Guid id, User rejectingUser);
|
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser);
|
||||
Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key);
|
||||
Task SendNotificationsAsync();
|
||||
Task HandleTimedOutRequestsAsync();
|
||||
Task<EmergencyAccessViewResponseModel> ViewAsync(Guid id, User user);
|
||||
}
|
||||
}
|
@ -29,5 +29,13 @@ namespace Bit.Core.Services
|
||||
Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip);
|
||||
Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip);
|
||||
Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email);
|
||||
Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token);
|
||||
Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email);
|
||||
Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email);
|
||||
Task SendEmergencyAccessRecoveryInitiated(EmergencyAccess emergencyAccess, string initiatingName, string email);
|
||||
Task SendEmergencyAccessRecoveryApproved(EmergencyAccess emergencyAccess, string approvingName, string email);
|
||||
Task SendEmergencyAccessRecoveryRejected(EmergencyAccess emergencyAccess, string rejectingName, string email);
|
||||
Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email);
|
||||
Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess ea, string initiatingName, string email);
|
||||
}
|
||||
}
|
||||
|
314
src/Core/Services/Implementations/EmergencyAccessService.cs
Normal file
314
src/Core/Services/Implementations/EmergencyAccessService.cs
Normal file
@ -0,0 +1,314 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Api.Response;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class EmergencyAccessService : IEmergencyAccessService
|
||||
{
|
||||
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ICipherRepository _cipherRepository;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IDataProtector _dataProtector;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
|
||||
public EmergencyAccessService(
|
||||
IEmergencyAccessRepository emergencyAccessRepository,
|
||||
IUserRepository userRepository,
|
||||
ICipherRepository cipherRepository,
|
||||
IMailService mailService,
|
||||
IPasswordHasher<User> passwordHasher,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_emergencyAccessRepository = emergencyAccessRepository;
|
||||
_userRepository = userRepository;
|
||||
_cipherRepository = cipherRepository;
|
||||
_mailService = mailService;
|
||||
_passwordHasher = passwordHasher;
|
||||
_dataProtector = dataProtectionProvider.CreateProtector("EmergencyAccessServiceDataProtector");
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> InviteAsync(User invitingUser, string invitingUsersName, string email, EmergencyAccessType type, int waitTime)
|
||||
{
|
||||
if (!invitingUser.Premium)
|
||||
{
|
||||
throw new BadRequestException("Not a premium user.");
|
||||
}
|
||||
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
{
|
||||
GrantorId = invitingUser.Id,
|
||||
Email = email.ToLowerInvariant(),
|
||||
Status = EmergencyAccessStatusType.Invited,
|
||||
Type = type,
|
||||
WaitTimeDays = waitTime,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await _emergencyAccessRepository.CreateAsync(emergencyAccess);
|
||||
await SendInviteAsync(emergencyAccess, invitingUsersName);
|
||||
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid userId)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, userId);
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task ResendInviteAsync(Guid invitingUserId, Guid emergencyAccessId, string invitingUsersName)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != invitingUserId ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.Invited)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
await SendInviteAsync(emergencyAccess, invitingUsersName);
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
if (!CoreHelpers.TokenIsValid("EmergencyAccessInvite", _dataProtector, token, user.Email, emergencyAccessId, _globalSettings))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(emergencyAccess.Email) ||
|
||||
!emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new BadRequestException("User email does not match invite.");
|
||||
}
|
||||
|
||||
if (emergencyAccess.Status != EmergencyAccessStatusType.Invited)
|
||||
{
|
||||
throw new BadRequestException("Already accepted.");
|
||||
}
|
||||
|
||||
var granteeEmail = emergencyAccess.Email;
|
||||
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
|
||||
emergencyAccess.GranteeId = user.Id;
|
||||
emergencyAccess.Email = null;
|
||||
|
||||
var grantor = await userService.GetUserByIdAsync(emergencyAccess.GrantorId);
|
||||
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
await _mailService.SendEmergencyAccessAcceptedEmailAsync(granteeEmail, grantor.Email);
|
||||
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid emergencyAccessId, Guid grantorId)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null || (emergencyAccess.GrantorId != grantorId && emergencyAccess.GranteeId != grantorId))
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
await _emergencyAccessRepository.DeleteAsync(emergencyAccess);
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAcccessId, string key, Guid confirmingUserId)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAcccessId);
|
||||
if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted ||
|
||||
emergencyAccess.GrantorId != confirmingUserId)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
var grantor = await _userRepository.GetByIdAsync(confirmingUserId);
|
||||
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
|
||||
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;
|
||||
emergencyAccess.KeyEncrypted = key;
|
||||
emergencyAccess.Email = null;
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
await _mailService.SendEmergencyAccessConfirmedEmailAsync(grantor.Name, grantee.Email);
|
||||
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(EmergencyAccess emergencyAccess, Guid savingUserId)
|
||||
{
|
||||
if (emergencyAccess.GrantorId != savingUserId)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
}
|
||||
|
||||
public async Task InitiateAsync(Guid id, User initiatingUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.Confirmed)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated;
|
||||
emergencyAccess.RevisionDate = now;
|
||||
emergencyAccess.RecoveryInitiatedDate = now;
|
||||
emergencyAccess.LastNotificationDate = now;
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
|
||||
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
|
||||
|
||||
await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, initiatingUser.Name, grantor.Email);
|
||||
}
|
||||
|
||||
public async Task ApproveAsync(Guid id, User approvingUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != approvingUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved;
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
|
||||
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
|
||||
await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, approvingUser.Name, grantee.Email);
|
||||
}
|
||||
|
||||
public async Task RejectAsync(Guid id, User rejectingUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != rejectingUser.Id ||
|
||||
(emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated &&
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved))
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Confirmed;
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
|
||||
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
|
||||
await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, rejectingUser.Name, grantee.Email);
|
||||
}
|
||||
|
||||
public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User requestingUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GranteeId != requestingUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
|
||||
|
||||
return (emergencyAccess, grantor);
|
||||
}
|
||||
|
||||
public async Task PasswordAsync(Guid id, User requestingUser, string newMasterPasswordHash, string key)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GranteeId != requestingUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId);
|
||||
|
||||
grantor.MasterPassword = _passwordHasher.HashPassword(grantor, newMasterPasswordHash);
|
||||
grantor.Key = key;
|
||||
// Disable TwoFactor providers since they will otherwise block logins
|
||||
grantor.SetTwoFactorProviders(new Dictionary<TwoFactorProviderType, TwoFactorProvider>());
|
||||
await _userRepository.ReplaceAsync(grantor);
|
||||
}
|
||||
|
||||
public async Task SendNotificationsAsync()
|
||||
{
|
||||
var toNotify = await _emergencyAccessRepository.GetManyToNotifyAsync();
|
||||
|
||||
foreach (var notify in toNotify)
|
||||
{
|
||||
var ea = notify.ToEmergencyAccess();
|
||||
ea.LastNotificationDate = DateTime.UtcNow;
|
||||
await _emergencyAccessRepository.ReplaceAsync(ea);
|
||||
|
||||
await _mailService.SendEmergencyAccessRecoveryReminder(ea, notify.GranteeName, notify.GrantorEmail);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleTimedOutRequestsAsync()
|
||||
{
|
||||
var expired = await _emergencyAccessRepository.GetExpiredRecoveriesAsync();
|
||||
|
||||
foreach (var details in expired)
|
||||
{
|
||||
var ea = details.ToEmergencyAccess();
|
||||
ea.Status = EmergencyAccessStatusType.RecoveryApproved;
|
||||
await _emergencyAccessRepository.ReplaceAsync(ea);
|
||||
|
||||
await _mailService.SendEmergencyAccessRecoveryApproved(ea, details.GrantorName, details.GranteeEmail);
|
||||
await _mailService.SendEmergencyAccessRecoveryTimedOut(ea, details.GranteeName, details.GrantorEmail);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccessViewResponseModel> ViewAsync(Guid id, User requestingUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GranteeId != requestingUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(emergencyAccess.GrantorId, false);
|
||||
|
||||
return new EmergencyAccessViewResponseModel(_globalSettings, emergencyAccess, ciphers);
|
||||
}
|
||||
|
||||
private async Task SendInviteAsync(EmergencyAccess emergencyAccess, string invitingUsersName)
|
||||
{
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var token = _dataProtector.Protect($"EmergencyAccessInvite {emergencyAccess.Id} {emergencyAccess.Email} {nowMillis}");
|
||||
await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);
|
||||
}
|
||||
}
|
||||
}
|
@ -489,5 +489,121 @@ namespace Bit.Core.Services
|
||||
writer.WriteSafeString($"<a href=\"{href}\" target=\"_blank\" {clickTrackingText}>{text}</a>");
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
|
||||
{
|
||||
var message = CreateDefaultMessage($"Emergency Access Contact Invite", emergencyAccess.Email);
|
||||
var model = new EmergencyAccessInvitedViewModel
|
||||
{
|
||||
Name = CoreHelpers.SanitizeForEmail(name),
|
||||
Email = WebUtility.UrlEncode(emergencyAccess.Email),
|
||||
Id = emergencyAccess.Id.ToString(),
|
||||
Token = WebUtility.UrlEncode(token),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessInvited", model);
|
||||
message.Category = "EmergencyAccessInvited";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"Accepted Emergency Access", email);
|
||||
var model = new EmergencyAccessAcceptedViewModel
|
||||
{
|
||||
GranteeEmail = CoreHelpers.SanitizeForEmail(granteeEmail),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessAccepted", model);
|
||||
message.Category = "EmergencyAccessAccepted";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage($"You Have Been Confirmed as Emergency Access Contact", email);
|
||||
var model = new EmergencyAccessConfirmedViewModel
|
||||
{
|
||||
Name = CoreHelpers.SanitizeForEmail(grantorName),
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessConfirmed", model);
|
||||
message.Category = "EmergencyAccessConfirmed";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessRecoveryInitiated(EmergencyAccess emergencyAccess, string initiatingName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("Emergency Access Initiated", email);
|
||||
|
||||
var remainingTime = DateTime.UtcNow - emergencyAccess.RecoveryInitiatedDate.GetValueOrDefault();
|
||||
|
||||
var model = new EmergencyAccessRecoveryViewModel
|
||||
{
|
||||
Name = CoreHelpers.SanitizeForEmail(initiatingName),
|
||||
Action = emergencyAccess.Type.ToString(),
|
||||
DaysLeft = emergencyAccess.WaitTimeDays - Convert.ToInt32((remainingTime).TotalDays),
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessRecovery", model);
|
||||
message.Category = "EmergencyAccessRecovery";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessRecoveryApproved(EmergencyAccess emergencyAccess, string approvingName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("Emergency Access Approved", email);
|
||||
var model = new EmergencyAccessApprovedViewModel
|
||||
{
|
||||
Name = CoreHelpers.SanitizeForEmail(approvingName),
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessApproved", model);
|
||||
message.Category = "EmergencyAccessApproved";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessRecoveryRejected(EmergencyAccess emergencyAccess, string rejectingName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("Emergency Access Rejected", email);
|
||||
var model = new EmergencyAccessRejectedViewModel
|
||||
{
|
||||
Name = CoreHelpers.SanitizeForEmail(rejectingName),
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessRejected", model);
|
||||
message.Category = "EmergencyAccessRejected";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("Pending Emergency Access Request", email);
|
||||
|
||||
var remainingTime = DateTime.UtcNow - emergencyAccess.RecoveryInitiatedDate.GetValueOrDefault();
|
||||
|
||||
var model = new EmergencyAccessRecoveryViewModel
|
||||
{
|
||||
Name = CoreHelpers.SanitizeForEmail(initiatingName),
|
||||
Action = emergencyAccess.Type.ToString(),
|
||||
DaysLeft = emergencyAccess.WaitTimeDays - Convert.ToInt32((remainingTime).TotalDays),
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessRecoveryReminder", model);
|
||||
message.Category = "EmergencyAccessRecoveryReminder";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess emergencyAccess, string initiatingName, string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("Emergency Access Granted", email);
|
||||
var model = new EmergencyAccessRecoveryTimedOutViewModel
|
||||
{
|
||||
Name = CoreHelpers.SanitizeForEmail(initiatingName),
|
||||
Action = emergencyAccess.Type.ToString(),
|
||||
};
|
||||
await AddMessageContentAsync(message, "EmergencyAccessRecoveryTimedOut", model);
|
||||
message.Category = "EmergencyAccessRecoveryTimedOut";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -107,5 +107,45 @@ namespace Bit.Core.Services
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessRecoveryInitiated(EmergencyAccess emergencyAccess, string initiatingName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessRecoveryApproved(EmergencyAccess emergencyAccess, string approvingName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessRecoveryRejected(EmergencyAccess emergencyAccess, string rejectingName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessRecoveryReminder(EmergencyAccess emergencyAccess, string initiatingName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendEmergencyAccessRecoveryTimedOut(EmergencyAccess ea, string initiatingName, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -553,14 +553,20 @@ namespace Bit.Core.Utilities
|
||||
|
||||
public static bool UserInviteTokenIsValid(IDataProtector protector, string token, string userEmail,
|
||||
Guid orgUserId, GlobalSettings globalSettings)
|
||||
{
|
||||
return TokenIsValid("OrganizationUserInvite", protector, token, userEmail, orgUserId, globalSettings);
|
||||
}
|
||||
|
||||
public static bool TokenIsValid(string firstTokenPart, IDataProtector protector, string token, string userEmail,
|
||||
Guid id, GlobalSettings globalSettings)
|
||||
{
|
||||
var invalid = true;
|
||||
try
|
||||
{
|
||||
var unprotectedData = protector.Unprotect(token);
|
||||
var dataParts = unprotectedData.Split(' ');
|
||||
if (dataParts.Length == 4 && dataParts[0] == "OrganizationUserInvite" &&
|
||||
new Guid(dataParts[1]) == orgUserId &&
|
||||
if (dataParts.Length == 4 && dataParts[0] == firstTokenPart &&
|
||||
new Guid(dataParts[1]) == id &&
|
||||
dataParts[2].Equals(userEmail, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var creationTime = FromEpocMilliseconds(Convert.ToInt64(dataParts[3]));
|
||||
|
@ -80,6 +80,7 @@ namespace Bit.Core.Utilities
|
||||
services.AddSingleton<ISsoUserRepository, SqlServerRepos.SsoUserRepository>();
|
||||
services.AddSingleton<ISendRepository, SqlServerRepos.SendRepository>();
|
||||
services.AddSingleton<ITaxRateRepository, SqlServerRepos.TaxRateRepository>();
|
||||
services.AddSingleton<IEmergencyAccessRepository, SqlServerRepos.EmergencyAccessRepository>();
|
||||
}
|
||||
|
||||
if (globalSettings.SelfHosted)
|
||||
@ -112,6 +113,7 @@ namespace Bit.Core.Utilities
|
||||
services.AddScoped<IGroupService, GroupService>();
|
||||
services.AddScoped<IPolicyService, PolicyService>();
|
||||
services.AddScoped<Services.IEventService, EventService>();
|
||||
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
||||
services.AddSingleton<IDeviceService, DeviceService>();
|
||||
services.AddSingleton<IAppleIapService, AppleIapService>();
|
||||
services.AddSingleton<ISsoConfigService, SsoConfigService>();
|
||||
|
@ -69,6 +69,7 @@
|
||||
<Folder Include="dbo\User Defined Types\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccessDetails_ReadByIdGrantorId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\SsoConfig_Create.sql" />
|
||||
<Build Include="dbo\Stored Procedures\SsoConfig_ReadByIdentifier.sql" />
|
||||
<Build Include="dbo\Stored Procedures\SsoConfig_ReadByOrganizationId.sql" />
|
||||
@ -287,6 +288,18 @@
|
||||
<Build Include="dbo\Views\SendView.sql" />
|
||||
<Build Include="dbo\Stored Procedures\OrganizationUser_ReadByUserIds.sql" />
|
||||
<Build Include="dbo\Stored Procedures\Send_ReadByDeletionDateBefore.sql" />
|
||||
<Build Include="dbo\Tables\EmergencyAccess.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccess_Create.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccess_ReadById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccess_ReadCountByGrantorIdEmail.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccess_Update.sql" />
|
||||
<Build Include="dbo\Views\EmergencyAccessDetailsView.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccessDetails_ReadByGrantorId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccess_DeleteById.sql" />
|
||||
<Build Include="dbo\Stored Procedures\User_BumpAccountRevisionDateByEmergencyAccessGranteeId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccessDetails_ReadByGranteeId.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccess_ReadToNotify.sql" />
|
||||
<Build Include="dbo\Stored Procedures\EmergencyAccessDetails_ReadExpiredRecoveries.sql" />
|
||||
<Build Include="dbo\Tables\TaxRate.sql" />
|
||||
<Build Include="dbo\Stored Procedures\TaxRate_Search.sql" />
|
||||
<Build Include="dbo\Stored Procedures\TaxRate_ReadByLocation.sql" />
|
||||
@ -298,4 +311,3 @@
|
||||
<Build Include="dbo\Stored Procedures\OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId.sql" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGranteeId]
|
||||
@GranteeId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[GranteeId] = @GranteeId
|
||||
END
|
@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGrantorId]
|
||||
@GrantorId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[GrantorId] = @GrantorId
|
||||
END
|
@ -0,0 +1,16 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByIdGrantorId]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@GrantorId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
AND
|
||||
[GrantorId] = @GrantorId
|
||||
END
|
@ -0,0 +1,14 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadExpiredRecoveries]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[Status] = 3
|
||||
AND
|
||||
DATEADD(DAY, [WaitTimeDays], [RecoveryInitiatedDate]) <= GETUTCDATE()
|
||||
END
|
48
src/Sql/dbo/Stored Procedures/EmergencyAccess_Create.sql
Normal file
48
src/Sql/dbo/Stored Procedures/EmergencyAccess_Create.sql
Normal file
@ -0,0 +1,48 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_Create]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@GrantorId UNIQUEIDENTIFIER,
|
||||
@GranteeId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(50),
|
||||
@KeyEncrypted VARCHAR(MAX),
|
||||
@Type TINYINT,
|
||||
@Status TINYINT,
|
||||
@WaitTimeDays SMALLINT,
|
||||
@RecoveryInitiatedDate DATETIME2(7),
|
||||
@LastNotificationDate DATETIME2(7),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[EmergencyAccess]
|
||||
(
|
||||
[Id],
|
||||
[GrantorId],
|
||||
[GranteeId],
|
||||
[Email],
|
||||
[KeyEncrypted],
|
||||
[Type],
|
||||
[Status],
|
||||
[WaitTimeDays],
|
||||
[RecoveryInitiatedDate],
|
||||
[LastNotificationDate],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@GrantorId,
|
||||
@GranteeId,
|
||||
@Email,
|
||||
@KeyEncrypted,
|
||||
@Type,
|
||||
@Status,
|
||||
@WaitTimeDays,
|
||||
@RecoveryInitiatedDate,
|
||||
@LastNotificationDate,
|
||||
@CreationDate,
|
||||
@RevisionDate
|
||||
)
|
||||
END
|
14
src/Sql/dbo/Stored Procedures/EmergencyAccess_DeleteById.sql
Normal file
14
src/Sql/dbo/Stored Procedures/EmergencyAccess_DeleteById.sql
Normal file
@ -0,0 +1,14 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_DeleteById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId] @Id
|
||||
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[EmergencyAccess]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
13
src/Sql/dbo/Stored Procedures/EmergencyAccess_ReadById.sql
Normal file
13
src/Sql/dbo/Stored Procedures/EmergencyAccess_ReadById.sql
Normal file
@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_ReadById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccess]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
@ -0,0 +1,21 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_ReadCountByGrantorIdEmail]
|
||||
@GrantorId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(50),
|
||||
@OnlyUsers BIT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[EmergencyAccess] EA
|
||||
LEFT JOIN
|
||||
[dbo].[User] U ON EA.[GranteeId] = U.[Id]
|
||||
WHERE
|
||||
EA.[GrantorId] = @GrantorId
|
||||
AND (
|
||||
(@OnlyUsers = 0 AND (EA.[Email] = @Email OR U.[Email] = @Email))
|
||||
OR (@OnlyUsers = 1 AND U.[Email] = @Email)
|
||||
)
|
||||
END
|
@ -0,0 +1,22 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_ReadToNotify]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
EA.*,
|
||||
Grantee.Name as GranteeName,
|
||||
Grantor.Email as GrantorEmail
|
||||
FROM
|
||||
[dbo].[EmergencyAccess] EA
|
||||
LEFT JOIN
|
||||
[dbo].[User] Grantor ON Grantor.[Id] = EA.[GrantorId]
|
||||
LEFT JOIN
|
||||
[dbo].[User] Grantee On Grantee.[Id] = EA.[GranteeId]
|
||||
WHERE
|
||||
EA.[Status] = 3
|
||||
AND
|
||||
DATEADD(DAY, EA.[WaitTimeDays] - 1, EA.[RecoveryInitiatedDate]) <= GETUTCDATE()
|
||||
AND
|
||||
DATEADD(DAY, 1, EA.[LastNotificationDate]) <= GETUTCDATE()
|
||||
END
|
36
src/Sql/dbo/Stored Procedures/EmergencyAccess_Update.sql
Normal file
36
src/Sql/dbo/Stored Procedures/EmergencyAccess_Update.sql
Normal file
@ -0,0 +1,36 @@
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_Update]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@GrantorId UNIQUEIDENTIFIER,
|
||||
@GranteeId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(50),
|
||||
@KeyEncrypted VARCHAR(MAX),
|
||||
@Type TINYINT,
|
||||
@Status TINYINT,
|
||||
@WaitTimeDays SMALLINT,
|
||||
@RecoveryInitiatedDate DATETIME2(7),
|
||||
@LastNotificationDate DATETIME2(7),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[EmergencyAccess]
|
||||
SET
|
||||
[GrantorId] = @GrantorId,
|
||||
[GranteeId] = @GranteeId,
|
||||
[Email] = @Email,
|
||||
[KeyEncrypted] = @KeyEncrypted,
|
||||
[Type] = @Type,
|
||||
[Status] = @Status,
|
||||
[WaitTimeDays] = @WaitTimeDays,
|
||||
[RecoveryInitiatedDate] = @RecoveryInitiatedDate,
|
||||
[LastNotificationDate] = @LastNotificationDate,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDate] @GranteeId
|
||||
END
|
@ -0,0 +1,18 @@
|
||||
CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId]
|
||||
@EmergencyAccessId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
U
|
||||
SET
|
||||
U.[AccountRevisionDate] = GETUTCDATE()
|
||||
FROM
|
||||
[dbo].[User] U
|
||||
INNER JOIN
|
||||
[dbo].[EmergencyAccess] EA ON EA.[GranteeId] = U.[Id]
|
||||
WHERE
|
||||
EA.[Id] = @EmergencyAccessId
|
||||
AND EA.[Status] = 2 -- Confirmed
|
||||
END
|
@ -79,6 +79,15 @@ BEGIN
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete Emergency Accesses
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[EmergencyAccess]
|
||||
WHERE
|
||||
[GrantorId] = @Id
|
||||
OR
|
||||
[GranteeId] = @Id
|
||||
|
||||
-- Finally, delete the user
|
||||
DELETE
|
||||
FROM
|
||||
|
18
src/Sql/dbo/Tables/EmergencyAccess.sql
Normal file
18
src/Sql/dbo/Tables/EmergencyAccess.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE TABLE [dbo].[EmergencyAccess]
|
||||
(
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[GrantorId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[GranteeId] UNIQUEIDENTIFIER NULL,
|
||||
[Email] NVARCHAR (50) NULL,
|
||||
[KeyEncrypted] VARCHAR (MAX) NULL,
|
||||
[WaitTimeDays] SMALLINT NULL,
|
||||
[Type] TINYINT NOT NULL,
|
||||
[Status] TINYINT NOT NULL,
|
||||
[RecoveryInitiatedDate] DATETIME2 (7) NULL,
|
||||
[LastNotificationDate] DATETIME2 (7) NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
CONSTRAINT [PK_EmergencyAccess] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_EmergencyAccess_GrantorId] FOREIGN KEY ([GrantorId]) REFERENCES [dbo].[User] ([Id]),
|
||||
CONSTRAINT [FK_EmergencyAccess_GranteeId] FOREIGN KEY ([GranteeId]) REFERENCES [dbo].[User] ([Id])
|
||||
)
|
14
src/Sql/dbo/Views/EmergencyAccessDetailsView.sql
Normal file
14
src/Sql/dbo/Views/EmergencyAccessDetailsView.sql
Normal file
@ -0,0 +1,14 @@
|
||||
CREATE VIEW [dbo].[EmergencyAccessDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
EA.*,
|
||||
GranteeU.[Name] GranteeName,
|
||||
ISNULL(GranteeU.[Email], EA.[Email]) GranteeEmail,
|
||||
GrantorU.[Name] GrantorName,
|
||||
GrantorU.[Email] GrantorEmail
|
||||
FROM
|
||||
[dbo].[EmergencyAccess] EA
|
||||
LEFT JOIN
|
||||
[dbo].[User] GranteeU ON GranteeU.[Id] = EA.[GranteeId]
|
||||
LEFT JOIN
|
||||
[dbo].[User] GrantorU ON GrantorU.[Id] = EA.[GrantorId]
|
473
util/Migrator/DbScripts/2020-11-18_00_EmergencyAccess.sql
Normal file
473
util/Migrator/DbScripts/2020-11-18_00_EmergencyAccess.sql
Normal file
@ -0,0 +1,473 @@
|
||||
/*
|
||||
* Add support for Emergency Access
|
||||
*/
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccess]') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[EmergencyAccess] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[GrantorId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[GranteeId] UNIQUEIDENTIFIER NULL,
|
||||
[Email] NVARCHAR (50) NULL,
|
||||
[KeyEncrypted] VARCHAR (MAX) NULL,
|
||||
[WaitTimeDays] SMALLINT NULL,
|
||||
[Type] TINYINT NOT NULL,
|
||||
[Status] TINYINT NOT NULL,
|
||||
[RecoveryInitiatedDate] DATETIME2 (7) NULL,
|
||||
[LastNotificationDate] DATETIME2 (7) NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
CONSTRAINT [PK_EmergencyAccess] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
ALTER TABLE [dbo].[EmergencyAccess] WITH NOCHECK
|
||||
ADD CONSTRAINT [FK_EmergencyAccess_GrantorId] FOREIGN KEY ([GrantorId]) REFERENCES [dbo].[User] ([Id]);
|
||||
|
||||
ALTER TABLE [dbo].[EmergencyAccess] WITH NOCHECK
|
||||
ADD CONSTRAINT [FK_EmergencyAccess_GranteeId] FOREIGN KEY ([GranteeId]) REFERENCES [dbo].[User] ([Id]);
|
||||
END
|
||||
GO
|
||||
|
||||
IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'EmergencyAccessDetailsView')
|
||||
BEGIN
|
||||
DROP VIEW [dbo].[EmergencyAccessDetailsView]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE VIEW [dbo].[EmergencyAccessDetailsView]
|
||||
AS
|
||||
SELECT
|
||||
EA.*,
|
||||
GranteeU.[Name] GranteeName,
|
||||
ISNULL(GranteeU.[Email], EA.[Email]) GranteeEmail,
|
||||
GrantorU.[Name] GrantorName,
|
||||
GrantorU.[Email] GrantorEmail
|
||||
FROM
|
||||
[dbo].[EmergencyAccess] EA
|
||||
LEFT JOIN
|
||||
[dbo].[User] GranteeU ON GranteeU.[Id] = EA.[GranteeId]
|
||||
LEFT JOIN
|
||||
[dbo].[User] GrantorU ON GrantorU.[Id] = EA.[GrantorId]
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[User_DeleteById]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[User_DeleteById]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[User_DeleteById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
WITH RECOMPILE
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
DECLARE @BatchSize INT = 100
|
||||
|
||||
-- Delete ciphers
|
||||
WHILE @BatchSize > 0
|
||||
BEGIN
|
||||
BEGIN TRANSACTION User_DeleteById_Ciphers
|
||||
|
||||
DELETE TOP(@BatchSize)
|
||||
FROM
|
||||
[dbo].[Cipher]
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
SET @BatchSize = @@ROWCOUNT
|
||||
|
||||
COMMIT TRANSACTION User_DeleteById_Ciphers
|
||||
END
|
||||
|
||||
BEGIN TRANSACTION User_DeleteById
|
||||
|
||||
-- Delete folders
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[Folder]
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete devices
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[Device]
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete collection users
|
||||
DELETE
|
||||
CU
|
||||
FROM
|
||||
[dbo].[CollectionUser] CU
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId]
|
||||
WHERE
|
||||
OU.[UserId] = @Id
|
||||
|
||||
-- Delete group users
|
||||
DELETE
|
||||
GU
|
||||
FROM
|
||||
[dbo].[GroupUser] GU
|
||||
INNER JOIN
|
||||
[dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId]
|
||||
WHERE
|
||||
OU.[UserId] = @Id
|
||||
|
||||
-- Delete organization users
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[OrganizationUser]
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete U2F logins
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[U2f]
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete SSO Users
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[SsoUser]
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete Emergency Accesses
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[EmergencyAccess]
|
||||
WHERE
|
||||
[GrantorId] = @Id
|
||||
OR
|
||||
[GranteeId] = @Id
|
||||
|
||||
-- Finally, delete the user
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[User]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
COMMIT TRANSACTION User_DeleteById
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccess_Create]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccess_Create]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_Create]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@GrantorId UNIQUEIDENTIFIER,
|
||||
@GranteeId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(50),
|
||||
@KeyEncrypted VARCHAR(MAX),
|
||||
@Type TINYINT,
|
||||
@Status TINYINT,
|
||||
@WaitTimeDays SMALLINT,
|
||||
@RecoveryInitiatedDate DATETIME2(7),
|
||||
@LastNotificationDate DATETIME2(7),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[EmergencyAccess]
|
||||
(
|
||||
[Id],
|
||||
[GrantorId],
|
||||
[GranteeId],
|
||||
[Email],
|
||||
[KeyEncrypted],
|
||||
[Type],
|
||||
[Status],
|
||||
[WaitTimeDays],
|
||||
[RecoveryInitiatedDate],
|
||||
[LastNotificationDate],
|
||||
[CreationDate],
|
||||
[RevisionDate]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@GrantorId,
|
||||
@GranteeId,
|
||||
@Email,
|
||||
@KeyEncrypted,
|
||||
@Type,
|
||||
@Status,
|
||||
@WaitTimeDays,
|
||||
@RecoveryInitiatedDate,
|
||||
@LastNotificationDate,
|
||||
@CreationDate,
|
||||
@RevisionDate
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccess_ReadById]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccess_ReadById]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_ReadById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccess]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccess_ReadCountByGrantorIdEmail]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccess_ReadCountByGrantorIdEmail]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_ReadCountByGrantorIdEmail]
|
||||
@GrantorId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(50),
|
||||
@OnlyUsers BIT
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[EmergencyAccess] EA
|
||||
LEFT JOIN
|
||||
[dbo].[User] U ON EA.[GranteeId] = U.[Id]
|
||||
WHERE
|
||||
EA.[GrantorId] = @GrantorId
|
||||
AND (
|
||||
(@OnlyUsers = 0 AND (EA.[Email] = @Email OR U.[Email] = @Email))
|
||||
OR (@OnlyUsers = 1 AND U.[Email] = @Email)
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccess_ReadToNotify]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccess_ReadToNotify]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_ReadToNotify]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
EA.*,
|
||||
Grantee.Name as GranteeName,
|
||||
Grantor.Email as GrantorEmail
|
||||
FROM
|
||||
[dbo].[EmergencyAccess] EA
|
||||
LEFT JOIN
|
||||
[dbo].[User] Grantor ON Grantor.[Id] = EA.[GrantorId]
|
||||
LEFT JOIN
|
||||
[dbo].[User] Grantee On Grantee.[Id] = EA.[GranteeId]
|
||||
WHERE
|
||||
EA.[Status] = 3
|
||||
AND
|
||||
DATEADD(DAY, EA.[WaitTimeDays] - 1, EA.[RecoveryInitiatedDate]) <= GETUTCDATE()
|
||||
AND
|
||||
DATEADD(DAY, 1, EA.[LastNotificationDate]) <= GETUTCDATE()
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccess_Update]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccess_Update]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_Update]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@GrantorId UNIQUEIDENTIFIER,
|
||||
@GranteeId UNIQUEIDENTIFIER,
|
||||
@Email NVARCHAR(50),
|
||||
@KeyEncrypted VARCHAR(MAX),
|
||||
@Type TINYINT,
|
||||
@Status TINYINT,
|
||||
@WaitTimeDays SMALLINT,
|
||||
@RecoveryInitiatedDate DATETIME2(7),
|
||||
@LastNotificationDate DATETIME2(7),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[EmergencyAccess]
|
||||
SET
|
||||
[GrantorId] = @GrantorId,
|
||||
[GranteeId] = @GranteeId,
|
||||
[Email] = @Email,
|
||||
[KeyEncrypted] = @KeyEncrypted,
|
||||
[Type] = @Type,
|
||||
[Status] = @Status,
|
||||
[WaitTimeDays] = @WaitTimeDays,
|
||||
[RecoveryInitiatedDate] = @RecoveryInitiatedDate,
|
||||
[LastNotificationDate] = @LastNotificationDate,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDate] @GranteeId
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccessDetails_ReadByGranteeId]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGranteeId]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGranteeId]
|
||||
@GranteeId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[GranteeId] = @GranteeId
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccessDetails_ReadByGrantorId]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGrantorId]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByGrantorId]
|
||||
@GrantorId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[GrantorId] = @GrantorId
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccessDetails_ReadByIdGrantorId]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccessDetails_ReadByIdGrantorId]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadByIdGrantorId]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@GrantorId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
AND
|
||||
[GrantorId] = @GrantorId
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccessDetails_ReadExpiredRecoveries]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccessDetails_ReadExpiredRecoveries]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccessDetails_ReadExpiredRecoveries]
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[EmergencyAccessDetailsView]
|
||||
WHERE
|
||||
[Status] = 3
|
||||
AND
|
||||
DATEADD(DAY, [WaitTimeDays], [RecoveryInitiatedDate]) <= GETUTCDATE()
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId]
|
||||
@EmergencyAccessId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
U
|
||||
SET
|
||||
U.[AccountRevisionDate] = GETUTCDATE()
|
||||
FROM
|
||||
[dbo].[User] U
|
||||
INNER JOIN
|
||||
[dbo].[EmergencyAccess] EA ON EA.[GranteeId] = U.[Id]
|
||||
WHERE
|
||||
EA.[Id] = @EmergencyAccessId
|
||||
AND EA.[Status] = 2 -- Confirmed
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EmergencyAccess_DeleteById]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[EmergencyAccess_DeleteById]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE PROCEDURE [dbo].[EmergencyAccess_DeleteById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByEmergencyAccessGranteeId] @Id
|
||||
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[EmergencyAccess]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
Loading…
Reference in New Issue
Block a user