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]); } }