From 16bdd67cad8af35c2cb2aac39a7f2c012693314f Mon Sep 17 00:00:00 2001 From: Colton Hurst Date: Mon, 20 Feb 2023 13:01:49 -0500 Subject: [PATCH] SM-281: Secrets Manager Trash (#2688) --- .../Commands/Trash/EmptyTrashCommand.cs | 28 +++ .../Commands/Trash/RestoreTrashCommand.cs | 28 +++ .../SecretsManagerCollectionExtensions.cs | 4 + .../Repositories/SecretRepository.cs | 47 +++- .../Trash/EmptyTrashCommandTests.cs | 48 ++++ .../Trash/RestoreTrashCommandTests.cs | 48 ++++ .../Controllers/SecretsController.cs | 1 + .../Controllers/SecretsTrashController.cs | 80 ++++++ .../Commands/Trash/IEmptyTrashCommand.cs | 7 + .../Commands/Trash/IRestoreTrashCommand.cs | 6 + .../Repositories/ISecretRepository.cs | 3 + .../Controllers/SecretsTrashControllerTest.cs | 227 ++++++++++++++++++ 12 files changed, 525 insertions(+), 2 deletions(-) create mode 100644 bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/EmptyTrashCommand.cs create mode 100644 bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/RestoreTrashCommand.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/EmptyTrashCommandTests.cs create mode 100644 bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/RestoreTrashCommandTests.cs create mode 100644 src/Api/SecretsManager/Controllers/SecretsTrashController.cs create mode 100644 src/Core/SecretsManager/Commands/Trash/IEmptyTrashCommand.cs create mode 100644 src/Core/SecretsManager/Commands/Trash/IRestoreTrashCommand.cs create mode 100644 test/Api.IntegrationTest/SecretsManager/Controllers/SecretsTrashControllerTest.cs diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/EmptyTrashCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/EmptyTrashCommand.cs new file mode 100644 index 0000000000..9951d05752 --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/EmptyTrashCommand.cs @@ -0,0 +1,28 @@ +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Commands.Trash.Interfaces; +using Bit.Core.SecretsManager.Repositories; + +namespace Bit.Commercial.Core.SecretsManager.Commands.Trash; + +public class EmptyTrashCommand : IEmptyTrashCommand +{ + private readonly ISecretRepository _secretRepository; + + public EmptyTrashCommand(ISecretRepository secretRepository) + { + _secretRepository = secretRepository; + } + + public async Task EmptyTrash(Guid organizationId, List ids) + { + var secrets = await _secretRepository.GetManyByOrganizationIdInTrashByIdsAsync(organizationId, ids); + + var missingId = ids.Except(secrets.Select(_ => _.Id)).Any(); + if (missingId) + { + throw new NotFoundException(); + } + + await _secretRepository.HardDeleteManyByIdAsync(ids); + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/RestoreTrashCommand.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/RestoreTrashCommand.cs new file mode 100644 index 0000000000..ad0b5bb7a2 --- /dev/null +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Commands/Trash/RestoreTrashCommand.cs @@ -0,0 +1,28 @@ +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Commands.Trash.Interfaces; +using Bit.Core.SecretsManager.Repositories; + +namespace Bit.Commercial.Core.SecretsManager.Commands.Trash; + +public class RestoreTrashCommand : IRestoreTrashCommand +{ + private readonly ISecretRepository _secretRepository; + + public RestoreTrashCommand(ISecretRepository secretRepository) + { + _secretRepository = secretRepository; + } + + public async Task RestoreTrash(Guid organizationId, List ids) + { + var secrets = await _secretRepository.GetManyByOrganizationIdInTrashByIdsAsync(organizationId, ids); + + var missingId = ids.Except(secrets.Select(_ => _.Id)).Any(); + if (missingId) + { + throw new NotFoundException(); + } + + await _secretRepository.RestoreManyByIdAsync(ids); + } +} diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs index 429833089f..3f871d8532 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/SecretsManagerCollectionExtensions.cs @@ -4,12 +4,14 @@ using Bit.Commercial.Core.SecretsManager.Commands.Porting; using Bit.Commercial.Core.SecretsManager.Commands.Projects; using Bit.Commercial.Core.SecretsManager.Commands.Secrets; using Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts; +using Bit.Commercial.Core.SecretsManager.Commands.Trash; using Bit.Core.SecretsManager.Commands.AccessPolicies.Interfaces; using Bit.Core.SecretsManager.Commands.AccessTokens.Interfaces; using Bit.Core.SecretsManager.Commands.Porting.Interfaces; using Bit.Core.SecretsManager.Commands.Projects.Interfaces; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces; +using Bit.Core.SecretsManager.Commands.Trash.Interfaces; using Microsoft.Extensions.DependencyInjection; namespace Bit.Commercial.Core.SecretsManager; @@ -32,5 +34,7 @@ public static class SecretsManagerCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } 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 43f1adb591..e08f89efbe 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretRepository.cs @@ -8,7 +8,6 @@ using Bit.Infrastructure.EntityFramework.SecretsManager.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; - namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories; public class SecretRepository : Repository, ISecretRepository @@ -72,6 +71,36 @@ public class SecretRepository : Repository>(secrets); } + public async Task> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable ids) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var secrets = await dbContext.Secret + .Where(s => ids.Contains(s.Id) && s.OrganizationId == organizationId && s.DeletedDate != null) + .Include("Projects") + .OrderBy(c => c.RevisionDate) + .ToListAsync(); + + return Mapper.Map>(secrets); + } + } + + public async Task> GetManyByOrganizationIdInTrashAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var secrets = await dbContext.Secret + .Where(c => c.OrganizationId == organizationId && c.DeletedDate != null) + .Include("Projects") + .OrderBy(c => c.RevisionDate) + .ToListAsync(); + + return Mapper.Map>(secrets); + } + } + public async Task> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType) { using (var scope = ServiceScopeFactory.CreateScope()) @@ -168,7 +197,6 @@ public class SecretRepository : Repository ids.Contains(c.Id)); await secrets.ForEachAsync(secret => { @@ -179,6 +207,21 @@ public class SecretRepository : Repository ids) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var secrets = dbContext.Secret.Where(c => ids.Contains(c.Id)); + await secrets.ForEachAsync(secret => + { + dbContext.Attach(secret); + secret.DeletedDate = null; + }); + await dbContext.SaveChangesAsync(); + } + } + public async Task> ImportAsync(IEnumerable secrets) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/EmptyTrashCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/EmptyTrashCommandTests.cs new file mode 100644 index 0000000000..603e5fcf4a --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/EmptyTrashCommandTests.cs @@ -0,0 +1,48 @@ +using Bit.Commercial.Core.SecretsManager.Commands.Trash; +using Bit.Core.Exceptions; +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.Trash; + +[SutProviderCustomize] +[ProjectCustomize] +public class EmptyTrashCommandTests +{ + [Theory] + [BitAutoData] + public async Task EmptyTrash_Throws_NotFoundException(Guid orgId, Secret s1, Secret s2, SutProvider sutProvider) + { + s1.DeletedDate = DateTime.Now; + + var ids = new List { s1.Id, s2.Id }; + sutProvider.GetDependency() + .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids) + .Returns(new List { s1 }); + + await Assert.ThrowsAsync(() => sutProvider.Sut.EmptyTrash(orgId, ids)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreManyByIdAsync(default); + } + + [Theory] + [BitAutoData] + public async Task EmptyTrash_Success(Guid orgId, Secret s1, Secret s2, SutProvider sutProvider) + { + s1.DeletedDate = DateTime.Now; + + var ids = new List { s1.Id, s2.Id }; + sutProvider.GetDependency() + .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids) + .Returns(new List { s1, s2 }); + + await sutProvider.Sut.EmptyTrash(orgId, ids); + + await sutProvider.GetDependency().Received(1).HardDeleteManyByIdAsync(ids); + } +} diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/RestoreTrashCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/RestoreTrashCommandTests.cs new file mode 100644 index 0000000000..1054ad154f --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Trash/RestoreTrashCommandTests.cs @@ -0,0 +1,48 @@ +using Bit.Commercial.Core.SecretsManager.Commands.Trash; +using Bit.Core.Exceptions; +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.Trash; + +[SutProviderCustomize] +[ProjectCustomize] +public class RestoreTrashCommandTests +{ + [Theory] + [BitAutoData] + public async Task RestoreTrash_Throws_NotFoundException(Guid orgId, Secret s1, Secret s2, SutProvider sutProvider) + { + s1.DeletedDate = DateTime.Now; + + var ids = new List { s1.Id, s2.Id }; + sutProvider.GetDependency() + .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids) + .Returns(new List { s1 }); + + await Assert.ThrowsAsync(() => sutProvider.Sut.RestoreTrash(orgId, ids)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().RestoreManyByIdAsync(default); + } + + [Theory] + [BitAutoData] + public async Task RestoreTrash_Success(Guid orgId, Secret s1, Secret s2, SutProvider sutProvider) + { + s1.DeletedDate = DateTime.Now; + + var ids = new List { s1.Id, s2.Id }; + sutProvider.GetDependency() + .GetManyByOrganizationIdInTrashByIdsAsync(orgId, ids) + .Returns(new List { s1, s2 }); + + await sutProvider.Sut.RestoreTrash(orgId, ids); + + await sutProvider.GetDependency().Received(1).RestoreManyByIdAsync(ids); + } +} diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index 7418fe0a17..5c91c2e807 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -70,6 +70,7 @@ public class SecretsController : Controller public async Task GetAsync([FromRoute] Guid id) { var secret = await _secretRepository.GetByIdAsync(id); + if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId)) { throw new NotFoundException(); diff --git a/src/Api/SecretsManager/Controllers/SecretsTrashController.cs b/src/Api/SecretsManager/Controllers/SecretsTrashController.cs new file mode 100644 index 0000000000..6f0b65d458 --- /dev/null +++ b/src/Api/SecretsManager/Controllers/SecretsTrashController.cs @@ -0,0 +1,80 @@ +using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.SecretsManager.Commands.Trash.Interfaces; +using Bit.Core.SecretsManager.Repositories; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.SecretsManager.Controllers; + +[SecretsManager] +[Authorize("secrets")] +public class TrashController : Controller +{ + private readonly ICurrentContext _currentContext; + private readonly ISecretRepository _secretRepository; + private readonly IEmptyTrashCommand _emptyTrashCommand; + private readonly IRestoreTrashCommand _restoreTrashCommand; + + public TrashController( + ICurrentContext currentContext, + ISecretRepository secretRepository, + IEmptyTrashCommand emptyTrashCommand, + IRestoreTrashCommand restoreTrashCommand) + { + _currentContext = currentContext; + _secretRepository = secretRepository; + _emptyTrashCommand = emptyTrashCommand; + _restoreTrashCommand = restoreTrashCommand; + } + + [HttpGet("secrets/{organizationId}/trash")] + public async Task ListByOrganizationAsync(Guid organizationId) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + if (!await _currentContext.OrganizationAdmin(organizationId)) + { + throw new UnauthorizedAccessException(); + } + + var secrets = await _secretRepository.GetManyByOrganizationIdInTrashAsync(organizationId); + return new SecretWithProjectsListResponseModel(secrets); + } + + [HttpPost("secrets/{organizationId}/trash/empty")] + public async Task EmptyTrashAsync(Guid organizationId, [FromBody] List ids) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + if (!await _currentContext.OrganizationAdmin(organizationId)) + { + throw new UnauthorizedAccessException(); + } + + await _emptyTrashCommand.EmptyTrash(organizationId, ids); + } + + [HttpPost("secrets/{organizationId}/trash/restore")] + public async Task RestoreTrashAsync(Guid organizationId, [FromBody] List ids) + { + if (!_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + if (!await _currentContext.OrganizationAdmin(organizationId)) + { + throw new UnauthorizedAccessException(); + } + + await _restoreTrashCommand.RestoreTrash(organizationId, ids); + } +} diff --git a/src/Core/SecretsManager/Commands/Trash/IEmptyTrashCommand.cs b/src/Core/SecretsManager/Commands/Trash/IEmptyTrashCommand.cs new file mode 100644 index 0000000000..74c5c21a0a --- /dev/null +++ b/src/Core/SecretsManager/Commands/Trash/IEmptyTrashCommand.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.SecretsManager.Commands.Trash.Interfaces; + +public interface IEmptyTrashCommand +{ + Task EmptyTrash(Guid organizationId, List ids); +} + diff --git a/src/Core/SecretsManager/Commands/Trash/IRestoreTrashCommand.cs b/src/Core/SecretsManager/Commands/Trash/IRestoreTrashCommand.cs new file mode 100644 index 0000000000..561b1ffd82 --- /dev/null +++ b/src/Core/SecretsManager/Commands/Trash/IRestoreTrashCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.SecretsManager.Commands.Trash.Interfaces; + +public interface IRestoreTrashCommand +{ + Task RestoreTrash(Guid organizationId, List ids); +} diff --git a/src/Core/SecretsManager/Repositories/ISecretRepository.cs b/src/Core/SecretsManager/Repositories/ISecretRepository.cs index 8bc9f8eb6e..bfc59fed6a 100644 --- a/src/Core/SecretsManager/Repositories/ISecretRepository.cs +++ b/src/Core/SecretsManager/Repositories/ISecretRepository.cs @@ -6,6 +6,8 @@ namespace Bit.Core.SecretsManager.Repositories; public interface ISecretRepository { Task> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType); + Task> GetManyByOrganizationIdInTrashAsync(Guid organizationId); + Task> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable ids); Task> GetManyByIds(IEnumerable ids); Task> GetManyByProjectIdAsync(Guid projectId, Guid userId, AccessClientType accessType); Task GetByIdAsync(Guid id); @@ -13,5 +15,6 @@ public interface ISecretRepository Task UpdateAsync(Secret secret); Task SoftDeleteManyByIdAsync(IEnumerable ids); Task HardDeleteManyByIdAsync(IEnumerable ids); + Task RestoreManyByIdAsync(IEnumerable ids); Task> ImportAsync(IEnumerable secrets); } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsTrashControllerTest.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsTrashControllerTest.cs new file mode 100644 index 0000000000..97dd827a8c --- /dev/null +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretsTrashControllerTest.cs @@ -0,0 +1,227 @@ +using System.Net; +using System.Net.Http.Headers; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Enums; +using Bit.Core.SecretsManager.Repositories; +using Xunit; +using Secret = Bit.Core.SecretsManager.Entities.Secret; + +namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; + +public class SecretsTrashControllerTest : IClassFixture, IAsyncLifetime +{ + private readonly string _mockEncryptedString = + "2.3Uk+WNBIoU5xzmVFNcoWzz==|1MsPIYuRfdOHfu/0uY6H2Q==|/98sp4wb6pHP1VTZ9JcNCYgQjEUMFPlqJgCwRk1YXKg="; + + private readonly HttpClient _client; + private readonly ApiApplicationFactory _factory; + private readonly ISecretRepository _secretRepository; + + private string _email = null!; + private SecretsManagerOrganizationHelper _organizationHelper = null!; + + public SecretsTrashControllerTest(ApiApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + _secretRepository = _factory.GetService(); + } + + 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; + } + + private async Task LoginAsync(string email) + { + var tokens = await _factory.LoginAsync(email); + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task ListByOrganization_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var response = await _client.GetAsync($"/secrets/{org.Id}/trash"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task ListByOrganization_NotAdmin_Unauthorized() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var response = await _client.GetAsync($"/secrets/{org.Id}/trash"); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task ListByOrganization_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + DeletedDate = DateTime.Now, + }); + + await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + }); + + var response = await _client.GetAsync($"/secrets/{org.Id}/trash"); + + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(); + Assert.Single(result!.Secrets); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Empty_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var ids = new List { Guid.NewGuid() }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/empty", ids); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Empty_NotAdmin_Unauthorized() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var ids = new List { Guid.NewGuid() }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/empty", ids); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Empty_Invalid_NotFound() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString + }); + + var ids = new List { secret.Id }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/empty", ids); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Empty_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + DeletedDate = DateTime.Now, + }); + + var ids = new List { secret.Id }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/empty", ids); + response.EnsureSuccessStatusCode(); + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + public async Task Restore_SmNotEnabled_NotFound(bool useSecrets, bool accessSecrets) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets); + await LoginAsync(_email); + + var ids = new List { Guid.NewGuid() }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/restore", ids); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Restore_NotAdmin_Unauthorized() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + var (email, _) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await LoginAsync(email); + + var ids = new List { Guid.NewGuid() }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/restore", ids); + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Fact] + public async Task Restore_Invalid_NotFound() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString + }); + + var ids = new List { secret.Id }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/restore", ids); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Restore_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true); + await LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + DeletedDate = DateTime.Now, + }); + + var ids = new List { secret.Id }; + var response = await _client.PostAsJsonAsync($"/secrets/{org.Id}/trash/restore", ids); + response.EnsureSuccessStatusCode(); + } +}