diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index e7275a6d2..be5eabb6e 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -57,6 +57,7 @@ public class OrganizationService : IOrganizationService private readonly IProviderUserRepository _providerUserRepository; private readonly ICountNewSmSeatsRequiredQuery _countNewSmSeatsRequiredQuery; private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; + private readonly IProviderRepository _providerRepository; private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IFeatureService _featureService; @@ -90,6 +91,7 @@ public class OrganizationService : IOrganizationService IOrgUserInviteTokenableFactory orgUserInviteTokenableFactory, IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, + IProviderRepository providerRepository, IFeatureService featureService) { _organizationRepository = organizationRepository; @@ -118,6 +120,7 @@ public class OrganizationService : IOrganizationService _providerUserRepository = providerUserRepository; _countNewSmSeatsRequiredQuery = countNewSmSeatsRequiredQuery; _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; + _providerRepository = providerRepository; _orgUserInviteTokenableFactory = orgUserInviteTokenableFactory; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _featureService = featureService; @@ -862,7 +865,7 @@ public class OrganizationService : IOrganizationService if (newSeatsRequired > 0) { - var (canScale, failureReason) = CanScale(organization, newSeatsRequired); + var (canScale, failureReason) = await CanScaleAsync(organization, newSeatsRequired); if (!canScale) { throw new BadRequestException(failureReason); @@ -1182,7 +1185,8 @@ public class OrganizationService : IOrganizationService return result; } - internal (bool canScale, string failureReason) CanScale(Organization organization, + internal async Task<(bool canScale, string failureReason)> CanScaleAsync( + Organization organization, int seatsToAdd) { var failureReason = ""; @@ -1197,6 +1201,13 @@ public class OrganizationService : IOrganizationService return (true, failureReason); } + var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); + + if (provider is { Enabled: true, Type: ProviderType.Reseller }) + { + return (false, "Seat limit has been reached. Contact your provider to purchase additional seats."); + } + if (organization.Seats.HasValue && organization.MaxAutoscaleSeats.HasValue && organization.MaxAutoscaleSeats.Value < organization.Seats.Value + seatsToAdd) @@ -1214,7 +1225,7 @@ public class OrganizationService : IOrganizationService return; } - var (canScale, failureMessage) = CanScale(organization, seatsToAdd); + var (canScale, failureMessage) = await CanScaleAsync(organization, seatsToAdd); if (!canScale) { throw new BadRequestException(failureMessage); diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs index 210e59681..a169a0093 100644 --- a/test/Core.Test/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/Services/OrganizationServiceTests.cs @@ -34,6 +34,7 @@ using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Fakes; using NSubstitute; using NSubstitute.ExceptionExtensions; +using NSubstitute.ReturnsExtensions; using Xunit; using Organization = Bit.Core.Entities.Organization; using OrganizationUser = Bit.Core.Entities.OrganizationUser; @@ -1606,15 +1607,16 @@ public class OrganizationServiceTests [BitAutoData(0, null, 100, true, "")] [BitAutoData(1, 100, null, true, "")] [BitAutoData(1, 100, 100, false, "Seat limit has been reached")] - public void CanScale(int seatsToAdd, int? currentSeats, int? maxAutoscaleSeats, + public async Task CanScaleAsync(int seatsToAdd, int? currentSeats, int? maxAutoscaleSeats, bool expectedResult, string expectedFailureMessage, Organization organization, SutProvider sutProvider) { organization.Seats = currentSeats; organization.MaxAutoscaleSeats = maxAutoscaleSeats; sutProvider.GetDependency().ManageUsers(organization.Id).Returns(true); + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).ReturnsNull(); - var (result, failureMessage) = sutProvider.Sut.CanScale(organization, seatsToAdd); + var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, seatsToAdd); if (expectedFailureMessage == string.Empty) { @@ -1628,16 +1630,35 @@ public class OrganizationServiceTests } [Theory, PaidOrganizationCustomize, BitAutoData] - public void CanScale_FailsOnSelfHosted(Organization organization, + public async Task CanScaleAsync_FailsOnSelfHosted(Organization organization, SutProvider sutProvider) { sutProvider.GetDependency().SelfHosted.Returns(true); - var (result, failureMessage) = sutProvider.Sut.CanScale(organization, 10); + var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, 10); Assert.False(result); Assert.Contains("Cannot autoscale on self-hosted instance", failureMessage); } + [Theory, PaidOrganizationCustomize, BitAutoData] + public async Task CanScaleAsync_FailsOnResellerManagedOrganization( + Organization organization, + SutProvider sutProvider) + { + var provider = new Provider + { + Enabled = true, + Type = ProviderType.Reseller + }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).Returns(provider); + + var (result, failureMessage) = await sutProvider.Sut.CanScaleAsync(organization, 10); + + Assert.False(result); + Assert.Contains("Seat limit has been reached. Contact your provider to purchase additional seats.", failureMessage); + } + [Theory, PaidOrganizationCustomize, BitAutoData] public async Task Delete_Success(Organization organization, SutProvider sutProvider) {