diff --git a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs index 8b8ac6e8b6..34eba65142 100644 --- a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -190,9 +190,8 @@ namespace Bit.Core.IdentityServer var deviceName = context.Request.Raw["DeviceName"]?.ToString(); var devicePushToken = context.Request.Raw["DevicePushToken"]?.ToString(); - DeviceType type; if(string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) || - string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out type)) + string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type)) { return null; } diff --git a/src/Core/Models/Data/EventTableEntity.cs b/src/Core/Models/Data/EventTableEntity.cs index 8ec719f814..484df17240 100644 --- a/src/Core/Models/Data/EventTableEntity.cs +++ b/src/Core/Models/Data/EventTableEntity.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Bit.Core.Enums; using Microsoft.WindowsAzure.Storage.Table; namespace Bit.Core.Models.Data diff --git a/src/Core/Repositories/IEventRepository.cs b/src/Core/Repositories/IEventRepository.cs index e9ae09a970..90e6abc297 100644 --- a/src/Core/Repositories/IEventRepository.cs +++ b/src/Core/Repositories/IEventRepository.cs @@ -10,6 +10,6 @@ namespace Bit.Core.Repositories { Task> GetManyByUserAsync(Guid userId, DateTime startDate, DateTime endDate); Task CreateAsync(ITableEntity entity); - Task CreateManyAsync(IEnumerable entities); + Task CreateManyAsync(IList entities); } } diff --git a/src/Core/Repositories/TableStorage/EventRepository.cs b/src/Core/Repositories/TableStorage/EventRepository.cs index 81fd0b0420..15cee5bf5d 100644 --- a/src/Core/Repositories/TableStorage/EventRepository.cs +++ b/src/Core/Repositories/TableStorage/EventRepository.cs @@ -54,30 +54,47 @@ namespace Bit.Core.Repositories.TableStorage await Table.ExecuteAsync(TableOperation.Insert(entity)); } - public async Task CreateManyAsync(IEnumerable entities) + public async Task CreateManyAsync(IList entities) { if(!entities?.Any() ?? true) { return; } - // A batch insert can only contain 100 entities at a time - var iterations = entities.Count() / 100; - for(var i = 0; i <= iterations; i++) + if(entities.Count == 1) { - var batch = new TableBatchOperation(); - var batchEntities = entities.Skip(i * 100).Take(100); - if(!batchEntities.Any()) + await CreateAsync(entities.First()); + return; + } + + var entityGroups = entities.GroupBy(e => e.PartitionKey); + foreach(var group in entityGroups) + { + var groupEntities = group.ToList(); + if(groupEntities.Count == 1) { - break; + await CreateAsync(groupEntities.First()); + continue; } - foreach(var entity in batchEntities) + // A batch insert can only contain 100 entities at a time + var iterations = groupEntities.Count / 100; + for(var i = 0; i <= iterations; i++) { - batch.InsertOrReplace(entity); - } + var batch = new TableBatchOperation(); + var batchEntities = groupEntities.Skip(i * 100).Take(100); + if(!batchEntities.Any()) + { + break; + } - await Table.ExecuteBatchAsync(batch); + foreach(var entity in batchEntities) + { + batch.InsertOrReplace(entity); + } + + await Table.ExecuteBatchAsync(batch); + } } } } diff --git a/src/Core/Services/IEventService.cs b/src/Core/Services/IEventService.cs index 6a5e6a97df..b42586c157 100644 --- a/src/Core/Services/IEventService.cs +++ b/src/Core/Services/IEventService.cs @@ -7,5 +7,6 @@ namespace Bit.Core.Services public interface IEventService { Task LogUserEventAsync(Guid userId, EventType type); + Task LogUserEventAsync(Guid userId, CurrentContext currentContext, EventType type); } } diff --git a/src/Core/Services/Implementations/EventService.cs b/src/Core/Services/Implementations/EventService.cs index eb1637f3d3..18c6f50a34 100644 --- a/src/Core/Services/Implementations/EventService.cs +++ b/src/Core/Services/Implementations/EventService.cs @@ -3,26 +3,58 @@ using System; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Models.Data; +using System.Linq; +using System.Collections.Generic; +using Microsoft.WindowsAzure.Storage.Table; namespace Bit.Core.Services { public class EventService : IEventService { private readonly IEventRepository _eventRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; private readonly GlobalSettings _globalSettings; public EventService( IEventRepository eventRepository, + IOrganizationUserRepository organizationUserRepository, GlobalSettings globalSettings) { _eventRepository = eventRepository; + _organizationUserRepository = organizationUserRepository; _globalSettings = globalSettings; } public async Task LogUserEventAsync(Guid userId, EventType type) { - var userEvent = new UserEvent(userId, type); - await _eventRepository.CreateAsync(userEvent); + var events = new List { new UserEvent(userId, type) }; + var orgs = await _organizationUserRepository.GetManyByUserAsync(userId); + var orgEvents = orgs.Where(o => o.Status == OrganizationUserStatusType.Confirmed) + .Select(o => new UserEvent(userId, o.Id, type)); + if(orgEvents.Any()) + { + events.AddRange(orgEvents); + await _eventRepository.CreateManyAsync(events); + } + else + { + await _eventRepository.CreateAsync(events.First()); + } + } + + public async Task LogUserEventAsync(Guid userId, CurrentContext currentContext, EventType type) + { + var events = new List { new UserEvent(userId, type) }; + var orgEvents = currentContext.Organizations.Select(o => new UserEvent(userId, o.Id, type)); + if(orgEvents.Any()) + { + events.AddRange(orgEvents); + await _eventRepository.CreateManyAsync(events); + } + else + { + await _eventRepository.CreateAsync(events.First()); + } } } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 292a753a55..06e2ffce7b 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -425,7 +425,7 @@ namespace Bit.Core.Services user.Key = key; await _userRepository.ReplaceAsync(user); - await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword); + await _eventService.LogUserEventAsync(user.Id, _currentContext, EventType.User_ChangedPassword); return IdentityResult.Success; } @@ -503,7 +503,7 @@ namespace Bit.Core.Services user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false); } await SaveUserAsync(user); - await _eventService.LogUserEventAsync(user.Id, EventType.User_Enabled2fa); + await _eventService.LogUserEventAsync(user.Id, _currentContext, EventType.User_Enabled2fa); } public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type) @@ -517,7 +517,7 @@ namespace Bit.Core.Services providers.Remove(type); user.SetTwoFactorProviders(providers); await SaveUserAsync(user); - await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa); + await _eventService.LogUserEventAsync(user.Id, _currentContext, EventType.User_Disabled2fa); } public async Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode) @@ -542,7 +542,7 @@ namespace Bit.Core.Services user.TwoFactorProviders = null; user.TwoFactorRecoveryCode = CoreHelpers.SecureRandomString(32, upper: false, special: false); await SaveUserAsync(user); - await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa); + await _eventService.LogUserEventAsync(user.Id, _currentContext, EventType.User_Recovered2fa); return true; } diff --git a/src/Core/Services/NoopImplementations/NoopEventService.cs b/src/Core/Services/NoopImplementations/NoopEventService.cs index 729f3e1af9..89744fb939 100644 --- a/src/Core/Services/NoopImplementations/NoopEventService.cs +++ b/src/Core/Services/NoopImplementations/NoopEventService.cs @@ -10,5 +10,10 @@ namespace Bit.Core.Services { return Task.FromResult(0); } + + public Task LogUserEventAsync(Guid userId, CurrentContext currentContext, EventType type) + { + return Task.FromResult(0); + } } }