1
0
mirror of https://github.com/bitwarden/server synced 2026-02-21 11:53:42 +00:00

gate add/edit endpoints behind premium membership and add test coverage (#7043)

This commit is contained in:
John Harrington
2026-02-20 07:48:18 -07:00
committed by GitHub
parent 708ea66393
commit a961626957
2 changed files with 97 additions and 2 deletions

View File

@@ -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<SendsController> _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<SendsController> 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)
{

View File

@@ -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<SendsController> _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<ILogger<SendsController>>();
_featureService = Substitute.For<IFeatureService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_hasPremiumAccessQuery = Substitute.For<IHasPremiumAccessQuery>();
_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<ClaimsPrincipal>()).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<ClaimsPrincipal>());
}
[Theory, AutoData]
public async Task Post_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).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<BadRequestException>(() => _sut.Post(request));
Assert.Equal("Email verified Sends require a premium membership", exception.Message);
await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any<Send>());
}
[Theory, AutoData]
public async Task PostFile_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).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<BadRequestException>(() => _sut.PostFile(request));
Assert.Equal("Email verified Sends require a premium membership", exception.Message);
await _nonAnonymousSendCommand.DidNotReceive()
.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>());
}
[Theory, AutoData]
public async Task Put_WithEmails_WhenNotPremium_ThrowsBadRequestException(Guid userId, Guid sendId)
{
_userService.GetProperUserId(Arg.Any<ClaimsPrincipal>()).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<BadRequestException>(() => _sut.Put(sendId.ToString(), request));
Assert.Equal("Email verified Sends require a premium membership", exception.Message);
await _nonAnonymousSendCommand.DidNotReceive().SaveSendAsync(Arg.Any<Send>());
}
[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<ClaimsPrincipal>()).Returns(userId);
_hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true);
_nonAnonymousSendCommand.SaveFileSendAsync(Arg.Any<Send>(), Arg.Any<SendFileData>(), Arg.Any<long>())
.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<ClaimsPrincipal>()).Returns(userId);
_hasPremiumAccessQuery.HasPremiumAccessAsync(userId).Returns(true);
var existingSend = new Send
{
Id = sendId,