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
Expand Up @@ -160,7 +160,7 @@ public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyReque
{
// V1 account registration
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key!, model.OrgIdentifier);
if (result.Succeeded)
{
return;
Expand All @@ -184,7 +184,35 @@ public async Task PostConvertToKeyConnectorAsync()
throw new UnauthorizedAccessException();
}

var result = await _userService.ConvertToKeyConnectorAsync(user);
var result = await _userService.ConvertToKeyConnectorAsync(user, null);
if (result.Succeeded)
{
return;
}

foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}

throw new BadRequestException(ModelState);
}

[HttpPost("key-connector/enroll")]
public async Task PostEnrollToKeyConnectorAsync([FromBody] KeyConnectorEnrollmentRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}

if (string.IsNullOrWhiteSpace(model.KeyConnectorKeyWrappedUserKey))
{
throw new BadRequestException("KeyConnectorKeyWrappedUserKey must be supplied when request body is provided.");
}

var result = await _userService.ConvertToKeyConnectorAsync(user, model.KeyConnectorKeyWrappedUserKey);
if (result.Succeeded)
{
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
ο»Ώusing Bit.Core.Utilities;

namespace Bit.Api.KeyManagement.Models.Requests;

public class KeyConnectorEnrollmentRequestModel
{
[EncryptedString]
public required string KeyConnectorKeyWrappedUserKey { get; set; }
}
2 changes: 1 addition & 1 deletion src/Core/Services/IUserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Task<IdentityResult> ChangeEmailAsync(User user, string masterPassword, string n
// TODO removed with https://bitwarden.atlassian.net/browse/PM-27328
[Obsolete("Use ISetKeyConnectorKeyCommand instead. This method will be removed in a future version.")]
Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier);
Task<IdentityResult> ConvertToKeyConnectorAsync(User user);
Task<IdentityResult> ConvertToKeyConnectorAsync(User user, string keyConnectorKeyWrappedUserKey);
Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Expand Down
7 changes: 6 additions & 1 deletion src/Core/Services/Implementations/UserService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@
return IdentityResult.Success;
}

public async Task<IdentityResult> ConvertToKeyConnectorAsync(User user)
public async Task<IdentityResult> ConvertToKeyConnectorAsync(User user, string keyConnectorKeyWrappedUserKey = null)
{
var identityResult = CheckCanUseKeyConnector(user);
if (identityResult != null)
Expand All @@ -534,6 +534,11 @@
user.MasterPassword = null;
user.UsesKeyConnector = true;

if (!string.IsNullOrWhiteSpace(keyConnectorKeyWrappedUserKey))
{
user.Key = keyConnectorKeyWrappedUserKey;
}

await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);

Expand Down Expand Up @@ -722,7 +727,7 @@
{
if (!CoreHelpers.FixedTimeEquals(
user.TwoFactorRecoveryCode,
recoveryCode.Replace(" ", string.Empty).Trim().ToLower()))

Check warning on line 730 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build MSSQL migrator utility (win-x64)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 730 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Sso, ./bitwarden_license/src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 730 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Billing, ./src, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)

Check warning on line 730 in src/Core/Services/Implementations/UserService.cs

View workflow job for this annotation

GitHub Actions / Build Docker images (Admin, ./src, true, true)

The behavior of 'string.ToLower()' could vary based on the current user's locale settings. Replace this call in 'UserService.RecoverTwoFactorAsync(User, string)' with a call to 'string.ToLower(CultureInfo)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1304)
{
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,60 @@ public async Task PostConvertToKeyConnectorAsync_Success()
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
}

[Fact]
public async Task PostEnrollToKeyConnectorAsync_NotLoggedIn_Unauthorized()
{
var request = new KeyConnectorEnrollmentRequestModel
{
KeyConnectorKeyWrappedUserKey = _mockEncryptedString
};

var response = await _client.PostAsJsonAsync("/accounts/key-connector/enroll", request);

Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}

[Fact]
public async Task PostEnrollToKeyConnectorAsync_KeyConnectorKeyWrappedUserKeyMissing_BadRequest()
{
var (ssoUserEmail, _) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Accepted);

var request = new KeyConnectorEnrollmentRequestModel
{
KeyConnectorKeyWrappedUserKey = " "
};

var response = await _client.PostAsJsonAsync("/accounts/key-connector/enroll", request);

Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);

var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(user);
Assert.False(user.UsesKeyConnector);
}

[Fact]
public async Task PostEnrollToKeyConnectorAsync_Success()
{
var (ssoUserEmail, _) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Accepted);

var request = new KeyConnectorEnrollmentRequestModel
{
KeyConnectorKeyWrappedUserKey = _mockEncryptedString
};

var response = await _client.PostAsJsonAsync("/accounts/key-connector/enroll", request);
response.EnsureSuccessStatusCode();

var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(user);
Assert.Null(user.MasterPassword);
Assert.True(user.UsesKeyConnector);
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, user.Key);
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
}

[Theory]
[BitAutoData]
public async Task RotateV2UserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());

await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
.ConvertToKeyConnectorAsync(Arg.Any<User>());
.ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string?>());
}

[Theory]
Expand All @@ -502,7 +502,7 @@ public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorFails_Thro
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.ConvertToKeyConnectorAsync(Arg.Any<User>())
.ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string?>())
.Returns(IdentityResult.Failed(new IdentityError { Description = "convert to key connector error" }));

var badRequestException =
Expand All @@ -511,7 +511,7 @@ public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorFails_Thro
Assert.Equal(1, badRequestException.ModelState!.ErrorCount);
Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
await sutProvider.GetDependency<IUserService>().Received(1)
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Any<string?>());
}

[Theory]
Expand All @@ -523,13 +523,90 @@ public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_O
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.ConvertToKeyConnectorAsync(Arg.Any<User>())
.ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string?>())
.Returns(IdentityResult.Success);

await sutProvider.Sut.PostConvertToKeyConnectorAsync();

await sutProvider.GetDependency<IUserService>().Received(1)
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Any<string?>());
}

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

await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostEnrollToKeyConnectorAsync(data));

await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
.ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string>());
}

[Theory]
[BitAutoData]
public async Task PostEnrollToKeyConnectorAsync_KeyConnectorKeyWrappedUserKeyMissing_ThrowsBadRequest(
SutProvider<AccountsKeyManagementController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(new User());

var request = new KeyConnectorEnrollmentRequestModel
{
KeyConnectorKeyWrappedUserKey = " "
};

var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PostEnrollToKeyConnectorAsync(request));

Assert.Equal("KeyConnectorKeyWrappedUserKey must be supplied when request body is provided.",
exception.Message);
await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
.ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string>());
}

[Theory]
[BitAutoData]
public async Task PostEnrollToKeyConnectorAsync_ConvertToKeyConnectorFails_ThrowsBadRequestWithErrorResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser,
KeyConnectorEnrollmentRequestModel data)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(IdentityResult.Failed(new IdentityError { Description = "convert to key connector error" }));

var badRequestException =
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostEnrollToKeyConnectorAsync(data));

Assert.Equal(1, badRequestException.ModelState!.ErrorCount);
Assert.Equal("convert to key connector error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
await sutProvider.GetDependency<IUserService>().Received(1)
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Is(data.KeyConnectorKeyWrappedUserKey));
}

[Theory]
[BitAutoData]
public async Task PostEnrollToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_OkResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser,
KeyConnectorEnrollmentRequestModel data)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.ConvertToKeyConnectorAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(IdentityResult.Success);

await sutProvider.Sut.PostEnrollToKeyConnectorAsync(data);

await sutProvider.GetDependency<IUserService>().Received(1)
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser), Arg.Is(data.KeyConnectorKeyWrappedUserKey));
}

[Theory]
Expand Down
Loading