mirror of
https://github.com/bitwarden/server.git
synced 2025-05-22 20:11:04 -05:00
doc: adding readme and comments to code for emergency access feature.
This commit is contained in:
parent
3c6c69ac33
commit
74adef4b6b
@ -90,6 +90,13 @@ public class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessRespons
|
||||
|
||||
public class EmergencyAccessTakeoverResponseModel : ResponseModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="EmergencyAccessTakeoverResponseModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccess">Consumed for the Encrypted Key value</param>
|
||||
/// <param name="grantor">consumed for the KDF configuration</param>
|
||||
/// <param name="obj">name of the objet</param>
|
||||
/// <exception cref="ArgumentNullException">emergencyAccess cannot be null</exception>
|
||||
public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj)
|
||||
{
|
||||
if (emergencyAccess == null)
|
||||
|
@ -2,6 +2,12 @@
|
||||
|
||||
public enum EmergencyAccessType : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Allows emergency contact to view the Grantor's data.
|
||||
/// </summary>
|
||||
View = 0,
|
||||
/// <summary>
|
||||
/// Allows emergency contact to take over the Grantor's account by overwriting the Grantor's password.
|
||||
/// </summary>
|
||||
Takeover = 1,
|
||||
}
|
||||
|
@ -58,38 +58,38 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime)
|
||||
public async Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime)
|
||||
{
|
||||
if (!await _userService.CanAccessPremium(invitingUser))
|
||||
if (!await _userService.CanAccessPremium(grantorUser))
|
||||
{
|
||||
throw new BadRequestException("Not a premium user.");
|
||||
}
|
||||
|
||||
if (type == EmergencyAccessType.Takeover && invitingUser.UsesKeyConnector)
|
||||
if (accessType == EmergencyAccessType.Takeover && grantorUser.UsesKeyConnector)
|
||||
{
|
||||
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
|
||||
}
|
||||
|
||||
var emergencyAccess = new EmergencyAccess
|
||||
{
|
||||
GrantorId = invitingUser.Id,
|
||||
Email = email.ToLowerInvariant(),
|
||||
GrantorId = grantorUser.Id,
|
||||
Email = emergencyContactEmail.ToLowerInvariant(),
|
||||
Status = EmergencyAccessStatusType.Invited,
|
||||
Type = type,
|
||||
Type = accessType,
|
||||
WaitTimeDays = waitTime,
|
||||
CreationDate = DateTime.UtcNow,
|
||||
RevisionDate = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await _emergencyAccessRepository.CreateAsync(emergencyAccess);
|
||||
await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser));
|
||||
await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));
|
||||
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid userId)
|
||||
public async Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid grantorId)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, userId);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, grantorId);
|
||||
if (emergencyAccess == null)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
@ -98,19 +98,19 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId)
|
||||
public async Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != invitingUser.Id ||
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.Invited)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser));
|
||||
await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService)
|
||||
public async Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null)
|
||||
@ -123,7 +123,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
|
||||
if (!data.IsValid(emergencyAccessId, user.Email))
|
||||
if (!data.IsValid(emergencyAccessId, granteeUser.Email))
|
||||
{
|
||||
throw new BadRequestException("Invalid token.");
|
||||
}
|
||||
@ -140,7 +140,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
// TODO PM-21687
|
||||
// Might not be reachable since the Tokenable.IsValid() does an email comparison
|
||||
if (string.IsNullOrWhiteSpace(emergencyAccess.Email) ||
|
||||
!emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
|
||||
!emergencyAccess.Email.Equals(granteeUser.Email, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
throw new BadRequestException("User email does not match invite.");
|
||||
}
|
||||
@ -148,7 +148,7 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
var granteeEmail = emergencyAccess.Email;
|
||||
|
||||
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
|
||||
emergencyAccess.GranteeId = user.Id;
|
||||
emergencyAccess.GranteeId = granteeUser.Id;
|
||||
emergencyAccess.Email = null;
|
||||
|
||||
var grantor = await userService.GetUserByIdAsync(emergencyAccess.GrantorId);
|
||||
@ -172,16 +172,16 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
await _emergencyAccessRepository.DeleteAsync(emergencyAccess);
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid confirmingUserId)
|
||||
public async Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted ||
|
||||
emergencyAccess.GrantorId != confirmingUserId)
|
||||
emergencyAccess.GrantorId != grantorId)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
|
||||
var grantor = await _userRepository.GetByIdAsync(confirmingUserId);
|
||||
var grantor = await _userRepository.GetByIdAsync(grantorId);
|
||||
if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)
|
||||
{
|
||||
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
|
||||
@ -198,14 +198,14 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
return emergencyAccess;
|
||||
}
|
||||
|
||||
public async Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser)
|
||||
public async Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser)
|
||||
{
|
||||
if (!await _userService.CanAccessPremium(savingUser))
|
||||
if (!await _userService.CanAccessPremium(grantorUser))
|
||||
{
|
||||
throw new BadRequestException("Not a premium user.");
|
||||
}
|
||||
|
||||
if (emergencyAccess.GrantorId != savingUser.Id)
|
||||
if (emergencyAccess.GrantorId != grantorUser.Id)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
@ -222,10 +222,11 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
}
|
||||
|
||||
public async Task InitiateAsync(Guid id, User initiatingUser)
|
||||
// TODO PM-21687: rename this to something like InitiateRecoveryAsync, and something similar for Approve and Reject
|
||||
public async Task InitiateAsync(Guid emergencyAccessId, User granteeUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id ||
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
if (emergencyAccess == null || emergencyAccess.GranteeId != granteeUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.Confirmed)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
@ -245,14 +246,14 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
emergencyAccess.LastNotificationDate = now;
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
|
||||
await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(initiatingUser), grantor.Email);
|
||||
await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(granteeUser), grantor.Email);
|
||||
}
|
||||
|
||||
public async Task ApproveAsync(Guid id, User approvingUser)
|
||||
public async Task ApproveAsync(Guid emergencyAccessId, User grantorUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != approvingUser.Id ||
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated)
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
@ -262,14 +263,14 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
|
||||
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
|
||||
await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(approvingUser), grantee.Email);
|
||||
await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(grantorUser), grantee.Email);
|
||||
}
|
||||
|
||||
public async Task RejectAsync(Guid id, User rejectingUser)
|
||||
public async Task RejectAsync(Guid emergencyAccessId, User grantorUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != rejectingUser.Id ||
|
||||
if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||
|
||||
(emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated &&
|
||||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved))
|
||||
{
|
||||
@ -280,17 +281,17 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
|
||||
|
||||
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
|
||||
await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(rejectingUser), grantee.Email);
|
||||
await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(grantorUser), grantee.Email);
|
||||
}
|
||||
|
||||
public async Task<ICollection<Policy>> GetPoliciesAsync(Guid id, User requestingUser)
|
||||
public async Task<ICollection<Policy>> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser)
|
||||
{
|
||||
// TODO PM-21687
|
||||
// Should we look up policies here or just verify the EmergencyAccess is correct
|
||||
// and handle policy logic else where? Should this be a query/Command?
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
|
||||
if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
@ -306,11 +307,12 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
return policies;
|
||||
}
|
||||
|
||||
public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User requestingUser)
|
||||
// TODO PM-21687: rename this to something like InitiateRecoveryTakeoverAsync
|
||||
public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
|
||||
if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
@ -326,11 +328,12 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
return (emergencyAccess, grantor);
|
||||
}
|
||||
|
||||
public async Task PasswordAsync(Guid id, User requestingUser, string newMasterPasswordHash, string key)
|
||||
// TODO PM-21687: rename this to something like FinishRecoveryTakeoverAsync
|
||||
public async Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
|
||||
if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
@ -392,11 +395,11 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<EmergencyAccessViewData> ViewAsync(Guid id, User requestingUser)
|
||||
public async Task<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View))
|
||||
if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View))
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
@ -410,11 +413,11 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User requestingUser)
|
||||
public async Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
|
||||
|
||||
if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View))
|
||||
if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View))
|
||||
{
|
||||
throw new BadRequestException("Emergency Access not valid.");
|
||||
}
|
||||
@ -429,18 +432,19 @@ public class EmergencyAccessService : IEmergencyAccessService
|
||||
await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);
|
||||
}
|
||||
|
||||
// TODO PM-21687: move this to the user entity -> User.GetNameOrEmail()?
|
||||
private static string NameOrEmail(User user)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Checks if EmergencyAccess Object is null
|
||||
* Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action)
|
||||
* Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet)
|
||||
* request type must equal the type of access requested (View or Takeover)
|
||||
*/
|
||||
//TODO PM-21687: this IsValidRequest() checks the validity based on the granteeUser. There should be a complementary method for the grantorUser
|
||||
private static bool IsValidRequest(
|
||||
EmergencyAccess availableAccess,
|
||||
User requestingUser,
|
@ -0,0 +1,147 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public interface IEmergencyAccessService
|
||||
{
|
||||
/// <summary>
|
||||
/// Invites a user via email to become an emergency contact for the Grantor user. The Grantor must have a premium subscription.
|
||||
/// the grantor user must not be a member of the organization that uses KeyConnector.
|
||||
/// </summary>
|
||||
/// <param name="grantorUser">The user initiating the emergency contact request</param>
|
||||
/// <param name="emergencyContactEmail">Emergency contact</param>
|
||||
/// <param name="accessType">Type of emergency access allowed to the emergency contact</param>
|
||||
/// <param name="waitTime">The amount of time to pass before the invite is auto confirmed</param>
|
||||
/// <returns>a new Emergency Access object</returns>
|
||||
Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
|
||||
/// <summary>
|
||||
/// Sends an invite to the emergency contact associated with the emergency access id.
|
||||
/// </summary>
|
||||
/// <param name="grantorUser">The grantor. This must be the owner of the Emergency Access object</param>
|
||||
/// <param name="emergencyAccessId">The Id of the emergency access being requested.</param>
|
||||
/// <returns>void</returns>
|
||||
Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId);
|
||||
/// <summary>
|
||||
/// A grantee user accepts the emergency contact request. This updates the emergency access status to be
|
||||
/// "Accepted", this is the middle step before the grantor user confirms the request.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">Id of the emergency access object being acted on.</param>
|
||||
/// <param name="granteeUser">User being invited to be an emergency contact</param>
|
||||
/// <param name="token">the tokenable that was sent via email</param>
|
||||
/// <param name="userService">service dependency</param>
|
||||
/// <returns>void</returns>
|
||||
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
|
||||
/// <summary>
|
||||
/// The creator of the emergency access request can delete the request.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">Id of the emergency access being acted on</param>
|
||||
/// <param name="grantorId">Id of the owner trying to delete the emergency access request</param>
|
||||
/// <returns>void</returns>
|
||||
Task DeleteAsync(Guid emergencyAccessId, Guid grantorId);
|
||||
/// <summary>
|
||||
/// The grantor user confirms the acceptance of the emergency contact request. This stores the encrypted key allowing the grantee
|
||||
/// access based on the emergency access type.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">Id of the emergency access being acted on.</param>
|
||||
/// <param name="key">The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key)</param>
|
||||
/// <param name="grantorId">Id of grantor user</param>
|
||||
/// <returns>emergency access object associated with the Id passed in</returns>
|
||||
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
|
||||
/// <summary>
|
||||
/// Fetches an emergency access object. The grantor user must own the object being fetched.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">Id of emergency access object</param>
|
||||
/// <param name="grantorId">Id of the owner of the emergency access object.</param>
|
||||
/// <returns>Details of the emergency access object</returns>
|
||||
Task<EmergencyAccessDetails> GetAsync(Guid emergencyAccessId, Guid grantorId);
|
||||
/// <summary>
|
||||
/// Updates the emergency access object.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccess">emergency access entity being updated</param>
|
||||
/// <param name="grantorUser">grantor user</param>
|
||||
/// <returns>void</returns>
|
||||
Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser);
|
||||
/// <summary>
|
||||
/// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">EmergencyAccess Id</param>
|
||||
/// <param name="granteeUser">grantee user</param>
|
||||
/// <returns>void</returns>
|
||||
Task InitiateAsync(Guid emergencyAccessId, User granteeUser);
|
||||
/// <summary>
|
||||
/// Approves a recovery request. Set's the EmergencyAccess.Status to RecoveryApproved.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">emergency access id</param>
|
||||
/// <param name="grantorUser">grantor user</param>
|
||||
/// <returns>void</returns>
|
||||
Task ApproveAsync(Guid emergencyAccessId, User grantorUser);
|
||||
/// <summary>
|
||||
/// Rejects a recovery request. Set's the EmergencyAccess.Status to Confirmed. This does not remove the emergency access entity. The
|
||||
/// Grantee user can still initiate another recovery request.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">emergency access id</param>
|
||||
/// <param name="grantorUser">grantor user</param>
|
||||
/// <returns>void</returns>
|
||||
Task RejectAsync(Guid emergencyAccessId, User grantorUser);
|
||||
/// <summary>
|
||||
/// This request is made by the Grantee user to fetch the policies <see cref="Policy"/> for the Grantor User.
|
||||
/// The Grantor User has to be the owner of the organization. <see cref="OrganizationUserType"/>
|
||||
/// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user
|
||||
/// are returned. This is used to ensure the password is of the proper complexity for the organization.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">EmergencyAccess.Id being acted on</param>
|
||||
/// <param name="granteeUser">User making the request, this is the Grantee</param>
|
||||
/// <returns>null if the GrantorUser is not an organization owner; A list of policies otherwise.</returns>
|
||||
Task<ICollection<Policy>> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser);
|
||||
/// <summary>
|
||||
/// Fetches the emergency access entity and grantor user. The grantor user is returned so the correct KDF configuration is
|
||||
/// used for the new password.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">Id of entity being accessed</param>
|
||||
/// <param name="granteeUser">grantee user of the emergency access entity</param>
|
||||
/// <returns>emergency access entity and the grantorUser</returns>
|
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
|
||||
/// <summary>
|
||||
/// Updates the grantor's password hash and updates the key for the EmergencyAccess entity.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">Emergency Access Id being acted on</param>
|
||||
/// <param name="granteeUser">user making the request</param>
|
||||
/// <param name="newMasterPasswordHash">new password hash set by grantee user</param>
|
||||
/// <param name="key">new encrypted user key</param>
|
||||
/// <returns>void</returns>
|
||||
Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key);
|
||||
/// <summary>
|
||||
/// sends a reminder email that there is a pending request for recovery.
|
||||
/// </summary>
|
||||
/// <returns>void</returns>
|
||||
Task SendNotificationsAsync();
|
||||
/// <summary>
|
||||
/// This handles the auto approval of recovery requests started in the <see cref="InitiateAsync"/> method.
|
||||
/// An email will be sent to the Grantee and the Grantor notifying each the recovery has been approved.
|
||||
/// </summary>
|
||||
/// <returns>void</returns>
|
||||
Task HandleTimedOutRequestsAsync();
|
||||
/// <summary>
|
||||
/// Fetched ciphers from the grantors account for the grantee to view.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">Emergency access entity being acted on</param>
|
||||
/// <param name="granteeUser">user requesting cipher items</param>
|
||||
/// <returns>ciphers associated with the emergency access request</returns>
|
||||
Task<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser);
|
||||
/// <summary>
|
||||
/// Returns attachment if the grantee user has access to the cipher through the emergency access entity.
|
||||
/// </summary>
|
||||
/// <param name="emergencyAccessId">EmergencyAccess entity being acted on</param>
|
||||
/// <param name="cipherId">cipher entity containing the attachment</param>
|
||||
/// <param name="attachmentId">Attachment entity</param>
|
||||
/// <param name="granteeUser">user making the request</param>
|
||||
/// <returns>attachment response </returns>
|
||||
Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser);
|
||||
}
|
95
src/Core/Auth/Services/EmergencyAccess/readme.md
Normal file
95
src/Core/Auth/Services/EmergencyAccess/readme.md
Normal file
@ -0,0 +1,95 @@
|
||||
# Emergency Access System
|
||||
This system allows users to share their `User.Key` with other users using public key exchange. An emergency contact (a grantee user) can view or takeover (reset the password) of the grantor user.
|
||||
|
||||
When an account is taken over all two factor methods are turned off and device verification is disabled.
|
||||
|
||||
This system is affected by the Key Rotation feature. The `EmergencyAccess.KeyEncrypted` is the `Grantor.UserKey` encrypted by the `Grantee.PublicKey`. So if the `User.Key` is rotated then all `EmergencyAccess` entities will need to be updated.
|
||||
|
||||
## Special Cases
|
||||
Users who use `KeyConnector` are not able to allow `Takeover` of their accounts. However, they can allow `View`.
|
||||
|
||||
A user who is not the `OrganizationUserType.Owner` will be removed from the organization.
|
||||
|
||||
## Step 1. Invitation
|
||||
|
||||
A grantor user invites another user to be their emergency contact, the grantee. This will create a new `EmergencyAccess` entity in the database with the `EmergencyAccessStatusType` set to `Invited`.
|
||||
The `EmergencyAccess.KeyEncrypted` field is empty, and the `GranteeId` is `null` since the user being invited via email may not have an account yet.
|
||||
|
||||
### code
|
||||
```csharp
|
||||
// creates entity.
|
||||
Task<EmergencyAccess> InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
|
||||
// resend email to the EmergencyAccess.Email.
|
||||
Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId);
|
||||
```
|
||||
|
||||
## Step 2. Acceptance
|
||||
|
||||
The grantee user receives an email they have been invited to be an emergency contact for a grantor user.
|
||||
|
||||
At this point the grantee user can accept the request. This will set the `EmergencyAccess.GranteeId` to the `User.Id` of the grantee user. The `EmergencyAccess.Status` is set to `Accepted`.
|
||||
|
||||
If the grantee user does not have an account then they can create an account and accept the invitation.
|
||||
|
||||
### Code
|
||||
```csharp
|
||||
// accepts the request to be an emergency contact.
|
||||
Task<EmergencyAccess> AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
|
||||
```
|
||||
|
||||
## Step 3. Confirmation
|
||||
|
||||
Once the grantee user has accepted, the `EmergencyAccess.GranteeId` allows the grantor user the ability to query for the `GranteeUser.PublicKey`. With the `Grantee.PublicKey`, the grantor on the client is able to safely encrypt their `User.Key` and save the encrypted string to the database.
|
||||
|
||||
The `EmergencyAccess.Status` is set to `Confirmed`, and the `EmergencyAccess.KeyEncrypted` is set.
|
||||
|
||||
### Code
|
||||
```csharp
|
||||
Task<EmergencyAccess> ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
|
||||
```
|
||||
|
||||
## Step 4. Recovery Approval
|
||||
|
||||
The grantee user can now exercise the ability to view or takeover the account. This is done by initiating the recovery. Initiating recovery has a time delay specified by `EmergencyAccess.WaitTime`. `WaitTime` is set in the initial invite. The grantor user can approve the request before the `WaitTime`, but they _cannot_ reject the request _after_ the `WaitTime` has completed. If the recovery request is not rejected then once the `WaitTime` has passed the grantee user will be able to access the emergency access entity.
|
||||
|
||||
### Code
|
||||
```csharp
|
||||
// Initiates the recovery process; Will set EmergencyAccess.Status to RecoveryInitiated.
|
||||
Task InitiateAsync(Guid id, User granteeUser);
|
||||
// Approved the recovery request; Will set EmergencyAccess.Status to RecoveryApproved.
|
||||
Task ApproveAsync(Guid id, User approvingUser);
|
||||
// Rejects the recovery request; Will set EmergencyAccess.Status to Confirmed.
|
||||
Task RejectAsync(Guid id, User rejectingUser);
|
||||
// Automatically set the EmergencyAccess.Status to RecoveryApproved after WaitTime has passed.
|
||||
Task HandleTimedOutRequestsAsync();
|
||||
```
|
||||
|
||||
## Step 5. Recovering the account
|
||||
|
||||
Once the `EmergencyAccess.Status` is `RecoveryApproved` the grantee user is able to exercise their ability to view or takeover the grantor account. Viewing allows the grantee user to view the vault data of the grantor user. Takeover allows the grantee to change the password of the grantor user.
|
||||
|
||||
### Takeover
|
||||
`TakeoverAsync(Guid, User)` returns the grantor user object along with the `EmergencyAccess` entity. The grantor user object is required since to update the password the client needs access to the grantor kdf configuration. Once the password has been set in the `PasswordAsync(Guid, User, string, string)` the account has been successfully recovered.
|
||||
|
||||
Taking over the account will change the password of the grantor user, empty the two factor array on the grantor user, and disable device verification.
|
||||
|
||||
```csharp
|
||||
// Takeover returns the grantor user and the emergency access entity.
|
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
|
||||
// Password sets the password for the grantor user.
|
||||
Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key);
|
||||
// Returns Ciphers the Grantee is allowed to view based on the EmergencyAccess status.
|
||||
Task<EmergencyAccessViewData> ViewAsync(Guid emergencyAccessId, User granteeUser);
|
||||
// Returns downloadable cipher attachments based on the EmergencyAccess status.
|
||||
Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser);
|
||||
```
|
||||
|
||||
## Optional steps
|
||||
|
||||
The grantor user is able to delete an emergency contact at anytime, at any point in the recovery process.
|
||||
|
||||
### Code
|
||||
```csharp
|
||||
// deletes the associated EmergencyAccess Entity
|
||||
Task DeleteAsync(Guid emergencyAccessId, Guid grantorId);
|
||||
```
|
@ -1,40 +0,0 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public interface IEmergencyAccessService
|
||||
{
|
||||
Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime);
|
||||
Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId);
|
||||
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, User savingUser);
|
||||
Task InitiateAsync(Guid id, User initiatingUser);
|
||||
Task ApproveAsync(Guid id, User approvingUser);
|
||||
Task RejectAsync(Guid id, User rejectingUser);
|
||||
/// <summary>
|
||||
/// This request is made by the Grantee user to fetch the policies <see cref="Policy"/> for the Grantor User.
|
||||
/// The Grantor User has to be the owner of the organization. <see cref="OrganizationUserType"/>
|
||||
/// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user
|
||||
/// are returned.
|
||||
/// </summary>
|
||||
/// <param name="id">EmergencyAccess.Id being acted on</param>
|
||||
/// <param name="requestingUser">User making the request, this is the Grantee</param>
|
||||
/// <returns>null if the GrantorUser is not an organization owner; A list of policies otherwise.</returns>
|
||||
Task<ICollection<Policy>> GetPoliciesAsync(Guid id, User requestingUser);
|
||||
Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser);
|
||||
Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key);
|
||||
Task SendNotificationsAsync();
|
||||
Task HandleTimedOutRequestsAsync();
|
||||
Task<EmergencyAccessViewData> ViewAsync(Guid id, User user);
|
||||
Task<AttachmentResponseData> GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User user);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user