From 77f8cc58e830c12daf273cd5803a5598d1e8fc81 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com> Date: Thu, 8 Aug 2024 15:12:52 +0200 Subject: [PATCH] SM-1146: Secrets Manager total counts (#4200) * SM-1146: SM Organization Counts for Projects, Secrets, Machine Accounts * SM-1146: Project total counts * SM-1146: models object renames * SM-1146: Service Account total counts * SM-1146: Unit test coverage for counts controller * SM-1146: Counts controller simplification, UT update * SM-1146: Service Account total counts from Service Account auth user * SM-1146: Integration Tests for total counts controller * SM-1146: Explicitly denying access for Service Accounts * SM-1146: Fix broken ProjectsController integration test * SM-1146: Integration tests for counts controller * SM-1146: Explicitly denying access for Service Accounts cleanup * SM-1146: Test cleanup * SM-1146: PR review comments fix * SM-1146: People, Service Accounts positive count on write access * Update bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> --- .../Repositories/ProjectRepository.cs | 52 ++ .../Repositories/SecretRepository.cs | 17 + .../Repositories/ServiceAccountRepository.cs | 42 ++ .../Controllers/CountsController.cs | 119 ++++ .../OrganizationCountsResponseModel.cs | 15 + .../Response/ProjectCountsResponseModel.cs | 15 + .../ServiceAccountCountsResponseModel.cs | 15 + .../Models/Data/ProjectCounts.cs | 10 + .../Models/Data/ServiceAccountCounts.cs | 10 + .../Repositories/IProjectRepository.cs | 2 + .../Repositories/ISecretRepository.cs | 1 + .../Repositories/IServiceAccountRepository.cs | 3 + .../Noop/NoopProjectRepository.cs | 11 + .../Repositories/Noop/NoopSecretRepository.cs | 6 + .../Noop/NoopServiceAccountRepository.cs | 12 + .../SecretsManager/Models/ServiceAccount.cs | 2 + .../Controllers/CountsControllerTests.cs | 550 ++++++++++++++++++ .../Controllers/ProjectsControllerTests.cs | 6 +- .../Controllers/CountsControllerTests.cs | 212 +++++++ 19 files changed, 1095 insertions(+), 5 deletions(-) create mode 100644 src/Api/SecretsManager/Controllers/CountsController.cs create mode 100644 src/Api/SecretsManager/Models/Response/OrganizationCountsResponseModel.cs create mode 100644 src/Api/SecretsManager/Models/Response/ProjectCountsResponseModel.cs create mode 100644 src/Api/SecretsManager/Models/Response/ServiceAccountCountsResponseModel.cs create mode 100644 src/Core/SecretsManager/Models/Data/ProjectCounts.cs create mode 100644 src/Core/SecretsManager/Models/Data/ServiceAccountCounts.cs create mode 100644 test/Api.IntegrationTest/SecretsManager/Controllers/CountsControllerTests.cs create mode 100644 test/Api.Test/SecretsManager/Controllers/CountsControllerTests.cs diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs index 55360a7248..99d34e8cf5 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ProjectRepository.cs @@ -169,6 +169,58 @@ public class ProjectRepository : Repository pa.Id, pa => (pa.Read, pa.Write)); } + public async Task GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId, + AccessClientType accessType) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.Project.Where(p => p.OrganizationId == organizationId && p.DeletedDate == null); + + query = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasReadAccessToProject(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + return await query.CountAsync(); + } + + public async Task GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.Project.Where(p => p.Id == projectId && p.DeletedDate == null); + + var queryReadAccess = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasReadAccessToProject(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + var queryWriteAccess = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasWriteAccessToProject(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + var secretsQuery = queryReadAccess.Select(project => project.Secrets.Count(s => s.DeletedDate == null)); + + var projectCountsQuery = queryWriteAccess.Select(project => new ProjectCounts + { + People = project.UserAccessPolicies.Count + project.GroupAccessPolicies.Count, + ServiceAccounts = project.ServiceAccountAccessPolicies.Count + }); + + var secrets = await secretsQuery.FirstOrDefaultAsync(); + var projectCounts = await projectCountsQuery.FirstOrDefaultAsync() ?? new ProjectCounts { Secrets = 0, People = 0, ServiceAccounts = 0 }; + projectCounts.Secrets = secrets; + + return projectCounts; + } + private record ProjectAccess(Guid Id, bool Read, bool Write); private static IQueryable BuildProjectAccessQuery(IQueryable projectQuery, Guid userId, diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs index a608fd2079..8b23e4cfde 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs @@ -325,6 +325,23 @@ public class SecretRepository : Repository GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId, + AccessClientType accessType) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.Secret.Where(s => s.OrganizationId == organizationId && s.DeletedDate == null); + + query = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasReadAccessToSecret(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + return await query.CountAsync(); + } + private IQueryable SecretToPermissionDetails(IQueryable query, Guid userId, AccessClientType accessType) { var secrets = accessType switch diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs index ffeb939e2d..20c457730b 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/ServiceAccountRepository.cs @@ -125,6 +125,48 @@ public class ServiceAccountRepository : Repository GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId, + AccessClientType accessType) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.ServiceAccount.Where(sa => sa.OrganizationId == organizationId); + + query = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasReadAccessToServiceAccount(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + return await query.CountAsync(); + } + + public async Task GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId, + AccessClientType accessType) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + var query = dbContext.ServiceAccount.Where(sa => sa.Id == serviceAccountId); + + query = accessType switch + { + AccessClientType.NoAccessCheck => query, + AccessClientType.User => query.Where(UserHasReadAccessToServiceAccount(userId)), + _ => throw new ArgumentOutOfRangeException(nameof(accessType), accessType, null), + }; + + var serviceAccountCountsQuery = query.Select(serviceAccount => new ServiceAccountCounts + { + Projects = serviceAccount.ProjectAccessPolicies.Count, + People = serviceAccount.UserAccessPolicies.Count + serviceAccount.GroupAccessPolicies.Count, + AccessTokens = serviceAccount.ApiKeys.Count + }); + + var serviceAccountCounts = await serviceAccountCountsQuery.FirstOrDefaultAsync(); + return serviceAccountCounts ?? new ServiceAccountCounts { Projects = 0, People = 0, AccessTokens = 0 }; + } + public async Task ServiceAccountsAreInOrganizationAsync(List serviceAccountIds, Guid organizationId) { await using var scope = ServiceScopeFactory.CreateAsyncScope(); diff --git a/src/Api/SecretsManager/Controllers/CountsController.cs b/src/Api/SecretsManager/Controllers/CountsController.cs new file mode 100644 index 0000000000..a37708d9ac --- /dev/null +++ b/src/Api/SecretsManager/Controllers/CountsController.cs @@ -0,0 +1,119 @@ +#nullable enable +using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Queries.Interfaces; +using Bit.Core.SecretsManager.Repositories; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.SecretsManager.Controllers; + +[Authorize("secrets")] +public class CountsController : Controller +{ + private readonly ICurrentContext _currentContext; + private readonly IAccessClientQuery _accessClientQuery; + private readonly IProjectRepository _projectRepository; + private readonly ISecretRepository _secretRepository; + private readonly IServiceAccountRepository _serviceAccountRepository; + + public CountsController( + ICurrentContext currentContext, + IAccessClientQuery accessClientQuery, + IProjectRepository projectRepository, + ISecretRepository secretRepository, + IServiceAccountRepository serviceAccountRepository) + { + _currentContext = currentContext; + _accessClientQuery = accessClientQuery; + _projectRepository = projectRepository; + _secretRepository = secretRepository; + _serviceAccountRepository = serviceAccountRepository; + } + + [HttpGet("organizations/{organizationId}/sm-counts")] + public async Task GetByOrganizationAsync([FromRoute] Guid organizationId) + { + var (accessType, userId) = await GetAccessClientAsync(organizationId); + + var projectsCountTask = _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId, + userId, accessType); + + var secretsCountTask = _secretRepository.GetSecretsCountByOrganizationIdAsync(organizationId, + userId, accessType); + + var serviceAccountsCountsTask = _serviceAccountRepository.GetServiceAccountCountByOrganizationIdAsync( + organizationId, userId, accessType); + + var counts = await Task.WhenAll(projectsCountTask, secretsCountTask, serviceAccountsCountsTask); + + return new OrganizationCountsResponseModel + { + Projects = counts[0], + Secrets = counts[1], + ServiceAccounts = counts[2] + }; + } + + + [HttpGet("projects/{projectId}/sm-counts")] + public async Task GetByProjectAsync([FromRoute] Guid projectId) + { + var project = await _projectRepository.GetByIdAsync(projectId); + if (project == null) + { + throw new NotFoundException(); + } + + var (accessType, userId) = await GetAccessClientAsync(project.OrganizationId); + + var projectsCounts = await _projectRepository.GetProjectCountsByIdAsync(projectId, userId, accessType); + + return new ProjectCountsResponseModel + { + Secrets = projectsCounts.Secrets, + People = projectsCounts.People, + ServiceAccounts = projectsCounts.ServiceAccounts + }; + } + + [HttpGet("service-accounts/{serviceAccountId}/sm-counts")] + public async Task GetByServiceAccountAsync([FromRoute] Guid serviceAccountId) + { + var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId); + if (serviceAccount == null) + { + throw new NotFoundException(); + } + + var (accessType, userId) = await GetAccessClientAsync(serviceAccount.OrganizationId); + + var serviceAccountCounts = + await _serviceAccountRepository.GetServiceAccountCountsByIdAsync(serviceAccountId, userId, accessType); + + return new ServiceAccountCountsResponseModel + { + Projects = serviceAccountCounts.Projects, + People = serviceAccountCounts.People, + AccessTokens = serviceAccountCounts.AccessTokens + }; + } + + private async Task<(AccessClientType, Guid)> GetAccessClientAsync(Guid organizationId) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + var (accessType, userId) = await _accessClientQuery.GetAccessClientAsync(User, organizationId); + if (accessType == AccessClientType.ServiceAccount) + { + throw new NotFoundException(); + } + + return (accessType, userId); + } +} diff --git a/src/Api/SecretsManager/Models/Response/OrganizationCountsResponseModel.cs b/src/Api/SecretsManager/Models/Response/OrganizationCountsResponseModel.cs new file mode 100644 index 0000000000..bb3f0013bd --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/OrganizationCountsResponseModel.cs @@ -0,0 +1,15 @@ +#nullable enable +using Bit.Core.Models.Api; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class OrganizationCountsResponseModel() : ResponseModel(_objectName) +{ + private const string _objectName = "organizationCounts"; + + public int Projects { get; set; } + + public int Secrets { get; set; } + + public int ServiceAccounts { get; set; } +} diff --git a/src/Api/SecretsManager/Models/Response/ProjectCountsResponseModel.cs b/src/Api/SecretsManager/Models/Response/ProjectCountsResponseModel.cs new file mode 100644 index 0000000000..df921a7b1a --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/ProjectCountsResponseModel.cs @@ -0,0 +1,15 @@ +#nullable enable +using Bit.Core.Models.Api; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class ProjectCountsResponseModel() : ResponseModel(_objectName) +{ + private const string _objectName = "projectCounts"; + + public int Secrets { get; set; } + + public int People { get; set; } + + public int ServiceAccounts { get; set; } +} diff --git a/src/Api/SecretsManager/Models/Response/ServiceAccountCountsResponseModel.cs b/src/Api/SecretsManager/Models/Response/ServiceAccountCountsResponseModel.cs new file mode 100644 index 0000000000..ac457d2539 --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/ServiceAccountCountsResponseModel.cs @@ -0,0 +1,15 @@ +#nullable enable +using Bit.Core.Models.Api; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class ServiceAccountCountsResponseModel() : ResponseModel(_objectName) +{ + private const string _objectName = "serviceAccountCounts"; + + public int Projects { get; set; } + + public int People { get; set; } + + public int AccessTokens { get; set; } +} diff --git a/src/Core/SecretsManager/Models/Data/ProjectCounts.cs b/src/Core/SecretsManager/Models/Data/ProjectCounts.cs new file mode 100644 index 0000000000..fa809c1fbf --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/ProjectCounts.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.SecretsManager.Models.Data; + +public class ProjectCounts +{ + public int Secrets { get; set; } + + public int People { get; set; } + + public int ServiceAccounts { get; set; } +} diff --git a/src/Core/SecretsManager/Models/Data/ServiceAccountCounts.cs b/src/Core/SecretsManager/Models/Data/ServiceAccountCounts.cs new file mode 100644 index 0000000000..261453dc23 --- /dev/null +++ b/src/Core/SecretsManager/Models/Data/ServiceAccountCounts.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.SecretsManager.Models.Data; + +public class ServiceAccountCounts +{ + public int Projects { get; set; } + + public int People { get; set; } + + public int AccessTokens { get; set; } +} diff --git a/src/Core/SecretsManager/Repositories/IProjectRepository.cs b/src/Core/SecretsManager/Repositories/IProjectRepository.cs index cc3aa40cf7..7a084b42cc 100644 --- a/src/Core/SecretsManager/Repositories/IProjectRepository.cs +++ b/src/Core/SecretsManager/Repositories/IProjectRepository.cs @@ -17,6 +17,8 @@ public interface IProjectRepository Task<(bool Read, bool Write)> AccessToProjectAsync(Guid id, Guid userId, AccessClientType accessType); Task ProjectsAreInOrganization(List projectIds, Guid organizationId); Task GetProjectCountByOrganizationIdAsync(Guid organizationId); + Task GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); + Task GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType); Task> AccessToProjectsAsync(IEnumerable projectIds, Guid userId, AccessClientType accessType); } diff --git a/src/Core/SecretsManager/Repositories/ISecretRepository.cs b/src/Core/SecretsManager/Repositories/ISecretRepository.cs index 693baf85ca..20ebb61e9a 100644 --- a/src/Core/SecretsManager/Repositories/ISecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/ISecretRepository.cs @@ -24,4 +24,5 @@ public interface ISecretRepository Task> AccessToSecretsAsync(IEnumerable ids, Guid userId, AccessClientType accessType); Task EmptyTrash(DateTime nowTime, uint deleteAfterThisNumberOfDays); Task GetSecretsCountByOrganizationIdAsync(Guid organizationId); + Task GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); } diff --git a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs index 9fa412ddf5..a2d12578d5 100644 --- a/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs +++ b/src/Core/SecretsManager/Repositories/IServiceAccountRepository.cs @@ -17,6 +17,9 @@ public interface IServiceAccountRepository Task> AccessToServiceAccountsAsync(IEnumerable ids, Guid userId, AccessClientType accessType); Task GetServiceAccountCountByOrganizationIdAsync(Guid organizationId); + Task GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); + Task GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId, AccessClientType accessType); + Task> GetManyByOrganizationIdWithSecretsDetailsAsync(Guid organizationId, Guid userId, AccessClientType accessType); Task ServiceAccountsAreInOrganizationAsync(List serviceAccountIds, Guid organizationId); } diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs index acd428a676..439b32197a 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopProjectRepository.cs @@ -63,6 +63,17 @@ public class NoopProjectRepository : IProjectRepository return Task.FromResult(0); } + public Task GetProjectCountByOrganizationIdAsync(Guid organizationId, Guid userId, + AccessClientType accessType) + { + return Task.FromResult(0); + } + + public Task GetProjectCountsByIdAsync(Guid projectId, Guid userId, AccessClientType accessType) + { + return Task.FromResult(null as ProjectCounts); + } + public Task> AccessToProjectsAsync(IEnumerable projectIds, Guid userId, AccessClientType accessType) { diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs index ba1d3ccb0b..2d434df597 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs @@ -96,4 +96,10 @@ public class NoopSecretRepository : ISecretRepository { return Task.FromResult(0); } + + public Task GetSecretsCountByOrganizationIdAsync(Guid organizationId, Guid userId, + AccessClientType accessType) + { + return Task.FromResult(0); + } } diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs index 8b5ece931e..7155608bcf 100644 --- a/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs +++ b/src/Core/SecretsManager/Repositories/Noop/NoopServiceAccountRepository.cs @@ -64,6 +64,18 @@ public class NoopServiceAccountRepository : IServiceAccountRepository return Task.FromResult(0); } + public Task GetServiceAccountCountByOrganizationIdAsync(Guid organizationId, Guid userId, + AccessClientType accessType) + { + return Task.FromResult(0); + } + + public Task GetServiceAccountCountsByIdAsync(Guid serviceAccountId, Guid userId, + AccessClientType accessType) + { + return Task.FromResult(null as ServiceAccountCounts); + } + public Task> GetManyByOrganizationIdWithSecretsDetailsAsync( Guid organizationId, Guid userId, AccessClientType accessType) { diff --git a/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs b/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs index 2587160792..812740e7ae 100644 --- a/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs +++ b/src/Infrastructure.EntityFramework/SecretsManager/Models/ServiceAccount.cs @@ -8,6 +8,8 @@ public class ServiceAccount : Core.SecretsManager.Entities.ServiceAccount public virtual Organization Organization { get; set; } public virtual ICollection GroupAccessPolicies { get; set; } public virtual ICollection UserAccessPolicies { get; set; } + public virtual ICollection ProjectAccessPolicies { get; set; } + public virtual ICollection ApiKeys { get; set; } } public class ServiceAccountMapperProfile : Profile diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/CountsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/CountsControllerTests.cs new file mode 100644 index 0000000000..eb4b4de8f4 --- /dev/null +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/CountsControllerTests.cs @@ -0,0 +1,550 @@ +using System.Net; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.SecretsManager.Enums; +using Bit.Api.IntegrationTest.SecretsManager.Helpers; +using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Repositories; +using Xunit; + +namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; + +public class CountsControllerTests : IClassFixture, IAsyncLifetime +{ + private readonly string _mockEncryptedString = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly IProjectRepository _projectRepository; + private readonly ISecretRepository _secretRepository; + private readonly IServiceAccountRepository _serviceAccountRepository; + private readonly IApiKeyRepository _apiKeyRepository; + private readonly IAccessPolicyRepository _accessPolicyRepository; + private readonly IGroupRepository _groupRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly LoginHelper _loginHelper; + + private string _email = null!; + private SecretsManagerOrganizationHelper _organizationHelper = null!; + + + public CountsControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + _projectRepository = _factory.GetService(); + _secretRepository = _factory.GetService(); + _serviceAccountRepository = _factory.GetService(); + _apiKeyRepository = _factory.GetService(); + _accessPolicyRepository = _factory.GetService(); + _groupRepository = _factory.GetService(); + _organizationUserRepository = _factory.GetService(); + _loginHelper = new LoginHelper(_factory, _client); + } + + + public async Task InitializeAsync() + { + _email = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(_email); + _organizationHelper = new SecretsManagerOrganizationHelper(_factory, _email); + } + + public Task DisposeAsync() + { + _client.Dispose(); + return Task.CompletedTask; + } + + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public async Task GetByOrganizationAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, + bool organizationEnabled) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); + await _loginHelper.LoginAsync(_email); + + var response = await _client.GetAsync($"/organizations/{org.Id}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetByOrganizationAsync_RunAsServiceAccount_NotFound() + { + var (_, org, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsServiceAccountWithPermission); + + var response = await _client.GetAsync($"/organizations/{org.Id}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetByOrganizationAsync_UserWithoutPermission_ZeroCounts() + { + var (_, org, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsUserWithPermission, 0); + + var projects = await CreateProjectsAsync(org.Id); + await CreateSecretsAsync(org.Id, projects[0]); + await CreateServiceAccountsAsync(org.Id); + + var response = await _client.GetAsync($"/organizations/{org.Id}/sm-counts"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Equal(0, result.Projects); + Assert.Equal(0, result.Secrets); + Assert.Equal(0, result.ServiceAccounts); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetByOrganizationAsync_Success(PermissionType permissionType) + { + var (projects, org, user) = await SetupProjectsWithAccessAsync(permissionType); + var projectsWithoutAccess = await CreateProjectsAsync(org.Id); + + var secrets = await CreateSecretsAsync(org.Id, projects[0]); + var secretsWithoutAccess = await CreateSecretsAsync(org.Id, projectsWithoutAccess[0]); + var secretsWithoutProject = await CreateSecretsAsync(org.Id, null); + + var serviceAccounts = await CreateServiceAccountsAsync(org.Id); + await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccounts[0].Id); + + var response = await _client.GetAsync($"/organizations/{org.Id}/sm-counts"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + if (permissionType == PermissionType.RunAsAdmin) + { + Assert.Equal(projects.Count + projectsWithoutAccess.Count, result.Projects); + Assert.Equal(secrets.Count + secretsWithoutAccess.Count + secretsWithoutProject.Count, + result.Secrets); + Assert.Equal(serviceAccounts.Count, result.ServiceAccounts); + } + else + { + Assert.Equal(projects.Count, result.Projects); + Assert.Equal(secrets.Count, result.Secrets); + Assert.Equal(1, result.ServiceAccounts); + } + } + + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public async Task GetByProjectAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, + bool organizationEnabled) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); + await _loginHelper.LoginAsync(_email); + + var projects = await CreateProjectsAsync(org.Id); + + var response = await _client.GetAsync($"/projects/{projects[0].Id}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetByProjectAsync_RunAsServiceAccount_NotFound() + { + var (projects, _, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsServiceAccountWithPermission); + + var response = await _client.GetAsync($"/projects/{projects[0].Id}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetByProjectAsync_NonExistingProject_NotFound(PermissionType permissionType) + { + await SetupProjectsWithAccessAsync(permissionType); + + var response = await _client.GetAsync($"/projects/{Guid.NewGuid().ToString()}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetByProjectAsync_UserWithoutPermission_ZeroCounts() + { + var (_, org, user) = await SetupProjectsWithAccessAsync(PermissionType.RunAsUserWithPermission, 0); + + var projects = await CreateProjectsAsync(org.Id); + + await CreateSecretsAsync(org.Id, projects[0]); + + var groups = await CreateGroupsAsync(org.Id, user); + await CreateGroupProjectAccessPolicyAsync(groups[0].Id, projects[0].Id); + + var serviceAccounts = await CreateServiceAccountsAsync(org.Id); + await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id); + + var response = await _client.GetAsync($"/projects/{projects[0].Id}/sm-counts"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Equal(0, result.Secrets); + Assert.Equal(0, result.People); + Assert.Equal(0, result.ServiceAccounts); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin, true)] + [InlineData(PermissionType.RunAsUserWithPermission, false)] + [InlineData(PermissionType.RunAsUserWithPermission, true)] + public async Task GetByProjectAsync_Success(PermissionType permissionType, bool userProjectWriteAccess) + { + var (projects, org, user) = await SetupProjectsWithAccessAsync(permissionType, 3, userProjectWriteAccess); + + var secrets = await CreateSecretsAsync(org.Id, projects[0]); + await CreateSecretsAsync(org.Id, projects[1]); + + var groups = await CreateGroupsAsync(org.Id, user); + await CreateGroupProjectAccessPolicyAsync(groups[0].Id, projects[0].Id); + await CreateGroupProjectAccessPolicyAsync(groups[0].Id, projects[1].Id); + var (_, user2) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await CreateUserProjectAccessPolicyAsync(user2.Id, projects[0].Id); + + var serviceAccounts = await CreateServiceAccountsAsync(org.Id); + await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccounts[0].Id); + await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id); + + var response = await _client.GetAsync($"/projects/{projects[0].Id}/sm-counts"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Equal(secrets.Count, result.Secrets); + if (userProjectWriteAccess) + { + Assert.Equal(permissionType == PermissionType.RunAsAdmin ? 2 : 3, result.People); + Assert.Equal(1, result.ServiceAccounts); + } + else + { + Assert.Equal(0, result.People); + Assert.Equal(0, result.ServiceAccounts); + } + } + + [Theory] + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + public async Task GetByServiceAccountAsync_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, + bool organizationEnabled) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); + await _loginHelper.LoginAsync(_email); + + var serviceAccounts = await CreateServiceAccountsAsync(org.Id); + + var response = await _client.GetAsync($"/service-accounts/{serviceAccounts[0].Id}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetByServiceAccountAsync_RunAsServiceAccount_NotFound() + { + var (_, org, _) = await SetupProjectsWithAccessAsync(PermissionType.RunAsServiceAccountWithPermission); + + var serviceAccounts = await CreateServiceAccountsAsync(org.Id); + + var response = await _client.GetAsync($"/service-accounts/{serviceAccounts[0].Id}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetByServiceAccountAsync_NonExistingServiceAccount_NotFound(PermissionType permissionType) + { + await SetupProjectsWithAccessAsync(permissionType); + + var response = await _client.GetAsync($"/service-accounts/{Guid.NewGuid().ToString()}/sm-counts"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task GetByServiceAccountAsync_UserWithoutPermission_ZeroCounts() + { + var (_, org, user) = await SetupProjectsWithAccessAsync(PermissionType.RunAsUserWithPermission, 0); + + var projects = await CreateProjectsAsync(org.Id); + + var serviceAccounts = await CreateServiceAccountsAsync(org.Id); + await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id); + + var groups = await CreateGroupsAsync(org.Id, user); + await CreateGroupServiceAccountAccessPolicyAsync(groups[0].Id, serviceAccounts[0].Id); + + await CreateApiKeysAsync(serviceAccounts[0]); + + var response = await _client.GetAsync($"/service-accounts/{serviceAccounts[0].Id}/sm-counts"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Equal(0, result.Projects); + Assert.Equal(0, result.People); + Assert.Equal(0, result.AccessTokens); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetByServiceAccountAsync_Success(PermissionType permissionType) + { + var (projects, org, user) = await SetupProjectsWithAccessAsync(permissionType); + + var serviceAccounts = await CreateServiceAccountsAsync(org.Id); + await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[0].Id); + await CreateServiceAccountProjectAccessPolicyAsync(projects[0].Id, serviceAccounts[1].Id); + await CreateServiceAccountProjectAccessPolicyAsync(projects[1].Id, serviceAccounts[0].Id); + + await CreateUserServiceAccountAccessPolicyAsync(user.Id, serviceAccounts[0].Id); + var groups = await CreateGroupsAsync(org.Id, user); + await CreateGroupServiceAccountAccessPolicyAsync(groups[0].Id, serviceAccounts[0].Id); + await CreateGroupServiceAccountAccessPolicyAsync(groups[0].Id, serviceAccounts[1].Id); + var (_, user2) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await CreateUserServiceAccountAccessPolicyAsync(user2.Id, serviceAccounts[0].Id); + + var apiKeys = await CreateApiKeysAsync(serviceAccounts[0]); + await CreateApiKeysAsync(serviceAccounts[1]); + + var response = await _client.GetAsync($"/service-accounts/{serviceAccounts[0].Id}/sm-counts"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(result); + Assert.Equal(2, result.Projects); + Assert.Equal(3, result.People); + Assert.Equal(apiKeys.Count, result.AccessTokens); + } + + private async Task> CreateProjectsAsync(Guid orgId, int numberToCreate = 3) + { + var projects = new List(); + for (var i = 0; i < numberToCreate; i++) + { + var project = await _projectRepository.CreateAsync(new Project + { + OrganizationId = orgId, + Name = _mockEncryptedString, + }); + projects.Add(project); + } + + return projects; + } + + private async Task> CreateSecretsAsync(Guid organizationId, Project? project, int numberToCreate = 3) + { + var secrets = new List(); + for (var i = 0; i < numberToCreate; i++) + { + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = organizationId, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString, + Projects = project != null ? new List { project } : null + }); + secrets.Add(secret); + } + + return secrets; + } + + private async Task> CreateServiceAccountsAsync(Guid organizationId, int numberToCreate = 3) + { + var serviceAccounts = new List(); + for (var i = 0; i < numberToCreate; i++) + { + var serviceAccount = await _serviceAccountRepository.CreateAsync(new ServiceAccount + { + OrganizationId = organizationId, + Name = _mockEncryptedString + }); + serviceAccounts.Add(serviceAccount); + } + + return serviceAccounts; + } + + private async Task> CreateGroupsAsync(Guid organizationId, OrganizationUser? user, + int numberToCreate = 3) + { + var groups = new List(); + + for (var i = 0; i < numberToCreate; i++) + { + var group = await _groupRepository.CreateAsync(new Group + { + OrganizationId = organizationId, + Name = _mockEncryptedString, + }); + groups.Add(group); + + if (user != null) + { + await _organizationUserRepository.UpdateGroupsAsync(user.Id, [group.Id]); + } + } + + return groups; + } + + private async Task> CreateApiKeysAsync(ServiceAccount serviceAccount, int numberToCreate = 3) + { + var apiKeys = new List(); + + for (var i = 0; i < numberToCreate; i++) + { + var apiKey = await _apiKeyRepository.CreateAsync(new ApiKey + { + Name = _mockEncryptedString, + ServiceAccountId = serviceAccount.Id, + Scope = "api.secrets", + Key = serviceAccount.OrganizationId.ToString(), + EncryptedPayload = _mockEncryptedString, + ClientSecretHash = "807613bbf6692e6809a571bc694a4719a5aa6863f7a62bd714003ab73de588e6" + }); + apiKeys.Add(apiKey); + } + + return apiKeys; + } + + private async Task<(List, Organization, OrganizationUser)> SetupProjectsWithAccessAsync( + PermissionType permissionType, + int projectsToCreate = 3, + bool writeAccess = false) + { + var (org, owner) = await _organizationHelper.Initialize(true, true, true); + var projects = await CreateProjectsAsync(org.Id, projectsToCreate); + var user = owner; + + switch (permissionType) + { + case PermissionType.RunAsAdmin: + await _loginHelper.LoginAsync(_email); + break; + case PermissionType.RunAsUserWithPermission: + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + user = orgUser; + await _loginHelper.LoginAsync(email); + + foreach (var project in projects) + { + await CreateUserProjectAccessPolicyAsync(user.Id, project.Id, writeAccess); + } + + break; + } + case PermissionType.RunAsServiceAccountWithPermission: + { + var apiKeyDetails = await _organizationHelper.CreateNewServiceAccountApiKeyAsync(); + await _loginHelper.LoginWithApiKeyAsync(apiKeyDetails); + + foreach (var project in projects) + { + await CreateServiceAccountProjectAccessPolicyAsync(project.Id, apiKeyDetails.ApiKey.ServiceAccountId!.Value); + } + + break; + } + default: + throw new ArgumentOutOfRangeException(nameof(permissionType), permissionType, null); + } + + return (projects, org, user); + } + + private async Task CreateUserProjectAccessPolicyAsync(Guid userId, Guid projectId, bool write = false) + { + var policy = new UserProjectAccessPolicy + { + OrganizationUserId = userId, + GrantedProjectId = projectId, + Read = true, + Write = write, + }; + await _accessPolicyRepository.CreateManyAsync([policy]); + } + + private async Task CreateGroupProjectAccessPolicyAsync(Guid groupId, Guid projectId) + { + var policy = new GroupProjectAccessPolicy + { + GroupId = groupId, + GrantedProjectId = projectId, + Read = true, + Write = false, + }; + await _accessPolicyRepository.CreateManyAsync([policy]); + } + + + private async Task CreateUserServiceAccountAccessPolicyAsync(Guid userId, Guid serviceAccountId) + { + var policy = new UserServiceAccountAccessPolicy + { + OrganizationUserId = userId, + GrantedServiceAccountId = serviceAccountId, + Read = true, + Write = false, + }; + await _accessPolicyRepository.CreateManyAsync([policy]); + } + + private async Task CreateGroupServiceAccountAccessPolicyAsync(Guid groupId, Guid serviceAccountId) + { + var policy = new GroupServiceAccountAccessPolicy + { + GroupId = groupId, + GrantedServiceAccountId = serviceAccountId, + Read = true, + Write = false + }; + await _accessPolicyRepository.CreateManyAsync([policy]); + } + + private async Task CreateServiceAccountProjectAccessPolicyAsync(Guid projectId, Guid serviceAccountId) + { + var policy = new ServiceAccountProjectAccessPolicy + { + ServiceAccountId = serviceAccountId, + GrantedProjectId = projectId, + Read = true, + Write = false, + }; + await _accessPolicyRepository.CreateManyAsync([policy]); + } +} diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs index bfa2cc3448..099dde5127 100644 --- a/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/ProjectsControllerTests.cs @@ -295,11 +295,7 @@ public class ProjectsControllerTests : IClassFixture, IAs Name = _mockEncryptedString, }); - var mockEncryptedString2 = - "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98xy4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; - var request = new ProjectCreateRequestModel { Name = mockEncryptedString2 }; - - var response = await _client.PutAsJsonAsync($"/projects/{project.Id}", request); + var response = await _client.GetAsync($"/projects/{project.Id}"); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } diff --git a/test/Api.Test/SecretsManager/Controllers/CountsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/CountsControllerTests.cs new file mode 100644 index 0000000000..330c6e02bc --- /dev/null +++ b/test/Api.Test/SecretsManager/Controllers/CountsControllerTests.cs @@ -0,0 +1,212 @@ +#nullable enable +using System.Security.Claims; +using Bit.Api.SecretsManager.Controllers; +using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Models.Data; +using Bit.Core.SecretsManager.Queries.Interfaces; +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.Api.Test.SecretsManager.Controllers; + +[ControllerCustomize(typeof(CountsController))] +[SutProviderCustomize] +[ProjectCustomize] +[JsonDocumentCustomize] +public class CountsControllerTests +{ + [Theory] + [BitAutoData] + public async Task GetByOrganizationAsync_NoAccess_Throws(SutProvider sutProvider, + Guid organizationId) + { + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByOrganizationAsync(organizationId)); + } + + [Theory] + [BitAutoData] + public async Task GetByOrganizationAsync_ServiceAccountAccess_Throws(SutProvider sutProvider, + Guid organizationId, Guid userId) + { + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); + + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), organizationId) + .Returns((AccessClientType.ServiceAccount, userId)); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByOrganizationAsync(organizationId)); + } + + [Theory] + [BitAutoData(AccessClientType.NoAccessCheck)] + [BitAutoData(AccessClientType.User)] + public async Task GetByOrganizationAsync_HasAccess_Success(AccessClientType accessClientType, + SutProvider sutProvider, Guid organizationId, Guid userId, + OrganizationCountsResponseModel expectedCountsResponseModel) + { + sutProvider.GetDependency().AccessSecretsManager(organizationId).Returns(true); + + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), organizationId).Returns((accessClientType, userId)); + + sutProvider.GetDependency() + .GetProjectCountByOrganizationIdAsync(organizationId, userId, accessClientType) + .Returns(expectedCountsResponseModel.Projects); + + sutProvider.GetDependency() + .GetSecretsCountByOrganizationIdAsync(organizationId, userId, accessClientType) + .Returns(expectedCountsResponseModel.Secrets); + + sutProvider.GetDependency() + .GetServiceAccountCountByOrganizationIdAsync(organizationId, userId, accessClientType) + .Returns(expectedCountsResponseModel.ServiceAccounts); + + var response = await sutProvider.Sut.GetByOrganizationAsync(organizationId); + + Assert.Equal(expectedCountsResponseModel.Projects, response.Projects); + Assert.Equal(expectedCountsResponseModel.Secrets, response.Secrets); + Assert.Equal(expectedCountsResponseModel.ServiceAccounts, response.ServiceAccounts); + } + + [Theory] + [BitAutoData] + public async Task GetByProjectAsync_ProjectNotFound_Throws(SutProvider sutProvider, + Guid projectId) + { + sutProvider.GetDependency().GetByIdAsync(projectId).Returns(default(Project)); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByProjectAsync(projectId)); + } + + [Theory] + [BitAutoData] + public async Task GetByProjectAsync_NoAccess_Throws(SutProvider sutProvider, Project project) + { + sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByProjectAsync(project.Id)); + } + + [Theory] + [BitAutoData] + public async Task GetByProjectAsync_ServiceAccountAccess_Throws(SutProvider sutProvider, + Guid userId, Project project) + { + sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true); + + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), project.OrganizationId) + .Returns((AccessClientType.ServiceAccount, userId)); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByProjectAsync(project.Id)); + } + + [Theory] + [BitAutoData(AccessClientType.NoAccessCheck)] + [BitAutoData(AccessClientType.User)] + public async Task GetByProjectAsync_HasAccess_Success(AccessClientType accessClientType, + SutProvider sutProvider, Guid userId, Project project, + ProjectCountsResponseModel expectedProjectCountsResponseModel) + { + sutProvider.GetDependency().GetByIdAsync(project.Id).Returns(project); + sutProvider.GetDependency().AccessSecretsManager(project.OrganizationId).Returns(true); + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), project.OrganizationId) + .Returns((accessClientType, userId)); + + sutProvider.GetDependency() + .GetProjectCountsByIdAsync(project.Id, userId, accessClientType) + .Returns(new ProjectCounts + { + Secrets = expectedProjectCountsResponseModel.Secrets, + People = expectedProjectCountsResponseModel.People, + ServiceAccounts = expectedProjectCountsResponseModel.ServiceAccounts + }); + + var response = await sutProvider.Sut.GetByProjectAsync(project.Id); + + Assert.Equal(expectedProjectCountsResponseModel.Secrets, response.Secrets); + Assert.Equal(expectedProjectCountsResponseModel.People, response.People); + Assert.Equal(expectedProjectCountsResponseModel.ServiceAccounts, response.ServiceAccounts); + } + + [Theory] + [BitAutoData] + public async Task GetByServiceAccountAsync_ServiceAccountNotFound_Throws(SutProvider sutProvider, + Guid serviceAccountId) + { + sutProvider.GetDependency().GetByIdAsync(serviceAccountId) + .Returns(default(ServiceAccount)); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByServiceAccountAsync(serviceAccountId)); + } + + [Theory] + [BitAutoData] + public async Task GetByServiceAccountAsync_NoAccess_Throws(SutProvider sutProvider, + ServiceAccount serviceAccount) + { + sutProvider.GetDependency().GetByIdAsync(serviceAccount.Id) + .Returns(serviceAccount); + sutProvider.GetDependency().AccessSecretsManager(serviceAccount.OrganizationId).Returns(false); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByServiceAccountAsync(serviceAccount.Id)); + } + + [Theory] + [BitAutoData] + public async Task GetByServiceAccountAsync_ServiceAccountAccess_Throws(SutProvider sutProvider, + Guid userId, ServiceAccount serviceAccount) + { + sutProvider.GetDependency().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount); + sutProvider.GetDependency().AccessSecretsManager(serviceAccount.OrganizationId).Returns(true); + + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), serviceAccount.OrganizationId) + .Returns((AccessClientType.ServiceAccount, userId)); + + await Assert.ThrowsAsync(() => sutProvider.Sut.GetByServiceAccountAsync(serviceAccount.Id)); + } + + [Theory] + [BitAutoData(AccessClientType.NoAccessCheck)] + [BitAutoData(AccessClientType.User)] + public async Task GetByServiceAccountAsync_HasAccess_Success(AccessClientType accessClientType, + SutProvider sutProvider, Guid userId, ServiceAccount serviceAccount, + ServiceAccountCountsResponseModel expectedServiceAccountCountsResponseModel) + { + sutProvider.GetDependency().GetByIdAsync(serviceAccount.Id) + .Returns(serviceAccount); + sutProvider.GetDependency().AccessSecretsManager(serviceAccount.OrganizationId).Returns(true); + sutProvider.GetDependency() + .GetAccessClientAsync(Arg.Any(), serviceAccount.OrganizationId) + .Returns((accessClientType, userId)); + + sutProvider.GetDependency() + .GetServiceAccountCountsByIdAsync(serviceAccount.Id, userId, accessClientType) + .Returns(new ServiceAccountCounts + { + Projects = expectedServiceAccountCountsResponseModel.Projects, + People = expectedServiceAccountCountsResponseModel.People, + AccessTokens = expectedServiceAccountCountsResponseModel.AccessTokens + }); + + var response = await sutProvider.Sut.GetByServiceAccountAsync(serviceAccount.Id); + + Assert.Equal(expectedServiceAccountCountsResponseModel.Projects, response.Projects); + Assert.Equal(expectedServiceAccountCountsResponseModel.People, response.People); + Assert.Equal(expectedServiceAccountCountsResponseModel.AccessTokens, response.AccessTokens); + } +}