diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index be96bcb286..bb7c3a63a2 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -33,8 +33,9 @@ public class OrganizationsController : Controller private readonly ICurrentContext _currentContext; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigService _ssoConfigService; - private readonly IGetOrganizationApiKeyCommand _getOrganizationApiKeyCommand; + private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery; private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand; + private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly GlobalSettings _globalSettings; @@ -48,8 +49,9 @@ public class OrganizationsController : Controller ICurrentContext currentContext, ISsoConfigRepository ssoConfigRepository, ISsoConfigService ssoConfigService, - IGetOrganizationApiKeyCommand getOrganizationApiKeyCommand, + IGetOrganizationApiKeyQuery getOrganizationApiKeyQuery, IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand, + ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand, IOrganizationApiKeyRepository organizationApiKeyRepository, GlobalSettings globalSettings) { @@ -62,8 +64,9 @@ public class OrganizationsController : Controller _currentContext = currentContext; _ssoConfigRepository = ssoConfigRepository; _ssoConfigService = ssoConfigService; - _getOrganizationApiKeyCommand = getOrganizationApiKeyCommand; + _getOrganizationApiKeyQuery = getOrganizationApiKeyQuery; _rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand; + _createOrganizationApiKeyCommand = createOrganizationApiKeyCommand; _organizationApiKeyRepository = organizationApiKeyRepository; _globalSettings = globalSettings; } @@ -514,8 +517,9 @@ public class OrganizationsController : Controller } } - var organizationApiKey = await _getOrganizationApiKeyCommand - .GetOrganizationApiKeyAsync(organization.Id, model.Type); + var organizationApiKey = await _getOrganizationApiKeyQuery + .GetOrganizationApiKeyAsync(organization.Id, model.Type) ?? + await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type); var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -565,8 +569,9 @@ public class OrganizationsController : Controller throw new NotFoundException(); } - var organizationApiKey = await _getOrganizationApiKeyCommand - .GetOrganizationApiKeyAsync(organization.Id, model.Type); + var organizationApiKey = await _getOrganizationApiKeyQuery + .GetOrganizationApiKeyAsync(organization.Id, model.Type) ?? + await _createOrganizationApiKeyCommand.CreateAsync(organization.Id, model.Type); var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommand.cs new file mode 100644 index 0000000000..d76725c70d --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommand.cs @@ -0,0 +1,32 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys; + +public class CreateOrganizationApiKeyCommand : ICreateOrganizationApiKeyCommand +{ + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + + public CreateOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository) + { + _organizationApiKeyRepository = organizationApiKeyRepository; + } + + public async Task CreateAsync(Guid organizationId, + OrganizationApiKeyType organizationApiKeyType) + { + var apiKey = new OrganizationApiKey + { + OrganizationId = organizationId, + Type = organizationApiKeyType, + ApiKey = CoreHelpers.SecureRandomString(30), + RevisionDate = DateTime.UtcNow, + }; + + await _organizationApiKeyRepository.CreateAsync(apiKey); + return apiKey; + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs similarity index 60% rename from src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommand.cs rename to src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs index 1a01562417..da4d7d3cd2 100644 --- a/src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQuery.cs @@ -2,15 +2,14 @@ using Bit.Core.Enums; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.Repositories; -using Bit.Core.Utilities; namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys; -public class GetOrganizationApiKeyCommand : IGetOrganizationApiKeyCommand +public class GetOrganizationApiKeyQuery : IGetOrganizationApiKeyQuery { private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; - public GetOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository) + public GetOrganizationApiKeyQuery(IOrganizationApiKeyRepository organizationApiKeyRepository) { _organizationApiKeyRepository = organizationApiKeyRepository; } @@ -25,20 +24,6 @@ public class GetOrganizationApiKeyCommand : IGetOrganizationApiKeyCommand var apiKeys = await _organizationApiKeyRepository .GetManyByOrganizationIdTypeAsync(organizationId, organizationApiKeyType); - if (apiKeys == null || !apiKeys.Any()) - { - var apiKey = new OrganizationApiKey - { - OrganizationId = organizationId, - Type = organizationApiKeyType, - ApiKey = CoreHelpers.SecureRandomString(30), - RevisionDate = DateTime.UtcNow, - }; - - await _organizationApiKeyRepository.CreateAsync(apiKey); - return apiKey; - } - // NOTE: Currently we only allow one type of api key per organization return apiKeys.Single(); } diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/ICreateOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/ICreateOrganizationApiKeyCommand.cs new file mode 100644 index 0000000000..a2ce7326de --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/ICreateOrganizationApiKeyCommand.cs @@ -0,0 +1,9 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; + +public interface ICreateOrganizationApiKeyCommand +{ + Task CreateAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType); +} diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyQuery.cs similarity index 84% rename from src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyCommand.cs rename to src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyQuery.cs index 5fcfdedd99..699b13adb9 100644 --- a/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyQuery.cs @@ -3,7 +3,7 @@ using Bit.Core.Enums; namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; -public interface IGetOrganizationApiKeyCommand +public interface IGetOrganizationApiKeyQuery { Task GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType); } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 4a6fbf3f7b..f83f17e8c6 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -24,7 +24,7 @@ public static class OrganizationServiceCollectionExtensions services.AddTokenizers(); services.AddOrganizationConnectionCommands(); services.AddOrganizationSponsorshipCommands(globalSettings); - services.AddOrganizationApiKeyCommands(); + services.AddOrganizationApiKeyCommandsQueries(); } private static void AddOrganizationConnectionCommands(this IServiceCollection services) @@ -59,10 +59,11 @@ public static class OrganizationServiceCollectionExtensions } } - private static void AddOrganizationApiKeyCommands(this IServiceCollection services) + private static void AddOrganizationApiKeyCommandsQueries(this IServiceCollection services) { - services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); } private static void AddTokenizers(this IServiceCollection services) diff --git a/test/Api.Test/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Controllers/OrganizationsControllerTests.cs index dddd9c5f05..fa1855c70d 100644 --- a/test/Api.Test/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationsControllerTests.cs @@ -26,9 +26,10 @@ public class OrganizationsControllerTests : IDisposable private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigService _ssoConfigService; private readonly IUserService _userService; - private readonly IGetOrganizationApiKeyCommand _getOrganizationApiKeyCommand; + private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery; private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand; private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand; private readonly OrganizationsController _sut; @@ -43,15 +44,16 @@ public class OrganizationsControllerTests : IDisposable _policyRepository = Substitute.For(); _ssoConfigRepository = Substitute.For(); _ssoConfigService = Substitute.For(); - _getOrganizationApiKeyCommand = Substitute.For(); + _getOrganizationApiKeyQuery = Substitute.For(); _rotateOrganizationApiKeyCommand = Substitute.For(); _organizationApiKeyRepository = Substitute.For(); _userService = Substitute.For(); + _createOrganizationApiKeyCommand = Substitute.For(); _sut = new OrganizationsController(_organizationRepository, _organizationUserRepository, _policyRepository, _organizationService, _userService, _paymentService, _currentContext, - _ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyCommand, _rotateOrganizationApiKeyCommand, - _organizationApiKeyRepository, _globalSettings); + _ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand, + _createOrganizationApiKeyCommand, _organizationApiKeyRepository, _globalSettings); } public void Dispose() diff --git a/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommandTest.cs b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommandTest.cs new file mode 100644 index 0000000000..6b7fb3151a --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/CreateOrganizationApiKeyCommandTest.cs @@ -0,0 +1,27 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReceivedExtensions; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationApiKeys; + +[SutProviderCustomize] +public class CreateOrganizationApiKeyCommandTest +{ + [Theory] + [BitAutoData] + public async Task CreateAsync_CreatesOrganizationApiKey(SutProvider sutProvider, + Guid organizationId, OrganizationApiKeyType keyType) + { + await sutProvider.Sut.CreateAsync(organizationId, keyType); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(o => o.OrganizationId == organizationId + && o.Type == keyType)); + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQueryTests.cs similarity index 73% rename from test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommandTests.cs rename to test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQueryTests.cs index de568f2656..d374bf8068 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyQueryTests.cs @@ -10,11 +10,11 @@ using Xunit; namespace Bit.Core.Test.OrganizationFeatures.OrganizationApiKeys; [SutProviderCustomize] -public class GetOrganizationApiKeyCommandTests +public class GetOrganizationApiKeyQueryTests { [Theory] [BitAutoData] - public async Task GetOrganizationApiKey_HasOne_Returns(SutProvider sutProvider, + public async Task GetOrganizationApiKey_HasOne_Returns(SutProvider sutProvider, Guid id, Guid organizationId, OrganizationApiKeyType keyType) { sutProvider.GetDependency() @@ -38,7 +38,7 @@ public class GetOrganizationApiKeyCommandTests [Theory] [BitAutoData] - public async Task GetOrganizationApiKey_HasTwo_Throws(SutProvider sutProvider, + public async Task GetOrganizationApiKey_HasTwo_Throws(SutProvider sutProvider, Guid organizationId, OrganizationApiKeyType keyType) { sutProvider.GetDependency() @@ -69,26 +69,7 @@ public class GetOrganizationApiKeyCommandTests [Theory] [BitAutoData] - public async Task GetOrganizationApiKey_HasNone_CreatesAndReturns(SutProvider sutProvider, - Guid organizationId, OrganizationApiKeyType keyType) - { - sutProvider.GetDependency() - .GetManyByOrganizationIdTypeAsync(organizationId, keyType) - .Returns(Enumerable.Empty()); - - var apiKey = await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType); - - Assert.NotNull(apiKey); - Assert.Equal(organizationId, apiKey.OrganizationId); - Assert.Equal(keyType, apiKey.Type); - await sutProvider.GetDependency() - .Received(1) - .CreateAsync(Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task GetOrganizationApiKey_BadType_Throws(SutProvider sutProvider, + public async Task GetOrganizationApiKey_BadType_Throws(SutProvider sutProvider, Guid organizationId, OrganizationApiKeyType keyType) { keyType = (OrganizationApiKeyType)byte.MaxValue;