using System.Security.Claims; using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; using Bit.Core.AdminConsole.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Models.Data; using Bit.Core.SecretsManager.Queries.ServiceAccounts.Interfaces; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; using Microsoft.AspNetCore.Authorization; using NSubstitute; using Xunit; namespace Bit.Api.Test.SecretsManager.Controllers; [ControllerCustomize(typeof(ServiceAccountsController))] [SutProviderCustomize] [JsonDocumentCustomize] public class ServiceAccountsControllerTests { [Theory] [BitAutoData] public async Task GetServiceAccountsByOrganization_ReturnsEmptyList( SutProvider sutProvider, Guid id) { sutProvider.GetDependency().AccessSecretsManager(id).Returns(true); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); var result = await sutProvider.Sut.ListByOrganizationAsync(id); await sutProvider.GetDependency().Received(1) .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(id)), Arg.Any(), Arg.Any(), Arg.Any()); Assert.Empty(result.Data); } [Theory] [BitAutoData] public async Task GetServiceAccountsByOrganization_Success(SutProvider sutProvider, ServiceAccountSecretsDetails resultServiceAccount) { sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); sutProvider.GetDependency().GetManyByOrganizationIdAsync(default, default, default, default) .ReturnsForAnyArgs(new List { resultServiceAccount }); var result = await sutProvider.Sut.ListByOrganizationAsync(resultServiceAccount.ServiceAccount.OrganizationId); await sutProvider.GetDependency().Received(1) .GetManyByOrganizationIdAsync(Arg.Is(AssertHelper.AssertPropertyEqual(resultServiceAccount.ServiceAccount.OrganizationId)), Arg.Any(), Arg.Any(), Arg.Any()); Assert.NotEmpty(result.Data); Assert.Single(result.Data); } [Theory] [BitAutoData] public async Task GetServiceAccountsByOrganization_AccessDenied_Throws( SutProvider sutProvider, Guid orgId) { sutProvider.GetDependency().AccessSecretsManager(default).ReturnsForAnyArgs(false); await Assert.ThrowsAsync(() => sutProvider.Sut.ListByOrganizationAsync(orgId)); } [Theory] [BitAutoData] public async Task CreateServiceAccount_NoAccess_Throws(SutProvider sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId) { sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data.ToServiceAccount(organizationId), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); var resultServiceAccount = data.ToServiceAccount(organizationId); sutProvider.GetDependency().CreateAsync(default, default) .ReturnsForAnyArgs(resultServiceAccount); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAsync(organizationId, data)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .CreateAsync(Arg.Any(), Arg.Any()); } [Theory] [BitAutoData(0)] public async Task CreateServiceAccount_WhenAutoscalingNotRequired_DoesNotCallUpdateSubscription( int newSlotsRequired, SutProvider sutProvider, ServiceAccountCreateRequestModel data, Organization organization) { ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization); await sutProvider.Sut.CreateAsync(organization.Id, data); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(sa => sa.Name == data.Name), Arg.Any()); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .UpdateSubscriptionAsync(Arg.Any()); } [Theory] [BitAutoData(1)] [BitAutoData(2)] public async Task CreateServiceAccount_WhenAutoscalingRequired_CallsUpdateSubscription(int newSlotsRequired, SutProvider sutProvider, ServiceAccountCreateRequestModel data, Organization organization) { ArrangeCreateServiceAccountAutoScalingTest(newSlotsRequired, sutProvider, data, organization); await sutProvider.Sut.CreateAsync(organization.Id, data); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(sa => sa.Name == data.Name), Arg.Any()); await sutProvider.GetDependency().Received(1) .UpdateSubscriptionAsync(Arg.Is(update => update.Autoscaling == true && update.SmServiceAccounts == organization.SmServiceAccounts + newSlotsRequired && !update.SmSeatsChanged && !update.MaxAutoscaleSmSeatsChanged && !update.MaxAutoscaleSmServiceAccountsChanged)); } [Theory] [BitAutoData] public async Task CreateServiceAccount_Success(SutProvider sutProvider, ServiceAccountCreateRequestModel data, Guid organizationId, Organization mockOrg) { mockOrg.Id = organizationId; sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data.ToServiceAccount(organizationId), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); sutProvider.GetDependency().GetByIdAsync(Arg.Is(organizationId)).Returns(mockOrg); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); var resultServiceAccount = data.ToServiceAccount(organizationId); sutProvider.GetDependency().CreateAsync(default, default) .ReturnsForAnyArgs(resultServiceAccount); await sutProvider.Sut.CreateAsync(organizationId, data); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Any(), Arg.Any()); } [Theory] [BitAutoData] public async Task UpdateServiceAccount_NoAccess_Throws(SutProvider sutProvider, ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount) { sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data.ToServiceAccount(existingServiceAccount.Id), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); sutProvider.GetDependency().GetByIdAsync(existingServiceAccount.Id) .ReturnsForAnyArgs(existingServiceAccount); var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id); sutProvider.GetDependency().UpdateAsync(default) .ReturnsForAnyArgs(resultServiceAccount); await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .UpdateAsync(Arg.Any()); } [Theory] [BitAutoData] public async Task UpdateServiceAccount_Success(SutProvider sutProvider, ServiceAccountUpdateRequestModel data, ServiceAccount existingServiceAccount) { sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data.ToServiceAccount(existingServiceAccount.Id), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); var resultServiceAccount = data.ToServiceAccount(existingServiceAccount.Id); sutProvider.GetDependency().UpdateAsync(default) .ReturnsForAnyArgs(resultServiceAccount); var result = await sutProvider.Sut.UpdateAsync(existingServiceAccount.Id, data); await sutProvider.GetDependency().Received(1) .UpdateAsync(Arg.Any()); } [Theory] [BitAutoData] public async Task CreateAccessToken_NoAccess_Throws(SutProvider sutProvider, AccessTokenCreateRequestModel data, ServiceAccount serviceAccount, string mockClientSecret) { sutProvider.GetDependency().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), serviceAccount, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); var resultAccessToken = data.ToApiKey(serviceAccount.Id); sutProvider.GetDependency() .CreateAsync(default) .ReturnsForAnyArgs(new ApiKeyClientSecretDetails { ApiKey = resultAccessToken, ClientSecret = mockClientSecret }); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateAccessTokenAsync(serviceAccount.Id, data)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .CreateAsync(Arg.Any()); } [Theory] [BitAutoData] public async Task CreateAccessToken_Success(SutProvider sutProvider, AccessTokenCreateRequestModel data, ServiceAccount serviceAccount, string mockClientSecret) { sutProvider.GetDependency().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), serviceAccount, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); var resultAccessToken = data.ToApiKey(serviceAccount.Id); sutProvider.GetDependency().CreateAsync(default) .ReturnsForAnyArgs(new ApiKeyClientSecretDetails { ApiKey = resultAccessToken, ClientSecret = mockClientSecret }); await sutProvider.Sut.CreateAccessTokenAsync(serviceAccount.Id, data); await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Any()); } [Theory] [BitAutoData] public async Task GetAccessTokens_NoAccess_Throws(SutProvider sutProvider, ServiceAccount data, ICollection resultApiKeys) { sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); foreach (var apiKey in resultApiKeys) { apiKey.Scope = "[\"api.secrets\"]"; } sutProvider.GetDependency().GetManyByServiceAccountIdAsync(default) .ReturnsForAnyArgs(resultApiKeys); await Assert.ThrowsAsync(() => sutProvider.Sut.GetAccessTokens(data.Id)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .GetManyByServiceAccountIdAsync(Arg.Any()); } [Theory] [BitAutoData] public async Task GetAccessTokens_Success(SutProvider sutProvider, ServiceAccount data, ICollection resultApiKeys) { sutProvider.GetDependency().GetByIdAsync(default).ReturnsForAnyArgs(data); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); foreach (var apiKey in resultApiKeys) { apiKey.Scope = "[\"api.secrets\"]"; } sutProvider.GetDependency().GetManyByServiceAccountIdAsync(default) .ReturnsForAnyArgs(resultApiKeys); var result = await sutProvider.Sut.GetAccessTokens(data.Id); await sutProvider.GetDependency().Received(1) .GetManyByServiceAccountIdAsync(Arg.Any()); Assert.NotEmpty(result.Data); Assert.Equal(resultApiKeys.Count, result.Data.Count()); } [Theory] [BitAutoData] public async Task RevokeAccessTokens_NoAccess_Throws(SutProvider sutProvider, RevokeAccessTokensRequest data, ServiceAccount serviceAccount) { sutProvider.GetDependency().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), serviceAccount, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Failed()); await Assert.ThrowsAsync(() => sutProvider.Sut.RevokeAccessTokensAsync(serviceAccount.Id, data)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() .RevokeAsync(Arg.Any(), Arg.Any()); } [Theory] [BitAutoData] public async Task RevokeAccessTokens_Success(SutProvider sutProvider, RevokeAccessTokensRequest data, ServiceAccount serviceAccount) { sutProvider.GetDependency().GetByIdAsync(serviceAccount.Id).Returns(serviceAccount); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), serviceAccount, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); await sutProvider.Sut.RevokeAccessTokensAsync(serviceAccount.Id, data); await sutProvider.GetDependency().Received(1) .RevokeAsync(Arg.Any(), Arg.Any()); } [Theory] [BitAutoData] public async Task BulkDelete_NoServiceAccountsFound_ThrowsNotFound(SutProvider sutProvider, List data) { var ids = data.Select(sa => sa.Id).ToList(); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List()); await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAsync(ids)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any>()); } [Theory] [BitAutoData] public async Task BulkDelete_ServiceAccountsFoundMisMatch_ThrowsNotFound(SutProvider sutProvider, List data, ServiceAccount mockSa) { data.Add(mockSa); var ids = data.Select(sa => sa.Id).ToList(); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(new List { mockSa }); await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAsync(ids)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any>()); } [Theory] [BitAutoData] public async Task BulkDelete_OrganizationMistMatch_ThrowsNotFound(SutProvider sutProvider, List data) { var ids = data.Select(sa => sa.Id).ToList(); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAsync(ids)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any>()); } [Theory] [BitAutoData] public async Task BulkDelete_NoAccessToSecretsManager_ThrowsNotFound(SutProvider sutProvider, List data) { var ids = data.Select(sa => sa.Id).ToList(); var organizationId = data.First().OrganizationId; foreach (var sa in data) { sa.OrganizationId = organizationId; } sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(false); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); await Assert.ThrowsAsync(() => sutProvider.Sut.BulkDeleteAsync(ids)); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().DeleteServiceAccounts(Arg.Any>()); } [Theory] [BitAutoData] public async Task BulkDelete_ReturnsAccessDeniedForProjectsWithoutAccess_Success(SutProvider sutProvider, List data) { var ids = data.Select(sa => sa.Id).ToList(); var organizationId = data.First().OrganizationId; foreach (var sa in data) { sa.OrganizationId = organizationId; sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), sa, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); } sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data.First(), Arg.Any>()).Returns(AuthorizationResult.Failed()); sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); var results = await sutProvider.Sut.BulkDeleteAsync(ids); Assert.Equal(data.Count, results.Data.Count()); Assert.Equal("access denied", results.Data.First().Error); data.Remove(data.First()); await sutProvider.GetDependency().Received(1) .DeleteServiceAccounts(Arg.Is(AssertHelper.AssertPropertyEqual(data))); } [Theory] [BitAutoData] public async Task BulkDelete_Success(SutProvider sutProvider, List data) { var ids = data.Select(sa => sa.Id).ToList(); var organizationId = data.First().OrganizationId; foreach (var sa in data) { sa.OrganizationId = organizationId; sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), sa, Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); } sutProvider.GetDependency().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true); sutProvider.GetDependency().GetManyByIds(Arg.Is(ids)).ReturnsForAnyArgs(data); var results = await sutProvider.Sut.BulkDeleteAsync(ids); await sutProvider.GetDependency().Received(1) .DeleteServiceAccounts(Arg.Is(AssertHelper.AssertPropertyEqual(data))); Assert.Equal(data.Count, results.Data.Count()); foreach (var result in results.Data) { Assert.Null(result.Error); } } private static void ArrangeCreateServiceAccountAutoScalingTest(int newSlotsRequired, SutProvider sutProvider, ServiceAccountCreateRequestModel data, Organization organization) { sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), data.ToServiceAccount(organization.Id), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); sutProvider.GetDependency().GetByIdAsync(Arg.Is(organization.Id)).Returns(organization); sutProvider.GetDependency() .CountNewServiceAccountSlotsRequiredAsync(organization.Id, 1) .ReturnsForAnyArgs(newSlotsRequired); sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(Guid.NewGuid()); var resultServiceAccount = data.ToServiceAccount(organization.Id); sutProvider.GetDependency().CreateAsync(default, default) .ReturnsForAnyArgs(resultServiceAccount); } }