diff --git a/src/Api/Controllers/CiphersController.cs b/src/Api/Controllers/CiphersController.cs index 7cc45b9f0..495f940a8 100644 --- a/src/Api/Controllers/CiphersController.cs +++ b/src/Api/Controllers/CiphersController.cs @@ -224,11 +224,12 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - IEnumerable orgCiphers; - if (await _currentContext.OrganizationAdmin(orgIdGuid)) + IEnumerable orgCiphers; + if (await _currentContext.OrganizationOwner(orgIdGuid)) { - // Admins, Owners and Providers can access all items even if not assigned to them - orgCiphers = await _cipherRepository.GetManyByOrganizationIdAsync(orgIdGuid); + // User may be a Provider for the organization, in which case GetManyByUserIdAsync won't return any results + // But they have access to all organization ciphers, so we can safely get by orgId instead + orgCiphers = await _cipherRepository.GetManyOrganizationDetailsByOrganizationIdAsync(orgIdGuid); } else { @@ -245,7 +246,8 @@ namespace Bit.Api.Controllers var responses = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, - collectionCiphersGroupDict)); + collectionCiphersGroupDict, c.OrganizationUseTotp)); + var providerId = await _currentContext.ProviderIdForOrg(orgIdGuid); if (providerId.HasValue) diff --git a/src/Api/Models/Response/CipherResponseModel.cs b/src/Api/Models/Response/CipherResponseModel.cs index 6ebf671e6..98e53abef 100644 --- a/src/Api/Models/Response/CipherResponseModel.cs +++ b/src/Api/Models/Response/CipherResponseModel.cs @@ -132,8 +132,8 @@ namespace Bit.Api.Models.Response public class CipherMiniDetailsResponseModel : CipherMiniResponseModel { public CipherMiniDetailsResponseModel(Cipher cipher, GlobalSettings globalSettings, - IDictionary> collectionCiphers, string obj = "cipherMiniDetails") - : base(cipher, globalSettings, false, obj) + IDictionary> collectionCiphers, bool orgUseTotp, string obj = "cipherMiniDetails") + : base(cipher, globalSettings, orgUseTotp, obj) { if (collectionCiphers?.ContainsKey(cipher.Id) ?? false) { diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index 51a0bab40..6b03730bb 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -11,6 +11,7 @@ namespace Bit.Core.Repositories { Task GetByIdAsync(Guid id, Guid userId); Task GetOrganizationDetailsByIdAsync(Guid id); + Task> GetManyOrganizationDetailsByOrganizationIdAsync(Guid organizationId); Task GetCanEditByIdAsync(Guid userId, Guid cipherId); Task> GetManyByUserIdAsync(Guid userId, bool withOrganizations = true); Task> GetManyByOrganizationIdAsync(Guid organizationId); diff --git a/src/Infrastructure.Dapper/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Repositories/CipherRepository.cs index deb8158aa..ece35da73 100644 --- a/src/Infrastructure.Dapper/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/CipherRepository.cs @@ -50,6 +50,20 @@ namespace Bit.Infrastructure.Dapper.Repositories } } + public async Task> GetManyOrganizationDetailsByOrganizationIdAsync( + Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[CipherOrganizationDetails_ReadByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + public async Task GetCanEditByIdAsync(Guid userId, Guid cipherId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Repositories/CipherRepository.cs index f92d8abbc..381111e40 100644 --- a/src/Infrastructure.EntityFramework/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/CipherRepository.cs @@ -259,6 +259,18 @@ namespace Bit.Infrastructure.EntityFramework.Repositories } } + public async Task> GetManyOrganizationDetailsByOrganizationIdAsync( + Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = new CipherOrganizationDetailsReadByIdQuery(organizationId); + var data = await query.Run(dbContext).ToListAsync(); + return data; + } + } + public async Task GetCanEditByIdAsync(Guid userId, Guid cipherId) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/CipherOrganizationDetailsReadByOrgizationIdQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/CipherOrganizationDetailsReadByOrgizationIdQuery.cs new file mode 100644 index 000000000..0a02ccfb7 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/CipherOrganizationDetailsReadByOrgizationIdQuery.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using Core.Models.Data; + +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries +{ + public class CipherOrganizationDetailsReadByOrgizationIdQuery : IQuery + { + private readonly Guid _organizationId; + + public CipherOrganizationDetailsReadByOrgizationIdQuery(Guid organizationId) + { + _organizationId = organizationId; + } + public virtual IQueryable Run(DatabaseContext dbContext) + { + var query = from c in dbContext.Ciphers + join o in dbContext.Organizations + on c.OrganizationId equals o.Id into o_g + from o in o_g.DefaultIfEmpty() + where c.OrganizationId == _organizationId + select new CipherOrganizationDetails + { + Id = c.Id, + UserId = c.UserId, + OrganizationId = c.OrganizationId, + Type = c.Type, + Data = c.Data, + Favorites = c.Favorites, + Folders = c.Folders, + Attachments = c.Attachments, + CreationDate = c.CreationDate, + RevisionDate = c.RevisionDate, + DeletedDate = c.DeletedDate, + OrganizationUseTotp = o.UseTotp, + }; + return query; + } + } +} diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index ee3768d8a..758701a30 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -390,5 +390,6 @@ + diff --git a/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationId.sql new file mode 100644 index 000000000..2bbbf298b --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/CipherOrganizationDetails_ReadByOrganizationId.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + C.*, + CASE + WHEN O.[UseTotp] = 1 THEN 1 + ELSE 0 + END [OrganizationUseTotp] + FROM + [dbo].[CipherView] C + LEFT JOIN + [dbo].[OrganizationView] O ON O.[Id] = C.[OrganizationId] + WHERE + C.[OrganizationId] = @OrganizationId +END diff --git a/util/Migrator/DbScripts/2022-05-31_00_CipherOrganizationDetails.sql b/util/Migrator/DbScripts/2022-05-31_00_CipherOrganizationDetails.sql new file mode 100644 index 000000000..829602f26 --- /dev/null +++ b/util/Migrator/DbScripts/2022-05-31_00_CipherOrganizationDetails.sql @@ -0,0 +1,27 @@ +--CipherOrganizationDetails_ReadByOrganizationId +IF OBJECT_ID('[dbo].[CipherOrganizationDetails_ReadByOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[CipherOrganizationDetails_ReadByOrganizationId] +END +GO + +CREATE PROCEDURE [dbo].[CipherOrganizationDetails_ReadByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + +SELECT + C.*, + CASE + WHEN O.[UseTotp] = 1 THEN 1 + ELSE 0 + END [OrganizationUseTotp] +FROM + [dbo].[CipherView] C + LEFT JOIN + [dbo].[OrganizationView] O ON O.[Id] = C.[OrganizationId] +WHERE + C.[OrganizationId] = @OrganizationId +END +GO \ No newline at end of file