1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-07 05:58:13 -05:00
bitwarden/src/Core/Services/Implementations/EmergencyAccessService.cs
Matt Gibson 5537470703
Use sas token for attachment downloads (#1153)
* Get limited life attachment download URL

This change limits url download to a 1min lifetime.
This requires moving to a new container to allow for non-public blob
access.

Clients will have to call GetAttachmentData api function to receive the download
URL. For backwards compatibility, attachment URLs are still present, but will not
work for attachments stored in non-public access blobs.

* Make GlobalSettings interface for testing

* Test LocalAttachmentStorageService equivalence

* Remove comment

* Add missing globalSettings using

* Simplify default attachment container

* Default to attachments containe for existing methods

A new upload method will be made for uploading to attachments-v2.
For compatibility for clients which don't use these new methods, we need
to still use the old container. The new container will be used only for
new uploads

* Remove Default MetaData fixture.

* Keep attachments container blob-level security for all instances

* Close unclosed FileStream

* Favor default value for noop services
2021-02-22 15:35:16 -06:00

363 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
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 Bit.Core.Settings;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
namespace Bit.Core.Services
{
public class EmergencyAccessService : IEmergencyAccessService
{
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IPolicyRepository _policyRepository;
private readonly IMailService _mailService;
private readonly IUserService _userService;
private readonly IDataProtector _dataProtector;
private readonly GlobalSettings _globalSettings;
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationService _organizationService;
public EmergencyAccessService(
IEmergencyAccessRepository emergencyAccessRepository,
IOrganizationUserRepository organizationUserRepository,
IUserRepository userRepository,
ICipherRepository cipherRepository,
IPolicyRepository policyRepository,
IMailService mailService,
IUserService userService,
IPasswordHasher<User> passwordHasher,
IDataProtectionProvider dataProtectionProvider,
GlobalSettings globalSettings,
IOrganizationService organizationService)
{
_emergencyAccessRepository = emergencyAccessRepository;
_organizationUserRepository = organizationUserRepository;
_userRepository = userRepository;
_cipherRepository = cipherRepository;
_policyRepository = policyRepository;
_mailService = mailService;
_userService = userService;
_passwordHasher = passwordHasher;
_dataProtector = dataProtectionProvider.CreateProtector("EmergencyAccessServiceDataProtector");
_globalSettings = globalSettings;
_organizationService = organizationService;
}
public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime)
{
if (! await _userService.CanAccessPremium(invitingUser))
{
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, NameOrEmail(invitingUser));
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(User invitingUser, Guid emergencyAccessId)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
if (emergencyAccess == null || emergencyAccess.GrantorId != invitingUser.Id ||
emergencyAccess.Status != EmergencyAccessStatusType.Invited)
{
throw new BadRequestException("Emergency Access not valid.");
}
await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser));
}
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(NameOrEmail(grantor), 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, NameOrEmail(initiatingUser), 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, NameOrEmail(approvingUser), 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, NameOrEmail(rejectingUser), grantee.Email);
}
public async Task<ICollection<Policy>> GetPoliciesAsync(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);
var grantorOrganizations = await _organizationUserRepository.GetManyByUserAsync(grantor.Id);
var isOrganizationOwner = grantorOrganizations.Any<OrganizationUser>(organization => organization.Type == OrganizationUserType.Owner);
var policies = isOrganizationOwner ? await _policyRepository.GetManyByUserIdAsync(grantor.Id) : null;
return policies;
}
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);
// Remove grantor from all organisations unless Owner
var orgUser = await _organizationUserRepository.GetManyByUserAsync(grantor.Id);
foreach (var o in orgUser)
{
if (o.Type != OrganizationUserType.Owner)
{
await _organizationService.DeleteUserAsync(o.OrganizationId, grantor.Id);
}
}
}
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);
}
private string NameOrEmail(User user)
{
return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name;
}
}
}