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);
+ }
}