diff --git a/src/Core/AdminConsole/Services/IEventService.cs b/src/Core/AdminConsole/Services/IEventService.cs index 7f7152eb32b4..028858fc60e2 100644 --- a/src/Core/AdminConsole/Services/IEventService.cs +++ b/src/Core/AdminConsole/Services/IEventService.cs @@ -14,7 +14,14 @@ namespace Bit.Core.Services; public interface IEventService { - Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null); + /// + /// Logs a user event and creates organization-scoped copies for each org the user belongs to. + /// + /// When true, also includes organizations where the user + /// has Accepted (not yet Confirmed) status. Use for flows where the user may not be fully confirmed + /// (e.g. device approval, TDE onboarding). + /// + Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null, bool includeAcceptedStatusOrgs = false); Task LogCipherEventAsync(Cipher cipher, EventType type, DateTime? date = null); Task LogCipherEventsAsync(IEnumerable> events); Task LogCollectionEventAsync(Collection collection, EventType type, DateTime? date = null); diff --git a/src/Core/Auth/Services/Implementations/AuthRequestService.cs b/src/Core/Auth/Services/Implementations/AuthRequestService.cs index 11682b524f7b..dfb91a6da41c 100644 --- a/src/Core/Auth/Services/Implementations/AuthRequestService.cs +++ b/src/Core/Auth/Services/Implementations/AuthRequestService.cs @@ -129,7 +129,7 @@ public async Task CreateAuthRequestAsync(AuthRequestCreateRequestMo Debug.Assert(user is not null, "user should have been validated to be non-null and thrown if it's not."); // A user event will automatically create logs for each organization/provider this user belongs to. - await _eventService.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval); + await _eventService.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval, includeAcceptedStatusOrgs: true); AuthRequest? firstAuthRequest = null; foreach (var organizationUser in organizationUsers) diff --git a/src/Core/Dirt/Services/Implementations/EventService.cs b/src/Core/Dirt/Services/Implementations/EventService.cs index dbab9286bcd8..f37c3b588125 100644 --- a/src/Core/Dirt/Services/Implementations/EventService.cs +++ b/src/Core/Dirt/Services/Implementations/EventService.cs @@ -44,7 +44,8 @@ public EventService( _globalSettings = globalSettings; } - public async Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null) + public async Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null, + bool includeAcceptedStatusOrgs = false) { var events = new List { @@ -58,16 +59,37 @@ public async Task LogUserEventAsync(Guid userId, EventType type, DateTime? date }; var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, userId); - var orgEvents = orgs.Where(o => CanUseEvents(orgAbilities, o.Id)) - .Select(o => new EventMessage(_currentContext) - { - OrganizationId = o.Id, - UserId = userId, - ActingUserId = userId, - Type = type, - Date = DateTime.UtcNow - }); + + IEnumerable orgEvents; + if (includeAcceptedStatusOrgs) + { + var orgUsers = await _organizationUserRepository.GetManyByUserAsync(userId); + orgEvents = orgUsers + .Where(ou => ou.Status is OrganizationUserStatusType.Confirmed + or OrganizationUserStatusType.Accepted) + .Where(ou => CanUseEvents(orgAbilities, ou.OrganizationId)) + .Select(ou => new EventMessage(_currentContext) + { + OrganizationId = ou.OrganizationId, + UserId = userId, + ActingUserId = userId, + Type = type, + Date = DateTime.UtcNow + }); + } + else + { + var orgs = await _currentContext.OrganizationMembershipAsync(_organizationUserRepository, userId); + orgEvents = orgs.Where(o => CanUseEvents(orgAbilities, o.Id)) + .Select(o => new EventMessage(_currentContext) + { + OrganizationId = o.Id, + UserId = userId, + ActingUserId = userId, + Type = type, + Date = DateTime.UtcNow + }); + } var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync(); var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, userId); diff --git a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs index 9c95930c1846..6a1f59af7ded 100644 --- a/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs +++ b/test/Core.Test/Auth/Services/AuthRequestServiceTests.cs @@ -375,7 +375,8 @@ await sutProvider.GetDependency() await sutProvider.GetDependency() .Received(1) - .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval); + .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval, + includeAcceptedStatusOrgs: true); await sutProvider.GetDependency() .Received(1) @@ -457,7 +458,8 @@ await sutProvider.GetDependency() await sutProvider.GetDependency() .Received(1) - .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval); + .LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval, + includeAcceptedStatusOrgs: true); await sutProvider.GetDependency() .Received(0) diff --git a/test/Core.Test/Dirt/Services/EventServiceTests.cs b/test/Core.Test/Dirt/Services/EventServiceTests.cs index 6901a6656d27..d8a6328b42ef 100644 --- a/test/Core.Test/Dirt/Services/EventServiceTests.cs +++ b/test/Core.Test/Dirt/Services/EventServiceTests.cs @@ -1,11 +1,14 @@ -using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Context; +using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; +using Bit.Core.AdminConsole.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -271,4 +274,76 @@ public async Task LogProviderOrganizationEventsAsync_LogsRequiredInfo(Provider p await sutProvider.GetDependency().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expected, new[] { "IdempotencyId" }))); } + + [Theory, BitAutoData] + public async Task LogUserEvent_IncludeAcceptedStatusOrgs_AcceptedOrgUser_CreatesOrgScopedEvent( + Guid userId, EventType eventType, OrganizationUser orgUser, SutProvider sutProvider) + { + orgUser.UserId = userId; + orgUser.Status = OrganizationUserStatusType.Accepted; + + var orgAbilities = new Dictionary + { + { orgUser.OrganizationId, new OrganizationAbility { UseEvents = true, Enabled = true } } + }; + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(orgAbilities); + sutProvider.GetDependency() + .GetProviderAbilitiesAsync() + .Returns(new Dictionary()); + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns(new List { orgUser }); + sutProvider.GetDependency() + .ProviderMembershipAsync(Arg.Any(), userId) + .Returns(new List()); + + await sutProvider.Sut.LogUserEventAsync(userId, eventType, includeAcceptedStatusOrgs: true); + + await sutProvider.GetDependency() + .Received(1) + .CreateManyAsync(Arg.Is>(events => + events.Count() == 2 + && events.Any(e => e.OrganizationId == null && e.UserId == userId && e.Type == eventType) + && events.Any(e => e.OrganizationId == orgUser.OrganizationId && e.UserId == userId && e.Type == eventType))); + } + + [Theory, BitAutoData] + public async Task LogUserEvent_IncludeAcceptedStatusOrgs_InvitedOrgUser_DoesNotCreateOrgScopedEvent( + Guid userId, EventType eventType, OrganizationUser orgUser, SutProvider sutProvider) + { + orgUser.UserId = userId; + orgUser.Status = OrganizationUserStatusType.Invited; + + var orgAbilities = new Dictionary + { + { orgUser.OrganizationId, new OrganizationAbility { UseEvents = true, Enabled = true } } + }; + + sutProvider.GetDependency() + .GetOrganizationAbilitiesAsync() + .Returns(orgAbilities); + sutProvider.GetDependency() + .GetProviderAbilitiesAsync() + .Returns(new Dictionary()); + sutProvider.GetDependency() + .GetManyByUserAsync(userId) + .Returns(new List { orgUser }); + sutProvider.GetDependency() + .ProviderMembershipAsync( + Arg.Any(), userId) + .Returns(new List()); + + await sutProvider.Sut.LogUserEventAsync(userId, eventType, includeAcceptedStatusOrgs: true); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Is(e => + e.OrganizationId == null && e.UserId == userId && e.Type == eventType)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .CreateManyAsync(default); + } }