diff --git a/src/Api/Models/Response/CollectionResponseModel.cs b/src/Api/Models/Response/CollectionResponseModel.cs index 253acbfdf..d56ef5469 100644 --- a/src/Api/Models/Response/CollectionResponseModel.cs +++ b/src/Api/Models/Response/CollectionResponseModel.cs @@ -89,6 +89,7 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel ReadOnly = collection.ReadOnly; HidePasswords = collection.HidePasswords; Manage = collection.Manage; + Unmanaged = collection.Unmanaged; Groups = collection.Groups?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty(); Users = collection.Users?.Select(g => new SelectionReadOnlyResponseModel(g)) ?? Enumerable.Empty(); } @@ -104,4 +105,5 @@ public class CollectionAccessDetailsResponseModel : CollectionResponseModel public bool ReadOnly { get; set; } public bool HidePasswords { get; set; } public bool Manage { get; set; } + public bool Unmanaged { get; set; } } diff --git a/src/Core/Models/Data/CollectionAdminDetails.cs b/src/Core/Models/Data/CollectionAdminDetails.cs index 8b96eb4db..036f7d037 100644 --- a/src/Core/Models/Data/CollectionAdminDetails.cs +++ b/src/Core/Models/Data/CollectionAdminDetails.cs @@ -14,4 +14,9 @@ public class CollectionAdminDetails : CollectionDetails /// Flag for whether the user has been explicitly assigned to the collection either directly or through a group. /// public bool Assigned { get; set; } + + /// + /// Flag for whether a collection is managed by an active user or group. + /// + public bool Unmanaged { get; set; } } diff --git a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs index 8773a69a8..b1c546373 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CollectionRepository.cs @@ -391,7 +391,8 @@ public class CollectionRepository : Repository new CollectionAdminDetails { Id = collectionGroup.Key.Id, @@ -404,7 +405,8 @@ public class CollectionRepository : Repository Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), - Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))) + Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))), + Unmanaged = collectionGroup.Key.Unmanaged }).ToList(); } else @@ -417,7 +419,8 @@ public class CollectionRepository : Repository Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), - Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))) + Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))), + Unmanaged = collectionGroup.Key.Unmanaged }).ToListAsync(); } @@ -511,7 +515,8 @@ public class CollectionRepository : Repository Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), - Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))) + Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))), + Unmanaged = collectionGroup.Select(c => c.Unmanaged).FirstOrDefault() }).FirstOrDefault(); } else @@ -539,7 +544,8 @@ public class CollectionRepository : Repository Convert.ToInt32(c.HidePasswords))), Manage = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Manage))), - Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))) + Assigned = Convert.ToBoolean(collectionGroup.Max(c => Convert.ToInt32(c.Assigned))), + Unmanaged = collectionGroup.Select(c => c.Unmanaged).FirstOrDefault() }).FirstOrDefaultAsync(); } diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs index f7b498494..6b0c313a3 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/CollectionAdminDetailsQuery.cs @@ -1,4 +1,5 @@ -using Bit.Core.Models.Data; +using Bit.Core.Enums; +using Bit.Core.Models.Data; namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; @@ -46,6 +47,17 @@ public class CollectionAdminDetailsQuery : IQuery from cg in cg_g.DefaultIfEmpty() select new { c, cu, cg }; + // Subqueries to determine if a colection is managed by an active user or group. + var activeUserManageRights = from cu in dbContext.CollectionUsers + join ou in dbContext.OrganizationUsers + on cu.OrganizationUserId equals ou.Id + where ou.Status == OrganizationUserStatusType.Confirmed && cu.Manage + select cu.CollectionId; + + var activeGroupManageRights = from cg in dbContext.CollectionGroups + where cg.Manage + select cg.CollectionId; + if (_organizationId.HasValue) { baseCollectionQuery = baseCollectionQuery.Where(x => x.c.OrganizationId == _organizationId); @@ -71,6 +83,7 @@ public class CollectionAdminDetailsQuery : IQuery HidePasswords = (bool?)x.cu.HidePasswords ?? (bool?)x.cg.HidePasswords ?? false, Manage = (bool?)x.cu.Manage ?? (bool?)x.cg.Manage ?? false, Assigned = x.cu != null || x.cg != null, + Unmanaged = !activeUserManageRights.Contains(x.c.Id) && !activeGroupManageRights.Contains(x.c.Id), }); } diff --git a/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql b/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql index 5cd6cc93a..0d1df79c3 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByIdWithPermissions.sql @@ -31,7 +31,29 @@ BEGIN CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL THEN 0 ELSE 1 - END) AS [Assigned] + END) AS [Assigned], + CASE + WHEN + -- No active 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 + OU2.[Status] = 2 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 diff --git a/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql b/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql index 88905eb6c..61384852b 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Collections/Collection_ReadByOrganizationIdWithPermissions.sql @@ -31,7 +31,29 @@ BEGIN CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL THEN 0 ELSE 1 - END) AS [Assigned] + END) AS [Assigned], + CASE + WHEN + -- No active 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 + OU2.[Status] = 2 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 diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs index dc420c876..9b7e8f519 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs @@ -310,6 +310,7 @@ public class CollectionRepositoryTests Assert.True(c1.Manage); Assert.False(c1.ReadOnly); Assert.False(c1.HidePasswords); + Assert.False(c1.Unmanaged); }, c2 => { Assert.NotNull(c2); @@ -319,6 +320,7 @@ public class CollectionRepositoryTests Assert.False(c2.Manage); Assert.True(c2.ReadOnly); Assert.False(c2.HidePasswords); + Assert.True(c2.Unmanaged); }, c3 => { Assert.NotNull(c3); @@ -328,6 +330,7 @@ public class CollectionRepositoryTests Assert.False(c3.Manage); Assert.False(c3.ReadOnly); Assert.False(c3.HidePasswords); + Assert.False(c3.Unmanaged); }); } @@ -436,6 +439,7 @@ public class CollectionRepositoryTests Assert.True(c1.Manage); Assert.False(c1.ReadOnly); Assert.False(c1.HidePasswords); + Assert.False(c1.Unmanaged); }, c2 => { Assert.NotNull(c2); @@ -445,6 +449,7 @@ public class CollectionRepositoryTests Assert.False(c2.Manage); Assert.True(c2.ReadOnly); Assert.False(c2.HidePasswords); + Assert.True(c2.Unmanaged); }, c3 => { Assert.NotNull(c3); @@ -454,6 +459,7 @@ public class CollectionRepositoryTests Assert.True(c3.Manage); // Group 2 is Manage Assert.False(c3.ReadOnly); Assert.False(c3.HidePasswords); + Assert.False(c3.Unmanaged); }); } } diff --git a/util/Migrator/DbScripts/2024-05-17_00_CollectionWithPermissionsAndUnmanagedQueries.sql b/util/Migrator/DbScripts/2024-05-17_00_CollectionWithPermissionsAndUnmanagedQueries.sql new file mode 100644 index 000000000..0e1526ca2 --- /dev/null +++ b/util/Migrator/DbScripts/2024-05-17_00_CollectionWithPermissionsAndUnmanagedQueries.sql @@ -0,0 +1,171 @@ +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 active 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 + OU2.[Status] = 2 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 + GROUP BY + C.[Id], + C.[OrganizationId], + C.[Name], + C.[CreationDate], + C.[RevisionDate], + C.[ExternalId] + + IF (@IncludeAccessRelationships = 1) + BEGIN + EXEC [dbo].[CollectionGroup_ReadByOrganizationId] @OrganizationId + EXEC [dbo].[CollectionUser_ReadByOrganizationId] @OrganizationId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Collection_ReadByIdWithPermissions] + @CollectionId 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 active 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 + OU2.[Status] = 2 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.[Id] = @CollectionId + GROUP BY + C.[Id], + C.[OrganizationId], + C.[Name], + C.[CreationDate], + C.[RevisionDate], + C.[ExternalId] + + IF (@IncludeAccessRelationships = 1) + BEGIN + EXEC [dbo].[CollectionGroup_ReadByCollectionId] @CollectionId + EXEC [dbo].[CollectionUser_ReadByCollectionId] @CollectionId + END +END +GO