diff --git a/src/Api/Tools/Controllers/SendsController.cs b/src/Api/Tools/Controllers/SendsController.cs index 7c2402ea94..5b7143efc3 100644 --- a/src/Api/Tools/Controllers/SendsController.cs +++ b/src/Api/Tools/Controllers/SendsController.cs @@ -7,6 +7,7 @@ using Bit.Api.Utilities; using Bit.Core; using Bit.Core.Auth.Identity; using Bit.Core.Auth.UserFeatures.SendAccess; +using Bit.Core.Billing.Premium.Queries; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; using Bit.Core.Services; @@ -36,6 +37,7 @@ public class SendsController : Controller private readonly ILogger _logger; private readonly IFeatureService _featureService; private readonly IPushNotificationService _pushNotificationService; + private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery; public SendsController( ISendRepository sendRepository, @@ -47,7 +49,9 @@ public class SendsController : Controller ISendFileStorageService sendFileStorageService, ILogger logger, IFeatureService featureService, - IPushNotificationService pushNotificationService) + IPushNotificationService pushNotificationService, + IHasPremiumAccessQuery hasPremiumAccessQuery + ) { _sendRepository = sendRepository; _userService = userService; @@ -59,6 +63,7 @@ public class SendsController : Controller _logger = logger; _featureService = featureService; _pushNotificationService = pushNotificationService; + _hasPremiumAccessQuery = hasPremiumAccessQuery; } #region Anonymous endpoints @@ -306,6 +311,13 @@ public class SendsController : Controller { model.ValidateCreation(); var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); + var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId); + + if (!hasPremium && !string.IsNullOrWhiteSpace(model.Emails)) + { + throw new BadRequestException("Email verified Sends require a premium membership"); + } + var send = model.ToSend(userId, _sendAuthorizationService); await _nonAnonymousSendCommand.SaveSendAsync(send); return new SendResponseModel(send); @@ -332,6 +344,13 @@ public class SendsController : Controller model.ValidateCreation(); var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); + var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId); + + if (!hasPremium && !string.IsNullOrWhiteSpace(model.Emails)) + { + throw new BadRequestException("Email verified Sends require a premium membership"); + } + var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService); var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value); return new SendFileUploadDataResponseModel @@ -397,6 +416,13 @@ public class SendsController : Controller { model.ValidateEdit(); var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found"); + var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId); + + if (!hasPremium && !string.IsNullOrWhiteSpace(model.Emails)) + { + throw new BadRequestException("Email verified Sends require a premium membership"); + } + var send = await _sendRepository.GetByIdAsync(new Guid(id)); if (send == null || send.UserId != userId) { diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index 6ca9aebc39..a5fe7d4e97 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Api.Tools.Models; using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Response; using Bit.Core; +using Bit.Core.Billing.Premium.Queries; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Platform.Push; @@ -39,6 +40,7 @@ public class SendsControllerTests : IDisposable private readonly ILogger _logger; private readonly IFeatureService _featureService; private readonly IPushNotificationService _pushNotificationService; + private readonly IHasPremiumAccessQuery _hasPremiumAccessQuery; public SendsControllerTests() { @@ -52,6 +54,7 @@ public class SendsControllerTests : IDisposable _logger = Substitute.For>(); _featureService = Substitute.For(); _pushNotificationService = Substitute.For(); + _hasPremiumAccessQuery = Substitute.For(); _sut = new SendsController( _sendRepository, @@ -63,7 +66,8 @@ public class SendsControllerTests : IDisposable _sendFileStorageService, _logger, _featureService, - _pushNotificationService + _pushNotificationService, + _hasPremiumAccessQuery ); } @@ -212,6 +216,7 @@ public class SendsControllerTests : IDisposable public async Task Post_WithEmails_InfersAuthTypeEmail(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); + _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true); var request = new SendRequestModel { Type = SendType.Text, @@ -259,6 +264,68 @@ public class SendsControllerTests : IDisposable _userService.Received(1).GetProperUserId(Arg.Any()); } + [Theory, AutoData] + public async Task Post_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(false); + var request = new SendRequestModel + { + Type = SendType.Text, + Key = "key", + Text = new SendTextModel { Text = "text" }, + Emails = "test@example.com", + DeletionDate = DateTime.UtcNow.AddDays(7) + }; + + var exception = await Assert.ThrowsAsync(() => _sut.Post(request)); + + Assert.Equal("Email verified Sends require a premium membership", exception.Message); + await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task PostFile_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(false); + var request = new SendRequestModel + { + Type = SendType.File, + Key = "key", + File = new SendFileModel { FileName = "test.txt" }, + FileLength = 1024L, + Emails = "test@example.com", + DeletionDate = DateTime.UtcNow.AddDays(7) + }; + + var exception = await Assert.ThrowsAsync(() => _sut.PostFile(request)); + + Assert.Equal("Email verified Sends require a premium membership", exception.Message); + await _nonAnonymousSendCommand.DidNotReceive() + .SaveFileSendAsync(Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory, AutoData] + public async Task Put_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId, Guid sendId) + { + _userService.GetProperUserId(Arg.Any()).Returns(userId); + _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(false); + var request = new SendRequestModel + { + Type = SendType.Text, + Key = "key", + Text = new SendTextModel { Text = "text" }, + Emails = "test@example.com", + DeletionDate = DateTime.UtcNow.AddDays(7) + }; + + var exception = await Assert.ThrowsAsync(() => _sut.Put(sendId.ToString(), request)); + + Assert.Equal("Email verified Sends require a premium membership", exception.Message); + await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any()); + } + [Theory] [InlineData(AuthType.Password)] [InlineData(AuthType.Email)] @@ -518,6 +585,7 @@ public class SendsControllerTests : IDisposable public async Task PostFile_WithEmails_InfersAuthTypeEmail(Guid userId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); + _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true); _nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns("https://example.com/upload") .AndDoes(callInfo => @@ -593,6 +661,7 @@ public class SendsControllerTests : IDisposable public async Task Put_ChangingFromPasswordToEmails_UpdatesAuthTypeToEmail(Guid userId, Guid sendId) { _userService.GetProperUserId(Arg.Any()).Returns(userId); + _hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true); var existingSend = new Send { Id = sendId,