diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretVersionRepository.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretVersionRepository.cs new file mode 100644 index 0000000000..22421f9921 --- /dev/null +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/Repositories/SecretVersionRepository.cs @@ -0,0 +1,94 @@ +using AutoMapper; +using Bit.Core.SecretsManager.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.EntityFramework.SecretsManager.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Commercial.Infrastructure.EntityFramework.SecretsManager.Repositories; + +public class SecretVersionRepository : Repository, ISecretVersionRepository +{ + public SecretVersionRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, db => db.SecretVersion) + { } + + public override async Task GetByIdAsync(Guid id) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var secretVersion = await dbContext.SecretVersion + .Where(sv => sv.Id == id) + .FirstOrDefaultAsync(); + return Mapper.Map(secretVersion); + } + + public async Task> GetManyBySecretIdAsync(Guid secretId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var secretVersions = await dbContext.SecretVersion + .Where(sv => sv.SecretId == secretId) + .OrderByDescending(sv => sv.VersionDate) + .ToListAsync(); + return Mapper.Map>(secretVersions); + } + + public async Task> GetManyByIdsAsync(IEnumerable ids) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var versionIds = ids.ToList(); + var secretVersions = await dbContext.SecretVersion + .Where(sv => versionIds.Contains(sv.Id)) + .OrderByDescending(sv => sv.VersionDate) + .ToListAsync(); + return Mapper.Map>(secretVersions); + } + + public override async Task CreateAsync(Core.SecretsManager.Entities.SecretVersion secretVersion) + { + const int maxVersionsToKeep = 10; + + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + await using var transaction = await dbContext.Database.BeginTransactionAsync(); + + // Get the IDs of the most recent (maxVersionsToKeep - 1) versions to keep + var versionsToKeepIds = await dbContext.SecretVersion + .Where(sv => sv.SecretId == secretVersion.SecretId) + .OrderByDescending(sv => sv.VersionDate) + .Take(maxVersionsToKeep - 1) + .Select(sv => sv.Id) + .ToListAsync(); + + // Delete all versions for this secret that are not in the "keep" list + if (versionsToKeepIds.Any()) + { + await dbContext.SecretVersion + .Where(sv => sv.SecretId == secretVersion.SecretId && !versionsToKeepIds.Contains(sv.Id)) + .ExecuteDeleteAsync(); + } + + secretVersion.SetNewId(); + var entity = Mapper.Map(secretVersion); + + await dbContext.AddAsync(entity); + await dbContext.SaveChangesAsync(); + await transaction.CommitAsync(); + + return secretVersion; + } + + public async Task DeleteManyByIdAsync(IEnumerable ids) + { + await using var scope = ServiceScopeFactory.CreateAsyncScope(); + var dbContext = GetDatabaseContext(scope); + + var secretVersionIds = ids.ToList(); + await dbContext.SecretVersion + .Where(sv => secretVersionIds.Contains(sv.Id)) + .ExecuteDeleteAsync(); + } +} diff --git a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/SecretsManagerEFServiceCollectionExtensions.cs b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/SecretsManagerEFServiceCollectionExtensions.cs index d6c8848079..ac52c40ba6 100644 --- a/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/SecretsManagerEFServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Infrastructure.EntityFramework/SecretsManager/SecretsManagerEFServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ public static class SecretsManagerEfServiceCollectionExtensions { services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Repositories/SecretVersionRepositoryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Repositories/SecretVersionRepositoryTests.cs new file mode 100644 index 0000000000..659a6d1233 --- /dev/null +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Repositories/SecretVersionRepositoryTests.cs @@ -0,0 +1,130 @@ +using Bit.Core.SecretsManager.Entities; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Commercial.Core.Test.SecretsManager.Repositories; + +public class SecretVersionRepositoryTests +{ + [Theory] + [BitAutoData] + public void SecretVersion_EntityCreation_Success(SecretVersion secretVersion) + { + // Arrange & Act + secretVersion.SetNewId(); + + // Assert + Assert.NotEqual(Guid.Empty, secretVersion.Id); + Assert.NotEqual(Guid.Empty, secretVersion.SecretId); + Assert.NotNull(secretVersion.Value); + Assert.NotEqual(default, secretVersion.VersionDate); + } + + [Theory] + [BitAutoData] + public void SecretVersion_WithServiceAccountEditor_Success(SecretVersion secretVersion, Guid serviceAccountId) + { + // Arrange & Act + secretVersion.EditorServiceAccountId = serviceAccountId; + secretVersion.EditorOrganizationUserId = null; + + // Assert + Assert.Equal(serviceAccountId, secretVersion.EditorServiceAccountId); + Assert.Null(secretVersion.EditorOrganizationUserId); + } + + [Theory] + [BitAutoData] + public void SecretVersion_WithOrganizationUserEditor_Success(SecretVersion secretVersion, Guid organizationUserId) + { + // Arrange & Act + secretVersion.EditorOrganizationUserId = organizationUserId; + secretVersion.EditorServiceAccountId = null; + + // Assert + Assert.Equal(organizationUserId, secretVersion.EditorOrganizationUserId); + Assert.Null(secretVersion.EditorServiceAccountId); + } + + [Theory] + [BitAutoData] + public void SecretVersion_NullableEditors_Success(SecretVersion secretVersion) + { + // Arrange & Act + secretVersion.EditorServiceAccountId = null; + secretVersion.EditorOrganizationUserId = null; + + // Assert + Assert.Null(secretVersion.EditorServiceAccountId); + Assert.Null(secretVersion.EditorOrganizationUserId); + } + + [Theory] + [BitAutoData] + public void SecretVersion_VersionDateSet_Success(SecretVersion secretVersion) + { + // Arrange + var versionDate = DateTime.UtcNow; + + // Act + secretVersion.VersionDate = versionDate; + + // Assert + Assert.Equal(versionDate, secretVersion.VersionDate); + } + + [Theory] + [BitAutoData] + public void SecretVersion_ValueEncrypted_Success(SecretVersion secretVersion, string encryptedValue) + { + // Arrange & Act + secretVersion.Value = encryptedValue; + + // Assert + Assert.Equal(encryptedValue, secretVersion.Value); + Assert.NotEmpty(secretVersion.Value); + } + + [Theory] + [BitAutoData] + public void SecretVersion_MultipleVersions_DifferentIds(List secretVersions, Guid secretId) + { + // Arrange & Act + foreach (var version in secretVersions) + { + version.SecretId = secretId; + version.SetNewId(); + } + + // Assert + var distinctIds = secretVersions.Select(v => v.Id).Distinct(); + Assert.Equal(secretVersions.Count, distinctIds.Count()); + Assert.All(secretVersions, v => Assert.Equal(secretId, v.SecretId)); + } + + [Theory] + [BitAutoData] + public void SecretVersion_VersionDateOrdering_Success(SecretVersion version1, SecretVersion version2, SecretVersion version3, Guid secretId) + { + // Arrange + var now = DateTime.UtcNow; + version1.SecretId = secretId; + version1.VersionDate = now.AddDays(-2); + + version2.SecretId = secretId; + version2.VersionDate = now.AddDays(-1); + + version3.SecretId = secretId; + version3.VersionDate = now; + + var versions = new List { version2, version3, version1 }; + + // Act + var orderedVersions = versions.OrderByDescending(v => v.VersionDate).ToList(); + + // Assert + Assert.Equal(version3.Id, orderedVersions[0].Id); // Most recent + Assert.Equal(version2.Id, orderedVersions[1].Id); + Assert.Equal(version1.Id, orderedVersions[2].Id); // Oldest + } +} diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index a5272413e2..ae1d12e887 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -42,7 +42,6 @@ public class PoliciesController : Controller private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; private readonly IPolicyRepository _policyRepository; private readonly IUserService _userService; - private readonly IFeatureService _featureService; private readonly ISavePolicyCommand _savePolicyCommand; private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand; @@ -55,7 +54,6 @@ public class PoliciesController : Controller IDataProtectorTokenFactory orgUserInviteTokenDataFactory, IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery, IOrganizationRepository organizationRepository, - IFeatureService featureService, ISavePolicyCommand savePolicyCommand, IVNextSavePolicyCommand vNextSavePolicyCommand) { @@ -69,7 +67,6 @@ public class PoliciesController : Controller _organizationRepository = organizationRepository; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; _organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery; - _featureService = featureService; _savePolicyCommand = savePolicyCommand; _vNextSavePolicyCommand = vNextSavePolicyCommand; } @@ -221,9 +218,7 @@ public class PoliciesController : Controller { var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, type, _currentContext); - var policy = _featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) ? - await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest) : - await _savePolicyCommand.VNextSaveAsync(savePolicyRequest); + var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyRequest); return new PolicyResponseModel(policy); } diff --git a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs index be0997f271..cf8da813be 100644 --- a/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Public/Controllers/PoliciesController.cs @@ -5,15 +5,10 @@ using System.Net; using Bit.Api.AdminConsole.Public.Models.Request; using Bit.Api.AdminConsole.Public.Models.Response; using Bit.Api.Models.Public.Response; -using Bit.Core; -using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Services; using Bit.Core.Context; -using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -24,25 +19,16 @@ namespace Bit.Api.AdminConsole.Public.Controllers; public class PoliciesController : Controller { private readonly IPolicyRepository _policyRepository; - private readonly IPolicyService _policyService; private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; - private readonly ISavePolicyCommand _savePolicyCommand; private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand; public PoliciesController( IPolicyRepository policyRepository, - IPolicyService policyService, ICurrentContext currentContext, - IFeatureService featureService, - ISavePolicyCommand savePolicyCommand, IVNextSavePolicyCommand vNextSavePolicyCommand) { _policyRepository = policyRepository; - _policyService = policyService; _currentContext = currentContext; - _featureService = featureService; - _savePolicyCommand = savePolicyCommand; _vNextSavePolicyCommand = vNextSavePolicyCommand; } @@ -97,17 +83,8 @@ public class PoliciesController : Controller [ProducesResponseType((int)HttpStatusCode.NotFound)] public async Task Put(PolicyType type, [FromBody] PolicyUpdateRequestModel model) { - Policy policy; - if (_featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)) - { - var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type); - policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel); - } - else - { - var policyUpdate = model.ToPolicyUpdate(_currentContext.OrganizationId!.Value, type); - policy = await _savePolicyCommand.SaveAsync(policyUpdate); - } + var savePolicyModel = model.ToSavePolicyModel(_currentContext.OrganizationId!.Value, type); + var policy = await _vNextSavePolicyCommand.SaveAsync(savePolicyModel); var response = new PolicyResponseModel(policy); return new JsonResult(response); diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs index 7968970048..5feda856d5 100644 --- a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -80,7 +80,7 @@ public class AccountsKeyManagementController : Controller [HttpPost("key-management/regenerate-keys")] public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request) { - if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration)) + if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration) && !_featureService.IsEnabled(FeatureFlagKeys.DataRecoveryTool)) { throw new NotFoundException(); } diff --git a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs new file mode 100644 index 0000000000..86e2d1f7e9 --- /dev/null +++ b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs @@ -0,0 +1,337 @@ +using Bit.Api.Models.Response; +using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Auth.Identity; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.SecretsManager.Controllers; + +[Authorize("secrets")] +public class SecretVersionsController : Controller +{ + private readonly ICurrentContext _currentContext; + private readonly ISecretVersionRepository _secretVersionRepository; + private readonly ISecretRepository _secretRepository; + private readonly IUserService _userService; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public SecretVersionsController( + ICurrentContext currentContext, + ISecretVersionRepository secretVersionRepository, + ISecretRepository secretRepository, + IUserService userService, + IOrganizationUserRepository organizationUserRepository) + { + _currentContext = currentContext; + _secretVersionRepository = secretVersionRepository; + _secretRepository = secretRepository; + _userService = userService; + _organizationUserRepository = organizationUserRepository; + } + + [HttpGet("secrets/{secretId}/versions")] + public async Task> GetVersionsBySecretIdAsync([FromRoute] Guid secretId) + { + var secret = await _secretRepository.GetByIdAsync(secretId); + if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId)) + { + throw new NotFoundException(); + } + + // For service accounts and organization API, skip user-level access checks + if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount || + _currentContext.IdentityClientType == IdentityClientType.Organization) + { + // Already verified Secrets Manager access above + var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId); + var responseList = versionList.Select(v => new SecretVersionResponseModel(v)); + return new ListResponseModel(responseList); + } + + var userId = _userService.GetProperUserId(User); + if (!userId.HasValue) + { + throw new NotFoundException(); + } + + var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin); + + var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient); + if (!access.Read) + { + throw new NotFoundException(); + } + + var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secretId); + var responses = versions.Select(v => new SecretVersionResponseModel(v)); + + return new ListResponseModel(responses); + } + + [HttpGet("secret-versions/{id}")] + public async Task GetByIdAsync([FromRoute] Guid id) + { + var secretVersion = await _secretVersionRepository.GetByIdAsync(id); + if (secretVersion == null) + { + throw new NotFoundException(); + } + + var secret = await _secretRepository.GetByIdAsync(secretVersion.SecretId); + if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId)) + { + throw new NotFoundException(); + } + + // For service accounts and organization API, skip user-level access checks + if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount || + _currentContext.IdentityClientType == IdentityClientType.Organization) + { + // Already verified Secrets Manager access above + return new SecretVersionResponseModel(secretVersion); + } + + var userId = _userService.GetProperUserId(User); + if (!userId.HasValue) + { + throw new NotFoundException(); + } + + var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin); + + var access = await _secretRepository.AccessToSecretAsync(secretVersion.SecretId, userId.Value, accessClient); + if (!access.Read) + { + throw new NotFoundException(); + } + + return new SecretVersionResponseModel(secretVersion); + } + + [HttpPost("secret-versions/get-by-ids")] + public async Task> GetManyByIdsAsync([FromBody] List ids) + { + if (!ids.Any()) + { + throw new BadRequestException("No version IDs provided."); + } + + // Get all versions + var versions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList(); + if (!versions.Any()) + { + throw new NotFoundException(); + } + + // Get all associated secrets and check permissions + var secretIds = versions.Select(v => v.SecretId).Distinct().ToList(); + var secrets = (await _secretRepository.GetManyByIds(secretIds)).ToList(); + + if (!secrets.Any()) + { + throw new NotFoundException(); + } + + // Ensure all secrets belong to the same organization + var organizationId = secrets.First().OrganizationId; + if (secrets.Any(s => s.OrganizationId != organizationId) || + !_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + // For service accounts and organization API, skip user-level access checks + if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount || + _currentContext.IdentityClientType == IdentityClientType.Organization) + { + // Already verified Secrets Manager access and organization ownership above + var serviceAccountResponses = versions.Select(v => new SecretVersionResponseModel(v)); + return new ListResponseModel(serviceAccountResponses); + } + + var userId = _userService.GetProperUserId(User); + if (!userId.HasValue) + { + throw new NotFoundException(); + } + + var isAdmin = await _currentContext.OrganizationAdmin(organizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin); + + // Verify read access to all associated secrets + var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient); + if (accessResults.Values.Any(access => !access.Read)) + { + throw new NotFoundException(); + } + + var responses = versions.Select(v => new SecretVersionResponseModel(v)); + return new ListResponseModel(responses); + } + + [HttpPut("secrets/{secretId}/versions/restore")] + public async Task RestoreVersionAsync([FromRoute] Guid secretId, [FromBody] RestoreSecretVersionRequestModel request) + { + if (!(_currentContext.IdentityClientType == IdentityClientType.User || _currentContext.IdentityClientType == IdentityClientType.ServiceAccount)) + { + throw new NotFoundException(); + } + + var secret = await _secretRepository.GetByIdAsync(secretId); + if (secret == null || !_currentContext.AccessSecretsManager(secret.OrganizationId)) + { + throw new NotFoundException(); + } + + // Get the version first to validate it belongs to this secret + var version = await _secretVersionRepository.GetByIdAsync(request.VersionId); + if (version == null || version.SecretId != secretId) + { + throw new NotFoundException(); + } + + // Store the current value before restoration + var currentValue = secret.Value; + + // For service accounts and organization API, skip user-level access checks + if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) + { + // Save current value as a version before restoring + if (currentValue != version.Value) + { + var editorUserId = _userService.GetProperUserId(User); + if (editorUserId.HasValue) + { + var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion + { + SecretId = secretId, + Value = currentValue!, + VersionDate = DateTime.UtcNow, + EditorServiceAccountId = editorUserId.Value + }; + + await _secretVersionRepository.CreateAsync(currentVersionSnapshot); + } + } + + // Already verified Secrets Manager access above + secret.Value = version.Value; + secret.RevisionDate = DateTime.UtcNow; + var updatedSec = await _secretRepository.UpdateAsync(secret); + return new SecretResponseModel(updatedSec, true, true); + } + + var userId = _userService.GetProperUserId(User); + if (!userId.HasValue) + { + throw new NotFoundException(); + } + + var orgAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin); + + var access = await _secretRepository.AccessToSecretAsync(secretId, userId.Value, accessClient); + if (!access.Write) + { + throw new NotFoundException(); + } + + // Save current value as a version before restoring + if (currentValue != version.Value) + { + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId.Value); + if (orgUser == null) + { + throw new NotFoundException(); + } + + var currentVersionSnapshot = new Core.SecretsManager.Entities.SecretVersion + { + SecretId = secretId, + Value = currentValue!, + VersionDate = DateTime.UtcNow, + EditorOrganizationUserId = orgUser.Id + }; + + await _secretVersionRepository.CreateAsync(currentVersionSnapshot); + } + + // Update the secret with the version's value + secret.Value = version.Value; + secret.RevisionDate = DateTime.UtcNow; + + var updatedSecret = await _secretRepository.UpdateAsync(secret); + + return new SecretResponseModel(updatedSecret, true, true); + } + + [HttpPost("secret-versions/delete")] + public async Task BulkDeleteAsync([FromBody] List ids) + { + if (!ids.Any()) + { + throw new BadRequestException("No version IDs provided."); + } + + var secretVersions = (await _secretVersionRepository.GetManyByIdsAsync(ids)).ToList(); + if (secretVersions.Count != ids.Count) + { + throw new NotFoundException(); + } + + // Ensure all versions belong to secrets in the same organization + var secretIds = secretVersions.Select(v => v.SecretId).Distinct().ToList(); + var secrets = await _secretRepository.GetManyByIds(secretIds); + var secretsList = secrets.ToList(); + + if (!secretsList.Any()) + { + throw new NotFoundException(); + } + + var organizationId = secretsList.First().OrganizationId; + if (secretsList.Any(s => s.OrganizationId != organizationId) || + !_currentContext.AccessSecretsManager(organizationId)) + { + throw new NotFoundException(); + } + + // For service accounts and organization API, skip user-level access checks + if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount || + _currentContext.IdentityClientType == IdentityClientType.Organization) + { + // Already verified Secrets Manager access and organization ownership above + await _secretVersionRepository.DeleteManyByIdAsync(ids); + return Ok(); + } + + var userId = _userService.GetProperUserId(User); + if (!userId.HasValue) + { + throw new NotFoundException(); + } + + var orgAdmin = await _currentContext.OrganizationAdmin(organizationId); + var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, orgAdmin); + + // Verify write access to all associated secrets + var accessResults = await _secretRepository.AccessToSecretsAsync(secretIds, userId.Value, accessClient); + if (accessResults.Values.Any(access => !access.Write)) + { + throw new NotFoundException(); + } + + await _secretVersionRepository.DeleteManyByIdAsync(ids); + + return Ok(); + } +} diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index e263b9747d..dcfe1be111 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -8,6 +8,7 @@ using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Repositories; using Bit.Core.SecretsManager.AuthorizationRequirements; using Bit.Core.SecretsManager.Commands.Secrets.Interfaces; using Bit.Core.SecretsManager.Entities; @@ -29,6 +30,7 @@ public class SecretsController : Controller private readonly ICurrentContext _currentContext; private readonly IProjectRepository _projectRepository; private readonly ISecretRepository _secretRepository; + private readonly ISecretVersionRepository _secretVersionRepository; private readonly ICreateSecretCommand _createSecretCommand; private readonly IUpdateSecretCommand _updateSecretCommand; private readonly IDeleteSecretCommand _deleteSecretCommand; @@ -38,11 +40,13 @@ public class SecretsController : Controller private readonly IUserService _userService; private readonly IEventService _eventService; private readonly IAuthorizationService _authorizationService; + private readonly IOrganizationUserRepository _organizationUserRepository; public SecretsController( ICurrentContext currentContext, IProjectRepository projectRepository, ISecretRepository secretRepository, + ISecretVersionRepository secretVersionRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand, @@ -51,11 +55,13 @@ public class SecretsController : Controller ISecretAccessPoliciesUpdatesQuery secretAccessPoliciesUpdatesQuery, IUserService userService, IEventService eventService, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + IOrganizationUserRepository organizationUserRepository) { _currentContext = currentContext; _projectRepository = projectRepository; _secretRepository = secretRepository; + _secretVersionRepository = secretVersionRepository; _createSecretCommand = createSecretCommand; _updateSecretCommand = updateSecretCommand; _deleteSecretCommand = deleteSecretCommand; @@ -65,6 +71,7 @@ public class SecretsController : Controller _userService = userService; _eventService = eventService; _authorizationService = authorizationService; + _organizationUserRepository = organizationUserRepository; } @@ -190,6 +197,44 @@ public class SecretsController : Controller } } + // Create a version record if the value changed + if (updateRequest.ValueChanged) + { + // Store the old value before updating + var oldValue = secret.Value; + var userId = _userService.GetProperUserId(User)!.Value; + Guid? editorServiceAccountId = null; + Guid? editorOrganizationUserId = null; + + if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) + { + editorServiceAccountId = userId; + } + else if (_currentContext.IdentityClientType == IdentityClientType.User) + { + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId); + if (orgUser != null) + { + editorOrganizationUserId = orgUser.Id; + } + else + { + throw new NotFoundException(); + } + } + + var secretVersion = new SecretVersion + { + SecretId = id, + Value = oldValue, + VersionDate = DateTime.UtcNow, + EditorServiceAccountId = editorServiceAccountId, + EditorOrganizationUserId = editorOrganizationUserId + }; + + await _secretVersionRepository.CreateAsync(secretVersion); + } + var result = await _updateSecretCommand.UpdateAsync(updatedSecret, accessPoliciesUpdates); await LogSecretEventAsync(secret, EventType.Secret_Edited); diff --git a/src/Api/SecretsManager/Models/Request/RestoreSecretVersionRequestModel.cs b/src/Api/SecretsManager/Models/Request/RestoreSecretVersionRequestModel.cs new file mode 100644 index 0000000000..19a6b35a75 --- /dev/null +++ b/src/Api/SecretsManager/Models/Request/RestoreSecretVersionRequestModel.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.SecretsManager.Models.Request; + +public class RestoreSecretVersionRequestModel +{ + [Required] + public Guid VersionId { get; set; } +} diff --git a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs index b95bc9e500..9d19e1d8cc 100644 --- a/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs +++ b/src/Api/SecretsManager/Models/Request/SecretUpdateRequestModel.cs @@ -28,6 +28,8 @@ public class SecretUpdateRequestModel : IValidatableObject public SecretAccessPoliciesRequestsModel AccessPoliciesRequests { get; set; } + public bool ValueChanged { get; set; } = false; + public Secret ToSecret(Secret secret) { secret.Key = Key; diff --git a/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs b/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs new file mode 100644 index 0000000000..07b8e88f7e --- /dev/null +++ b/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs @@ -0,0 +1,28 @@ +using Bit.Core.Models.Api; +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Api.SecretsManager.Models.Response; + +public class SecretVersionResponseModel : ResponseModel +{ + private const string _objectName = "secretVersion"; + + public Guid Id { get; set; } + public Guid SecretId { get; set; } + public string Value { get; set; } = string.Empty; + public DateTime VersionDate { get; set; } + public Guid? EditorServiceAccountId { get; set; } + public Guid? EditorOrganizationUserId { get; set; } + + public SecretVersionResponseModel() : base(_objectName) { } + + public SecretVersionResponseModel(SecretVersion secretVersion) : base(_objectName) + { + Id = secretVersion.Id; + SecretId = secretVersion.SecretId; + Value = secretVersion.Value; + VersionDate = secretVersion.VersionDate; + EditorServiceAccountId = secretVersion.EditorServiceAccountId; + EditorOrganizationUserId = secretVersion.EditorOrganizationUserId; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index 595e487580..e6cc3da2a2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -4,7 +4,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Context; @@ -25,8 +24,6 @@ public class VerifyOrganizationDomainCommand( IEventService eventService, IGlobalSettings globalSettings, ICurrentContext currentContext, - IFeatureService featureService, - ISavePolicyCommand savePolicyCommand, IVNextSavePolicyCommand vNextSavePolicyCommand, IMailService mailService, IOrganizationUserRepository organizationUserRepository, @@ -144,15 +141,8 @@ public class VerifyOrganizationDomainCommand( PerformedBy = actingUser }; - if (featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)) - { - var savePolicyModel = new SavePolicyModel(policyUpdate, actingUser); - await vNextSavePolicyCommand.SaveAsync(savePolicyModel); - } - else - { - await savePolicyCommand.SaveAsync(policyUpdate); - } + var savePolicyModel = new SavePolicyModel(policyUpdate, actingUser); + await vNextSavePolicyCommand.SaveAsync(savePolicyModel); } private async Task SendVerifiedDomainUserEmailAsync(OrganizationDomain domain) diff --git a/src/Core/Auth/Services/Implementations/SsoConfigService.cs b/src/Core/Auth/Services/Implementations/SsoConfigService.cs index 1a35585b2c..0cb8b68042 100644 --- a/src/Core/Auth/Services/Implementations/SsoConfigService.cs +++ b/src/Core/Auth/Services/Implementations/SsoConfigService.cs @@ -5,7 +5,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -26,8 +25,6 @@ public class SsoConfigService : ISsoConfigService private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IEventService _eventService; - private readonly IFeatureService _featureService; - private readonly ISavePolicyCommand _savePolicyCommand; private readonly IVNextSavePolicyCommand _vNextSavePolicyCommand; public SsoConfigService( @@ -36,8 +33,6 @@ public class SsoConfigService : ISsoConfigService IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IEventService eventService, - IFeatureService featureService, - ISavePolicyCommand savePolicyCommand, IVNextSavePolicyCommand vNextSavePolicyCommand) { _ssoConfigRepository = ssoConfigRepository; @@ -45,8 +40,6 @@ public class SsoConfigService : ISsoConfigService _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _eventService = eventService; - _featureService = featureService; - _savePolicyCommand = savePolicyCommand; _vNextSavePolicyCommand = vNextSavePolicyCommand; } @@ -97,19 +90,10 @@ public class SsoConfigService : ISsoConfigService Enabled = true }; - if (_featureService.IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor)) - { - var performedBy = new SystemUser(EventSystemUser.Unknown); - await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(singleOrgPolicy, performedBy)); - await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(resetPasswordPolicy, performedBy)); - await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(requireSsoPolicy, performedBy)); - } - else - { - await _savePolicyCommand.SaveAsync(singleOrgPolicy); - await _savePolicyCommand.SaveAsync(resetPasswordPolicy); - await _savePolicyCommand.SaveAsync(requireSsoPolicy); - } + var performedBy = new SystemUser(EventSystemUser.Unknown); + await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(singleOrgPolicy, performedBy)); + await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(resetPasswordPolicy, performedBy)); + await _vNextSavePolicyCommand.SaveAsync(new SavePolicyModel(requireSsoPolicy, performedBy)); } await LogEventsAsync(config, oldConfig); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0d3dd37df4..ccc3555567 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -141,7 +141,6 @@ public static class FeatureFlagKeys public const string AutomaticConfirmUsers = "pm-19934-auto-confirm-organization-users"; public const string PM23845_VNextApplicationCache = "pm-24957-refactor-memory-application-cache"; public const string BlockClaimedDomainAccountCreation = "pm-28297-block-uninvited-claimed-domain-registration"; - public const string PolicyValidatorsRefactor = "pm-26423-refactor-policy-side-effects"; public const string IncreaseBulkReinviteLimitForCloud = "pm-28251-increase-bulk-reinvite-limit-for-cloud"; /* Architecture */ @@ -213,6 +212,7 @@ public static class FeatureFlagKeys public const string NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change"; public const string DisableType0Decryption = "pm-25174-disable-type-0-decryption"; public const string ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component"; + public const string DataRecoveryTool = "pm-28813-data-recovery-tool"; /* Mobile Team */ public const string AndroidImportLoginsFlow = "import-logins-flow"; diff --git a/src/Core/SecretsManager/Repositories/ISecretVersionRepository.cs b/src/Core/SecretsManager/Repositories/ISecretVersionRepository.cs new file mode 100644 index 0000000000..b6dd1d778d --- /dev/null +++ b/src/Core/SecretsManager/Repositories/ISecretVersionRepository.cs @@ -0,0 +1,12 @@ +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Core.SecretsManager.Repositories; + +public interface ISecretVersionRepository +{ + Task GetByIdAsync(Guid id); + Task> GetManyBySecretIdAsync(Guid secretId); + Task> GetManyByIdsAsync(IEnumerable ids); + Task CreateAsync(SecretVersion secretVersion); + Task DeleteManyByIdAsync(IEnumerable ids); +} diff --git a/src/Core/SecretsManager/Repositories/Noop/NoopSecretVersionRepository.cs b/src/Core/SecretsManager/Repositories/Noop/NoopSecretVersionRepository.cs new file mode 100644 index 0000000000..caa5d96a7c --- /dev/null +++ b/src/Core/SecretsManager/Repositories/Noop/NoopSecretVersionRepository.cs @@ -0,0 +1,31 @@ +using Bit.Core.SecretsManager.Entities; + +namespace Bit.Core.SecretsManager.Repositories.Noop; + +public class NoopSecretVersionRepository : ISecretVersionRepository +{ + public Task GetByIdAsync(Guid id) + { + return Task.FromResult(null as SecretVersion); + } + + public Task> GetManyBySecretIdAsync(Guid secretId) + { + return Task.FromResult(Enumerable.Empty()); + } + + public Task CreateAsync(SecretVersion secretVersion) + { + return Task.FromResult(secretVersion); + } + + public Task DeleteManyByIdAsync(IEnumerable ids) + { + return Task.CompletedTask; + } + + public Task> GetManyByIdsAsync(IEnumerable ids) + { + return Task.FromResult(Enumerable.Empty()); + } +} diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 3446d1af2a..a1d4af464a 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -483,7 +483,7 @@ public class GlobalSettings : IGlobalSettings public string CertificatePassword { get; set; } public string RedisConnectionString { get; set; } public string CosmosConnectionString { get; set; } - public string LicenseKey { get; set; } = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZWtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzM0NTY2NDAwLCJleHAiOjE3NjQ5NzkyMDAsImNvbXBhbnlfbmFtZSI6IkJpdHdhcmRlbiBJbmMuIiwiY29udGFjdF9pbmZvIjoiY29udGFjdEBkdWVuZGVzb2Z0d2FyZS5jb20iLCJlZGl0aW9uIjoiU3RhcnRlciIsImlkIjoiNjg3OCIsImZlYXR1cmUiOlsiaXN2IiwidW5saW1pdGVkX2NsaWVudHMiXSwicHJvZHVjdCI6IkJpdHdhcmRlbiJ9.TYc88W_t2t0F2AJV3rdyKwGyQKrKFriSAzm1tWFNHNR9QizfC-8bliGdT4Wgeie-ynCXs9wWaF-sKC5emg--qS7oe2iIt67Qd88WS53AwgTvAddQRA4NhGB1R7VM8GAikLieSos-DzzwLYRgjZdmcsprItYGSJuY73r-7-F97ta915majBytVxGF966tT9zF1aYk0bA8FS6DcDYkr5f7Nsy8daS_uIUAgNa_agKXtmQPqKujqtUb6rgWEpSp4OcQcG-8Dpd5jHqoIjouGvY-5LTgk5WmLxi_m-1QISjxUJrUm-UGao3_VwV5KFGqYrz8csdTl-HS40ihWcsWnrV0ug"; + public string LicenseKey { get; set; } = "eyJhbGciOiJQUzI1NiIsImtpZCI6IklkZW50aXR5U2VydmVyTGljZW5zZUtleS83Y2VhZGJiNzgxMzA0NjllODgwNjg5MTAyNTQxNGYxNiIsInR5cCI6ImxpY2Vuc2Urand0In0.eyJpc3MiOiJodHRwczovL2R1ZW5kZXNvZnR3YXJlLmNvbSIsImF1ZCI6IklkZW50aXR5U2VydmVyIiwiaWF0IjoxNzY1MDY1NjAwLCJleHAiOjE3OTY1MTUyMDAsImNvbXBhbnlfbmFtZSI6IkJpdHdhcmRlbiBJbmMuIiwiY29udGFjdF9pbmZvIjoiY29udGFjdEBkdWVuZGVzb2Z0d2FyZS5jb20iLCJlZGl0aW9uIjoiU3RhcnRlciIsImlkIjoiOTUxNSIsImZlYXR1cmUiOlsiaXN2IiwidW5saW1pdGVkX2NsaWVudHMiXSwiY2xpZW50X2xpbWl0IjowfQ.rWUsq-XBKNwPG7BRKG-vShXHuyHLHJCh0sEWdWT4Rkz4ArIPOAepEp9wNya-hxFKkBTFlPaQ5IKk4wDTvkQkuq1qaI_v6kSCdaP9fvXp0rmh4KcFEffVLB-wAOK2S2Cld5DzdyCoskUUfwNQP7xuLsz2Ydxe_whSRIdv8bsMbvTC3Kl8PYZPZ4MxqW8rSZ_mEuCpSe5-Q40sB7aiu_7YmWLJaKrfBTIqYH-XuzQj36Aemoei0efcntej-gvxovy-5SiSEsGuRZj41rjEZYOuj5KgHihJViO1VDHK6CNtlu2Ks8bkv6G2hO-TkF16Y28ywEG_beLEf_s5dzhbDBDbvA"; /// /// Sliding lifetime of a refresh token in seconds. /// diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 4d75da92fe..38a4813ecd 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -161,16 +161,16 @@ public class CustomTokenRequestValidator : BaseRequestValidator(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); } diff --git a/test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs new file mode 100644 index 0000000000..9393795e55 --- /dev/null +++ b/test/Api.IntegrationTest/SecretsManager/Controllers/SecretVersionsControllerTests.cs @@ -0,0 +1,289 @@ +using System.Net; +using Bit.Api.IntegrationTest.Factories; +using Bit.Api.IntegrationTest.SecretsManager.Enums; +using Bit.Api.IntegrationTest.SecretsManager.Helpers; +using Bit.Api.Models.Response; +using Bit.Api.SecretsManager.Models.Request; +using Bit.Api.SecretsManager.Models.Response; +using Bit.Core.Enums; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Repositories; +using Xunit; + +namespace Bit.Api.IntegrationTest.SecretsManager.Controllers; + +public class SecretVersionsControllerTests : 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 readonly ISecretVersionRepository _secretVersionRepository; + private readonly IAccessPolicyRepository _accessPolicyRepository; + private readonly LoginHelper _loginHelper; + + private string _email = null!; + private SecretsManagerOrganizationHelper _organizationHelper = null!; + + public SecretVersionsControllerTests(ApiApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + _secretRepository = _factory.GetService(); + _secretVersionRepository = _factory.GetService(); + _accessPolicyRepository = _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 GetVersionsBySecretId_SmAccessDenied_NotFound(bool useSecrets, bool accessSecrets, bool organizationEnabled) + { + var (org, _) = await _organizationHelper.Initialize(useSecrets, accessSecrets, organizationEnabled); + await _loginHelper.LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + var response = await _client.GetAsync($"/secrets/{secret.Id}/versions"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Theory] + [InlineData(PermissionType.RunAsAdmin)] + [InlineData(PermissionType.RunAsUserWithPermission)] + public async Task GetVersionsBySecretId_Success(PermissionType permissionType) + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + await _loginHelper.LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + // Create some versions + var version1 = await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = _mockEncryptedString, + VersionDate = DateTime.UtcNow.AddDays(-2) + }); + + var version2 = await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = _mockEncryptedString, + VersionDate = DateTime.UtcNow.AddDays(-1) + }); + + if (permissionType == PermissionType.RunAsUserWithPermission) + { + var (email, orgUser) = await _organizationHelper.CreateNewUser(OrganizationUserType.User, true); + await _loginHelper.LoginAsync(email); + + var accessPolicies = new List + { + new UserSecretAccessPolicy + { + GrantedSecretId = secret.Id, + OrganizationUserId = orgUser.Id, + Read = true, + Write = true + } + }; + await _accessPolicyRepository.CreateManyAsync(accessPolicies); + } + + var response = await _client.GetAsync($"/secrets/{secret.Id}/versions"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + + Assert.NotNull(result); + Assert.Equal(2, result.Data.Count()); + } + + [Fact] + public async Task GetVersionById_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + await _loginHelper.LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + var version = await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = _mockEncryptedString, + VersionDate = DateTime.UtcNow + }); + + var response = await _client.GetAsync($"/secret-versions/{version.Id}"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(result); + Assert.Equal(version.Id, result.Id); + Assert.Equal(secret.Id, result.SecretId); + } + + [Fact] + public async Task RestoreVersion_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + await _loginHelper.LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = "OriginalValue", + Note = _mockEncryptedString + }); + + var version = await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = "OldValue", + VersionDate = DateTime.UtcNow.AddDays(-1) + }); + + var request = new RestoreSecretVersionRequestModel + { + VersionId = version.Id + }; + + var response = await _client.PutAsJsonAsync($"/secrets/{secret.Id}/versions/restore", request); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(result); + Assert.Equal("OldValue", result.Value); + } + + [Fact] + public async Task BulkDelete_Success() + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + await _loginHelper.LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + var version1 = await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = _mockEncryptedString, + VersionDate = DateTime.UtcNow.AddDays(-2) + }); + + var version2 = await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = _mockEncryptedString, + VersionDate = DateTime.UtcNow.AddDays(-1) + }); + + var ids = new List { version1.Id, version2.Id }; + + var response = await _client.PostAsJsonAsync("/secret-versions/delete", ids); + response.EnsureSuccessStatusCode(); + + var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secret.Id); + Assert.Empty(versions); + } + + [Fact] + public async Task GetVersionsBySecretId_ReturnsOrderedByVersionDate() + { + var (org, _) = await _organizationHelper.Initialize(true, true, true); + await _loginHelper.LoginAsync(_email); + + var secret = await _secretRepository.CreateAsync(new Secret + { + OrganizationId = org.Id, + Key = _mockEncryptedString, + Value = _mockEncryptedString, + Note = _mockEncryptedString + }); + + // Create versions in random order + await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = "Version2", + VersionDate = DateTime.UtcNow.AddDays(-1) + }); + + await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = "Version3", + VersionDate = DateTime.UtcNow + }); + + await _secretVersionRepository.CreateAsync(new SecretVersion + { + SecretId = secret.Id, + Value = "Version1", + VersionDate = DateTime.UtcNow.AddDays(-2) + }); + + var response = await _client.GetAsync($"/secrets/{secret.Id}/versions"); + response.EnsureSuccessStatusCode(); + + var result = await response.Content.ReadFromJsonAsync>(); + + Assert.NotNull(result); + Assert.Equal(3, result.Data.Count()); + + var versions = result.Data.ToList(); + // Should be ordered by VersionDate descending (newest first) + Assert.Equal("Version3", versions[0].Value); + Assert.Equal("Version2", versions[1].Value); + Assert.Equal("Version1", versions[2].Value); + } +} diff --git a/test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs b/test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs index c2360f5f9a..bd10eab617 100644 --- a/test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs +++ b/test/Api.Test/AdminConsole/Public/Controllers/PoliciesControllerTests.cs @@ -1,14 +1,11 @@ using Bit.Api.AdminConsole.Public.Controllers; using Bit.Api.AdminConsole.Public.Models.Request; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Context; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -22,7 +19,7 @@ public class PoliciesControllerTests { [Theory] [BitAutoData] - public async Task Put_WhenPolicyValidatorsRefactorEnabled_UsesVNextSavePolicyCommand( + public async Task Put_UsesVNextSavePolicyCommand( Guid organizationId, PolicyType policyType, PolicyUpdateRequestModel model, @@ -33,9 +30,6 @@ public class PoliciesControllerTests policy.Data = null; sutProvider.GetDependency() .OrganizationId.Returns(organizationId); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) - .Returns(true); sutProvider.GetDependency() .SaveAsync(Arg.Any()) .Returns(policy); @@ -52,36 +46,4 @@ public class PoliciesControllerTests m.PolicyUpdate.Enabled == model.Enabled.GetValueOrDefault() && m.PerformedBy is SystemUser)); } - - [Theory] - [BitAutoData] - public async Task Put_WhenPolicyValidatorsRefactorDisabled_UsesLegacySavePolicyCommand( - Guid organizationId, - PolicyType policyType, - PolicyUpdateRequestModel model, - Policy policy, - SutProvider sutProvider) - { - // Arrange - policy.Data = null; - sutProvider.GetDependency() - .OrganizationId.Returns(organizationId); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) - .Returns(false); - sutProvider.GetDependency() - .SaveAsync(Arg.Any()) - .Returns(policy); - - // Act - await sutProvider.Sut.Put(policyType, model); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .SaveAsync(Arg.Is(p => - p.OrganizationId == organizationId && - p.Type == policyType && - p.Enabled == model.Enabled)); - } } diff --git a/test/Api.Test/Controllers/PoliciesControllerTests.cs b/test/Api.Test/Controllers/PoliciesControllerTests.cs index 89d6ddefdc..efb9f7aaa9 100644 --- a/test/Api.Test/Controllers/PoliciesControllerTests.cs +++ b/test/Api.Test/Controllers/PoliciesControllerTests.cs @@ -3,7 +3,6 @@ using System.Text.Json; using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request; using Bit.Api.AdminConsole.Models.Response.Organizations; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; @@ -291,7 +290,7 @@ public class PoliciesControllerTests string token, string email, Organization organization - ) + ) { // Arrange organization.UsePolicies = true; @@ -302,14 +301,15 @@ public class PoliciesControllerTests var decryptedToken = Substitute.For(); decryptedToken.Valid.Returns(false); - var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + var orgUserInviteTokenDataFactory = + sutProvider.GetDependency>(); orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) .Returns(x => - { - x[1] = decryptedToken; - return true; - }); + { + x[1] = decryptedToken; + return true; + }); // Act & Assert await Assert.ThrowsAsync(() => @@ -325,7 +325,7 @@ public class PoliciesControllerTests string token, string email, Organization organization - ) + ) { // Arrange organization.UsePolicies = true; @@ -338,14 +338,15 @@ public class PoliciesControllerTests decryptedToken.OrgUserId = organizationUserId; decryptedToken.OrgUserEmail = email; - var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + var orgUserInviteTokenDataFactory = + sutProvider.GetDependency>(); orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) .Returns(x => - { - x[1] = decryptedToken; - return true; - }); + { + x[1] = decryptedToken; + return true; + }); sutProvider.GetDependency() .GetByIdAsync(organizationUserId) @@ -366,7 +367,7 @@ public class PoliciesControllerTests string email, OrganizationUser orgUser, Organization organization - ) + ) { // Arrange organization.UsePolicies = true; @@ -379,14 +380,15 @@ public class PoliciesControllerTests decryptedToken.OrgUserId = organizationUserId; decryptedToken.OrgUserEmail = email; - var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + var orgUserInviteTokenDataFactory = + sutProvider.GetDependency>(); orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) .Returns(x => - { - x[1] = decryptedToken; - return true; - }); + { + x[1] = decryptedToken; + return true; + }); orgUser.OrganizationId = Guid.Empty; @@ -409,7 +411,7 @@ public class PoliciesControllerTests string email, OrganizationUser orgUser, Organization organization - ) + ) { // Arrange organization.UsePolicies = true; @@ -422,14 +424,15 @@ public class PoliciesControllerTests decryptedToken.OrgUserId = organizationUserId; decryptedToken.OrgUserEmail = email; - var orgUserInviteTokenDataFactory = sutProvider.GetDependency>(); + var orgUserInviteTokenDataFactory = + sutProvider.GetDependency>(); orgUserInviteTokenDataFactory.TryUnprotect(token, out Arg.Any()) .Returns(x => - { - x[1] = decryptedToken; - return true; - }); + { + x[1] = decryptedToken; + return true; + }); orgUser.OrganizationId = orgId; sutProvider.GetDependency() @@ -463,7 +466,7 @@ public class PoliciesControllerTests [Theory] [BitAutoData] - public async Task PutVNext_WhenPolicyValidatorsRefactorEnabled_UsesVNextSavePolicyCommand( + public async Task PutVNext_UsesVNextSavePolicyCommand( SutProvider sutProvider, Guid orgId, SavePolicyRequest model, Policy policy, Guid userId) { @@ -478,10 +481,6 @@ public class PoliciesControllerTests .OrganizationOwner(orgId) .Returns(true); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) - .Returns(true); - sutProvider.GetDependency() .SaveAsync(Arg.Any()) .Returns(policy); @@ -492,12 +491,11 @@ public class PoliciesControllerTests // Assert await sutProvider.GetDependency() .Received(1) - .SaveAsync(Arg.Is( - m => m.PolicyUpdate.OrganizationId == orgId && - m.PolicyUpdate.Type == policy.Type && - m.PolicyUpdate.Enabled == model.Policy.Enabled && - m.PerformedBy.UserId == userId && - m.PerformedBy.IsOrganizationOwnerOrProvider == true)); + .SaveAsync(Arg.Is(m => m.PolicyUpdate.OrganizationId == orgId && + m.PolicyUpdate.Type == policy.Type && + m.PolicyUpdate.Enabled == model.Policy.Enabled && + m.PerformedBy.UserId == userId && + m.PerformedBy.IsOrganizationOwnerOrProvider == true)); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -507,51 +505,4 @@ public class PoliciesControllerTests Assert.Equal(policy.Id, result.Id); Assert.Equal(policy.Type, result.Type); } - - [Theory] - [BitAutoData] - public async Task PutVNext_WhenPolicyValidatorsRefactorDisabled_UsesSavePolicyCommand( - SutProvider sutProvider, Guid orgId, - SavePolicyRequest model, Policy policy, Guid userId) - { - // Arrange - policy.Data = null; - - sutProvider.GetDependency() - .UserId - .Returns(userId); - - sutProvider.GetDependency() - .OrganizationOwner(orgId) - .Returns(true); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) - .Returns(false); - - sutProvider.GetDependency() - .VNextSaveAsync(Arg.Any()) - .Returns(policy); - - // Act - var result = await sutProvider.Sut.PutVNext(orgId, policy.Type, model); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .VNextSaveAsync(Arg.Is( - m => m.PolicyUpdate.OrganizationId == orgId && - m.PolicyUpdate.Type == policy.Type && - m.PolicyUpdate.Enabled == model.Policy.Enabled && - m.PerformedBy.UserId == userId && - m.PerformedBy.IsOrganizationOwnerOrProvider == true)); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SaveAsync(default); - - Assert.NotNull(result); - Assert.Equal(policy.Id, result.Id); - Assert.Equal(policy.Type, result.Type); - } } diff --git a/test/Api.Test/SecretsManager/Controllers/SecretVersionsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/SecretVersionsControllerTests.cs new file mode 100644 index 0000000000..79a339fcba --- /dev/null +++ b/test/Api.Test/SecretsManager/Controllers/SecretVersionsControllerTests.cs @@ -0,0 +1,307 @@ +using Bit.Api.SecretsManager.Controllers; +using Bit.Api.SecretsManager.Models.Request; +using Bit.Core.Auth.Identity; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Entities; +using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.SecretsManager.AutoFixture.SecretsFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.SecretsManager.Controllers; + +[ControllerCustomize(typeof(SecretVersionsController))] +[SutProviderCustomize] +[SecretCustomize] +public class SecretVersionsControllerTests +{ + [Theory] + [BitAutoData] + public async Task GetVersionsBySecretId_SecretNotFound_Throws( + SutProvider sutProvider, + Guid secretId) + { + sutProvider.GetDependency().GetByIdAsync(secretId).Returns((Secret?)null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetVersionsBySecretIdAsync(secretId)); + } + + [Theory] + [BitAutoData] + public async Task GetVersionsBySecretId_NoAccess_Throws( + SutProvider sutProvider, + Secret secret) + { + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(false); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetVersionsBySecretIdAsync(secret.Id)); + } + + [Theory] + [BitAutoData] + public async Task GetVersionsBySecretId_NoReadAccess_Throws( + SutProvider sutProvider, + Secret secret, + Guid userId) + { + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(false); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((false, false)); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetVersionsBySecretIdAsync(secret.Id)); + } + + [Theory] + [BitAutoData] + public async Task GetVersionsBySecretId_Success( + SutProvider sutProvider, + Secret secret, + List versions, + Guid userId) + { + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(false); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, false)); + + foreach (var version in versions) + { + version.SecretId = secret.Id; + } + sutProvider.GetDependency().GetManyBySecretIdAsync(secret.Id).Returns(versions); + + var result = await sutProvider.Sut.GetVersionsBySecretIdAsync(secret.Id); + + Assert.Equal(versions.Count, result.Data.Count()); + await sutProvider.GetDependency().Received(1) + .GetManyBySecretIdAsync(Arg.Is(secret.Id)); + } + + [Theory] + [BitAutoData] + public async Task GetById_VersionNotFound_Throws( + SutProvider sutProvider, + Guid versionId) + { + sutProvider.GetDependency().GetByIdAsync(versionId).Returns((SecretVersion?)null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetByIdAsync(versionId)); + } + + [Theory] + [BitAutoData] + public async Task GetById_Success( + SutProvider sutProvider, + SecretVersion version, + Secret secret, + Guid userId) + { + version.SecretId = secret.Id; + sutProvider.GetDependency().GetByIdAsync(version.Id).Returns(version); + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(false); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, false)); + + var result = await sutProvider.Sut.GetByIdAsync(version.Id); + + Assert.Equal(version.Id, result.Id); + Assert.Equal(version.SecretId, result.SecretId); + } + + [Theory] + [BitAutoData] + public async Task RestoreVersion_NoWriteAccess_Throws( + SutProvider sutProvider, + Secret secret, + SecretVersion version, + RestoreSecretVersionRequestModel request, + Guid userId) + { + version.SecretId = secret.Id; + request.VersionId = version.Id; + + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(false); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, false)); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.RestoreVersionAsync(secret.Id, request)); + } + + [Theory] + [BitAutoData] + public async Task RestoreVersion_VersionNotFound_Throws( + SutProvider sutProvider, + Secret secret, + RestoreSecretVersionRequestModel request, + Guid userId) + { + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, true)); + sutProvider.GetDependency().GetByIdAsync(request.VersionId).Returns((SecretVersion?)null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.RestoreVersionAsync(secret.Id, request)); + } + + [Theory] + [BitAutoData] + public async Task RestoreVersion_VersionBelongsToDifferentSecret_Throws( + SutProvider sutProvider, + Secret secret, + SecretVersion version, + RestoreSecretVersionRequestModel request, + Guid userId) + { + version.SecretId = Guid.NewGuid(); // Different secret + request.VersionId = version.Id; + + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, true)); + sutProvider.GetDependency().GetByIdAsync(request.VersionId).Returns(version); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.RestoreVersionAsync(secret.Id, request)); + } + + [Theory] + [BitAutoData] + public async Task RestoreVersion_Success( + SutProvider sutProvider, + Secret secret, + SecretVersion version, + RestoreSecretVersionRequestModel request, + Guid userId, + OrganizationUser organizationUser) + { + version.SecretId = secret.Id; + request.VersionId = version.Id; + var versionValue = version.Value; + organizationUser.OrganizationId = secret.OrganizationId; + organizationUser.UserId = userId; + + sutProvider.GetDependency().GetByIdAsync(secret.Id).Returns(secret); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, true)); + sutProvider.GetDependency().GetByIdAsync(request.VersionId).Returns(version); + sutProvider.GetDependency() + .GetByOrganizationAsync(secret.OrganizationId, userId).Returns(organizationUser); + sutProvider.GetDependency().UpdateAsync(Arg.Any()).Returns(x => x.Arg()); + + var result = await sutProvider.Sut.RestoreVersionAsync(secret.Id, request); + + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Is(s => s.Value == versionValue)); + } + + [Theory] + [BitAutoData] + public async Task BulkDelete_EmptyIds_Throws( + SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => + sutProvider.Sut.BulkDeleteAsync(new List())); + } + + [Theory] + [BitAutoData] + public async Task BulkDelete_VersionNotFound_Throws( + SutProvider sutProvider, + List ids) + { + sutProvider.GetDependency().GetByIdAsync(ids[0]).Returns((SecretVersion?)null); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.BulkDeleteAsync(ids)); + } + + [Theory] + [BitAutoData] + public async Task BulkDelete_NoWriteAccess_Throws( + SutProvider sutProvider, + List versions, + Secret secret, + Guid userId) + { + var ids = versions.Select(v => v.Id).ToList(); + foreach (var version in versions) + { + version.SecretId = secret.Id; + sutProvider.GetDependency().GetByIdAsync(version.Id).Returns(version); + } + + sutProvider.GetDependency().GetManyByIds(Arg.Any>()) + .Returns(new List { secret }); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(false); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, false)); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.BulkDeleteAsync(ids)); + } + + [Theory] + [BitAutoData] + public async Task BulkDelete_Success( + SutProvider sutProvider, + List versions, + Secret secret, + Guid userId) + { + var ids = versions.Select(v => v.Id).ToList(); + foreach (var version in versions) + { + version.SecretId = secret.Id; + } + + sutProvider.GetDependency().GetManyByIdsAsync(ids).Returns(versions); + sutProvider.GetDependency().GetManyByIds(Arg.Any>()) + .Returns(new List { secret }); + sutProvider.GetDependency().AccessSecretsManager(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().IdentityClientType.Returns(IdentityClientType.ServiceAccount); + sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); + sutProvider.GetDependency().OrganizationAdmin(secret.OrganizationId).Returns(true); + sutProvider.GetDependency().AccessToSecretAsync(secret.Id, userId, default) + .ReturnsForAnyArgs((true, true)); + + await sutProvider.Sut.BulkDeleteAsync(ids); + + await sutProvider.GetDependency().Received(1) + .DeleteManyByIdAsync(Arg.Is>(x => x.SequenceEqual(ids))); + } +} diff --git a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs index 83a4229f39..51f61ad7c1 100644 --- a/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs +++ b/test/Api.Test/SecretsManager/Controllers/SecretsControllerTests.cs @@ -2,6 +2,7 @@ using Bit.Api.SecretsManager.Controllers; using Bit.Api.SecretsManager.Models.Request; using Bit.Api.Test.SecretsManager.Enums; +using Bit.Core.Auth.Identity; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -244,6 +245,7 @@ public class SecretsControllerTests { data = SetupSecretUpdateRequest(data); SetControllerUser(sutProvider, new Guid()); + sutProvider.GetDependency().IdentityClientType.Returns(IdentityClientType.ServiceAccount); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Any(), Arg.Any>()).ReturnsForAnyArgs(AuthorizationResult.Success()); @@ -602,6 +604,7 @@ public class SecretsControllerTests { data = SetupSecretUpdateRequest(data, true); + sutProvider.GetDependency().IdentityClientType.Returns(IdentityClientType.ServiceAccount); sutProvider.GetDependency() .AuthorizeAsync(Arg.Any(), Arg.Any(), Arg.Any>()).Returns(AuthorizationResult.Success()); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index 3f0443d31b..ef4c2c941e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.Context; @@ -183,17 +182,17 @@ public class VerifyOrganizationDomainCommandTests _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) - .SaveAsync(Arg.Is(x => x.Type == PolicyType.SingleOrg && - x.OrganizationId == domain.OrganizationId && - x.Enabled && + .SaveAsync(Arg.Is(x => x.PolicyUpdate.Type == PolicyType.SingleOrg && + x.PolicyUpdate.OrganizationId == domain.OrganizationId && + x.PolicyUpdate.Enabled && x.PerformedBy is StandardUser && x.PerformedBy.UserId == userId)); } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_WhenPolicyValidatorsRefactorFlagEnabled_UsesVNextSavePolicyCommand( + public async Task UserVerifyOrganizationDomainAsync_UsesVNextSavePolicyCommand( OrganizationDomain domain, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency() @@ -207,10 +206,6 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .UserId.Returns(userId); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) - .Returns(true); - _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); await sutProvider.GetDependency() @@ -240,9 +235,9 @@ public class VerifyOrganizationDomainCommandTests _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceive() - .SaveAsync(Arg.Any()); + .SaveAsync(Arg.Any()); } [Theory, BitAutoData] diff --git a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs index 7319df17aa..2f4d00a7fa 100644 --- a/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Auth/Services/SsoConfigServiceTests.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyUpdateEvents.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -14,7 +13,6 @@ using Bit.Core.Auth.Services; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -342,26 +340,26 @@ public class SsoConfigServiceTests await sutProvider.Sut.SaveAsync(ssoConfig, organization); - await sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SaveAsync( - Arg.Is(t => t.Type == PolicyType.SingleOrg && - t.OrganizationId == organization.Id && - t.Enabled) + Arg.Is(t => t.PolicyUpdate.Type == PolicyType.SingleOrg && + t.PolicyUpdate.OrganizationId == organization.Id && + t.PolicyUpdate.Enabled) ); - await sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SaveAsync( - Arg.Is(t => t.Type == PolicyType.ResetPassword && - t.GetDataModel().AutoEnrollEnabled && - t.OrganizationId == organization.Id && - t.Enabled) + Arg.Is(t => t.PolicyUpdate.Type == PolicyType.ResetPassword && + t.PolicyUpdate.GetDataModel().AutoEnrollEnabled && + t.PolicyUpdate.OrganizationId == organization.Id && + t.PolicyUpdate.Enabled) ); - await sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SaveAsync( - Arg.Is(t => t.Type == PolicyType.RequireSso && - t.OrganizationId == organization.Id && - t.Enabled) + Arg.Is(t => t.PolicyUpdate.Type == PolicyType.RequireSso && + t.PolicyUpdate.OrganizationId == organization.Id && + t.PolicyUpdate.Enabled) ); await sutProvider.GetDependency().ReceivedWithAnyArgs() @@ -369,7 +367,7 @@ public class SsoConfigServiceTests } [Theory, BitAutoData] - public async Task SaveAsync_Tde_WhenPolicyValidatorsRefactorEnabled_UsesVNextSavePolicyCommand( + public async Task SaveAsync_Tde_UsesVNextSavePolicyCommand( SutProvider sutProvider, Organization organization) { var ssoConfig = new SsoConfig @@ -383,10 +381,6 @@ public class SsoConfigServiceTests OrganizationId = organization.Id, }; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PolicyValidatorsRefactor) - .Returns(true); - await sutProvider.Sut.SaveAsync(ssoConfig, organization); await sutProvider.GetDependency() diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs index 920d3b0ad3..1ed2cac17a 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs @@ -520,10 +520,6 @@ public class IdentityServerSsoTests var keyConnectorUrl = AssertHelper.AssertJsonProperty(keyConnectorOption, "KeyConnectorUrl", JsonValueKind.String).GetString(); Assert.Equal("https://key_connector.com", keyConnectorUrl); - - // For backwards compatibility reasons the url should also be on the root - keyConnectorUrl = AssertHelper.AssertJsonProperty(root, "KeyConnectorUrl", JsonValueKind.String).GetString(); - Assert.Equal("https://key_connector.com", keyConnectorUrl); } private static async Task RunSuccessTestAsync(MemberDecryptionType memberDecryptionType)