1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

Move Key Connector controller endpoints into Key Management team ownership

This commit is contained in:
Maciej Zieniuk 2025-03-27 12:39:58 +00:00
parent d4b0058372
commit aba05f7970
No known key found for this signature in database
GPG Key ID: 9CACE59F1272ACD9
6 changed files with 282 additions and 53 deletions

View File

@ -284,52 +284,6 @@ public class AccountsController : Controller
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }
[HttpPost("set-key-connector-key")]
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("convert-to-key-connector")]
public async Task PostConvertToKeyConnector()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ConvertToKeyConnectorAsync(user);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("kdf")] [HttpPost("kdf")]
public async Task PostKdf([FromBody] KdfRequestModel model) public async Task PostKdf([FromBody] KdfRequestModel model)
{ {

View File

@ -23,7 +23,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.KeyManagement.Controllers; namespace Bit.Api.KeyManagement.Controllers;
[Route("accounts/key-management")] [Route("accounts")]
[Authorize("Application")] [Authorize("Application")]
public class AccountsKeyManagementController : Controller public class AccountsKeyManagementController : Controller
{ {
@ -73,7 +73,7 @@ public class AccountsKeyManagementController : Controller
_webauthnKeyValidator = webAuthnKeyValidator; _webauthnKeyValidator = webAuthnKeyValidator;
} }
[HttpPost("regenerate-keys")] [HttpPost("key-management/regenerate-keys")]
public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request) public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel request)
{ {
if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration)) if (!_featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration))
@ -89,7 +89,7 @@ public class AccountsKeyManagementController : Controller
} }
[HttpPost("rotate-user-account-keys")] [HttpPost("key-management/rotate-user-account-keys")]
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model) public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
@ -128,4 +128,54 @@ public class AccountsKeyManagementController : Controller
throw new BadRequestException(ModelState); throw new BadRequestException(ModelState);
} }
[HttpPost("key-management/set-key-connector-key")]
// Backwards compatibility, to be deleted in the future
[HttpPost("set-key-connector-key")]
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
[HttpPost("key-management/convert-to-key-connector")]
// Backwards compatibility, to be deleted in the future
[HttpPost("convert-to-key-connector")]
public async Task PostConvertToKeyConnectorAsync()
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var result = await _userService.ConvertToKeyConnectorAsync(user);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new BadRequestException(ModelState);
}
} }

View File

@ -3,7 +3,7 @@ using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
namespace Bit.Api.Auth.Models.Request.Accounts; namespace Bit.Api.KeyManagement.Models.Requests;
public class SetKeyConnectorKeyRequestModel public class SetKeyConnectorKeyRequestModel
{ {

View File

@ -59,7 +59,8 @@ public static class OrganizationTestHelpers
string userEmail, string userEmail,
OrganizationUserType type, OrganizationUserType type,
bool accessSecretsManager = false, bool accessSecretsManager = false,
Permissions? permissions = null Permissions? permissions = null,
OrganizationUserStatusType userStatusType = OrganizationUserStatusType.Confirmed
) where T : class ) where T : class
{ {
var userRepository = factory.GetService<IUserRepository>(); var userRepository = factory.GetService<IUserRepository>();
@ -74,7 +75,7 @@ public static class OrganizationTestHelpers
UserId = user.Id, UserId = user.Id,
Key = null, Key = null,
Type = type, Type = type,
Status = OrganizationUserStatusType.Confirmed, Status = userStatusType,
ExternalId = null, ExternalId = null,
AccessSecretsManager = accessSecretsManager, AccessSecretsManager = accessSecretsManager,
}; };

View File

@ -1,4 +1,5 @@
using System.Net; #nullable enable
using System.Net;
using Bit.Api.IntegrationTest.Factories; using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers; using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.KeyManagement.Models.Requests; using Bit.Api.KeyManagement.Models.Requests;
@ -30,6 +31,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
private readonly LoginHelper _loginHelper; private readonly LoginHelper _loginHelper;
private readonly IUserRepository _userRepository; private readonly IUserRepository _userRepository;
private readonly IPasswordHasher<User> _passwordHasher; private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationRepository _organizationRepository;
private string _ownerEmail = null!; private string _ownerEmail = null!;
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory) public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
@ -43,6 +45,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
_emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>(); _emergencyAccessRepository = _factory.GetService<IEmergencyAccessRepository>();
_organizationUserRepository = _factory.GetService<IOrganizationUserRepository>(); _organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
_passwordHasher = _factory.GetService<IPasswordHasher<User>>(); _passwordHasher = _factory.GetService<IPasswordHasher<User>>();
_organizationRepository = _factory.GetService<IOrganizationRepository>();
} }
public async Task InitializeAsync() public async Task InitializeAsync()
@ -252,4 +255,96 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory); Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfMemory, userNewState.KdfMemory);
Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism); Assert.Equal(request.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism, userNewState.KdfParallelism);
} }
[Theory]
[BitAutoData("/accounts/key-management/set-key-connector-key")]
[BitAutoData("/accounts/set-key-connector-key")]
public async Task PostSetKeyConnectorKeyAsync_NotLoggedIn_Unauthorized(string uri,
SetKeyConnectorKeyRequestModel request)
{
var response = await _client.PostAsJsonAsync(uri, request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Theory]
[BitAutoData("/accounts/key-management/set-key-connector-key")]
[BitAutoData("/accounts/set-key-connector-key")]
public async Task PostSetKeyConnectorKeyAsync_Success(string uri, 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);
request.Key = _mockEncryptedString;
request.OrgIdentifier = organizationSsoIdentifier;
var response = await _client.PostAsJsonAsync(uri, request);
response.EnsureSuccessStatusCode();
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(user);
Assert.Equal(request.Key, user.Key);
Assert.True(user.UsesKeyConnector);
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
var ssoOrganizationUser =
await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
Assert.NotNull(ssoOrganizationUser);
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
Assert.Null(ssoOrganizationUser.Email);
}
[Theory]
[BitAutoData("/accounts/key-management/convert-to-key-connector")]
[BitAutoData("/accounts/convert-to-key-connector")]
public async Task PostConvertToKeyConnectorAsync_NotLoggedIn_Unauthorized(string uri)
{
var response = await _client.PostAsJsonAsync(uri, new { });
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Theory]
[BitAutoData("/accounts/key-management/convert-to-key-connector")]
[BitAutoData("/accounts/convert-to-key-connector")]
public async Task PostConvertToKeyConnectorAsync_Success(string uri)
{
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 response = await _client.PostAsJsonAsync(uri, new { });
response.EnsureSuccessStatusCode();
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(user);
Assert.Null(user.MasterPassword);
Assert.True(user.UsesKeyConnector);
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
}
} }

View File

@ -178,4 +178,133 @@ public class AccountsKeyManagementControllerTests
Assert.NotEmpty(ex.ModelState.Values); Assert.NotEmpty(ex.ModelState.Values);
} }
} }
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_UserNull_Throws(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data, User expectedUser)
{
expectedUser.PublicKey = null;
expectedUser.PrivateKey = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(IdentityResult.Failed(new IdentityError { Description = "set key connector key error" }));
var badRequestException =
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
Assert.Equal(1, badRequestException.ModelState.ErrorCount);
Assert.Equal("set key connector key error", badRequestException.ModelState.Root.Errors[0].ErrorMessage);
await sutProvider.GetDependency<IUserService>().Received(1)
.SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
{
Assert.Equal(expectedUser.Id, user.Id);
Assert.Equal(data.Key, user.Key);
Assert.Equal(data.Kdf, user.Kdf);
Assert.Equal(data.KdfIterations, user.KdfIterations);
Assert.Equal(data.KdfMemory, user.KdfMemory);
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data, User expectedUser)
{
expectedUser.PublicKey = null;
expectedUser.PrivateKey = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.SetKeyConnectorKeyAsync(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(IdentityResult.Success);
await sutProvider.Sut.PostSetKeyConnectorKeyAsync(data);
await sutProvider.GetDependency<IUserService>().Received(1)
.SetKeyConnectorKeyAsync(Arg.Do<User>(user =>
{
Assert.Equal(expectedUser.Id, user.Id);
Assert.Equal(data.Key, user.Key);
Assert.Equal(data.Kdf, user.Kdf);
Assert.Equal(data.KdfIterations, user.KdfIterations);
Assert.Equal(data.KdfMemory, user.KdfMemory);
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(
SutProvider<AccountsKeyManagementController> sutProvider)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());
await sutProvider.GetDependency<IUserService>().ReceivedWithAnyArgs(0)
.ConvertToKeyConnectorAsync(Arg.Any<User>());
}
[Theory]
[BitAutoData]
public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorFails_ThrowsBadRequestWithErrorResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.ConvertToKeyConnectorAsync(Arg.Any<User>())
.Returns(IdentityResult.Failed(new IdentityError { Description = "convert to key connector error" }));
var badRequestException =
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.PostConvertToKeyConnectorAsync());
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));
}
[Theory]
[BitAutoData]
public async Task PostConvertToKeyConnectorAsync_ConvertToKeyConnectorSucceeds_OkResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser)
{
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<IUserService>()
.ConvertToKeyConnectorAsync(Arg.Any<User>())
.Returns(IdentityResult.Success);
await sutProvider.Sut.PostConvertToKeyConnectorAsync();
await sutProvider.GetDependency<IUserService>().Received(1)
.ConvertToKeyConnectorAsync(Arg.Is(expectedUser));
}
} }