mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +01: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:
parent
7cac93ea90
commit
a1d227c121
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ using Bit.Commercial.Core.SecretsManager.Commands.Secrets;
|
|||||||
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
|
||||||
using Bit.Commercial.Core.SecretsManager.Commands.Trash;
|
using Bit.Commercial.Core.SecretsManager.Commands.Trash;
|
||||||
using Bit.Commercial.Core.SecretsManager.Queries;
|
using Bit.Commercial.Core.SecretsManager.Queries;
|
||||||
|
using Bit.Commercial.Core.SecretsManager.Queries.Projects;
|
||||||
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
|
using Bit.Commercial.Core.SecretsManager.Queries.ServiceAccounts;
|
||||||
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Commands.AccessTokens.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.ServiceAccounts.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
|
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Queries.Interfaces;
|
using Bit.Core.SecretsManager.Queries.Interfaces;
|
||||||
|
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
|
using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -34,6 +36,7 @@ public static class SecretsManagerCollectionExtensions
|
|||||||
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, ServiceAccountAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, AccessPolicyAuthorizationHandler>();
|
||||||
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
services.AddScoped<IAccessClientQuery, AccessClientQuery>();
|
||||||
|
services.AddScoped<IMaxProjectsQuery, MaxProjectsQuery>();
|
||||||
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
services.AddScoped<IServiceAccountSecretsDetailsQuery, ServiceAccountSecretsDetailsQuery>();
|
||||||
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
services.AddScoped<ICreateSecretCommand, CreateSecretCommand>();
|
||||||
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
services.AddScoped<IUpdateSecretCommand, UpdateSecretCommand>();
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
|
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -22,6 +23,7 @@ public class ProjectsController : Controller
|
|||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IProjectRepository _projectRepository;
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly IMaxProjectsQuery _maxProjectsQuery;
|
||||||
private readonly ICreateProjectCommand _createProjectCommand;
|
private readonly ICreateProjectCommand _createProjectCommand;
|
||||||
private readonly IUpdateProjectCommand _updateProjectCommand;
|
private readonly IUpdateProjectCommand _updateProjectCommand;
|
||||||
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
||||||
@ -31,6 +33,7 @@ public class ProjectsController : Controller
|
|||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IProjectRepository projectRepository,
|
IProjectRepository projectRepository,
|
||||||
|
IMaxProjectsQuery maxProjectsQuery,
|
||||||
ICreateProjectCommand createProjectCommand,
|
ICreateProjectCommand createProjectCommand,
|
||||||
IUpdateProjectCommand updateProjectCommand,
|
IUpdateProjectCommand updateProjectCommand,
|
||||||
IDeleteProjectCommand deleteProjectCommand,
|
IDeleteProjectCommand deleteProjectCommand,
|
||||||
@ -39,6 +42,7 @@ public class ProjectsController : Controller
|
|||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_projectRepository = projectRepository;
|
_projectRepository = projectRepository;
|
||||||
|
_maxProjectsQuery = maxProjectsQuery;
|
||||||
_createProjectCommand = createProjectCommand;
|
_createProjectCommand = createProjectCommand;
|
||||||
_updateProjectCommand = updateProjectCommand;
|
_updateProjectCommand = updateProjectCommand;
|
||||||
_deleteProjectCommand = deleteProjectCommand;
|
_deleteProjectCommand = deleteProjectCommand;
|
||||||
@ -74,6 +78,13 @@ public class ProjectsController : Controller
|
|||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (max, atMax) = await _maxProjectsQuery.GetByOrgIdAsync(organizationId);
|
||||||
|
if (atMax != null && atMax.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException($"You have reached the maximum number of projects ({max}) for this plan.");
|
||||||
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User).Value;
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.ClientType);
|
var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.ClientType);
|
||||||
|
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
namespace Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
|
|
||||||
|
public interface IMaxProjectsQuery
|
||||||
|
{
|
||||||
|
Task<(short? max, bool? atMax)> GetByOrgIdAsync(Guid organizationId);
|
||||||
|
}
|
@ -116,6 +116,19 @@ public class ProjectsControllerTests : IClassFixture<ApiApplicationFactory>, IAs
|
|||||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
public async Task Create_AtMaxProjects_BadRequest(PermissionType permissionType)
|
||||||
|
{
|
||||||
|
var (_, organization) = await SetupProjectsWithAccessAsync(permissionType, 3);
|
||||||
|
var request = new ProjectCreateRequestModel { Name = _mockEncryptedString };
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync($"/organizations/{organization.Id}/projects", request);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(PermissionType.RunAsAdmin)]
|
[InlineData(PermissionType.RunAsAdmin)]
|
||||||
[InlineData(PermissionType.RunAsUserWithPermission)]
|
[InlineData(PermissionType.RunAsUserWithPermission)]
|
||||||
|
@ -8,6 +8,7 @@ using Bit.Core.Exceptions;
|
|||||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Entities;
|
using Bit.Core.SecretsManager.Entities;
|
||||||
using Bit.Core.SecretsManager.Models.Data;
|
using Bit.Core.SecretsManager.Models.Data;
|
||||||
|
using Bit.Core.SecretsManager.Queries.Projects.Interfaces;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture;
|
||||||
@ -122,6 +123,24 @@ public class ProjectsControllerTests
|
|||||||
.CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().ClientType);
|
.CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().ClientType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async void Create_AtMaxProjects_Throws(SutProvider<ProjectsController> sutProvider,
|
||||||
|
Guid orgId, ProjectCreateRequestModel data)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.ToProject(orgId),
|
||||||
|
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
||||||
|
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid());
|
||||||
|
sutProvider.GetDependency<IMaxProjectsQuery>().GetByOrgIdAsync(orgId).Returns(((short)3, true));
|
||||||
|
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAsync(orgId, data));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICreateProjectCommand>().DidNotReceiveWithAnyArgs()
|
||||||
|
.CreateAsync(Arg.Any<Project>(), Arg.Any<Guid>(), sutProvider.GetDependency<ICurrentContext>().ClientType);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async void Create_Success(SutProvider<ProjectsController> sutProvider,
|
public async void Create_Success(SutProvider<ProjectsController> sutProvider,
|
||||||
|
Loading…
Reference in New Issue
Block a user