1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-04 20:50:21 -05:00

[PM-18087] Add cipher permissions to response models (#5418)

* Add Manage permission to UserCipherDetails and CipherDetails_ReadByIdUserId

* Add Manage property to CipherDetails and UserCipherDetailsQuery

* Add integration test for CipherRepository Manage permission rules

* Update CipherDetails_ReadWithoutOrganizationsByUserId to include Manage permission

* Refactor UserCipherDetailsQuery to include detailed permission and organization properties

* Refactor CipherRepositoryTests to improve test organization and readability

- Split large test method into smaller, focused methods
- Added helper methods for creating test data and performing assertions
- Improved test coverage for cipher permissions in different scenarios
- Maintained existing test logic while enhancing code structure

* Refactor CipherRepositoryTests to consolidate cipher permission tests

- Removed redundant helper methods for permission assertions
- Simplified test methods for GetCipherPermissionsForOrganizationAsync, GetManyByUserIdAsync, and GetByIdAsync
- Maintained existing test coverage for cipher manage permissions
- Improved code readability and reduced code duplication

* Add integration test for CipherRepository group collection manage permissions

- Added new test method GetCipherPermissionsForOrganizationAsync_ManageProperty_RespectsCollectionGroupRules
- Implemented helper method CreateCipherInOrganizationCollectionWithGroup to support group-based collection permission testing
- Verified manage permissions are correctly applied based on group collection access settings

* Add @Manage parameter to Cipher stored procedures

- Updated CipherDetails_Create, CipherDetails_CreateWithCollections, and CipherDetails_Update stored procedures
- Added @Manage parameter with comment "-- not used"
- Included new stored procedure implementations in migration script
- Consistent with previous work on adding Manage property to cipher details

* Update UserCipherDetails functions to reorder Manage and ViewPassword columns

* [PM-18086] Add CanRestore and CanDelete authorization methods.

* [PM-18086] Address code review feedback.

* [PM-18086] Add missing part.

* [PM-18087] Add CipherPermissionsResponseModel for cipher permissions

* Add GetManyOrganizationAbilityAsync method to application cache service

* Add organization ability context to cipher response models

This change introduces organization ability context to various cipher response models across multiple controllers. The modifications include:

- Updating CipherResponseModel to include permissions based on user and organization ability
- Modifying CiphersController methods to fetch and pass organization abilities
- Updating SyncController to include organization abilities in sync response
- Adding organization ability context to EmergencyAccessController response generation

* Remove organization ability context from EmergencyAccessController

This change simplifies the EmergencyAccessController by removing unnecessary organization ability fetching and passing. Since emergency access only retrieves personal ciphers, the organization ability context is no longer needed in the response generation.

* Remove unused IApplicationCacheService from EmergencyAccessController

* Refactor EmergencyAccessViewResponseModel constructor

Remove unnecessary JsonConstructor attribute and simplify constructor initialization for EmergencyAccessViewResponseModel

* Refactor organization ability retrieval in CiphersController

Extract methods to simplify organization ability fetching for ciphers, reducing code duplication and improving readability. Added two private helper methods:
- GetOrganizationAbilityAsync: Retrieves organization ability for a single cipher
- GetManyOrganizationAbilitiesAsync: Retrieves organization abilities for multiple ciphers

* Update CiphersControllerTests to use GetUserByPrincipalAsync

Modify test methods to:
- Replace GetProperUserId with GetUserByPrincipalAsync
- Use User object instead of separate userId
- Update mocking to return User object
- Ensure user ID is correctly set in test scenarios

* Refactor CipherPermissionsResponseModel to use constructor-based initialization

* Refactor CipherPermissionsResponseModel to use record type and init-only properties

* [PM-18086] Undo files

* [PM-18086] Undo files

* Refactor organization abilities retrieval in cipher-related controllers and models

- Update CiphersController to use GetOrganizationAbilitiesAsync instead of individual methods
- Modify CipherResponseModel and CipherDetailsResponseModel to accept organization abilities dictionary
- Update CipherPermissionsResponseModel to handle organization abilities lookup
- Remove deprecated organization ability retrieval methods
- Simplify sync and emergency access response model handling of organization abilities

* Remove GetManyOrganizationAbilityAsync method

- Delete unused method from IApplicationCacheService interface
- Remove corresponding implementation in InMemoryApplicationCacheService
- Continues cleanup of organization ability retrieval methods

* Update CiphersControllerTests to include organization abilities retrieval

- Add organization abilities retrieval in test setup for PutCollections_vNext method
- Ensure consistent mocking of IApplicationCacheService in test scenarios

* Update error message for missing organization ability

---------

Co-authored-by: Jimmy Vo <huynhmaivo82@gmail.com>
This commit is contained in:
Rui Tomé 2025-03-10 15:27:30 +00:00 committed by GitHub
parent 88e91734f1
commit 6e7c5b172c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 206 additions and 79 deletions

View File

@ -167,7 +167,7 @@ public class EmergencyAccessController : Controller
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
var viewResult = await _emergencyAccessService.ViewAsync(id, user); var viewResult = await _emergencyAccessService.ViewAsync(id, user);
return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers); return new EmergencyAccessViewResponseModel(_globalSettings, viewResult.EmergencyAccess, viewResult.Ciphers, user);
} }
[HttpGet("{id}/{cipherId}/attachment/{attachmentId}")] [HttpGet("{id}/{cipherId}/attachment/{attachmentId}")]

View File

@ -116,11 +116,17 @@ public class EmergencyAccessViewResponseModel : ResponseModel
public EmergencyAccessViewResponseModel( public EmergencyAccessViewResponseModel(
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
EmergencyAccess emergencyAccess, EmergencyAccess emergencyAccess,
IEnumerable<CipherDetails> ciphers) IEnumerable<CipherDetails> ciphers,
User user)
: base("emergencyAccessView") : base("emergencyAccessView")
{ {
KeyEncrypted = emergencyAccess.KeyEncrypted; KeyEncrypted = emergencyAccess.KeyEncrypted;
Ciphers = ciphers.Select(c => new CipherResponseModel(c, globalSettings)); Ciphers = ciphers.Select(cipher =>
new CipherResponseModel(
cipher,
user,
organizationAbilities: null, // Emergency access only retrieves personal ciphers so organizationAbilities is not needed
globalSettings));
} }
public string KeyEncrypted { get; set; } public string KeyEncrypted { get; set; }

View File

@ -79,14 +79,16 @@ public class CiphersController : Controller
[HttpGet("{id}")] [HttpGet("{id}")]
public async Task<CipherResponseModel> Get(Guid id) public async Task<CipherResponseModel> Get(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
return new CipherResponseModel(cipher, _globalSettings); var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
return new CipherResponseModel(cipher, user, organizationAbilities, _globalSettings);
} }
[HttpGet("{id}/admin")] [HttpGet("{id}/admin")]
@ -109,32 +111,37 @@ public class CiphersController : Controller
[HttpGet("{id}/details")] [HttpGet("{id}/details")]
public async Task<CipherDetailsResponseModel> GetDetails(Guid id) public async Task<CipherDetailsResponseModel> GetDetails(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id); var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);
return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers);
} }
[HttpGet("")] [HttpGet("")]
public async Task<ListResponseModel<CipherDetailsResponseModel>> Get() public async Task<ListResponseModel<CipherDetailsResponseModel>> Get()
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var hasOrgs = _currentContext.Organizations?.Any() ?? false; var hasOrgs = _currentContext.Organizations?.Any() ?? false;
// TODO: Use hasOrgs proper for cipher listing here? // TODO: Use hasOrgs proper for cipher listing here?
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: true || hasOrgs); var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: true || hasOrgs);
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null; Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;
if (hasOrgs) if (hasOrgs)
{ {
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(userId); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id);
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
} }
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings, var responses = ciphers.Select(cipher => new CipherDetailsResponseModel(
cipher,
user,
organizationAbilities,
_globalSettings,
collectionCiphersGroupDict)).ToList(); collectionCiphersGroupDict)).ToList();
return new ListResponseModel<CipherDetailsResponseModel>(responses); return new ListResponseModel<CipherDetailsResponseModel>(responses);
} }
@ -142,30 +149,38 @@ public class CiphersController : Controller
[HttpPost("")] [HttpPost("")]
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model) public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = model.ToCipherDetails(userId); var cipher = model.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
await _cipherService.SaveDetailsAsync(cipher, userId, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue); await _cipherService.SaveDetailsAsync(cipher, user.Id, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue);
var response = new CipherResponseModel(cipher, _globalSettings); var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response; return response;
} }
[HttpPost("create")] [HttpPost("create")]
public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model) public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = model.Cipher.ToCipherDetails(userId); var cipher = model.Cipher.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
await _cipherService.SaveDetailsAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue); await _cipherService.SaveDetailsAsync(cipher, user.Id, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);
var response = new CipherResponseModel(cipher, _globalSettings); var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response; return response;
} }
@ -191,8 +206,8 @@ public class CiphersController : Controller
[HttpPost("{id}")] [HttpPost("{id}")]
public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model) public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -200,7 +215,7 @@ public class CiphersController : Controller
ValidateClientVersionForFido2CredentialSupport(cipher); ValidateClientVersionForFido2CredentialSupport(cipher);
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList(); var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList();
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ? var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
(Guid?)null : new Guid(model.OrganizationId); (Guid?)null : new Guid(model.OrganizationId);
if (cipher.OrganizationId != modelOrgId) if (cipher.OrganizationId != modelOrgId)
@ -209,9 +224,13 @@ public class CiphersController : Controller
"then try again."); "then try again.");
} }
await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), userId, model.LastKnownRevisionDate, collectionIds); await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), user.Id, model.LastKnownRevisionDate, collectionIds);
var response = new CipherResponseModel(cipher, _globalSettings); var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response; return response;
} }
@ -278,7 +297,14 @@ public class CiphersController : Controller
})); }));
} }
var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings)); var user = await _userService.GetUserByPrincipalAsync(User);
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var responses = ciphers.Select(cipher =>
new CipherDetailsResponseModel(
cipher,
user,
organizationAbilities,
_globalSettings));
return new ListResponseModel<CipherDetailsResponseModel>(responses); return new ListResponseModel<CipherDetailsResponseModel>(responses);
} }
@ -572,12 +598,16 @@ public class CiphersController : Controller
[HttpPost("{id}/partial")] [HttpPost("{id}/partial")]
public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model) public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId); var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);
await _cipherRepository.UpdatePartialAsync(id, userId, folderId, model.Favorite); await _cipherRepository.UpdatePartialAsync(id, user.Id, folderId, model.Favorite);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
var response = new CipherResponseModel(cipher, _globalSettings); var response = new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response; return response;
} }
@ -585,9 +615,9 @@ public class CiphersController : Controller
[HttpPost("{id}/share")] [HttpPost("{id}/share")]
public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareRequestModel model) public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await _cipherRepository.GetByIdAsync(id); var cipher = await _cipherRepository.GetByIdAsync(id);
if (cipher == null || cipher.UserId != userId || if (cipher == null || cipher.UserId != user.Id ||
!await _currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId))) !await _currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId)))
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -597,10 +627,14 @@ public class CiphersController : Controller
var original = cipher.Clone(); var original = cipher.Clone();
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId), await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate); model.CollectionIds.Select(c => new Guid(c)), user.Id, model.Cipher.LastKnownRevisionDate);
var sharedCipher = await GetByIdAsync(id, userId); var sharedCipher = await GetByIdAsync(id, user.Id);
var response = new CipherResponseModel(sharedCipher, _globalSettings); var response = new CipherResponseModel(
sharedCipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
return response; return response;
} }
@ -608,8 +642,8 @@ public class CiphersController : Controller
[HttpPost("{id}/collections")] [HttpPost("{id}/collections")]
public async Task<CipherDetailsResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model) public async Task<CipherDetailsResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null || !cipher.OrganizationId.HasValue || if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{ {
@ -617,20 +651,25 @@ public class CiphersController : Controller
} }
await _cipherService.SaveCollectionsAsync(cipher, await _cipherService.SaveCollectionsAsync(cipher,
model.CollectionIds.Select(c => new Guid(c)), userId, false); model.CollectionIds.Select(c => new Guid(c)), user.Id, false);
var updatedCipher = await GetByIdAsync(id, userId); var updatedCipher = await GetByIdAsync(id, user.Id);
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);
return new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers); return new CipherDetailsResponseModel(
updatedCipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings,
collectionCiphers);
} }
[HttpPut("{id}/collections_v2")] [HttpPut("{id}/collections_v2")]
[HttpPost("{id}/collections_v2")] [HttpPost("{id}/collections_v2")]
public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model) public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null || !cipher.OrganizationId.HasValue || if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword) !await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword)
{ {
@ -638,10 +677,10 @@ public class CiphersController : Controller
} }
await _cipherService.SaveCollectionsAsync(cipher, await _cipherService.SaveCollectionsAsync(cipher,
model.CollectionIds.Select(c => new Guid(c)), userId, false); model.CollectionIds.Select(c => new Guid(c)), user.Id, false);
var updatedCipher = await GetByIdAsync(id, userId); var updatedCipher = await GetByIdAsync(id, user.Id);
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id); var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id);
// If a user removes the last Can Manage access of a cipher, the "updatedCipher" will return null // If a user removes the last Can Manage access of a cipher, the "updatedCipher" will return null
// We will be returning an "Unavailable" property so the client knows the user can no longer access this // We will be returning an "Unavailable" property so the client knows the user can no longer access this
var response = new OptionalCipherDetailsResponseModel() var response = new OptionalCipherDetailsResponseModel()
@ -649,7 +688,12 @@ public class CiphersController : Controller
Unavailable = updatedCipher is null, Unavailable = updatedCipher is null,
Cipher = updatedCipher is null Cipher = updatedCipher is null
? null ? null
: new CipherDetailsResponseModel(updatedCipher, _globalSettings, collectionCiphers) : new CipherDetailsResponseModel(
updatedCipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings,
collectionCiphers)
}; };
return response; return response;
} }
@ -839,15 +883,19 @@ public class CiphersController : Controller
[HttpPut("{id}/restore")] [HttpPut("{id}/restore")]
public async Task<CipherResponseModel> PutRestore(Guid id) public async Task<CipherResponseModel> PutRestore(Guid id)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
} }
await _cipherService.RestoreAsync(cipher, userId); await _cipherService.RestoreAsync(cipher, user.Id);
return new CipherResponseModel(cipher, _globalSettings); return new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
} }
[HttpPut("{id}/restore-admin")] [HttpPut("{id}/restore-admin")]
@ -996,10 +1044,10 @@ public class CiphersController : Controller
[HttpPost("{id}/attachment/v2")] [HttpPost("{id}/attachment/v2")]
public async Task<AttachmentUploadDataResponseModel> PostAttachment(Guid id, [FromBody] AttachmentRequestModel request) public async Task<AttachmentUploadDataResponseModel> PostAttachment(Guid id, [FromBody] AttachmentRequestModel request)
{ {
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = request.AdminRequest ? var cipher = request.AdminRequest ?
await _cipherRepository.GetOrganizationDetailsByIdAsync(id) : await _cipherRepository.GetOrganizationDetailsByIdAsync(id) :
await GetByIdAsync(id, userId); await GetByIdAsync(id, user.Id);
if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue || if (cipher == null || (request.AdminRequest && (!cipher.OrganizationId.HasValue ||
!await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })))) !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id }))))
@ -1013,13 +1061,17 @@ public class CiphersController : Controller
} }
var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher, var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher,
request.Key, request.FileName, request.FileSize, request.AdminRequest, userId); request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id);
return new AttachmentUploadDataResponseModel return new AttachmentUploadDataResponseModel
{ {
AttachmentId = attachmentId, AttachmentId = attachmentId,
Url = uploadUrl, Url = uploadUrl,
FileUploadType = _attachmentStorageService.FileUploadType, FileUploadType = _attachmentStorageService.FileUploadType,
CipherResponse = request.AdminRequest ? null : new CipherResponseModel((CipherDetails)cipher, _globalSettings), CipherResponse = request.AdminRequest ? null : new CipherResponseModel(
(CipherDetails)cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings),
CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null, CipherMiniResponse = request.AdminRequest ? new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp) : null,
}; };
} }
@ -1077,8 +1129,8 @@ public class CiphersController : Controller
{ {
ValidateAttachment(); ValidateAttachment();
var userId = _userService.GetProperUserId(User).Value; var user = await _userService.GetUserByPrincipalAsync(User);
var cipher = await GetByIdAsync(id, userId); var cipher = await GetByIdAsync(id, user.Id);
if (cipher == null) if (cipher == null)
{ {
throw new NotFoundException(); throw new NotFoundException();
@ -1087,10 +1139,14 @@ public class CiphersController : Controller
await Request.GetFileAsync(async (stream, fileName, key) => await Request.GetFileAsync(async (stream, fileName, key) =>
{ {
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key, await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
Request.ContentLength.GetValueOrDefault(0), userId); Request.ContentLength.GetValueOrDefault(0), user.Id);
}); });
return new CipherResponseModel(cipher, _globalSettings); return new CipherResponseModel(
cipher,
user,
await _applicationCacheService.GetOrganizationAbilitiesAsync(),
_globalSettings);
} }
[HttpPost("{id}/attachment-admin")] [HttpPost("{id}/attachment-admin")]

View File

@ -36,6 +36,7 @@ public class SyncController : Controller
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService;
public SyncController( public SyncController(
IUserService userService, IUserService userService,
@ -49,7 +50,8 @@ public class SyncController : Controller
ISendRepository sendRepository, ISendRepository sendRepository,
GlobalSettings globalSettings, GlobalSettings globalSettings,
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService) IFeatureService featureService,
IApplicationCacheService applicationCacheService)
{ {
_userService = userService; _userService = userService;
_folderRepository = folderRepository; _folderRepository = folderRepository;
@ -63,6 +65,7 @@ public class SyncController : Controller
_globalSettings = globalSettings; _globalSettings = globalSettings;
_currentContext = currentContext; _currentContext = currentContext;
_featureService = featureService; _featureService = featureService;
_applicationCacheService = applicationCacheService;
} }
[HttpGet("")] [HttpGet("")]
@ -104,7 +107,9 @@ public class SyncController : Controller
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id); var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id);
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id); var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails, organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
return response; return response;

View File

@ -0,0 +1,27 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Vault.Authorization.Permissions;
using Bit.Core.Vault.Models.Data;
namespace Bit.Api.Vault.Models.Response;
public record CipherPermissionsResponseModel
{
public bool Delete { get; init; }
public bool Restore { get; init; }
public CipherPermissionsResponseModel(
User user,
CipherDetails cipherDetails,
IDictionary<Guid, OrganizationAbility> organizationAbilities)
{
OrganizationAbility organizationAbility = null;
if (cipherDetails.OrganizationId.HasValue && !organizationAbilities.TryGetValue(cipherDetails.OrganizationId.Value, out organizationAbility))
{
throw new Exception("OrganizationAbility not found for organization cipher.");
}
Delete = NormalCipherPermissions.CanDelete(user, cipherDetails, organizationAbility);
Restore = NormalCipherPermissions.CanRestore(user, cipherDetails, organizationAbility);
}
}

View File

@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums; using Bit.Core.Vault.Enums;
@ -96,26 +97,37 @@ public class CipherMiniResponseModel : ResponseModel
public class CipherResponseModel : CipherMiniResponseModel public class CipherResponseModel : CipherMiniResponseModel
{ {
public CipherResponseModel(CipherDetails cipher, IGlobalSettings globalSettings, string obj = "cipher") public CipherResponseModel(
CipherDetails cipher,
User user,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
IGlobalSettings globalSettings,
string obj = "cipher")
: base(cipher, globalSettings, cipher.OrganizationUseTotp, obj) : base(cipher, globalSettings, cipher.OrganizationUseTotp, obj)
{ {
FolderId = cipher.FolderId; FolderId = cipher.FolderId;
Favorite = cipher.Favorite; Favorite = cipher.Favorite;
Edit = cipher.Edit; Edit = cipher.Edit;
ViewPassword = cipher.ViewPassword; ViewPassword = cipher.ViewPassword;
Permissions = new CipherPermissionsResponseModel(user, cipher, organizationAbilities);
} }
public Guid? FolderId { get; set; } public Guid? FolderId { get; set; }
public bool Favorite { get; set; } public bool Favorite { get; set; }
public bool Edit { get; set; } public bool Edit { get; set; }
public bool ViewPassword { get; set; } public bool ViewPassword { get; set; }
public CipherPermissionsResponseModel Permissions { get; set; }
} }
public class CipherDetailsResponseModel : CipherResponseModel public class CipherDetailsResponseModel : CipherResponseModel
{ {
public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings, public CipherDetailsResponseModel(
CipherDetails cipher,
User user,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
GlobalSettings globalSettings,
IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = "cipherDetails") IDictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphers, string obj = "cipherDetails")
: base(cipher, globalSettings, obj) : base(cipher, user, organizationAbilities, globalSettings, obj)
{ {
if (collectionCiphers?.ContainsKey(cipher.Id) ?? false) if (collectionCiphers?.ContainsKey(cipher.Id) ?? false)
{ {
@ -127,15 +139,24 @@ public class CipherDetailsResponseModel : CipherResponseModel
} }
} }
public CipherDetailsResponseModel(CipherDetails cipher, GlobalSettings globalSettings, public CipherDetailsResponseModel(
CipherDetails cipher,
User user,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
GlobalSettings globalSettings,
IEnumerable<CollectionCipher> collectionCiphers, string obj = "cipherDetails") IEnumerable<CollectionCipher> collectionCiphers, string obj = "cipherDetails")
: base(cipher, globalSettings, obj) : base(cipher, user, organizationAbilities, globalSettings, obj)
{ {
CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List<Guid>(); CollectionIds = collectionCiphers?.Select(c => c.CollectionId) ?? new List<Guid>();
} }
public CipherDetailsResponseModel(CipherDetailsWithCollections cipher, GlobalSettings globalSettings, string obj = "cipherDetails") public CipherDetailsResponseModel(
: base(cipher, globalSettings, obj) CipherDetailsWithCollections cipher,
User user,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
GlobalSettings globalSettings,
string obj = "cipherDetails")
: base(cipher, user, organizationAbilities, globalSettings, obj)
{ {
CollectionIds = cipher.CollectionIds ?? new List<Guid>(); CollectionIds = cipher.CollectionIds ?? new List<Guid>();
} }

View File

@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
@ -21,6 +22,7 @@ public class SyncResponseModel : ResponseModel
User user, User user,
bool userTwoFactorEnabled, bool userTwoFactorEnabled,
bool userHasPremiumFromOrganization, bool userHasPremiumFromOrganization,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
IEnumerable<Guid> organizationIdsManagingUser, IEnumerable<Guid> organizationIdsManagingUser,
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails, IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails,
IEnumerable<ProviderUserProviderDetails> providerUserDetails, IEnumerable<ProviderUserProviderDetails> providerUserDetails,
@ -37,7 +39,13 @@ public class SyncResponseModel : ResponseModel
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser); providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser);
Folders = folders.Select(f => new FolderResponseModel(f)); Folders = folders.Select(f => new FolderResponseModel(f));
Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict)); Ciphers = ciphers.Select(cipher =>
new CipherDetailsResponseModel(
cipher,
user,
organizationAbilities,
globalSettings,
collectionCiphersDict));
Collections = collections?.Select( Collections = collections?.Select(
c => new CollectionDetailsResponseModel(c)) ?? new List<CollectionDetailsResponseModel>(); c => new CollectionDetailsResponseModel(c)) ?? new List<CollectionDetailsResponseModel>();
Domains = excludeDomains ? null : new DomainsResponseModel(user, false); Domains = excludeDomains ? null : new DomainsResponseModel(user, false);

View File

@ -27,17 +27,18 @@ namespace Bit.Api.Test.Controllers;
public class CiphersControllerTests public class CiphersControllerTests
{ {
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(Guid userId, Guid folderId, SutProvider<CiphersController> sutProvider) public async Task PutPartialShouldReturnCipherWithGivenFolderAndFavoriteValues(User user, Guid folderId, SutProvider<CiphersController> sutProvider)
{ {
var isFavorite = true; var isFavorite = true;
var cipherId = Guid.NewGuid(); var cipherId = Guid.NewGuid();
sutProvider.GetDependency<IUserService>() sutProvider.GetDependency<IUserService>()
.GetProperUserId(Arg.Any<ClaimsPrincipal>()) .GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(userId); .Returns(user);
var cipherDetails = new CipherDetails var cipherDetails = new CipherDetails
{ {
UserId = user.Id,
Favorite = isFavorite, Favorite = isFavorite,
FolderId = folderId, FolderId = folderId,
Type = Core.Vault.Enums.CipherType.SecureNote, Type = Core.Vault.Enums.CipherType.SecureNote,
@ -45,7 +46,7 @@ public class CiphersControllerTests
}; };
sutProvider.GetDependency<ICipherRepository>() sutProvider.GetDependency<ICipherRepository>()
.GetByIdAsync(cipherId, userId) .GetByIdAsync(cipherId, user.Id)
.Returns(Task.FromResult(cipherDetails)); .Returns(Task.FromResult(cipherDetails));
var result = await sutProvider.Sut.PutPartial(cipherId, new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() }); var result = await sutProvider.Sut.PutPartial(cipherId, new CipherPartialRequestModel { Favorite = isFavorite, FolderId = folderId.ToString() });
@ -55,12 +56,12 @@ public class CiphersControllerTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task PutCollections_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, Guid userId, public async Task PutCollections_vNextShouldThrowExceptionWhenCipherIsNullOrNoOrgValue(Guid id, CipherCollectionsRequestModel model, User user,
SutProvider<CiphersController> sutProvider) SutProvider<CiphersController> sutProvider)
{ {
sutProvider.GetDependency<IUserService>().GetProperUserId(default).Returns(userId); sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(Guid.NewGuid()).Returns(false); sutProvider.GetDependency<ICurrentContext>().OrganizationUser(Guid.NewGuid()).Returns(false);
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsNull(); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, user.Id).ReturnsNull();
var requestAction = async () => await sutProvider.Sut.PutCollections_vNext(id, model); var requestAction = async () => await sutProvider.Sut.PutCollections_vNext(id, model);
@ -75,6 +76,7 @@ public class CiphersControllerTests
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails);
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>()); 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() } });
var cipherService = sutProvider.GetDependency<ICipherService>(); var cipherService = sutProvider.GetDependency<ICipherService>();
await sutProvider.Sut.PutCollections_vNext(id, model); await sutProvider.Sut.PutCollections_vNext(id, model);
@ -90,6 +92,7 @@ public class CiphersControllerTests
sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails); sutProvider.GetDependency<ICipherRepository>().GetByIdAsync(id, userId).ReturnsForAnyArgs(cipherDetails);
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns((ICollection<CollectionCipher>)new List<CollectionCipher>()); 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() } });
var result = await sutProvider.Sut.PutCollections_vNext(id, model); var result = await sutProvider.Sut.PutCollections_vNext(id, model);
@ -115,6 +118,7 @@ public class CiphersControllerTests
private void SetupUserAndOrgMocks(Guid id, Guid userId, SutProvider<CiphersController> sutProvider) private void SetupUserAndOrgMocks(Guid id, Guid userId, SutProvider<CiphersController> sutProvider)
{ {
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(new User { Id = userId });
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true); sutProvider.GetDependency<ICurrentContext>().OrganizationUser(default).ReturnsForAnyArgs(true);
sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns(new List<CollectionCipher>()); sutProvider.GetDependency<ICollectionCipherRepository>().GetManyByUserIdCipherIdAsync(userId, id).Returns(new List<CollectionCipher>());
} }