Files
server/src/Api/Controllers/CiphersController.cs
Matt Gibson 5537470703 Use sas token for attachment downloads (#1153)
* Get limited life attachment download URL

This change limits url download to a 1min lifetime.
This requires moving to a new container to allow for non-public blob
access.

Clients will have to call GetAttachmentData api function to receive the download
URL. For backwards compatibility, attachment URLs are still present, but will not
work for attachments stored in non-public access blobs.

* Make GlobalSettings interface for testing

* Test LocalAttachmentStorageService equivalence

* Remove comment

* Add missing globalSettings using

* Simplify default attachment container

* Default to attachments containe for existing methods

A new upload method will be made for uploading to attachments-v2.
For compatibility for clients which don't use these new methods, we need
to still use the old container. The new container will be used only for
new uploads

* Remove Default MetaData fixture.

* Keep attachments container blob-level security for all instances

* Close unclosed FileStream

* Favor default value for noop services
2021-02-22 15:35:16 -06:00

701 lines
29 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Bit.Core.Repositories;
using Microsoft.AspNetCore.Authorization;
using Bit.Core.Models.Api;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Context;
using Bit.Api.Utilities;
using System.Collections.Generic;
using Bit.Core.Models.Table;
using Bit.Core.Settings;
namespace Bit.Api.Controllers
{
[Route("ciphers")]
[Authorize("Application")]
public class CiphersController : Controller
{
private readonly ICipherRepository _cipherRepository;
private readonly ICollectionCipherRepository _collectionCipherRepository;
private readonly ICipherService _cipherService;
private readonly IUserService _userService;
private readonly IAttachmentStorageService _attachmentStorageService;
private readonly ICurrentContext _currentContext;
private readonly GlobalSettings _globalSettings;
public CiphersController(
ICipherRepository cipherRepository,
ICollectionCipherRepository collectionCipherRepository,
ICipherService cipherService,
IUserService userService,
IAttachmentStorageService attachmentStorageService,
ICurrentContext currentContext,
GlobalSettings globalSettings)
{
_cipherRepository = cipherRepository;
_collectionCipherRepository = collectionCipherRepository;
_cipherService = cipherService;
_userService = userService;
_attachmentStorageService = attachmentStorageService;
_currentContext = currentContext;
_globalSettings = globalSettings;
}
[HttpGet("{id}")]
public async Task<CipherResponseModel> Get(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
if (cipher == null)
{
throw new NotFoundException();
}
return new CipherResponseModel(cipher, _globalSettings);
}
[HttpGet("{id}/admin")]
public async Task<CipherMiniResponseModel> GetAdmin(string id)
{
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
}
[HttpGet("{id}/full-details")]
[HttpGet("{id}/details")]
public async Task<CipherDetailsResponseModel> GetDetails(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipherId = new Guid(id);
var cipher = await _cipherRepository.GetByIdAsync(cipherId, userId);
if (cipher == null)
{
throw new NotFoundException();
}
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, cipherId);
return new CipherDetailsResponseModel(cipher, _globalSettings, collectionCiphers);
}
[HttpGet("")]
public async Task<ListResponseModel<CipherDetailsResponseModel>> Get()
{
var userId = _userService.GetProperUserId(User).Value;
var hasOrgs = _currentContext.Organizations?.Any() ?? false;
// TODO: Use hasOrgs proper for cipher listing here?
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, true || hasOrgs);
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict = null;
if (hasOrgs)
{
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(userId);
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
}
var responses = ciphers.Select(c => new CipherDetailsResponseModel(c, _globalSettings,
collectionCiphersGroupDict)).ToList();
return new ListResponseModel<CipherDetailsResponseModel>(responses);
}
[HttpPost("")]
public async Task<CipherResponseModel> Post([FromBody]CipherRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = model.ToCipherDetails(userId);
if (cipher.OrganizationId.HasValue && !_currentContext.OrganizationUser(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.SaveDetailsAsync(cipher, userId, model.LastKnownRevisionDate, null, cipher.OrganizationId.HasValue);
var response = new CipherResponseModel(cipher, _globalSettings);
return response;
}
[HttpPost("create")]
public async Task<CipherResponseModel> PostCreate([FromBody]CipherCreateRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = model.Cipher.ToCipherDetails(userId);
if (cipher.OrganizationId.HasValue && !_currentContext.OrganizationUser(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.SaveDetailsAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, cipher.OrganizationId.HasValue);
var response = new CipherResponseModel(cipher, _globalSettings);
return response;
}
[HttpPost("admin")]
public async Task<CipherMiniResponseModel> PostAdmin([FromBody]CipherCreateRequestModel model)
{
var cipher = model.Cipher.ToOrganizationCipher();
if (!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false);
var response = new CipherMiniResponseModel(cipher, _globalSettings, false);
return response;
}
[HttpPut("{id}")]
[HttpPost("{id}")]
public async Task<CipherResponseModel> Put(string id, [FromBody]CipherRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
if (cipher == null)
{
throw new NotFoundException();
}
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
(Guid?)null : new Guid(model.OrganizationId);
if (cipher.OrganizationId != modelOrgId)
{
throw new BadRequestException("Organization mismatch. Re-sync if you recently shared this item, " +
"then try again.");
}
await _cipherService.SaveDetailsAsync(model.ToCipherDetails(cipher), userId, model.LastKnownRevisionDate);
var response = new CipherResponseModel(cipher, _globalSettings);
return response;
}
[HttpPut("{id}/admin")]
[HttpPost("{id}/admin")]
public async Task<CipherMiniResponseModel> PutAdmin(string id, [FromBody]CipherRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
// object cannot be a descendant of CipherDetails, so let's clone it.
var cipherClone = model.ToCipher(cipher).Clone();
await _cipherService.SaveAsync(cipherClone, userId, model.LastKnownRevisionDate, null, true, false);
var response = new CipherMiniResponseModel(cipherClone, _globalSettings, cipher.OrganizationUseTotp);
return response;
}
[HttpGet("organization-details")]
public async Task<ListResponseModel<CipherMiniDetailsResponseModel>> GetOrganizationCollections(
string organizationId)
{
var userId = _userService.GetProperUserId(User).Value;
var orgIdGuid = new Guid(organizationId);
if (!_currentContext.ManageAllCollections(orgIdGuid) && !_currentContext.AccessReports(orgIdGuid))
{
throw new NotFoundException();
}
var ciphers = await _cipherRepository.GetManyByOrganizationIdAsync(orgIdGuid);
var collectionCiphers = await _collectionCipherRepository.GetManyByOrganizationIdAsync(orgIdGuid);
var collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
var responses = ciphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings,
collectionCiphersGroupDict));
return new ListResponseModel<CipherMiniDetailsResponseModel>(responses);
}
[HttpPost("import")]
public async Task PostImport([FromBody]ImportCiphersRequestModel model)
{
if (!_globalSettings.SelfHosted &&
(model.Ciphers.Count() > 6000 || model.FolderRelationships.Count() > 6000 ||
model.Folders.Count() > 1000))
{
throw new BadRequestException("You cannot import this much data at once.");
}
var userId = _userService.GetProperUserId(User).Value;
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
await _cipherService.ImportCiphersAsync(folders, ciphers, model.FolderRelationships);
}
[HttpPost("import-organization")]
public async Task PostImport([FromQuery]string organizationId,
[FromBody]ImportOrganizationCiphersRequestModel model)
{
if (!_globalSettings.SelfHosted &&
(model.Ciphers.Count() > 6000 || model.CollectionRelationships.Count() > 12000 ||
model.Collections.Count() > 1000))
{
throw new BadRequestException("You cannot import this much data at once.");
}
var orgId = new Guid(organizationId);
if (!_currentContext.AccessImportExport(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var collections = model.Collections.Select(c => c.ToCollection(orgId)).ToList();
var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();
await _cipherService.ImportCiphersAsync(collections, ciphers, model.CollectionRelationships, userId);
}
[HttpPut("{id}/partial")]
[HttpPost("{id}/partial")]
public async Task PutPartial(string id, [FromBody]CipherPartialRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var folderId = string.IsNullOrWhiteSpace(model.FolderId) ? null : (Guid?)new Guid(model.FolderId);
await _cipherRepository.UpdatePartialAsync(new Guid(id), userId, folderId, model.Favorite);
}
[HttpPut("{id}/share")]
[HttpPost("{id}/share")]
public async Task<CipherResponseModel> PutShare(string id, [FromBody]CipherShareRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipherId = new Guid(id);
var cipher = await _cipherRepository.GetByIdAsync(cipherId);
if (cipher == null || cipher.UserId != userId ||
!_currentContext.OrganizationUser(new Guid(model.Cipher.OrganizationId)))
{
throw new NotFoundException();
}
var original = cipher.Clone();
await _cipherService.ShareAsync(original, model.Cipher.ToCipher(cipher), new Guid(model.Cipher.OrganizationId),
model.CollectionIds.Select(c => new Guid(c)), userId, model.Cipher.LastKnownRevisionDate);
var sharedCipher = await _cipherRepository.GetByIdAsync(cipherId, userId);
var response = new CipherResponseModel(sharedCipher, _globalSettings);
return response;
}
[HttpPut("{id}/collections")]
[HttpPost("{id}/collections")]
public async Task PutCollections(string id, [FromBody]CipherCollectionsRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.OrganizationUser(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.SaveCollectionsAsync(cipher,
model.CollectionIds.Select(c => new Guid(c)), userId, false);
}
[HttpPut("{id}/collections-admin")]
[HttpPost("{id}/collections-admin")]
public async Task PutCollectionsAdmin(string id, [FromBody]CipherCollectionsRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.SaveCollectionsAsync(cipher,
model.CollectionIds.Select(c => new Guid(c)), userId, true);
}
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
if (cipher == null)
{
throw new NotFoundException();
}
await _cipherService.DeleteAsync(cipher, userId);
}
[HttpDelete("{id}/admin")]
[HttpPost("{id}/delete-admin")]
public async Task DeleteAdmin(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.DeleteAsync(cipher, userId, true);
}
[HttpDelete("")]
[HttpPost("delete")]
public async Task DeleteMany([FromBody]CipherBulkDeleteRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only delete up to 500 items at a time. " +
"Consider using the \"Purge Vault\" option instead.");
}
var userId = _userService.GetProperUserId(User).Value;
await _cipherService.DeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId);
}
[HttpDelete("admin")]
[HttpPost("delete-admin")]
public async Task DeleteManyAdmin([FromBody]CipherBulkDeleteRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only delete up to 500 items at a time. " +
"Consider using the \"Purge Vault\" option instead.");
}
if (model == null || string.IsNullOrWhiteSpace(model.OrganizationId) ||
!_currentContext.ManageAllCollections(new Guid(model.OrganizationId)))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
await _cipherService.DeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId, new Guid(model.OrganizationId), true);
}
[HttpPut("{id}/delete")]
public async Task PutDelete(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
if (cipher == null)
{
throw new NotFoundException();
}
await _cipherService.SoftDeleteAsync(cipher, userId);
}
[HttpPut("{id}/delete-admin")]
public async Task PutDeleteAdmin(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.SoftDeleteAsync(cipher, userId, true);
}
[HttpPut("delete")]
public async Task PutDeleteMany([FromBody]CipherBulkDeleteRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only delete up to 500 items at a time.");
}
var userId = _userService.GetProperUserId(User).Value;
await _cipherService.SoftDeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId);
}
[HttpPut("delete-admin")]
public async Task PutDeleteManyAdmin([FromBody]CipherBulkDeleteRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only delete up to 500 items at a time.");
}
if (model == null || string.IsNullOrWhiteSpace(model.OrganizationId) ||
!_currentContext.ManageAllCollections(new Guid(model.OrganizationId)))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
await _cipherService.SoftDeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId, new Guid(model.OrganizationId), true);
}
[HttpPut("{id}/restore")]
public async Task<CipherResponseModel> PutRestore(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
if (cipher == null)
{
throw new NotFoundException();
}
await _cipherService.RestoreAsync(cipher, userId);
return new CipherResponseModel(cipher, _globalSettings);
}
[HttpPut("{id}/restore-admin")]
public async Task<CipherMiniResponseModel> PutRestoreAdmin(string id)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(new Guid(id));
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.RestoreAsync(cipher, userId, true);
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
}
[HttpPut("restore")]
public async Task<ListResponseModel<CipherResponseModel>> PutRestoreMany([FromBody] CipherBulkRestoreRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only restore up to 500 items at a time.");
}
var userId = _userService.GetProperUserId(User).Value;
var cipherIdsToRestore = new HashSet<Guid>(model.Ids.Select(i => new Guid(i)));
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId);
var restoringCiphers = ciphers.Where(c => cipherIdsToRestore.Contains(c.Id) && c.Edit);
await _cipherService.RestoreManyAsync(restoringCiphers, userId);
var responses = restoringCiphers.Select(c => new CipherResponseModel(c, _globalSettings));
return new ListResponseModel<CipherResponseModel>(responses);
}
[HttpPut("move")]
[HttpPost("move")]
public async Task MoveMany([FromBody]CipherBulkMoveRequestModel model)
{
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
{
throw new BadRequestException("You can only move up to 500 items at a time.");
}
var userId = _userService.GetProperUserId(User).Value;
await _cipherService.MoveManyAsync(model.Ids.Select(i => new Guid(i)),
string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);
}
[HttpPut("share")]
[HttpPost("share")]
public async Task PutShareMany([FromBody]CipherBulkShareRequestModel model)
{
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
if (!_currentContext.OrganizationUser(organizationId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User).Value;
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, false);
var ciphersDict = ciphers.ToDictionary(c => c.Id);
var shareCiphers = new List<(Cipher, DateTime?)>();
foreach (var cipher in model.Ciphers)
{
if (!ciphersDict.ContainsKey(cipher.Id.Value))
{
throw new BadRequestException("Trying to share ciphers that you do not own.");
}
shareCiphers.Add((cipher.ToCipher(ciphersDict[cipher.Id.Value]), cipher.LastKnownRevisionDate));
}
await _cipherService.ShareManyAsync(shareCiphers, organizationId,
model.CollectionIds.Select(c => new Guid(c)), userId);
}
[HttpPost("purge")]
public async Task PostPurge([FromBody]CipherPurgeRequestModel model, string organizationId = null)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
{
ModelState.AddModelError("MasterPasswordHash", "Invalid password.");
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
if (string.IsNullOrWhiteSpace(organizationId))
{
await _cipherRepository.DeleteByUserIdAsync(user.Id);
}
else
{
var orgId = new Guid(organizationId);
if (!_currentContext.ManageAllCollections(orgId))
{
throw new NotFoundException();
}
await _cipherService.PurgeAsync(orgId);
}
}
[HttpPost("{id}/attachment")]
[RequestSizeLimit(105_906_176)]
[DisableFormValueModelBinding]
public async Task<CipherResponseModel> PostAttachment(string id)
{
ValidateAttachment();
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId);
if (cipher == null)
{
throw new NotFoundException();
}
await Request.GetFileAsync(async (stream, fileName, key) =>
{
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
Request.ContentLength.GetValueOrDefault(0), userId);
});
return new CipherResponseModel(cipher, _globalSettings);
}
[HttpPost("{id}/attachment-admin")]
[RequestSizeLimit(105_906_176)]
[DisableFormValueModelBinding]
public async Task<CipherMiniResponseModel> PostAttachmentAdmin(string id)
{
ValidateAttachment();
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(idGuid);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await Request.GetFileAsync(async (stream, fileName, key) =>
{
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
Request.ContentLength.GetValueOrDefault(0), userId, true);
});
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
}
[HttpGet("{id}/attachment/{attachmentId}")]
public async Task<AttachmentResponseModel> GetAttachmentData(string id, string attachmentId)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id), userId);
var attachments = cipher.GetAttachments();
if (!attachments.ContainsKey(attachmentId))
{
throw new NotFoundException();
}
var data = attachments[attachmentId];
var response = new AttachmentResponseModel(attachmentId, data, cipher, _globalSettings)
{
Url = await _attachmentStorageService.GetAttachmentDownloadUrlAsync(cipher, data)
};
return response;
}
[HttpPost("{id}/attachment/{attachmentId}/share")]
[RequestSizeLimit(105_906_176)]
[DisableFormValueModelBinding]
public async Task PostAttachmentShare(string id, string attachmentId, Guid organizationId)
{
ValidateAttachment();
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(new Guid(id));
if (cipher == null || cipher.UserId != userId || !_currentContext.OrganizationUser(organizationId))
{
throw new NotFoundException();
}
await Request.GetFileAsync(async (stream, fileName, key) =>
{
await _cipherService.CreateAttachmentShareAsync(cipher, stream,
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
});
}
[HttpDelete("{id}/attachment/{attachmentId}")]
[HttpPost("{id}/attachment/{attachmentId}/delete")]
public async Task DeleteAttachment(string id, string attachmentId)
{
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid, userId);
if (cipher == null)
{
throw new NotFoundException();
}
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
}
[HttpDelete("{id}/attachment/{attachmentId}/admin")]
[HttpPost("{id}/attachment/{attachmentId}/delete-admin")]
public async Task DeleteAttachmentAdmin(string id, string attachmentId)
{
var idGuid = new Guid(id);
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetByIdAsync(idGuid);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!_currentContext.ManageAllCollections(cipher.OrganizationId.Value))
{
throw new NotFoundException();
}
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true);
}
private void ValidateAttachment()
{
if (!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}
if (Request.ContentLength > 105906176) // 101 MB, give em' 1 extra MB for cushion
{
throw new BadRequestException("Max file size is 100 MB.");
}
}
}
}