diff --git a/src/Api/Public/Controllers/CollectionsController.cs b/src/Api/Public/Controllers/CollectionsController.cs index 656f2980ca..836fe3a4f9 100644 --- a/src/Api/Public/Controllers/CollectionsController.cs +++ b/src/Api/Public/Controllers/CollectionsController.cs @@ -8,7 +8,6 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces; using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -21,18 +20,15 @@ public class CollectionsController : Controller private readonly ICollectionRepository _collectionRepository; private readonly IUpdateCollectionCommand _updateCollectionCommand; private readonly ICurrentContext _currentContext; - private readonly IApplicationCacheService _applicationCacheService; public CollectionsController( ICollectionRepository collectionRepository, IUpdateCollectionCommand updateCollectionCommand, - ICurrentContext currentContext, - IApplicationCacheService applicationCacheService) + ICurrentContext currentContext) { _collectionRepository = collectionRepository; _updateCollectionCommand = updateCollectionCommand; _currentContext = currentContext; - _applicationCacheService = applicationCacheService; } /// @@ -49,7 +45,8 @@ public class CollectionsController : Controller public async Task Get(Guid id) { (var collection, var access) = await _collectionRepository.GetByIdWithAccessAsync(id); - if (collection == null || collection.OrganizationId != _currentContext.OrganizationId) + if (collection == null || collection.OrganizationId != _currentContext.OrganizationId || + collection.Type == CollectionType.DefaultUserCollection) { return new NotFoundResult(); } diff --git a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs index 6e07bd9ff8..cbdf3913cc 100644 --- a/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/AdminConsole/Repositories/IOrganizationUserRepository.cs @@ -22,7 +22,19 @@ public interface IOrganizationUserRepository : IRepository GetByOrganizationAsync(Guid organizationId, Guid userId); Task>> GetByIdWithCollectionsAsync(Guid id); Task GetDetailsByIdAsync(Guid id); + /// + /// Returns the OrganizationUser and its associated collections (excluding DefaultUserCollections). + /// + /// The id of the OrganizationUser + /// A tuple containing the OrganizationUser and its associated collections Task<(OrganizationUserUserDetails? OrganizationUser, ICollection Collections)> GetDetailsByIdWithCollectionsAsync(Guid id); + /// + /// Returns the OrganizationUsers and their associated collections (excluding DefaultUserCollections). + /// + /// The id of the organization + /// Whether to include groups + /// Whether to include collections + /// A list of OrganizationUserUserDetails Task> GetManyDetailsByOrganizationAsync(Guid organizationId, bool includeGroups = false, bool includeCollections = false); Task> GetManyDetailsByUserAsync(Guid userId, OrganizationUserStatusType? status = null); diff --git a/src/Core/Repositories/ICollectionRepository.cs b/src/Core/Repositories/ICollectionRepository.cs index 1d41a6ee1f..da4f6aa580 100644 --- a/src/Core/Repositories/ICollectionRepository.cs +++ b/src/Core/Repositories/ICollectionRepository.cs @@ -16,12 +16,12 @@ public interface ICollectionRepository : IRepository /// /// Return all collections that belong to the organization. Does not include any permission details or group/user - /// access relationships. + /// access relationships. Excludes default collections (My Items collections). /// Task> GetManyByOrganizationIdAsync(Guid organizationId); /// - /// Return all collections that belong to the organization. Includes group/user access relationships for each collection. + /// Return all shared collections that belong to the organization. Includes group/user access relationships for each collection. /// Task>> GetManyByOrganizationIdWithAccessAsync(Guid organizationId); @@ -34,9 +34,10 @@ public interface ICollectionRepository : IRepository Task> GetManyByUserIdAsync(Guid userId); /// - /// Returns all collections for an organization, including permission info for the specified user. + /// Returns all shared collections for an organization, including permission info for the specified user. /// This does not perform any authorization checks internally! /// 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); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index f311baec90..bb392a2e60 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -257,7 +257,8 @@ public class OrganizationUserRepository : Repository new CollectionAccessSelection { @@ -369,6 +370,8 @@ public class OrganizationUserRepository : Repository c.OrganizationUserId).ToList(); } diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 73268d75bf..0840a3f751 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; @@ -216,7 +217,8 @@ public class CollectionRepository : Repository if (_organizationId.HasValue) { - baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId); + baseCollectionQuery = baseCollectionQuery.Where(x => + x.c.OrganizationId == _organizationId && + x.c.Type != CollectionType.DefaultUserCollection); } else if (_collectionId.HasValue) { diff --git a/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql b/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql index c78d7390a7..b3cf499f77 100644 --- a/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql +++ b/src/Sql/dbo/Stored Procedures/CollectionUser_ReadByOrganizationUserIds.sql @@ -10,6 +10,10 @@ BEGIN [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] != 1 -- Exclude DefaultUserCollection END diff --git a/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql index 0d317ebded..6a7fefeb6b 100644 --- a/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/Collection_ReadByOrganizationId.sql @@ -9,5 +9,6 @@ BEGIN FROM [dbo].[CollectionView] WHERE - [OrganizationId] = @OrganizationId + [OrganizationId] = @OrganizationId AND + [Type] != 1 -- Exclude DefaultUserCollection END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql index b76e4b8775..ed683d8392 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUserUserDetails_ReadWithCollectionsById.sql @@ -15,6 +15,9 @@ BEGIN [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] != 1 -- Exclude default user collections END diff --git a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql index 267024f56c..bd8d48b29b 100644 --- a/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql +++ b/src/Sql/dbo/Vault/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql @@ -66,7 +66,8 @@ BEGIN LEFT JOIN [dbo].[CollectionGroup] CG ON CG.[CollectionId] = C.[Id] AND CG.[GroupId] = GU.[GroupId] WHERE - C.[OrganizationId] = @OrganizationId + C.[OrganizationId] = @OrganizationId AND + C.[Type] != 1 -- Exclude DefaultUserCollection GROUP BY C.[Id], C.[OrganizationId], diff --git a/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs b/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs new file mode 100644 index 0000000000..d896fc9c74 --- /dev/null +++ b/test/Api.Test/Public/Controllers/CollectionsControllerTests.cs @@ -0,0 +1,122 @@ +using Bit.Api.Models.Public.Response; +using Bit.Api.Public.Controllers; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Mvc; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Public.Controllers; + +[ControllerCustomize(typeof(CollectionsController))] +[SutProviderCustomize] +public class CollectionsControllerTests +{ + [Theory, BitAutoData] + public async Task Get_WithDefaultUserCollection_ReturnsNotFound( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.DefaultUserCollection; + var access = new CollectionAccessDetails + { + Groups = new List(), + Users = new List() + }; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdWithAccessAsync(collection.Id) + .Returns(new Tuple(collection, access)); + + // Act + var result = await sutProvider.Sut.Get(collection.Id); + + // Assert + Assert.IsType(result); + } + + [Theory, BitAutoData] + public async Task Get_WithSharedCollection_ReturnsCollection( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.SharedCollection; + var access = new CollectionAccessDetails + { + Groups = [], + Users = [] + }; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdWithAccessAsync(collection.Id) + .Returns(new Tuple(collection, access)); + + // Act + var result = await sutProvider.Sut.Get(collection.Id); + + // Assert + var jsonResult = Assert.IsType(result); + var response = Assert.IsType(jsonResult.Value); + Assert.Equal(collection.Id, response.Id); + Assert.Equal(collection.Type, response.Type); + } + + [Theory, BitAutoData] + public async Task Delete_WithDefaultUserCollection_ReturnsBadRequest( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.DefaultUserCollection; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdAsync(collection.Id) + .Returns(collection); + + // Act + var result = await sutProvider.Sut.Delete(collection.Id); + + // Assert + var badRequestResult = Assert.IsType(result); + var errorResponse = Assert.IsType(badRequestResult.Value); + Assert.Contains("You cannot delete a collection with the type as DefaultUserCollection", errorResponse.Message); + + await sutProvider.GetDependency() + .DidNotReceive() + .DeleteAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task Delete_WithSharedCollection_ReturnsOk( + Collection collection, SutProvider sutProvider) + { + // Arrange + collection.Type = CollectionType.SharedCollection; + + sutProvider.GetDependency() + .OrganizationId.Returns(collection.OrganizationId); + sutProvider.GetDependency() + .GetByIdAsync(collection.Id) + .Returns(collection); + + // Act + var result = await sutProvider.Sut.Delete(collection.Id); + + // Assert + Assert.IsType(result); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(collection); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs index b96998415d..90596a23b1 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs @@ -296,10 +296,29 @@ public class CollectionRepositoryTests } }, null); + // Create a default user collection (should be excluded from admin console results) + var defaultCollection = new Collection + { + Name = "My Items Collection", + OrganizationId = organization.Id, + Type = CollectionType.DefaultUserCollection + }; + + await collectionRepository.CreateAsync(defaultCollection, null, users: new[] + { + new CollectionAccessSelection() + { + Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true + } + }); + var collections = await collectionRepository.GetManyByOrganizationIdWithPermissionsAsync(organization.Id, user.Id, true); Assert.NotNull(collections); + // Should return only 3 collections (excluding the default user collection) + Assert.Equal(3, collections.Count); + collections = collections.OrderBy(c => c.Name).ToList(); Assert.Collection(collections, c1 => @@ -463,4 +482,69 @@ public class CollectionRepositoryTests Assert.False(c3.Unmanaged); }); } + + /// + /// Test to ensure collections are properly retrieved by organization + /// + [DatabaseTheory, DatabaseData] + public async Task GetManyByOrganizationIdAsync_Success( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository, + IOrganizationUserRepository organizationUserRepository) + { + var user = await userRepository.CreateAsync(new User + { + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", + }); + + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "billing@email.com" + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = user.Id, + Status = OrganizationUserStatusType.Confirmed, + }); + + var collection1 = new Collection { Name = "Collection 1", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection1, null, null); + + var collection2 = new Collection { Name = "Collection 2", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection2, null, null); + + var collection3 = new Collection { Name = "Collection 3", OrganizationId = organization.Id, }; + await collectionRepository.CreateAsync(collection3, null, null); + + // Create a default user collection (should not be returned by this method) + var defaultCollection = new Collection + { + Name = "My Items", + OrganizationId = organization.Id, + Type = CollectionType.DefaultUserCollection + }; + await collectionRepository.CreateAsync(defaultCollection, null, null); + + var collections = await collectionRepository.GetManyByOrganizationIdAsync(organization.Id); + + Assert.NotNull(collections); + Assert.Equal(3, collections.Count); // Should only return the 3 shared collections, excluding the default user collection + Assert.All(collections, c => Assert.Equal(organization.Id, c.OrganizationId)); + Assert.All(collections, c => Assert.NotEqual(CollectionType.DefaultUserCollection, c.Type)); + + // Verify specific collections are returned + Assert.Contains(collections, c => c.Name == "Collection 1"); + Assert.Contains(collections, c => c.Name == "Collection 2"); + Assert.Contains(collections, c => c.Name == "Collection 3"); + Assert.DoesNotContain(collections, c => c.Name == "My Items"); + } } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index 0df5dcfb50..6919ce7bce 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -184,6 +184,87 @@ public class OrganizationUserRepositoryTests r.EncryptedPrivateKey == "privatekey"); } + [DatabaseTheory, DatabaseData] + public async Task GetManyDetailsByOrganizationAsync_WithIncludeCollections_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 regular collection + var regularCollection = await collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = "Regular 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 = regularCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + }, + new CollectionAccessSelection + { + Id = defaultCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true + } + }); + + // Get organization users with collections included + var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync( + organization.Id, includeGroups: false, includeCollections: true); + + Assert.NotNull(organizationUsers); + Assert.Single(organizationUsers); + + var orgUserWithCollections = organizationUsers.First(); + Assert.NotNull(orgUserWithCollections.Collections); + + // Should only include the regular collection, not the default collection + Assert.Single(orgUserWithCollections.Collections); + Assert.Equal(regularCollection.Id, orgUserWithCollections.Collections.First().Id); + Assert.DoesNotContain(orgUserWithCollections.Collections, c => c.Id == defaultCollection.Id); + } + [DatabaseTheory, DatabaseData] public async Task GetManyDetailsByUserAsync_Works(IUserRepository userRepository, IOrganizationRepository organizationRepository, @@ -498,6 +579,17 @@ public class OrganizationUserRepositoryTests RevisionDate = requestTime }); + // Create a default user collection that should be excluded from admin results + var defaultCollection = await collectionRepository.CreateAsync(new Collection + { + Id = CoreHelpers.GenerateComb(), + OrganizationId = organization.Id, + Name = "My Items", + Type = CollectionType.DefaultUserCollection, + CreationDate = requestTime, + RevisionDate = requestTime + }); + var group1 = await groupRepository.CreateAsync(new Group { Id = CoreHelpers.GenerateComb(), @@ -544,6 +636,13 @@ public class OrganizationUserRepositoryTests ReadOnly = true, HidePasswords = false, Manage = false + }, + new CollectionAccessSelection + { + Id = defaultCollection.Id, + ReadOnly = false, + HidePasswords = false, + Manage = true } ], Groups = [group1.Id] @@ -605,7 +704,11 @@ public class OrganizationUserRepositoryTests var orgUser1 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[0].OrganizationUser.Id); var group1Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[0].OrganizationUser.Id); Assert.Equal(orgUserCollection[0].OrganizationUser.Id, orgUser1.OrganizationUser.Id); + + // Should only return the regular collection, not the default collection (even though both were assigned) + Assert.Single(orgUser1.Collections); Assert.Equal(collection1.Id, orgUser1.Collections.First().Id); + Assert.DoesNotContain(orgUser1.Collections, c => c.Id == defaultCollection.Id); Assert.Equal(group1.Id, group1Database.First()); diff --git a/util/Migrator/DbScripts/2025-06-30_00_ExcludeDefaultCollections.sql b/util/Migrator/DbScripts/2025-06-30_00_ExcludeDefaultCollections.sql new file mode 100644 index 0000000000..d743f4e08c --- /dev/null +++ b/util/Migrator/DbScripts/2025-06-30_00_ExcludeDefaultCollections.sql @@ -0,0 +1,149 @@ +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationIdWithPermissions] + @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] != 1 -- Exclude DefaultUserCollection + 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 + +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[CollectionView] + WHERE + [OrganizationId] = @OrganizationId AND + [Type] != 1 -- Exclude DefaultUserCollection +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUserUserDetails_ReadWithCollectionsById] + @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] != 1 -- Exclude default user collections +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CollectionUser_ReadByOrganizationUserIds] + @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] != 1 -- Exclude DefaultUserCollection +END +GO