diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs index 5feda856d5aa..b944cdd05289 100644 --- a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -1,8 +1,8 @@ -#nullable enable -using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.KeyManagement.Models.Requests; +using Bit.Api.KeyManagement.Models.Responses; using Bit.Api.KeyManagement.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; @@ -14,6 +14,7 @@ using Bit.Core.Exceptions; using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Services; @@ -45,11 +46,13 @@ private readonly IRotationValidator, IEnumerable> _webauthnKeyValidator; private readonly IRotationValidator, IEnumerable> _deviceValidator; + private readonly IKeyConnectorConfirmationDetailsQuery _keyConnectorConfirmationDetailsQuery; public AccountsKeyManagementController(IUserService userService, IFeatureService featureService, IOrganizationUserRepository organizationUserRepository, IEmergencyAccessRepository emergencyAccessRepository, + IKeyConnectorConfirmationDetailsQuery keyConnectorConfirmationDetailsQuery, IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand, IRotateUserAccountKeysCommand rotateUserKeyCommandV2, IRotationValidator, IEnumerable> cipherValidator, @@ -75,6 +78,7 @@ public AccountsKeyManagementController(IUserService userService, _organizationUserValidator = organizationUserValidator; _webauthnKeyValidator = webAuthnKeyValidator; _deviceValidator = deviceValidator; + _keyConnectorConfirmationDetailsQuery = keyConnectorConfirmationDetailsQuery; } [HttpPost("key-management/regenerate-keys")] @@ -178,4 +182,17 @@ public async Task PostConvertToKeyConnectorAsync() throw new BadRequestException(ModelState); } + + [HttpGet("key-connector/confirmation-details/{orgSsoIdentifier}")] + public async Task GetKeyConnectorConfirmationDetailsAsync(string orgSsoIdentifier) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var details = await _keyConnectorConfirmationDetailsQuery.Run(orgSsoIdentifier, user.Id); + return new KeyConnectorConfirmationDetailsResponseModel(details); + } } diff --git a/src/Api/KeyManagement/Models/Responses/KeyConnectorConfirmationDetailsResponseModel.cs b/src/Api/KeyManagement/Models/Responses/KeyConnectorConfirmationDetailsResponseModel.cs new file mode 100644 index 000000000000..68d2c689df1e --- /dev/null +++ b/src/Api/KeyManagement/Models/Responses/KeyConnectorConfirmationDetailsResponseModel.cs @@ -0,0 +1,24 @@ +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Models.Api; + +namespace Bit.Api.KeyManagement.Models.Responses; + +public class KeyConnectorConfirmationDetailsResponseModel : ResponseModel +{ + private const string _objectName = "keyConnectorConfirmationDetails"; + + public KeyConnectorConfirmationDetailsResponseModel(KeyConnectorConfirmationDetails details, + string obj = _objectName) : base(obj) + { + ArgumentNullException.ThrowIfNull(details); + + OrganizationName = details.OrganizationName; + } + + public KeyConnectorConfirmationDetailsResponseModel() : base(_objectName) + { + OrganizationName = string.Empty; + } + + public string OrganizationName { get; set; } +} diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs index 0e551c5d0e20..abaf9406bae4 100644 --- a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -26,5 +26,6 @@ private static void AddKeyManagementCommands(this IServiceCollection services) private static void AddKeyManagementQueries(this IServiceCollection services) { services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Core/KeyManagement/Models/Data/KeyConnectorConfirmationDetails.cs b/src/Core/KeyManagement/Models/Data/KeyConnectorConfirmationDetails.cs new file mode 100644 index 000000000000..3821831badfc --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/KeyConnectorConfirmationDetails.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.KeyManagement.Models.Data; + +public class KeyConnectorConfirmationDetails +{ + public required string OrganizationName { get; set; } +} diff --git a/src/Core/KeyManagement/Queries/Interfaces/IKeyConnectorConfirmationDetailsQuery.cs b/src/Core/KeyManagement/Queries/Interfaces/IKeyConnectorConfirmationDetailsQuery.cs new file mode 100644 index 000000000000..60b78c03f422 --- /dev/null +++ b/src/Core/KeyManagement/Queries/Interfaces/IKeyConnectorConfirmationDetailsQuery.cs @@ -0,0 +1,8 @@ +using Bit.Core.KeyManagement.Models.Data; + +namespace Bit.Core.KeyManagement.Queries.Interfaces; + +public interface IKeyConnectorConfirmationDetailsQuery +{ + public Task Run(string orgSsoIdentifier, Guid userId); +} diff --git a/src/Core/KeyManagement/Queries/KeyConnectorConfirmationDetailsQuery.cs b/src/Core/KeyManagement/Queries/KeyConnectorConfirmationDetailsQuery.cs new file mode 100644 index 000000000000..0c210e2fd1e2 --- /dev/null +++ b/src/Core/KeyManagement/Queries/KeyConnectorConfirmationDetailsQuery.cs @@ -0,0 +1,35 @@ +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.KeyManagement.Queries; + +public class KeyConnectorConfirmationDetailsQuery : IKeyConnectorConfirmationDetailsQuery +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public KeyConnectorConfirmationDetailsQuery(IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository) + { + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + } + + public async Task Run(string orgSsoIdentifier, Guid userId) + { + var org = await _organizationRepository.GetByIdentifierAsync(orgSsoIdentifier); + if (org is not { UseKeyConnector: true }) + { + throw new NotFoundException(); + } + + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(org.Id, userId); + if (orgUser == null) + { + throw new NotFoundException(); + } + + return new KeyConnectorConfirmationDetails { OrganizationName = org.Name, }; + } +} diff --git a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index 1630bc0dc0bf..1c456df10635 100644 --- a/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.IntegrationTest/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -3,9 +3,11 @@ using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Helpers; using Bit.Api.KeyManagement.Models.Requests; +using Bit.Api.KeyManagement.Models.Responses; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models; using Bit.Api.Vault.Models.Request; +using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.Accounts; @@ -286,20 +288,7 @@ public async Task PostSetKeyConnectorKeyAsync_NotLoggedIn_Unauthorized(SetKeyCon public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier, SetKeyConnectorKeyRequestModel request) { - var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, - PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10, - paymentMethod: PaymentMethodType.Card); - organization.UseKeyConnector = true; - organization.UseSso = true; - organization.Identifier = organizationSsoIdentifier; - await _organizationRepository.ReplaceAsync(organization); - - var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(ssoUserEmail); - await _loginHelper.LoginAsync(ssoUserEmail); - - await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail, - OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Invited); + var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier); var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail); Assert.NotNull(ssoUser); @@ -340,19 +329,7 @@ public async Task PostConvertToKeyConnectorAsync_NotLoggedIn_Unauthorized() [Fact] public async Task PostConvertToKeyConnectorAsync_Success() { - var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, - PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10, - paymentMethod: PaymentMethodType.Card); - organization.UseKeyConnector = true; - organization.UseSso = true; - await _organizationRepository.ReplaceAsync(organization); - - var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; - await _factory.LoginWithNewAccount(ssoUserEmail); - await _loginHelper.LoginAsync(ssoUserEmail); - - await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail, - OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted); + var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Accepted); var response = await _client.PostAsJsonAsync("/accounts/convert-to-key-connector", new { }); response.EnsureSuccessStatusCode(); @@ -556,4 +533,41 @@ public async Task RotateUpgradeToV2UserAccountKeysAsync_Success(RotateUserAccoun Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory); Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism); } + + [Fact] + public async Task GetKeyConnectorConfirmationDetailsAsync_Success() + { + var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited); + + await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail, + OrganizationUserType.User, userStatusType: OrganizationUserStatusType.Accepted); + + var response = await _client.GetAsync($"/accounts/key-connector/confirmation-details/{organization.Identifier}"); + response.EnsureSuccessStatusCode(); + var result = await response.Content.ReadFromJsonAsync(); + + Assert.NotNull(result); + Assert.Equal(organization.Name, result.OrganizationName); + } + + private async Task<(string, Organization)> SetupKeyConnectorTestAsync(OrganizationUserStatusType userStatusType, + string organizationSsoIdentifier = "test-sso-identifier") + { + var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, + PlanType.EnterpriseAnnually, _ownerEmail, passwordManagerSeats: 10, + paymentMethod: PaymentMethodType.Card); + organization.UseKeyConnector = true; + organization.UseSso = true; + organization.Identifier = organizationSsoIdentifier; + await _organizationRepository.ReplaceAsync(organization); + + var ssoUserEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com"; + await _factory.LoginWithNewAccount(ssoUserEmail); + await _loginHelper.LoginAsync(ssoUserEmail); + + await OrganizationTestHelpers.CreateUserAsync(_factory, organization.Id, ssoUserEmail, + OrganizationUserType.User, userStatusType: userStatusType); + + return (ssoUserEmail, organization); + } } diff --git a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs index b0afcd914423..a1f3088f527b 100644 --- a/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs +++ b/test/Api.Test/KeyManagement/Controllers/AccountsKeyManagementControllerTests.cs @@ -16,6 +16,7 @@ using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Models.Api.Request; using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Services; @@ -363,4 +364,39 @@ public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_O await sutProvider.GetDependency().Received(1) .ConvertToKeyConnectorAsync(Arg.Is(expectedUser)); } + + [Theory] + [BitAutoData] + public async Task GetKeyConnectorConfirmationDetailsAsync_NoUser_Throws( + SutProvider sutProvider, string orgSsoIdentifier) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()) + .ReturnsNull(); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetKeyConnectorConfirmationDetailsAsync(orgSsoIdentifier)); + + await sutProvider.GetDependency().ReceivedWithAnyArgs(0) + .Run(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetKeyConnectorConfirmationDetailsAsync_Success( + SutProvider sutProvider, User expectedUser, string orgSsoIdentifier) + { + sutProvider.GetDependency().GetUserByPrincipalAsync(Arg.Any()) + .Returns(expectedUser); + sutProvider.GetDependency().Run(orgSsoIdentifier, expectedUser.Id) + .Returns( + new KeyConnectorConfirmationDetails { OrganizationName = "test" } + ); + + var result = await sutProvider.Sut.GetKeyConnectorConfirmationDetailsAsync(orgSsoIdentifier); + + Assert.NotNull(result); + Assert.Equal("test", result.OrganizationName); + await sutProvider.GetDependency().Received(1) + .Run(orgSsoIdentifier, expectedUser.Id); + } } diff --git a/test/Core.Test/KeyManagement/Queries/KeyConnectorConfirmationDetailsQueryTests.cs b/test/Core.Test/KeyManagement/Queries/KeyConnectorConfirmationDetailsQueryTests.cs new file mode 100644 index 000000000000..612d63f2894b --- /dev/null +++ b/test/Core.Test/KeyManagement/Queries/KeyConnectorConfirmationDetailsQueryTests.cs @@ -0,0 +1,86 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Queries; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.KeyManagement.Queries; + +[SutProviderCustomize] +public class KeyConnectorConfirmationDetailsQueryTests +{ + [Theory] + [BitAutoData] + public async Task Run_OrganizationNotFound_Throws(SutProvider sutProvider, + Guid userId, string orgSsoIdentifier) + { + await Assert.ThrowsAsync(() => sutProvider.Sut.Run(orgSsoIdentifier, userId)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .GetByOrganizationAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task Run_OrganizationNotKeyConnector_Throws( + SutProvider sutProvider, + Guid userId, string orgSsoIdentifier, Organization org) + { + org.Identifier = orgSsoIdentifier; + org.UseKeyConnector = false; + sutProvider.GetDependency().GetByIdentifierAsync(orgSsoIdentifier).Returns(org); + + await Assert.ThrowsAsync(() => sutProvider.Sut.Run(orgSsoIdentifier, userId)); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(0) + .GetByOrganizationAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task Run_OrganizationUserNotFound_Throws(SutProvider sutProvider, + Guid userId, string orgSsoIdentifier + , Organization org) + { + org.Identifier = orgSsoIdentifier; + org.UseKeyConnector = true; + sutProvider.GetDependency().GetByIdentifierAsync(orgSsoIdentifier).Returns(org); + sutProvider.GetDependency() + .GetByOrganizationAsync(Arg.Any(), Arg.Any()).Returns(Task.FromResult(null)); + + await Assert.ThrowsAsync(() => sutProvider.Sut.Run(orgSsoIdentifier, userId)); + + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationAsync(org.Id, userId); + } + + [Theory] + [BitAutoData] + public async Task Run_Success(SutProvider sutProvider, Guid userId, + string orgSsoIdentifier + , Organization org, OrganizationUser orgUser) + { + org.Identifier = orgSsoIdentifier; + org.UseKeyConnector = true; + orgUser.OrganizationId = org.Id; + orgUser.UserId = userId; + + sutProvider.GetDependency().GetByIdentifierAsync(orgSsoIdentifier).Returns(org); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, userId) + .Returns(orgUser); + + var result = await sutProvider.Sut.Run(orgSsoIdentifier, userId); + + Assert.Equal(org.Name, result.OrganizationName); + await sutProvider.GetDependency() + .Received(1) + .GetByOrganizationAsync(org.Id, userId); + } +}