From 74ab7e86725ae1745934119be083f3d5c5819fa0 Mon Sep 17 00:00:00 2001 From: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> Date: Thu, 29 Jun 2023 11:42:44 -0500 Subject: [PATCH] [SM-771] Add new endpoint for bulk enabling users for Secrets Manager (#3020) * Add new endpoint for bulk enabling users for sm * Review updates --- .../OrganizationUsersController.cs | 27 +++++++ ...OrganizationServiceCollectionExtensions.cs | 3 + .../EnableAccessSecretsManagerCommand.cs | 43 ++++++++++ .../IEnableAccessSecretsManagerCommand.cs | 9 +++ .../EnableAccessSecretsManagerCommandTests.cs | 81 +++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 src/Core/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommand.cs create mode 100644 src/Core/SecretsManager/Commands/EnableAccessSecretsManager/Interfaces/IEnableAccessSecretsManagerCommand.cs create mode 100644 test/Core.Test/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommandTests.cs diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index 65d7b262d9..baaf9de6d5 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -9,6 +9,7 @@ using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -19,6 +20,7 @@ namespace Bit.Api.Controllers; [Authorize("Application")] public class OrganizationUsersController : Controller { + private readonly IEnableAccessSecretsManagerCommand _enableAccessSecretsManagerCommand; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationService _organizationService; @@ -29,6 +31,7 @@ public class OrganizationUsersController : Controller private readonly ICurrentContext _currentContext; public OrganizationUsersController( + IEnableAccessSecretsManagerCommand enableAccessSecretsManagerCommand, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, IOrganizationService organizationService, @@ -38,6 +41,7 @@ public class OrganizationUsersController : Controller IPolicyRepository policyRepository, ICurrentContext currentContext) { + _enableAccessSecretsManagerCommand = enableAccessSecretsManagerCommand; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _organizationService = organizationService; @@ -420,6 +424,29 @@ public class OrganizationUsersController : Controller return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _organizationService.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService)); } + [HttpPatch("enable-secrets-manager")] + [HttpPut("enable-secrets-manager")] + public async Task> BulkEnableSecretsManagerAsync(Guid orgId, + [FromBody] OrganizationUserBulkRequestModel model) + { + if (!await _currentContext.ManageUsers(orgId)) + { + throw new NotFoundException(); + } + + var orgUsers = (await _organizationUserRepository.GetManyAsync(model.Ids)) + .Where(ou => ou.OrganizationId == orgId).ToList(); + if (orgUsers.Count == 0) + { + throw new BadRequestException("Users invalid."); + } + + var results = await _enableAccessSecretsManagerCommand.EnableUsersAsync(orgUsers); + + return new ListResponseModel(results.Select(r => + new OrganizationUserBulkResponseModel(r.organizationUser.Id, r.error))); + } + private async Task RestoreOrRevokeUserAsync( Guid orgId, Guid id, diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 983fa3b352..9e36537797 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -15,6 +15,8 @@ using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterpri using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted; +using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager; +using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tokens; @@ -29,6 +31,7 @@ public static class OrganizationServiceCollectionExtensions public static void AddOrganizationServices(this IServiceCollection services, IGlobalSettings globalSettings) { services.AddScoped(); + services.AddScoped(); services.AddTokenizers(); services.AddOrganizationGroupCommands(); services.AddOrganizationConnectionCommands(); diff --git a/src/Core/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommand.cs b/src/Core/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommand.cs new file mode 100644 index 0000000000..fb172fbd16 --- /dev/null +++ b/src/Core/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommand.cs @@ -0,0 +1,43 @@ +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces; + +namespace Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager; + +public class EnableAccessSecretsManagerCommand : IEnableAccessSecretsManagerCommand +{ + private readonly IOrganizationUserRepository _organizationUserRepository; + + public EnableAccessSecretsManagerCommand(IOrganizationUserRepository organizationUserRepository) + { + _organizationUserRepository = organizationUserRepository; + } + + public async Task> EnableUsersAsync( + IEnumerable organizationUsers) + { + var results = new List<(OrganizationUser organizationUser, string error)>(); + var usersToEnable = new List(); + + foreach (var orgUser in organizationUsers) + { + if (orgUser.AccessSecretsManager) + { + results.Add((orgUser, "User already has access to Secrets Manager")); + } + else + { + orgUser.AccessSecretsManager = true; + usersToEnable.Add(orgUser); + results.Add((orgUser, "")); + } + } + + if (usersToEnable.Any()) + { + await _organizationUserRepository.ReplaceManyAsync(usersToEnable); + } + + return results; + } +} diff --git a/src/Core/SecretsManager/Commands/EnableAccessSecretsManager/Interfaces/IEnableAccessSecretsManagerCommand.cs b/src/Core/SecretsManager/Commands/EnableAccessSecretsManager/Interfaces/IEnableAccessSecretsManagerCommand.cs new file mode 100644 index 0000000000..b7fa2150ce --- /dev/null +++ b/src/Core/SecretsManager/Commands/EnableAccessSecretsManager/Interfaces/IEnableAccessSecretsManagerCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Entities; + +namespace Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager.Interfaces; + +public interface IEnableAccessSecretsManagerCommand +{ + Task> EnableUsersAsync( + IEnumerable organizationUsers); +} diff --git a/test/Core.Test/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommandTests.cs b/test/Core.Test/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommandTests.cs new file mode 100644 index 0000000000..ae783bc547 --- /dev/null +++ b/test/Core.Test/SecretsManager/Commands/EnableAccessSecretsManager/EnableAccessSecretsManagerCommandTests.cs @@ -0,0 +1,81 @@ +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Commands.EnableAccessSecretsManager; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.SecretsManager.Commands.EnableAccessSecretsManager; + +[SutProviderCustomize] +public class EnableAccessSecretsManagerCommandTests +{ + [Theory] + [BitAutoData] + public async Task EnableUsers_UsersAlreadyEnabled_DoesNotCallRepository( + SutProvider sutProvider, ICollection data) + { + foreach (var item in data) + { + item.AccessSecretsManager = true; + } + + var result = await sutProvider.Sut.EnableUsersAsync(data); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .ReplaceManyAsync(default); + + Assert.Equal(data.Count, result.Count); + Assert.Equal(data.Count, + result.Where(x => x.error == "User already has access to Secrets Manager").ToList().Count); + } + + [Theory] + [BitAutoData] + public async Task EnableUsers_OneUserNotEnabled_CallsRepositoryForOne( + SutProvider sutProvider, ICollection data) + { + var firstUser = new List(); + foreach (var item in data) + { + if (item == data.First()) + { + item.AccessSecretsManager = false; + firstUser.Add(item); + } + else + { + item.AccessSecretsManager = true; + } + } + + var result = await sutProvider.Sut.EnableUsersAsync(data); + + await sutProvider.GetDependency().Received(1) + .ReplaceManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(firstUser))); + + Assert.Equal(data.Count, result.Count); + Assert.Equal(data.Count - 1, + result.Where(x => x.error == "User already has access to Secrets Manager").ToList().Count); + } + + [Theory] + [BitAutoData] + public async Task EnableUsers_Success( + SutProvider sutProvider, ICollection data) + { + foreach (var item in data) + { + item.AccessSecretsManager = false; + } + + var result = await sutProvider.Sut.EnableUsersAsync(data); + + await sutProvider.GetDependency().Received(1) + .ReplaceManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data))); + + Assert.Equal(data.Count, result.Count); + } +}