From 69da981480c5c4db56830886e86585fafef88a0d Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 16:55:29 -0800 Subject: [PATCH 01/44] [PM-14378] Introduce GetCipherPermissionsForOrganization query for Dapper CipherRepository --- .../Data/OrganizationCipherPermission.cs | 45 +++++++++++++ .../Vault/Repositories/ICipherRepository.cs | 10 +++ .../Vault/Repositories/CipherRepository.cs | 14 ++++ ...ionPermissions_GetManyByOrganizationId.sql | 66 ++++++++++++++++++ ..._00_CipherOrganizationPermissionsQuery.sql | 67 +++++++++++++++++++ 5 files changed, 202 insertions(+) create mode 100644 src/Core/Vault/Models/Data/OrganizationCipherPermission.cs create mode 100644 src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql diff --git a/src/Core/Vault/Models/Data/OrganizationCipherPermission.cs b/src/Core/Vault/Models/Data/OrganizationCipherPermission.cs new file mode 100644 index 000000000000..3cca0f8407f3 --- /dev/null +++ b/src/Core/Vault/Models/Data/OrganizationCipherPermission.cs @@ -0,0 +1,45 @@ +namespace Bit.Core.Vault.Models.Data; + +/// +/// Data model that represents a Users permissions for a given cipher +/// that belongs to an organization. +/// To be used internally for authorization. +/// +public class OrganizationCipherPermission +{ + /// + /// The cipher Id + /// + public Guid Id { get; set; } + + /// + /// The organization Id that the cipher belongs to. + /// + public Guid OrganizationId { get; set; } + + /// + /// The user can read the cipher. + /// See for password visibility. + /// + public bool Read { get; set; } + + /// + /// The user has permission to view the password of the cipher. + /// + public bool ViewPassword { get; set; } + + /// + /// The user has permission to edit the cipher. + /// + public bool Edit { get; set; } + + /// + /// The user has manage level access to the cipher. + /// + public bool Manage { get; set; } + + /// + /// The cipher is not assigned to any collection within the organization. + /// + public bool Unassigned { get; set; } +} diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index f3f34c595bf1..2950cb99c266 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -39,6 +39,16 @@ Task CreateAsync(IEnumerable ciphers, IEnumerable collection Task RestoreByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); Task DeleteDeletedAsync(DateTime deletedDateBefore); + /// + /// Low-level query to get all cipher permissions for a user in an organization. DOES NOT consider the user's + /// organization role, any collection management settings on the organization, or special unassigned cipher + /// permissions. + /// + /// Recommended to use instead to handle those cases. + /// + Task> GetCipherPermissionsForOrganizationAsync(Guid organizationId, + Guid userId); + /// /// Updates encrypted data for ciphers during a key rotation /// diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 69b1383f4b04..098e8299e40c 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -309,6 +309,20 @@ public async Task DeleteByOrganizationIdAsync(Guid organizationId) } } + public async Task> GetCipherPermissionsForOrganizationAsync( + Guid organizationId, Guid userId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[CipherOrganizationPermissions_GetManyByOrganizationId]", + new { OrganizationId = organizationId, UserId = userId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + /// public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( Guid userId, IEnumerable ciphers) diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql new file mode 100644 index 000000000000..16d9482408a3 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherOrganizationPermissions_GetManyByOrganizationId.sql @@ -0,0 +1,66 @@ +CREATE PROCEDURE [dbo].[CipherOrganizationPermissions_GetManyByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.[Id], + C.[OrganizationId], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) [Read], + MAX(CASE + WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 1) = 0 + THEN 1 + ELSE 0 + END) [ViewPassword], + MAX(CASE + WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 1) = 0 + THEN 1 + ELSE 0 + END) [Edit], + + MAX(COALESCE(CU.[Manage], CG.[Manage], 0)) [Manage], + CASE + WHEN COUNT(CC.[CollectionId]) > 0 THEN 0 + ELSE 1 + END [Unassigned] + FROM + [dbo].[CipherDetails](@UserId) C + INNER JOIN + [OrganizationUser] OU ON + C.[UserId] IS NULL + AND C.[OrganizationId] = @OrganizationId + AND OU.[UserId] = @UserId + INNER JOIN + [dbo].[Organization] O ON + O.[Id] = OU.[OrganizationId] + AND O.[Id] = C.[OrganizationId] + AND O.[Enabled] = 1 + LEFT JOIN + [dbo].[CollectionCipher] CC ON + CC.[CipherId] = C.[Id] + LEFT JOIN + [dbo].[CollectionUser] CU ON + CU.[CollectionId] = CC.[CollectionId] + 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] = CC.[CollectionId] + AND CG.[GroupId] = GU.[GroupId] + GROUP BY + C.[Id], + C.[OrganizationId] +END diff --git a/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql b/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql new file mode 100644 index 000000000000..2eb4de905338 --- /dev/null +++ b/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql @@ -0,0 +1,67 @@ +CREATE OR ALTER PROCEDURE [dbo].[CipherOrganizationPermissions_GetManyByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.[Id], + C.[OrganizationId], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) [Read], + MAX(CASE + WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 1) = 0 + THEN 1 + ELSE 0 + END) [ViewPassword], + MAX(CASE + WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 1) = 0 + THEN 1 + ELSE 0 + END) [Edit], + + MAX(COALESCE(CU.[Manage], CG.[Manage], 0)) [Manage], + CASE + WHEN COUNT(CC.[CollectionId]) > 0 THEN 0 + ELSE 1 + END [Unassigned] + FROM + [dbo].[CipherDetails](@UserId) C + INNER JOIN + [OrganizationUser] OU ON + C.[UserId] IS NULL + AND C.[OrganizationId] = @OrganizationId + AND OU.[UserId] = @UserId + INNER JOIN + [dbo].[Organization] O ON + O.[Id] = OU.[OrganizationId] + AND O.[Id] = C.[OrganizationId] + AND O.[Enabled] = 1 + LEFT JOIN + [dbo].[CollectionCipher] CC ON + CC.[CipherId] = C.[Id] + LEFT JOIN + [dbo].[CollectionUser] CU ON + CU.[CollectionId] = CC.[CollectionId] + 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] = CC.[CollectionId] + AND CG.[GroupId] = GU.[GroupId] + GROUP BY + C.[Id], + C.[OrganizationId] +END +GO From c85a9302918eb083ed420cc3980c03e5a60a7eae Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 16:56:26 -0800 Subject: [PATCH 02/44] [PM-14378] Introduce GetCipherPermissionsForOrganization method for Entity Framework --- .../Vault/Repositories/CipherRepository.cs | 51 +++++++++++++- .../CipherOrganizationPermissionsQuery.cs | 70 +++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index c12167a78c6e..d389e236f84b 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Runtime.InteropServices; +using System.Text.Json; using System.Text.Json.Nodes; using AutoMapper; using Bit.Core.KeyManagement.UserKey; @@ -302,6 +303,54 @@ public async Task DeleteDeletedAsync(DateTime deletedDateBefore) } } + public async Task> + GetCipherPermissionsForOrganizationAsync(Guid organizationId, Guid userId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new CipherOrganizationPermissionsQuery(organizationId, userId).Run(dbContext); + + ICollection permissions; + + // SQLite does not support the GROUP BY clause + if (dbContext.Database.IsSqlite()) + { + permissions = (await query.ToListAsync()) + .GroupBy(c => new { c.Id, c.OrganizationId }) + .Select(g => new OrganizationCipherPermission + { + Id = g.Key.Id, + OrganizationId = g.Key.OrganizationId, + Read = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Read))), + ViewPassword = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.ViewPassword))), + Edit =Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))), + Manage = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Manage))), + Unassigned = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Unassigned))), + }).ToList(); + } + else + { + var groupByQuery = from p in query + group p by new { p.Id, p.OrganizationId } + into g + select new OrganizationCipherPermission + { + Id = g.Key.Id, + OrganizationId = g.Key.OrganizationId, + Read = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Read))), + ViewPassword = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.ViewPassword))), + Edit =Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))), + Manage = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Manage))), + Unassigned = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Unassigned))), + }; + permissions = await groupByQuery.ToListAsync(); + } + + return permissions; + } + } + public async Task GetByIdAsync(Guid id, Guid userId) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs new file mode 100644 index 000000000000..62e13b4b12af --- /dev/null +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs @@ -0,0 +1,70 @@ +using Bit.Core.Auth.Enums; +using Bit.Core.Enums; +using Bit.Core.Vault.Models.Data; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories.Queries; +using Microsoft.EntityFrameworkCore; +using Org.BouncyCastle.Utilities.IO; +using Sentry.Protocol; +using Stripe.TestHelpers; + +namespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries; + +public class CipherOrganizationPermissionsQuery : IQuery +{ + private readonly Guid _organizationId; + private readonly Guid _userId; + + public CipherOrganizationPermissionsQuery(Guid organizationId, Guid userId) + { + _organizationId = organizationId; + _userId = userId; + } + + public IQueryable Run(DatabaseContext dbContext) + { + return from c in dbContext.Ciphers + + join ou in dbContext.OrganizationUsers + on new { CipherUserId = c.UserId, c.OrganizationId, UserId = (Guid?)_userId } equals + new { CipherUserId = (Guid?)null, OrganizationId = (Guid?)ou.OrganizationId, ou.UserId, } + + join o in dbContext.Organizations + on new { c.OrganizationId, OuOrganizationId = ou.OrganizationId, Enabled = true } equals + new { OrganizationId = (Guid?)o.Id, OuOrganizationId = o.Id, o.Enabled } + + join cc in dbContext.CollectionCiphers + on c.Id equals cc.CipherId into cc_g + from cc in cc_g.DefaultIfEmpty() + + join cu in dbContext.CollectionUsers + on new { cc.CollectionId, OrganizationUserId = ou.Id } equals + new { cu.CollectionId, cu.OrganizationUserId } into cu_g + from cu in cu_g.DefaultIfEmpty() + + join gu in dbContext.GroupUsers + on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals + new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g + from gu in gu_g.DefaultIfEmpty() + + join g in dbContext.Groups + on gu.GroupId equals g.Id into g_g + from g in g_g.DefaultIfEmpty() + + join cg in dbContext.CollectionGroups + on new { cc.CollectionId, gu.GroupId } equals + new { cg.CollectionId, cg.GroupId } into cg_g + from cg in cg_g.DefaultIfEmpty() + + select new OrganizationCipherPermission() + { + Id = c.Id, + OrganizationId = o.Id, + Read = cu != null || cg != null, + ViewPassword = !((bool?)cu.HidePasswords ?? (bool?)cg.HidePasswords ?? true), + Edit = !((bool?)cu.ReadOnly ?? (bool?)cg.ReadOnly ?? true), + Manage = (bool?)cu.Manage ?? (bool?)cg.Manage ?? false, + Unassigned = !dbContext.CollectionCiphers.Any(cc => cc.CipherId == c.Id) + }; + } +} From db5bd641437e6424e15f1c34833f45761b2b25cb Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 16:56:44 -0800 Subject: [PATCH 03/44] [PM-14378] Add integration tests for new repository method --- .../Repositories/CipherRepositoryTests.cs | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index ce9b5ef7ae30..42c8a40f643c 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -8,6 +8,7 @@ using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; using Xunit; namespace Bit.Infrastructure.IntegrationTest.Repositories; @@ -198,4 +199,218 @@ await cipherRepository.ReplaceAsync(new CipherDetails Assert.NotEqual(default, userProperty); Assert.Equal(folder.Id, userProperty.Value.GetGuid()); } + + [DatabaseTheory, DatabaseData] + public async Task GetCipherPermissionsForOrganizationAsync_Works( + ICipherRepository cipherRepository, + IUserRepository userRepository, + ICollectionCipherRepository collectionCipherRepository, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + 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 Organization", + BillingEmail = user.Email, + Plan = "Test" + }); + + var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser + { + UserId = user.Id, + OrganizationId = organization.Id, + Status = OrganizationUserStatusType.Confirmed, + Type = OrganizationUserType.Owner, + }); + + // MANAGE + + var manageCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Manage Collection", + OrganizationId = organization.Id + }); + + var manageCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, OrganizationId = organization.Id, Data = "" + }); + + collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id, + new List { manageCollection.Id }); + + collectionRepository.UpdateUsersAsync(manageCollection.Id, new List + { + new() + { + Id = orgUser.Id, + HidePasswords = false, + ReadOnly = false, + Manage = true + } + }); + + // EDIT + + var editCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Edit Collection", + OrganizationId = organization.Id + }); + + var editCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + collectionCipherRepository.UpdateCollectionsForAdminAsync(editCipher.Id, organization.Id, + new List { editCollection.Id }); + + collectionRepository.UpdateUsersAsync(editCollection.Id, + new List + { + new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false } + }); + + // EDIT EXCEPT PASSWORD + + var editExceptPasswordCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "Edit Except Password Collection", + OrganizationId = organization.Id + }); + + var editExceptPasswordCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + collectionCipherRepository.UpdateCollectionsForAdminAsync(editExceptPasswordCipher.Id, organization.Id, + new List { editExceptPasswordCollection.Id }); + + collectionRepository.UpdateUsersAsync(editExceptPasswordCollection.Id, new List + { + new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false } + }); + + // VIEW ONLY + + var viewOnlyCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "View Only Collection", + OrganizationId = organization.Id + }); + + var viewOnlyCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id, + new List { viewOnlyCollection.Id }); + + collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id, + new List + { + new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false } + }); + + // VIEW EXCEPT PASSWORD + + var viewExceptPasswordCollection = await collectionRepository.CreateAsync(new Collection + { + Name = "View Except Password Collection", + OrganizationId = organization.Id + }); + + var viewExceptPasswordCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + collectionCipherRepository.UpdateCollectionsForAdminAsync(viewExceptPasswordCipher.Id, organization.Id, + new List { viewExceptPasswordCollection.Id }); + + collectionRepository.UpdateUsersAsync(viewExceptPasswordCollection.Id, + new List + { + new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = true, Manage = false } + }); + + // UNASSIGNED + + var unassignedCipher = await cipherRepository.CreateAsync(new Cipher + { + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" + }); + + var permissions = await cipherRepository.GetCipherPermissionsForOrganizationAsync(organization.Id, user.Id); + + Assert.NotEmpty(permissions); + + var manageCipherPermission = permissions.FirstOrDefault(c => c.Id == manageCipher.Id); + Assert.NotNull(manageCipherPermission); + Assert.True(manageCipherPermission.Manage); + Assert.True(manageCipherPermission.Edit); + Assert.True(manageCipherPermission.Read); + Assert.True(manageCipherPermission.ViewPassword); + Assert.False(manageCipherPermission.Unassigned); + + var editCipherPermission = permissions.FirstOrDefault(c => c.Id == editCipher.Id); + Assert.NotNull(editCipherPermission); + Assert.False(editCipherPermission.Manage); + Assert.True(editCipherPermission.Edit); + Assert.True(editCipherPermission.Read); + Assert.True(editCipherPermission.ViewPassword); + Assert.False(editCipherPermission.Unassigned); + + var editExceptPasswordCipherPermission = permissions.FirstOrDefault(c => c.Id == editExceptPasswordCipher.Id); + Assert.NotNull(editExceptPasswordCipherPermission); + Assert.False(editExceptPasswordCipherPermission.Manage); + Assert.True(editExceptPasswordCipherPermission.Edit); + Assert.True(editExceptPasswordCipherPermission.Read); + Assert.False(editExceptPasswordCipherPermission.ViewPassword); + Assert.False(editExceptPasswordCipherPermission.Unassigned); + + var viewOnlyCipherPermission = permissions.FirstOrDefault(c => c.Id == viewOnlyCipher.Id); + Assert.NotNull(viewOnlyCipherPermission); + Assert.False(viewOnlyCipherPermission.Manage); + Assert.False(viewOnlyCipherPermission.Edit); + Assert.True(viewOnlyCipherPermission.Read); + Assert.True(viewOnlyCipherPermission.ViewPassword); + Assert.False(viewOnlyCipherPermission.Unassigned); + + var viewExceptPasswordCipherPermission = permissions.FirstOrDefault(c => c.Id == viewExceptPasswordCipher.Id); + Assert.NotNull(viewExceptPasswordCipherPermission); + Assert.False(viewExceptPasswordCipherPermission.Manage); + Assert.False(viewExceptPasswordCipherPermission.Edit); + Assert.True(viewExceptPasswordCipherPermission.Read); + Assert.False(viewExceptPasswordCipherPermission.ViewPassword); + Assert.False(viewExceptPasswordCipherPermission.Unassigned); + + var unassignedCipherPermission = permissions.FirstOrDefault(c => c.Id == unassignedCipher.Id); + Assert.NotNull(unassignedCipherPermission); + Assert.True(unassignedCipherPermission.Unassigned); + Assert.False(unassignedCipherPermission.Manage); + Assert.False(unassignedCipherPermission.Edit); + Assert.False(unassignedCipherPermission.Read); + Assert.False(unassignedCipherPermission.ViewPassword); + } } From a03ddee961400dfb44e9cba0898744119f14859b Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 16:57:07 -0800 Subject: [PATCH 04/44] [PM-14378] Introduce IGetCipherPermissionsForUserQuery CQRS query --- .../GetCipherPermissionsForUserQuery.cs | 103 ++++++++++++++++++ .../IGetCipherPermissionsForUserQuery.cs | 19 ++++ .../Vault/VaultServiceCollectionExtensions.cs | 1 + 3 files changed, 123 insertions(+) create mode 100644 src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs create mode 100644 src/Core/Vault/Queries/IGetCipherPermissionsForUserQuery.cs diff --git a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs new file mode 100644 index 000000000000..4c715297e9fc --- /dev/null +++ b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs @@ -0,0 +1,103 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; + +namespace Bit.Core.Vault.Queries; + +public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuery +{ + private readonly ICurrentContext _currentContext; + private readonly ICipherRepository _cipherRepository; + private readonly IApplicationCacheService _applicationCacheService; + private readonly IFeatureService _featureService; + + public GetCipherPermissionsForUserQuery(ICurrentContext currentContext, ICipherRepository cipherRepository, IApplicationCacheService applicationCacheService, IFeatureService featureService) + { + _currentContext = currentContext; + _cipherRepository = cipherRepository; + _applicationCacheService = applicationCacheService; + _featureService = featureService; + } + + public async Task> GetByOrganization(Guid organizationId) + { + var org = _currentContext.GetOrganization(organizationId); + var userId = _currentContext.UserId; + + if (org == null || !userId.HasValue) + { + throw new NotFoundException(); + } + + var cipherPermissions = (await _cipherRepository.GetCipherPermissionsForOrganizationAsync(organizationId, userId.Value)).ToList(); + + if (await CanEditAllCiphersAsync(org)) + { + foreach (var cipher in cipherPermissions) + { + cipher.Edit = true; + cipher.Manage = true; + cipher.ViewPassword = true; + } + } + + if (await CanAccessUnassignedCiphersAsync(org)) + { + foreach (var unassignedCipher in cipherPermissions.Where(c => c.Unassigned)) + { + unassignedCipher.Edit = true; + unassignedCipher.Manage = true; + unassignedCipher.ViewPassword = true; + } + } + + return cipherPermissions.ToDictionary(c => c.Id); + } + + private async Task CanEditAllCiphersAsync(CurrentContextOrganization org) + { + // Custom users with EditAnyCollection permissions can always edit all ciphers + if (org is { Type: OrganizationUserType.Custom, Permissions.EditAnyCollection: true }) + { + return true; + } + + var orgAbility = await _applicationCacheService.GetOrganizationAbilityAsync(org.Id); + + // Owners/Admins can only edit all ciphers if the organization has the setting enabled + if (orgAbility is { AllowAdminAccessToAllCollectionItems: true } && org is + { Type: OrganizationUserType.Admin or OrganizationUserType.Owner }) + { + return true; + } + + // Provider users can edit all ciphers if RestrictProviderAccess is disabled + if (await _currentContext.ProviderUserForOrgAsync(org.Id)) + { + return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + } + + return false; + } + + private async Task CanAccessUnassignedCiphersAsync(CurrentContextOrganization org) + { + if (org is + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.EditAnyCollection: true }) + { + return true; + } + + // Provider users can only access all ciphers if RestrictProviderAccess is disabled + if (await _currentContext.ProviderUserForOrgAsync(org.Id)) + { + return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + } + + return false; + } +} diff --git a/src/Core/Vault/Queries/IGetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/IGetCipherPermissionsForUserQuery.cs new file mode 100644 index 000000000000..3ab40f26f0ba --- /dev/null +++ b/src/Core/Vault/Queries/IGetCipherPermissionsForUserQuery.cs @@ -0,0 +1,19 @@ +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Vault.Queries; + +public interface IGetCipherPermissionsForUserQuery +{ + /// + /// Retrieves the permissions of every organization cipher (including unassigned) for the + /// ICurrentContext's user. + /// + /// It considers the Collection Management setting for allowing Admin/Owners access to all ciphers. + /// + /// + /// The primary use case of this query is internal cipher authorization logic. + /// + /// + /// A dictionary of CipherIds and a corresponding OrganizationCipherPermission + public Task> GetByOrganization(Guid organizationId); +} diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 5296f47e3ec4..3fda5a1ad3a8 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -15,5 +15,6 @@ public static IServiceCollection AddVaultServices(this IServiceCollection servic private static void AddVaultQueries(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); } } From 532dd076faa30628c8d1b6536a07ca7e5d5328e7 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 17:01:03 -0800 Subject: [PATCH 05/44] [PM-14378] Introduce SecurityTaskOperationRequirement --- .../SecurityTaskOperationRequirement.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOperationRequirement.cs diff --git a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOperationRequirement.cs b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOperationRequirement.cs new file mode 100644 index 000000000000..4ced1d70b951 --- /dev/null +++ b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOperationRequirement.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Authorization.Infrastructure; + +namespace Bit.Core.Vault.Authorization.SecurityTasks; + +public class SecurityTaskOperationRequirement : OperationAuthorizationRequirement +{ + public SecurityTaskOperationRequirement(string name) + { + Name = name; + } +} + +public static class SecurityTaskOperations +{ + public static readonly SecurityTaskOperationRequirement Read = new SecurityTaskOperationRequirement(nameof(Read)); + public static readonly SecurityTaskOperationRequirement Create = new SecurityTaskOperationRequirement(nameof(Create)); + public static readonly SecurityTaskOperationRequirement Update = new SecurityTaskOperationRequirement(nameof(Update)); + + /// + /// List all security tasks for a specific organization. + /// + /// var orgContext = _currentContext.GetOrganization(organizationId); + /// _authorizationService.AuthorizeOrThrowAsync(User, SecurityTaskOperations.ListAllForOrganization, orgContext); + /// + /// + public static readonly SecurityTaskOperationRequirement ListAllForOrganization = new SecurityTaskOperationRequirement(nameof(ListAllForOrganization)); +} From 5c38328c6491bdb25b567856f94d2cf8f5028b44 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 17:01:24 -0800 Subject: [PATCH 06/44] [PM-14378] Introduce SecurityTaskAuthorizationHandler.cs --- .../SecurityTaskAuthorizationHandler.cs | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs diff --git a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs new file mode 100644 index 000000000000..35fcab2446a3 --- /dev/null +++ b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs @@ -0,0 +1,140 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; +using Microsoft.AspNetCore.Authorization; + + +namespace Bit.Core.Vault.Authorization.SecurityTasks; + +public class SecurityTaskAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery; + + private readonly Dictionary> _cipherPermissionCache = new(); + + public SecurityTaskAuthorizationHandler(ICurrentContext currentContext, IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery) + { + _currentContext = currentContext; + _getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, + SecurityTaskOperationRequirement requirement, + SecurityTask task) + { + if (!_currentContext.UserId.HasValue) + { + return; + } + + var authorized = requirement switch + { + not null when requirement == SecurityTaskOperations.Read => await CanReadAsync(task), + not null when requirement == SecurityTaskOperations.Create => await CanCreateAsync(task), + not null when requirement == SecurityTaskOperations.Update => await CanUpdateAsync(task), + _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, null) + }; + + if (authorized) + { + context.Succeed(requirement); + } + } + + private async Task CanReadAsync(SecurityTask task) + { + var org = _currentContext.GetOrganization(task.OrganizationId); + + if (org == null) + { + // The user does not belong to the organization + return false; + } + + if (task.CipherId.HasValue) + { + return await CanReadCipherForOrgAsync(org, task.CipherId.Value); + } + + return true; + } + + private async Task CanCreateAsync(SecurityTask task) + { + var org = _currentContext.GetOrganization(task.OrganizationId); + + // User must be an Admin/Owner or have custom permissions for reporting + if (org is + not ({ Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or + { Permissions.EditAnyCollection: true } or + { Permissions.AccessReports: true })) + { + return false; + } + + if (task.CipherId.HasValue) + { + return await CipherBelongsToOrgAsync(org, task.CipherId.Value); + } + + return true; + } + + private async Task CanUpdateAsync(SecurityTask task) + { + var org = _currentContext.GetOrganization(task.OrganizationId); + + if (org == null) + { + // The user does not belong to the organization + return false; + } + + if (task.CipherId.HasValue) + { + // Updating a cipher task requires edit access to the cipher + return await CanEditCipherForOrgAsync(org, task.CipherId.Value); + } + + return true; + } + + private async Task CanEditCipherForOrgAsync(CurrentContextOrganization org, Guid cipherId) + { + var ciphers = await GetCipherPermissionsForOrgAsync(org); + + return ciphers.TryGetValue(cipherId, out var cipher) && cipher.Edit; + } + + private async Task CanReadCipherForOrgAsync(CurrentContextOrganization org, Guid cipherId) + { + var ciphers = await GetCipherPermissionsForOrgAsync(org); + + return ciphers.TryGetValue(cipherId, out var cipher) && cipher.Read; + } + + private async Task CipherBelongsToOrgAsync(CurrentContextOrganization org, Guid cipherId) + { + var ciphers = await GetCipherPermissionsForOrgAsync(org); + + return ciphers.ContainsKey(cipherId); + } + + private async Task> GetCipherPermissionsForOrgAsync(CurrentContextOrganization organization) + { + // Re-use permissions we've already fetched for the organization + if (_cipherPermissionCache.TryGetValue(organization.Id, out var cachedCiphers)) + { + return cachedCiphers; + } + + var cipherPermissions = await _getCipherPermissionsForUserQuery.GetByOrganization(organization.Id); + + _cipherPermissionCache.Add(organization.Id, cipherPermissions); + + return cipherPermissions; + } +} From 19a1814d9fe1b5dc384cdd4a5f30fdf3e576ada8 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 17:01:40 -0800 Subject: [PATCH 07/44] [PM-14378] Introduce SecurityTaskOrganizationAuthorizationHandler.cs --- ...ityTaskOrganizationAuthorizationHandler.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs diff --git a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs new file mode 100644 index 000000000000..bf1beee8d2cc --- /dev/null +++ b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs @@ -0,0 +1,47 @@ +using Bit.Core.Context; +using Bit.Core.Enums; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.Vault.Authorization.SecurityTasks; + +public class + SecurityTaskOrganizationAuthorizationHandler : AuthorizationHandler +{ + private readonly ICurrentContext _currentContext; + + public SecurityTaskOrganizationAuthorizationHandler(ICurrentContext currentContext) + { + _currentContext = currentContext; + } + + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + SecurityTaskOperationRequirement requirement, + CurrentContextOrganization resource) + { + if (!_currentContext.UserId.HasValue) + { + return Task.CompletedTask; + } + + var authorized = requirement switch + { + not null when requirement == SecurityTaskOperations.ListAllForOrganization => CanListAllTasksForOrganization(resource), + _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, null) + }; + + if (authorized) + { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + + private static bool CanListAllTasksForOrganization(CurrentContextOrganization org) + { + return org is + { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or + { Permissions.AccessReports: true }; + } +} From df3e424aaf9fecb8467e5ab833062a53141b1319 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 17:01:53 -0800 Subject: [PATCH 08/44] [PM-14378] Register new authorization handlers --- src/Api/Utilities/ServiceCollectionExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index 3d206fd88786..69de110f5b07 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Bit.Core.IdentityServer; using Bit.Core.Settings; using Bit.Core.Utilities; +using Bit.Core.Vault.Authorization.SecurityTasks; using Bit.SharedWeb.Health; using Bit.SharedWeb.Swagger; using Microsoft.AspNetCore.Authorization; @@ -101,5 +102,7 @@ public static void AddAuthorizationHandlers(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } From 78ea8b5480c3680e48289129e74c851bf7d3bc57 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 17:09:51 -0800 Subject: [PATCH 09/44] [PM-14378] Formatting --- ...ityTaskOrganizationAuthorizationHandler.cs | 4 +- .../GetCipherPermissionsForUserQuery.cs | 8 +- .../Vault/Repositories/CipherRepository.cs | 27 ++++--- .../CipherOrganizationPermissionsQuery.cs | 76 +++++++++---------- .../Repositories/CipherRepositoryTests.cs | 10 ++- 5 files changed, 61 insertions(+), 64 deletions(-) diff --git a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs index bf1beee8d2cc..67b3496b941e 100644 --- a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs +++ b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs @@ -41,7 +41,7 @@ protected override Task HandleRequirementAsync(AuthorizationHandlerContext conte private static bool CanListAllTasksForOrganization(CurrentContextOrganization org) { return org is - { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or - { Permissions.AccessReports: true }; + { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or + { Permissions.AccessReports: true }; } } diff --git a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs index 4c715297e9fc..4b9a4b179bc5 100644 --- a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs +++ b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs @@ -12,7 +12,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer private readonly ICurrentContext _currentContext; private readonly ICipherRepository _cipherRepository; private readonly IApplicationCacheService _applicationCacheService; - private readonly IFeatureService _featureService; + private readonly IFeatureService _featureService; public GetCipherPermissionsForUserQuery(ICurrentContext currentContext, ICipherRepository cipherRepository, IApplicationCacheService applicationCacheService, IFeatureService featureService) { @@ -69,7 +69,7 @@ private async Task CanEditAllCiphersAsync(CurrentContextOrganization org) // Owners/Admins can only edit all ciphers if the organization has the setting enabled if (orgAbility is { AllowAdminAccessToAllCollectionItems: true } && org is - { Type: OrganizationUserType.Admin or OrganizationUserType.Owner }) + { Type: OrganizationUserType.Admin or OrganizationUserType.Owner }) { return true; } @@ -86,8 +86,8 @@ private async Task CanEditAllCiphersAsync(CurrentContextOrganization org) private async Task CanAccessUnassignedCiphersAsync(CurrentContextOrganization org) { if (org is - { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or - { Permissions.EditAnyCollection: true }) + { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or + { Permissions.EditAnyCollection: true }) { return true; } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index d389e236f84b..5fa308698097 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -1,5 +1,4 @@ -using System.Runtime.InteropServices; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Nodes; using AutoMapper; using Bit.Core.KeyManagement.UserKey; @@ -324,7 +323,7 @@ public async Task> OrganizationId = g.Key.OrganizationId, Read = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Read))), ViewPassword = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.ViewPassword))), - Edit =Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))), + Edit = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))), Manage = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Manage))), Unassigned = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Unassigned))), }).ToList(); @@ -332,18 +331,18 @@ public async Task> else { var groupByQuery = from p in query - group p by new { p.Id, p.OrganizationId } + group p by new { p.Id, p.OrganizationId } into g - select new OrganizationCipherPermission - { - Id = g.Key.Id, - OrganizationId = g.Key.OrganizationId, - Read = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Read))), - ViewPassword = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.ViewPassword))), - Edit =Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))), - Manage = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Manage))), - Unassigned = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Unassigned))), - }; + select new OrganizationCipherPermission + { + Id = g.Key.Id, + OrganizationId = g.Key.OrganizationId, + Read = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Read))), + ViewPassword = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.ViewPassword))), + Edit = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Edit))), + Manage = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Manage))), + Unassigned = Convert.ToBoolean(g.Max(c => Convert.ToInt32(c.Unassigned))), + }; permissions = await groupByQuery.ToListAsync(); } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs index 62e13b4b12af..b05bb0bd579c 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs @@ -1,12 +1,6 @@ -using Bit.Core.Auth.Enums; -using Bit.Core.Enums; -using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Models.Data; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; -using Microsoft.EntityFrameworkCore; -using Org.BouncyCastle.Utilities.IO; -using Sentry.Protocol; -using Stripe.TestHelpers; namespace Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries; @@ -25,46 +19,46 @@ public IQueryable Run(DatabaseContext dbContext) { return from c in dbContext.Ciphers - join ou in dbContext.OrganizationUsers - on new { CipherUserId = c.UserId, c.OrganizationId, UserId = (Guid?)_userId } equals - new { CipherUserId = (Guid?)null, OrganizationId = (Guid?)ou.OrganizationId, ou.UserId, } + join ou in dbContext.OrganizationUsers + on new { CipherUserId = c.UserId, c.OrganizationId, UserId = (Guid?)_userId } equals + new { CipherUserId = (Guid?)null, OrganizationId = (Guid?)ou.OrganizationId, ou.UserId, } - join o in dbContext.Organizations - on new { c.OrganizationId, OuOrganizationId = ou.OrganizationId, Enabled = true } equals - new { OrganizationId = (Guid?)o.Id, OuOrganizationId = o.Id, o.Enabled } + join o in dbContext.Organizations + on new { c.OrganizationId, OuOrganizationId = ou.OrganizationId, Enabled = true } equals + new { OrganizationId = (Guid?)o.Id, OuOrganizationId = o.Id, o.Enabled } - join cc in dbContext.CollectionCiphers - on c.Id equals cc.CipherId into cc_g - from cc in cc_g.DefaultIfEmpty() + join cc in dbContext.CollectionCiphers + on c.Id equals cc.CipherId into cc_g + from cc in cc_g.DefaultIfEmpty() - join cu in dbContext.CollectionUsers - on new { cc.CollectionId, OrganizationUserId = ou.Id } equals - new { cu.CollectionId, cu.OrganizationUserId } into cu_g - from cu in cu_g.DefaultIfEmpty() + join cu in dbContext.CollectionUsers + on new { cc.CollectionId, OrganizationUserId = ou.Id } equals + new { cu.CollectionId, cu.OrganizationUserId } into cu_g + from cu in cu_g.DefaultIfEmpty() - join gu in dbContext.GroupUsers - on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals - new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g - from gu in gu_g.DefaultIfEmpty() + join gu in dbContext.GroupUsers + on new { CollectionId = (Guid?)cu.CollectionId, OrganizationUserId = ou.Id } equals + new { CollectionId = (Guid?)null, gu.OrganizationUserId } into gu_g + from gu in gu_g.DefaultIfEmpty() - join g in dbContext.Groups - on gu.GroupId equals g.Id into g_g - from g in g_g.DefaultIfEmpty() + join g in dbContext.Groups + on gu.GroupId equals g.Id into g_g + from g in g_g.DefaultIfEmpty() - join cg in dbContext.CollectionGroups - on new { cc.CollectionId, gu.GroupId } equals - new { cg.CollectionId, cg.GroupId } into cg_g - from cg in cg_g.DefaultIfEmpty() + join cg in dbContext.CollectionGroups + on new { cc.CollectionId, gu.GroupId } equals + new { cg.CollectionId, cg.GroupId } into cg_g + from cg in cg_g.DefaultIfEmpty() - select new OrganizationCipherPermission() - { - Id = c.Id, - OrganizationId = o.Id, - Read = cu != null || cg != null, - ViewPassword = !((bool?)cu.HidePasswords ?? (bool?)cg.HidePasswords ?? true), - Edit = !((bool?)cu.ReadOnly ?? (bool?)cg.ReadOnly ?? true), - Manage = (bool?)cu.Manage ?? (bool?)cg.Manage ?? false, - Unassigned = !dbContext.CollectionCiphers.Any(cc => cc.CipherId == c.Id) - }; + select new OrganizationCipherPermission() + { + Id = c.Id, + OrganizationId = o.Id, + Read = cu != null || cg != null, + ViewPassword = !((bool?)cu.HidePasswords ?? (bool?)cg.HidePasswords ?? true), + Edit = !((bool?)cu.ReadOnly ?? (bool?)cg.ReadOnly ?? true), + Manage = (bool?)cu.Manage ?? (bool?)cg.Manage ?? false, + Unassigned = !dbContext.CollectionCiphers.Any(cc => cc.CipherId == c.Id) + }; } } diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index 42c8a40f643c..aef4fa50b729 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; -using Bit.Infrastructure.EntityFramework.Repositories; using Xunit; namespace Bit.Infrastructure.IntegrationTest.Repositories; @@ -212,7 +211,10 @@ public async Task GetCipherPermissionsForOrganizationAsync_Works( var user = await userRepository.CreateAsync(new User { - Name = "Test User", Email = $"test+{Guid.NewGuid()}@email.com", ApiKey = "TEST", SecurityStamp = "stamp", + Name = "Test User", + Email = $"test+{Guid.NewGuid()}@email.com", + ApiKey = "TEST", + SecurityStamp = "stamp", }); var organization = await organizationRepository.CreateAsync(new Organization @@ -240,7 +242,9 @@ public async Task GetCipherPermissionsForOrganizationAsync_Works( var manageCipher = await cipherRepository.CreateAsync(new Cipher { - Type = CipherType.Login, OrganizationId = organization.Id, Data = "" + Type = CipherType.Login, + OrganizationId = organization.Id, + Data = "" }); collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id, From 4d8023828ba8c0ceb481221f102bf0623166feb6 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 11 Dec 2024 15:22:48 -0800 Subject: [PATCH 10/44] [PM-14378] Add unit tests for GetCipherPermissionsForUserQuery --- .../GetCipherPermissionsForUserQuery.cs | 22 +- .../GetCipherPermissionsForUserQueryTests.cs | 224 ++++++++++++++++++ 2 files changed, 228 insertions(+), 18 deletions(-) create mode 100644 test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs diff --git a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs index 4b9a4b179bc5..650c7dd65559 100644 --- a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs +++ b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs @@ -12,14 +12,12 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer private readonly ICurrentContext _currentContext; private readonly ICipherRepository _cipherRepository; private readonly IApplicationCacheService _applicationCacheService; - private readonly IFeatureService _featureService; - public GetCipherPermissionsForUserQuery(ICurrentContext currentContext, ICipherRepository cipherRepository, IApplicationCacheService applicationCacheService, IFeatureService featureService) + public GetCipherPermissionsForUserQuery(ICurrentContext currentContext, ICipherRepository cipherRepository, IApplicationCacheService applicationCacheService) { _currentContext = currentContext; _cipherRepository = cipherRepository; _applicationCacheService = applicationCacheService; - _featureService = featureService; } public async Task> GetByOrganization(Guid organizationId) @@ -38,16 +36,16 @@ public async Task> GetByOrganiza { foreach (var cipher in cipherPermissions) { + cipher.Read = true; cipher.Edit = true; cipher.Manage = true; cipher.ViewPassword = true; } - } - - if (await CanAccessUnassignedCiphersAsync(org)) + } else if (await CanAccessUnassignedCiphersAsync(org)) { foreach (var unassignedCipher in cipherPermissions.Where(c => c.Unassigned)) { + unassignedCipher.Read = true; unassignedCipher.Edit = true; unassignedCipher.Manage = true; unassignedCipher.ViewPassword = true; @@ -74,12 +72,6 @@ private async Task CanEditAllCiphersAsync(CurrentContextOrganization org) return true; } - // Provider users can edit all ciphers if RestrictProviderAccess is disabled - if (await _currentContext.ProviderUserForOrgAsync(org.Id)) - { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); - } - return false; } @@ -92,12 +84,6 @@ private async Task CanAccessUnassignedCiphersAsync(CurrentContextOrganizat return true; } - // Provider users can only access all ciphers if RestrictProviderAccess is disabled - if (await _currentContext.ProviderUserForOrgAsync(org.Id)) - { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); - } - return false; } } diff --git a/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs new file mode 100644 index 000000000000..e681328a65da --- /dev/null +++ b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs @@ -0,0 +1,224 @@ +using AutoFixture; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Services; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Queries; + +[SutProviderCustomize] +public class GetCipherPermissionsForUserQueryTests +{ + private static Guid _noAccessCipherId = Guid.NewGuid(); + private static Guid _readOnlyCipherId = Guid.NewGuid(); + private static Guid _editCipherId = Guid.NewGuid(); + private static Guid _manageCipherId = Guid.NewGuid(); + private static Guid _readExceptPasswordCipherId = Guid.NewGuid(); + private static Guid _unassignedCipherId = Guid.NewGuid(); + + private static List _cipherIds = new [] + { + _noAccessCipherId, + _readOnlyCipherId, + _editCipherId, + _manageCipherId, + _readExceptPasswordCipherId, + _unassignedCipherId + }.ToList(); + + + [Theory, BitAutoData] + public async Task GetCipherPermissionsForUserQuery_Base(Guid userId, CurrentContextOrganization org, SutProvider sutProvider + ) + { + var organizationId = org.Id; + org.Type = OrganizationUserType.User; + org.Permissions.EditAnyCollection = false; + var cipherPermissions = CreateCipherPermissions(); + + sutProvider.GetDependency().GetOrganization(organizationId).Returns(org); + sutProvider.GetDependency().UserId.Returns(userId); + + sutProvider.GetDependency().GetCipherPermissionsForOrganizationAsync(organizationId, userId) + .Returns(cipherPermissions); + + var result = await sutProvider.Sut.GetByOrganization(organizationId); + + Assert.Equal(6, result.Count); + Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); + Assert.Equal(false, result[_noAccessCipherId].Read); + Assert.Equal(true, result[_readOnlyCipherId].Read); + Assert.Equal(false, result[_readOnlyCipherId].Edit); + Assert.Equal(true, result[_editCipherId].Edit); + Assert.Equal(true, result[_manageCipherId].Manage); + Assert.Equal(true, result[_readExceptPasswordCipherId].Read); + Assert.Equal(false, result[_readExceptPasswordCipherId].ViewPassword); + Assert.Equal(true, result[_unassignedCipherId].Unassigned); + Assert.Equal(false, result[_unassignedCipherId].Read); + } + + [Theory, BitAutoData] + public async Task GetCipherPermissionsForUserQuery_CanEditAllCiphers_CustomUser(Guid userId, CurrentContextOrganization org, SutProvider sutProvider + ) + { + var organizationId = org.Id; + var cipherPermissions = CreateCipherPermissions(); + org.Permissions.EditAnyCollection = true; + org.Type = OrganizationUserType.Custom; + + sutProvider.GetDependency().GetOrganization(organizationId).Returns(org); + sutProvider.GetDependency().UserId.Returns(userId); + + sutProvider.GetDependency().GetCipherPermissionsForOrganizationAsync(organizationId, userId) + .Returns(cipherPermissions); + + var result = await sutProvider.Sut.GetByOrganization(organizationId); + + Assert.Equal(6, result.Count); + Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); + Assert.All(result, x => Assert.True(x.Value.Read && x.Value.Edit && x.Value.Manage && x.Value.ViewPassword)); + } + + [Theory, BitAutoData] + public async Task GetCipherPermissionsForUserQuery_CanEditAllCiphers_Admin(Guid userId, CurrentContextOrganization org, SutProvider sutProvider + ) + { + var organizationId = org.Id; + var cipherPermissions = CreateCipherPermissions(); + org.Permissions.EditAnyCollection = false; + org.Type = OrganizationUserType.Admin; + + sutProvider.GetDependency().GetOrganization(organizationId).Returns(org); + sutProvider.GetDependency().UserId.Returns(userId); + + sutProvider.GetDependency().GetOrganizationAbilityAsync(org.Id).Returns(new OrganizationAbility + { + AllowAdminAccessToAllCollectionItems = true + }); + + sutProvider.GetDependency().GetCipherPermissionsForOrganizationAsync(organizationId, userId) + .Returns(cipherPermissions); + + var result = await sutProvider.Sut.GetByOrganization(organizationId); + + Assert.Equal(6, result.Count); + Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); + Assert.All(result, x => Assert.True(x.Value.Read && x.Value.Edit && x.Value.Manage && x.Value.ViewPassword)); + } + + [Theory, BitAutoData] + public async Task GetCipherPermissionsForUserQuery_CanEditUnassignedCiphers(Guid userId, CurrentContextOrganization org, SutProvider sutProvider + ) + { + var organizationId = org.Id; + var cipherPermissions = CreateCipherPermissions(); + org.Type = OrganizationUserType.Owner; + org.Permissions.EditAnyCollection = false; + + sutProvider.GetDependency().GetOrganization(organizationId).Returns(org); + sutProvider.GetDependency().UserId.Returns(userId); + + sutProvider.GetDependency().GetCipherPermissionsForOrganizationAsync(organizationId, userId) + .Returns(cipherPermissions); + + var result = await sutProvider.Sut.GetByOrganization(organizationId); + + Assert.Equal(6, result.Count); + Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); + Assert.Equal(false, result[_noAccessCipherId].Read); + Assert.Equal(true, result[_readOnlyCipherId].Read); + Assert.Equal(false, result[_readOnlyCipherId].Edit); + Assert.Equal(true, result[_editCipherId].Edit); + Assert.Equal(true, result[_manageCipherId].Manage); + Assert.Equal(true, result[_readExceptPasswordCipherId].Read); + Assert.Equal(false, result[_readExceptPasswordCipherId].ViewPassword); + + Assert.Equal(true, result[_unassignedCipherId].Unassigned); + Assert.Equal(true, result[_unassignedCipherId].Read); + Assert.Equal(true, result[_unassignedCipherId].Edit); + Assert.Equal(true, result[_unassignedCipherId].ViewPassword); + Assert.Equal(true, result[_unassignedCipherId].Manage); + } + + private List CreateCipherPermissions() + { + // User has no relationship with the cipher + var noAccessCipher = new OrganizationCipherPermission + { + Id = _noAccessCipherId, + Read = false, + Edit = false, + Manage = false, + ViewPassword = false, + Unassigned = false + }; + + var readOnlyCipher = new OrganizationCipherPermission + { + Id = _readOnlyCipherId, + Read = true, + Edit = false, + Manage = false, + ViewPassword = true, + Unassigned = false + }; + + var editCipher = new OrganizationCipherPermission + { + Id = _editCipherId, + Read = true, + Edit = true, + Manage = false, + ViewPassword = true, + Unassigned = false + }; + + var manageCipher = new OrganizationCipherPermission + { + Id = _manageCipherId, + Read = true, + Edit = true, + Manage = true, + ViewPassword = true, + Unassigned = false + }; + + var readExceptPasswordCipher = new OrganizationCipherPermission + { + Id = _readExceptPasswordCipherId, + Read = true, + Edit = false, + Manage = false, + ViewPassword = false, + Unassigned = false + }; + + var unassignedCipher = new OrganizationCipherPermission + { + Id = _unassignedCipherId, + Read = false, + Edit = false, + Manage = false, + ViewPassword = false, + Unassigned = true + }; + + return new List + { + noAccessCipher, + readOnlyCipher, + editCipher, + manageCipher, + readExceptPasswordCipher, + unassignedCipher + }; + } +} From b40d1443c74d945b5ce3cea9100a4dee367edf54 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 11 Dec 2024 16:23:37 -0800 Subject: [PATCH 11/44] [PM-15378] Cleanup SecurityTaskAuthorizationHandler and add tests --- .../SecurityTaskAuthorizationHandler.cs | 72 +-- .../SecurityTaskAuthorizationHandlerTests.cs | 430 ++++++++++++++++++ 2 files changed, 467 insertions(+), 35 deletions(-) create mode 100644 test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs diff --git a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs index 35fcab2446a3..626174bfa271 100644 --- a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs +++ b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs @@ -5,7 +5,6 @@ using Bit.Core.Vault.Queries; using Microsoft.AspNetCore.Authorization; - namespace Bit.Core.Vault.Authorization.SecurityTasks; public class SecurityTaskAuthorizationHandler : AuthorizationHandler @@ -30,11 +29,19 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext return; } + var org = _currentContext.GetOrganization(task.OrganizationId); + + if (org == null) + { + // User must be a member of the organization + return; + } + var authorized = requirement switch { - not null when requirement == SecurityTaskOperations.Read => await CanReadAsync(task), - not null when requirement == SecurityTaskOperations.Create => await CanCreateAsync(task), - not null when requirement == SecurityTaskOperations.Update => await CanUpdateAsync(task), + not null when requirement == SecurityTaskOperations.Read => await CanReadAsync(task, org), + not null when requirement == SecurityTaskOperations.Create => await CanCreateAsync(task, org), + not null when requirement == SecurityTaskOperations.Update => await CanUpdateAsync(task, org), _ => throw new ArgumentOutOfRangeException(nameof(requirement), requirement, null) }; @@ -44,62 +51,50 @@ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext } } - private async Task CanReadAsync(SecurityTask task) + private async Task CanReadAsync(SecurityTask task, CurrentContextOrganization org) { - var org = _currentContext.GetOrganization(task.OrganizationId); - - if (org == null) + if (!task.CipherId.HasValue) { - // The user does not belong to the organization + // Tasks without cipher IDs are not possible currently return false; } - if (task.CipherId.HasValue) + if (HasAdminAccessToSecurityTasks(org)) { - return await CanReadCipherForOrgAsync(org, task.CipherId.Value); + // Admins can read any task for ciphers in the organization + return await CipherBelongsToOrgAsync(org, task.CipherId.Value); } - return true; + return await CanReadCipherForOrgAsync(org, task.CipherId.Value); } - private async Task CanCreateAsync(SecurityTask task) + private async Task CanCreateAsync(SecurityTask task, CurrentContextOrganization org) { - var org = _currentContext.GetOrganization(task.OrganizationId); - - // User must be an Admin/Owner or have custom permissions for reporting - if (org is - not ({ Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or - { Permissions.EditAnyCollection: true } or - { Permissions.AccessReports: true })) + if (!task.CipherId.HasValue) { + // Tasks without cipher IDs are not possible currently return false; } - if (task.CipherId.HasValue) + if (!HasAdminAccessToSecurityTasks(org)) { - return await CipherBelongsToOrgAsync(org, task.CipherId.Value); + // User must be an Admin/Owner or have custom permissions for reporting + return false; } - return true; + return await CipherBelongsToOrgAsync(org, task.CipherId.Value); } - private async Task CanUpdateAsync(SecurityTask task) + private async Task CanUpdateAsync(SecurityTask task, CurrentContextOrganization org) { - var org = _currentContext.GetOrganization(task.OrganizationId); - - if (org == null) + if (!task.CipherId.HasValue) { - // The user does not belong to the organization + // Tasks without cipher IDs are not possible currently return false; } - if (task.CipherId.HasValue) - { - // Updating a cipher task requires edit access to the cipher - return await CanEditCipherForOrgAsync(org, task.CipherId.Value); - } - - return true; + // Only users that can edit the cipher can update the task + return await CanEditCipherForOrgAsync(org, task.CipherId.Value); } private async Task CanEditCipherForOrgAsync(CurrentContextOrganization org, Guid cipherId) @@ -123,6 +118,13 @@ private async Task CipherBelongsToOrgAsync(CurrentContextOrganization org, return ciphers.ContainsKey(cipherId); } + private bool HasAdminAccessToSecurityTasks(CurrentContextOrganization org) + { + return org is + { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or + { Type: OrganizationUserType.Custom, Permissions.AccessReports: true }; + } + private async Task> GetCipherPermissionsForOrgAsync(CurrentContextOrganization organization) { // Re-use permissions we've already fetched for the organization diff --git a/test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs b/test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs new file mode 100644 index 000000000000..e4566347761d --- /dev/null +++ b/test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs @@ -0,0 +1,430 @@ +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Authorization; + +[SutProviderCustomize] +public class SecurityTaskAuthorizationHandlerTests +{ + [Theory, CurrentContextOrganizationCustomize, BitAutoData] + public async Task MissingOrg_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns((CurrentContextOrganization)null); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Read }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize, BitAutoData] + public async Task MissingCipherId_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var operations = new[] + { + SecurityTaskOperations.Read, SecurityTaskOperations.Create, SecurityTaskOperations.Update + }; + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = null + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + foreach (var operation in operations) + { + var context = new AuthorizationHandlerContext( + new[] { operation }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded, operation.ToString()); + } + + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] + public async Task Read_User_CanReadCipher_Success( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + Read = true + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Read }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData] + public async Task Read_Admin_Success( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Read }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData] + public async Task Read_Admin_MissingCipher_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary()); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Read }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] + public async Task Read_User_CannotReadCipher_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + Read = false + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Read }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] + public async Task Create_User_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + Read = true, + Edit = true, + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Create }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData] + public async Task Create_Admin_MissingCipher_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary()); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Create }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData] + public async Task Create_Admin_Success( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + Read = true, + Edit = true, + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Create }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] + public async Task Update_User_CanEditCipher_Success( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + Read = true, + Edit = true + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Update }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData] + public async Task Update_Admin_CanEditCipher_Success( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + Edit = true + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Update }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.Admin), BitAutoData] + public async Task Read_Admin_ReadonlyCipher_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary()); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Update }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] + public async Task Update_User_CannotEditCipher_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + var task = new SecurityTask + { + OrganizationId = organization.Id, + CipherId = Guid.NewGuid() + }; + var cipherPermissions = new OrganizationCipherPermission + { + Id = task.CipherId.Value, + Read = true, + Edit = false + }; + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + sutProvider.GetDependency().GetByOrganization(organization.Id).Returns(new Dictionary + { + { task.CipherId.Value, cipherPermissions } + }); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.Update }, + new ClaimsPrincipal(), + task); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } +} From ca15550db160341ea4fdae9c6993c390adbd785f Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 11 Dec 2024 16:42:42 -0800 Subject: [PATCH 12/44] [PM-14378] Add tests for SecurityTaskOrganizationAuthorizationHandler --- ...ityTaskOrganizationAuthorizationHandler.cs | 2 +- ...skOrganizationAuthorizationHandlerTests.cs | 105 ++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs diff --git a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs index 67b3496b941e..ec3800dc9437 100644 --- a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs +++ b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskOrganizationAuthorizationHandler.cs @@ -42,6 +42,6 @@ private static bool CanListAllTasksForOrganization(CurrentContextOrganization or { return org is { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or - { Permissions.AccessReports: true }; + { Type: OrganizationUserType.Custom, Permissions.AccessReports: true }; } } diff --git a/test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs b/test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs new file mode 100644 index 000000000000..c5a6ffcc1df5 --- /dev/null +++ b/test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs @@ -0,0 +1,105 @@ +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Test.AdminConsole.AutoFixture; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Entities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Authorization; + +[SutProviderCustomize] +public class SecurityTaskOrganizationAuthorizationHandlerTests +{ + [Theory, CurrentContextOrganizationCustomize, BitAutoData] + public async Task MissingOrg_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns((CurrentContextOrganization)null); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.ListAllForOrganization }, + new ClaimsPrincipal(), + organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize, BitAutoData] + public async Task MissingUserId_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + + sutProvider.GetDependency().UserId.Returns(null as Guid?); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.ListAllForOrganization }, + new ClaimsPrincipal(), + organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize] + [BitAutoData(OrganizationUserType.Owner)] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task ListAllForOrganization_Admin_Success( + OrganizationUserType userType, + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + organization.Type = userType; + if (organization.Type == OrganizationUserType.Custom) + { + organization.Permissions.AccessReports = true; + } + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.ListAllForOrganization }, + new ClaimsPrincipal(), + organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.True(context.HasSucceeded); + } + + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] + public async Task ListAllForOrganization_User_Failure( + CurrentContextOrganization organization, + SutProvider sutProvider) + { + var userId = Guid.NewGuid(); + + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(organization.Id).Returns(organization); + + var context = new AuthorizationHandlerContext( + new[] { SecurityTaskOperations.ListAllForOrganization }, + new ClaimsPrincipal(), + organization); + + await sutProvider.Sut.HandleAsync(context); + + Assert.False(context.HasSucceeded); + } + +} From bcf321064c316f982aaa4ae862156aee52b55e6e Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 11 Dec 2024 16:44:57 -0800 Subject: [PATCH 13/44] [PM-14378] Formatting --- .../SecurityTaskAuthorizationHandler.cs | 4 +- .../GetCipherPermissionsForUserQuery.cs | 3 +- .../SecurityTaskAuthorizationHandlerTests.cs | 6 +- ...skOrganizationAuthorizationHandlerTests.cs | 1 - .../GetCipherPermissionsForUserQueryTests.cs | 58 +++++++++---------- 5 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs index 626174bfa271..eedae9908341 100644 --- a/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs +++ b/src/Core/Vault/Authorization/SecurityTasks/SecurityTaskAuthorizationHandler.cs @@ -121,8 +121,8 @@ private async Task CipherBelongsToOrgAsync(CurrentContextOrganization org, private bool HasAdminAccessToSecurityTasks(CurrentContextOrganization org) { return org is - { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or - { Type: OrganizationUserType.Custom, Permissions.AccessReports: true }; + { Type: OrganizationUserType.Admin or OrganizationUserType.Owner } or + { Type: OrganizationUserType.Custom, Permissions.AccessReports: true }; } private async Task> GetCipherPermissionsForOrgAsync(CurrentContextOrganization organization) diff --git a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs index 650c7dd65559..df336776eab3 100644 --- a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs +++ b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs @@ -41,7 +41,8 @@ public async Task> GetByOrganiza cipher.Manage = true; cipher.ViewPassword = true; } - } else if (await CanAccessUnassignedCiphersAsync(org)) + } + else if (await CanAccessUnassignedCiphersAsync(org)) { foreach (var unassignedCipher in cipherPermissions.Where(c => c.Unassigned)) { diff --git a/test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs b/test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs index e4566347761d..43bdceac9898 100644 --- a/test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs +++ b/test/Core.Test/Vault/Authorization/SecurityTaskAuthorizationHandlerTests.cs @@ -298,10 +298,10 @@ public async Task Create_Admin_Success( Assert.True(context.HasSucceeded); } - [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] + [Theory, CurrentContextOrganizationCustomize(Type = OrganizationUserType.User), BitAutoData] public async Task Update_User_CanEditCipher_Success( - CurrentContextOrganization organization, - SutProvider sutProvider) + CurrentContextOrganization organization, + SutProvider sutProvider) { var userId = Guid.NewGuid(); var task = new SecurityTask diff --git a/test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs b/test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs index c5a6ffcc1df5..d0b2ecbcf051 100644 --- a/test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs +++ b/test/Core.Test/Vault/Authorization/SecurityTaskOrganizationAuthorizationHandlerTests.cs @@ -3,7 +3,6 @@ using Bit.Core.Enums; using Bit.Core.Test.AdminConsole.AutoFixture; using Bit.Core.Vault.Authorization.SecurityTasks; -using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Authorization; diff --git a/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs index e681328a65da..af92f50ed056 100644 --- a/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs +++ b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs @@ -1,6 +1,4 @@ -using AutoFixture; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Services; @@ -24,7 +22,7 @@ public class GetCipherPermissionsForUserQueryTests private static Guid _readExceptPasswordCipherId = Guid.NewGuid(); private static Guid _unassignedCipherId = Guid.NewGuid(); - private static List _cipherIds = new [] + private static List _cipherIds = new[] { _noAccessCipherId, _readOnlyCipherId, @@ -53,16 +51,16 @@ public async Task GetCipherPermissionsForUserQuery_Base(Guid userId, CurrentCont var result = await sutProvider.Sut.GetByOrganization(organizationId); Assert.Equal(6, result.Count); - Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); - Assert.Equal(false, result[_noAccessCipherId].Read); - Assert.Equal(true, result[_readOnlyCipherId].Read); - Assert.Equal(false, result[_readOnlyCipherId].Edit); - Assert.Equal(true, result[_editCipherId].Edit); - Assert.Equal(true, result[_manageCipherId].Manage); - Assert.Equal(true, result[_readExceptPasswordCipherId].Read); - Assert.Equal(false, result[_readExceptPasswordCipherId].ViewPassword); - Assert.Equal(true, result[_unassignedCipherId].Unassigned); - Assert.Equal(false, result[_unassignedCipherId].Read); + Assert.All(result, x => Assert.Contains(x.Key, _cipherIds)); + Assert.False(result[_noAccessCipherId].Read); + Assert.True(result[_readOnlyCipherId].Read); + Assert.False(result[_readOnlyCipherId].Edit); + Assert.True(result[_editCipherId].Edit); + Assert.True(result[_manageCipherId].Manage); + Assert.True(result[_readExceptPasswordCipherId].Read); + Assert.False(result[_readExceptPasswordCipherId].ViewPassword); + Assert.True(result[_unassignedCipherId].Unassigned); + Assert.False(result[_unassignedCipherId].Read); } [Theory, BitAutoData] @@ -83,7 +81,7 @@ public async Task GetCipherPermissionsForUserQuery_CanEditAllCiphers_CustomUser( var result = await sutProvider.Sut.GetByOrganization(organizationId); Assert.Equal(6, result.Count); - Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); + Assert.All(result, x => Assert.Contains(x.Key, _cipherIds)); Assert.All(result, x => Assert.True(x.Value.Read && x.Value.Edit && x.Value.Manage && x.Value.ViewPassword)); } @@ -110,7 +108,7 @@ public async Task GetCipherPermissionsForUserQuery_CanEditAllCiphers_Admin(Guid var result = await sutProvider.Sut.GetByOrganization(organizationId); Assert.Equal(6, result.Count); - Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); + Assert.All(result, x => Assert.Contains(x.Key, _cipherIds)); Assert.All(result, x => Assert.True(x.Value.Read && x.Value.Edit && x.Value.Manage && x.Value.ViewPassword)); } @@ -132,20 +130,20 @@ public async Task GetCipherPermissionsForUserQuery_CanEditUnassignedCiphers(Guid var result = await sutProvider.Sut.GetByOrganization(organizationId); Assert.Equal(6, result.Count); - Assert.All(result, x => Assert.True(_cipherIds.Contains(x.Key))); - Assert.Equal(false, result[_noAccessCipherId].Read); - Assert.Equal(true, result[_readOnlyCipherId].Read); - Assert.Equal(false, result[_readOnlyCipherId].Edit); - Assert.Equal(true, result[_editCipherId].Edit); - Assert.Equal(true, result[_manageCipherId].Manage); - Assert.Equal(true, result[_readExceptPasswordCipherId].Read); - Assert.Equal(false, result[_readExceptPasswordCipherId].ViewPassword); - - Assert.Equal(true, result[_unassignedCipherId].Unassigned); - Assert.Equal(true, result[_unassignedCipherId].Read); - Assert.Equal(true, result[_unassignedCipherId].Edit); - Assert.Equal(true, result[_unassignedCipherId].ViewPassword); - Assert.Equal(true, result[_unassignedCipherId].Manage); + Assert.All(result, x => Assert.Contains(x.Key, _cipherIds)); + Assert.False(result[_noAccessCipherId].Read); + Assert.True(result[_readOnlyCipherId].Read); + Assert.False(result[_readOnlyCipherId].Edit); + Assert.True(result[_editCipherId].Edit); + Assert.True(result[_manageCipherId].Manage); + Assert.True(result[_readExceptPasswordCipherId].Read); + Assert.False(result[_readExceptPasswordCipherId].ViewPassword); + + Assert.True(result[_unassignedCipherId].Unassigned); + Assert.True(result[_unassignedCipherId].Read); + Assert.True(result[_unassignedCipherId].Edit); + Assert.True(result[_unassignedCipherId].ViewPassword); + Assert.True(result[_unassignedCipherId].Manage); } private List CreateCipherPermissions() From 021634c7b4c5ea7723cba99c78c5dbedaea282ad Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 11 Dec 2024 16:48:23 -0800 Subject: [PATCH 14/44] [PM-14378] Update date in migration file --- ...y.sql => 2024-12-11_00_CipherOrganizationPermissionsQuery.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename util/Migrator/DbScripts/{2024-12-05_00_CipherOrganizationPermissionsQuery.sql => 2024-12-11_00_CipherOrganizationPermissionsQuery.sql} (100%) diff --git a/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql b/util/Migrator/DbScripts/2024-12-11_00_CipherOrganizationPermissionsQuery.sql similarity index 100% rename from util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql rename to util/Migrator/DbScripts/2024-12-11_00_CipherOrganizationPermissionsQuery.sql From b10df9bce931d3e0e3047ee3abc9b39b54b65c4d Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Fri, 13 Dec 2024 10:48:56 -0800 Subject: [PATCH 15/44] [PM-14378] Add missing awaits --- .../Repositories/CipherRepositoryTests.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs index aef4fa50b729..73be5cfeddd6 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/CipherRepositoryTests.cs @@ -247,10 +247,10 @@ public async Task GetCipherPermissionsForOrganizationAsync_Works( Data = "" }); - collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id, + await collectionCipherRepository.UpdateCollectionsForAdminAsync(manageCipher.Id, organization.Id, new List { manageCollection.Id }); - collectionRepository.UpdateUsersAsync(manageCollection.Id, new List + await collectionRepository.UpdateUsersAsync(manageCollection.Id, new List { new() { @@ -276,10 +276,10 @@ public async Task GetCipherPermissionsForOrganizationAsync_Works( Data = "" }); - collectionCipherRepository.UpdateCollectionsForAdminAsync(editCipher.Id, organization.Id, + await collectionCipherRepository.UpdateCollectionsForAdminAsync(editCipher.Id, organization.Id, new List { editCollection.Id }); - collectionRepository.UpdateUsersAsync(editCollection.Id, + await collectionRepository.UpdateUsersAsync(editCollection.Id, new List { new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = false } @@ -300,10 +300,10 @@ public async Task GetCipherPermissionsForOrganizationAsync_Works( Data = "" }); - collectionCipherRepository.UpdateCollectionsForAdminAsync(editExceptPasswordCipher.Id, organization.Id, + await collectionCipherRepository.UpdateCollectionsForAdminAsync(editExceptPasswordCipher.Id, organization.Id, new List { editExceptPasswordCollection.Id }); - collectionRepository.UpdateUsersAsync(editExceptPasswordCollection.Id, new List + await collectionRepository.UpdateUsersAsync(editExceptPasswordCollection.Id, new List { new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = false, Manage = false } }); @@ -323,10 +323,10 @@ public async Task GetCipherPermissionsForOrganizationAsync_Works( Data = "" }); - collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id, + await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewOnlyCipher.Id, organization.Id, new List { viewOnlyCollection.Id }); - collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id, + await collectionRepository.UpdateUsersAsync(viewOnlyCollection.Id, new List { new() { Id = orgUser.Id, HidePasswords = false, ReadOnly = true, Manage = false } @@ -347,10 +347,10 @@ public async Task GetCipherPermissionsForOrganizationAsync_Works( Data = "" }); - collectionCipherRepository.UpdateCollectionsForAdminAsync(viewExceptPasswordCipher.Id, organization.Id, + await collectionCipherRepository.UpdateCollectionsForAdminAsync(viewExceptPasswordCipher.Id, organization.Id, new List { viewExceptPasswordCollection.Id }); - collectionRepository.UpdateUsersAsync(viewExceptPasswordCollection.Id, + await collectionRepository.UpdateUsersAsync(viewExceptPasswordCollection.Id, new List { new() { Id = orgUser.Id, HidePasswords = true, ReadOnly = true, Manage = false } From 3a04e88250434dec1007b361a66bc5ede8a1aa8b Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Fri, 13 Dec 2024 17:14:55 -0500 Subject: [PATCH 16/44] Added bulk create request model --- .../Request/BulkCreateSecurityTasksRequestModel.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/Api/Models/Request/BulkCreateSecurityTasksRequestModel.cs diff --git a/src/Api/Models/Request/BulkCreateSecurityTasksRequestModel.cs b/src/Api/Models/Request/BulkCreateSecurityTasksRequestModel.cs new file mode 100644 index 000000000000..9292063498e6 --- /dev/null +++ b/src/Api/Models/Request/BulkCreateSecurityTasksRequestModel.cs @@ -0,0 +1,9 @@ +using Bit.Core.Vault.Enums; + +namespace Bit.Api.Models.Request; + +public class BulkCreateSecurityTasksRequestModel +{ + public SecurityTaskType Type { get; set; } + public Guid CipherId { get; set; } +} From d00b25b8c73d2aa3231829a1cfe337039dd52a1a Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Fri, 20 Dec 2024 12:56:39 -0500 Subject: [PATCH 17/44] Created sproc to create bulk security tasks --- .../SecurityTask/SecurityTask_CreateMany.sql | 27 ++++++++++ .../User Defined Types/SecurityTaskType.sql | 9 ++++ .../2024-12-19_00_SecurityTaskCreateMany.sql | 51 +++++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql create mode 100644 src/Sql/dbo/User Defined Types/SecurityTaskType.sql create mode 100644 util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql new file mode 100644 index 000000000000..85cf9764cb0a --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[SecurityTask_CreateMany] + @Tasks AS [dbo].[SecurityTaskType] READONLY +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[SecurityTask] + ( + [Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate] + ) + SELECT + ST.[Id], + ST.[OrganizationId], + ST.[CipherId], + ST.[Type], + ST.[Status], + ST.[CreationDate], + ST.[RevisionDate] + FROM + @Tasks ST +END diff --git a/src/Sql/dbo/User Defined Types/SecurityTaskType.sql b/src/Sql/dbo/User Defined Types/SecurityTaskType.sql new file mode 100644 index 000000000000..42beba17d95e --- /dev/null +++ b/src/Sql/dbo/User Defined Types/SecurityTaskType.sql @@ -0,0 +1,9 @@ +CREATE TYPE [dbo].[SecurityTaskType] AS TABLE( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [Type] TINYINT NOT NULL, + [Status] TINYINT NOT NULL, + [CreationDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, +); diff --git a/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql b/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql new file mode 100644 index 000000000000..82c5196b64e7 --- /dev/null +++ b/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql @@ -0,0 +1,51 @@ +-- Create SecurityTaskType +IF NOT EXISTS ( + SELECT + * + FROM + sys.types + WHERE + [Name] = 'SecurityTaskType' AND + is_user_defined = 1 +) +BEGIN +CREATE TYPE [dbo].[SecurityTaskType] AS TABLE( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [CipherId] UNIQUEIDENTIFIER NOT NULL, + [Type] TINYINT NOT NULL, + [Status] TINYINT NOT NULL, + [CreationDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, +); +END +GO + +-- SecurityTask_CreateMany +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_CreateMany] + @Tasks AS [dbo].[SecurityTaskType] READONLY +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[SecurityTask] + ( + [Id], + [OrganizationId], + [CipherId], + [Type], + [Status], + [CreationDate], + [RevisionDate] + ) + SELECT + ST.[Id], + ST.[OrganizationId], + ST.[CipherId], + ST.[Type], + ST.[Status], + ST.[CreationDate], + ST.[RevisionDate] + FROM + @Tasks ST +END From 3198ea71daf9eb49eb232ff6c0145b9221934d9f Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Fri, 20 Dec 2024 18:12:34 -0500 Subject: [PATCH 18/44] Renamed tasks to SecurityTasksInput --- .../Models/Request/BulkCreateSecurityTasksRequestModel.cs | 3 +-- .../SecurityTask/SecurityTask_CreateMany.sql | 4 ++-- .../DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) rename src/Api/{ => Vault}/Models/Request/BulkCreateSecurityTasksRequestModel.cs (58%) diff --git a/src/Api/Models/Request/BulkCreateSecurityTasksRequestModel.cs b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs similarity index 58% rename from src/Api/Models/Request/BulkCreateSecurityTasksRequestModel.cs rename to src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs index 9292063498e6..b3352dcd6c0e 100644 --- a/src/Api/Models/Request/BulkCreateSecurityTasksRequestModel.cs +++ b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs @@ -4,6 +4,5 @@ namespace Bit.Api.Models.Request; public class BulkCreateSecurityTasksRequestModel { - public SecurityTaskType Type { get; set; } - public Guid CipherId { get; set; } + public IEnumerable Tasks { get; set; } } diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql index 85cf9764cb0a..8914b15de571 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_CreateMany.sql @@ -1,5 +1,5 @@ CREATE PROCEDURE [dbo].[SecurityTask_CreateMany] - @Tasks AS [dbo].[SecurityTaskType] READONLY + @SecurityTasksInput AS [dbo].[SecurityTaskType] READONLY AS BEGIN SET NOCOUNT ON @@ -23,5 +23,5 @@ BEGIN ST.[CreationDate], ST.[RevisionDate] FROM - @Tasks ST + @SecurityTasksInput ST END diff --git a/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql b/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql index 82c5196b64e7..19ada043aa1f 100644 --- a/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql +++ b/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql @@ -23,7 +23,7 @@ GO -- SecurityTask_CreateMany CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_CreateMany] - @Tasks AS [dbo].[SecurityTaskType] READONLY + @SecurityTasksInput AS [dbo].[SecurityTaskType] READONLY AS BEGIN SET NOCOUNT ON @@ -47,5 +47,5 @@ BEGIN ST.[CreationDate], ST.[RevisionDate] FROM - @Tasks ST + @SecurityTasksInput ST END From 4fabb8a4438533c9f2d488cdb89c1ce577716ce3 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Fri, 20 Dec 2024 18:14:01 -0500 Subject: [PATCH 19/44] Added create many implementation for sqlserver and ef core --- .../Repositories/ISecurityTaskRepository.cs | 7 ++++++ src/Infrastructure.Dapper/DapperHelpers.cs | 23 ++++++++++++++++- .../Repositories/SecurityTaskRepository.cs | 25 +++++++++++++++++++ .../Repositories/SecurityTaskRepository.cs | 22 ++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs index 34f1f2ee6411..159207bfc552 100644 --- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -13,4 +13,11 @@ public interface ISecurityTaskRepository : IRepository /// Optional filter for task status. If not provided, returns tasks of all statuses /// Task> GetManyByUserIdStatusAsync(Guid userId, SecurityTaskStatus? status = null); + + /// + /// Creates bulk security tasks for an organization. + /// + /// + /// + Task> CreateManyAsync(IEnumerable tasks); } diff --git a/src/Infrastructure.Dapper/DapperHelpers.cs b/src/Infrastructure.Dapper/DapperHelpers.cs index c256612447e6..35b6eb0127c2 100644 --- a/src/Infrastructure.Dapper/DapperHelpers.cs +++ b/src/Infrastructure.Dapper/DapperHelpers.cs @@ -5,6 +5,8 @@ using System.Reflection; using Bit.Core.Entities; using Bit.Core.Models.Data; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; using Dapper; #nullable enable @@ -81,7 +83,7 @@ private static bool TryGetPropertyInfo(Expression> columnExpres return true; } - // Value type properties will implicitly box into the object so + // Value type properties will implicitly box into the object so // we need to look past the Convert expression // i => (System.Object?)i.Id if ( @@ -153,6 +155,18 @@ public static class DapperHelpers ] ); + private static readonly DataTableBuilder _securityTaskTypeTableBuilder = new( + [ + st => st.Id, + st => st.OrganizationId, + st => st.CipherId, + st => st.Type, + st => st.Status, + st => st.CreationDate, + st => st.RevisionDate, + ] + ); + public static DataTable ToGuidIdArrayTVP(this IEnumerable ids) { return ids.ToArrayTVP("GuidId"); @@ -212,6 +226,13 @@ public static DataTable ToTvp(this IEnumerable organiza return table; } + public static DataTable ToTvp(this IEnumerable securityTasks) + { + var table = _securityTaskTypeTableBuilder.Build(securityTasks ?? []); + table.SetTypeName("[dbo].[SecurityTaskType]"); + return table; + } + public static DataTable BuildTable(this IEnumerable entities, DataTable table, List<(string name, Type type, Func getter)> columnData) { diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs index dfe8a04814cc..72ac7fa52d19 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -32,4 +32,29 @@ public async Task> GetManyByUserIdStatusAsync(Guid use return results.ToList(); } + + /// + public async Task> CreateManyAsync(IEnumerable tasks) + { + if (tasks?.Any() != true) + { + return Array.Empty(); + } + + var tasksList = tasks.ToList(); + foreach (var task in tasksList) + { + task.SetNewId(); + } + + var securityTasksTvp = tasksList.ToTvp(); + await using var connection = new SqlConnection(ConnectionString); + + var results = await connection.ExecuteAsync( + $"[{Schema}].[SecurityTask_CreateMany]", + new { SecurityTasksInput = securityTasksTvp }, + commandType: CommandType.StoredProcedure); + + return tasksList.Select(t => t.Id).ToList(); + } } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs index bd56df1bcfca..5dcc35c6fd77 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -25,4 +25,26 @@ public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper var data = await query.Run(dbContext).ToListAsync(); return data; } + + /// + public async Task> CreateManyAsync(IEnumerable tasks) + { + if (tasks?.Any() != true) + { + return Array.Empty(); + } + + var tasksList = tasks.ToList(); + foreach (var task in tasksList) + { + task.SetNewId(); + } + + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + await dbContext.AddRangeAsync(tasksList); + await dbContext.SaveChangesAsync(); + + return tasksList.Select(t => t.Id).ToList(); + } } From ef37e9dd342c002cfec40a62c0bd311af224acbd Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 14:55:41 -0500 Subject: [PATCH 20/44] removed trailing comma --- src/Sql/dbo/User Defined Types/SecurityTaskType.sql | 2 +- .../DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Sql/dbo/User Defined Types/SecurityTaskType.sql b/src/Sql/dbo/User Defined Types/SecurityTaskType.sql index 42beba17d95e..fa8f75fec9a3 100644 --- a/src/Sql/dbo/User Defined Types/SecurityTaskType.sql +++ b/src/Sql/dbo/User Defined Types/SecurityTaskType.sql @@ -5,5 +5,5 @@ CREATE TYPE [dbo].[SecurityTaskType] AS TABLE( [Type] TINYINT NOT NULL, [Status] TINYINT NOT NULL, [CreationDate] DATETIME2(7) NOT NULL, - [RevisionDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL ); diff --git a/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql b/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql index 19ada043aa1f..6f7b131e17f8 100644 --- a/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql +++ b/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql @@ -16,7 +16,7 @@ CREATE TYPE [dbo].[SecurityTaskType] AS TABLE( [Type] TINYINT NOT NULL, [Status] TINYINT NOT NULL, [CreationDate] DATETIME2(7) NOT NULL, - [RevisionDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL ); END GO @@ -49,3 +49,4 @@ BEGIN FROM @SecurityTasksInput ST END +GO From 0edb923b00da6c6b757f975768171be0c257cee7 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 15:52:05 -0500 Subject: [PATCH 21/44] created ef implementatin for create many and added integration test --- .../Repositories/SecurityTaskRepository.cs | 3 +- .../SecurityTaskRepositoryTests.cs | 43 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs index 5dcc35c6fd77..ea0d14f65609 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -42,7 +42,8 @@ public async Task> CreateManyAsync(IEnumerable>(tasksList); + await dbContext.AddRangeAsync(entities); await dbContext.SaveChangesAsync(); return tasksList.Select(t => t.Id).ToList(); diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs index 2010c90a5e0d..eb5a310db3f6 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Vault/Repositories/SecurityTaskRepositoryTests.cs @@ -223,4 +223,47 @@ await collectionRepository.UpdateUsersAsync(collection.Id, Assert.DoesNotContain(task1, completedTasks, new SecurityTaskComparer()); Assert.DoesNotContain(task3, completedTasks, new SecurityTaskComparer()); } + + [DatabaseTheory, DatabaseData] + public async Task CreateManyAsync( + IOrganizationRepository organizationRepository, + ICipherRepository cipherRepository, + ISecurityTaskRepository securityTaskRepository) + { + var organization = await organizationRepository.CreateAsync(new Organization + { + Name = "Test Org", + PlanType = PlanType.EnterpriseAnnually, + Plan = "Test Plan", + BillingEmail = "" + }); + + var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher1); + + var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", }; + await cipherRepository.CreateAsync(cipher2); + + var tasks = new List + { + new() + { + OrganizationId = organization.Id, + CipherId = cipher1.Id, + Status = SecurityTaskStatus.Pending, + Type = SecurityTaskType.UpdateAtRiskCredential, + }, + new() + { + OrganizationId = organization.Id, + CipherId = cipher2.Id, + Status = SecurityTaskStatus.Completed, + Type = SecurityTaskType.UpdateAtRiskCredential, + } + }; + + var taskIds = await securityTaskRepository.CreateManyAsync(tasks); + + Assert.Equal(2, taskIds.Count); + } } From 268490188e767201d57449fc7a3c88a98a56e5f4 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 15:52:57 -0500 Subject: [PATCH 22/44] Refactored request model --- .../Request/BulkCreateSecurityTasksRequestModel.cs | 4 ++-- src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs diff --git a/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs index b3352dcd6c0e..6c8c7e03b30e 100644 --- a/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs +++ b/src/Api/Vault/Models/Request/BulkCreateSecurityTasksRequestModel.cs @@ -1,6 +1,6 @@ -using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Models.Api; -namespace Bit.Api.Models.Request; +namespace Bit.Api.Vault.Models.Request; public class BulkCreateSecurityTasksRequestModel { diff --git a/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs new file mode 100644 index 000000000000..679fd15e22df --- /dev/null +++ b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs @@ -0,0 +1,9 @@ +using Bit.Core.Vault.Enums; + +namespace Bit.Core.Vault.Models.Api; + +public class SecurityTaskCreateRequest +{ + public SecurityTaskType Type { get; set; } + public Guid CipherId { get; set; } +} From 0adb3e351d659a71b0d0105f4e3c8171d786b37f Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 15:53:15 -0500 Subject: [PATCH 23/44] Refactored request model --- src/Api/Vault/Controllers/SecurityTaskController.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index a0b18cb8476f..e7616e9031a0 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -1,4 +1,5 @@ using Bit.Api.Models.Response; +using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core; using Bit.Core.Services; @@ -54,4 +55,11 @@ public async Task Complete(Guid taskId) await _markTaskAsCompleteCommand.CompleteAsync(taskId); return NoContent(); } + + [HttpPost("{orgId:guid}/bulk-create")] + public async Task BulkCreateTasks(Guid orgId, [FromBody] BulkCreateSecurityTasksRequestModel model) + { + + return NoContent(); + } } From ba848ce082e0fa095f9cd261ad30d15b79b5c52a Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 16:03:34 -0500 Subject: [PATCH 24/44] created create many tasks command interface and class --- src/Core/Vault/Commands/CreateManyTasksCommand.cs | 12 ++++++++++++ .../Commands/Interfaces/ICreateManyTasksCommand.cs | 8 ++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/Core/Vault/Commands/CreateManyTasksCommand.cs create mode 100644 src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs diff --git a/src/Core/Vault/Commands/CreateManyTasksCommand.cs b/src/Core/Vault/Commands/CreateManyTasksCommand.cs new file mode 100644 index 000000000000..6eb70b4e2024 --- /dev/null +++ b/src/Core/Vault/Commands/CreateManyTasksCommand.cs @@ -0,0 +1,12 @@ +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Models.Api; + +namespace Bit.Core.Vault.Commands; + +public class CreateManyTasksCommand : ICreateManyTasksCommand +{ + public async Task CreateAsync(Guid organizationId, IEnumerable tasks) + { + + } +} diff --git a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs new file mode 100644 index 000000000000..4208ad259254 --- /dev/null +++ b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs @@ -0,0 +1,8 @@ +using Bit.Core.Vault.Models.Api; + +namespace Bit.Core.Vault.Commands.Interfaces; + +public interface ICreateManyTasksCommand +{ + Task CreateAsync(Guid organizationId, IEnumerable tasks); +} From 7738ce454ec9af9d677373117f46e559d86a2c6b Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 17:03:29 -0500 Subject: [PATCH 25/44] added security authorization handler work temp --- .../Vault/Commands/Interfaces/ICreateManyTasksCommand.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs index 4208ad259254..e42352cdf685 100644 --- a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs +++ b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs @@ -4,5 +4,11 @@ namespace Bit.Core.Vault.Commands.Interfaces; public interface ICreateManyTasksCommand { + /// + /// Creates multiple security tasks for an organization. + /// + /// The + /// + /// Task CreateAsync(Guid organizationId, IEnumerable tasks); } From 67f3215234e492220c1ba2da3ed138f43b08f7cf Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 17:43:33 -0500 Subject: [PATCH 26/44] Added the implementation for the create manys tasks command --- .../Vault/Commands/CreateManyTasksCommand.cs | 56 ++++++++++++++++++- .../Interfaces/ICreateManyTasksCommand.cs | 9 ++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/Core/Vault/Commands/CreateManyTasksCommand.cs b/src/Core/Vault/Commands/CreateManyTasksCommand.cs index 6eb70b4e2024..c97dfb467d17 100644 --- a/src/Core/Vault/Commands/CreateManyTasksCommand.cs +++ b/src/Core/Vault/Commands/CreateManyTasksCommand.cs @@ -1,12 +1,64 @@ -using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Commands.Interfaces; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; using Bit.Core.Vault.Models.Api; +using Bit.Core.Vault.Repositories; +using Microsoft.AspNetCore.Authorization; namespace Bit.Core.Vault.Commands; public class CreateManyTasksCommand : ICreateManyTasksCommand { - public async Task CreateAsync(Guid organizationId, IEnumerable tasks) + private readonly IAuthorizationService _authorizationService; + private readonly ICurrentContext _currentContext; + private readonly ISecurityTaskRepository _securityTaskRepository; + + public CreateManyTasksCommand( + ISecurityTaskRepository securityTaskRepository, + IAuthorizationService authorizationService, + ICurrentContext currentContext) + { + _securityTaskRepository = securityTaskRepository; + _authorizationService = authorizationService; + _currentContext = currentContext; + } + + /// + public async Task> CreateAsync(Guid organizationId, IEnumerable tasks) { + if (!_currentContext.UserId.HasValue) + { + throw new NotFoundException(); + } + + var tasksList = tasks?.ToList(); + + if (tasksList is null || tasksList.Count == 0) + { + throw new BadRequestException("No tasks provided."); + } + + var securityTasks = tasksList.Select(t => new SecurityTask + { + OrganizationId = organizationId, + CipherId = t.CipherId, + Type = t.Type, + Status = SecurityTaskStatus.Pending + }).ToList(); + + // Verify authorization for each task + foreach (var task in securityTasks) + { + await _authorizationService.AuthorizeOrThrowAsync( + _currentContext.HttpContext.User, + task, + SecurityTaskOperations.Create); + } + return await _securityTaskRepository.CreateManyAsync(securityTasks); } } diff --git a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs index e42352cdf685..46e7cfacbba4 100644 --- a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs +++ b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs @@ -1,4 +1,5 @@ -using Bit.Core.Vault.Models.Api; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Api; namespace Bit.Core.Vault.Commands.Interfaces; @@ -6,9 +7,11 @@ public interface ICreateManyTasksCommand { /// /// Creates multiple security tasks for an organization. + /// Each task must be authorized and the user must have the Create permission + /// and associated ciphers must belong to the organization. /// /// The /// - /// - Task CreateAsync(Guid organizationId, IEnumerable tasks); + /// Collection of created security task IDs + Task> CreateAsync(Guid organizationId, IEnumerable tasks); } From d2bed0985e3753defd675a38e4b9f6fca99593b2 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 17:43:51 -0500 Subject: [PATCH 27/44] Added comment --- src/Core/Vault/Repositories/ISecurityTaskRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs index 159207bfc552..ba1f6bf12357 100644 --- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -17,7 +17,7 @@ public interface ISecurityTaskRepository : IRepository /// /// Creates bulk security tasks for an organization. /// - /// - /// + /// Collection of tasks to create + /// Collection of created security task IDs Task> CreateManyAsync(IEnumerable tasks); } From 6aaba7e7ac85cd9f6038d9fcf4199482b1a93d6e Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 17:53:57 -0500 Subject: [PATCH 28/44] Changed return to return list of created security tasks --- src/Core/Vault/Commands/CreateManyTasksCommand.cs | 3 ++- .../Commands/Interfaces/ICreateManyTasksCommand.cs | 4 ++-- .../Vault/Repositories/ISecurityTaskRepository.cs | 4 ++-- .../Vault/Repositories/SecurityTaskRepository.cs | 10 +++++----- .../Vault/Repositories/SecurityTaskRepository.cs | 11 ++++++----- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Core/Vault/Commands/CreateManyTasksCommand.cs b/src/Core/Vault/Commands/CreateManyTasksCommand.cs index c97dfb467d17..1b21f202eb5a 100644 --- a/src/Core/Vault/Commands/CreateManyTasksCommand.cs +++ b/src/Core/Vault/Commands/CreateManyTasksCommand.cs @@ -28,7 +28,8 @@ public CreateManyTasksCommand( } /// - public async Task> CreateAsync(Guid organizationId, IEnumerable tasks) + public async Task> CreateAsync(Guid organizationId, + IEnumerable tasks) { if (!_currentContext.UserId.HasValue) { diff --git a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs index 46e7cfacbba4..3aa0f850703b 100644 --- a/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs +++ b/src/Core/Vault/Commands/Interfaces/ICreateManyTasksCommand.cs @@ -12,6 +12,6 @@ public interface ICreateManyTasksCommand /// /// The /// - /// Collection of created security task IDs - Task> CreateAsync(Guid organizationId, IEnumerable tasks); + /// Collection of created security tasks + Task> CreateAsync(Guid organizationId, IEnumerable tasks); } diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs index ba1f6bf12357..a5118426da4a 100644 --- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -18,6 +18,6 @@ public interface ISecurityTaskRepository : IRepository /// Creates bulk security tasks for an organization. /// /// Collection of tasks to create - /// Collection of created security task IDs - Task> CreateManyAsync(IEnumerable tasks); + /// Collection of created security tasks + Task> CreateManyAsync(IEnumerable tasks); } diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs index 72ac7fa52d19..15df755bfec3 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -34,14 +34,14 @@ public async Task> GetManyByUserIdStatusAsync(Guid use } /// - public async Task> CreateManyAsync(IEnumerable tasks) + public async Task> CreateManyAsync(IEnumerable tasks) { - if (tasks?.Any() != true) + var tasksList = tasks?.ToList(); + if (tasksList is null || tasksList.Count == 0) { - return Array.Empty(); + return Array.Empty(); } - var tasksList = tasks.ToList(); foreach (var task in tasksList) { task.SetNewId(); @@ -55,6 +55,6 @@ public async Task> CreateManyAsync(IEnumerable t new { SecurityTasksInput = securityTasksTvp }, commandType: CommandType.StoredProcedure); - return tasksList.Select(t => t.Id).ToList(); + return tasksList; } } diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs index ea0d14f65609..2a6ad6efc0ec 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -27,14 +27,15 @@ public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper } /// - public async Task> CreateManyAsync(IEnumerable tasks) + public async Task> CreateManyAsync( + IEnumerable tasks) { - if (tasks?.Any() != true) + var tasksList = tasks?.ToList(); + if (tasksList is null || tasksList.Count == 0) { - return Array.Empty(); + return Array.Empty(); } - var tasksList = tasks.ToList(); foreach (var task in tasksList) { task.SetNewId(); @@ -46,6 +47,6 @@ public async Task> CreateManyAsync(IEnumerable t.Id).ToList(); + return tasksList; } } From 04c7c56fde2a5379ed26425d166d22535ef5b83b Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 18:07:43 -0500 Subject: [PATCH 29/44] Registered command --- src/Core/Vault/VaultServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Vault/VaultServiceCollectionExtensions.cs b/src/Core/Vault/VaultServiceCollectionExtensions.cs index 4995d0405f2d..a6b8085e33c1 100644 --- a/src/Core/Vault/VaultServiceCollectionExtensions.cs +++ b/src/Core/Vault/VaultServiceCollectionExtensions.cs @@ -20,5 +20,6 @@ private static void AddVaultQueries(this IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } From 6df61321d3b33a9818af606b6b74dd43b93fa03c Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 18:08:04 -0500 Subject: [PATCH 30/44] Completed bulk create action --- .../Controllers/SecurityTaskController.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index e7616e9031a0..27ee691419b5 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -20,15 +20,18 @@ public class SecurityTaskController : Controller private readonly IUserService _userService; private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery; private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand; + private readonly ICreateManyTasksCommand _createManyTasksCommand; public SecurityTaskController( IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery, - IMarkTaskAsCompleteCommand markTaskAsCompleteCommand) + IMarkTaskAsCompleteCommand markTaskAsCompleteCommand, + ICreateManyTasksCommand createManyTasksCommand) { _userService = userService; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; _markTaskAsCompleteCommand = markTaskAsCompleteCommand; + _createManyTasksCommand = createManyTasksCommand; } /// @@ -56,10 +59,18 @@ public async Task Complete(Guid taskId) return NoContent(); } + /// + /// Bulk create security tasks for an organization. + /// + /// + /// + /// A list response model containing the security tasks created for the organization. [HttpPost("{orgId:guid}/bulk-create")] - public async Task BulkCreateTasks(Guid orgId, [FromBody] BulkCreateSecurityTasksRequestModel model) + public async Task> BulkCreateTasks(Guid orgId, + [FromBody] BulkCreateSecurityTasksRequestModel model) { - - return NoContent(); + var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks); + var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); + return new ListResponseModel(response); } } From e3352f4a23cd9ea62cc7eba5ea5afc90275bf915 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 18:31:51 -0500 Subject: [PATCH 31/44] Added unit tests for the command --- .../Commands/CreateManyTasksCommandTest.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs diff --git a/test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs b/test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs new file mode 100644 index 000000000000..23e92965f25f --- /dev/null +++ b/test/Core.Test/Vault/Commands/CreateManyTasksCommandTest.cs @@ -0,0 +1,85 @@ +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Test.Vault.AutoFixture; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Commands; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Models.Api; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Commands; + +[SutProviderCustomize] +[SecurityTaskCustomize] +public class CreateManyTasksCommandTest +{ + private static void Setup(SutProvider sutProvider, Guid? userId, + bool authorizedCreate = false) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency() + .AuthorizeAsync(Arg.Any(), Arg.Any(), + Arg.Is>(reqs => + reqs.Contains(SecurityTaskOperations.Create))) + .Returns(authorizedCreate ? AuthorizationResult.Success() : AuthorizationResult.Failed()); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NotLoggedIn_NotFoundException( + SutProvider sutProvider, + Guid organizationId, + IEnumerable tasks) + { + Setup(sutProvider, null, true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, tasks)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_NoTasksProvided_BadRequestException( + SutProvider sutProvider, + Guid organizationId) + { + Setup(sutProvider, Guid.NewGuid()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, null)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_AuthorizationFailed_NotFoundException( + SutProvider sutProvider, + Guid organizationId, + IEnumerable tasks) + { + Setup(sutProvider, Guid.NewGuid()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, tasks)); + } + + [Theory] + [BitAutoData] + public async Task CreateAsync_AuthorizationSucceeded_ReturnsSecurityTasks( + SutProvider sutProvider, + Guid organizationId, + IEnumerable tasks, + ICollection securityTasks) + { + Setup(sutProvider, Guid.NewGuid(), true); + sutProvider.GetDependency() + .CreateManyAsync(Arg.Any>()) + .Returns(securityTasks); + + var result = await sutProvider.Sut.CreateAsync(organizationId, tasks); + + Assert.Equal(securityTasks, result); + } +} From e7abb094ac80d1136dbc9a5ae56cf61688a75676 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 18:44:32 -0500 Subject: [PATCH 32/44] removed hard coded table name --- .../Vault/Repositories/SecurityTaskRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs index 15df755bfec3..931d7aa6d8cd 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -51,7 +51,7 @@ public async Task> CreateManyAsync(IEnumerable Date: Mon, 23 Dec 2024 18:49:15 -0500 Subject: [PATCH 33/44] Fixed lint issue --- src/Infrastructure.Dapper/DapperHelpers.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Infrastructure.Dapper/DapperHelpers.cs b/src/Infrastructure.Dapper/DapperHelpers.cs index 35b6eb0127c2..c00a218274aa 100644 --- a/src/Infrastructure.Dapper/DapperHelpers.cs +++ b/src/Infrastructure.Dapper/DapperHelpers.cs @@ -6,7 +6,6 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Vault.Entities; -using Bit.Core.Vault.Enums; using Dapper; #nullable enable From 72b121d00129a8b7728153dc9e030afe61a3fb94 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Fri, 27 Dec 2024 10:09:54 -0500 Subject: [PATCH 34/44] Added JsonConverter attribute to allow enum value to be passed as string --- src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs index 679fd15e22df..19f14b76dc4e 100644 --- a/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs +++ b/src/Core/Vault/Models/Api/SecurityTaskCreateRequest.cs @@ -1,9 +1,11 @@ -using Bit.Core.Vault.Enums; +using System.Text.Json.Serialization; +using Bit.Core.Vault.Enums; namespace Bit.Core.Vault.Models.Api; public class SecurityTaskCreateRequest { + [JsonConverter(typeof(JsonStringEnumConverter))] public SecurityTaskType Type { get; set; } public Guid CipherId { get; set; } } From 770fa343598ce6759ddf35eaad2ef7d44408a8f8 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Thu, 9 Jan 2025 15:25:57 -0500 Subject: [PATCH 35/44] Removed makshift security task operations --- .../Authorization/SecurityTaskOperations.cs | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 src/Core/Vault/Authorization/SecurityTaskOperations.cs diff --git a/src/Core/Vault/Authorization/SecurityTaskOperations.cs b/src/Core/Vault/Authorization/SecurityTaskOperations.cs deleted file mode 100644 index 77b504723f14..000000000000 --- a/src/Core/Vault/Authorization/SecurityTaskOperations.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.AspNetCore.Authorization.Infrastructure; - -namespace Bit.Core.Vault.Authorization; - -public class SecurityTaskOperationRequirement : OperationAuthorizationRequirement -{ - public SecurityTaskOperationRequirement(string name) - { - Name = name; - } -} - -public static class SecurityTaskOperations -{ - public static readonly SecurityTaskOperationRequirement Update = new(nameof(Update)); -} From 239a1919cedd2235c1cc334e723eacc372ea2d07 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Thu, 9 Jan 2025 15:28:02 -0500 Subject: [PATCH 36/44] Fixed references --- src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs | 2 +- test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs index b46fb0cecb22..77b8a8625c7c 100644 --- a/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs +++ b/src/Core/Vault/Commands/MarkTaskAsCompletedCommand.cs @@ -1,7 +1,7 @@ using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Utilities; -using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Authorization.SecurityTasks; using Bit.Core.Vault.Commands.Interfaces; using Bit.Core.Vault.Enums; using Bit.Core.Vault.Repositories; diff --git a/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs b/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs index 82550df48dc7..ca9a42cdb3e2 100644 --- a/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs +++ b/test/Core.Test/Vault/Commands/MarkTaskAsCompletedCommandTest.cs @@ -3,7 +3,7 @@ using Bit.Core.Context; using Bit.Core.Exceptions; using Bit.Core.Test.Vault.AutoFixture; -using Bit.Core.Vault.Authorization; +using Bit.Core.Vault.Authorization.SecurityTasks; using Bit.Core.Vault.Commands; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; From 714666b6a34acd861eb93983174bea8259fd7b58 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Thu, 9 Jan 2025 15:30:25 -0500 Subject: [PATCH 37/44] Removed old migration --- ..._00_CipherOrganizationPermissionsQuery.sql | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 util/Migrator/DbScripts/2024-12-11_00_CipherOrganizationPermissionsQuery.sql diff --git a/util/Migrator/DbScripts/2024-12-11_00_CipherOrganizationPermissionsQuery.sql b/util/Migrator/DbScripts/2024-12-11_00_CipherOrganizationPermissionsQuery.sql deleted file mode 100644 index 2eb4de905338..000000000000 --- a/util/Migrator/DbScripts/2024-12-11_00_CipherOrganizationPermissionsQuery.sql +++ /dev/null @@ -1,67 +0,0 @@ -CREATE OR ALTER PROCEDURE [dbo].[CipherOrganizationPermissions_GetManyByOrganizationId] - @OrganizationId UNIQUEIDENTIFIER, - @UserId UNIQUEIDENTIFIER -AS -BEGIN - SET NOCOUNT ON - - SELECT - C.[Id], - C.[OrganizationId], - MAX(CASE - WHEN - CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL - THEN 0 - ELSE 1 - END) [Read], - MAX(CASE - WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 1) = 0 - THEN 1 - ELSE 0 - END) [ViewPassword], - MAX(CASE - WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 1) = 0 - THEN 1 - ELSE 0 - END) [Edit], - - MAX(COALESCE(CU.[Manage], CG.[Manage], 0)) [Manage], - CASE - WHEN COUNT(CC.[CollectionId]) > 0 THEN 0 - ELSE 1 - END [Unassigned] - FROM - [dbo].[CipherDetails](@UserId) C - INNER JOIN - [OrganizationUser] OU ON - C.[UserId] IS NULL - AND C.[OrganizationId] = @OrganizationId - AND OU.[UserId] = @UserId - INNER JOIN - [dbo].[Organization] O ON - O.[Id] = OU.[OrganizationId] - AND O.[Id] = C.[OrganizationId] - AND O.[Enabled] = 1 - LEFT JOIN - [dbo].[CollectionCipher] CC ON - CC.[CipherId] = C.[Id] - LEFT JOIN - [dbo].[CollectionUser] CU ON - CU.[CollectionId] = CC.[CollectionId] - 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] = CC.[CollectionId] - AND CG.[GroupId] = GU.[GroupId] - GROUP BY - C.[Id], - C.[OrganizationId] -END -GO From a3ffb5fe9673e6455b995511513cda4e030826b1 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Thu, 9 Jan 2025 16:51:08 -0500 Subject: [PATCH 38/44] Rebased --- .../Controllers/SecurityTaskController.cs | 17 ++++ .../Queries/GetTasksForOrganizationQuery.cs | 44 +++++++++ .../Queries/IGetTasksForOrganizationQuery.cs | 15 +++ .../Repositories/ISecurityTaskRepository.cs | 8 ++ .../Repositories/SecurityTaskRepository.cs | 14 +++ .../Repositories/SecurityTaskRepository.cs | 27 ++++++ ...ecurityTask_ReadByOrganizationIdStatus.sql | 19 ++++ .../GetTasksForOrganizationQueryTests.cs | 92 +++++++++++++++++++ ...1-09_00_SecurityTaskReadByOrganization.sql | 20 ++++ 9 files changed, 256 insertions(+) create mode 100644 src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs create mode 100644 src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs create mode 100644 src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql create mode 100644 test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs create mode 100644 util/Migrator/DbScripts/2025-01-09_00_SecurityTaskReadByOrganization.sql diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 27ee691419b5..88b7aed9c67e 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -20,17 +20,20 @@ public class SecurityTaskController : Controller private readonly IUserService _userService; private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery; private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand; + private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery; private readonly ICreateManyTasksCommand _createManyTasksCommand; public SecurityTaskController( IUserService userService, IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery, IMarkTaskAsCompleteCommand markTaskAsCompleteCommand, + IGetTasksForOrganizationQuery getTasksForOrganizationQuery, ICreateManyTasksCommand createManyTasksCommand) { _userService = userService; _getTaskDetailsForUserQuery = getTaskDetailsForUserQuery; _markTaskAsCompleteCommand = markTaskAsCompleteCommand; + _getTasksForOrganizationQuery = getTasksForOrganizationQuery; _createManyTasksCommand = createManyTasksCommand; } @@ -59,6 +62,20 @@ public async Task Complete(Guid taskId) return NoContent(); } + /// + /// Retrieves security tasks for an organization. Restricted to organization administrators. + /// + /// The organization Id + /// Optional filter for task status. If not provided, returns tasks of all statuses. + [HttpGet("organization")] + public async Task> ListForOrganization( + [FromQuery] Guid organizationId, [FromQuery] SecurityTaskStatus? status) + { + var securityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(organizationId, status); + var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); + return new ListResponseModel(response); + } + /// /// Bulk create security tasks for an organization. /// diff --git a/src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs b/src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs new file mode 100644 index 000000000000..8f71f3cc3bb1 --- /dev/null +++ b/src/Core/Vault/Queries/GetTasksForOrganizationQuery.cs @@ -0,0 +1,44 @@ +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Utilities; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Repositories; +using Microsoft.AspNetCore.Authorization; + +namespace Bit.Core.Vault.Queries; + +public class GetTasksForOrganizationQuery : IGetTasksForOrganizationQuery +{ + private readonly ISecurityTaskRepository _securityTaskRepository; + private readonly IAuthorizationService _authorizationService; + private readonly ICurrentContext _currentContext; + + public GetTasksForOrganizationQuery( + ISecurityTaskRepository securityTaskRepository, + IAuthorizationService authorizationService, + ICurrentContext currentContext + ) + { + _securityTaskRepository = securityTaskRepository; + _authorizationService = authorizationService; + _currentContext = currentContext; + } + + public async Task> GetTasksAsync(Guid organizationId, + SecurityTaskStatus? status = null) + { + var organization = _currentContext.GetOrganization(organizationId); + var userId = _currentContext.UserId; + + if (organization == null || !userId.HasValue) + { + throw new NotFoundException(); + } + + await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, organization, SecurityTaskOperations.ListAllForOrganization); + + return (await _securityTaskRepository.GetManyByOrganizationIdStatusAsync(organizationId, status)).ToList(); + } +} diff --git a/src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs b/src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs new file mode 100644 index 000000000000..c61f379008a7 --- /dev/null +++ b/src/Core/Vault/Queries/IGetTasksForOrganizationQuery.cs @@ -0,0 +1,15 @@ +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; + +namespace Bit.Core.Vault.Queries; + +public interface IGetTasksForOrganizationQuery +{ + /// + /// Retrieves all security tasks for an organization. + /// + /// The Id of the organization + /// Optional filter for task status. If not provided, returns tasks of all statuses + /// A collection of security tasks + Task> GetTasksAsync(Guid organizationId, SecurityTaskStatus? status = null); +} diff --git a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs index a5118426da4a..cc8303345d03 100644 --- a/src/Core/Vault/Repositories/ISecurityTaskRepository.cs +++ b/src/Core/Vault/Repositories/ISecurityTaskRepository.cs @@ -14,6 +14,14 @@ public interface ISecurityTaskRepository : IRepository /// Task> GetManyByUserIdStatusAsync(Guid userId, SecurityTaskStatus? status = null); + /// + /// Retrieves all security tasks for an organization. + /// + /// The id of the organization + /// Optional filter for task status. If not provided, returns tasks of all statuses + /// + Task> GetManyByOrganizationIdStatusAsync(Guid organizationId, SecurityTaskStatus? status = null); + /// /// Creates bulk security tasks for an organization. /// diff --git a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs index 931d7aa6d8cd..3a316fe7c584 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/SecurityTaskRepository.cs @@ -33,6 +33,20 @@ public async Task> GetManyByUserIdStatusAsync(Guid use return results.ToList(); } + /// + public async Task> GetManyByOrganizationIdStatusAsync(Guid organizationId, + SecurityTaskStatus? status = null) + { + await using var connection = new SqlConnection(ConnectionString); + + var results = await connection.QueryAsync( + $"[{Schema}].[SecurityTask_ReadByOrganizationIdStatus]", + new { OrganizationId = organizationId, Status = status }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + /// public async Task> CreateManyAsync(IEnumerable tasks) { diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs index 2a6ad6efc0ec..a3ba2632fe0e 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/SecurityTaskRepository.cs @@ -26,6 +26,33 @@ public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper return data; } + /// + public async Task> GetManyByOrganizationIdStatusAsync(Guid organizationId, + SecurityTaskStatus? status = null) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var query = from st in dbContext.SecurityTasks + join o in dbContext.Organizations + on st.OrganizationId equals o.Id + where + o.Enabled && + st.OrganizationId == organizationId && + (status == null || st.Status == status) + select new Core.Vault.Entities.SecurityTask + { + Id = st.Id, + OrganizationId = st.OrganizationId, + CipherId = st.CipherId, + Status = st.Status, + Type = st.Type, + CreationDate = st.CreationDate, + RevisionDate = st.RevisionDate, + }; + + return await query.OrderByDescending(st => st.CreationDate).ToListAsync(); + } + /// public async Task> CreateManyAsync( IEnumerable tasks) diff --git a/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql new file mode 100644 index 000000000000..19e436e71d18 --- /dev/null +++ b/src/Sql/Vault/dbo/Stored Procedures/SecurityTask/SecurityTask_ReadByOrganizationIdStatus.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[SecurityTask_ReadByOrganizationIdStatus] + @OrganizationId UNIQUEIDENTIFIER, + @Status TINYINT = NULL +AS +BEGIN + SET NOCOUNT ON + + SELECT + ST.* + FROM + [dbo].[SecurityTaskView] ST + INNER JOIN + [dbo].[Organization] O ON O.[Id] = ST.[OrganizationId] + WHERE + ST.[OrganizationId] = @OrganizationId + AND O.[Enabled] = 1 + AND ST.[Status] = COALESCE(@Status, ST.[Status]) + ORDER BY ST.[CreationDate] DESC +END diff --git a/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs new file mode 100644 index 000000000000..59ec7350daa5 --- /dev/null +++ b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs @@ -0,0 +1,92 @@ +using System.Security.Claims; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Authorization.SecurityTasks; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Enums; +using Bit.Core.Vault.Queries; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.AspNetCore.Authorization; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Vault.Queries; + +[SutProviderCustomize] +public class GetTasksForOrganizationQueryTests +{ + [Theory, BitAutoData] + public async Task GetTasksAsync_Success( + Guid userId, CurrentContextOrganization org, + SutProvider sutProvider) + { + var status = SecurityTaskStatus.Pending; + sutProvider.GetDependency().HttpContext.User.Returns(new ClaimsPrincipal()); + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(org.Id).Returns(org); + sutProvider.GetDependency().AuthorizeAsync( + Arg.Any(), org, Arg.Is>( + e => e.Contains(SecurityTaskOperations.ListAllForOrganization) + ) + ).Returns(AuthorizationResult.Success()); + sutProvider.GetDependency().GetManyByOrganizationIdStatusAsync(org.Id, status).Returns(new List() + { + new() { Id = Guid.NewGuid() }, + new() { Id = Guid.NewGuid() }, + }); + + var result = await sutProvider.Sut.GetTasksAsync(org.Id, status); + + Assert.Equal(2, result.Count); + sutProvider.GetDependency().Received(1).AuthorizeAsync( + Arg.Any(), org, Arg.Is>( + e => e.Contains(SecurityTaskOperations.ListAllForOrganization) + ) + ); + sutProvider.GetDependency().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + } + + [Theory, BitAutoData] + public async Task GetTaskAsync_MissingOrg_Failure(Guid userId, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(Arg.Any()).Returns((CurrentContextOrganization)null); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetTasksAsync(Guid.NewGuid())); + } + + [Theory, BitAutoData] + public async Task GetTaskAsync_MissingUser_Failure(CurrentContextOrganization org, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(null as Guid?); + sutProvider.GetDependency().GetOrganization(org.Id).Returns(org); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetTasksAsync(org.Id)); + } + + [Theory, BitAutoData] + public async Task GetTasksAsync_Unauthorized_Failure( + Guid userId, CurrentContextOrganization org, + SutProvider sutProvider) + { + sutProvider.GetDependency().HttpContext.User.Returns(new ClaimsPrincipal()); + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetOrganization(org.Id).Returns(org); + sutProvider.GetDependency().AuthorizeAsync( + Arg.Any(), org, Arg.Is>( + e => e.Contains(SecurityTaskOperations.ListAllForOrganization) + ) + ).Returns(AuthorizationResult.Failed()); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetTasksAsync(org.Id)); + + sutProvider.GetDependency().Received(1).AuthorizeAsync( + Arg.Any(), org, Arg.Is>( + e => e.Contains(SecurityTaskOperations.ListAllForOrganization) + ) + ); + sutProvider.GetDependency().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + } +} diff --git a/util/Migrator/DbScripts/2025-01-09_00_SecurityTaskReadByOrganization.sql b/util/Migrator/DbScripts/2025-01-09_00_SecurityTaskReadByOrganization.sql new file mode 100644 index 000000000000..11774e20920e --- /dev/null +++ b/util/Migrator/DbScripts/2025-01-09_00_SecurityTaskReadByOrganization.sql @@ -0,0 +1,20 @@ +CREATE OR ALTER PROCEDURE [dbo].[SecurityTask_ReadByOrganizationIdStatus] + @OrganizationId UNIQUEIDENTIFIER, + @Status TINYINT = NULL +AS +BEGIN + SET NOCOUNT ON + + SELECT + ST.* + FROM + [dbo].[SecurityTaskView] ST + INNER JOIN + [dbo].[Organization] O ON O.[Id] = ST.[OrganizationId] + WHERE + ST.[OrganizationId] = @OrganizationId + AND O.[Enabled] = 1 + AND ST.[Status] = COALESCE(@Status, ST.[Status]) + ORDER BY ST.[CreationDate] DESC +END +GO From a99afb6db5e28e17e75edef4621469ab40a112be Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 16:55:29 -0800 Subject: [PATCH 39/44] [PM-14378] Introduce GetCipherPermissionsForOrganization query for Dapper CipherRepository --- ..._00_CipherOrganizationPermissionsQuery.sql | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql diff --git a/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql b/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql new file mode 100644 index 000000000000..2eb4de905338 --- /dev/null +++ b/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql @@ -0,0 +1,67 @@ +CREATE OR ALTER PROCEDURE [dbo].[CipherOrganizationPermissions_GetManyByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.[Id], + C.[OrganizationId], + MAX(CASE + WHEN + CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL + THEN 0 + ELSE 1 + END) [Read], + MAX(CASE + WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 1) = 0 + THEN 1 + ELSE 0 + END) [ViewPassword], + MAX(CASE + WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 1) = 0 + THEN 1 + ELSE 0 + END) [Edit], + + MAX(COALESCE(CU.[Manage], CG.[Manage], 0)) [Manage], + CASE + WHEN COUNT(CC.[CollectionId]) > 0 THEN 0 + ELSE 1 + END [Unassigned] + FROM + [dbo].[CipherDetails](@UserId) C + INNER JOIN + [OrganizationUser] OU ON + C.[UserId] IS NULL + AND C.[OrganizationId] = @OrganizationId + AND OU.[UserId] = @UserId + INNER JOIN + [dbo].[Organization] O ON + O.[Id] = OU.[OrganizationId] + AND O.[Id] = C.[OrganizationId] + AND O.[Enabled] = 1 + LEFT JOIN + [dbo].[CollectionCipher] CC ON + CC.[CipherId] = C.[Id] + LEFT JOIN + [dbo].[CollectionUser] CU ON + CU.[CollectionId] = CC.[CollectionId] + 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] = CC.[CollectionId] + AND CG.[GroupId] = GU.[GroupId] + GROUP BY + C.[Id], + C.[OrganizationId] +END +GO From 4cfbd3d631397603ec3c6186c4d84881bb81309b Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Thu, 5 Dec 2024 16:56:26 -0800 Subject: [PATCH 40/44] [PM-14378] Introduce GetCipherPermissionsForOrganization method for Entity Framework --- .../Repositories/Queries/CipherOrganizationPermissionsQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs index 89e70f4f9225..99ca5b9729c7 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs @@ -1,4 +1,4 @@ -using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Models.Data; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; From 2481ca11702badc02f0cbc3612d82b489e45907b Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 11 Dec 2024 15:22:48 -0800 Subject: [PATCH 41/44] [PM-14378] Add unit tests for GetCipherPermissionsForUserQuery --- .../Vault/Queries/GetCipherPermissionsForUserQueryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs index 0afac589251d..2e907f84452c 100644 --- a/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs +++ b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Services; From b4e890a1ef4e7076b286481dde9addb9a31310bd Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Mon, 23 Dec 2024 18:08:04 -0500 Subject: [PATCH 42/44] Completed bulk create action --- src/Api/Vault/Controllers/SecurityTaskController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 88b7aed9c67e..47394284c36c 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -1,4 +1,4 @@ -using Bit.Api.Models.Response; +using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core; From 897285faf15fba91943c1422b96326720497dae0 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Thu, 9 Jan 2025 17:02:03 -0500 Subject: [PATCH 43/44] bumped migration version --- ..._00_CipherOrganizationPermissionsQuery.sql | 67 ------------------- ... 2025-01-09_00_SecurityTaskCreateMany.sql} | 0 2 files changed, 67 deletions(-) delete mode 100644 util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql rename util/Migrator/DbScripts/{2024-12-19_00_SecurityTaskCreateMany.sql => 2025-01-09_00_SecurityTaskCreateMany.sql} (100%) diff --git a/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql b/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql deleted file mode 100644 index 2eb4de905338..000000000000 --- a/util/Migrator/DbScripts/2024-12-05_00_CipherOrganizationPermissionsQuery.sql +++ /dev/null @@ -1,67 +0,0 @@ -CREATE OR ALTER PROCEDURE [dbo].[CipherOrganizationPermissions_GetManyByOrganizationId] - @OrganizationId UNIQUEIDENTIFIER, - @UserId UNIQUEIDENTIFIER -AS -BEGIN - SET NOCOUNT ON - - SELECT - C.[Id], - C.[OrganizationId], - MAX(CASE - WHEN - CU.[CollectionId] IS NULL AND CG.[CollectionId] IS NULL - THEN 0 - ELSE 1 - END) [Read], - MAX(CASE - WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 1) = 0 - THEN 1 - ELSE 0 - END) [ViewPassword], - MAX(CASE - WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 1) = 0 - THEN 1 - ELSE 0 - END) [Edit], - - MAX(COALESCE(CU.[Manage], CG.[Manage], 0)) [Manage], - CASE - WHEN COUNT(CC.[CollectionId]) > 0 THEN 0 - ELSE 1 - END [Unassigned] - FROM - [dbo].[CipherDetails](@UserId) C - INNER JOIN - [OrganizationUser] OU ON - C.[UserId] IS NULL - AND C.[OrganizationId] = @OrganizationId - AND OU.[UserId] = @UserId - INNER JOIN - [dbo].[Organization] O ON - O.[Id] = OU.[OrganizationId] - AND O.[Id] = C.[OrganizationId] - AND O.[Enabled] = 1 - LEFT JOIN - [dbo].[CollectionCipher] CC ON - CC.[CipherId] = C.[Id] - LEFT JOIN - [dbo].[CollectionUser] CU ON - CU.[CollectionId] = CC.[CollectionId] - 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] = CC.[CollectionId] - AND CG.[GroupId] = GU.[GroupId] - GROUP BY - C.[Id], - C.[OrganizationId] -END -GO diff --git a/util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql b/util/Migrator/DbScripts/2025-01-09_00_SecurityTaskCreateMany.sql similarity index 100% rename from util/Migrator/DbScripts/2024-12-19_00_SecurityTaskCreateMany.sql rename to util/Migrator/DbScripts/2025-01-09_00_SecurityTaskCreateMany.sql From e6c7722e327b54a4d36b5120b64f7b2150a495e1 Mon Sep 17 00:00:00 2001 From: gbubemismith Date: Thu, 9 Jan 2025 17:08:05 -0500 Subject: [PATCH 44/44] Fixed lint issue --- src/Api/Vault/Controllers/SecurityTaskController.cs | 2 +- .../Repositories/Queries/CipherOrganizationPermissionsQuery.cs | 2 +- .../Vault/Queries/GetCipherPermissionsForUserQueryTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Api/Vault/Controllers/SecurityTaskController.cs b/src/Api/Vault/Controllers/SecurityTaskController.cs index 47394284c36c..88b7aed9c67e 100644 --- a/src/Api/Vault/Controllers/SecurityTaskController.cs +++ b/src/Api/Vault/Controllers/SecurityTaskController.cs @@ -1,4 +1,4 @@ -using Bit.Api.Models.Response; +using Bit.Api.Models.Response; using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Response; using Bit.Core; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs index 99ca5b9729c7..89e70f4f9225 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/Queries/CipherOrganizationPermissionsQuery.cs @@ -1,4 +1,4 @@ -using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Models.Data; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; diff --git a/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs index 2e907f84452c..0afac589251d 100644 --- a/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs +++ b/test/Core.Test/Vault/Queries/GetCipherPermissionsForUserQueryTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Data.Organizations; using Bit.Core.Services;