Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -45,11 +46,13 @@ private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestMode
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyValidator;
private readonly IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>> _deviceValidator;
private readonly IKeyConnectorConfirmationDetailsQuery _keyConnectorConfirmationDetailsQuery;

public AccountsKeyManagementController(IUserService userService,
IFeatureService featureService,
IOrganizationUserRepository organizationUserRepository,
IEmergencyAccessRepository emergencyAccessRepository,
IKeyConnectorConfirmationDetailsQuery keyConnectorConfirmationDetailsQuery,
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand,
IRotateUserAccountKeysCommand rotateUserKeyCommandV2,
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
Expand All @@ -75,6 +78,7 @@ public AccountsKeyManagementController(IUserService userService,
_organizationUserValidator = organizationUserValidator;
_webauthnKeyValidator = webAuthnKeyValidator;
_deviceValidator = deviceValidator;
_keyConnectorConfirmationDetailsQuery = keyConnectorConfirmationDetailsQuery;
}

[HttpPost("key-management/regenerate-keys")]
Expand Down Expand Up @@ -178,4 +182,17 @@ public async Task PostConvertToKeyConnectorAsync()

throw new BadRequestException(ModelState);
}

[HttpGet("key-connector/confirmation-details/{orgSsoIdentifier}")]
public async Task<KeyConnectorConfirmationDetailsResponseModel> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ private static void AddKeyManagementCommands(this IServiceCollection services)
private static void AddKeyManagementQueries(this IServiceCollection services)
{
services.AddScoped<IUserAccountKeysQuery, UserAccountKeysQuery>();
services.AddScoped<IKeyConnectorConfirmationDetailsQuery, KeyConnectorConfirmationDetailsQuery>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
๏ปฟnamespace Bit.Core.KeyManagement.Models.Data;

public class KeyConnectorConfirmationDetails
{
public required string OrganizationName { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
๏ปฟusing Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.Queries.Interfaces;

public interface IKeyConnectorConfirmationDetailsQuery
{
public Task<KeyConnectorConfirmationDetails> Run(string orgSsoIdentifier, Guid userId);
}
Original file line number Diff line number Diff line change
@@ -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<KeyConnectorConfirmationDetails> 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, };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<KeyConnectorConfirmationDetailsResponseModel>();

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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,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;
Expand Down Expand Up @@ -362,4 +363,39 @@ public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_O
await sutProvider.GetDependency<IUserService>().Received(1)
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
}

[Theory]
[BitAutoData]
public async Task GetKeyConnectorConfirmationDetailsAsync_NoUser_Throws(
SutProvider<AccountsKeyManagementController> sutProvider, string orgSsoIdentifier)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.ReturnsNull();

await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
sutProvider.Sut.GetKeyConnectorConfirmationDetailsAsync(orgSsoIdentifier));

await sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().ReceivedWithAnyArgs(0)
.Run(Arg.Any<string>(), Arg.Any<Guid>());
}

[Theory]
[BitAutoData]
public async Task GetKeyConnectorConfirmationDetailsAsync_Success(
SutProvider<AccountsKeyManagementController> sutProvider, User expectedUser, string orgSsoIdentifier)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IKeyConnectorConfirmationDetailsQuery>().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<IKeyConnectorConfirmationDetailsQuery>().Received(1)
.Run(orgSsoIdentifier, expectedUser.Id);
}
}
Original file line number Diff line number Diff line change
@@ -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<KeyConnectorConfirmationDetailsQuery> sutProvider,
Guid userId, string orgSsoIdentifier)
{
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));

await sutProvider.GetDependency<IOrganizationUserRepository>()
.ReceivedWithAnyArgs(0)
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}

[Theory]
[BitAutoData]
public async Task Run_OrganizationNotKeyConnector_Throws(
SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,
Guid userId, string orgSsoIdentifier, Organization org)
{
org.Identifier = orgSsoIdentifier;
org.UseKeyConnector = false;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);

await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));

await sutProvider.GetDependency<IOrganizationUserRepository>()
.ReceivedWithAnyArgs(0)
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}

[Theory]
[BitAutoData]
public async Task Run_OrganizationUserNotFound_Throws(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider,
Guid userId, string orgSsoIdentifier
, Organization org)
{
org.Identifier = orgSsoIdentifier;
org.UseKeyConnector = true;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(Task.FromResult<OrganizationUser>(null));

await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Run(orgSsoIdentifier, userId));

await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetByOrganizationAsync(org.Id, userId);
}

[Theory]
[BitAutoData]
public async Task Run_Success(SutProvider<KeyConnectorConfirmationDetailsQuery> sutProvider, Guid userId,
string orgSsoIdentifier
, Organization org, OrganizationUser orgUser)
{
org.Identifier = orgSsoIdentifier;
org.UseKeyConnector = true;
orgUser.OrganizationId = org.Id;
orgUser.UserId = userId;

sutProvider.GetDependency<IOrganizationRepository>().GetByIdentifierAsync(orgSsoIdentifier).Returns(org);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(org.Id, userId)
.Returns(orgUser);

var result = await sutProvider.Sut.Run(orgSsoIdentifier, userId);

Assert.Equal(org.Name, result.OrganizationName);
await sutProvider.GetDependency<IOrganizationUserRepository>()
.Received(1)
.GetByOrganizationAsync(org.Id, userId);
}
}
Loading