1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 08:32:50 -05:00

[SM-895] Enforce project maximums (#3214)

* Add ProjectLimitQuery

* Add query to DI

* Add unit tests

* Add query to controller

* Add controller unit tests

* add integration tests

* rename query and variables

* More renaming
This commit is contained in:
Thomas Avery
2023-08-28 12:34:37 -05:00
committed by GitHub
parent 7cac93ea90
commit a1d227c121
7 changed files with 194 additions and 0 deletions

View File

@ -0,0 +1,45 @@
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Utilities;
namespace Bit.Commercial.Core.SecretsManager.Queries.Projects;
public class MaxProjectsQuery : IMaxProjectsQuery
{
private readonly IOrganizationRepository _organizationRepository;
private readonly IProjectRepository _projectRepository;
public MaxProjectsQuery(
IOrganizationRepository organizationRepository,
IProjectRepository projectRepository)
{
_organizationRepository = organizationRepository;
_projectRepository = projectRepository;
}
public async Task<(short? max, bool? atMax)> GetByOrgIdAsync(Guid organizationId)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
{
throw new NotFoundException();
}
var plan = StaticStore.GetSecretsManagerPlan(org.PlanType);
if (plan == null)
{
throw new BadRequestException("Existing plan not found.");
}
if (plan.Type == PlanType.Free)
{
var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId);
return projects >= plan.MaxProjects ? (plan.MaxProjects, true) : (plan.MaxProjects, false);
}
return (null, null);
}
}

View File

@ -10,6 +10,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
using Bit.Commercial.Core.SecretsManager.Commands.Trash;
using Bit.Commercial.Core.SecretsManager.Queries;
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces;
@ -19,6 +20,7 @@ using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
using Bit.Core.SecretsManager.Queries.Interfaces;
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
@ -34,6 +36,7 @@ public static class SecretsManagerCollectionExtensions
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();

View File

@ -0,0 +1,97 @@
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects;
[SutProviderCustomize]
public class MaxProjectsQueryTests
{
[Theory]
[BitAutoData]
public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider<MaxProjectsQuery> sutProvider,
Guid organizationId)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(default).ReturnsNull();
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organizationId);
}
[Theory]
[BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsAnnually2019)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)]
public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id));
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData(PlanType.TeamsMonthly)]
[BitAutoData(PlanType.TeamsAnnually)]
[BitAutoData(PlanType.EnterpriseMonthly)]
[BitAutoData(PlanType.EnterpriseAnnually)]
public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id);
Assert.Null(limit);
Assert.Null(overLimit);
await sutProvider.GetDependency<IProjectRepository>().DidNotReceiveWithAnyArgs()
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
[Theory]
[BitAutoData(PlanType.Free, 0, false)]
[BitAutoData(PlanType.Free, 1, false)]
[BitAutoData(PlanType.Free, 2, false)]
[BitAutoData(PlanType.Free, 3, true)]
[BitAutoData(PlanType.Free, 4, true)]
[BitAutoData(PlanType.Free, 40, true)]
public async Task GetByOrgIdAsync_SmFreePlan_Success(PlanType planType, int projects, bool shouldBeAtMax,
SutProvider<MaxProjectsQuery> sutProvider, Organization organization)
{
organization.PlanType = planType;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IProjectRepository>().GetProjectCountByOrganizationIdAsync(organization.Id)
.Returns(projects);
var (max, atMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id);
Assert.NotNull(max);
Assert.NotNull(atMax);
Assert.Equal(3, max.Value);
Assert.Equal(shouldBeAtMax, atMax);
await sutProvider.GetDependency<IProjectRepository>().Received(1)
.GetProjectCountByOrganizationIdAsync(organization.Id);
}
}