From 34a3d4a4df4bf8ebc293d8866a9ae02eb5b59ec6 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:05:04 -0400 Subject: [PATCH] [AC-1593] Auto-Grant SM access to org owner when they add SM (#3349) * Auto grant SM access to org owner * Thomas' feedback --- .../Controllers/OrganizationsController.cs | 26 ++- .../OrganizationsControllerTests.cs | 196 ++++++++++++++++++ 2 files changed, 220 insertions(+), 2 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index a4d8a330b..632230ebc 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -320,8 +320,16 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - var result = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); - return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 }; + var (success, paymentIntentClientSecret) = await _upgradeOrganizationPlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); + + if (model.UseSecretsManager && success) + { + var userId = _userService.GetProperUserId(User).Value; + + await TryGrantOwnerAccessToSecretsManagerAsync(orgIdGuid, userId); + } + + return new PaymentResponseModel { Success = success, PaymentIntentClientSecret = paymentIntentClientSecret }; } [HttpPost("{id}/subscription")] @@ -374,6 +382,9 @@ public class OrganizationsController : Controller model.AdditionalServiceAccounts); var userId = _userService.GetProperUserId(User).Value; + + await TryGrantOwnerAccessToSecretsManagerAsync(organization.Id, userId); + var organizationDetails = await _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed); @@ -786,4 +797,15 @@ public class OrganizationsController : Controller await _organizationService.UpdateAsync(model.ToOrganization(organization)); return new OrganizationResponseModel(organization); } + + private async Task TryGrantOwnerAccessToSecretsManagerAsync(Guid organizationId, Guid userId) + { + var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId); + + if (organizationUser != null) + { + organizationUser.AccessSecretsManager = true; + await _organizationUserRepository.ReplaceAsync(organizationUser); + } + } } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 0edc80080..7a3f4437c 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -1,6 +1,8 @@ using System.Security.Claims; using AutoFixture.Xunit2; using Bit.Api.AdminConsole.Controllers; +using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Models.Request.Organizations; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; @@ -10,13 +12,17 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Context; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Api.Test.AdminConsole.Controllers; @@ -145,4 +151,194 @@ public class OrganizationsControllerTests : IDisposable await _organizationService.DeleteUserAsync(orgId, user.Id); await _organizationService.Received(1).DeleteUserAsync(orgId, user.Id); } + + [Theory, AutoData] + public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException( + Guid organizationId, + OrganizationUpgradeRequestModel model) + { + _currentContext.EditSubscription(organizationId).Returns(false); + + await Assert.ThrowsAsync(() => _sut.PostUpgrade(organizationId.ToString(), model)); + } + + [Theory, AutoData] + public async Task OrganizationsController_PostUpgrade_NonSMUpgrade_ReturnsCorrectResponse( + Guid organizationId, + OrganizationUpgradeRequestModel model, + bool success, + string paymentIntentClientSecret) + { + model.UseSecretsManager = false; + + _currentContext.EditSubscription(organizationId).Returns(true); + + _upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any()) + .Returns(new Tuple(success, paymentIntentClientSecret)); + + var response = await _sut.PostUpgrade(organizationId.ToString(), model); + + Assert.Equal(success, response.Success); + Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret); + } + + [Theory, AutoData] + public async Task OrganizationsController_PostUpgrade_SMUpgrade_ProvidesAccess_ReturnsCorrectResponse( + Guid organizationId, + Guid userId, + OrganizationUpgradeRequestModel model, + bool success, + string paymentIntentClientSecret, + OrganizationUser organizationUser) + { + model.UseSecretsManager = true; + organizationUser.AccessSecretsManager = false; + + _currentContext.EditSubscription(organizationId).Returns(true); + + _upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any()) + .Returns(new Tuple(success, paymentIntentClientSecret)); + + _userService.GetProperUserId(Arg.Any()).Returns(userId); + + _organizationUserRepository.GetByOrganizationAsync(organizationId, userId).Returns(organizationUser); + + var response = await _sut.PostUpgrade(organizationId.ToString(), model); + + Assert.Equal(success, response.Success); + Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret); + + await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is(orgUser => + orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true)); + } + + [Theory, AutoData] + public async Task OrganizationsController_PostUpgrade_SMUpgrade_NullOrgUser_ReturnsCorrectResponse( + Guid organizationId, + Guid userId, + OrganizationUpgradeRequestModel model, + bool success, + string paymentIntentClientSecret) + { + model.UseSecretsManager = true; + + _currentContext.EditSubscription(organizationId).Returns(true); + + _upgradeOrganizationPlanCommand.UpgradePlanAsync(organizationId, Arg.Any()) + .Returns(new Tuple(success, paymentIntentClientSecret)); + + _userService.GetProperUserId(Arg.Any()).Returns(userId); + + _organizationUserRepository.GetByOrganizationAsync(organizationId, userId).ReturnsNull(); + + var response = await _sut.PostUpgrade(organizationId.ToString(), model); + + Assert.Equal(success, response.Success); + Assert.Equal(paymentIntentClientSecret, response.PaymentIntentClientSecret); + + await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any()); + } + + [Theory, AutoData] + public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrg_ThrowsNotFoundException( + Guid organizationId, + SecretsManagerSubscribeRequestModel model) + { + _organizationRepository.GetByIdAsync(organizationId).ReturnsNull(); + + await Assert.ThrowsAsync(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model)); + } + + [Theory, AutoData] + public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_UserCannotEditSubscription_ThrowsNotFoundException( + Guid organizationId, + SecretsManagerSubscribeRequestModel model, + Organization organization) + { + _organizationRepository.GetByIdAsync(organizationId).Returns(organization); + + _currentContext.EditSubscription(organizationId).Returns(false); + + await Assert.ThrowsAsync(() => _sut.PostSubscribeSecretsManagerAsync(organizationId, model)); + } + + [Theory, AutoData] + public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_ProvidesAccess_ReturnsCorrectResponse( + Guid organizationId, + SecretsManagerSubscribeRequestModel model, + Organization organization, + Guid userId, + OrganizationUser organizationUser, + OrganizationUserOrganizationDetails organizationUserOrganizationDetails) + { + organizationUser.AccessSecretsManager = false; + + var ssoConfigurationData = new SsoConfigurationData + { + MemberDecryptionType = MemberDecryptionType.KeyConnector, + KeyConnectorUrl = "https://example.com" + }; + + organizationUserOrganizationDetails.Permissions = string.Empty; + organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize(); + + _organizationRepository.GetByIdAsync(organizationId).Returns(organization); + + _currentContext.EditSubscription(organizationId).Returns(true); + + _userService.GetProperUserId(Arg.Any()).Returns(userId); + + _organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).Returns(organizationUser); + + _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed) + .Returns(organizationUserOrganizationDetails); + + var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model); + + Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId); + Assert.Equal(response.Name, organizationUserOrganizationDetails.Name); + + await _addSecretsManagerSubscriptionCommand.Received(1) + .SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts); + await _organizationUserRepository.Received(1).ReplaceAsync(Arg.Is(orgUser => + orgUser.Id == organizationUser.Id && orgUser.AccessSecretsManager == true)); + } + + [Theory, AutoData] + public async Task OrganizationsController_PostSubscribeSecretsManagerAsync_NullOrgUser_ReturnsCorrectResponse( + Guid organizationId, + SecretsManagerSubscribeRequestModel model, + Organization organization, + Guid userId, + OrganizationUserOrganizationDetails organizationUserOrganizationDetails) + { + var ssoConfigurationData = new SsoConfigurationData + { + MemberDecryptionType = MemberDecryptionType.KeyConnector, + KeyConnectorUrl = "https://example.com" + }; + + organizationUserOrganizationDetails.Permissions = string.Empty; + organizationUserOrganizationDetails.SsoConfig = ssoConfigurationData.Serialize(); + + _organizationRepository.GetByIdAsync(organizationId).Returns(organization); + + _currentContext.EditSubscription(organizationId).Returns(true); + + _userService.GetProperUserId(Arg.Any()).Returns(userId); + + _organizationUserRepository.GetByOrganizationAsync(organization.Id, userId).ReturnsNull(); + + _organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id, OrganizationUserStatusType.Confirmed) + .Returns(organizationUserOrganizationDetails); + + var response = await _sut.PostSubscribeSecretsManagerAsync(organizationId, model); + + Assert.Equal(response.Id, organizationUserOrganizationDetails.OrganizationId); + Assert.Equal(response.Name, organizationUserOrganizationDetails.Name); + + await _addSecretsManagerSubscriptionCommand.Received(1) + .SignUpAsync(organization, model.AdditionalSmSeats, model.AdditionalServiceAccounts); + await _organizationUserRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any()); + } }