1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-13 09:08:17 -05:00

[PM-12485] Create OrganizationUpdateKeys command (#5600)

* Add OrganizationUpdateKeysCommand

* Add unit tests for OrganizationUpdateKeysCommand to validate permission checks and key updates

* Register OrganizationUpdateKeysCommand for dependency injection

* Refactor OrganizationsController to use IOrganizationUpdateKeysCommand for updating organization keys

* Remove outdated unit tests for UpdateOrganizationKeysAsync in OrganizationServiceTests

* Remove UpdateOrganizationKeysAsync method from IOrganizationService and OrganizationService implementations

* Add IOrganizationUpdateKeysCommand dependency mock to OrganizationsControllerTests
This commit is contained in:
Rui Tomé 2025-04-09 15:23:29 +01:00 committed by GitHub
parent 0a4f97b50e
commit f1a4829e5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 151 additions and 69 deletions

View File

@ -65,6 +65,7 @@ public class OrganizationsController : Controller
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IPricingClient _pricingClient;
private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -88,7 +89,8 @@ public class OrganizationsController : Controller
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
IOrganizationDeleteCommand organizationDeleteCommand,
IPolicyRequirementQuery policyRequirementQuery,
IPricingClient pricingClient)
IPricingClient pricingClient,
IOrganizationUpdateKeysCommand organizationUpdateKeysCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -112,6 +114,7 @@ public class OrganizationsController : Controller
_organizationDeleteCommand = organizationDeleteCommand;
_policyRequirementQuery = policyRequirementQuery;
_pricingClient = pricingClient;
_organizationUpdateKeysCommand = organizationUpdateKeysCommand;
}
[HttpGet("{id}")]
@ -490,7 +493,7 @@ public class OrganizationsController : Controller
}
[HttpPost("{id}/keys")]
public async Task<OrganizationKeysResponseModel> PostKeys(string id, [FromBody] OrganizationKeysRequestModel model)
public async Task<OrganizationKeysResponseModel> PostKeys(Guid id, [FromBody] OrganizationKeysRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -498,7 +501,7 @@ public class OrganizationsController : Controller
throw new UnauthorizedAccessException();
}
var org = await _organizationService.UpdateOrganizationKeysAsync(new Guid(id), model.PublicKey,
var org = await _organizationUpdateKeysCommand.UpdateOrganizationKeysAsync(id, model.PublicKey,
model.EncryptedPrivateKey);
return new OrganizationKeysResponseModel(org);
}

View File

@ -0,0 +1,13 @@
using Bit.Core.AdminConsole.Entities;
public interface IOrganizationUpdateKeysCommand
{
/// <summary>
/// Update the keys for an organization.
/// </summary>
/// <param name="orgId">The ID of the organization to update.</param>
/// <param name="publicKey">The public key for the organization.</param>
/// <param name="privateKey">The private key for the organization.</param>
/// <returns>The updated organization.</returns>
Task<Organization> UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey);
}

View File

@ -0,0 +1,47 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
public class OrganizationUpdateKeysCommand : IOrganizationUpdateKeysCommand
{
private readonly ICurrentContext _currentContext;
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationService _organizationService;
public const string OrganizationKeysAlreadyExistErrorMessage = "Organization Keys already exist.";
public OrganizationUpdateKeysCommand(
ICurrentContext currentContext,
IOrganizationRepository organizationRepository,
IOrganizationService organizationService)
{
_currentContext = currentContext;
_organizationRepository = organizationRepository;
_organizationService = organizationService;
}
public async Task<Organization> UpdateOrganizationKeysAsync(Guid organizationId, string publicKey, string privateKey)
{
if (!await _currentContext.ManageResetPassword(organizationId))
{
throw new UnauthorizedAccessException();
}
// If the keys already exist, error out
var organization = await _organizationRepository.GetByIdAsync(organizationId);
if (organization.PublicKey != null && organization.PrivateKey != null)
{
throw new BadRequestException(OrganizationKeysAlreadyExistErrorMessage);
}
// Update org with generated public/private key
organization.PublicKey = publicKey;
organization.PrivateKey = privateKey;
await _organizationService.UpdateAsync(organization);
return organization;
}
}

View File

@ -43,7 +43,6 @@ public interface IOrganizationService
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
bool overwriteExisting, EventSystemUser eventSystemUser);
Task DeleteSsoUserAsync(Guid userId, Guid? organizationId);
Task<Organization> UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey);
Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId);
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,

View File

@ -1418,28 +1418,6 @@ public class OrganizationService : IOrganizationService
}
}
public async Task<Organization> UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey)
{
if (!await _currentContext.ManageResetPassword(orgId))
{
throw new UnauthorizedAccessException();
}
// If the keys already exist, error out
var org = await _organizationRepository.GetByIdAsync(orgId);
if (org.PublicKey != null && org.PrivateKey != null)
{
throw new BadRequestException("Organization Keys already exist");
}
// Update org with generated public/private key
org.PublicKey = publicKey;
org.PrivateKey = privateKey;
await UpdateAsync(org);
return org;
}
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid> existingUsers = null)
{

View File

@ -60,6 +60,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationDomainCommandsQueries();
services.AddOrganizationSignUpCommands();
services.AddOrganizationDeleteCommands();
services.AddOrganizationUpdateCommands();
services.AddOrganizationEnableCommands();
services.AddOrganizationDisableCommands();
services.AddOrganizationAuthCommands();
@ -77,6 +78,11 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IOrganizationInitiateDeleteCommand, OrganizationInitiateDeleteCommand>();
}
private static void AddOrganizationUpdateCommands(this IServiceCollection services)
{
services.AddScoped<IOrganizationUpdateKeysCommand, OrganizationUpdateKeysCommand>();
}
private static void AddOrganizationEnableCommands(this IServiceCollection services) =>
services.AddScoped<IOrganizationEnableCommand, OrganizationEnableCommand>();

View File

@ -60,6 +60,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
private readonly IPolicyRequirementQuery _policyRequirementQuery;
private readonly IPricingClient _pricingClient;
private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand;
private readonly OrganizationsController _sut;
public OrganizationsControllerTests()
@ -86,6 +87,7 @@ public class OrganizationsControllerTests : IDisposable
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
_pricingClient = Substitute.For<IPricingClient>();
_organizationUpdateKeysCommand = Substitute.For<IOrganizationUpdateKeysCommand>();
_sut = new OrganizationsController(
_organizationRepository,
@ -109,7 +111,8 @@ public class OrganizationsControllerTests : IDisposable
_cloudOrganizationSignUpCommand,
_organizationDeleteCommand,
_policyRequirementQuery,
_pricingClient);
_pricingClient,
_organizationUpdateKeysCommand);
}
public void Dispose()

View File

@ -0,0 +1,75 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations;
[SutProviderCustomize]
public class OrganizationUpdateKeysCommandTests
{
[Theory, BitAutoData]
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPasswordPermission_ThrowsUnauthorizedException(
Guid orgId, string publicKey, string privateKey, SutProvider<OrganizationUpdateKeysCommand> sutProvider)
{
sutProvider.GetDependency<ICurrentContext>()
.ManageResetPassword(orgId)
.Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => sutProvider.Sut.UpdateOrganizationKeysAsync(orgId, publicKey, privateKey));
}
[Theory, BitAutoData]
public async Task UpdateOrganizationKeysAsync_WhenKeysAlreadyExist_ThrowsBadRequestException(
Organization organization, string publicKey, string privateKey,
SutProvider<OrganizationUpdateKeysCommand> sutProvider)
{
organization.PublicKey = "existingPublicKey";
organization.PrivateKey = "existingPrivateKey";
sutProvider.GetDependency<ICurrentContext>()
.ManageResetPassword(organization.Id)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateOrganizationKeysAsync(organization.Id, publicKey, privateKey));
Assert.Equal(OrganizationUpdateKeysCommand.OrganizationKeysAlreadyExistErrorMessage, exception.Message);
}
[Theory, BitAutoData]
public async Task UpdateOrganizationKeysAsync_WhenKeysDoNotExist_UpdatesOrganization(
Organization organization, string publicKey, string privateKey,
SutProvider<OrganizationUpdateKeysCommand> sutProvider)
{
organization.PublicKey = null;
organization.PrivateKey = null;
sutProvider.GetDependency<ICurrentContext>()
.ManageResetPassword(organization.Id)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var result = await sutProvider.Sut.UpdateOrganizationKeysAsync(organization.Id, publicKey, privateKey);
Assert.Equal(publicKey, result.PublicKey);
Assert.Equal(privateKey, result.PrivateKey);
await sutProvider.GetDependency<IOrganizationService>()
.Received(1)
.UpdateAsync(organization);
}
}

View File

@ -814,48 +814,6 @@ public class OrganizationServiceTests
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
}
[Theory, BitAutoData]
public async Task UpdateOrganizationKeysAsync_WithoutManageResetPassword_Throws(Guid orgId, string publicKey,
string privateKey, SutProvider<OrganizationService> sutProvider)
{
var currentContext = Substitute.For<ICurrentContext>();
currentContext.ManageResetPassword(orgId).Returns(false);
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => sutProvider.Sut.UpdateOrganizationKeysAsync(orgId, publicKey, privateKey));
}
[Theory, BitAutoData]
public async Task UpdateOrganizationKeysAsync_KeysAlreadySet_Throws(Organization org, string publicKey,
string privateKey, SutProvider<OrganizationService> sutProvider)
{
var currentContext = sutProvider.GetDependency<ICurrentContext>();
currentContext.ManageResetPassword(org.Id).Returns(true);
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(org.Id).Returns(org);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.UpdateOrganizationKeysAsync(org.Id, publicKey, privateKey));
Assert.Contains("Organization Keys already exist", exception.Message);
}
[Theory, BitAutoData]
public async Task UpdateOrganizationKeysAsync_KeysAlreadySet_Success(Organization org, string publicKey,
string privateKey, SutProvider<OrganizationService> sutProvider)
{
org.PublicKey = null;
org.PrivateKey = null;
var currentContext = sutProvider.GetDependency<ICurrentContext>();
currentContext.ManageResetPassword(org.Id).Returns(true);
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
organizationRepository.GetByIdAsync(org.Id).Returns(org);
await sutProvider.Sut.UpdateOrganizationKeysAsync(org.Id, publicKey, privateKey);
}
[Theory]
[PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually)]
[BitAutoData("Cannot set max seat autoscaling below seat count", 1, 0, 2, 2)]