mirror of
https://github.com/bitwarden/server.git
synced 2025-04-16 10:38: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:
parent
0a4f97b50e
commit
f1a4829e5e
@ -65,6 +65,7 @@ public class OrganizationsController : Controller
|
|||||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand;
|
||||||
|
|
||||||
public OrganizationsController(
|
public OrganizationsController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -88,7 +89,8 @@ public class OrganizationsController : Controller
|
|||||||
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
|
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
|
||||||
IOrganizationDeleteCommand organizationDeleteCommand,
|
IOrganizationDeleteCommand organizationDeleteCommand,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IPricingClient pricingClient)
|
IPricingClient pricingClient,
|
||||||
|
IOrganizationUpdateKeysCommand organizationUpdateKeysCommand)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -112,6 +114,7 @@ public class OrganizationsController : Controller
|
|||||||
_organizationDeleteCommand = organizationDeleteCommand;
|
_organizationDeleteCommand = organizationDeleteCommand;
|
||||||
_policyRequirementQuery = policyRequirementQuery;
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
|
_organizationUpdateKeysCommand = organizationUpdateKeysCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -490,7 +493,7 @@ public class OrganizationsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/keys")]
|
[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);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -498,7 +501,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new UnauthorizedAccessException();
|
throw new UnauthorizedAccessException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var org = await _organizationService.UpdateOrganizationKeysAsync(new Guid(id), model.PublicKey,
|
var org = await _organizationUpdateKeysCommand.UpdateOrganizationKeysAsync(id, model.PublicKey,
|
||||||
model.EncryptedPrivateKey);
|
model.EncryptedPrivateKey);
|
||||||
return new OrganizationKeysResponseModel(org);
|
return new OrganizationKeysResponseModel(org);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -43,7 +43,6 @@ public interface IOrganizationService
|
|||||||
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
||||||
bool overwriteExisting, EventSystemUser eventSystemUser);
|
bool overwriteExisting, EventSystemUser eventSystemUser);
|
||||||
Task DeleteSsoUserAsync(Guid userId, Guid? organizationId);
|
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, Guid? revokingUserId);
|
||||||
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||||
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
||||||
|
@ -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,
|
private async Task UpdateUsersAsync(Group group, HashSet<string> groupUsers,
|
||||||
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid> existingUsers = null)
|
Dictionary<string, Guid> existingUsersIdDict, HashSet<Guid> existingUsers = null)
|
||||||
{
|
{
|
||||||
|
@ -60,6 +60,7 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddOrganizationDomainCommandsQueries();
|
services.AddOrganizationDomainCommandsQueries();
|
||||||
services.AddOrganizationSignUpCommands();
|
services.AddOrganizationSignUpCommands();
|
||||||
services.AddOrganizationDeleteCommands();
|
services.AddOrganizationDeleteCommands();
|
||||||
|
services.AddOrganizationUpdateCommands();
|
||||||
services.AddOrganizationEnableCommands();
|
services.AddOrganizationEnableCommands();
|
||||||
services.AddOrganizationDisableCommands();
|
services.AddOrganizationDisableCommands();
|
||||||
services.AddOrganizationAuthCommands();
|
services.AddOrganizationAuthCommands();
|
||||||
@ -77,6 +78,11 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IOrganizationInitiateDeleteCommand, OrganizationInitiateDeleteCommand>();
|
services.AddScoped<IOrganizationInitiateDeleteCommand, OrganizationInitiateDeleteCommand>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddOrganizationUpdateCommands(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<IOrganizationUpdateKeysCommand, OrganizationUpdateKeysCommand>();
|
||||||
|
}
|
||||||
|
|
||||||
private static void AddOrganizationEnableCommands(this IServiceCollection services) =>
|
private static void AddOrganizationEnableCommands(this IServiceCollection services) =>
|
||||||
services.AddScoped<IOrganizationEnableCommand, OrganizationEnableCommand>();
|
services.AddScoped<IOrganizationEnableCommand, OrganizationEnableCommand>();
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
|
private readonly IOrganizationUpdateKeysCommand _organizationUpdateKeysCommand;
|
||||||
private readonly OrganizationsController _sut;
|
private readonly OrganizationsController _sut;
|
||||||
|
|
||||||
public OrganizationsControllerTests()
|
public OrganizationsControllerTests()
|
||||||
@ -86,6 +87,7 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
|
_organizationDeleteCommand = Substitute.For<IOrganizationDeleteCommand>();
|
||||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||||
_pricingClient = Substitute.For<IPricingClient>();
|
_pricingClient = Substitute.For<IPricingClient>();
|
||||||
|
_organizationUpdateKeysCommand = Substitute.For<IOrganizationUpdateKeysCommand>();
|
||||||
|
|
||||||
_sut = new OrganizationsController(
|
_sut = new OrganizationsController(
|
||||||
_organizationRepository,
|
_organizationRepository,
|
||||||
@ -109,7 +111,8 @@ public class OrganizationsControllerTests : IDisposable
|
|||||||
_cloudOrganizationSignUpCommand,
|
_cloudOrganizationSignUpCommand,
|
||||||
_organizationDeleteCommand,
|
_organizationDeleteCommand,
|
||||||
_policyRequirementQuery,
|
_policyRequirementQuery,
|
||||||
_pricingClient);
|
_pricingClient,
|
||||||
|
_organizationUpdateKeysCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -814,48 +814,6 @@ public class OrganizationServiceTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
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]
|
[Theory]
|
||||||
[PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually)]
|
[PaidOrganizationCustomize(CheckedPlanType = PlanType.EnterpriseAnnually)]
|
||||||
[BitAutoData("Cannot set max seat autoscaling below seat count", 1, 0, 2, 2)]
|
[BitAutoData("Cannot set max seat autoscaling below seat count", 1, 0, 2, 2)]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user