From 7dfb04298d0b3d504a788b565bb0ff329302bb81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Mon, 25 Jul 2022 09:56:23 +0100 Subject: [PATCH] [EC-92] Add organization vault export to event logs (#2128) * Added nullable OrganizationId to EventModel * Added EventType Organization_ClientExportedVault * Updated CollectController to save the event Organization_ClientExportedVault * Added OrganizationExportResponseModel to encapsulate Organization Export data * Added OrganizationExportController to have a single endpoint for Organization vault export * Added method GetOrganizationCollections to ICollectionService to get collections for an organization * Added GetOrganizationCiphers to ICipherService to get ciphers for an organization * Updated controllers to use new methods in ICollectionService and ICipherService --- src/Api/Controllers/CiphersController.cs | 30 +-------- src/Api/Controllers/CollectionsController.cs | 17 +---- .../OrganizationExportController.cs | 64 +++++++++++++++++++ .../OrganizationExportResponseModel.cs | 13 ++++ src/Core/Enums/EventType.cs | 2 +- src/Core/Services/ICipherService.cs | 1 + src/Core/Services/ICollectionService.cs | 1 + .../Services/Implementations/CipherService.cs | 42 +++++++++++- .../Implementations/CollectionService.cs | 30 ++++++++- src/Events/Controllers/CollectController.cs | 13 +++- src/Events/Models/EventModel.cs | 1 + 11 files changed, 164 insertions(+), 50 deletions(-) create mode 100644 src/Api/Controllers/OrganizationExportController.cs create mode 100644 src/Api/Models/Response/OrganizationExportResponseModel.cs diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index e84731a9a..f5831acaa 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -216,40 +216,12 @@ namespace Bit.Api.Controllers { var userId = _userService.GetProperUserId(User).Value; var orgIdGuid = new Guid(organizationId); - if (!await _currentContext.ViewAllCollections(orgIdGuid) && !await _currentContext.AccessReports(orgIdGuid)) - { - throw new NotFoundException(); - } - - IEnumerable orgCiphers; - if (await _currentContext.OrganizationAdmin(orgIdGuid)) - { - // Admins, Owners and Providers can access all items even if not assigned to them - orgCiphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(orgIdGuid); - } - else - { - var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, true); - orgCiphers = ciphers.Where(c => c.OrganizationId == orgIdGuid); - } - - var orgCipherIds = orgCiphers.Select(c => c.Id); - - var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(orgIdGuid); - var collectionCiphersGroupDict = collectionCiphers - .Where(c => orgCipherIds.Contains(c.CipherId)) - .GroupBy(c => c.CipherId).ToDictionary(s => s.Key); + (IEnumerable orgCiphers, Dictionary> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, orgIdGuid); var responses = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, collectionCiphersGroupDict, c.OrganizationUseTotp)); - - var providerId = await _currentContext.ProviderIdForOrg(orgIdGuid); - if (providerId.HasValue) - { - await _providerService.LogProviderAccessToOrganizationAsync(orgIdGuid); - } return new ListResponseModel(responses); } diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index be71b3481..548ff80d4 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -75,22 +75,7 @@ namespace Bit.Api.Controllers [HttpGet("")] public async Task> Get(Guid orgId) { - if (!await _currentContext.ViewAllCollections(orgId) && !await _currentContext.ManageUsers(orgId)) - { - throw new NotFoundException(); - } - - IEnumerable orgCollections; - if (await _currentContext.OrganizationAdmin(orgId)) - { - // Admins, Owners and Providers can access all items even if not assigned to them - orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(orgId); - } - else - { - var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value); - orgCollections = collections.Where(c => c.OrganizationId == orgId); - } + IEnumerable orgCollections = await _collectionService.GetOrganizationCollections(orgId); var responses = orgCollections.Select(c => new CollectionResponseModel(c)); return new ListResponseModel(responses); diff --git a/src/Api/Controllers/OrganizationExportController.cs b/src/Api/Controllers/OrganizationExportController.cs new file mode 100644 index 000000000..422618e75 --- /dev/null +++ b/src/Api/Controllers/OrganizationExportController.cs @@ -0,0 +1,64 @@ +using Bit.Api.Models.Response; +using Bit.Core.Entities; +using Bit.Core.Services; +using Bit.Core.Settings; +using Core.Models.Data; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers +{ + [Route("organizations/{organizationId}")] + [Authorize("Application")] + public class OrganizationExportController : Controller + { + private readonly IUserService _userService; + private readonly ICollectionService _collectionService; + private readonly ICipherService _cipherService; + private readonly GlobalSettings _globalSettings; + + public OrganizationExportController( + ICipherService cipherService, + ICollectionService collectionService, + IUserService userService, + GlobalSettings globalSettings) + { + _cipherService = cipherService; + _collectionService = collectionService; + _userService = userService; + _globalSettings = globalSettings; + } + + [HttpGet("export")] + public async Task Export(Guid organizationId) + { + var userId = _userService.GetProperUserId(User).Value; + + IEnumerable orgCollections = await _collectionService.GetOrganizationCollections(organizationId); + (IEnumerable orgCiphers, Dictionary> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId); + + var result = new OrganizationExportResponseModel + { + Collections = GetOrganizationCollectionsResponse(orgCollections), + Ciphers = await GetOrganizationCiphersResponse(orgCiphers, collectionCiphersGroupDict) + }; + + return result; + } + + private ListResponseModel GetOrganizationCollectionsResponse(IEnumerable orgCollections) + { + var collections = orgCollections.Select(c => new CollectionResponseModel(c)); + return new ListResponseModel(collections); + } + + private async Task> GetOrganizationCiphersResponse(IEnumerable orgCiphers, + Dictionary> collectionCiphersGroupDict) + { + var responses = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, + collectionCiphersGroupDict, c.OrganizationUseTotp)); + + return new ListResponseModel(responses); + } + } +} diff --git a/src/Api/Models/Response/OrganizationExportResponseModel.cs b/src/Api/Models/Response/OrganizationExportResponseModel.cs new file mode 100644 index 000000000..f5ce61873 --- /dev/null +++ b/src/Api/Models/Response/OrganizationExportResponseModel.cs @@ -0,0 +1,13 @@ +namespace Bit.Api.Models.Response +{ + public class OrganizationExportResponseModel + { + public OrganizationExportResponseModel() + { + } + + public ListResponseModel Collections { get; set; } + + public ListResponseModel Ciphers { get; set; } + } +} diff --git a/src/Core/Enums/EventType.cs b/src/Core/Enums/EventType.cs index acce76c33..98d844008 100644 --- a/src/Core/Enums/EventType.cs +++ b/src/Core/Enums/EventType.cs @@ -56,7 +56,7 @@ Organization_Updated = 1600, Organization_PurgedVault = 1601, - // Organization_ClientExportedVault = 1602, + Organization_ClientExportedVault = 1602, Organization_VaultAccessed = 1603, Organization_EnabledSso = 1604, Organization_DisabledSso = 1605, diff --git a/src/Core/Services/ICipherService.cs b/src/Core/Services/ICipherService.cs index e8a3e78f1..9afeb5926 100644 --- a/src/Core/Services/ICipherService.cs +++ b/src/Core/Services/ICipherService.cs @@ -39,5 +39,6 @@ namespace Bit.Core.Services Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId); Task GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId); Task ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData); + Task<(IEnumerable, Dictionary>)> GetOrganizationCiphers(Guid userId, Guid organizationId); } } diff --git a/src/Core/Services/ICollectionService.cs b/src/Core/Services/ICollectionService.cs index 7c2e286f5..015474b6f 100644 --- a/src/Core/Services/ICollectionService.cs +++ b/src/Core/Services/ICollectionService.cs @@ -8,5 +8,6 @@ namespace Bit.Core.Services Task SaveAsync(Collection collection, IEnumerable groups = null, Guid? assignUserId = null); Task DeleteAsync(Collection collection); Task DeleteUserAsync(Collection collection, Guid organizationUserId); + Task> GetOrganizationCollections(Guid organizationId); } } diff --git a/src/Core/Services/Implementations/CipherService.cs b/src/Core/Services/Implementations/CipherService.cs index 1376b09e7..dfa156974 100644 --- a/src/Core/Services/Implementations/CipherService.cs +++ b/src/Core/Services/Implementations/CipherService.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -29,6 +30,8 @@ namespace Bit.Core.Services private readonly GlobalSettings _globalSettings; private const long _fileSizeLeeway = 1024L * 1024L; // 1MB private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly IProviderService _providerService; public CipherService( ICipherRepository cipherRepository, @@ -43,7 +46,8 @@ namespace Bit.Core.Services IUserService userService, IPolicyRepository policyRepository, GlobalSettings globalSettings, - IReferenceEventService referenceEventService) + IReferenceEventService referenceEventService, + ICurrentContext currentContext) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -58,6 +62,7 @@ namespace Bit.Core.Services _policyRepository = policyRepository; _globalSettings = globalSettings; _referenceEventService = referenceEventService; + _currentContext = currentContext; } public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate, @@ -845,6 +850,41 @@ namespace Bit.Core.Services await _pushService.PushSyncCiphersAsync(restoringUserId); } + public async Task<(IEnumerable, Dictionary>)> GetOrganizationCiphers(Guid userId, Guid organizationId) + { + if (!await _currentContext.ViewAllCollections(organizationId) && !await _currentContext.AccessReports(organizationId)) + { + throw new NotFoundException(); + } + + IEnumerable orgCiphers; + if (await _currentContext.OrganizationAdmin(organizationId)) + { + // Admins, Owners and Providers can access all items even if not assigned to them + orgCiphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(organizationId); + } + else + { + var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, true); + orgCiphers = ciphers.Where(c => c.OrganizationId == organizationId); + } + + var orgCipherIds = orgCiphers.Select(c => c.Id); + + var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(organizationId); + var collectionCiphersGroupDict = collectionCiphers + .Where(c => orgCipherIds.Contains(c.CipherId)) + .GroupBy(c => c.CipherId).ToDictionary(s => s.Key); + + var providerId = await _currentContext.ProviderIdForOrg(organizationId); + if (providerId.HasValue) + { + await _providerService.LogProviderAccessToOrganizationAsync(organizationId); + } + + return (orgCiphers, collectionCiphersGroupDict); + } + private async Task UserCanEditAsync(Cipher cipher, Guid userId) { if (!cipher.OrganizationId.HasValue && cipher.UserId.HasValue && cipher.UserId.Value == userId) diff --git a/src/Core/Services/Implementations/CollectionService.cs b/src/Core/Services/Implementations/CollectionService.cs index 192785e33..20b12694c 100644 --- a/src/Core/Services/Implementations/CollectionService.cs +++ b/src/Core/Services/Implementations/CollectionService.cs @@ -1,4 +1,5 @@ -using Bit.Core.Entities; +using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -16,6 +17,7 @@ namespace Bit.Core.Services private readonly IUserRepository _userRepository; private readonly IMailService _mailService; private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; public CollectionService( IEventService eventService, @@ -24,7 +26,8 @@ namespace Bit.Core.Services ICollectionRepository collectionRepository, IUserRepository userRepository, IMailService mailService, - IReferenceEventService referenceEventService) + IReferenceEventService referenceEventService, + ICurrentContext currentContext) { _eventService = eventService; _organizationRepository = organizationRepository; @@ -33,6 +36,7 @@ namespace Bit.Core.Services _userRepository = userRepository; _mailService = mailService; _referenceEventService = referenceEventService; + _currentContext = currentContext; } public async Task SaveAsync(Collection collection, IEnumerable groups = null, @@ -111,5 +115,27 @@ namespace Bit.Core.Services await _collectionRepository.DeleteUserAsync(collection.Id, organizationUserId); await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_Updated); } + + public async Task> GetOrganizationCollections(Guid organizationId) + { + if (!await _currentContext.ViewAllCollections(organizationId) && !await _currentContext.ManageUsers(organizationId)) + { + throw new NotFoundException(); + } + + IEnumerable orgCollections; + if (await _currentContext.OrganizationAdmin(organizationId)) + { + // Admins, Owners and Providers can access all items even if not assigned to them + orgCollections = await _collectionRepository.GetManyByOrganizationIdAsync(organizationId); + } + else + { + var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value); + orgCollections = collections.Where(c => c.OrganizationId == organizationId); + } + + return orgCollections; + } } } diff --git a/src/Events/Controllers/CollectController.cs b/src/Events/Controllers/CollectController.cs index 2cdffb52f..f2599d26e 100644 --- a/src/Events/Controllers/CollectController.cs +++ b/src/Events/Controllers/CollectController.cs @@ -17,15 +17,18 @@ namespace Bit.Events.Controllers private readonly ICurrentContext _currentContext; private readonly IEventService _eventService; private readonly ICipherRepository _cipherRepository; + private readonly IOrganizationRepository _organizationRepository; public CollectController( ICurrentContext currentContext, IEventService eventService, - ICipherRepository cipherRepository) + ICipherRepository cipherRepository, + IOrganizationRepository organizationRepository) { _currentContext = currentContext; _eventService = eventService; _cipherRepository = cipherRepository; + _organizationRepository = organizationRepository; } [HttpPost] @@ -78,6 +81,14 @@ namespace Bit.Events.Controllers } cipherEvents.Add(new Tuple(cipher, eventModel.Type, eventModel.Date)); break; + case EventType.Organization_ClientExportedVault: + if (!eventModel.OrganizationId.HasValue) + { + continue; + } + var organization = await _organizationRepository.GetByIdAsync(eventModel.OrganizationId.Value); + await _eventService.LogOrganizationEventAsync(organization, eventModel.Type, eventModel.Date); + break; default: continue; } diff --git a/src/Events/Models/EventModel.cs b/src/Events/Models/EventModel.cs index f491b1dca..80b69398a 100644 --- a/src/Events/Models/EventModel.cs +++ b/src/Events/Models/EventModel.cs @@ -7,5 +7,6 @@ namespace Bit.Events.Models public EventType Type { get; set; } public Guid? CipherId { get; set; } public DateTime Date { get; set; } + public Guid? OrganizationId { get; set; } } }