Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
129 changes: 65 additions & 64 deletions src/Api/Vault/Controllers/CiphersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -91,9 +92,9 @@ public async Task<CipherResponseModel> 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")]
Expand Down Expand Up @@ -122,9 +123,9 @@ public async Task<CipherDetailsResponseModel> 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")]
Expand All @@ -147,16 +148,17 @@ public async Task<ListResponseModel<CipherDetailsResponseModel>> 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<CipherDetailsResponseModel>(responses);
}


[HttpPost("")]
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
{
Expand All @@ -179,11 +181,7 @@ public async Task<CipherResponseModel> 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;
}

Expand Down Expand Up @@ -274,11 +272,7 @@ public async Task<CipherResponseModel> 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;
}

Expand Down Expand Up @@ -373,13 +367,9 @@ public async Task<ListResponseModel<CipherDetailsResponseModel>> 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<CipherDetailsResponseModel>(responses);
}
Expand Down Expand Up @@ -719,11 +709,7 @@ public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPart
await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite);

var updatedCipher = await GetByIdAsync(id, user.Id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Was there an scenario that caused you to remove this call to get the updated cipher? Claude is also pointing out that this would lead to stale data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed this in the Claude's comment.

var response = new CipherResponseModel(
updatedCipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
var response = new CipherResponseModel(updatedCipher, user, await GetOrganizationAbilityAsync(updatedCipher), _globalSettings);
return response;
}

Expand Down Expand Up @@ -762,11 +748,7 @@ public async Task<CipherResponseModel> 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;
}

Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -920,11 +892,8 @@ public async Task<CipherResponseModel> 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")]
Expand All @@ -948,12 +917,9 @@ public async Task<ListResponseModel<CipherResponseModel>> 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<CipherResponseModel>(responses);
}
Expand Down Expand Up @@ -1128,9 +1094,10 @@ public async Task<CipherResponseModel> 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
);
}
Expand All @@ -1146,7 +1113,6 @@ public async Task<ListResponseModel<CipherResponseModel>> PutUnarchiveMany([From

var userId = _userService.GetProperUserId(User).Value;
var user = await _userService.GetUserByPrincipalAsync(User);
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();

var cipherIdsToUnarchive = new HashSet<Guid>(model.Ids);

Expand All @@ -1157,7 +1123,9 @@ public async Task<ListResponseModel<CipherResponseModel>> 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<CipherResponseModel>(responses);
}
Expand All @@ -1176,7 +1144,7 @@ public async Task<CipherResponseModel> PutRestore(Guid id)
return new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
await GetOrganizationAbilityAsync(cipher),
_globalSettings);
}

Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -1453,7 +1423,7 @@ await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
return new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
await GetOrganizationAbilityAsync(cipher),
_globalSettings);
}

Expand Down Expand Up @@ -1696,4 +1666,35 @@ private async Task<CipherDetails> GetByIdAsync(Guid cipherId, Guid userId)

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);
}

}
21 changes: 20 additions & 1 deletion src/Api/Vault/Controllers/SyncController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
: [];
Expand All @@ -141,6 +142,24 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
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;
}
Comment on lines +145 to +161
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 Suggested β€” DRY: Duplicated helper method

This method is nearly identical to CiphersController.GetOrganizationAbilitiesAsync (lines 1682-1697). Similarly, SyncResponseModel.GetOrganizationAbility duplicates CiphersController.GetOrganizationAbility. Consider extracting these into a shared static helper (e.g., on a base class or a small utility) to avoid maintaining three copies of the same logic.


private ICollection<CipherDetails> FilterSSHKeys(ICollection<CipherDetails> ciphers)
{
if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down
Loading
Loading