diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs index dff766da12dd..aa21b18bef91 100644 --- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs +++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs @@ -137,7 +137,7 @@ public EmergencyAccessViewResponseModel( new CipherResponseModel( cipher, user, - organizationAbilities: null, // Emergency access only retrieves personal ciphers so organizationAbilities is not needed + null, // Emergency access only retrieves personal ciphers so organizationAbility is not needed globalSettings)); } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index eb658eacf1d1..cae0ac42415e 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -14,6 +14,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -91,9 +92,9 @@ public async Task Get(Guid id) throw new NotFoundException(); } - var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var organizationAbility = await GetOrganizationAbilityAsync(cipher); - return new CipherResponseModel(cipher, user, organizationAbilities, _globalSettings); + return new CipherResponseModel(cipher, user, organizationAbility, _globalSettings); } [HttpGet("{id}/admin")] @@ -122,9 +123,9 @@ public async Task GetDetails(Guid id) throw new NotFoundException(); } - var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var organizationAbility = await GetOrganizationAbilityAsync(cipher); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); - return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers); + return new CipherDetailsResponseModel(cipher, user, organizationAbility, _globalSettings, collectionCiphers); } [HttpGet("{id}/full-details")] @@ -147,16 +148,17 @@ public async Task> GetAll() var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id); collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); } - var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var organizationAbilities = await GetOrganizationAbilitiesAsync(ciphers); var responses = ciphers.Select(cipher => new CipherDetailsResponseModel( cipher, user, - organizationAbilities, + GetOrganizationAbility(cipher, organizationAbilities), _globalSettings, - collectionCiphersGroupDict)).ToList(); + collectionCiphersGroupDict)).ToArray(); return new ListResponseModel(responses); } + [HttpPost("")] public async Task Post([FromBody] CipherRequestModel model) { @@ -179,11 +181,7 @@ public async Task Post([FromBody] CipherRequestModel model) } await _cipherService.SaveDetailsAsync(cipher, user.Id, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue); - var response = new CipherResponseModel( - cipher, - user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), - _globalSettings); + var response = new CipherResponseModel(cipher, user, await GetOrganizationAbilityAsync(cipher), _globalSettings); return response; } @@ -274,11 +272,7 @@ public async Task Put(Guid id, [FromBody] CipherRequestMode await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds); - var response = new CipherResponseModel( - cipher, - user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), - _globalSettings); + var response = new CipherResponseModel(cipher, user, await GetOrganizationAbilityAsync(cipher), _globalSettings); return response; } @@ -373,13 +367,9 @@ public async Task> GetAssignedOrga } var user = await _userService.GetUserByPrincipalAsync(User); - var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId); var responses = ciphers.Select(cipher => - new CipherDetailsResponseModel( - cipher, - user, - organizationAbilities, - _globalSettings)); + new CipherDetailsResponseModel(cipher, user, organizationAbility, _globalSettings)); return new ListResponseModel(responses); } @@ -719,11 +709,7 @@ public async Task PutPartial(Guid id, [FromBody] CipherPart await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite); var updatedCipher = await GetByIdAsync(id, user.Id); - var response = new CipherResponseModel( - updatedCipher, - user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), - _globalSettings); + var response = new CipherResponseModel(updatedCipher, user, await GetOrganizationAbilityAsync(updatedCipher), _globalSettings); return response; } @@ -762,11 +748,7 @@ public async Task PutShare(Guid id, [FromBody] CipherShareR model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate); var sharedCipher = await GetByIdAsync(id, user.Id); - var response = new CipherResponseModel( - sharedCipher, - user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), - _globalSettings); + var response = new CipherResponseModel(sharedCipher, user, await GetOrganizationAbilityAsync(sharedCipher), _globalSettings); return response; } @@ -794,12 +776,7 @@ await _cipherService.SaveCollectionsAsync(cipher, var updatedCipher = await GetByIdAsync(id, user.Id); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id); - return new CipherDetailsResponseModel( - updatedCipher, - user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), - _globalSettings, - collectionCiphers); + return new CipherDetailsResponseModel(updatedCipher, user, await GetOrganizationAbilityAsync(updatedCipher), _globalSettings, collectionCiphers); } [HttpPost("{id}/collections")] @@ -832,12 +809,7 @@ await _cipherService.SaveCollectionsAsync(cipher, Unavailable = updatedCipher is null, Cipher = updatedCipher is null ? null - : new CipherDetailsResponseModel( - updatedCipher, - user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), - _globalSettings, - collectionCiphers) + : new CipherDetailsResponseModel(updatedCipher, user, await GetOrganizationAbilityAsync(updatedCipher), _globalSettings, collectionCiphers) }; return response; } @@ -920,11 +892,8 @@ public async Task PutArchive(Guid id) throw new BadRequestException("Cipher was not archived. Ensure the provided ID is correct and you have permission to archive it."); } - return new CipherResponseModel(archivedCipherOrganizationDetails.First(), - await _userService.GetUserByPrincipalAsync(User), - await _applicationCacheService.GetOrganizationAbilitiesAsync(), - _globalSettings - ); + var archivedCipher = archivedCipherOrganizationDetails.First(); + return new CipherResponseModel(archivedCipher, await _userService.GetUserByPrincipalAsync(User), await GetOrganizationAbilityAsync(archivedCipher), _globalSettings); } [HttpPut("archive")] @@ -948,12 +917,9 @@ public async Task> PutArchiveMany([FromBo throw new BadRequestException("No ciphers were archived. Ensure the provided IDs are correct and you have permission to archive them."); } - var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); - var responses = archivedCiphers.Select(c => new CipherResponseModel(c, - user, - organizationAbilities, - _globalSettings - )); + var organizationAbilities = await GetOrganizationAbilitiesAsync(archivedCiphers); + var responses = archivedCiphers.Select(cipher => + new CipherResponseModel(cipher, user, GetOrganizationAbility(cipher, organizationAbilities), _globalSettings)).ToArray(); return new ListResponseModel(responses); } @@ -1128,9 +1094,10 @@ public async Task PutUnarchive(Guid id) throw new BadRequestException("Cipher was not unarchived. Ensure the provided ID is correct and you have permission to archive it."); } - return new CipherResponseModel(unarchivedCipherDetails.First(), + var unarchivedCipher = unarchivedCipherDetails.First(); + return new CipherResponseModel(unarchivedCipher, await _userService.GetUserByPrincipalAsync(User), - await _applicationCacheService.GetOrganizationAbilitiesAsync(), + await GetOrganizationAbilityAsync(unarchivedCipher), _globalSettings ); } @@ -1146,7 +1113,6 @@ public async Task> PutUnarchiveMany([From var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User); - var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); var cipherIdsToUnarchive = new HashSet(model.Ids); @@ -1157,7 +1123,9 @@ public async Task> PutUnarchiveMany([From throw new BadRequestException("Ciphers were not unarchived. Ensure the provided ID is correct and you have permission to archive it."); } - var responses = unarchivedCipherOrganizationDetails.Select(c => new CipherResponseModel(c, user, organizationAbilities, _globalSettings)); + var organizationAbilities = await GetOrganizationAbilitiesAsync(unarchivedCipherOrganizationDetails); + var responses = unarchivedCipherOrganizationDetails.Select(cipher => + new CipherResponseModel(cipher, user, GetOrganizationAbility(cipher, organizationAbilities), _globalSettings)).ToArray(); return new ListResponseModel(responses); } @@ -1176,7 +1144,7 @@ public async Task PutRestore(Guid id) return new CipherResponseModel( cipher, user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), + await GetOrganizationAbilityAsync(cipher), _globalSettings); } @@ -1369,15 +1337,17 @@ await _cipherRepository.GetOrganizationDetailsByIdAsync(id) : var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher, request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id, request.LastKnownRevisionDate); + + var cipherDetails = (CipherDetails)cipher; return new AttachmentUploadDataResponseModel { AttachmentId = attachmentId, Url = uploadUrl, FileUploadType = _attachmentStorageService.FileUploadType, CipherResponse = request.AdminRequest ? null : new CipherResponseModel( - (CipherDetails)cipher, + cipherDetails, user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), + await GetOrganizationAbilityAsync(cipherDetails), _globalSettings), CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null, }; @@ -1453,7 +1423,7 @@ await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, return new CipherResponseModel( cipher, user, - await _applicationCacheService.GetOrganizationAbilitiesAsync(), + await GetOrganizationAbilityAsync(cipher), _globalSettings); } @@ -1696,4 +1666,35 @@ private async Task GetByIdAsync(Guid cipherId, Guid userId) return lastKnownRevisionDate; } +#nullable enable + + private async Task GetOrganizationAbilityAsync(CipherDetails cipher) + { + if (cipher.OrganizationId.HasValue) + { + return await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value); + } + return null; + } + + private static OrganizationAbility? GetOrganizationAbility(CipherDetails cipher, IDictionary organizationAbilities) => + cipher.OrganizationId.HasValue && organizationAbilities.TryGetValue(cipher.OrganizationId.Value, out var ability) ? ability : null; + + private async Task> GetOrganizationAbilitiesAsync( + IEnumerable ciphers) + { + var orgIds = ciphers + .Where(c => c.OrganizationId.HasValue) + .Select(c => c.OrganizationId!.Value) + .Distinct() + .ToList(); + + if (orgIds.Count == 0) + { + return new Dictionary(); + } + + return await _applicationCacheService.GetOrganizationAbilitiesAsync(orgIds); + } + } diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index b186e4b60116..a1186b04d375 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -15,6 +15,7 @@ using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -123,7 +124,7 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id); - var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var organizationAbilities = await GetOrganizationAbilitiesAsync(ciphers); var webAuthnCredentials = _featureService.IsEnabled(FeatureFlagKeys.PM2035PasskeyUnlock) ? await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id) : []; @@ -141,6 +142,24 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, return response; } + private async Task> GetOrganizationAbilitiesAsync(ICollection ciphers) + { + var orgIds = ciphers + .Where(c => c.OrganizationId.HasValue) + .Select(c => c.OrganizationId!.Value) + .Distinct() + .ToList(); + + if (orgIds.Count == 0) + { + return new Dictionary(); + } + + var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(orgIds); + + return organizationAbilities; + } + private ICollection FilterSSHKeys(ICollection ciphers) { if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride)) diff --git a/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs b/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs index b3082fc689af..879bbf86ea46 100644 --- a/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherPermissionsResponseModel.cs @@ -16,10 +16,9 @@ public record CipherPermissionsResponseModel public CipherPermissionsResponseModel( User user, CipherDetails cipherDetails, - IDictionary organizationAbilities) + OrganizationAbility organizationAbility) { - OrganizationAbility organizationAbility = null; - if (cipherDetails.OrganizationId.HasValue && !organizationAbilities.TryGetValue(cipherDetails.OrganizationId.Value, out organizationAbility)) + if (cipherDetails.OrganizationId.HasValue && organizationAbility?.Id != cipherDetails.OrganizationId) { throw new Exception("OrganizationAbility not found for organization cipher."); } diff --git a/src/Api/Vault/Models/Response/CipherResponseModel.cs b/src/Api/Vault/Models/Response/CipherResponseModel.cs index ac11eb3cd302..d76f144dd185 100644 --- a/src/Api/Vault/Models/Response/CipherResponseModel.cs +++ b/src/Api/Vault/Models/Response/CipherResponseModel.cs @@ -1,5 +1,4 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable + using System.Text.Json; using Bit.Core.Entities; @@ -11,6 +10,8 @@ using Bit.Core.Vault.Models.Data; namespace Bit.Api.Vault.Models.Response; +// FIXME: Update this file to be null safe and then delete the line below +#nullable disable public class CipherMiniResponseModel : ResponseModel { @@ -111,13 +112,13 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo public CipherRepromptType Reprompt { get; set; } public string Key { get; set; } } - +#nullable enable public class CipherResponseModel : CipherMiniResponseModel { public CipherResponseModel( CipherDetails cipher, User user, - IDictionary organizationAbilities, + OrganizationAbility? organizationAbility, IGlobalSettings globalSettings, string obj = "cipher") : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj) @@ -127,7 +128,7 @@ public CipherResponseModel( Edit = cipher.Edit; ArchivedDate = cipher.ArchivedDate; ViewPassword = cipher.ViewPassword; - Permissions = new CipherPermissionsResponseModel(user, cipher, organizationAbilities); + Permissions = new CipherPermissionsResponseModel(user, cipher, organizationAbility); } public Guid? FolderId { get; set; } @@ -143,10 +144,10 @@ public class CipherDetailsResponseModel : CipherResponseModel public CipherDetailsResponseModel( CipherDetails cipher, User user, - IDictionary organizationAbilities, + OrganizationAbility? organizationAbility, GlobalSettings globalSettings, IDictionary> collectionCiphers, string obj = "cipherDetails") - : base(cipher, user, organizationAbilities, globalSettings, obj) + : base(cipher, user, organizationAbility, globalSettings, obj) { if (collectionCiphers?.TryGetValue(cipher.Id, out var collectionCipher) ?? false) { @@ -161,10 +162,10 @@ public CipherDetailsResponseModel( public CipherDetailsResponseModel( CipherDetails cipher, User user, - IDictionary organizationAbilities, + OrganizationAbility? organizationAbility, GlobalSettings globalSettings, IEnumerable collectionCiphers, string obj = "cipherDetails") - : base(cipher, user, organizationAbilities, globalSettings, obj) + : base(cipher, user, organizationAbility, globalSettings, obj) { CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? []; } @@ -172,10 +173,10 @@ public CipherDetailsResponseModel( public CipherDetailsResponseModel( CipherDetailsWithCollections cipher, User user, - IDictionary organizationAbilities, + OrganizationAbility? organizationAbility, GlobalSettings globalSettings, string obj = "cipherDetails") - : base(cipher, user, organizationAbilities, globalSettings, obj) + : base(cipher, user, organizationAbility, globalSettings, obj) { CollectionIds = cipher.CollectionIds ?? []; } diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index b292a10eb381..0b74276597d1 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -53,7 +53,7 @@ public SyncResponseModel( new CipherDetailsResponseModel( cipher, user, - organizationAbilities, + GetOrganizationAbility(cipher, organizationAbilities), globalSettings, collectionCiphersDict)); Collections = collections?.Select( @@ -88,16 +88,30 @@ public SyncResponseModel( } : null, WebAuthnPrfOptions = webAuthnPrfOptions.Length > 0 ? webAuthnPrfOptions : null, - V2UpgradeToken = V2UpgradeTokenData.FromJson(user.V2UpgradeToken) is { } data + V2UpgradeToken = V2UpgradeTokenData.FromJson(user.V2UpgradeToken) is { } tokenData ? new V2UpgradeTokenResponseModel { - WrappedUserKey1 = data.WrappedUserKey1, - WrappedUserKey2 = data.WrappedUserKey2 + WrappedUserKey1 = tokenData.WrappedUserKey1, + WrappedUserKey2 = tokenData.WrappedUserKey2 } : null }; } +#nullable enable + + private static OrganizationAbility? GetOrganizationAbility(CipherDetails cipherDetails, IDictionary organizationAbilities) + { + if (!cipherDetails.OrganizationId.HasValue) + { + return null; + } + organizationAbilities.TryGetValue(cipherDetails.OrganizationId.Value, out var organizationAbility); + return organizationAbility; + } + +#nullable disable + public ProfileResponseModel Profile { get; set; } public IEnumerable Folders { get; set; } public IEnumerable Collections { get; set; } diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 85e6e7ad9367..44f81f54c72a 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -99,7 +99,7 @@ public async Task PutCollections_vNextShouldSaveUpdatedCipher(Guid id, CipherCol sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); - sutProvider.GetDependency().GetOrganizationAbilitiesAsync().Returns(new Dictionary { { cipherDetails.OrganizationId.Value, new OrganizationAbility { Id = cipherDetails.OrganizationId.Value } } }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); var cipherService = sutProvider.GetDependency(); await sutProvider.Sut.PutCollections_vNext(id, model); @@ -115,7 +115,7 @@ public async Task PutCollections_vNextReturnOptionalDetailsCipherUnavailableFals sutProvider.GetDependency().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); sutProvider.GetDependency().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection)new List()); - sutProvider.GetDependency().GetOrganizationAbilitiesAsync().Returns(new Dictionary { { cipherDetails.OrganizationId.Value, new OrganizationAbility { Id = cipherDetails.OrganizationId.Value } } }); + sutProvider.GetDependency().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value }); var result = await sutProvider.Sut.PutCollections_vNext(id, model); @@ -2004,11 +2004,8 @@ public async Task PutShare_WithNullFolderAndFalseFavorite_UpdatesFieldsCorrectly .Returns(sharedCipher); sutProvider.GetDependency() - .GetOrganizationAbilitiesAsync() - .Returns(new Dictionary - { - { organizationId, new OrganizationAbility { Id = organizationId } } - }); + .GetOrganizationAbilityAsync(organizationId) + .Returns(new OrganizationAbility { Id = organizationId }); var result = await sutProvider.Sut.PutShare(cipherId, model); @@ -2082,11 +2079,8 @@ public async Task PutShare_WithFolderAndFavoriteSet_AddsUserSpecificFields( .Returns(sharedCipher); sutProvider.GetDependency() - .GetOrganizationAbilitiesAsync() - .Returns(new Dictionary - { - { organizationId, new OrganizationAbility { Id = organizationId } } - }); + .GetOrganizationAbilityAsync(organizationId) + .Returns(new OrganizationAbility { Id = organizationId }); var result = await sutProvider.Sut.PutShare(cipherId, model); @@ -2161,11 +2155,8 @@ public async Task PutShare_UpdateExistingFolderAndFavorite_UpdatesUserSpecificFi .Returns(sharedCipher); sutProvider.GetDependency() - .GetOrganizationAbilitiesAsync() - .Returns(new Dictionary - { - { organizationId, new OrganizationAbility { Id = organizationId } } - }); + .GetOrganizationAbilityAsync(organizationId) + .Returns(new OrganizationAbility { Id = organizationId }); var result = await sutProvider.Sut.PutShare(cipherId, model);