diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 33efeb5fa5..8af56710cd 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -85,6 +85,25 @@ namespace Bit.Api.Controllers return new ListResponseModel(responses); } + [HttpGet("organization-details")] + public async Task> GetOrganizationSubvaults(string organizationId) + { + var userId = _userService.GetProperUserId(User).Value; + var orgIdGuid = new Guid(organizationId); + if(!_currentContext.OrganizationAdmin(orgIdGuid)) + { + throw new NotFoundException(); + } + + var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(orgIdGuid); + + var subvaultCiphers = await _subvaultCipherRepository.GetManyByOrganizationIdAsync(orgIdGuid); + var subvaultCiphersGroupDict = subvaultCiphers.GroupBy(s => s.CipherId).ToDictionary(s => s.Key); + + var responses = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, subvaultCiphersGroupDict)); + return new ListResponseModel(responses); + } + [Obsolete] [HttpGet("history")] public Task Get(DateTime since) @@ -116,7 +135,7 @@ namespace Bit.Api.Controllers { throw new NotFoundException(); } - + await _cipherService.UpdatePartialAsync(new Guid(id), userId, cipher.FolderId, !cipher.Favorite); } diff --git a/src/Core/Models/Api/Response/CipherResponseModel.cs b/src/Core/Models/Api/Response/CipherResponseModel.cs index 716c31a17b..616723f416 100644 --- a/src/Core/Models/Api/Response/CipherResponseModel.cs +++ b/src/Core/Models/Api/Response/CipherResponseModel.cs @@ -6,9 +6,9 @@ using System.Linq; namespace Bit.Core.Models.Api { - public class CipherResponseModel : ResponseModel + public class CipherMiniResponseModel : ResponseModel { - public CipherResponseModel(CipherDetails cipher, string obj = "cipher") + public CipherMiniResponseModel(Cipher cipher, string obj = "cipherMini") : base(obj) { if(cipher == null) @@ -20,8 +20,6 @@ namespace Bit.Core.Models.Api Type = cipher.Type; RevisionDate = cipher.RevisionDate; OrganizationId = cipher.OrganizationId?.ToString(); - FolderId = cipher.FolderId?.ToString(); - Favorite = cipher.Favorite; switch(cipher.Type) { @@ -35,13 +33,24 @@ namespace Bit.Core.Models.Api public string Id { get; set; } public string OrganizationId { get; set; } - public string FolderId { get; set; } public Enums.CipherType Type { get; set; } - public bool Favorite { get; set; } public dynamic Data { get; set; } public DateTime RevisionDate { get; set; } } + public class CipherResponseModel : CipherMiniResponseModel + { + public CipherResponseModel(CipherDetails cipher, string obj = "cipher") + : base(cipher, obj) + { + FolderId = cipher.FolderId?.ToString(); + Favorite = cipher.Favorite; + } + + public string FolderId { get; set; } + public bool Favorite { get; set; } + } + public class CipherDetailsResponseModel : CipherResponseModel { public CipherDetailsResponseModel(CipherDetails cipher, @@ -68,6 +77,25 @@ namespace Bit.Core.Models.Api public IEnumerable SubvaultIds { get; set; } } + public class CipherMiniDetailsResponseModel : CipherMiniResponseModel + { + public CipherMiniDetailsResponseModel(Cipher cipher, + IDictionary> subvaultCiphers, string obj = "cipherMiniDetails") + : base(cipher, obj) + { + if(subvaultCiphers.ContainsKey(cipher.Id)) + { + SubvaultIds = subvaultCiphers[cipher.Id].Select(s => s.SubvaultId); + } + else + { + SubvaultIds = new Guid[] { }; + } + } + + public IEnumerable SubvaultIds { get; set; } + } + public class CipherFullDetailsResponseModel : CipherDetailsResponseModel { public CipherFullDetailsResponseModel(CipherFullDetails cipher, IEnumerable subvaultCiphers) diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index 585ef80dad..d6c3b3f7dc 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -12,6 +12,7 @@ namespace Bit.Core.Repositories Task GetFullDetailsByIdAsync(Guid id, Guid userId); Task> GetManyByUserIdAsync(Guid userId); Task> GetManyByUserIdHasSubvaultsAsync(Guid userId); + Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByTypeAndUserIdAsync(Enums.CipherType type, Guid userId); Task CreateAsync(CipherDetails cipher); Task ReplaceAsync(CipherDetails cipher); diff --git a/src/Core/Repositories/ISubvaultCipherRepository.cs b/src/Core/Repositories/ISubvaultCipherRepository.cs index d163b5f463..a9d54c8daa 100644 --- a/src/Core/Repositories/ISubvaultCipherRepository.cs +++ b/src/Core/Repositories/ISubvaultCipherRepository.cs @@ -8,6 +8,7 @@ namespace Bit.Core.Repositories public interface ISubvaultCipherRepository { Task> GetManyByUserIdAsync(Guid userId); + Task> GetManyByOrganizationIdAsync(Guid organizationId); Task> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId); Task UpdateSubvaultsAsync(Guid cipherId, Guid userId, IEnumerable subvaultIds); } diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index 8f342a6a5e..98f6ff0b51 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -76,6 +76,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[Cipher_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task> GetManyByTypeAndUserIdAsync(Enums.CipherType type, Guid userId) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs b/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs index 6330511437..d6e6f0e4c1 100644 --- a/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs +++ b/src/Core/Repositories/SqlServer/SubvaultCipherRepository.cs @@ -33,6 +33,19 @@ namespace Bit.Core.Repositories.SqlServer } } + public async Task> GetManyByOrganizationIdAsync(Guid organizationId) + { + using(var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[SubvaultCipher_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task> GetManyByUserIdCipherIdAsync(Guid userId, Guid cipherId) { using(var connection = new SqlConnection(ConnectionString)) diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 33560ade9a..b84f75cdeb 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -174,5 +174,7 @@ + + \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Cipher_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/Cipher_ReadByOrganizationId.sql new file mode 100644 index 0000000000..ee05ffc2fe --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/Cipher_ReadByOrganizationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[Cipher_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[CipherView] + WHERE + [OrganizationId] = @OrganizationId +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/SubvaultCipher_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/SubvaultCipher_ReadByOrganizationId.sql new file mode 100644 index 0000000000..9fe445d0d3 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/SubvaultCipher_ReadByOrganizationId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[SubvaultCipher_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + SC.* + FROM + [dbo].[SubvaultCipher] SC + INNER JOIN + [dbo].[Subvault] S ON S.[Id] = SC.[SubvaultId] + WHERE + S.[OrganizationId] = @OrganizationId +END \ No newline at end of file diff --git a/src/Sql/dbo/Tables/Cipher.sql b/src/Sql/dbo/Tables/Cipher.sql index 8605e8c3ff..8994eeae62 100644 --- a/src/Sql/dbo/Tables/Cipher.sql +++ b/src/Sql/dbo/Tables/Cipher.sql @@ -18,6 +18,12 @@ CREATE NONCLUSTERED INDEX [IX_Cipher_UserId_Type] ON [dbo].[Cipher]([UserId] ASC, [Type] ASC); +GO +CREATE NONCLUSTERED INDEX [IX_Cipher_OrganizationId_Type] + ON [dbo].[Cipher]([OrganizationId] ASC, [Type] ASC) + WHERE [OrganizationId] IS NOT NULL; + + GO CREATE TRIGGER [dbo].[Cipher_Inserted] ON [dbo].[Cipher] AFTER INSERT