[PM-33041] Organization Ability: Refactor CipherResponseModel (#7202)

This commit is contained in:
Jimmy Vo
2026-03-26 11:35:41 -04:00
committed by GitHub
parent db1fff211e
commit 34aba63b90
7 changed files with 126 additions and 101 deletions

View File

@@ -137,7 +137,7 @@ public class EmergencyAccessViewResponseModel : ResponseModel
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));
}

View File

@@ -14,6 +14,7 @@ using Bit.Core.Context;
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 class CiphersController : Controller
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 class CiphersController : Controller
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 class CiphersController : Controller
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<CipherDetailsResponseModel>(responses);
}
[HttpPost("")]
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
{
@@ -179,11 +181,7 @@ public class CiphersController : Controller
}
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 class CiphersController : Controller
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 class CiphersController : Controller
}
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<CipherDetailsResponseModel>(responses);
}
@@ -719,11 +709,7 @@ public class CiphersController : Controller
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 class CiphersController : Controller
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 @@ public class CiphersController : Controller
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 @@ public class CiphersController : Controller
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 class CiphersController : Controller
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 class CiphersController : Controller
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<CipherResponseModel>(responses);
}
@@ -1128,9 +1094,10 @@ public class CiphersController : Controller
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 class CiphersController : Controller
var userId = _userService.GetProperUserId(User).Value;
var user = await _userService.GetUserByPrincipalAsync(User);
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var cipherIdsToUnarchive = new HashSet<Guid>(model.Ids);
@@ -1157,7 +1123,9 @@ public class CiphersController : Controller
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<CipherResponseModel>(responses);
}
@@ -1176,7 +1144,7 @@ public class CiphersController : Controller
return new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
await GetOrganizationAbilityAsync(cipher),
_globalSettings);
}
@@ -1369,15 +1337,17 @@ public class CiphersController : Controller
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,
};
@@ -1470,7 +1440,7 @@ public class CiphersController : Controller
return new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
await GetOrganizationAbilityAsync(cipher),
_globalSettings);
}
@@ -1713,4 +1683,35 @@ public class CiphersController : Controller
return lastKnownRevisionDate;
}
#nullable enable
private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CipherDetails cipher)
{
if (cipher.OrganizationId.HasValue)
{
return await _applicationCacheService.GetOrganizationAbilityAsync(cipher.OrganizationId.Value);
}
return null;
}
private static OrganizationAbility? GetOrganizationAbility(CipherDetails cipher, IDictionary<Guid, OrganizationAbility> organizationAbilities) =>
cipher.OrganizationId.HasValue && organizationAbilities.TryGetValue(cipher.OrganizationId.Value, out var ability) ? ability : null;
private async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync(
IEnumerable<CipherDetails> ciphers)
{
var orgIds = ciphers
.Where(c => c.OrganizationId.HasValue)
.Select(c => c.OrganizationId!.Value)
.Distinct()
.ToList();
if (orgIds.Count == 0)
{
return new Dictionary<Guid, OrganizationAbility>();
}
return await _applicationCacheService.GetOrganizationAbilitiesAsync(orgIds);
}
}

View File

@@ -15,6 +15,7 @@ using Bit.Core.Exceptions;
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 @@ public class SyncController : Controller
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 @@ public class SyncController : Controller
return response;
}
private async Task<IDictionary<Guid, OrganizationAbility>> GetOrganizationAbilitiesAsync(ICollection<CipherDetails> ciphers)
{
var orgIds = ciphers
.Where(c => c.OrganizationId.HasValue)
.Select(c => c.OrganizationId!.Value)
.Distinct()
.ToList();
if (orgIds.Count == 0)
{
return new Dictionary<Guid, OrganizationAbility>();
}
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(orgIds);
return organizationAbilities;
}
private ICollection<CipherDetails> FilterSSHKeys(ICollection<CipherDetails> ciphers)
{
if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride))

View File

@@ -16,10 +16,9 @@ public record CipherPermissionsResponseModel
public CipherPermissionsResponseModel(
User user,
CipherDetails cipherDetails,
IDictionary<Guid, OrganizationAbility> 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.");
}

View File

@@ -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.Enums;
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 class CipherMiniResponseModel : ResponseModel
public CipherRepromptType Reprompt { get; set; }
public string Key { get; set; }
}
#nullable enable
public class CipherResponseModel : CipherMiniResponseModel
{
public CipherResponseModel(
CipherDetails cipher,
User user,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
OrganizationAbility? organizationAbility,
IGlobalSettings globalSettings,
string obj = "cipher")
: base(cipher, globalSettings, cipher.OrganizationUseTotp, obj)
@@ -127,7 +128,7 @@ public class CipherResponseModel : CipherMiniResponseModel
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<Guid, OrganizationAbility> organizationAbilities,
OrganizationAbility? organizationAbility,
GlobalSettings globalSettings,
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> 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 class CipherDetailsResponseModel : CipherResponseModel
public CipherDetailsResponseModel(
CipherDetails cipher,
User user,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
OrganizationAbility? organizationAbility,
GlobalSettings globalSettings,
IEnumerable<CollectionCipher> 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 class CipherDetailsResponseModel : CipherResponseModel
public CipherDetailsResponseModel(
CipherDetailsWithCollections cipher,
User user,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
OrganizationAbility? organizationAbility,
GlobalSettings globalSettings,
string obj = "cipherDetails")
: base(cipher, user, organizationAbilities, globalSettings, obj)
: base(cipher, user, organizationAbility, globalSettings, obj)
{
CollectionIds = cipher.CollectionIds ?? [];
}

View File

@@ -53,7 +53,7 @@ public class SyncResponseModel() : ResponseModel("sync")
new CipherDetailsResponseModel(
cipher,
user,
organizationAbilities,
GetOrganizationAbility(cipher, organizationAbilities),
globalSettings,
collectionCiphersDict));
Collections = collections?.Select(
@@ -88,16 +88,30 @@ public class SyncResponseModel() : ResponseModel("sync")
}
: 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<Guid, OrganizationAbility> 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<FolderResponseModel> Folders { get; set; }
public IEnumerable<CollectionDetailsResponseModel> Collections { get; set; }

View File

@@ -100,7 +100,7 @@ public class CiphersControllerTests
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails);
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(new Dictionary<Guid, OrganizationAbility> { { cipherDetails.OrganizationId.Value, new OrganizationAbility { Id = cipherDetails.OrganizationId.Value } } });
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value });
var cipherService = sutProvider.GetDependency<ICipherService>();
await sutProvider.Sut.PutCollections_vNext(id, model);
@@ -116,7 +116,7 @@ public class CiphersControllerTests
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails);
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>());
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(new Dictionary<Guid, OrganizationAbility> { { cipherDetails.OrganizationId.Value, new OrganizationAbility { Id = cipherDetails.OrganizationId.Value } } });
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(cipherDetails.OrganizationId.Value).Returns(new OrganizationAbility { Id = cipherDetails.OrganizationId.Value });
var result = await sutProvider.Sut.PutCollections_vNext(id, model);
@@ -2005,11 +2005,8 @@ public class CiphersControllerTests
.Returns(sharedCipher);
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilitiesAsync()
.Returns(new Dictionary<Guid, OrganizationAbility>
{
{ organizationId, new OrganizationAbility { Id = organizationId } }
});
.GetOrganizationAbilityAsync(organizationId)
.Returns(new OrganizationAbility { Id = organizationId });
var result = await sutProvider.Sut.PutShare(cipherId, model);
@@ -2083,11 +2080,8 @@ public class CiphersControllerTests
.Returns(sharedCipher);
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilitiesAsync()
.Returns(new Dictionary<Guid, OrganizationAbility>
{
{ organizationId, new OrganizationAbility { Id = organizationId } }
});
.GetOrganizationAbilityAsync(organizationId)
.Returns(new OrganizationAbility { Id = organizationId });
var result = await sutProvider.Sut.PutShare(cipherId, model);
@@ -2162,11 +2156,8 @@ public class CiphersControllerTests
.Returns(sharedCipher);
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilitiesAsync()
.Returns(new Dictionary<Guid, OrganizationAbility>
{
{ organizationId, new OrganizationAbility { Id = organizationId } }
});
.GetOrganizationAbilityAsync(organizationId)
.Returns(new OrganizationAbility { Id = organizationId });
var result = await sutProvider.Sut.PutShare(cipherId, model);