diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 4a965eb22cde..5087edd2aade 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -46,6 +46,7 @@ public interface IOrganizationUserRepository : IRepository> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups = false, bool includeSharedCollections = false); Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null); + Task> GetManyConfirmedAcceptedDetailsByUserAsync(Guid userId); Task GetDetailsByUserAsync(Guid userId, Guid organizationId, OrganizationUserStatusType? status = null); Task UpdateGroupsAsync(Guid orgUserId, IEnumerable groupIds); diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index 7dbe4b18ff8e..e62303d56e54 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -344,6 +344,19 @@ public async Task> GetManyDetai } } + public async Task> GetManyConfirmedAcceptedDetailsByUserAsync(Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId]", + new { UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task GetDetailsByUserAsync(Guid userId, Guid organizationId, OrganizationUserStatusType? status = null) { diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index afc3de9e487f..ba428490588e 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -380,7 +380,6 @@ public async Task GetDetailsByUserAsync(Gui { var dbContext = GetDatabaseContext(scope); var view = new OrganizationUserOrganizationDetailsViewQuery(); - var t = await (view.Run(dbContext)).ToArrayAsync(); var entity = await view.Run(dbContext) .FirstOrDefaultAsync(o => o.UserId == userId && o.OrganizationId == organizationId && @@ -574,6 +573,22 @@ public async Task> GetManyDetai } } + public async Task> GetManyConfirmedAcceptedDetailsByUserAsync(Guid userId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var view = new OrganizationUserOrganizationDetailsViewQuery(); + var query = from organizationUserDetails in view.Run(dbContext) + where organizationUserDetails.UserId == userId && + (organizationUserDetails.Status == OrganizationUserStatusType.Confirmed || + organizationUserDetails.Status == OrganizationUserStatusType.Accepted) + select organizationUserDetails; + var organizationUsers = await query.ToListAsync(); + return organizationUsers; + } + } + public async Task> GetManyPublicKeysByOrganizationUserAsync(Guid organizationId, IEnumerable Ids) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId.sql new file mode 100644 index 000000000000..90524e536ca0 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserOrganizationDetailsView] + WHERE + [UserId] = @UserId + AND [Status] IN (1, 2) -- Accepted = 1, Confirmed = 2 +END \ No newline at end of file diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetManyConfirmedAcceptedDetailsByUserAsyncTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetManyConfirmedAcceptedDetailsByUserAsyncTests.cs new file mode 100644 index 000000000000..b41ea081042f --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/GetManyConfirmedAcceptedDetailsByUserAsyncTests.cs @@ -0,0 +1,157 @@ +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository; + +public class GetManyConfirmedAcceptedDetailsByUserAsyncTests +{ + [Theory, DatabaseData] + public async Task ReturnsDetails_WhenUserIsConfirmed( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user); + + // Act + var results = await organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id); + + // Assert + Assert.Single(results); + var result = results.Single(); + Assert.Equal(organization.Id, result.OrganizationId); + Assert.Equal(user.Id, result.UserId); + Assert.Equal(OrganizationUserStatusType.Confirmed, result.Status); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task ReturnsDetails_WhenUserIsAccepted( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user); + + // Act + var results = await organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id); + + // Assert + Assert.Single(results); + var result = results.Single(); + Assert.Equal(organization.Id, result.OrganizationId); + Assert.Equal(user.Id, result.UserId); + Assert.Equal(OrganizationUserStatusType.Accepted, result.Status); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task ReturnsDetailsAcrossMultipleOrganizations_WhenUserIsConfirmedOrAccepted( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + + var confirmedOrg = await organizationRepository.CreateTestOrganizationAsync(); + await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(confirmedOrg, user); + + var acceptedOrg = await organizationRepository.CreateTestOrganizationAsync(); + await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(acceptedOrg, user); + + // Act + var results = await organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id); + + // Assert + Assert.Equal(2, results.Count); + Assert.Contains(results, r => r.OrganizationId == confirmedOrg.Id && r.Status == OrganizationUserStatusType.Confirmed); + Assert.Contains(results, r => r.OrganizationId == acceptedOrg.Id && r.Status == OrganizationUserStatusType.Accepted); + + // Annul + await organizationRepository.DeleteAsync(confirmedOrg); + await organizationRepository.DeleteAsync(acceptedOrg); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task DoesNotReturnDetails_WhenUserIsInvited( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization); + + // Act + var results = await organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id); + + // Assert + Assert.DoesNotContain(results, r => r.OrganizationId == organization.Id); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task DoesNotReturnDetails_WhenUserIsRevoked( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var user = await userRepository.CreateTestUserAsync(); + var organization = await organizationRepository.CreateTestOrganizationAsync(); + await organizationUserRepository.CreateRevokedTestOrganizationUserAsync(organization, user); + + // Act + var results = await organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(user.Id); + + // Assert + Assert.DoesNotContain(results, r => r.OrganizationId == organization.Id); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteAsync(user); + } + + [Theory, DatabaseData] + public async Task DoesNotReturnDetails_ForOtherUsers( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository) + { + // Arrange + var targetUser = await userRepository.CreateTestUserAsync(); + var otherUser = await userRepository.CreateTestUserAsync(); + + var organization = await organizationRepository.CreateTestOrganizationAsync(); + await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, otherUser); + + // Act + var results = await organizationUserRepository.GetManyConfirmedAcceptedDetailsByUserAsync(targetUser.Id); + + // Assert + Assert.DoesNotContain(results, r => r.OrganizationId == organization.Id); + + // Annul + await organizationRepository.DeleteAsync(organization); + await userRepository.DeleteManyAsync([targetUser, otherUser]); + } +} diff --git a/util/Migrator/DbScripts/2026-04-06_00_AddOrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId.sql b/util/Migrator/DbScripts/2026-04-06_00_AddOrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId.sql new file mode 100644 index 000000000000..324e04a4dd39 --- /dev/null +++ b/util/Migrator/DbScripts/2026-04-06_00_AddOrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId.sql @@ -0,0 +1,15 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserOrganizationDetails_ReadAcceptedConfirmedByUserId] + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationUserOrganizationDetailsView] + WHERE + [UserId] = @UserId + AND [Status] IN (1,2) -- 1 = Accepted, 2 = Confirmed +END +GO