diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 081a86c9ad..ecea7caa96 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -155,7 +155,7 @@ public class OrganizationUsersController : BaseAdminConsoleController [Authorize] public async Task Get(Guid orgId, Guid id, bool includeGroups = false) { - var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id); + var (organizationUser, collections) = await _organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(id); if (organizationUser == null || organizationUser.OrganizationId != orgId) { throw new NotFoundException(); diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 220c812cae..e312f009c9 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -80,7 +80,7 @@ public class MembersController : Controller [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Get(Guid id) { - var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithCollectionsAsync(id); + var (orgUser, collections) = await _organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(id); if (orgUser == null || orgUser.OrganizationId != _currentContext.OrganizationId) { return new NotFoundResult(); @@ -123,7 +123,7 @@ public class MembersController : Controller [ProducesResponseType(typeof(ListResponseModel), (int)HttpStatusCode.OK)] public async Task List() { - var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeCollections: true); + var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId!.Value, includeSharedCollections: true); var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails); var memberResponses = organizationUserUserDetails.Select(u => diff --git a/src/Api/Controllers/CollectionsController.cs b/src/Api/Controllers/CollectionsController.cs index b3542cfde2..a1de65299f 100644 --- a/src/Api/Controllers/CollectionsController.cs +++ b/src/Api/Controllers/CollectionsController.cs @@ -81,7 +81,7 @@ public class CollectionsController : Controller [HttpGet("details")] public async Task> GetManyWithDetails(Guid orgId) { - var allOrgCollections = await _collectionRepository.GetManyByOrganizationIdWithPermissionsAsync( + var allOrgCollections = await _collectionRepository.GetManySharedByOrganizationIdWithPermissionsAsync( orgId, _currentContext.UserId.Value, true); var readAllAuthorized = diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 41622c24b7..583e86ab4d 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -28,21 +28,21 @@ public interface IOrganizationUserRepository : IRepository /// The id of the OrganizationUser /// A tuple containing the OrganizationUser and its associated collections - Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithCollectionsAsync(Guid id); + Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id); /// /// Returns the OrganizationUsers and their associated collections (excluding DefaultUserCollections). /// /// The id of the organization /// Whether to include groups - /// Whether to include collections + /// Whether to include shared collections /// A list of OrganizationUserUserDetails - Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false); + Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeSharedCollections = false); /// /// /// This method is optimized for performance. /// Reduces database round trips by fetching all data in fewer queries. /// - Task> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups = false, bool includeCollections = false); + Task> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups = false, bool includeSharedCollections = false); Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null); Task GetDetailsByUserAsync(Guid userId, Guid organizationId, diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 3f3b71d2d5..2db809e3de 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -45,7 +45,7 @@ public interface ICollectionRepository : IRepository /// Optionally, you can include access relationships for other Groups/Users and the collections. /// Excludes default collections (My Items collections) - used by Admin Console Collections tab. /// - Task> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships); + Task> GetManySharedByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships); /// /// Returns the collection by Id, including permission info for the specified user. diff --git a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs index bd670347a9..ff2488d084 100644 --- a/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -187,12 +187,12 @@ public class OrganizationUserRepository : Repository, IO return results.SingleOrDefault(); } } - public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithCollectionsAsync(Guid id) + public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryMultipleAsync( - "[dbo].[OrganizationUserUserDetails_ReadWithCollectionsById]", + "[dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById]", new { Id = id }, commandType: CommandType.StoredProcedure); @@ -202,7 +202,7 @@ public class OrganizationUserRepository : Repository, IO } } - public async Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeCollections) + public async Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeSharedCollections) { using (var connection = new SqlConnection(ConnectionString)) { @@ -216,7 +216,7 @@ public class OrganizationUserRepository : Repository, IO var users = results.ToList(); - if (!includeCollections && !includeGroups) + if (!includeSharedCollections && !includeGroups) { return users; } @@ -231,10 +231,10 @@ public class OrganizationUserRepository : Repository, IO commandType: CommandType.StoredProcedure)).GroupBy(u => u.OrganizationUserId).ToList(); } - if (includeCollections) + if (includeSharedCollections) { userCollections = (await connection.QueryAsync( - "[dbo].[CollectionUser_ReadByOrganizationUserIds]", + "[dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds]", new { OrganizationUserIds = orgUserIds }, commandType: CommandType.StoredProcedure)).GroupBy(u => u.OrganizationUserId).ToList(); } @@ -267,7 +267,7 @@ public class OrganizationUserRepository : Repository, IO } } - public async Task> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups, bool includeCollections) + public async Task> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups, bool includeSharedCollections) { using (var connection = new SqlConnection(ConnectionString)) { @@ -278,7 +278,7 @@ public class OrganizationUserRepository : Repository, IO { OrganizationId = organizationId, IncludeGroups = includeGroups, - IncludeCollections = includeCollections + IncludeCollections = includeSharedCollections }, commandType: CommandType.StoredProcedure); @@ -297,7 +297,7 @@ public class OrganizationUserRepository : Repository, IO // Read collection associations (third result set, if requested) Dictionary>? userCollectionMap = null; - if (includeCollections) + if (includeSharedCollections) { var collectionUsers = await results.ReadAsync(); userCollectionMap = collectionUsers diff --git a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs index 1531703427..2c733956c0 100644 --- a/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CollectionRepository.cs @@ -153,12 +153,12 @@ public class CollectionRepository : Repository, ICollectionRep } } - public async Task> GetManyByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships) + public async Task> GetManySharedByOrganizationIdWithPermissionsAsync(Guid organizationId, Guid userId, bool includeAccessRelationships) { using (var connection = new SqlConnection(ConnectionString)) { var results = await connection.QueryMultipleAsync( - $"[{Schema}].[Collection_ReadByOrganizationIdWithPermissions]", + $"[{Schema}].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions]", new { OrganizationId = organizationId, UserId = userId, IncludeAccessRelationships = includeAccessRelationships }, commandType: CommandType.StoredProcedure); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index ae55099775..c15bd72c5b 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -350,7 +350,7 @@ public class OrganizationUserRepository : Repository Collections)> GetDetailsByIdWithCollectionsAsync(Guid id) + public async Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithSharedCollectionsAsync(Guid id) { var organizationUserUserDetails = await GetDetailsByIdAsync(id); using (var scope = ServiceScopeFactory.CreateScope()) @@ -359,7 +359,7 @@ public class OrganizationUserRepository : Repository new CollectionAccessSelection { @@ -438,7 +438,7 @@ public class OrganizationUserRepository : Repository> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeCollections) + public async Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups, bool includeSharedCollections) { using (var scope = ServiceScopeFactory.CreateScope()) { @@ -448,7 +448,7 @@ public class OrganizationUserRepository : Repository g.OrganizationUserId).ToList(); } - if (includeCollections) + if (includeSharedCollections) { collections = (await (from cu in dbContext.CollectionUsers join ou in userIdEntities on cu.OrganizationUserId equals ou.Id join c in dbContext.Collections on cu.CollectionId equals c.Id - where c.Type != CollectionType.DefaultUserCollection + where c.Type == CollectionType.SharedCollection select cu).ToListAsync()) .GroupBy(c => c.OrganizationUserId).ToList(); } @@ -506,7 +506,7 @@ public class OrganizationUserRepository : Repository> GetManyDetailsByOrganizationAsync_vNext( - Guid organizationId, bool includeGroups, bool includeCollections) + Guid organizationId, bool includeGroups, bool includeSharedCollections) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -541,7 +541,7 @@ public class OrganizationUserRepository : Repository gu.GroupId).ToList() : new List(), - Collections = includeCollections + Collections = includeSharedCollections ? ou.CollectionUsers .Where(cu => cu.Collection.Type == CollectionType.SharedCollection) .Select(cu => new CollectionAccessSelection diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 74150246b1..141928b78a 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -303,7 +303,7 @@ public class CollectionRepository : Repository> GetManyByOrganizationIdWithPermissionsAsync( + public async Task> GetManySharedByOrganizationIdWithPermissionsAsync( Guid organizationId, Guid userId, bool includeAccessRelationships) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs index 2b6e61d056..2ec671d20b 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs @@ -62,7 +62,7 @@ public class CollectionAdminDetailsQuery : IQuery { baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId && - x.c.Type != CollectionType.DefaultUserCollection); + x.c.Type == CollectionType.SharedCollection); } else if (_collectionId.HasValue) { diff --git a/src/Sql/dbo/Stored Procedures/CollectionUser_ReadSharedCollectionsByOrganizationUserIds.sql b/src/Sql/dbo/Stored Procedures/CollectionUser_ReadSharedCollectionsByOrganizationUserIds.sql new file mode 100644 index 0000000000..55cd477ab0 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/CollectionUser_ReadSharedCollectionsByOrganizationUserIds.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds] + @OrganizationUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + CU.* + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] + INNER JOIN + @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id] + WHERE + C.[Type] = 0 -- Only SharedCollection +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithSharedCollectionsById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithSharedCollectionsById.sql new file mode 100644 index 0000000000..96db73c80a --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithSharedCollectionsById.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [OrganizationUserUserDetails_ReadById] @Id + + SELECT + CU.[CollectionId] Id, + CU.[ReadOnly], + CU.[HidePasswords], + CU.[Manage] + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = [OU].[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] + WHERE + [OrganizationUserId] = @Id + AND C.[Type] = 0 -- Only SharedCollection +END diff --git a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql new file mode 100644 index 0000000000..52120fe28a --- /dev/null +++ b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadSharedCollectionsByOrganizationIdWithPermissions.sql @@ -0,0 +1,86 @@ +CREATE PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @IncludeAccessRelationships BIT +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.*, + MIN(CASE + WHEN + COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0 + THEN 0 + ELSE 1 + END) AS [ReadOnly], + MIN(CASE + WHEN + COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0 + THEN 0 + ELSE 1 + END) AS [HidePasswords], + MAX(CASE + WHEN + COALESCE(CU.[Manage], CG.[Manage], 0) = 0 + THEN 0 + ELSE 1 + END) AS [Manage], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) AS [Assigned], + CASE + WHEN + -- No user or group has manage rights + NOT EXISTS( + SELECT 1 + FROM [dbo].[CollectionUser] CU2 + JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id] + WHERE + CU2.[CollectionId] = C.[Id] AND + CU2.[Manage] = 1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionGroup] CG2 + WHERE + CG2.[CollectionId] = C.[Id] AND + CG2.[Manage] = 1 + ) + THEN 1 + ELSE 0 + END AS [Unmanaged] + FROM + [dbo].[CollectionView] C + LEFT JOIN + [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id] + LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] + LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] + WHERE + C.[OrganizationId] = @OrganizationId AND + C.[Type] = 0 -- Only SharedCollection + GROUP BY + C.[Id], + C.[OrganizationId], + C.[Name], + C.[CreationDate], + C.[RevisionDate], + C.[ExternalId], + C.[DefaultUserCollectionEmail], + C.[Type] + + IF (@IncludeAccessRelationships = 1) + BEGIN + EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId + EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId + END +END diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index 7edd3c662c..f9b50e736d 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -287,7 +287,7 @@ public class OrganizationUsersControllerTests .Returns(true); sutProvider.GetDependency() - .GetDetailsByIdWithCollectionsAsync(organizationUser.Id) + .GetDetailsByIdWithSharedCollectionsAsync(organizationUser.Id) .Returns((organizationUser, collections)); sutProvider.GetDependency() diff --git a/test/Api.Test/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Controllers/CollectionsControllerTests.cs index 33b7e20327..c345e3602f 100644 --- a/test/Api.Test/Controllers/CollectionsControllerTests.cs +++ b/test/Api.Test/Controllers/CollectionsControllerTests.cs @@ -107,7 +107,7 @@ public class CollectionsControllerTests await sutProvider.Sut.GetManyWithDetails(organization.Id); - await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true); + await sutProvider.GetDependency().Received(1).GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true); } [Theory, BitAutoData] @@ -143,12 +143,12 @@ public class CollectionsControllerTests .Returns(AuthorizationResult.Success()); sutProvider.GetDependency() - .GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true) + .GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true) .Returns(collections); var response = await sutProvider.Sut.GetManyWithDetails(organization.Id); - await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdWithPermissionsAsync(organization.Id, userId, true); + await sutProvider.GetDependency().Received(1).GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, userId, true); Assert.Single(response.Data); Assert.All(response.Data, c => Assert.Equal(organization.Id, c.OrganizationId)); Assert.All(response.Data, c => Assert.Equal(managedCollection.Id, c.Id)); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs index 79a96eeaeb..0f8feb4a6a 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs @@ -312,7 +312,7 @@ public class CollectionRepositoryTests } }); - var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true); + var collections = await collectionRepository.GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true); Assert.NotNull(collections); @@ -442,7 +442,7 @@ public class CollectionRepositoryTests } }, null); - var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true); + var collections = await collectionRepository.GetManySharedByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true); Assert.NotNull(collections); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index b77406abf5..287c50afca 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -430,7 +430,7 @@ public class OrganizationUserRepositoryTests // Get organization users with collections included var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync( - organization.Id, includeGroups: false, includeCollections: true); + organization.Id, includeGroups: false, includeSharedCollections: true); Assert.NotNull(organizationUsers); Assert.Single(organizationUsers); @@ -444,6 +444,83 @@ public class OrganizationUserRepositoryTests Assert.DoesNotContain(orgUserWithCollections.Collections, c => c.Id == defaultCollection.Id); } + [DatabaseTheory, DatabaseData] + public async Task GetDetailsByIdWithSharedCollectionsAsync_ExcludesDefaultCollections( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@example.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + BillingEmail = user.Email, + Plan = "Test", + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + }); + + // Create a shared collection + var sharedCollection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = "Shared Collection", + Type = CollectionType.SharedCollection + }); + + // Create a default user collection + var defaultCollection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = "Default Collection", + Type = CollectionType.DefaultUserCollection, + DefaultUserCollectionEmail = user.Email + }); + + // Assign the organization user to both collections + await organizationUserRepository.ReplaceAsync(orgUser, new List + { + new CollectionAccessSelection + { + Id = sharedCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + }, + new CollectionAccessSelection + { + Id = defaultCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + } + }); + + // Get organization user details with collections + var (orgUserDetails, collections) = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUser.Id); + + Assert.NotNull(orgUserDetails); + Assert.NotNull(collections); + + // Should only include the shared collection, not the default collection + Assert.Single(collections); + Assert.Equal(sharedCollection.Id, collections.First().Id); + Assert.DoesNotContain(collections, c => c.Id == defaultCollection.Id); + } + [DatabaseTheory, DatabaseData] public async Task GetManyDetailsByUserAsync_Works(IUserRepository userRepository, IOrganizationRepository organizationRepository, @@ -835,7 +912,7 @@ public class OrganizationUserRepositoryTests await organizationUserRepository.CreateManyAsync(orgUserCollection); - var orgUser1 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[0].OrganizationUser.Id); + var orgUser1 = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUserCollection[0].OrganizationUser.Id); var group1Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[0].OrganizationUser.Id); Assert.Equal(orgUserCollection[0].OrganizationUser.Id, orgUser1.OrganizationUser.Id); @@ -846,13 +923,13 @@ public class OrganizationUserRepositoryTests Assert.Equal(group1.Id, group1Database.First()); - var orgUser2 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[1].OrganizationUser.Id); + var orgUser2 = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUserCollection[1].OrganizationUser.Id); var group2Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[1].OrganizationUser.Id); Assert.Equal(orgUserCollection[1].OrganizationUser.Id, orgUser2.OrganizationUser.Id); Assert.Equal(collection2.Id, orgUser2.Collections.First().Id); Assert.Equal(group2.Id, group2Database.First()); - var orgUser3 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[2].OrganizationUser.Id); + var orgUser3 = await organizationUserRepository.GetDetailsByIdWithSharedCollectionsAsync(orgUserCollection[2].OrganizationUser.Id); var group3Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[2].OrganizationUser.Id); Assert.Equal(orgUserCollection[2].OrganizationUser.Id, orgUser3.OrganizationUser.Id); Assert.Equal(collection3.Id, orgUser3.Collections.First().Id); @@ -928,7 +1005,7 @@ public class OrganizationUserRepositoryTests AccessSecretsManager = true }); - var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: false, includeCollections: false); + var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: false, includeSharedCollections: false); Assert.NotNull(responseModel); Assert.Equal(2, responseModel.Count); @@ -1083,7 +1160,7 @@ public class OrganizationUserRepositoryTests await organizationUserRepository.CreateManyAsync(createOrgUserWithCollections); - var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: true, includeCollections: true); + var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: true, includeSharedCollections: true); Assert.NotNull(responseModel); Assert.Single(responseModel); diff --git a/util/Migrator/DbScripts/2026-01-22_00_AddSharedCollectionsStoredProcedures.sql b/util/Migrator/DbScripts/2026-01-22_00_AddSharedCollectionsStoredProcedures.sql new file mode 100644 index 0000000000..bef4cd17c5 --- /dev/null +++ b/util/Migrator/DbScripts/2026-01-22_00_AddSharedCollectionsStoredProcedures.sql @@ -0,0 +1,135 @@ +-- Add stored procedures for reading shared collections (Type = 0) + +CREATE OR ALTER PROCEDURE [dbo].[CollectionUser_ReadSharedCollectionsByOrganizationUserIds] + @OrganizationUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SELECT + CU.* + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = OU.[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] + INNER JOIN + @OrganizationUserIds OUI ON OUI.[Id] = OU.[Id] + WHERE + C.[Type] = 0 -- Only SharedCollection +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithSharedCollectionsById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [OrganizationUserUserDetails_ReadById] @Id + + SELECT + CU.[CollectionId] Id, + CU.[ReadOnly], + CU.[HidePasswords], + CU.[Manage] + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + [dbo].[CollectionUser] CU ON CU.[OrganizationUserId] = [OU].[Id] + INNER JOIN + [dbo].[Collection] C ON CU.[CollectionId] = C.[Id] + WHERE + [OrganizationUserId] = @Id + AND C.[Type] = 0 -- Only SharedCollection +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadSharedCollectionsByOrganizationIdWithPermissions] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @IncludeAccessRelationships BIT +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.*, + MIN(CASE + WHEN + COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0 + THEN 0 + ELSE 1 + END) AS [ReadOnly], + MIN(CASE + WHEN + COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0 + THEN 0 + ELSE 1 + END) AS [HidePasswords], + MAX(CASE + WHEN + COALESCE(CU.[Manage], CG.[Manage], 0) = 0 + THEN 0 + ELSE 1 + END) AS [Manage], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) AS [Assigned], + CASE + WHEN + -- No user or group has manage rights + NOT EXISTS( + SELECT 1 + FROM [dbo].[CollectionUser] CU2 + JOIN [dbo].[OrganizationUser] OU2 ON CU2.[OrganizationUserId] = OU2.[Id] + WHERE + CU2.[CollectionId] = C.[Id] AND + CU2.[Manage] = 1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM [dbo].[CollectionGroup] CG2 + WHERE + CG2.[CollectionId] = C.[Id] AND + CG2.[Manage] = 1 + ) + THEN 1 + ELSE 0 + END AS [Unmanaged] + FROM + [dbo].[CollectionView] C + LEFT JOIN + [dbo].[OrganizationUser] OU ON C.[OrganizationId] = OU.[OrganizationId] AND OU.[UserId] = @UserId + LEFT JOIN + [dbo].[CollectionUser] CU ON CU.[CollectionId] = C.[Id] AND CU.[OrganizationUserId] = [OU].[Id] + LEFT JOIN + [dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id] + LEFT JOIN + [dbo].[Group] G ON G.[Id] = GU.[GroupId] + LEFT JOIN + [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] + WHERE + C.[OrganizationId] = @OrganizationId AND + C.[Type] = 0 -- Only SharedCollection + GROUP BY + C.[Id], + C.[OrganizationId], + C.[Name], + C.[CreationDate], + C.[RevisionDate], + C.[ExternalId], + C.[DefaultUserCollectionEmail], + C.[Type] + + IF (@IncludeAccessRelationships = 1) + BEGIN + EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId + EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId + END +END +GO