diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs index 6e86088dc..055fda0c8 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Projects/CreateProjectCommand.cs @@ -1,4 +1,5 @@ -using Bit.Core.SecretsManager.Commands.Projects.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -6,15 +7,35 @@ namespace Bit.Commercial.Core.SecretsManager.Commands.Projects; public class CreateProjectCommand : ICreateProjectCommand { + private readonly IAccessPolicyRepository _accessPolicyRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IProjectRepository _projectRepository; - public CreateProjectCommand(IProjectRepository projectRepository) + public CreateProjectCommand( + IAccessPolicyRepository accessPolicyRepository, + IOrganizationUserRepository organizationUserRepository, + IProjectRepository projectRepository + ) { + _accessPolicyRepository = accessPolicyRepository; + _organizationUserRepository = organizationUserRepository; _projectRepository = projectRepository; } - public async Task CreateAsync(Project project) + public async Task CreateAsync(Project project, Guid userId) { - return await _projectRepository.CreateAsync(project); + var createdProject = await _projectRepository.CreateAsync(project); + + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(createdProject.OrganizationId, + userId); + var accessPolicy = new UserProjectAccessPolicy() + { + OrganizationUserId = orgUser.Id, + GrantedProjectId = createdProject.Id, + Read = true, + Write = true, + }; + await _accessPolicyRepository.CreateManyAsync(new List { accessPolicy }); + return createdProject; } } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/CreateProjectCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/CreateProjectCommandTests.cs new file mode 100644 index 000000000..3739d4699 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Projects/CreateProjectCommandTests.cs @@ -0,0 +1,40 @@ +using Bit.Commercial.Core.SecretsManager.Commands.Projects; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Commercial.Core.Test.SecretsManager.Projects; + +[SutProviderCustomize] +[ProjectCustomize] +public class CreateProjectCommandTests +{ + [Theory] + [BitAutoData] + public async Task CreateAsync_CallsCreate(Project data, + Guid userId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByOrganizationAsync(Arg.Any(), Arg.Any()) + .Returns(new OrganizationUser() { Id = userId }); + + sutProvider.GetDependency() + .CreateAsync(Arg.Any()) + .Returns(data); + + await sutProvider.Sut.CreateAsync(data, userId); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(data)); + + await sutProvider.GetDependency().Received(1) + .CreateManyAsync(Arg.Any>()); + } +} diff --git a/src/Api/SecretsManager/Controllers/ProjectsController.cs b/src/Api/SecretsManager/Controllers/ProjectsController.cs index 437079d87..9dfb8f11c 100644 --- a/src/Api/SecretsManager/Controllers/ProjectsController.cs +++ b/src/Api/SecretsManager/Controllers/ProjectsController.cs @@ -65,7 +65,8 @@ public class ProjectsController : Controller throw new NotFoundException(); } - var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId)); + var userId = _userService.GetProperUserId(User).Value; + var result = await _createProjectCommand.CreateAsync(createRequest.ToProject(organizationId), userId); return new ProjectResponseModel(result); } diff --git a/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs b/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs index c5f43d360..50cd714cb 100644 --- a/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs +++ b/src/Core/SecretsManager/Commands/Projects/Interfaces/ICreateProjectCommand.cs @@ -4,5 +4,5 @@ namespace Bit.Core.SecretsManager.Commands.Projects.Interfaces; public interface ICreateProjectCommand { - Task CreateAsync(Project project); + Task CreateAsync(Project project, Guid userId); } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs index 1b6db5c9c..fa9dccc93 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTest.cs @@ -1,9 +1,11 @@ using System.Net; using System.Net.Http.Headers; using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.SecretsManager.Enums; using Bit.Api.Models.Response; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; @@ -20,6 +22,7 @@ public class ProjectsControllerTest : IClassFixture, IAsy private readonly HttpClient _client; private readonly ApiApplicationFactory _factory; private readonly IProjectRepository _projectRepository; + private readonly IAccessPolicyRepository _accessPolicyRepository; private string _email = null!; private SecretsManagerOrganizationHelper _organizationHelper = null!; @@ -29,6 +32,7 @@ public class ProjectsControllerTest : IClassFixture, IAsy _factory = factory; _client = _factory.CreateClient(); _projectRepository = _factory.GetService(); + _accessPolicyRepository = _factory.GetService(); } public async Task InitializeAsync() @@ -64,21 +68,28 @@ public class ProjectsControllerTest : IClassFixture, IAsy } [Fact] - public async Task ListByOrganization_Success() + public async Task ListByOrganization_UserWithoutPermission_EmptyList() { var (org, _) = await _organizationHelper.Initialize(true, true); - await LoginAsync(_email); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); - var projectIds = new List(); - for (var i = 0; i < 3; i++) - { - var project = await _projectRepository.CreateAsync(new Project - { - OrganizationId = org.Id, - Name = _mockEncryptedString - }); - projectIds.Add(project.Id); - } + await CreateProjectsAsync(org.Id); + + var response = await _client.GetAsync($"/organizations/{org.Id}/projects"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(result); + Assert.Empty(result!.Data); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task ListByOrganization_Success(PermissionType permissionType) + { + var (projectIds, org) = await SetupProjectsWithAccessAsync(permissionType); var response = await _client.GetAsync($"/organizations/{org.Id}/projects"); response.EnsureSuccessStatusCode(); @@ -104,11 +115,22 @@ public class ProjectsControllerTest : IClassFixture, IAsy Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task Create_Success() + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Create_Success(PermissionType permissionType) { - var (org, _) = await _organizationHelper.Initialize(true, true); + var (org, adminOrgUser) = await _organizationHelper.Initialize(true, true); await LoginAsync(_email); + var orgUserId = adminOrgUser.Id; + + if (permissionType == PermissionType.RunAsUserWithPermission) + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + orgUserId = orgUser.Id; + } + var request = new ProjectCreateRequestModel { Name = _mockEncryptedString }; var response = await _client.PostAsJsonAsync($"/organizations/{org.Id}/projects", request); @@ -126,6 +148,17 @@ public class ProjectsControllerTest : IClassFixture, IAsy AssertHelper.AssertRecent(createdProject.RevisionDate); AssertHelper.AssertRecent(createdProject.CreationDate); Assert.Null(createdProject.DeletedDate); + + // Check permissions have been bootstrapped. + var accessPolicies = await _accessPolicyRepository.GetManyByGrantedProjectIdAsync(createdProject.Id); + Assert.NotNull(accessPolicies); + var ap = (UserProjectAccessPolicy)accessPolicies.First(); + Assert.Equal(createdProject.Id, ap.GrantedProjectId); + Assert.Equal(orgUserId, ap.OrganizationUserId); + Assert.True(ap.Read); + Assert.True(ap.Write); + AssertHelper.AssertRecent(ap.CreationDate); + AssertHelper.AssertRecent(ap.RevisionDate); } [Theory] @@ -140,34 +173,28 @@ public class ProjectsControllerTest : IClassFixture, IAsy var initialProject = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, - Name = _mockEncryptedString + Name = _mockEncryptedString, }); - var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + var mockEncryptedString2 = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; var request = new ProjectCreateRequestModel { Name = mockEncryptedString2 }; var response = await _client.PutAsJsonAsync($"/projects/{initialProject.Id}", request); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } - [Fact] - public async Task Update_Success() + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Update_Success(PermissionType permissionType) { - var (org, _) = await _organizationHelper.Initialize(true, true); - await LoginAsync(_email); + var initialProject = await SetupProjectWithAccessAsync(permissionType); - var initialProject = await _projectRepository.CreateAsync(new Project - { - OrganizationId = org.Id, - Name = _mockEncryptedString - }); + var mockEncryptedString2 = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; - var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; - - var request = new ProjectUpdateRequestModel - { - Name = mockEncryptedString2 - }; + var request = new ProjectUpdateRequestModel { Name = mockEncryptedString2 }; var response = await _client.PutAsJsonAsync($"/projects/{initialProject.Id}", request); response.EnsureSuccessStatusCode(); @@ -180,21 +207,21 @@ public class ProjectsControllerTest : IClassFixture, IAsy Assert.NotNull(result); Assert.Equal(request.Name, updatedProject.Name); AssertHelper.AssertRecent(updatedProject.RevisionDate); - AssertHelper.AssertRecent(updatedProject.CreationDate); Assert.Null(updatedProject.DeletedDate); Assert.NotEqual(initialProject.Name, updatedProject.Name); Assert.NotEqual(initialProject.RevisionDate, updatedProject.RevisionDate); } [Fact] - public async Task Update_NonExistingProject_Throws_NotFound() + public async Task Update_NonExistingProject_NotFound() { await _organizationHelper.Initialize(true, true); await LoginAsync(_email); var request = new ProjectUpdateRequestModel { - Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", + Name = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", }; var response = await _client.PutAsJsonAsync("/projects/c53de509-4581-402c-8cbd-f26d2c516fba", request); @@ -203,7 +230,7 @@ public class ProjectsControllerTest : IClassFixture, IAsy } [Fact] - public async Task Update_MissingAccessPolicy_Throws_NotFound() + public async Task Update_MissingAccessPolicy_NotFound() { var (org, _) = await _organizationHelper.Initialize(true, true); var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); @@ -212,12 +239,13 @@ public class ProjectsControllerTest : IClassFixture, IAsy var project = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, - Name = _mockEncryptedString + Name = _mockEncryptedString, }); var request = new ProjectUpdateRequestModel { - Name = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", + Name = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg=", }; var response = await _client.PutAsJsonAsync($"/projects/{project.Id}", request); @@ -237,10 +265,11 @@ public class ProjectsControllerTest : IClassFixture, IAsy var project = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, - Name = _mockEncryptedString + Name = _mockEncryptedString, }); - var mockEncryptedString2 = "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + var mockEncryptedString2 = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; var request = new ProjectCreateRequestModel { Name = mockEncryptedString2 }; var response = await _client.PutAsJsonAsync($"/projects/{project.Id}", request); @@ -248,27 +277,7 @@ public class ProjectsControllerTest : IClassFixture, IAsy } [Fact] - public async Task Get_Success() - { - var (org, _) = await _organizationHelper.Initialize(true, true); - await LoginAsync(_email); - - var createdProject = await _projectRepository.CreateAsync(new Project - { - OrganizationId = org.Id, - Name = _mockEncryptedString - }); - - var response = await _client.GetAsync($"/projects/{createdProject.Id}"); - response.EnsureSuccessStatusCode(); - var result = await response.Content.ReadFromJsonAsync(); - Assert.Equal(createdProject.Name, result!.Name); - Assert.Equal(createdProject.RevisionDate, result.RevisionDate); - Assert.Equal(createdProject.CreationDate, result.CreationDate); - } - - [Fact] - public async Task Get_MissingAccessPolicy_Throws_NotFound() + public async Task Get_MissingAccessPolicy_NotFound() { var (org, _) = await _organizationHelper.Initialize(true, true); var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); @@ -277,13 +286,28 @@ public class ProjectsControllerTest : IClassFixture, IAsy var createdProject = await _projectRepository.CreateAsync(new Project { OrganizationId = org.Id, - Name = _mockEncryptedString + Name = _mockEncryptedString, }); var response = await _client.GetAsync($"/projects/{createdProject.Id}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Get_Success(PermissionType permissionType) + { + var project = await SetupProjectWithAccessAsync(permissionType); + + var response = await _client.GetAsync($"/projects/{project.Id}"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(); + Assert.Equal(project.Name, result!.Name); + Assert.Equal(project.RevisionDate, result.RevisionDate); + Assert.Equal(project.CreationDate, result.CreationDate); + } + [Theory] [InlineData(false, false)] [InlineData(true, false)] @@ -293,53 +317,124 @@ public class ProjectsControllerTest : IClassFixture, IAsy var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); await LoginAsync(_email); - var projectIds = new List(); - for (var i = 0; i < 3; i++) - { - var project = await _projectRepository.CreateAsync(new Project - { - OrganizationId = org.Id, - Name = _mockEncryptedString, - }); - projectIds.Add(project.Id); - } + var projectIds = await CreateProjectsAsync(org.Id); var response = await _client.PostAsync("/projects/delete", JsonContent.Create(projectIds)); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } [Fact] - public async Task Delete_Success() + public async Task Delete_MissingAccessPolicy_AccessDenied() { var (org, _) = await _organizationHelper.Initialize(true, true); - await LoginAsync(_email); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); - var projectIds = new List(); - for (var i = 0; i < 3; i++) - { - var project = await _projectRepository.CreateAsync(new Project - { - OrganizationId = org.Id, - Name = _mockEncryptedString, - }); - projectIds.Add(project.Id); - } + var projectIds = await CreateProjectsAsync(org.Id); + + var response = await _client.PostAsync("/projects/delete", JsonContent.Create(projectIds)); + + var results = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(results); + Assert.Equal(projectIds.OrderBy(x => x), + results!.Data.Select(x => x.Id).OrderBy(x => x)); + Assert.All(results.Data, item => Assert.Equal("access denied", item.Error)); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task Delete_Success(PermissionType permissionType) + { + var (projectIds, _) = await SetupProjectsWithAccessAsync(permissionType); var response = await _client.PostAsync("/projects/delete", JsonContent.Create(projectIds)); response.EnsureSuccessStatusCode(); var results = await response.Content.ReadFromJsonAsync>(); Assert.NotNull(results); - - var index = 0; - foreach (var result in results!.Data) - { - Assert.Equal(projectIds[index], result.Id); - Assert.Null(result.Error); - index++; - } + Assert.Equal(projectIds.OrderBy(x => x), + results!.Data.Select(x => x.Id).OrderBy(x => x)); + Assert.DoesNotContain(results.Data, x => x.Error != null); var projects = await _projectRepository.GetManyByIds(projectIds); Assert.Empty(projects); } + + private async Task> CreateProjectsAsync(Guid orgId, int numberToCreate = 3) + { + var projectIds = new List(); + for (var i = 0; i < numberToCreate; i++) + { + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = orgId, + Name = _mockEncryptedString, + }); + projectIds.Add(project.Id); + } + + return projectIds; + } + + private async Task<(List, Organization)> SetupProjectsWithAccessAsync(PermissionType permissionType, + int projectsToCreate = 3) + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + var projectIds = await CreateProjectsAsync(org.Id, projectsToCreate); + + if (permissionType == PermissionType.RunAsAdmin) + { + return (projectIds, org); + } + + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var accessPolicies = projectIds.Select(projectId => new UserProjectAccessPolicy + { + GrantedProjectId = projectId, + OrganizationUserId = orgUser.Id, + Read = true, + Write = true, + }) + .Cast() + .ToList(); + + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + + return (projectIds, org); + } + + private async Task SetupProjectWithAccessAsync(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var initialProject = await _projectRepository.CreateAsync(new Project + { + OrganizationId = org.Id, + Name = _mockEncryptedString, + }); + + if (permissionType == PermissionType.RunAsAdmin) + { + return initialProject; + } + + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var accessPolicies = new List + { + new UserProjectAccessPolicy + { + GrantedProjectId = initialProject.Id, OrganizationUserId = orgUser.Id, Read = true, Write = true, + }, + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + + return initialProject; + } } diff --git a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs index 14b477467..2cd3bf774 100644 --- a/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/AccessPoliciesControllerTests.cs @@ -1,5 +1,6 @@ using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.Test.SecretsManager.Enums; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -25,12 +26,6 @@ namespace Bit.Api.Test.SecretsManager.Controllers; [JsonDocumentCustomize] public class AccessPoliciesControllerTests { - public enum PermissionType - { - RunAsAdmin, - RunAsUserWithPermission, - } - private static void SetupAdmin(SutProvider sutProvider, Guid organizationId) { sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); diff --git a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs index 747d21326..39ba7bdba 100644 --- a/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/ProjectsControllerTests.cs @@ -1,10 +1,17 @@ using Bit.Api.SecretsManager.Controllers; +using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.Test.SecretsManager.Enums; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Core.Test.SecretsManager.AutoFixture.ProjectsFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; using NSubstitute; using Xunit; @@ -16,28 +23,226 @@ namespace Bit.Api.Test.SecretsManager.Controllers; [JsonDocumentCustomize] public class ProjectsControllerTests { + private static void SetupAdmin(SutProvider sutProvider, Guid organizationId) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(true); + } + + private static void SetupUserWithPermission(SutProvider sutProvider, Guid organizationId) + { + sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); + sutProvider.GetDependency().OrganizationAdmin(organizationId).Returns(false); + sutProvider.GetDependency().OrganizationUser(default).ReturnsForAnyArgs(true); + } + + [Theory] + [BitAutoData] + public async void ListByOrganization_SmNotEnabled_Throws(SutProvider sutProvider, Guid data) + { + sutProvider.GetDependency().AccessSecretsManager(data).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.ListByOrganizationAsync(data)); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void ListByOrganization_ReturnsEmptyList(PermissionType permissionType, + SutProvider sutProvider, Guid data) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, data); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, data); + break; + } + + var result = await sutProvider.Sut.ListByOrganizationAsync(data); + + await sutProvider.GetDependency().Received(1) + .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)), Arg.Any(), + Arg.Any()); + Assert.Empty(result.Data); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void ListByOrganization_Success(PermissionType permissionType, + SutProvider sutProvider, Guid data, Project mockProject) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, data); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, data); + break; + } + + sutProvider.GetDependency().GetManyByOrganizationIdAsync(default, default, default) + .ReturnsForAnyArgs(new List { mockProject }); + + var result = await sutProvider.Sut.ListByOrganizationAsync(data); + + await sutProvider.GetDependency().Received(1) + .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data)), Arg.Any(), + Arg.Any()); + Assert.NotEmpty(result.Data); + Assert.Single(result.Data); + } + + [Theory] + [BitAutoData] + public async void Create_SmNotEnabled_Throws(SutProvider sutProvider, Guid orgId, + ProjectCreateRequestModel data) + { + sutProvider.GetDependency().AccessSecretsManager(orgId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(orgId, data)); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void Create_Success(PermissionType permissionType, SutProvider sutProvider, + Guid orgId, ProjectCreateRequestModel data) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, orgId); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, orgId); + break; + } + + var resultProject = data.ToProject(orgId); + sutProvider.GetDependency().CreateAsync(default, default) + .ReturnsForAnyArgs(resultProject); + + await sutProvider.Sut.CreateAsync(orgId, data); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void Update_Success(PermissionType permissionType, SutProvider sutProvider, + Guid orgId, ProjectUpdateRequestModel data) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, orgId); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, orgId); + break; + } + + var resultProject = data.ToProject(orgId); + sutProvider.GetDependency().UpdateAsync(default, default) + .ReturnsForAnyArgs(resultProject); + + await sutProvider.Sut.UpdateAsync(orgId, data); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async void Get_SmNotEnabled_Throws(SutProvider sutProvider, Guid data) + { + sutProvider.GetDependency().AccessSecretsManager(data).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAsync(data)); + } + + [Theory] + [BitAutoData] + public async void Get_ThrowsNotFound(SutProvider sutProvider, Guid data) + { + sutProvider.GetDependency().AccessSecretsManager(data).Returns(true); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAsync(data)); + } + + [Theory] + [BitAutoData(PermissionType.RunAsAdmin)] + [BitAutoData(PermissionType.RunAsUserWithPermission)] + public async void Get_Success(PermissionType permissionType, SutProvider sutProvider, + Guid orgId, Guid data) + { + switch (permissionType) + { + case PermissionType.RunAsAdmin: + SetupAdmin(sutProvider, orgId); + break; + case PermissionType.RunAsUserWithPermission: + SetupUserWithPermission(sutProvider, orgId); + sutProvider.GetDependency() + .UserHasReadAccessToProject(Arg.Is(data), Arg.Any()).ReturnsForAnyArgs(true); + break; + } + + sutProvider.GetDependency().GetByIdAsync(Arg.Is(data)) + .ReturnsForAnyArgs(new Project { Id = data, OrganizationId = orgId }); + + await sutProvider.Sut.GetAsync(data); + + await sutProvider.GetDependency().Received(1) + .GetByIdAsync(Arg.Is(data)); + } + + [Theory] + [BitAutoData] + public async void Get_UserWithoutPermission_Throws(SutProvider sutProvider, Guid orgId, + Guid data) + { + SetupUserWithPermission(sutProvider, orgId); + sutProvider.GetDependency().UserHasReadAccessToProject(Arg.Is(data), Arg.Any()) + .ReturnsForAnyArgs(false); + + sutProvider.GetDependency().GetByIdAsync(Arg.Is(data)) + .ReturnsForAnyArgs(new Project { Id = data, OrganizationId = orgId }); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAsync(data)); + } + [Theory] [BitAutoData] public async void BulkDeleteProjects_Success(SutProvider sutProvider, List data) { sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); - var ids = data.Select(project => project.Id)?.ToList(); - var mockResult = new List>(); - foreach (var project in data) - { - mockResult.Add(new Tuple(project, "")); - } + var ids = data.Select(project => project.Id).ToList(); + var mockResult = data.Select(project => new Tuple(project, "")).ToList(); + sutProvider.GetDependency().DeleteProjects(ids, default).ReturnsForAnyArgs(mockResult); var results = await sutProvider.Sut.BulkDeleteAsync(ids); await sutProvider.GetDependency().Received(1) - .DeleteProjects(Arg.Is(ids), Arg.Any()); + .DeleteProjects(Arg.Is(ids), Arg.Any()); Assert.Equal(data.Count, results.Data.Count()); } [Theory] [BitAutoData] - public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException(SutProvider sutProvider) + public async void BulkDeleteProjects_NoGuids_ThrowsArgumentNullException( + SutProvider sutProvider) { sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAsync(new List())); diff --git a/test/Api.Test/SecretsManager/Enums/PermissionType.cs b/test/Api.Test/SecretsManager/Enums/PermissionType.cs new file mode 100644 index 000000000..e411859fc --- /dev/null +++ b/test/Api.Test/SecretsManager/Enums/PermissionType.cs @@ -0,0 +1,7 @@ +namespace Bit.Api.Test.SecretsManager.Enums; + +public enum PermissionType +{ + RunAsAdmin, + RunAsUserWithPermission, +}